# app/models/mensagem_template.rb # # Corpo de um evento em UM canal, montado com blocos no editor. # # `blocos` vem de um campo hidden preenchido por JavaScript — ou seja, de fora. # Por isso #normalizar_blocos DESCARTA o que não está no catálogo: tipo # desconhecido, campo que aquele tipo não tem, e qualquer coisa que não seja # Hash. Sem isso, um JSON adulterado no form viraria conteúdo renderizado sem # passar por validação nenhuma. class MensagemTemplate < ApplicationRecord CANAIS = %w[whatsapp email].freeze # Teto de segurança: um template com milhares de blocos só serviria para # travar o render do e-mail. MAX_BLOCOS = 40 MAX_LINHAS_TABELA = 20 belongs_to :evento_notificacao validates :canal, inclusion: { in: CANAIS } validates :canal, uniqueness: { scope: :evento_notificacao_id } validate :assunto_no_email before_validation :normalizar_blocos scope :ativos, -> { where(ativo: true) } scope :do_canal, ->(canal) { where(canal: canal.to_s) } def email? = canal == 'email' def whatsapp? = canal == 'whatsapp' # Um template só entra em uso quando tem conteúdo — template ativo e vazio # mandaria mensagem em branco, o que é pior do que cair no texto padrão. def utilizavel? ativo? && blocos.present? end def renderizador(dados = {}) Notificacao::Renderizador.new(blocos, dados) end def preview(dados = nil) dados ||= Notificacao::Variaveis.amostra(evento_notificacao&.gatilho) r = renderizador(dados) email? ? r.html : r.texto end private def normalizar_blocos lista = blocos.is_a?(Array) ? blocos : [] self.blocos = lista.filter_map { |bruto| limpar(bruto) }.first(MAX_BLOCOS) end def limpar(bruto) return nil unless bruto.is_a?(Hash) tipo = bruto['tipo'] || bruto[:tipo] return nil unless Notificacao::Blocos.valido?(tipo) limpo = { 'tipo' => tipo.to_s } Notificacao::Blocos.campos(tipo).each do |campo, especie| valor = bruto[campo] || bruto[campo.to_sym] limpo[campo] = especie == :lista ? limpar_linhas(valor) : valor.to_s end limpo end def limpar_linhas(valor) Array(valor).filter_map do |linha| next unless linha.is_a?(Hash) rotulo = (linha['rotulo'] || linha[:rotulo]).to_s conteudo = (linha['valor'] || linha[:valor]).to_s next if rotulo.blank? && conteudo.blank? { 'rotulo' => rotulo, 'valor' => conteudo } end.first(MAX_LINHAS_TABELA) end def assunto_no_email return unless email? && ativo? && blocos.present? && assunto.to_s.strip.blank? errors.add(:assunto, 'é obrigatório no e-mail') end end