Adição da função de configuração de email e whatts
This commit is contained in:
26
app/services/notificacao/cliente_twilio.rb
Normal file
26
app/services/notificacao/cliente_twilio.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# app/services/notificacao/cliente_twilio.rb
|
||||
#
|
||||
# Monta o Twilio::REST::Client com timeout. Sem timeout, uma saída de internet
|
||||
# bloqueada no servidor pendura o worker Puma até o proxy cortar a requisição —
|
||||
# e o botão "enviar teste" da tela ficaria girando sem resposta.
|
||||
#
|
||||
# O construtor com http_client é posicional na twilio-ruby 7.x
|
||||
# (username, password, account_sid, region, http_client). Se a assinatura mudar
|
||||
# numa atualização da gem, cai no construtor simples em vez de quebrar o envio.
|
||||
module Notificacao
|
||||
module ClienteTwilio
|
||||
TIMEOUT = 15
|
||||
|
||||
def self.montar(sid, token)
|
||||
require 'twilio-ruby'
|
||||
|
||||
begin
|
||||
http = Twilio::HTTP::Client.new(timeout: TIMEOUT)
|
||||
Twilio::REST::Client.new(sid, token, nil, nil, http)
|
||||
rescue ArgumentError, NameError => e
|
||||
Rails.logger.warn("[ClienteTwilio] sem timeout customizado (#{e.class}: #{e.message})")
|
||||
Twilio::REST::Client.new(sid, token)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
27
app/services/notificacao/resultado.rb
Normal file
27
app/services/notificacao/resultado.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
# app/services/notificacao/resultado.rb
|
||||
#
|
||||
# Retorno dos testes de envio. Existe para o controller não precisar saber nada
|
||||
# sobre SMTP nem sobre a API do Twilio — só perguntar `ok?` e jogar no flash.
|
||||
module Notificacao
|
||||
Resultado = Struct.new(:ok, :mensagem, :detalhe, keyword_init: true) do
|
||||
def self.sucesso(mensagem)
|
||||
new(ok: true, mensagem: mensagem)
|
||||
end
|
||||
|
||||
def self.falha(mensagem, detalhe = nil)
|
||||
new(ok: false, mensagem: mensagem, detalhe: detalhe)
|
||||
end
|
||||
|
||||
def ok?
|
||||
!!ok
|
||||
end
|
||||
|
||||
# O detalhe técnico (mensagem crua do servidor/Twilio) só aparece no erro —
|
||||
# é o que permite ao admin resolver sozinho sem abrir o log do container.
|
||||
def para_flash
|
||||
texto = mensagem.to_s
|
||||
texto += " [detalhe técnico: #{detalhe.to_s.truncate(180)}]" if !ok? && detalhe.present?
|
||||
{ ok? ? :notice : :alert => texto }
|
||||
end
|
||||
end
|
||||
end
|
||||
69
app/services/notificacao/teste_email.rb
Normal file
69
app/services/notificacao/teste_email.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
# app/services/notificacao/teste_email.rb
|
||||
#
|
||||
# Dispara um e-mail de teste com as credenciais que ACABARAM de ser salvas e
|
||||
# devolve o erro REAL traduzido para português. É o oposto do NotificacaoService,
|
||||
# que engole as falhas de propósito para não derrubar o fechamento — aqui o
|
||||
# objetivo é justamente mostrar o problema.
|
||||
module Notificacao
|
||||
class TesteEmail
|
||||
def initialize(config = ConfiguracaoNotificacao.instancia, destinatario: nil, usuario: nil)
|
||||
@config = config
|
||||
@destinatario = destinatario.presence || config.email_admin.presence
|
||||
@usuario = usuario
|
||||
end
|
||||
|
||||
def executar
|
||||
if @destinatario.blank?
|
||||
return Resultado.falha('Preencha o "E-mail do administrador" antes de enviar o teste.')
|
||||
end
|
||||
|
||||
unless @config.smtp_pronto?
|
||||
return Resultado.falha('Marque "Ativar envio de e-mail" e preencha servidor, porta, ' \
|
||||
'usuário e senha antes de testar.')
|
||||
end
|
||||
|
||||
entrega = TesteMailer.teste(@destinatario, @config, @usuario)
|
||||
# development.rb define raise_delivery_errors = false e o mail() copia isso
|
||||
# para a mensagem — sem estas duas linhas o teste "passaria" em silêncio.
|
||||
entrega.message.raise_delivery_errors = true
|
||||
entrega.message.perform_deliveries = true
|
||||
# NUNCA deliver_later: o adapter é :async (thread in-process) e a exceção
|
||||
# nunca voltaria para o flash.
|
||||
entrega.deliver_now
|
||||
|
||||
Resultado.sucesso("E-mail de teste enviado para #{@destinatario}. " \
|
||||
'Confira a caixa de entrada e também o spam.')
|
||||
rescue Net::SMTPAuthenticationError => e
|
||||
Resultado.falha('Usuário ou senha do SMTP recusados pelo servidor. No Gmail/Google ' \
|
||||
'Workspace é preciso usar uma "Senha de app" de 16 caracteres, ' \
|
||||
'não a senha da conta.', e.message)
|
||||
rescue Net::SMTPSyntaxError, Net::SMTPFatalError => e
|
||||
Resultado.falha("O servidor recusou a mensagem. Normalmente o remetente " \
|
||||
"(#{@config.remetente_email.presence || @config.smtp_username}) precisa ser " \
|
||||
'o mesmo usuário que autenticou.', e.message)
|
||||
rescue Net::SMTPServerBusy => e
|
||||
Resultado.falha('Servidor SMTP ocupado ou aplicando limite de envio. ' \
|
||||
'Tente de novo em alguns minutos.', e.message)
|
||||
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ETIMEDOUT => e
|
||||
Resultado.falha("Tempo esgotado ao conectar em #{@config.smtp_address}:#{@config.smtp_port}. " \
|
||||
'A porta pode estar bloqueada pelo provedor ou pelo firewall — ' \
|
||||
'tente 587 (STARTTLS) ou 465 (SSL).', e.message)
|
||||
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH => e
|
||||
Resultado.falha("Conexão recusada por #{@config.smtp_address}:#{@config.smtp_port}. " \
|
||||
'Servidor ou porta incorretos.', e.message)
|
||||
rescue SocketError => e
|
||||
Resultado.falha("Não foi possível resolver o endereço \"#{@config.smtp_address}\". " \
|
||||
'Confira a grafia do servidor.', e.message)
|
||||
rescue OpenSSL::SSL::SSLError => e
|
||||
Resultado.falha('Erro de TLS/SSL. A porta 465 exige SSL direto e a 587 exige STARTTLS — ' \
|
||||
'confira se a porta escolhida bate com o servidor.', e.message)
|
||||
rescue Net::SMTPError, EOFError, IOError => e
|
||||
# Net::SMTPError é um módulo incluído nas classes acima; por isso vem
|
||||
# depois delas, como rede de segurança.
|
||||
Resultado.falha("O servidor SMTP interrompeu a conexão: #{e.message}", e.message)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("[TesteEmail] #{e.class}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}")
|
||||
Resultado.falha("Falha inesperada ao enviar (#{e.class}).", e.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
74
app/services/notificacao/teste_whatsapp.rb
Normal file
74
app/services/notificacao/teste_whatsapp.rb
Normal file
@@ -0,0 +1,74 @@
|
||||
# app/services/notificacao/teste_whatsapp.rb
|
||||
#
|
||||
# Dispara um WhatsApp de teste pelo Twilio para o número administrativo e
|
||||
# traduz o código de erro da API para uma instrução acionável em português —
|
||||
# os erros de Sandbox (opt-in) são de longe os mais comuns e o código cru
|
||||
# ("63003") não diz nada para quem está configurando.
|
||||
module Notificacao
|
||||
class TesteWhatsapp
|
||||
MENSAGENS = {
|
||||
20003 => 'Account SID ou Auth Token inválidos: o Twilio recusou a autenticação.',
|
||||
20404 => 'Account SID não encontrado no Twilio. Confira se copiou o SID certo (começa com "AC").',
|
||||
21211 => 'Número de destino inválido. Informe com DDD, ex.: 11 92005-1157.',
|
||||
21606 => 'O número remetente não é um número Twilio habilitado para envio.',
|
||||
21608 => 'Conta trial do Twilio: só é possível enviar para números verificados no console da Twilio.',
|
||||
21610 => 'Este número pediu descadastro (enviou STOP). Ele precisa enviar START para voltar a receber.',
|
||||
63003 => 'O Twilio não encontrou o destinatário nesse canal. No Sandbox do WhatsApp o número ' \
|
||||
'precisa enviar "join <sua-palavra>" para o número do sandbox ANTES de conseguir receber.',
|
||||
63007 => 'O número remetente não está habilitado como canal de WhatsApp. Use exatamente o ' \
|
||||
'número do Sandbox (ou do Sender aprovado) do Twilio.',
|
||||
63015 => 'No Sandbox só dá para enviar a números que fizeram o opt-in ("join <sua-palavra>").',
|
||||
63016 => 'Passou da janela de 24h desde a última mensagem do destinatário — nesse caso o Twilio ' \
|
||||
'só aceita template aprovado. Peça para ele mandar qualquer mensagem e tente de novo.',
|
||||
63018 => 'Limite de mensagens do Twilio atingido. Aguarde e tente novamente.',
|
||||
63038 => 'Limite diário de mensagens da conta Twilio atingido.'
|
||||
}.freeze
|
||||
|
||||
def initialize(config = ConfiguracaoNotificacao.instancia, destino: nil)
|
||||
@config = config
|
||||
@destino = ConfiguracaoNotificacao.canal(destino.presence || config.whatsapp_admin)
|
||||
end
|
||||
|
||||
def executar
|
||||
if @destino.blank?
|
||||
return Resultado.falha('Preencha o "WhatsApp do administrador" com DDD ' \
|
||||
'(ex.: 11 92005-1157) antes de testar.')
|
||||
end
|
||||
|
||||
credenciais = @config.credenciais_whatsapp
|
||||
if credenciais.blank?
|
||||
return Resultado.falha('Marque "Ativar WhatsApp" e preencha Account SID, Auth Token e o ' \
|
||||
'número remetente do Twilio.')
|
||||
end
|
||||
if credenciais[:from].blank?
|
||||
return Resultado.falha('O número remetente do Twilio está vazio ou em formato inválido.')
|
||||
end
|
||||
|
||||
msg = ClienteTwilio
|
||||
.montar(credenciais[:sid], credenciais[:token])
|
||||
.messages.create(from: credenciais[:from], to: @destino, body: corpo)
|
||||
|
||||
Resultado.sucesso("WhatsApp de teste aceito pelo Twilio para " \
|
||||
"#{@destino.sub('whatsapp:', '')} (SID #{msg.sid}, status \"#{msg.status}\"). " \
|
||||
'Atenção: "queued"/"accepted" significa que a Twilio recebeu — ' \
|
||||
'a entrega final ainda pode falhar depois.')
|
||||
rescue Twilio::REST::RestError => e
|
||||
Resultado.falha(MENSAGENS[e.code] || "O Twilio recusou o envio (código #{e.code}).",
|
||||
"[#{e.code}] #{e.message}")
|
||||
rescue Twilio::REST::TwilioError => e
|
||||
Resultado.falha('Não foi possível falar com a API do Twilio (rede ou timeout). ' \
|
||||
'Verifique a saída de internet do servidor.', e.message)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("[TesteWhatsapp] #{e.class}: #{e.message}")
|
||||
Resultado.falha("Falha inesperada ao enviar (#{e.class}).", e.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def corpo
|
||||
"✅ Teste do Reem Notas\n" \
|
||||
"Se você recebeu esta mensagem, a integração com o Twilio está funcionando.\n" \
|
||||
"Enviado em #{Time.current.strftime('%d/%m/%Y às %H:%M')}."
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,27 @@
|
||||
# app/services/notificacao_service.rb
|
||||
#
|
||||
# Envia notificações aos motoristas quando uma consolidação é finalizada.
|
||||
# Canais controlados pelas configurações: notificacao_whatsapp / notificacao_email.
|
||||
# Falhas de envio NUNCA quebram o fluxo de finalização (apenas logam).
|
||||
# Envia notificações aos motoristas quando uma consolidação é finalizada ou
|
||||
# quando o pagamento é efetuado.
|
||||
#
|
||||
# Canais e credenciais vêm de ConfiguracaoNotificacao (tela /admin/
|
||||
# configuracao_notificacao), com fallback para o .env.
|
||||
#
|
||||
# Falhas de envio NUNCA quebram o fluxo de finalização — apenas logam. Isso é
|
||||
# intencional: um SMTP fora do ar não pode impedir o fechamento de um pagamento.
|
||||
# Para DIAGNOSTICAR o envio, use os botões de teste da tela, que usam
|
||||
# Notificacao::TesteEmail / Notificacao::TesteWhatsapp e mostram o erro real.
|
||||
class NotificacaoService
|
||||
EVENTOS = {
|
||||
fechado: { mailer: :pagamento_fechado,
|
||||
emoji: '🚚',
|
||||
titulo: 'Seu pagamento de entregas foi fechado.',
|
||||
cta: 'Acesse seu painel para baixar o extrato:' },
|
||||
pago: { mailer: :pagamento_efetuado,
|
||||
emoji: '✅',
|
||||
titulo: 'Seu pagamento foi efetuado.',
|
||||
cta: 'Acesse seu painel para conferir:' }
|
||||
}.freeze
|
||||
|
||||
def self.notificar_finalizacao(consolidacao)
|
||||
new(consolidacao).notificar_todos
|
||||
end
|
||||
@@ -16,106 +33,73 @@ class NotificacaoService
|
||||
|
||||
def initialize(consolidacao)
|
||||
@consolidacao = consolidacao
|
||||
@config = ConfiguracaoNotificacao.instancia
|
||||
end
|
||||
|
||||
def notificar_todos
|
||||
@consolidacao.consolidacao_motoristas.ativos.each do |cm|
|
||||
user = User.motorista.ativos.find_by('LOWER(nome) = ?', cm.motorista_nome.downcase)
|
||||
next unless user
|
||||
|
||||
enviar_whatsapp(user, cm) if whatsapp_ativo?
|
||||
enviar_email(user, cm) if email_ativo? && user.email.present?
|
||||
end
|
||||
@consolidacao.consolidacao_motoristas.ativos.each { |cm| notificar(cm, :fechado) }
|
||||
end
|
||||
|
||||
def enviar_aviso_pagamento(cm)
|
||||
user = User.motorista.ativos.find_by('LOWER(nome) = ?', cm.motorista_nome.downcase)
|
||||
return unless user
|
||||
|
||||
enviar_whatsapp_pagamento(user, cm) if whatsapp_ativo?
|
||||
enviar_email_pagamento(user, cm) if email_ativo? && user.email.present?
|
||||
def enviar_aviso_pagamento(consolidacao_motorista)
|
||||
notificar(consolidacao_motorista, :pago)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def whatsapp_ativo?
|
||||
Configuracao.valor('notificacao_whatsapp') == 'true' &&
|
||||
ENV['TWILIO_ACCOUNT_SID'].present?
|
||||
def notificar(cm, evento)
|
||||
user = motorista_de(cm)
|
||||
return unless user
|
||||
|
||||
enviar_whatsapp(user, cm, evento) if @config.whatsapp_habilitado?
|
||||
enviar_email(user, cm, evento) if @config.email_habilitado? && user.email.present?
|
||||
end
|
||||
|
||||
def email_ativo?
|
||||
Configuracao.valor('notificacao_email') == 'true'
|
||||
def motorista_de(cm)
|
||||
User.motorista.ativos.find_by('LOWER(nome) = ?', cm.motorista_nome.downcase)
|
||||
end
|
||||
|
||||
def enviar_whatsapp(user, cm, evento)
|
||||
# O telefone no cadastro pode estar como "(11) 92005-1157"; mandar esse
|
||||
# texto cru para o Twilio devolve erro 21211 (destino inválido), que o
|
||||
# rescue abaixo esconderia. Normalizar para E.164 é o que faz o envio
|
||||
# realmente funcionar.
|
||||
destino = ConfiguracaoNotificacao.canal(user.telefone)
|
||||
return if destino.blank?
|
||||
|
||||
credenciais = @config.credenciais_whatsapp
|
||||
return if credenciais.blank? || credenciais[:from].blank?
|
||||
|
||||
Notificacao::ClienteTwilio
|
||||
.montar(credenciais[:sid], credenciais[:token])
|
||||
.messages.create(from: credenciais[:from], to: destino, body: mensagem(user, cm, evento))
|
||||
|
||||
Rails.logger.info("[Notificacao] WhatsApp (#{evento}) enviado para #{user.nome}")
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("[Notificacao] Falha WhatsApp (#{evento}) #{user.nome}: #{e.class}: #{e.message}")
|
||||
end
|
||||
|
||||
def enviar_email(user, cm, evento)
|
||||
ConsolidacaoMailer
|
||||
.public_send(EVENTOS.fetch(evento)[:mailer], user, @consolidacao, cm)
|
||||
.deliver_later
|
||||
Rails.logger.info("[Notificacao] Email (#{evento}) agendado para #{user.email}")
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("[Notificacao] Falha email (#{evento}) #{user.email}: #{e.class}: #{e.message}")
|
||||
end
|
||||
|
||||
def mensagem(user, cm, evento)
|
||||
evt = EVENTOS.fetch(evento)
|
||||
"Olá #{user.nome.split.first}! #{evt[:emoji]}\n" \
|
||||
"#{evt[:titulo]}\n" \
|
||||
"📋 #{@consolidacao.nome}\n" \
|
||||
"💰 Valor: #{moeda(cm.valor_total)}\n" \
|
||||
"#{evt[:cta]} https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista"
|
||||
end
|
||||
|
||||
# Moeda BR com separador de milhar: R$ 1.234,56
|
||||
def moeda(v)
|
||||
inteiro, decimais = format('%.2f', v.to_f).split('.')
|
||||
def moeda(valor)
|
||||
inteiro, decimais = format('%.2f', valor.to_f).split('.')
|
||||
inteiro = inteiro.reverse.gsub(/(\d{3})(?=\d)/, '\1.').reverse
|
||||
"R$ #{inteiro},#{decimais}"
|
||||
end
|
||||
|
||||
def mensagem(user, cm)
|
||||
valor = moeda(cm.valor_total)
|
||||
"Olá #{user.nome.split.first}! 🚚\n" \
|
||||
"Seu pagamento de entregas foi fechado.\n" \
|
||||
"📋 #{@consolidacao.nome}\n" \
|
||||
"💰 Valor: #{valor}\n" \
|
||||
"Acesse seu painel para baixar o extrato: " \
|
||||
"https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista"
|
||||
end
|
||||
|
||||
def enviar_whatsapp(user, cm)
|
||||
return if user.telefone.blank?
|
||||
|
||||
require 'twilio-ruby'
|
||||
cliente = Twilio::REST::Client.new(ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN'])
|
||||
cliente.messages.create(
|
||||
from: ENV['TWILIO_WHATSAPP_FROM'],
|
||||
to: "whatsapp:#{user.telefone}",
|
||||
body: mensagem(user, cm)
|
||||
)
|
||||
Rails.logger.info("[Notificacao] WhatsApp enviado para #{user.nome}")
|
||||
rescue => e
|
||||
Rails.logger.error("[Notificacao] Falha WhatsApp #{user.nome}: #{e.message}")
|
||||
end
|
||||
|
||||
def enviar_email(user, cm)
|
||||
ConsolidacaoMailer.pagamento_fechado(user, @consolidacao, cm).deliver_later
|
||||
Rails.logger.info("[Notificacao] Email agendado para #{user.email}")
|
||||
rescue => e
|
||||
Rails.logger.error("[Notificacao] Falha email #{user.email}: #{e.message}")
|
||||
end
|
||||
|
||||
# ── Aviso de pagamento efetuado ─────────────────────────────
|
||||
def mensagem_pagamento(user, cm)
|
||||
valor = moeda(cm.valor_total)
|
||||
"Olá #{user.nome.split.first}! ✅\n" \
|
||||
"Seu pagamento foi efetuado.\n" \
|
||||
"📋 #{@consolidacao.nome}\n" \
|
||||
"💰 Valor: #{valor}\n" \
|
||||
"Acesse seu painel para conferir: " \
|
||||
"https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista"
|
||||
end
|
||||
|
||||
def enviar_whatsapp_pagamento(user, cm)
|
||||
return if user.telefone.blank?
|
||||
|
||||
require 'twilio-ruby'
|
||||
cliente = Twilio::REST::Client.new(ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN'])
|
||||
cliente.messages.create(
|
||||
from: ENV['TWILIO_WHATSAPP_FROM'],
|
||||
to: "whatsapp:#{user.telefone}",
|
||||
body: mensagem_pagamento(user, cm)
|
||||
)
|
||||
Rails.logger.info("[Notificacao] WhatsApp pagamento enviado para #{user.nome}")
|
||||
rescue => e
|
||||
Rails.logger.error("[Notificacao] Falha WhatsApp pagamento #{user.nome}: #{e.message}")
|
||||
end
|
||||
|
||||
def enviar_email_pagamento(user, cm)
|
||||
ConsolidacaoMailer.pagamento_efetuado(user, @consolidacao, cm).deliver_later
|
||||
Rails.logger.info("[Notificacao] Email pagamento agendado para #{user.email}")
|
||||
rescue => e
|
||||
Rails.logger.error("[Notificacao] Falha email pagamento #{user.email}: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user