Files
mercadodevida/work/artifacts/F-152/implementer.md
2026-08-22 07:09:03 +02:00

6.2 KiB

F-152 — Implementer evidence

Problema

Los emails transaccionales de creación de cuenta y confirmación de orden no se enviaban:

  • Al registrarse un usuario (POST /auth/register) no se disparaba el email de bienvenida (account_created).
  • Al confirmarse un pago (POST /payments/webhookPaymentSucceeded) no se enviaba el email de confirmación de orden (estado PAID).

SMTP se lee de store_settings (smtp_host/port/secure/user/pass/from) y la capa de envío reusa nodemailer y el patrón de sendOrderStatusEmail / LoggingEmailProvider.

Cambios

Identity — welcome email

  • project/src/modules/identity/domain/ports.ts
    • Nuevo puerto WelcomeMailer: sendWelcome(input: { email: string; name?: string }): Promise<void> (name opcional: el dominio User no almacena nombre).
  • project/src/modules/identity/infrastructure/settings-welcome-mailer.ts (nuevo)
    • SettingsWelcomeMailer(pool) — lee SMTP de store_settings, usa nodemailer, y función pura buildWelcomeEmail (verificable sin SMTP). Subject ¡Bienvenido a Mercado de Vida! coincide con la plantilla account_created. Lanza SMTP is not configured cuando falta SMTP.
  • project/src/modules/identity/api/identity.routes.ts
    • IdentityRoutesDeps.welcomeMailer?: WelcomeMailer; en el handler de POST /auth/register se despacha el email fire-and-forget (void mailer.sendWelcome(...).catch(request.log.warn(...))). Best-effort: un fallo SMTP se loguea (warning) y se traga; el registro nunca se rompe.
  • project/src/modules/identity/index.ts
    • Re-exporta SettingsWelcomeMailer (y SettingsPasswordResetMailer) para que build-app.ts los importe desde el index (cumple R2 de boundaries).
  • project/src/app/build-app.ts
    • welcomeMailer: new SettingsWelcomeMailer(deps.pool) inyectado en IdentityRoutesDeps.

Payments — order confirmation

  • project/src/modules/payments/api/payments.routes.ts
    • En el handler de POST /payments/webhook, tras const outcome = await service.handleWebhook(event), si event.type === 'PaymentSucceeded' && event.orderId && outcome.kind === 'processed' se resuelve el email del cliente (orders_orders.user_ididentity_users.email) y se llama sendOrderStatusEmail(deps.pool, { to, orderId, state: 'PAID' }) dentro de try/catch (log de advertencia si falla SMTP/no-config). El gate outcome.kind === 'processed' asegura envío único frente a webhooks duplicados (PaymentsService devuelve { kind: 'duplicate' } antes de cualquier transición de estado).

Orders — barrel

  • project/src/modules/orders/index.ts
    • Re-exporta sendOrderStatusEmail, buildOrderStatusEmail, ORDER_STATE_LABELS y type OrderStatusNotificationInput para que payments los consuma sin deep-import (boundary clean).

Notifications — consistencia de plantilla

  • project/src/modules/notifications/domain/notification.ts
    • Añadido account_created al union EmailTemplate y a los mapas SUBJECTS/BODIES de LoggingEmailProvider.
  • project/src/modules/notifications/infrastructure/log-email-provider.ts
    • Añadido account_created a SUBJECTS y BODIES.
  • project/src/modules/notifications/api/notifications.routes.ts
    • Añadido account_created al enum del schema de POST /notifications/dispatch.

Tests

  • project/src/modules/identity/infrastructure/settings-welcome-mailer.test.ts (nuevo, 5 tests)
    • buildWelcomeEmail incluye el email en el greeting y HTML-escapea el nombre (XSS-safe).
    • SettingsWelcomeMailer.sendWelcome (nodemailer mockeado): asserta que sendMail se llama una sola vez con to == email y subject == '¡Bienvenido a Mercado de Vida!' (plantilla account_created).
    • Lanza SMTP is not configured cuando no hay SMTP (readSmtpOptions).
  • project/src/modules/orders/tests/orders-index.test.ts (nuevo, 1 test)
    • El barrel de orders/index.ts re-exporta sendOrderStatusEmail, buildOrderStatusEmail, ORDER_STATE_LABELS y el tipo.
  • order-status-mailer.test.ts cubre buildOrderStatusEmail / ORDER_STATE_LABELS (PAID incluido) — preexistente, sin tocar.

Verificación

tsc --noEmit (project/tsconfig.json)   ✅ 0 errores
prettier --check (archivos tocados)    ✅ All matched files use Prettier code style
eslint  (archivos tocados)             ✅ 0 errores
lint:boundaries (node scripts/check-module-boundaries.mjs src)  ✅ sin nuevas violaciones (queda SOLO la R1 preexistente de security.routes, ajena a F-152)
vitest run (suite completa)            ✅ 197 passed | 56 skipped (253)
verify.sh                              ✅ exit 0
git diff --check                       ✅

Decisiones

  • Dispatch del welcome email en el route handler, no en RegisterUser: la aceptación exige "se loguea un warning" ante fallo SMTP, pero RegisterUser no posee logger. identity.routes.ts sí tiene request.log. Dispachar allí best-effort (fire-and-forget + .catch(request.log.warn)) satisface observabilidad y best-effort, siguiendo el precedente de sendOrderStatusEmail en payments. RegisterUser se mantiene puro (orquestación de dominio).
  • Gate outcome.kind === 'processed' garantiza idempotencia frente a webhooks duplicados (PaymentsService devuelve { kind: 'duplicate' } antes de cualquier transición). Evita tabla de dedup adicional.
  • buildWelcomeEmail es pura + XSS-safe: name se HTML-escapea (unit test) para que la personalización no inyecte markup en el body.
  • Path gotcha ./domain vs ../domain: el reader tool renderizó orders/index.ts como ../domain/order.js cuando el archivo real usa un solo punto ./domain/order.js (confirmado con od -c/python3 repr). El re-export final se valida con typecheck (import resuelto correctamente).

Estado runtime

No hay endpoint HTTP nuevo verificable en vivo más allá de los tests unitarios; la entrega se valida por: (a) welcome email enviado al email registrado según el test de SettingsWelcomeMailer (nodemailer mockeado), (b) orden transita a PAID y se envía sendOrderStatusEmail({ state: 'PAID' }) tras PaymentSucceeded con outcome.kind === 'processed' (gate de dedup).