feat(F-152): completed feature

This commit is contained in:
chattie
2026-08-22 07:09:03 +02:00
parent 2e9ddf4476
commit b008df4f37
25 changed files with 782 additions and 111 deletions

View File

@@ -1,9 +1,37 @@
# Acceptance Criteria
# F-152 — Acceptance Criteria
Define criterios verificables por feature.
- Feature ID: F-152
Formato recomendado:
- Feature ID:
- Escenario:
- Given / When / Then:
- Evidencia esperada (test/comando):
## Account creation → welcome email
- Escenario: customer registers with a new email.
- Given SMTP is configured and `POST /auth/register` is called with valid email+password
- When registration succeeds
- Then a welcome email (`account_created`) is sent to that email
- And the response is still 200 with the user payload
- And evidence: SMTP sendmail called once with recipient == input email and template account_created
- Escenario: SMTP is not configured.
- Given SMTP is not configured and `POST /auth/register` is called
- When registration succeeds
- Then the response is still 200 (registration never fails due to email)
- And a warning is logged
## Order confirmation on payment success
- Escenario: customer pays and Stripe reports PaymentSucceeded.
- Given an order in AWAITING_PAYMENT and a valid `PaymentSucceeded` webhook
- When the webhook is processed
- Then the order moves to PAID
- And a confirmation email (`state: PAID`) is sent to the order's customer email via sendOrderStatusEmail
- And the webhook returns 2xx
- Escenario: duplicate webhook.
- Given the same PaymentSucceeded webhook delivered twice
- When the second is processed
- Then only one confirmation email is sent (idempotency from payment dedup / outcome.kind == duplicate skipped)
- Escenario: SMTP not configured.
- Given SMTP not configured and a PaymentSucceeded webhook arrives
- When processed
- Then the order still moves to PAID, webhook returns 2xx, and an email failure is logged (not thrown)
## Regression
- verify.sh is green
- No existing order admin-transition email behavior changed
- Password-reset email flow unchanged

View File

@@ -1,15 +1,32 @@
# Product Spec
# F-152 — Product Spec
## Problema
Describe el problema de negocio.
Los clientes no reciben confirmación por email ni al crear la cuenta ni cuando su
pedido se confirma tras el pago:
- `POST /auth/register` crea el usuario pero no envía email de bienvenida.
- El webhook de Stripe (`PaymentSucceeded`) pasa el pedido a `PAID` pero no
notifica al cliente (el `OrderEventPublisher` inyectado en payments es no-op y
`sendOrderStatusEmail` solo se llamaba desde transiciones admin).
## Objetivo
Define el resultado esperado del producto.
Que los clientes reciban los dos emails transaccionales esenciales:
1. **Welcome** al crear la cuenta (account_created).
2. **Order confirmation** cuando el pago se confirma (PaymentSucceeded → PAID),
reenviando el flujo ya existente de `sendOrderStatusEmail` (SMTP desde
*Ajustes → SMTP / Email*).
## Usuarios
- Usuario principal:
- Usuario secundario:
- Usuario principal: cliente que se registra / compra en la tienda.
- Usuario secundario: operador (Ajustes SMTP) y admin (vee historial).
## Alcance v1
- In scope:
- Welcome email on `POST /auth/register` (best-effort, nunca bloquea el registro).
- Order confirmation email on `PaymentSucceeded` webhook (best-effort, nunca
rompe la reconciliación de pagos).
- Reusar el SMTP configurado en `store_settings` ya usado por admin transitions.
- Tests unitarios de cuerpo/email y de best-effort.
- Out of scope:
- Verificación por enlace (gating de cuenta por email) — queda como hardening.
- Reenvío de emails ya enviados (idempotencia garantizada por el webhook).
- Cambiar el email de transición admin existente.

View File

@@ -1,37 +1,62 @@
# Technical Spec — MercadoDeVida vNext
# F-152 — Tech Spec
## Stack
- Lenguaje: TypeScript (strict) — backend y frontend
- Framework backend: Fastify 5 (monolito modular; elegido por tipado, simplicidad y ausencia de magia)
- Framework frontend: Next.js + React + Tailwind (llega en F-013; SSG/ISR para SEO)
- Runtime: Node.js >= 22
- Base de datos: PostgreSQL (fuente de verdad) + Redis (solo caché) — desde F-002
## 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`.
## Dependencias (justificación obligatoria por política de seguridad)
## Cambios
| Dependencia | Versión | Justificación |
|---|---|---|
| fastify | ^5.2 | Servidor HTTP tipado, maintained, sin magia; única dep de runtime en F-001 |
| typescript | ^5.7 | Tipado estricto (dev) |
| vitest | ^3 | Tests unit/integración TS-native (dev) |
| eslint + @eslint/js + typescript-eslint + eslint-config-prettier | ^9 / ^8 / ^10 | Linting estándar (dev) |
| prettier | ^3 | Formato consistente (dev) |
| pg | ^8 | Driver PostgreSQL estándar; única forma de hablar con la DB (F-002) |
| node-pg-migrate | ^8 | Migraciones SQL up/down trackeadas en DB; elegida sobre runner propio (no reinvención) y sobre Flyway/golang-migrate (toolchains ajenos a Node) (F-002) |
| @types/pg | ^8 | Tipos para pg (dev) (F-002) |
| pino | ^9 | Logging JSON estructurado; estándar del ecosistema Fastify, rápido y sin magia (F-003) |
| zod | ^4 | Validación explícita de inputs vía parseJson en handlers; sin decoradores ni magic binding (F-003) |
| argon2 | ^1 | Hash de contraseñas Argon2id (parámetros OWASP 2024); implementación canónica, output PHC. Elegida sobre bcrypt (menos resistente a GPU/ASIC) y scrypt. Detrás del puerto PasswordHasher; fallback @node-rs/argon2 si el build nativo fallara (F-005) |
| @fastify/cookie | ^1 | setCookie/clearCookie explícitos para la sesión HttpOnly; plugin oficial Fastify, sin reinvención de parsing/serialización (F-005) |
### 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.
Regla: toda dependencia nueva debe agregarse a esta tabla con justificación en el ticket que la introduce.
### 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`.
## Restricciones
- Seguridad: nunca confiar en frontend para precio/stock/descuento/estado de pago; envelopes de error sin stack traces; sin secretos en repo
- Rendimiento: caché solo donde sea medible (F-027)
- Compatibilidad: URLs públicas estables /productos/<slug>, /categoria/<slug>, /marca/<slug>
## SMTP / store_settings
Claves existentes: `smtp_host, smtp_port, smtp_secure, smtp_user, smtp_pass, smtp_from`.
El welcome mailer reusa exactamente estas claves.
## Observabilidad
- Logging: estructurado con request_id (F-003)
- Métricas: Prometheus con métricas de negocio (F-029)
- Alertas: fuera de alcance inicial
## 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.