196 lines
8.2 KiB
Ruby
196 lines
8.2 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_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
|