diff --git a/app/controllers/admin/configuracao_notificacoes_controller.rb b/app/controllers/admin/configuracao_notificacoes_controller.rb index 1352454..c757e52 100644 --- a/app/controllers/admin/configuracao_notificacoes_controller.rb +++ b/app/controllers/admin/configuracao_notificacoes_controller.rb @@ -9,7 +9,9 @@ class Admin::ConfiguracaoNotificacoesController < ApplicationController smtp_ativo smtp_address smtp_port smtp_username smtp_password smtp_domain smtp_autenticacao remetente_email remetente_nome email_notificacoes_ativo email_admin - whatsapp_ativo twilio_account_sid twilio_auth_token twilio_from whatsapp_admin + whatsapp_ativo whatsapp_provedor whatsapp_intervalo_segundos whatsapp_admin + twilio_account_sid twilio_auth_token twilio_from + baileys_url baileys_token ].freeze def show @@ -60,6 +62,7 @@ class Admin::ConfiguracaoNotificacoesController < ApplicationController # tela nunca exibe o valor, então salvar sem redigitar não pode apagá-lo. permitidos.delete(:smtp_password) if permitidos[:smtp_password].blank? permitidos.delete(:twilio_auth_token) if permitidos[:twilio_auth_token].blank? + permitidos.delete(:baileys_token) if permitidos[:baileys_token].blank? permitidos end @@ -79,11 +82,15 @@ class Admin::ConfiguracaoNotificacoesController < ApplicationController email_admin: @config.email_admin, email_notificacoes_ativo: @config.email_notificacoes_ativo, whatsapp_ativo: @config.whatsapp_ativo, + whatsapp_provedor: @config.whatsapp_provedor, + whatsapp_intervalo: @config.whatsapp_intervalo_segundos, twilio_account_sid: @config.twilio_account_sid, twilio_from: @config.twilio_from, whatsapp_admin: @config.whatsapp_admin, + baileys_url: @config.baileys_url, smtp_password_definida: @config.smtp_password?, - twilio_auth_token_definido: @config.twilio_auth_token? + twilio_auth_token_definido: @config.twilio_auth_token?, + baileys_token_definido: @config.baileys_token? } end diff --git a/app/services/notificacao/teste_whatsapp.rb b/app/services/notificacao/teste_whatsapp.rb index 9f1b93f..e730dc3 100644 --- a/app/services/notificacao/teste_whatsapp.rb +++ b/app/services/notificacao/teste_whatsapp.rb @@ -1,9 +1,11 @@ # app/services/notificacao/teste_whatsapp.rb # -# Dispara um WhatsApp de teste pelo Twilio para o número administrativo e -# traduz o código de erro da API para uma instrução acionável em português — -# os erros de Sandbox (opt-in) são de longe os mais comuns e o código cru -# ("63003") não diz nada para quem está configurando. +# Dispara um WhatsApp de teste para o número administrativo, pelo provedor +# escolhido na tela (QR/Baileys ou Twilio). +# +# No Twilio, traduz o código de erro da API para uma instrução acionável em +# português — os erros de Sandbox (opt-in) são de longe os mais comuns e o +# código cru ("63003") não diz nada para quem está configurando. module Notificacao class TesteWhatsapp MENSAGENS = { @@ -26,7 +28,8 @@ module Notificacao def initialize(config = ConfiguracaoNotificacao.instancia, destino: nil) @config = config - @destino = ConfiguracaoNotificacao.canal(destino.presence || config.whatsapp_admin) + @numero = ConfiguracaoNotificacao.normalizar_telefone(destino.presence || config.whatsapp_admin) + @destino = @numero && ConfiguracaoNotificacao.canal(@numero) end def executar @@ -35,6 +38,21 @@ module Notificacao '(ex.: 11 92005-1157) antes de testar.') end + @config.baileys? ? via_qr : via_twilio + end + + private + + # Modo QR: quem responde é a ponte Baileys (container `whatsapp`). Os erros + # dela já vêm traduzidos pelo ClienteWhatsapp. + def via_qr + resposta = ClienteWhatsapp.padrao.enviar(para: @numero, texto: corpo) + return Resultado.falha(resposta.erro.to_s) unless resposta.ok? + + Resultado.sucesso("WhatsApp de teste enviado para #{@numero} pela ponte QR.") + end + + def via_twilio credenciais = @config.credenciais_whatsapp if credenciais.blank? return Resultado.falha('Marque "Ativar WhatsApp" e preencha Account SID, Auth Token e o ' \ @@ -63,11 +81,9 @@ module Notificacao Resultado.falha("Falha inesperada ao enviar (#{e.class}).", e.message) end - private - def corpo "✅ Teste do Reem Notas\n" \ - "Se você recebeu esta mensagem, a integração com o Twilio está funcionando.\n" \ + "Se você recebeu esta mensagem, o envio de WhatsApp está funcionando.\n" \ "Enviado em #{Time.current.strftime('%d/%m/%Y às %H:%M')}." end end diff --git a/app/views/admin/configuracao_notificacoes/show.html.erb b/app/views/admin/configuracao_notificacoes/show.html.erb index 62fdf5f..8714221 100644 --- a/app/views/admin/configuracao_notificacoes/show.html.erb +++ b/app/views/admin/configuracao_notificacoes/show.html.erb @@ -23,7 +23,7 @@ <%= icone :email, espaco: false %> Notificações e E-mail

