feat(F-153): completed feature

This commit is contained in:
chattie
2026-08-22 07:41:08 +02:00
parent 569815fd87
commit 4b799f5c1b
16 changed files with 418 additions and 141 deletions

View File

@@ -1,62 +1,49 @@
# F-152 — Tech Spec
# F-153 — Tech Spec
## Principios
- Best-effort: un email fallido o SMTP no configurado **nunca** debe fallar el
registro ni el webhook de pagos. Se loguea y se continúa.
- Reusar infraestructura existente: SMTP desde `store_settings`
(`smtp_host/port/secure/user/pass/from`), mismo patrón que
`SettingsPasswordResetMailer` y `sendOrderStatusEmail`.
- Boundaries: identity no importa orders/payments a nivel de dominio; el wiring
del order-confirmation email se hace en el *route handler* de payments (posee
`deps.pool`), reusando `sendOrderStatusEmail` exportado públicamente por
`orders/index.ts`.
## Principles
- Associate the customer email to the order **read model** (not a per-request hack):
`OrderView.email` is resolved once by the orders repository via a `LEFT JOIN identity_users`.
- Reuse the existing SQL pattern: `orders/infrastructure/pg-order-repository.ts` `search`
already does `LEFT JOIN identity_users u ON u.id = o.user_id` — F-153 extends that to every
order read so the email is always available on the view.
- Boundaries: `identity_users` is referenced only as a **SQL table name** (pre-existing in
`search`); no TypeScript import crosses the identity/orders boundary. `identity` does not
import orders; orders references `identity_users` table name (string) at infrastructure.
- NoUncheckedIndexedAccess is ON → index access returns `T | undefined`; use `!` or `?? null`
when mapping rows.
- Backward compatible: `email` is an additive field on the serialized output; no state
transition, no new migration, no endpoint change.
## Cambios
## Changes
### 1. Order confirmation on PaymentSucceeded (payments)
- `orders/index.ts`: exportar `sendOrderStatusEmail` (y `ORDER_STATE_LABELS`).
- `payments/api/payments.routes.ts`: en el handler de `/payments/webhook`, tras
`const outcome = await service.handleWebhook(event)`, si
`event.type === 'PaymentSucceeded' && event.orderId && outcome.kind === 'processed'`,
fetch customer email (`identity_users.email` via `orders_orders.user_id`) y
`sendOrderStatusEmail(deps.pool, { to, orderId: event.orderId, state: 'PAID' })`
dentro de try/catch; log de advertencia si falla SMTP/no-config.
### 1. Domain — OrderView carries email
- `orders/domain/order.ts`: add `email: string | null;` to `OrderView` (`Order` itself unchanged —
email is a denormalized read-model attribute, not a core domain field).
### 2. Welcome email on registration (identity)
- `identity/domain/ports.ts`: nuevo puerto `WelcomeMailer` con
`sendWelcome(input: { email: string; name?: string }): Promise<void>` (name es
opcional: `User` no almacena nombre en el dominio actual).
- `identity/infrastructure/settings-welcome-mailer.ts`: `SettingsWelcomeMailer(pool)`
modelado en `SettingsPasswordResetMailer` — lee SMTP de `store_settings`, usa
`nodemailer`, y una función pura `buildWelcomeEmail` (verificable sin SMTP).
El subject (`¡Bienvenido a Mercado de Vida!`) coincide con la plantilla
`account_created` de notificaciones. Lanza si SMTP no está configurado.
- `identity/api/identity.routes.ts`: `IdentityRoutesDeps.welcomeMailer?: WelcomeMailer`;
en el handler de `POST /auth/register`, tras `registerUser.execute` exitoso, se
despacha el welcome 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 por email. `RegisterUser` se mantiene sin
depender de email (puro orquestación de dominio).
- `identity/index.ts`: re-exporta `SettingsWelcomeMailer` (y
`SettingsPasswordResetMailer`) para que `build-app.ts` los importe desde el
index en lugar de deep-importar infra (cumple R2 de boundaries).
- `notifications/domain/notification.ts` + `notifications/api/notifications.routes.ts`:
añadir `account_created` al union `EmailTemplate`, a los mapas SUBJECTS/BODIES
de `LoggingEmailProvider` y al enum del schema de `POST /notifications/dispatch`.
### 2. Infrastructure — resolve email in the repo
- `orders/infrastructure/pg-order-repository.ts`:
- Add `email: string | null` to `OrderRow`.
- Add helper `toOrderView(row, items): OrderView = { ...toOrder(row), email: row.email, items }`.
- `SELECT o.*``SELECT o.*, u.email` with `LEFT JOIN identity_users u ON u.id = o.user_id`
in `findById`, `findByIdAndUserId`, `findAll`, `search`.
- Build every returned `OrderView` via `toOrderView` (so `email` is always set; `null` when the
order has no linked identity_user).
## SMTP / store_settings
Claves existentes: `smtp_host, smtp_port, smtp_secure, smtp_user, smtp_pass, smtp_from`.
El welcome mailer reusa exactamente estas claves.
### 3. API — expose + consume email
- `orders/api/orders.routes.ts`:
- `serializeOrder`: add `email: string | null` to the param type and to the output
(`email: order.email`).
- Admin force-transition (`POST /orders/:id/transitions/admin`): replace the inline
`SELECT email FROM identity_users WHERE id = order.userId` with `const to = order.email;`
(single source of truth; the LEFT JOIN already resolved it). Keep the try/catch + warn
and the "El cliente no tiene email asociado" fallback (now only when `order.email` is null).
## Testing
- `settings-welcome-mailer.test.ts`: `buildWelcomeEmail` (greeting, nombre, XSS),
`SettingsWelcomeMailer.sendWelcome` (nodemailer mockeado: sendmail called once
con recipient == email y subject == plantilla `account_created`), y lanza cuando
SMTP no está configurado (`SMTP is not configured`).
- `orders/tests/orders-index.test.ts`: el barrel de `orders/index.ts` re-exporta
`sendOrderStatusEmail`, `buildOrderStatusEmail`, `ORDER_STATE_LABELS`.
- `order-status-mailer.test.ts` ya existe cubriendo `buildOrderStatusEmail` (PAID
pertenece a `ORDER_STATE_LABELS`).
- `tsc --noEmit` limpio; `prettier --check` y `eslint` limpios en los archivos tocados;
suite completa de vitest verde (197 tests); `lint:boundaries` sin nuevas violaciones.
- verify.sh green.
- New `orders/infrastructure/pg-order-repository.test.ts`: mock `pg.Pool`, assert `findById`
returns `email` from the JOIN when the linked identity_user has one, and `null` when there is
no linked user.
- Update existing `OrderView` literals in tests (`order-service`, `payments-service`,
`checkout-service`) to include `email: null` (additive field).
- `order-status-mailer.test.ts` (existing) remains green (no change to email senders).
- tsc --noEmit clean; prettier + eslint clean; lint:boundaries no new violations;
vitest run full suite green; verify.sh green.