# 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