52 lines
3.0 KiB
Markdown
52 lines
3.0 KiB
Markdown
# F-153 — Architectural Decision Record
|
|
|
|
## Status: accepted (architect)
|
|
|
|
## Context
|
|
The order read model (`OrderView`) carries the customer's `userId` but never the `email`, even
|
|
though `orders_orders.user_id` references `identity_users` whose `email citext NOT NULL UNIQUE`
|
|
always exists. As a result:
|
|
|
|
- Order serialization (`serializeOrder`) never displays the customer email — the reported
|
|
"customer email missing" symptom.
|
|
- The admin force-transition (`POST /orders/:id/transitions/admin`) worked around the gap with
|
|
an inline `SELECT email FROM identity_users WHERE id = order.userId`, a fragile per-request
|
|
lookup that produced "El cliente no tiene email asociado" whenever the view itself did not
|
|
carry the association (and would N+1 if reused broadly).
|
|
|
|
## Decision
|
|
Associate the customer's email to the order **read model** and resolve it once, in the orders
|
|
repository, by joining `identity_users` (the same `LEFT JOIN` already used by the `search`
|
|
method):
|
|
|
|
1. Add `email: string | null` to `OrderView` (`identity/domain` vs `orders` boundary untouched;
|
|
`Order` core aggregate stays without email — email is a denormalized read attribute).
|
|
2. Add `email` to `OrderRow`; extend `findById`, `findByIdAndUserId`, `findAll`, and `search` to
|
|
`SELECT o.*, u.email FROM orders_orders o LEFT JOIN identity_users u ON u.id = o.user_id`,
|
|
and build the view through a single `toOrderView(row, items)` helper.
|
|
3. Surface `email` in `serializeOrder` (detail + list).
|
|
4. In the admin force-transition, consume `order.email` as the single source of truth and remove
|
|
the inline lookup.
|
|
|
|
## Rationale
|
|
- **Single source of truth**: once the email is on the view, both the API serialization and the
|
|
admin notification read it from one place — no divergent lookups.
|
|
- **Reuses an established pattern**: `orders/infrastructure/pg-order-repository.ts` `search`
|
|
already joins `identity_users`; F-153 generalizes that to all order reads. No new pattern.
|
|
- **Boundary-safe**: `identity_users` is referenced only as a SQL table-name string at the orders
|
|
infrastructure layer (no TypeScript import of `identity` from `orders`). `identity` does not
|
|
import `orders`/`payments`. `lint:boundaries` R1/R2 unaffected.
|
|
- **No migration / backward compatible**: `identity_users.email` already exists and is `NOT
|
|
NULL`; `email: string | null` on the view is null only when an order has no linked user.
|
|
Serialization only adds a field.
|
|
- **No N+1 on lists**: `findAll`/`search` resolve email in the same SELECT, so the admin list
|
|
returns emails without per-item lookups.
|
|
|
|
## Consequences
|
|
- Additive API field `email` on order DTOs; frontend/admin consumers gain the value for free.
|
|
- `order-service.test.ts`, `payments-service.test.ts`, `checkout-service.test.ts`
|
|
`OrderView` literals gain `email: null`.
|
|
- New `orders/infrastructure/pg-order-repository.test.ts` covers email resolution (happy path
|
|
and no-linked-user).
|
|
- Admin transition no longer depends on a raw `identity_users` query in the route layer.
|