- Servidor de e-mail (SMTP) e WhatsApp (Twilio) usados pelo sistema + Servidor de e-mail (SMTP) e WhatsApp usados pelo sistema

<%= link_to 'Voltar', admin_configuracoes_path, @@ -55,6 +55,7 @@ <% ilegiveis = [] %> <% ilegiveis << 'senha do SMTP' if @config.smtp_password_cifrado.present? && @config.smtp_password.blank? %> <% ilegiveis << 'token do Twilio' if @config.twilio_auth_token_cifrado.present? && @config.twilio_auth_token.blank? %> + <% ilegiveis << 'token da ponte do WhatsApp' if @config.baileys_token_cifrado.present? && @config.baileys_token.blank? %> <% if ilegiveis.any? %>
Não foi possível ler <%= ilegiveis.to_sentence %> que estava gravada no banco @@ -163,15 +164,15 @@
- <%# ────────────────────────── WHATSAPP / TWILIO ───────────────────────── %> + <%# ────────────────────────────── WHATSAPP ───────────────────────────── %>

- <%= icone :telefone, espaco: false %> WhatsApp (Twilio) + <%= icone :telefone, espaco: false %> WhatsApp

- Credenciais em Twilio Console → Account Info + Escolha quem entrega as mensagens

+ <%# Provedor: sem este seletor as colunas do Baileys existiam no banco mas + não havia como sair do Twilio pela tela — o pareamento por QR ficava + inalcançável. %> +
+
+ <%= f.label :whatsapp_provedor, 'Provedor', class: rotulo %> + <%= f.select :whatsapp_provedor, + [['QR code (grátis, não oficial)', 'baileys'], ['Twilio (oficial, pago)', 'twilio']], + {}, class: input %> +

+ No modo QR o pareamento é feito em + <%= link_to 'Notificações → WhatsApp', admin_whatsapp_sessao_path, + data: { turbo: false }, class: 'text-[#f97316] underline' %>. +

+
+ +
+ <%= f.label :whatsapp_intervalo_segundos, 'Intervalo entre mensagens (segundos)', class: rotulo %> + <%= f.number_field :whatsapp_intervalo_segundos, min: 0, max: 300, class: input %> +

+ Disparo em rajada é o que mais causa banimento no modo QR. Abaixo de 5s, só com poucos contatos. +

+
+
+ + <%# ── Modo QR (Baileys) ───────────────────────────────── %> +
+

Modo QR code

+

+ ⚠️ Canal não oficial — a Meta pode banir o número. + Use um chip dedicado. Em branco, os dois campos abaixo usam + BAILEYS_URL e + WHATSAPP_TOKEN do .env. +

+
+
+ <%= f.label :baileys_url, 'Endereço da ponte', class: rotulo %> + <%= f.text_field :baileys_url, class: "#{input} font-mono text-sm", + placeholder: ConfiguracaoNotificacao::BAILEYS_URL_PADRAO %> +
+
+ <%= f.label :baileys_token, 'Token da ponte', class: rotulo %> + <%= f.password_field :baileys_token, value: nil, autocomplete: 'new-password', + class: "#{input} font-mono text-sm", + placeholder: @config.baileys_token? ? '•••••••• (em branco mantém o atual)' : 'mesmo valor de WHATSAPP_TOKEN' %> +
+
+
+ +

Modo Twilio

+

Credenciais em Twilio Console → Account Info

+
<%= f.label :twilio_account_sid, 'Account SID', class: rotulo %>