Saltar a contenido

20 — Sistema de emails transaccionales

Resumen

El SaaS envía 15 tipos de email profesionales, todos en 5 idiomas (es / en / fr / de / pt), HTML responsive + texto plano (multipart MIME).

No hay FastAPI ni Celery para emails. Todo corre en Django vía EmailMultiAlternatives + SMTP configurado en .env.

Regla de idioma (importante)

Todos los emails usan users.locale — el idioma que el usuario tiene seleccionado en el client dashboard.

Flujo:

  1. El usuario cambia idioma con el selector de globo (header) → se guarda en users.locale vía PATCH /api/dashboard/profile/me/.
  2. También puede cambiarlo en Account → Profile → Language.
  3. Los webhooks Stripe (sin sesión de usuario) resuelven locale con resolve_bot_owner_locale(bot) → lee el locale del owner en users.
  4. Signup pasa locale explícito desde el formulario de registro.

Módulo central: backend_django/authentication/email_locale.py

resolve_user_locale(email)           # por email del usuario
resolve_account_owner_locale(account_id)
resolve_bot_owner_locale(bot_obj)  # webhooks Stripe
locale_from_request_user(user)     # requests autenticados

Configuración SMTP

Variables en .env:

EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=tu-email@gmail.com
EMAIL_HOST_PASSWORD=app-password-de-gmail
DEFAULT_FROM_EMAIL=BLIMX <noreply@tudominio.com>
FRONTEND_URL=https://app.tudominio.com

Test rápido:

cd backend_django && source venv/bin/activate
python manage.py shell <<'PY'
from authentication.utils import send_verification_email
send_verification_email('tu-email@gmail.com', 'test-token', locale='es')
PY

Catálogo completo de emails

Auth (authentication/utils.py)

# Email Trigger Locale
1 Verificación de cuenta POST /api/auth/signup/ Del formulario signup
2 Reenvío verificación POST /api/auth/resend-verification/ users.locale
3 Reset de contraseña POST /api/auth/password-reset-request/ users.locale

Pre-launch (authentication/emails_invitation.py)

# Email Trigger Locale
4 Solicitud recibida POST /api/auth/invitation-request/ Del formulario (campo locale)
5 Código aprobado Superadmin aprueba invitation_request.locale
6 Solicitud rechazada Superadmin rechaza invitation_request.locale

Ver 21-prelaunch-invitations.md.

Account (api/views_accounts.py)

# Email Trigger Locale
7 Verificación email account POST /api/account/email-verification/request/ locale_from_request_user
8 Cierre de cuenta POST /api/account/close/ locale_from_request_user

Billing — Stripe webhooks (authentication/emails_billing.py + emails_lifecycle.py)

# Email Evento Stripe Locale
9 Factura pagada (PDF) invoice.payment_succeeded users.locale del owner
10 Pago fallido invoice.payment_failed resolve_bot_owner_locale
11 Suscripción finalizada customer.subscription.deleted resolve_bot_owner_locale
12 Bienvenida plan activado checkout.session.completed / subscription.created resolve_bot_owner_locale
13 Plan cambiado customer.subscription.updated (plan_id distinto) resolve_bot_owner_locale

Bot users (authentication/emails_lifecycle.py)

# Email Trigger Locale
14 Invitación a colaborar en bot POST /api/dashboard/bot-users/ (create) Del formulario (locale del invitado)

Arquitectura de módulos

backend_django/authentication/
├── utils.py                 ← _send_transactional_email(), verify, reset
├── email_locale.py          ← resolve_user_locale(), resolve_bot_owner_locale()
├── emails_invitation.py     ← pre-launch (3 emails)
├── emails_lifecycle.py      ← welcome, account closed, plan changed, bot invite
└── emails_billing.py        ← subscription ended, payment failed

backend_django/api/
├── views_stripe_webhook.py  ← dispara emails 9-13
├── views_accounts.py        ← email 8
├── services/invoice_service.py ← email 9 (PDF + email localizado)

Cómo añadir un email nuevo (~30 min)

  1. Añade strings en 5 idiomas en el módulo correspondiente (emails_*.py).
  2. Usa _build_html_email() o _build_cta_html() para el HTML.
  3. Llama _send_transactional_email(subject, plain, html, email, lang, "EmailType").
  4. En la view/webhook, resuelve locale:
    from authentication.email_locale import locale_from_request_user, resolve_bot_owner_locale
    locale = locale_from_request_user(request.user)  # request autenticado
    # o
    locale = resolve_bot_owner_locale(bot_obj)       # webhook Stripe
    
  5. Documenta el email en esta tabla.

Frontend: persistir idioma

El selector de globo (LanguageSelector.tsx) hace:

i18n.changeLanguage(langCode)
await api.patch('/api/dashboard/profile/me/', { locale: langCode })

Al cargar el dashboard, DashboardLayout sincroniza i18n ← users.locale del perfil.

Lo que NO envía emails

Evento Por qué
Card expiring Stripe lo envía nativamente (activar en Customer Portal settings)
Lead capture Exterminado (Help Center, no ventas)
Chat / IA limit Exterminado

Troubleshooting

Ver 17-troubleshooting.md sección "Los emails no llegan".

Checklist rápido:

  1. .env tiene EMAIL_HOST_USER y EMAIL_HOST_PASSWORD correctos.
  2. users.locale tiene valor (SELECT email, locale FROM users LIMIT 5;).
  3. Logs: journalctl -u blimx-django -f | grep -i email.
  4. Gmail: usar App Password, no la password normal.