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,119 @@
# app/models/evento_notificacao.rb
#
# Um evento que dispara mensagem. O ADM cadastra quantos quiser e liga cada um a
# grupos de contato — mas o GATILHO sai de GATILHOS, porque gatilho é código.
#
# manual → botão "disparar agora" (aviso pontual)
# consolidacao_finalizada → NotificacaoService.notificar_finalizacao
# pagamento_efetuado → NotificacaoService.notificar_pagamento
# valor_alterado → edição de lançamento / desconto após o fechamento
# operacao_alterada → NF que entrou/saiu, entrega que mudou de status
# agendado → resumo periódico (frequencia + hora)
#
# `sistema: true` marca os que nascem com o app: editáveis e desligáveis, mas
# não apagáveis — apagar deixaria o código disparando no vazio.
class EventoNotificacao < ApplicationRecord
self.table_name = 'eventos_notificacao'
GATILHOS = %w[manual consolidacao_finalizada pagamento_efetuado
valor_alterado operacao_alterada agendado].freeze
GATILHO_LABEL = {
'manual' => 'Disparo manual (botão)',
'consolidacao_finalizada' => 'Consolidação finalizada',
'pagamento_efetuado' => 'Pagamento efetuado',
'valor_alterado' => 'Valor alterado no fechamento',
'operacao_alterada' => 'Dados da operação mudaram',
'agendado' => 'Agendado (resumo periódico)'
}.freeze
FREQUENCIAS = %w[diaria semanal].freeze
# Gatilhos que se referem a UMA pessoa específica (o motorista do pagamento),
# e não só a grupos. Só neles `notificar_envolvido` faz sentido.
GATILHOS_COM_ENVOLVIDO = %w[consolidacao_finalizada pagamento_efetuado valor_alterado].freeze
has_many :assinaturas, class_name: 'GrupoEventoAssinatura', dependent: :destroy
has_many :grupos_contato, through: :assinaturas, source: :grupo_contato
has_many :notificacao_envios, dependent: :nullify
has_many :mensagem_templates, dependent: :destroy
accepts_nested_attributes_for :assinaturas, allow_destroy: true
validates :nome, presence: true
validates :chave, presence: true, uniqueness: { case_sensitive: false }
validates :gatilho, inclusion: { in: GATILHOS }
validates :frequencia, inclusion: { in: FREQUENCIAS }, if: :agendado?
validates :hora, numericality: { only_integer: true, greater_than_or_equal_to: 0,
less_than_or_equal_to: 23 }, allow_nil: true
validates :dia_semana, numericality: { only_integer: true, greater_than_or_equal_to: 0,
less_than_or_equal_to: 6 }, allow_nil: true
validate :semanal_tem_dia
before_validation :normalizar
scope :ativos, -> { where(ativo: true) }
scope :ordenados, -> { order(Arel.sql('LOWER(nome)')) }
scope :do_gatilho, ->(g) { ativos.where(gatilho: g) }
def agendado? = gatilho == 'agendado'
def manual? = gatilho == 'manual'
def envolvido_aplicavel? = GATILHOS_COM_ENVOLVIDO.include?(gatilho)
def gatilho_label = GATILHO_LABEL[gatilho] || gatilho
# Só pode apagar o que o ADM criou.
def apagavel? = !sistema?
# Template do canal, criando um vazio em memória quando ainda não existe —
# assim a tela do editor abre igual para evento novo e evento já configurado.
def template(canal)
mensagem_templates.find { |t| t.canal == canal.to_s } ||
mensagem_templates.build(canal: canal.to_s, blocos: [])
end
# O corpo configurado no editor vence o texto padrão do código. Um template
# ativo porém VAZIO não conta (mandaria mensagem em branco).
def template_utilizavel(canal)
t = mensagem_templates.detect { |x| x.canal == canal.to_s }
t&.utilizavel? ? t : nil
end
def variaveis = Notificacao::Variaveis.para(gatilho)
# Contatos que devem receber este evento, sem repetição — a mesma pessoa pode
# estar em dois grupos assinantes.
def destinatarios(canal)
ids = assinaturas.where(ativo: true)
.select { |a| a.cobre_canal?(canal) }
.map(&:grupo_contato_id)
return Contato.none if ids.empty?
Contato.ativos.where(grupo_contato_id: ids).com_canal(canal).distinct
end
# Já passou da hora de disparar hoje/esta semana?
def vencido?(agora = Time.current)
return false unless agendado? && ativo?
return false unless agora.hour >= hora.to_i
return false if frequencia == 'semanal' && agora.wday != dia_semana.to_i
return true if ultimo_disparo_em.blank?
ultimo_disparo_em < agora.beginning_of_day
end
private
def normalizar
self.nome = nome.to_s.strip.squeeze(' ').presence
self.chave = chave.presence || nome.to_s.parameterize(separator: '_').presence
self.chave = chave.to_s.strip.downcase.presence
self.dia_semana = nil unless agendado? && frequencia == 'semanal'
self.frequencia = nil unless agendado?
end
def semanal_tem_dia
return unless agendado? && frequencia == 'semanal' && dia_semana.blank?
errors.add(:dia_semana, 'é obrigatório na frequência semanal')
end
end