# app/services/notificacao_service.rb # # Envia notificações aos motoristas quando uma consolidação é finalizada ou # quando o pagamento é efetuado. # # Canais e credenciais vêm de ConfiguracaoNotificacao (tela /admin/ # configuracao_notificacao), com fallback para o .env. # # Falhas de envio NUNCA quebram o fluxo de finalização — apenas logam. Isso é # intencional: um SMTP fora do ar não pode impedir o fechamento de um pagamento. # Para DIAGNOSTICAR o envio, use os botões de teste da tela, que usam # Notificacao::TesteEmail / Notificacao::TesteWhatsapp e mostram o erro real. class NotificacaoService EVENTOS = { fechado: { mailer: :pagamento_fechado, chave: 'consolidacao_finalizada', emoji: '🚚', titulo: 'Seu pagamento de entregas foi fechado.', cta: 'Acesse seu painel para baixar o extrato:' }, pago: { mailer: :pagamento_efetuado, chave: 'pagamento_efetuado', emoji: '✅', titulo: 'Seu pagamento foi efetuado.', cta: 'Acesse seu painel para conferir:' } }.freeze def self.notificar_finalizacao(consolidacao) new(consolidacao).notificar_todos end # Avisa UM motorista de que o pagamento dele foi efetuado. def self.notificar_pagamento(consolidacao, consolidacao_motorista) new(consolidacao).enviar_aviso_pagamento(consolidacao_motorista) end def initialize(consolidacao) @consolidacao = consolidacao @config = ConfiguracaoNotificacao.instancia end def notificar_todos @consolidacao.consolidacao_motoristas.ativos.each { |cm| notificar(cm, :fechado) } end def enviar_aviso_pagamento(consolidacao_motorista) notificar(consolidacao_motorista, :pago) end private def notificar(cm, evento) user = motorista_de(cm) # Aviso pessoal ao motorista: continua com o e-mail formatado do # ConsolidacaoMailer (não vira texto puro) e passa a usar o provedor de # WhatsApp escolhido na tela — Baileys por QR ou Twilio. if user enviar_whatsapp(user, cm, evento) if @config.whatsapp_habilitado? enviar_email(user, cm, evento) if @config.email_habilitado? && user.email.present? end # Cópia para os grupos que assinam este evento (diretoria, operação...). # `envolvido: nil` de propósito: o motorista já foi avisado acima e receberia # a mensagem duas vezes. avisar_grupos(cm, evento, user) end # O corpo aqui é o mesmo texto do WhatsApp — na etapa do editor de blocos ele # passa a vir do template configurado pelo ADM. def avisar_grupos(cm, evento, user) evt = EVENTOS.fetch(evento) Notificacao::Despachante.disparar_gatilho( gatilho: evt[:chave], dados: variaveis(cm, user), assunto: "#{evt[:titulo]} — #{@consolidacao.nome}", corpo: mensagem_para_grupos(cm, evento, user) ) end # Contexto das {{variaveis}} do editor de blocos. As chaves são as mesmas de # Notificacao::Variaveis::FECHAMENTO — é o que o ADM vê na paleta da tela. def variaveis(cm, user) Notificacao::Variaveis.comuns_reais.merge( 'motorista' => user&.nome.presence || cm.motorista_nome, 'consolidacao' => @consolidacao.nome, 'periodo' => "#{l_data(@consolidacao.data_inicio)} a #{l_data(@consolidacao.data_fim)}", 'valor' => moeda(cm.valor_total), # ConsolidacaoMotorista não guarda contagem — só valor_total. As entregas # do motorista são as linhas classificadas na consolidação (mesma fonte do # #recalcular_valor), descontando os lançamentos de desconto, que não são # entregas. 'entregas' => entregas_do_motorista(cm).to_s, 'link_painel' => "https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista" ) end def l_data(data) data&.strftime('%d/%m/%Y').to_s end def entregas_do_motorista(cm) @consolidacao.consolidacao_entregas .where(motorista_nome: cm.motorista_nome) .where.not(tipo: :desconto) .count rescue StandardError => e Rails.logger.warn("[Notificacao] contagem de entregas indisponível: #{e.class}: #{e.message}") nil end def mensagem_para_grupos(cm, evento, user) evt = EVENTOS.fetch(evento) "#{evt[:emoji]} #{evt[:titulo]}\n" \ "👤 #{user&.nome || cm.motorista_nome}\n" \ "📋 #{@consolidacao.nome}\n" \ "💰 Valor: #{moeda(cm.valor_total)}" end def motorista_de(cm) User.motorista.ativos.find_by('LOWER(nome) = ?', cm.motorista_nome.downcase) end def enviar_whatsapp(user, cm, evento) # O telefone no cadastro pode estar como "(11) 92005-1157"; a normalização # para E.164 acontece dentro de Notificacao::Whatsapp — mandar o texto cru # devolve erro de destino inválido que o rescue esconderia. destino = ConfiguracaoNotificacao.normalizar_telefone(user.telefone) return if destino.blank? texto = mensagem(user, cm, evento) envio = registrar_envio(evento, canal: 'whatsapp', destino: destino, user: user, corpo: texto) resposta = Notificacao::Whatsapp.enviar(para: destino, texto: texto, config: @config) if resposta.ok? envio&.marcar_enviado! Rails.logger.info("[Notificacao] WhatsApp (#{evento}) enviado para #{user.nome}") else envio&.marcar_falha!(resposta.erro) Rails.logger.error("[Notificacao] Falha WhatsApp (#{evento}) #{user.nome}: #{resposta.erro}") end end def enviar_email(user, cm, evento) evt = EVENTOS.fetch(evento) envio = registrar_envio(evento, canal: 'email', destino: user.email, user: user, corpo: mensagem(user, cm, evento), assunto: evt[:titulo]) ConsolidacaoMailer.public_send(evt[:mailer], user, @consolidacao, cm).deliver_later # `deliver_later` só ENFILEIRA: marcar como enviado aqui significa "saiu do # nosso lado". Uma falha do SMTP aparece no log do Rails, não aqui. envio&.marcar_enviado! Rails.logger.info("[Notificacao] Email (#{evento}) agendado para #{user.email}") rescue StandardError => e envio&.marcar_falha!("#{e.class}: #{e.message}") Rails.logger.error("[Notificacao] Falha email (#{evento}) #{user.email}: #{e.class}: #{e.message}") end # Log do aviso pessoal. Nunca levanta: se a tabela ainda não existir (deploy # antes do migrate), o envio precisa seguir mesmo sem registro. def registrar_envio(evento, canal:, destino:, user:, corpo:, assunto: nil) NotificacaoEnvio.create!( evento_notificacao: evento_registrado(evento), user: user, canal: canal, destino: destino, assunto: assunto, corpo: corpo, status: 'pendente' ) rescue StandardError => e Rails.logger.warn("[Notificacao] sem log de envio (#{e.class}: #{e.message})") nil end def evento_registrado(evento) @eventos_cache ||= {} @eventos_cache[evento] ||= EventoNotificacao.find_by(chave: EVENTOS.fetch(evento)[:chave]) end def mensagem(user, cm, evento) evt = EVENTOS.fetch(evento) "Olá #{user.nome.split.first}! #{evt[:emoji]}\n" \ "#{evt[:titulo]}\n" \ "📋 #{@consolidacao.nome}\n" \ "💰 Valor: #{moeda(cm.valor_total)}\n" \ "#{evt[:cta]} https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista" end # Moeda BR com separador de milhar: R$ 1.234,56 def moeda(valor) inteiro, decimais = format('%.2f', valor.to_f).split('.') inteiro = inteiro.reverse.gsub(/(\d{3})(?=\d)/, '\1.').reverse "R$ #{inteiro},#{decimais}" end end