# 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_completos.merge('contato' => nome.to_s) end # O contexto do gatilho + as variáveis que o TEMPLATE usa e o gatilho não # conhece ({{entregas_mes}}, {{telefone_suporte}}, {{hora}}…). # # Resolvido UMA vez por disparo, não por destinatário: `{{entregas_mes}}` é # uma query, e o valor é o mesmo para todo mundo — só `{{contato}}` muda. def dados_completos @dados_completos ||= ResolvedorVariaveis.completar(textos_do_disparo, @dados) end # Tudo que será interpolado: os blocos dos dois templates (o evento pode ter # WhatsApp e e-mail com textos diferentes), o assunto e o corpo padrão. def textos_do_disparo [@assunto, @corpo, @evento.template_utilizavel('whatsapp')&.blocos, @evento.template_utilizavel('email')&.blocos, @evento.template_utilizavel('email')&.assunto] 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 # `destino_whatsapp` resolve pessoa (número) ou grupo (JID) — ver Contato. lista = @evento.destinatarios('whatsapp').map { |c| [c.destino_whatsapp, 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