# app/models/contato.rb # # Destinatário das notificações, cadastrado à mão pelo ADM: nome, número e/ou # e-mail, e o grupo a que pertence. # # Telefone é gravado SEMPRE em E.164 (+5511920051157). O ADM digita como quiser # — "(11) 92005-1157", "011 92005 1157" — e a normalização acontece aqui, uma # vez, na gravação. Mandar o texto cru para o provedor devolve erro de destino # inválido que o rescue do envio esconderia. class Contato < ApplicationRecord belongs_to :grupo_contato, optional: true belongs_to :user, optional: true has_many :notificacao_envios, dependent: :nullify EMAIL_RE = /\A[^@\s]+@[^@\s]+\z/ # Um destinatário é uma PESSOA (telefone e/ou e-mail) ou um GRUPO do WhatsApp. # O grupo entra aqui, e não em tabela própria, para herdar grupo interno, # assinatura de eventos e log de envios sem duplicar essa máquina. TIPOS = %w[pessoa grupo_whatsapp].freeze # Identificador de grupo do WhatsApp: "120363012345678901@g.us". JID_GRUPO_RE = /\A\d{5,}@g\.us\z/ validates :nome, presence: true validates :tipo, inclusion: { in: TIPOS } validates :email, format: { with: EMAIL_RE }, allow_blank: true validate :telefone_ou_email, if: :pessoa? validate :telefone_valido, if: :pessoa? validate :grupo_identificado, if: :grupo_whatsapp? before_validation :normalizar scope :ativos, -> { where(ativo: true) } scope :ordenados, -> { order(Arel.sql('LOWER(nome)')) } scope :pessoas, -> { where(tipo: 'pessoa') } scope :grupos_whatsapp, -> { where(tipo: 'grupo_whatsapp') } # "Tem WhatsApp" = número de pessoa OU identificador de grupo. scope :com_whatsapp, -> { where.not(telefone: [nil, '']).or(where.not(whatsapp_grupo_jid: [nil, ''])) } scope :com_email, -> { where.not(email: [nil, '']) } # Filtra pelo canal do disparo. 'ambos' = quem tiver qualquer um dos dois. scope :com_canal, ->(canal) { case canal.to_s when 'whatsapp' then com_whatsapp when 'email' then com_email else where("(telefone IS NOT NULL AND telefone <> '') OR (email IS NOT NULL AND email <> '')") end } def pessoa? = tipo.to_s != 'grupo_whatsapp' def grupo_whatsapp? = tipo.to_s == 'grupo_whatsapp' def whatsapp? = destino_whatsapp.present? def email? = email.present? && pessoa? # Para onde o WhatsApp vai: o JID do grupo ou o número da pessoa. É o que a # ponte recebe — ela aceita as duas formas. def destino_whatsapp grupo_whatsapp? ? whatsapp_grupo_jid.presence : telefone.presence end # {{contato}} na mensagem. Em grupo não existe "primeiro nome" — cortar # "Motoristas SP" em "Motoristas" só empobreceria o texto. def primeiro_nome grupo_whatsapp? ? nome.to_s : nome.to_s.split.first.to_s end private def normalizar self.nome = nome.to_s.strip.squeeze(' ').presence self.tipo = tipo.presence || 'pessoa' if grupo_whatsapp? # Grupo não tem telefone nem e-mail: limpar evita um cadastro meio pessoa, # meio grupo, que mandaria a mesma mensagem duas vezes. self.telefone = nil self.email = nil self.whatsapp_grupo_jid = whatsapp_grupo_jid.to_s.strip.presence self.whatsapp_grupo_nome = whatsapp_grupo_nome.to_s.strip.presence else self.whatsapp_grupo_jid = nil self.whatsapp_grupo_nome = nil self.email = email.to_s.strip.downcase.presence # Mantém o que foi digitado se não der para normalizar — a validação acusa # e o ADM vê o próprio valor de volta no campo para corrigir. bruto = telefone.to_s.strip.presence self.telefone = bruto && (ConfiguracaoNotificacao.normalizar_telefone(bruto) || bruto) end end def grupo_identificado return if whatsapp_grupo_jid.to_s.match?(JID_GRUPO_RE) errors.add(:whatsapp_grupo_jid, 'inválido: escolha um grupo da lista (o identificador termina em @g.us)') end def telefone_ou_email return if telefone.present? || email.present? errors.add(:base, 'Informe ao menos um WhatsApp ou um e-mail.') end def telefone_valido return if telefone.blank? return if ConfiguracaoNotificacao.normalizar_telefone(telefone).present? errors.add(:telefone, 'não parece um número brasileiro válido (DDD + número)') end end