feat(F-152): completed feature
This commit is contained in:
116
work/artifacts/F-152/implementer.md
Normal file
116
work/artifacts/F-152/implementer.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 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 (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_id` →
|
||||
`identity_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
|
||||
|
||||
```text
|
||||
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).
|
||||
Reference in New Issue
Block a user