feat(F-152): completed feature
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
87
spec/tech.md
87
spec/tech.md
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user