# app/services/notificacao/catalogo_variaveis.rb # # TODAS as variáveis que o editor de mensagem oferece, de quatro origens: # # :contexto — vêm com o disparo (motorista, consolidação, valor…). Só têm # valor nos gatilhos que as produzem; em outro gatilho saem # vazias, e a tela avisa isso. # :sistema — não dependem de contexto nenhum (empresa, data, hora, mês). # :dados — CONSULTAM O BANCO na hora do envio (entregas do mês, total a # pagar…). Resolvidas sob demanda: só a variável realmente # escrita no template vira query. # :personalizada — cadastradas pelo ADM (VariavelPersonalizada), valor fixo. # # Antes, `Notificacao::Variaveis::POR_GATILHO` amarrava a lista ao gatilho: o ADM # via 6 variáveis e nada mais, e qualquer texto com número (“entregas do mês”) # precisava de deploy. Aqui a lista é aberta — e o que não se aplica é MARCADO em # vez de escondido. # # ⚠️ Um `resolver` NUNCA pode levantar: notificação não derruba fechamento. O # ResolvedorVariaveis engole e devolve vazio. module Notificacao module CatalogoVariaveis ORIGENS = { contexto: 'Do que aconteceu', sistema: 'Empresa e data', dados: 'Números do sistema', personalizada: 'Suas variáveis' }.freeze # ── Sistema: sempre disponíveis ─────────────────────────── SISTEMA = { 'empresa' => { descricao: 'Nome da empresa', amostra: 'Reem Transportes', resolver: -> { Configuracao.valor('empresa_nome').presence || 'Reem Transportes' } }, 'data' => { descricao: 'Data de hoje (dd/mm/aaaa)', amostra: '26/08/2026', resolver: -> { Date.current.strftime('%d/%m/%Y') } }, 'hora' => { descricao: 'Hora do envio', amostra: '14:35', resolver: -> { Time.current.strftime('%H:%M') } }, 'dia_semana' => { descricao: 'Dia da semana por extenso', amostra: 'quarta-feira', resolver: -> { I18n.l(Date.current, format: '%A') rescue Date::DAYNAMES[Date.current.wday] } }, 'mes' => { descricao: 'Mês por extenso', amostra: 'agosto', resolver: -> { I18n.l(Date.current, format: '%B') rescue Date::MONTHNAMES[Date.current.month] } }, 'mes_ano' => { descricao: 'Mês/ano (mm/aaaa)', amostra: '08/2026', resolver: -> { Date.current.strftime('%m/%Y') } }, 'link_sistema' => { descricao: 'Endereço do sistema', amostra: 'https://app.reemtransportes.com.br', resolver: -> { Configuracao.valor('url_sistema').presence || ENV.fetch('APP_URL', '') } } }.freeze # ── Contexto: quem produz é o gatilho ───────────────────── # `gatilhos:` diz em quais a variável tem valor — é o que a tela usa para # avisar "esta sai vazia aqui". CONTEXTO = { 'contato' => { descricao: 'Primeiro nome de quem recebe', amostra: 'Carlos', gatilhos: :todos }, 'quem' => { descricao: 'Quem executou a ação no sistema', amostra: 'Ana Paula', gatilhos: :todos }, 'motorista' => { descricao: 'Nome do motorista', amostra: 'Carlos Matheus Pimentel' }, 'consolidacao' => { descricao: 'Nome da consolidação', amostra: 'UBS NORTE 01–14/08' }, 'periodo' => { descricao: 'Período do fechamento', amostra: '01/08/2026 a 14/08/2026' }, 'valor' => { descricao: 'Valor do motorista', amostra: 'R$ 5.060,00' }, 'entregas' => { descricao: 'Entregas atendidas no fechamento', amostra: '280' }, 'link_painel' => { descricao: 'Link do painel do motorista', amostra: 'https://app.reem.com.br/motorista' }, 'o_que_mudou' => { descricao: 'O que foi alterado', amostra: 'Desconto de R$ 120,00 (avaria)' }, 'operacao' => { descricao: 'Operação afetada', amostra: 'UBS NORTE AGO 2026' }, 'nf' => { descricao: 'Nota fiscal', amostra: '85382' }, 'entregues' => { descricao: 'Notas entregues (resumo)', amostra: '4.851' }, 'recusas' => { descricao: 'Notas não entregues (resumo)', amostra: '122' }, 'pendentes' => { descricao: 'Notas em aberto (resumo)', amostra: '0' }, 'retentativas' => { descricao: 'Visitas além da primeira (resumo)', amostra: '4' }, 'fora_operacao' => { descricao: 'Notas fora da operação (resumo)', amostra: '2' } }.freeze # Quais variáveis de contexto cada gatilho realmente preenche. POR_GATILHO = { 'manual' => [], 'consolidacao_finalizada' => %w[motorista consolidacao periodo valor entregas link_painel], 'pagamento_efetuado' => %w[motorista consolidacao periodo valor entregas link_painel], 'valor_alterado' => %w[motorista consolidacao periodo valor entregas link_painel o_que_mudou], 'operacao_alterada' => %w[operacao o_que_mudou nf], 'agendado' => %w[operacao periodo entregues recusas pendentes retentativas fora_operacao] }.freeze # ── Dados: consultam o banco na hora do envio ───────────── # Cada uma responde a uma pergunta que o ADM hoje só conseguiria escrevendo à # mão — e que ficaria desatualizada na mensagem seguinte. DADOS = { 'entregas_mes' => { descricao: 'Entregas atendidas no mês corrente', amostra: '4.977', resolver: -> { Entrega.atendidas.da_conta_gade.no_periodo_checkout(Date.current.beginning_of_month, Date.current).count } }, 'entregas_hoje' => { descricao: 'Entregas atendidas hoje', amostra: '213', resolver: -> { Entrega.atendidas.da_conta_gade.no_periodo_checkout(Date.current, Date.current).count } }, 'motoristas_ativos_mes' => { descricao: 'Motoristas com entrega no mês', amostra: '17', resolver: -> { Entrega.motoristas_ativos(inicio: Date.current.beginning_of_month, fim: Date.current).size } }, 'consolidacoes_abertas' => { descricao: 'Consolidações em rascunho', amostra: '3', resolver: -> { Consolidacao.ativas.rascunho.count } }, 'total_a_pagar_mes' => { descricao: 'Total a pagar no mês (fechado e não pago)', amostra: 'R$ 72.508,00', resolver: lambda { totais = Analytics::TotaisPorMotorista.new(inicio: Date.current.beginning_of_month, fim: Date.current.end_of_month) Formatador.moeda(totais.total_pendente) } }, 'total_pago_mes' => { descricao: 'Total já pago no mês', amostra: 'R$ 21.300,00', resolver: lambda { totais = Analytics::TotaisPorMotorista.new(inicio: Date.current.beginning_of_month, fim: Date.current.end_of_month) Formatador.moeda(totais.total_pago) } }, 'total_consolidado_mes' => { descricao: 'Total consolidado no mês', amostra: 'R$ 93.808,00', resolver: lambda { totais = Analytics::TotaisPorMotorista.new(inicio: Date.current.beginning_of_month, fim: Date.current.end_of_month) Formatador.moeda(totais.total_geral) } } }.freeze module_function # Todas as variáveis conhecidas, incluindo as que o ADM cadastrou. # { chave => { origem:, descricao:, amostra: } } def todas base = {} SISTEMA.each { |c, m| base[c] = { origem: :sistema, descricao: m[:descricao], amostra: m[:amostra] } } CONTEXTO.each { |c, m| base[c] = { origem: :contexto, descricao: m[:descricao], amostra: m[:amostra] } } DADOS.each { |c, m| base[c] = { origem: :dados, descricao: m[:descricao], amostra: m[:amostra] } } personalizadas.each do |v| base[v.chave] = { origem: :personalizada, descricao: v.descricao.presence || 'Sua variável', amostra: v.valor } end base end # A variável tem valor NESTE gatilho? Contexto de outro gatilho sai vazio — # a tela mostra, em vez de esconder a variável (foi o pedido: liberdade com # aviso, não lista curta). def aplicavel?(chave, gatilho) meta = CONTEXTO[chave.to_s] return true if meta.nil? # sistema/dados/personalizada: sempre return true if meta[:gatilhos] == :todos Array(POR_GATILHO[gatilho.to_s]).include?(chave.to_s) end def origem(chave) todas.dig(chave.to_s, :origem) end def existe?(chave) = todas.key?(chave.to_s) # Agrupadas para a tela do editor, na ordem de ORIGENS. def por_origem(gatilho = nil) agrupadas = ORIGENS.keys.index_with { |o| [] } todas.each do |chave, meta| agrupadas[meta[:origem]] << meta.merge(chave: chave, aplicavel: gatilho.nil? || aplicavel?(chave, gatilho)) end agrupadas end # Contexto do PREVIEW: valores de amostra, nunca de envio real. def amostra(gatilho = nil) todas.filter_map { |chave, meta| [chave, meta[:amostra].to_s] if gatilho.nil? || aplicavel?(chave, gatilho) }.to_h end def personalizadas return [] unless VariavelPersonalizada.table_exists? VariavelPersonalizada.ativas.to_a rescue StandardError [] end end end