Implantação da geração de mensagem de forma livre e mudança na engine de mensagem

This commit is contained in:
2026-08-24 17:32:16 -03:00
parent 94bbe7556f
commit 88f19dd8f5
77 changed files with 4650 additions and 25 deletions

View File

@@ -0,0 +1,64 @@
# app/jobs/detectar_mudancas_operacao_job.rb
#
# Compara os números de cada operação com o retrato anterior (OperacaoSnapshot)
# e dispara `operacao_alterada` no que mudou.
#
# POR QUE VARREDURA e não gatilho no código: a maior parte das mudanças da
# operação NÃO passa por aqui — ela acontece no SimpliRoute e chega pelo sync do
# espelho, que é read-only do nosso lado. Sem comparar retratos, "o que mudou"
# seria só o que o próprio sistema editou, que é a minoria dos casos.
#
# A PRIMEIRA execução de cada operação só grava o retrato e não avisa nada: sem
# retrato anterior, toda operação existente pareceria "nova" e o primeiro deploy
# dispararia uma mensagem por operação cadastrada.
class DetectarMudancasOperacaoJob < ApplicationJob
queue_as :default
def perform
return unless EventoNotificacao.ativos.exists?(gatilho: 'operacao_alterada')
Operacao.nomes_validos.each { |tabela| verificar(tabela) }
rescue StandardError => e
Rails.logger.error("[MudancasOperacao] #{e.class}: #{e.message}")
end
private
def verificar(tabela)
atuais = contar(tabela)
snapshot = OperacaoSnapshot.para(tabela)
if snapshot.new_record?
snapshot.registrar!(atuais)
return
end
diferenca = snapshot.diferenca(atuais)
return if diferenca.empty?
Notificacao::Gatilhos.operacao_alterada(
operacao: Operacao.label(tabela),
o_que_mudou: OperacaoSnapshot.descrever(diferenca)
)
snapshot.registrar!(atuais)
rescue StandardError => e
# Uma operação problemática não pode impedir a varredura das demais.
Rails.logger.error("[MudancasOperacao] #{tabela}: #{e.class}: #{e.message}")
end
# Sem filtro de data: a comparação é do estado ATUAL da operação inteira, que
# é o que muda quando uma NF entra, sai ou troca de status.
#
# ⚠️ CUSTO: OperacaoMetricas carrega as visitas da operação em memória para
# deduplicar por NF. Uma operação mensal tem alguns milhares de linhas, e o
# job varre TODAS as tabelas a cada execução (8×/dia pelo schedule). É o
# preço de reusar a mesma contagem do dashboard em vez de escrever um SQL
# paralelo que divergiria dele. Se passar a incomodar, o caminho é restringir
# a varredura às operações do mês corrente e do anterior — as antigas não
# mudam mais.
def contar(tabela)
m = Analytics::OperacaoMetricas.new(tabelas: [tabela])
{ total_notas: m.total, entregues: m.sucesso,
nao_entregues: m.recusas, pendentes: m.pendentes }
end
end

View File

@@ -0,0 +1,48 @@
# app/jobs/disparar_eventos_agendados_job.rb
#
# Varre os eventos de gatilho `agendado` e dispara os que venceram. Roda de hora
# em hora pelo cron do container (config/schedule.rb).
#
# POR QUE DE HORA EM HORA e não "no horário exato": o evento guarda hora e
# frequência escolhidas na tela, e essas podem mudar a qualquer momento — reler
# o crontab a cada edição seria acoplar a UI ao cron do sistema. Uma varredura
# horária resolve com uma linha fixa no schedule, e `EventoNotificacao#vencido?`
# garante que cada evento dispara UMA vez por dia (ele grava ultimo_disparo_em).
#
# PERÍODO do resumo — recortes FECHADOS, para o número não mudar depois de
# enviado:
# diária → o dia anterior
# semanal → os 7 dias anteriores
class DispararEventosAgendadosJob < ApplicationJob
queue_as :default
def perform(agora = Time.current)
EventoNotificacao.ativos.where(gatilho: 'agendado').find_each do |evento|
next unless evento.vencido?(agora)
inicio, fim = janela(evento, agora)
resumo = Analytics::ResumoOperacao.new(inicio: inicio, fim: fim)
# Resumo zerado (feriado, fim de semana) só treinaria o leitor a ignorar a
# mensagem. Marca como disparado assim mesmo, senão tentaria de hora em
# hora até o dia virar.
if resumo.vazio?
evento.update_columns(ultimo_disparo_em: agora)
Rails.logger.info("[Agendados] #{evento.chave}: período sem movimento, nada enviado")
next
end
Notificacao::Gatilhos.resumo_agendado(evento, inicio: inicio, fim: fim)
evento.update_columns(ultimo_disparo_em: agora)
end
rescue StandardError => e
Rails.logger.error("[Agendados] #{e.class}: #{e.message}")
end
private
def janela(evento, agora)
hoje = agora.to_date
evento.frequencia == 'semanal' ? [hoje - 7, hoje - 1] : [hoje - 1, hoje - 1]
end
end

View File

@@ -0,0 +1,26 @@
# app/jobs/notificacao_job.rb
#
# Entrega das notificações fora da requisição web.
#
# POR QUE JOB: o envio de WhatsApp pausa entre uma mensagem e outra (o canal por
# QR bane número que dispara em rajada). Um `sleep` de 5s vezes 30 contatos
# dentro do controller pendura o worker Puma por dois minutos e meio.
#
# ⚠️ O adapter é o :async do Rails (fila em memória, igual ao `deliver_later` que
# o app já usa): job pendente se perde se o container reiniciar. Para
# notificação isso é aceitável — o log em notificacao_envios mostra o que
# ficou 'pendente' e a tela permite reenviar.
class NotificacaoJob < ApplicationJob
queue_as :default
def perform(evento_id, dados = {}, assunto = nil, corpo = nil, envolvido_id = nil)
evento = EventoNotificacao.find_by(id: evento_id)
return if evento.nil? || !evento.ativo?
envolvido = envolvido_id && User.find_by(id: envolvido_id)
Notificacao::Despachante.new(evento, dados: dados, assunto: assunto,
corpo: corpo, envolvido: envolvido).executar
rescue StandardError => e
Rails.logger.error("[NotificacaoJob] evento #{evento_id}: #{e.class}: #{e.message}")
end
end