6.2 KiB
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/webhook→PaymentSucceeded) no se enviaba el email de confirmación de orden (estadoPAID).
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>(nameopcional: el dominioUserno almacena nombre).
- Nuevo puerto
project/src/modules/identity/infrastructure/settings-welcome-mailer.ts(nuevo)SettingsWelcomeMailer(pool)— lee SMTP destore_settings, usanodemailer, y función purabuildWelcomeEmail(verificable sin SMTP). Subject¡Bienvenido a Mercado de Vida!coincide con la plantillaaccount_created. LanzaSMTP is not configuredcuando falta SMTP.
project/src/modules/identity/api/identity.routes.tsIdentityRoutesDeps.welcomeMailer?: WelcomeMailer; en el handler dePOST /auth/registerse 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(ySettingsPasswordResetMailer) para quebuild-app.tslos importe desde el index (cumple R2 de boundaries).
- Re-exporta
project/src/app/build-app.tswelcomeMailer: new SettingsWelcomeMailer(deps.pool)inyectado enIdentityRoutesDeps.
Payments — order confirmation
project/src/modules/payments/api/payments.routes.ts- En el handler de
POST /payments/webhook, trasconst outcome = await service.handleWebhook(event), sievent.type === 'PaymentSucceeded' && event.orderId && outcome.kind === 'processed'se resuelve el email del cliente (orders_orders.user_id→identity_users.email) y se llamasendOrderStatusEmail(deps.pool, { to, orderId, state: 'PAID' })dentro de try/catch (log de advertencia si falla SMTP/no-config). El gateoutcome.kind === 'processed'asegura envío único frente a webhooks duplicados (PaymentsService devuelve{ kind: 'duplicate' }antes de cualquier transición de estado).
- En el handler de
Orders — barrel
project/src/modules/orders/index.ts- Re-exporta
sendOrderStatusEmail,buildOrderStatusEmail,ORDER_STATE_LABELSytype OrderStatusNotificationInputpara que payments los consuma sin deep-import (boundary clean).
- Re-exporta
Notifications — consistencia de plantilla
project/src/modules/notifications/domain/notification.ts- Añadido
account_createdal unionEmailTemplatey a los mapas SUBJECTS/BODIES deLoggingEmailProvider.
- Añadido
project/src/modules/notifications/infrastructure/log-email-provider.ts- Añadido
account_createdaSUBJECTSyBODIES.
- Añadido
project/src/modules/notifications/api/notifications.routes.ts- Añadido
account_createdal enum del schema dePOST /notifications/dispatch.
- Añadido
Tests
project/src/modules/identity/infrastructure/settings-welcome-mailer.test.ts(nuevo, 5 tests)buildWelcomeEmailincluye el email en el greeting y HTML-escapea el nombre (XSS-safe).SettingsWelcomeMailer.sendWelcome(nodemailer mockeado): asserta quesendMailse llama una sola vez conto == emailysubject == '¡Bienvenido a Mercado de Vida!'(plantillaaccount_created).- Lanza
SMTP is not configuredcuando no hay SMTP (readSmtpOptions).
project/src/modules/orders/tests/orders-index.test.ts(nuevo, 1 test)- El barrel de
orders/index.tsre-exportasendOrderStatusEmail,buildOrderStatusEmail,ORDER_STATE_LABELSy el tipo.
- El barrel de
order-status-mailer.test.tscubrebuildOrderStatusEmail/ORDER_STATE_LABELS(PAIDincluido) — 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, peroRegisterUserno posee logger.identity.routes.tssí tienerequest.log. Dispachar allí best-effort (fire-and-forget+.catch(request.log.warn)) satisface observabilidad y best-effort, siguiendo el precedente desendOrderStatusEmailen payments.RegisterUserse 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. buildWelcomeEmailes pura + XSS-safe:namese HTML-escapea (unit test) para que la personalización no inyecte markup en el body.- Path gotcha
./domainvs../domain: el reader tool renderizóorders/index.tscomo../domain/order.jscuando el archivo real usa un solo punto./domain/order.js(confirmado conod -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).