Files
Reem-Notas/app/services/notificacao/despachante.rb
2026-08-24 19:00:21 -03:00

177 lines
7.3 KiB
Ruby

# app/services/notificacao/despachante.rb
#
# Entrega UMA mensagem já montada aos destinatários de um evento.
#
# Separação proposital: o Despachante resolve QUEM recebe, por qual canal,
# respeita o intervalo entre envios e registra o resultado. O TEXTO vem do
# template de blocos do evento (MensagemTemplate), renderizado por canal — HTML
# no e-mail, texto puro no WhatsApp, do mesmo template.
#
# `corpo:`/`assunto:` são o FALLBACK: valem quando o evento ainda não tem
# template montado (o texto padrão do código) ou quando a mensagem é digitada na
# hora (disparo manual). Sem esse fallback, ligar o editor apagaria as
# notificações dos eventos que já funcionavam.
#
# Destinatários = contatos dos grupos que assinam o evento
# + (opcional) a pessoa diretamente envolvida no fato, que é o
# comportamento que já existia: o motorista dono do pagamento.
#
# ⚠️ INTERVALO: no WhatsApp por sessão QR (canal não oficial), disparar em
# rajada para dezenas de números é a forma mais rápida de perder o número.
# Por isso o envio roda em job, com pausa configurável entre mensagens —
# nunca dentro da requisição web.
module Notificacao
class Despachante
# Dispara em background. Nunca levanta: uma notificação não pode derrubar o
# fechamento de um pagamento (mesma regra do NotificacaoService).
# Dispara UM evento específico (disparo manual pelo botão da tela).
def self.disparar(chave:, dados: {}, assunto: nil, corpo: nil, envolvido: nil)
evento = EventoNotificacao.ativos.find_by(chave: chave.to_s)
return if evento.nil?
agendar(evento, dados: dados, assunto: assunto, corpo: corpo, envolvido: envolvido)
rescue StandardError => e
Rails.logger.error("[Despachante] falha ao agendar #{chave}: #{e.class}: #{e.message}")
end
# Dispara TODOS os eventos ativos de um gatilho.
#
# É o que o código de negócio usa. Buscar pela `chave` (como era antes) fazia
# só o evento de SISTEMA disparar: um evento criado pelo ADM com o mesmo
# gatilho ficava mudo para sempre, sem erro nenhum — a tela oferecia o
# gatilho e nada acontecia. Com N eventos no mesmo gatilho, cada um tem seus
# próprios grupos e sua própria mensagem, que é o ponto de poder cadastrar.
def self.disparar_gatilho(gatilho:, dados: {}, assunto: nil, corpo: nil, envolvido: nil)
EventoNotificacao.ativos.where(gatilho: gatilho.to_s).find_each do |evento|
agendar(evento, dados: dados, assunto: assunto, corpo: corpo, envolvido: envolvido)
end
rescue StandardError => e
Rails.logger.error("[Despachante] falha ao agendar gatilho #{gatilho}: #{e.class}: #{e.message}")
end
def self.agendar(evento, dados:, assunto:, corpo:, envolvido:)
NotificacaoJob.perform_later(evento.id, (dados || {}).stringify_keys,
assunto.to_s, corpo.to_s, envolvido&.id)
end
private_class_method :agendar
def initialize(evento, dados: {}, assunto: nil, corpo: nil, envolvido: nil)
@evento = evento
@dados = (dados || {}).stringify_keys
@assunto = assunto.presence || evento.nome
@corpo = corpo.to_s
@envolvido = envolvido
@config = ConfiguracaoNotificacao.instancia
end
def executar
enviar_whatsapp if @config.whatsapp_habilitado?
enviar_email if @config.email_habilitado?
end
private
def enviar_whatsapp
intervalo = @config.intervalo_envio
destinos_whatsapp.each_with_index do |(destino, contato, user), i|
# Pausa ENTRE mensagens, não antes da primeira.
sleep(intervalo) if i.positive? && intervalo.positive?
corpo = corpo_whatsapp(contato, user)
envio = registrar(canal: 'whatsapp', destino: destino, contato: contato,
user: user, corpo: corpo)
resposta = Whatsapp.enviar(para: destino, texto: corpo, config: @config)
resposta.ok? ? envio.marcar_enviado! : envio.marcar_falha!(resposta.erro)
end
end
# ── Corpo por canal ─────────────────────────────────────────
# A renderização acontece POR DESTINATÁRIO, não uma vez só: {{contato}} é o
# primeiro nome de quem recebe, então um render compartilhado mandaria o
# nome da primeira pessoa para todo mundo. Renderizar é manipulação de
# string — o custo por destinatário é irrelevante perto do envio em si.
def dados_para(contato, user)
nome = contato&.primeiro_nome.presence || user&.nome.to_s.split.first
@dados.merge('contato' => nome.to_s)
end
# O template do editor vence; sem template utilizável, cai no texto que o
# chamador passou (texto padrão do código ou mensagem digitada na hora).
def corpo_whatsapp(contato, user)
template = @evento.template_utilizavel('whatsapp')
return @corpo unless template
template.renderizador(dados_para(contato, user)).texto.presence || @corpo
end
# [assunto, corpo, html?] — com template o e-mail sai em HTML montado; sem
# template, texto puro que o mailer formata.
def email_montado(contato, user)
template = @evento.template_utilizavel('email')
return [@assunto, @corpo, false] unless template
r = template.renderizador(dados_para(contato, user))
[r.interpolar(template.assunto).presence || @assunto, r.html, true]
end
def enviar_email
destinos_email.each do |destino, contato, user|
assunto, corpo, html = email_montado(contato, user)
envio = registrar(canal: 'email', destino: destino, contato: contato,
user: user, corpo: corpo, assunto: assunto)
begin
NotificacaoMailer.mensagem(destino, assunto, corpo, html: html).deliver_now
envio.marcar_enviado!
rescue StandardError => e
envio.marcar_falha!("#{e.class}: #{e.message}")
end
end
end
# [[destino, contato, user], ...] sem repetir o mesmo número/e-mail — a mesma
# pessoa pode ser contato de um grupo E o envolvido no fato.
def destinos_whatsapp
lista = @evento.destinatarios('whatsapp').map { |c| [c.telefone, c, c.user] }
lista += envolvido_whatsapp
dedup(lista)
end
def destinos_email
lista = @evento.destinatarios('email').map { |c| [c.email, c, c.user] }
lista += envolvido_email
dedup(lista)
end
def envolvido_whatsapp
return [] unless notificar_envolvido?
numero = ConfiguracaoNotificacao.normalizar_telefone(@envolvido.telefone)
numero.present? ? [[numero, nil, @envolvido]] : []
end
def envolvido_email
return [] unless notificar_envolvido? && @envolvido.email.present?
[[@envolvido.email, nil, @envolvido]]
end
def notificar_envolvido?
@envolvido.present? && @evento.notificar_envolvido?
end
def dedup(lista)
lista.reject { |destino, _, _| destino.blank? }
.uniq { |destino, _, _| destino.to_s.downcase }
end
def registrar(canal:, destino:, contato:, user:, corpo:, assunto: nil)
NotificacaoEnvio.create!(
evento_notificacao: @evento, contato: contato, user: user,
canal: canal, destino: destino, assunto: assunto,
corpo: corpo, status: 'pendente'
)
end
end
end