From e3d4546c3c28e33e16a39bc7571b9ff7e84012d7f41cb8cbacada2a7beba94f8 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 11 Aug 2026 23:42:00 -0300 Subject: [PATCH] =?UTF-8?q?Adi=C3=A7=C3=A3o=20da=20fun=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20configura=C3=A7=C3=A3o=20de=20email=20e=20whatts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/README.md b/README.md index f877f1d..2607905 100644 --- a/README.md +++ b/README.md @@ -1786,3 +1786,161 @@ app/views/admin/configuracoes/index.html.erb # idem (arquivo distint +--- + +
+✉️ Notificações e E-mail configuráveis pela tela — SMTP + WhatsApp (11/08/2026) + +> ⚠️ **STATUS: implementado, ainda NÃO executado.** Nada aqui foi rodado contra um banco nem +> contra os servidores reais (Gmail/Twilio) — não há Ruby nem Postgres na máquina de +> desenvolvimento. Foi conferida a sintaxe de todos os arquivos `.rb` e `.erb` e validado o +> algoritmo de normalização de telefone em Ruby puro. **A migration, a suíte e o envio real +> continuam pendentes** — roteiro no fim desta seção. + +### 🎯 O problema +Servidor de e-mail e credenciais do Twilio viviam **só no `.env`**: trocar a senha de app do Gmail +ou o número remetente exigia editar o arquivo no servidor e **reiniciar o container**. O ADM não +tinha como fazer nada disso pela interface. + +Pior: as chaves `notificacao_whatsapp` e `notificacao_email` existiam em `configuracoes` mas +**nunca tiveram UI** — `Admin::ConfiguracoesController#index` filtra por `CHAVES_MOEDA`. Ligar +notificação só era possível pelo `rails console`. + +### 🆕 A tela +**Configurações → card "Notificações e E-mail"** (`/admin/configuracao_notificacao`). +**Só `admin`** — `ConfiguracaoNotificacaoPolicy` é mais restrita que `ConfiguracaoPolicy`, que +libera `index?` para gerente: aqui ficam senha de e-mail e token do Twilio. + +O acesso é **exclusivamente pelo card dentro de Configurações** — de propósito não há item no menu +lateral, para não expor um atalho de credenciais na navegação de todo dia. + +| Bloco | Campos | +|---|---| +| **Servidor de e-mail (SMTP)** | ativo, servidor, porta, usuário, senha, autenticação, domínio, e-mail e nome do remetente + toggle "avisar motoristas por e-mail" | +| **WhatsApp (Twilio)** | ativo, Account SID, Auth Token, número remetente | +| **Destinatários administrativos** | e-mail do admin, WhatsApp do admin | + +Três botões: **Salvar**, **Salvar e enviar e-mail de teste**, **Salvar e enviar WhatsApp de teste**. + +### ⚙️ Como funciona (técnico) — os 6 pontos não-óbvios + +**1. Hierarquia banco > `.env`, sem quebrar nada.** `ConfiguracaoNotificacao#smtp_settings` devolve +**`nil`** quando não está pronto. O ActionMailer faz `.merge(options || {})` por cima do que o +`config/initializers/smtp.rb` montou no boot — então o fallback para o `.env` é **automático**. +Enquanto os toggles estiverem desligados, o comportamento é **idêntico ao de antes desta tela**. + +**2. `default delivery_method_options:`, NÃO um `before_action`.** Um callback que mexesse em +`message.delivery_method` seria **descartado**: `ActionMailer::Base#mail` roda *depois* dos +callbacks e chama `wrap_delivery_behavior!`, que reconfigura a mensagem. O +`delivery_method_options` é lido *dentro* do próprio `mail()`. + +**3. `proc`, NÃO lambda.** O Devise avalia o `default from:` com `instance_eval(&proc)`, que passa +1 argumento. Um `-> { }` de aridade 0 estouraria **`ArgumentError` em todo "esqueci minha senha"**. + +**4. `config.parent_mailer = 'ApplicationMailer'`** no `devise.rb` — sem isso o reset de senha +continuaria preso ao `.env`. Efeito colateral aceito: os e-mails do Devise passam a usar +`app/views/layouts/mailer.html.erb`. + +**5. Segredos cifrados sem `master.key`.** Senha SMTP e Auth Token vão para colunas +`*_cifrado` (AES-256-GCM) via `AtributoCifrado`, com chave derivada do `secret_key_base`. **Não** se +usou ActiveRecord Encryption: exigiria 3 chaves novas, dependeria da ordem dos initializers e +estouraria `Errors::Decryption` na leitura. Aqui o reader faz `rescue → nil`, o app **degrada para +o `.env`** e a tela mostra um banner amarelo pedindo para redigitar. + +**6. Testes com `deliver_now` e `raise_delivery_errors = true` forçado.** O `development.rb` define +`raise_delivery_errors = false` e o adapter do ActiveJob é `:async` (thread in-process) — com +`deliver_later` **o teste "passaria" em silêncio mesmo com a senha errada**. + +### 🩹 Bug pré-existente corrigido de passagem +`NotificacaoService` mandava `to: "whatsapp:#{user.telefone}"` com o telefone **cru do cadastro**. +Um telefone gravado como `(11) 92005-1157` vira `whatsapp:(11) 92005-1157`, o Twilio devolve +**21211** — e o `rescue` engolia. **Provavelmente nenhum WhatsApp a motorista jamais chegou.** +Agora passa por `ConfiguracaoNotificacao.canal`, que normaliza para E.164. + +> Casos cobertos: `11 920051157`, `(11) 92005-1157`, `011 …`, `+55 11 …`, `whatsapp:+55…` → +> `+5511920051157`. O prefixo `55` só é removido quando sobra número demais — senão quebraria o +> **DDD 55** (Santa Maria/RS), onde `55991234567` já é o número completo. + +### 🔐 Segurança +- A senha gravada **nunca volta para o HTML** (`password_field value: nil`). Campo em branco + significa "mantenha a atual" — salvar sem redigitar não apaga o que está lá. +- `AuditoriaLog` registra a mudança com `acao: 'editar_notificacoes'`, mas grava apenas + `smtp_password_definida: true/false`. **Nunca a senha nem o token** — `dados_novos` é exibido em + `/admin/auditoria_logs`, que **gerente também acessa**. +- Os erros do Twilio vêm traduzidos (63003/63015 = falta o `join ` do sandbox, 21608 = + conta trial só envia a número verificado, 20003 = SID/token inválidos…), para o ADM resolver + sozinho sem abrir o log do container. + +### 🆕 Migration adicionada (rodar `db:migrate`) +``` +20260811000001_create_configuracao_notificacoes.rb +``` +Tabela **singleton** (índice único em `singleton_guard` — impede dois workers Puma criarem linhas +concorrentes). Copia as flags antigas de `configuracoes` e **não** importa credenciais do `.env`: +os campos nascem vazios e o fallback segue mandando até alguém preencher a tela. + +### 📂 Arquivos +``` +db/migrate/20260811000001_create_configuracao_notificacoes.rb # NOVO — tabela singleton +app/models/configuracao_notificacao.rb # NOVO — smtp_settings, credenciais, E.164 +app/models/concerns/atributo_cifrado.rb # NOVO — AES-256-GCM sem master.key +app/policies/configuracao_notificacao_policy.rb # NOVO — show?/update? = admin +app/controllers/admin/configuracao_notificacoes_controller.rb # NOVO — show/update + params[:acao] +app/views/admin/configuracao_notificacoes/show.html.erb # NOVO — a tela +app/services/notificacao/resultado.rb # NOVO — ok?/mensagem/detalhe → flash +app/services/notificacao/teste_email.rb # NOVO — erros SMTP em português +app/services/notificacao/teste_whatsapp.rb # NOVO — códigos Twilio em português +app/services/notificacao/cliente_twilio.rb # NOVO — client com timeout de 15s +app/mailers/teste_mailer.rb · app/views/teste_mailer/teste.html.erb # NOVO +app/services/notificacao_service.rb # lê do banco; unifica os 2 pares duplicados +app/mailers/application_mailer.rb # default from: / delivery_method_options: proc +app/mailers/consolidacao_mailer.rb # removido o `default from:` que anulava o proc +config/initializers/devise.rb # + parent_mailer +config/initializers/smtp.rb # vira fallback (só comentário) +config/initializers/inflections.rb # + irregular 'notificacao' +config/routes.rb # + resource :configuracao_notificacao +app/views/admin/configuracoes/index.html.erb # + card "Notificações e E-mail" (só admin) +app/models/configuracao.rb # marca notificacao_* como obsoletas +.env.example # hierarquia banco > .env + NOTIFICACAO_SECRET +spec/models/configuracao_notificacao_spec.rb # NOVO +spec/policies/configuracao_notificacao_policy_spec.rb # NOVO +spec/requests/admin/configuracao_notificacoes_spec.rb # NOVO +spec/models/table_names_spec.rb # + ConfiguracaoNotificacao +``` + +### ⏳ Pendente de execução — roteiro +```bash +# 1. Migrar +docker-compose exec app bundle exec rails db:migrate + +# 2. Suíte +docker-compose exec app bundle exec rspec spec/models spec/policies spec/requests + +# 3. Confirmar a assinatura da gem (não pôde ser verificada fora do container) +docker-compose exec app bundle exec rails runner \ + 'p Twilio::HTTP::Client.instance_method(:initialize).parameters' +``` +4. **Permissão:** logar como **gerente** → o card não aparece e `/admin/configuracao_notificacao` + redireciona com "Você não tem permissão". Como **admin** → a tela abre. +5. **E-mail:** `smtp.gmail.com`, porta 587, **Senha de app de 16 caracteres** (não a senha da + conta), marcar "Ativar envio de e-mail" → **Salvar e enviar e-mail de teste**. Conferir o spam. +6. **WhatsApp:** SID / Auth Token / número remetente do Twilio, marcar "Ativar WhatsApp" → + **Salvar e enviar WhatsApp de teste**. ⚠️ **No sandbox, o número que vai RECEBER precisa antes + mandar `join `** para o número do sandbox — sem esse opt-in volta 63003/63015. +7. **Não regressão:** com os toggles desligados, finalizar uma consolidação e conferir que nada + mudou; e testar o **"Esqueci minha senha"**, que trocou de mailer pai. + +### ⚠️ Alertas +- **Rotação do `SECRET_KEY_BASE` torna senha e token ilegíveis.** O sistema não quebra (volta ao + `.env` e avisa na tela), mas os dois campos precisam ser redigitados. Para desacoplar, defina + **`NOTIFICACAO_SECRET`** no `.env` com uma string longa e **fixa**. +- **Não cachear a config em `Rails.cache`:** o `production.rb` usa `:memory_store`, que é por + processo — a tela pareceria "não salvar" para os outros workers. É 1 `SELECT` por e-mail. +- `app/views/configuracoes/index.html.erb` (fora do `admin/`) **não recebeu o card**: não tem rota + e é código morto — o vivo é `app/views/admin/configuracoes/index.html.erb`. + +> **Migration nova** (`db:migrate` obrigatório) e **sem gem nova** — `twilio-ruby` já estava no +> Gemfile. ⚠️ Reiniciar o Puma após o deploy. + +
+