# F-153 — Tech Spec ## 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. ## Changes ### 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. 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). ### 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 - 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.