ADR-0004. Polymorphic AllocationUsage(usageType, usageId) shared by SaleOrder + Reservation
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-03-12 |
| Deciders | sale-team |
| Supersedes | — |
Context
- A dining table (AllocationUnit) can be occupied by either an active SaleOrder (a customer eating now) or a Reservation (a future booking).
- Both need: the unit, time window, status (ACTIVE/SUCCESS/CANCELLED/EXPIRED), assignee.
- Two parallel tables (
SaleOrderAllocation+ReservationAllocation) duplicate the model and break shared queries like "what's free at 7pm".
Decision
Single AllocationUsage table with polymorphic (usageType, usageId) columns generated via generatePrincipalColumnDefs({ discriminator: 'usage' }):
usageType ∈ {SALE_ORDER, RESERVATION}usageIdreferences the parent's id- Same lifecycle, same WebSocket rooms, same cancellation cascade
When a Reservation transitions to CHECKED_IN, sale spawns a SaleOrder and the AllocationUsage's usageType is updated to SALE_ORDER + usageId to the new order id (the same usage row continues).
Consequences
| Pros | Cons |
|---|---|
| Single occupancy model | No DB FK to parent (polymorphic) |
| "Free at 7pm" query is one table scan | Service-layer must validate usageId exists |
| Cancellation cascade is uniform | Casts/discriminants in service code |
WebSocket fanout via getAllocationUsageRooms works for both | Type-safety relies on TS, not DB |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
| Separate tables per usage type | Strong DB FKs | Duplicates schema + queries + WS rooms | Maintenance pain |
AllocationUsage with both saleOrderId + reservationId (nullable, XOR) | Single table, FKs preserved | XOR constraint complex; queries always filter on type | Half-rejected pattern |
References
core/src/models/schemas/allocation/allocation-usage/schema.tssale/src/services/allocation-usage.service.tssale/src/services/reservation.service.ts(creates Reservation + AllocationUsage in one TX)