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

134 lines
5.8 KiB
Ruby

# app/services/notificacao/gatilhos.rb
#
# Os pontos do sistema que DISPARAM um evento de notificação. Cada método monta
# o contexto das {{variaveis}} daquele gatilho e entrega ao Despachante.
#
# Existe para que o código de negócio (consolidação, edição de lançamento, jobs)
# tenha UMA linha de chamada, sem saber de template, canal ou destinatário — e
# para que a lista de variáveis que o editor oferece (Notificacao::Variaveis)
# tenha um par exato aqui. Variável anunciada na tela e nunca preenchida sai como
# vazio na mensagem, o que é pior do que não existir.
#
# NADA aqui levanta: uma notificação não pode derrubar um fechamento nem uma
# edição de lançamento. O Despachante já engole, e os métodos que montam o
# contexto também.
module Notificacao
module Gatilhos
module_function
# Valor do motorista mudou DEPOIS do fechamento (lançamento editado,
# desconto aplicado, pilar remarcado). Em rascunho o valor muda a cada
# clique do wizard — avisar ali seria spam, não informação.
def valor_alterado(consolidacao, consolidacao_motorista, anterior:)
return unless consolidacao.finalizada?
atual = consolidacao_motorista.valor_total.to_f
return if anterior.to_f == atual
Despachante.disparar_gatilho(
gatilho: 'valor_alterado',
dados: Variaveis.comuns_reais.merge(
'motorista' => consolidacao_motorista.motorista_nome,
'consolidacao' => consolidacao.nome,
'periodo' => periodo(consolidacao.data_inicio, consolidacao.data_fim),
'valor' => moeda(atual),
'entregas' => contar_entregas(consolidacao, consolidacao_motorista).to_s,
'link_painel' => link_painel,
'o_que_mudou' => "Valor passou de #{moeda(anterior)} para #{moeda(atual)}"
),
assunto: "Valor alterado — #{consolidacao_motorista.motorista_nome}",
corpo: "⚠️ O valor de #{consolidacao_motorista.motorista_nome} em #{consolidacao.nome} " \
"passou de #{moeda(anterior)} para #{moeda(atual)}.",
# O próprio motorista é parte interessada: o valor DELE mudou depois de
# fechado. Fica sob o `notificar_envolvido` do evento, então o ADM pode
# desligar sem perder o aviso à diretoria.
envolvido: motorista_usuario(consolidacao_motorista)
)
rescue StandardError => e
Rails.logger.error("[Gatilhos] valor_alterado: #{e.class}: #{e.message}")
end
# Dados da operação mudaram. Duas origens, os dois caminham para cá:
# - edição de lançamento no SimpliRoute feita por dentro do sistema;
# - varredura periódica que compara o espelho com o retrato anterior
# (DetectarMudancasOperacaoJob) — é por onde chega a maioria, já que o
# espelho sincroniza mudanças feitas fora daqui.
def operacao_alterada(operacao:, o_que_mudou:, nf: nil)
Despachante.disparar_gatilho(
gatilho: 'operacao_alterada',
dados: Variaveis.comuns_reais.merge(
'operacao' => operacao.to_s,
'o_que_mudou' => o_que_mudou.to_s,
'nf' => nf.to_s
),
assunto: "Alteração na operação #{operacao}",
corpo: "🔄 #{operacao}: #{o_que_mudou}#{nf.present? ? " (NF #{nf})" : ''}"
)
rescue StandardError => e
Rails.logger.error("[Gatilhos] operacao_alterada: #{e.class}: #{e.message}")
end
# Resumo periódico de um evento `agendado`.
def resumo_agendado(evento, inicio:, fim:)
numeros = Analytics::ResumoOperacao.new(inicio: inicio, fim: fim).numeros
Despachante.disparar(
chave: evento.chave,
dados: Variaveis.comuns_reais.merge(numeros).merge(
'periodo' => periodo(inicio, fim)
),
assunto: "Resumo da operação — #{periodo(inicio, fim)}",
corpo: corpo_resumo(numeros, inicio, fim)
)
rescue StandardError => e
Rails.logger.error("[Gatilhos] resumo_agendado #{evento.chave}: #{e.class}: #{e.message}")
end
# ── Auxiliares ─────────────────────────────────────────────
def corpo_resumo(numeros, inicio, fim)
"📊 Resumo da operação — #{periodo(inicio, fim)}\n" \
"• Entregues: #{numeros['entregues']}\n" \
"• Não entregues: #{numeros['recusas']}\n" \
"• Em aberto: #{numeros['pendentes']}\n" \
"• Retentativas: #{numeros['retentativas']}\n" \
"• Notas fora da operação: #{numeros['fora_operacao']}"
end
def periodo(inicio, fim)
"#{inicio&.strftime('%d/%m/%Y')} a #{fim&.strftime('%d/%m/%Y')}"
end
def link_painel
"https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista"
end
# Mesma busca do NotificacaoService (casa pelo nome, que é o que a
# consolidação guarda). Só roda em consolidação finalizada, então o custo
# por recálculo é irrelevante.
def motorista_usuario(consolidacao_motorista)
User.motorista.ativos.find_by('LOWER(nome) = ?', consolidacao_motorista.motorista_nome.to_s.downcase)
rescue StandardError
nil
end
def contar_entregas(consolidacao, consolidacao_motorista)
consolidacao.consolidacao_entregas
.where(motorista_nome: consolidacao_motorista.motorista_nome)
.where.not(tipo: :desconto)
.count
rescue StandardError
nil
end
# Moeda BR com separador de milhar. Duplica o helper do NotificacaoService de
# propósito: este módulo é chamado de dentro de models e jobs, onde puxar o
# service inteiro só para formatar número seria acoplamento à toa.
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
end