3.0 KiB
3.0 KiB
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 inlineSELECT 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):
- Add
email: string | nulltoOrderView(identity/domainvsordersboundary untouched;Ordercore aggregate stays without email — email is a denormalized read attribute). - Add
emailtoOrderRow; extendfindById,findByIdAndUserId,findAll, andsearchtoSELECT o.*, u.email FROM orders_orders o LEFT JOIN identity_users u ON u.id = o.user_id, and build the view through a singletoOrderView(row, items)helper. - Surface
emailinserializeOrder(detail + list). - In the admin force-transition, consume
order.emailas 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.tssearchalready joinsidentity_users; F-153 generalizes that to all order reads. No new pattern. - Boundary-safe:
identity_usersis referenced only as a SQL table-name string at the orders infrastructure layer (no TypeScript import ofidentityfromorders).identitydoes not importorders/payments.lint:boundariesR1/R2 unaffected. - No migration / backward compatible:
identity_users.emailalready exists and isNOT NULL;email: string | nullon the view is null only when an order has no linked user. Serialization only adds a field. - No N+1 on lists:
findAll/searchresolve email in the same SELECT, so the admin list returns emails without per-item lookups.
Consequences
- Additive API field
emailon order DTOs; frontend/admin consumers gain the value for free. order-service.test.ts,payments-service.test.ts,checkout-service.test.tsOrderViewliterals gainemail: null.- New
orders/infrastructure/pg-order-repository.test.tscovers email resolution (happy path and no-linked-user). - Admin transition no longer depends on a raw
identity_usersquery in the route layer.