Files
Reem-Notas/app/services/notificacao_service.rb

191 lines
7.4 KiB
Ruby

# app/services/notificacao_service.rb
#
# 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,
chave: 'consolidacao_finalizada',
emoji: '🚚',
titulo: 'Seu pagamento de entregas foi fechado.',
cta: 'Acesse seu painel para baixar o extrato:' },
pago: { mailer: :pagamento_efetuado,
chave: '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
# Avisa UM motorista de que o pagamento dele foi efetuado.
def self.notificar_pagamento(consolidacao, consolidacao_motorista)
new(consolidacao).enviar_aviso_pagamento(consolidacao_motorista)
end
def initialize(consolidacao)
@consolidacao = consolidacao
@config = ConfiguracaoNotificacao.instancia
end
def notificar_todos
@consolidacao.consolidacao_motoristas.ativos.each { |cm| notificar(cm, :fechado) }
end
def enviar_aviso_pagamento(consolidacao_motorista)
notificar(consolidacao_motorista, :pago)
end
private
def notificar(cm, evento)
user = motorista_de(cm)
# Aviso pessoal ao motorista: continua com o e-mail formatado do
# ConsolidacaoMailer (não vira texto puro) e passa a usar o provedor de
# WhatsApp escolhido na tela — Baileys por QR ou Twilio.
if user
enviar_whatsapp(user, cm, evento) if @config.whatsapp_habilitado?
enviar_email(user, cm, evento) if @config.email_habilitado? && user.email.present?
end
# Cópia para os grupos que assinam este evento (diretoria, operação...).
# `envolvido: nil` de propósito: o motorista já foi avisado acima e receberia
# a mensagem duas vezes.
avisar_grupos(cm, evento, user)
end
# O corpo aqui é o mesmo texto do WhatsApp — na etapa do editor de blocos ele
# passa a vir do template configurado pelo ADM.
def avisar_grupos(cm, evento, user)
evt = EVENTOS.fetch(evento)
Notificacao::Despachante.disparar(
chave: evt[:chave],
dados: variaveis(cm, user),
assunto: "#{evt[:titulo]}#{@consolidacao.nome}",
corpo: mensagem_para_grupos(cm, evento, user)
)
end
# Contexto das {{variaveis}} do editor de blocos. As chaves são as mesmas de
# Notificacao::Variaveis::FECHAMENTO — é o que o ADM vê na paleta da tela.
def variaveis(cm, user)
Notificacao::Variaveis.comuns_reais.merge(
'motorista' => user&.nome.presence || cm.motorista_nome,
'consolidacao' => @consolidacao.nome,
'periodo' => "#{l_data(@consolidacao.data_inicio)} a #{l_data(@consolidacao.data_fim)}",
'valor' => moeda(cm.valor_total),
# ConsolidacaoMotorista não guarda contagem — só valor_total. As entregas
# do motorista são as linhas classificadas na consolidação (mesma fonte do
# #recalcular_valor), descontando os lançamentos de desconto, que não são
# entregas.
'entregas' => entregas_do_motorista(cm).to_s,
'link_painel' => "https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista"
)
end
def l_data(data)
data&.strftime('%d/%m/%Y').to_s
end
def entregas_do_motorista(cm)
@consolidacao.consolidacao_entregas
.where(motorista_nome: cm.motorista_nome)
.where.not(tipo: :desconto)
.count
rescue StandardError => e
Rails.logger.warn("[Notificacao] contagem de entregas indisponível: #{e.class}: #{e.message}")
nil
end
def mensagem_para_grupos(cm, evento, user)
evt = EVENTOS.fetch(evento)
"#{evt[:emoji]} #{evt[:titulo]}\n" \
"👤 #{user&.nome || cm.motorista_nome}\n" \
"📋 #{@consolidacao.nome}\n" \
"💰 Valor: #{moeda(cm.valor_total)}"
end
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"; a normalização
# para E.164 acontece dentro de Notificacao::Whatsapp — mandar o texto cru
# devolve erro de destino inválido que o rescue esconderia.
destino = ConfiguracaoNotificacao.normalizar_telefone(user.telefone)
return if destino.blank?
texto = mensagem(user, cm, evento)
envio = registrar_envio(evento, canal: 'whatsapp', destino: destino, user: user, corpo: texto)
resposta = Notificacao::Whatsapp.enviar(para: destino, texto: texto, config: @config)
if resposta.ok?
envio&.marcar_enviado!
Rails.logger.info("[Notificacao] WhatsApp (#{evento}) enviado para #{user.nome}")
else
envio&.marcar_falha!(resposta.erro)
Rails.logger.error("[Notificacao] Falha WhatsApp (#{evento}) #{user.nome}: #{resposta.erro}")
end
end
def enviar_email(user, cm, evento)
evt = EVENTOS.fetch(evento)
envio = registrar_envio(evento, canal: 'email', destino: user.email, user: user,
corpo: mensagem(user, cm, evento), assunto: evt[:titulo])
ConsolidacaoMailer.public_send(evt[:mailer], user, @consolidacao, cm).deliver_later
# `deliver_later` só ENFILEIRA: marcar como enviado aqui significa "saiu do
# nosso lado". Uma falha do SMTP aparece no log do Rails, não aqui.
envio&.marcar_enviado!
Rails.logger.info("[Notificacao] Email (#{evento}) agendado para #{user.email}")
rescue StandardError => e
envio&.marcar_falha!("#{e.class}: #{e.message}")
Rails.logger.error("[Notificacao] Falha email (#{evento}) #{user.email}: #{e.class}: #{e.message}")
end
# Log do aviso pessoal. Nunca levanta: se a tabela ainda não existir (deploy
# antes do migrate), o envio precisa seguir mesmo sem registro.
def registrar_envio(evento, canal:, destino:, user:, corpo:, assunto: nil)
NotificacaoEnvio.create!(
evento_notificacao: evento_registrado(evento), user: user, canal: canal,
destino: destino, assunto: assunto, corpo: corpo, status: 'pendente'
)
rescue StandardError => e
Rails.logger.warn("[Notificacao] sem log de envio (#{e.class}: #{e.message})")
nil
end
def evento_registrado(evento)
@eventos_cache ||= {}
@eventos_cache[evento] ||= EventoNotificacao.find_by(chave: EVENTOS.fetch(evento)[:chave])
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(valor)
inteiro, decimais = format('%.2f', valor.to_f).split('.')
inteiro = inteiro.reverse.gsub(/(\d{3})(?=\d)/, '\1.').reverse
"R$ #{inteiro},#{decimais}"
end
end