O Wedding Finance Planner envia lembretes e avisos por dois canais independentes que funcionam em paralelo: email (SMTP via Nodemailer) e WhatsApp (Baileys embutido).
Cada notificação é renderizada no idioma do destinatário (User.locale)
desde a v0.5.0. Os templates em
src/lib/notifications/templates.ts
são async e recebem locale: Locale em cada variante de
RenderInput. O orquestrador notify() aceita target.locale opcional;
quando não passado, usa EventSettings.defaultLocale.
Importante: dentro do template nunca chame getLocale() do
next-intl. Em cron jobs e webhooks, ele retorna o default porque não há
request lifecycle do destinatário. Sempre propague o locale lido do
banco. Veja i18n.md para o padrão completo.
| Evento | Quando dispara | ||
|---|---|---|---|
ACCOUNT_CREATED |
Admin cria conta de outro usuário | ✅ | ✅ |
PASSWORD_RESET |
Usuário pede reset (link expira em 60 min) | ✅ | ✅ |
PASSWORD_RESET_BY_ADMIN |
Admin redefine senha de outro usuário | ✅ | ✅ |
PAYMENT_DUE |
Pagamento vence em até 3 dias | ✅ | ✅ |
PAYMENT_OVERDUE |
Pagamento já passou da data | ✅ | ✅ |
TASK_DUE |
Tarefa vence em até 2 dias | ✅ | ✅ |
TASK_OVERDUE |
Tarefa já passou da data | ✅ | ✅ |
GUEST_RSVP |
Convidado responde o RSVP (individual ou de grupo) | ✅ (gestores/noivos) | ✅ |
SYSTEM_WHATSAPP_DOWN |
Conexão WhatsApp caiu por > ~1 min, ou exige novo QR | ✅ (admins) | — |
SYSTEM_WHATSAPP_RECOVERED |
Conexão WhatsApp voltou após queda já avisada | ✅ (admins) | — |
GUEST_RSVP vai para todos os usuários ativos com role ADMIN/GROOM/BRIDE/PLANNER
(reusa o mesmo conjunto do cron de lembretes). É best-effort e deduplicado por dia
por refId (o convidado/grupo só gera uma notificação por dia, mesmo reabrindo o link).
O disparo é em notifyRsvpResponse (src/lib/notifications/rsvp.ts),
chamado por publicRsvpRespond e publicRsvpRespondForGroup.
Cada envio é registrado em NotificationLog:
status = SENT |
FAILED |
errorMsg em caso de falhakind + refType + refId permitem idempotência por dia (um pagamento
X nunca recebe duas notificações PAYMENT_DUE no mesmo dia).Gestores (ADMIN/GROOM/BRIDE) veem em Ajustes › Notificações os últimos 50 envios
(NotificationLog) com tipo, canal, destinatário, status (SENT/FAILED) e a mensagem de
erro do SMTP. Use isso para diagnosticar, por exemplo, falhas recorrentes quando o admin
ainda está com o e-mail placeholder admin@admin.com do seed — troque o e-mail em
Ajustes › Time (qualquer usuário) ou em Perfil (o seu, com confirmação de senha).
Pode ser feita em duas formas:
.env (recomendado)SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false # true para porta 465
SMTP_USER=seu@gmail.com
SMTP_PASS=sua-app-password # NÃO use a senha normal do Gmail
SMTP_FROM="\"Wedding Finance\" <noreply@seudominio.com>"
APP_URL=https://seu.dominio # usado para montar links em emails
Acesse Ajustes › Casamento — botão “Configurar SMTP”.
SMTP_PASS.⚠️ Não funciona com a senha normal da conta. Google exige App Password para clientes SMTP desde 2022.
| Provedor | Host | Porta | Notas |
|---|---|---|---|
| Gmail | smtp.gmail.com | 587 | precisa App Password |
| Outlook/Office 365 | smtp.office365.com | 587 | basic auth costuma estar desativado — use OAuth ou SMTP relay |
| SendGrid | smtp.sendgrid.net | 587 | username = apikey |
| Amazon SES | email-smtp.us-east-1.amazonaws.com | 587 | gerar credenciais SMTP no console |
| Mailgun | smtp.mailgun.org | 587 | usar domínio sandbox para testes |
💡 Importante: o sistema usa Baileys, um cliente WhatsApp Web não oficial. Ele abre uma sessão paralela à do seu celular (semelhante a usar WhatsApp Web). Não é WhatsApp Business API.
/dashboard/settings logado como ADMIN../.whatsapp-auth/ (gitignored — não commitar).Botão Enviar teste dispara uma mensagem para o número do próprio admin.
Se a sessão expirar (acontece a cada algumas semanas, ou se você desconectar do celular), repita os passos acima. O sistema continua funcionando — apenas o WhatsApp para de enviar até reconectar.
A integração sobe sozinha junto com o servidor (hook
src/instrumentation.ts do Next.js 16). Se a
conexão cair, o socket é reiniciado em back-off exponencial: 3s, 6s, 12s,
24s, 48s e teto de 60s. Um watchdog roda a cada 60s e força o restart se
algum erro inesperado interromper a cadeia de reconexão.
Além disso, os admins ativos recebem email automaticamente quando:
🔌 WhatsApp instável.⚠ Ação necessária.⚠ Ação necessária.✅ WhatsApp voltou.Anti-spam: cada queda gera no máximo 1 email DOWN por dia (idempotência
via NotificationLog + cooldown de 30 min em memória) e 1 email RECOVERED
por dia, e somente se houve um DOWN antes no mesmo dia.
Variável de ambiente opcional WHATSAPP_AUTOSTART="false" desliga o autostart
(útil em dev).
Os lembretes recorrentes (PAYMENT_DUE, PAYMENT_OVERDUE, TASK_*) são
disparados pelo endpoint:
GET /api/cron/reminders
Authorization: Bearer <CRON_SECRET>
Não há scheduler embutido — você precisa configurar um cron externo que chame esse endpoint periodicamente (sugestão: a cada 30 minutos).
Para evitar serializar dezenas de queries em loop, o handler:
Promise.all com seis queries paralelas: lista de
destinatários (users ativos com role notificável), idempotência
batched (loadNotifiedTodaySet agrega um findMany por kind+refId+refType
no NotificationLog do dia) e os quatro recortes de pagamentos/tarefas
(vencendo, vencidos × payments, tasks).Set de já-notificados — sem ida ao banco
por iteração.Promise.all dentro do mesmo
evento. Antes era serial (for ... await), o que multiplicava latência
por número de destinatários.A janela de “hoje” usa o helper startOfTodayBRT() em
src/lib/notifications/log.ts — UTC-3,
sem depender do timezone do processo Node. O cron e a função
wasNotifiedToday usam a mesma origem de “início do dia”, então a
idempotência não vaza entre dias mesmo se o servidor estiver em UTC.
Antes do hardening,
wasNotifiedTodayusavanew Date().setHours(0,…)(timezone local do processo) enquanto o cron já operava em BRT — janelas deslocadas podiam disparar lembrete duplicado ao virar dia em UTC.
*/30 * * * * curl -fsS -H "Authorization: Bearer SEU_CRON_SECRET" \
http://localhost:3005/api/cron/reminders >> /var/log/wedding-cron.log 2>&1
Veja a seção 8 de instalacao-windows.md.
timingSafeEquals
(src/lib/timing-safe.ts).openssl rand -hex 32 (Linux/Mac)node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" (qualquer)Templates de email vivem em src/lib/notifications/templates.ts (ou diretório similar). Para alterar visual/copy, edite lá. Sempre passe dados de usuário por um helper de escape de HTML quando montar emails — XSS em emails é vetor real (alguns clientes renderizam HTML).
Veja troubleshooting.md.
NotificationLog no banco (npm run db:studio).SMTP_* estão setadas (Settings › Casamento).NotificationLog).Se o mesmo lembrete não chega: verifique se NotificationLog já tem entrada
de sucesso para hoje. Se sim, foi pulado por design.
sendWhatsApp(phone, text, media?) e sendEmail({ ..., attachments? }) aceitam
mídia: imagens viram image com legenda (WhatsApp) / inline via cid
(e-mail); PDFs viram document / anexo. O orquestrador notify() repassa via
options.media.
O kind SAVE_THE_DATE (template em notifications/templates.ts) é o primeiro
consumidor: aviso da data com arte anexada, variáveis {nomes}/{convidados}/{data}/{local}
e linhas de site/lista de presentes condicionais. O disparo em massa usa a fila
Broadcast/BroadcastRecipient drenada por um worker com throttle
(broadcast-worker.ts, env BROADCAST_INTERVAL_MS) e o backstop
GET /api/cron/broadcast. Detalhes em save-the-date.md.