orders — module #15, completing the sell path
7 tables, 175 columns — schema-locked 2026-07-07, reopened 2026-07-08 for Remediation Phase 4 (Item 15, +1 col), again 2026-07-10 for Header/Line Remediation fix #6 (order_line.sale_line_id, +1 col), and again 2026-07-20 for the Gap-Fill Batch's B10 fix (order_payment.status gains 'forfeited' + a new terminal-state guard trigger — CHECK/trigger only, no column change). orders is the sell path's commercial layer: quotes, orders, special orders, and preorders, with deposits/installment schedules and light fulfillment tracking — completing crm → pricing → inventory → orders → pos. Up from v1's 7 tables / 133 cols (see the Design-Phase Integrity blocks in module_spec/orders.md and PROJECT_DECISIONS #29 for the full v1→v2 delta-accounting record — this is the first module built under those rules). Depends on platform (tenant ownership, and now legal_entity), identity (actor FKs throughout), multi_loc (site), crm (customer, address), pricing (price_rule — Hard Contract 1 snapshot target), inventory (item_variant, stock_reservation), pos (sale, sale_payment, and — since 2026-07-10 — sale_line — the fulfillment-link seam), and shared (currency).
PROJECT_DECISIONS entries: #29 (design/lock), #40 (Remediation Phase 4, Item 15), #50 (Header/Line Remediation, fix #6), #74 (Gap-Fill Batch, B10 — forfeited deposits).
Global rules for this schema:
- Uniform tenant-scoping — all 7 tables carry
tenant_id NOT NULLFK →platform.tenant, no mixed-scope case anywhere in this module. All 7 have RLS enabled with a permissive tenant-isolation policy named<table>_tenant_isolation,USING/WITH CHECKagainstcurrent_setting('app.current_tenant_id')::uuid. Plain index ontenant_idon all 7. updated_atis trigger-maintained viaplatform.set_updated_at()on all 7 tables — no new one-off trigger function was needed at original 2026-07-07 lock (unlikepos'strg_pos_sale_requires_open_session); nothing inordersrequired a same-row-insufficient, cross-table-state check enforced at the DB level at that time. This changed on 2026-07-20 (Gap-Fill Batch, B10):order_paymentnow carries its own one-off guard trigger,trg_order_payment_guard_status— see the dedicated section below.- Soft delete on all 7 tables (
deleted_at timestamptz, nullable). - Online-first, NOT offline-first — deliberately no offline-sync quintet (
client_uuid/origin/sync_status/synced_at). Only a single, light, nullableidempotency_keyonorder_headerfor API-retry dedup, matchingpricing.price_rule's own precedent, notpos's full offline quintet (Part D, D12, explicitly ruled the quintet out — orders is web/phone/email/counter, never an offline POS terminal). - Agent-as-actor attribution, continuing the canonical pattern — every
*_actor_idcolumn targetsidentity.actor(the polymorphic root), neveridentity.identity_userdirectly. - Autonomy treatment is per-table, selective.
order_header,order_line,order_payment,order_fulfillment,order_templateget the full autonomy column set (actor attribution,automation_source, the full review seam,decision_provenance) — each carries a real, live-effect decision surface (a quote can be agent-drafted; a price override, a substitution, and a fulfillment delay/shortfall are all real judgment calls).order_fulfillment_line/order_template_lineget zero autonomy columns — pure denormalized split/tracking tables with no independent decision surface beyond what their parent row already gates, matchingcrm.customer_note's DR-38 reasoning andpos.pos_sync_conflict's "nothing forautomation_sourceto record" reasoning for the same class of finding. - v1 baseline vs. this build — zero consolidation, pure addition. v1's
docs/old/schema/schema_modules/schema_orders.mdspecified 7 tables / 133 cols. This build's delta: table count unchanged at 7, column count up from 133 to 173 (+40) — every one of v1's 7 tables survives BUILT, none consolidated or dropped (Block 2 of the retained Design-Phase Integrity record reports this honestly rather than manufacturing a consolidation). See PROJECT_DECISIONS #29 for the full per-table delta table. - Pricing's Hard Contract 1 honored verbatim on
order_line. See dedicated section below — identical field names/types topos.sale_line's own 6 snapshot fields. - Reservation and fulfillment-link seams both real, not deferred —
order_line.stock_reservation_id → inventory.stock_reservationandorder_header.fulfilled_sale_id → pos.sale/order_payment.pos_sale_payment_id → pos.sale_paymentare enforced FKs, since all 3 targets exist live. See dedicated sections below. - A 4th real seam, added 2026-07-10 (Header/Line Remediation fix #6) —
order_line.sale_line_id → pos.sale_lineis a nullable, composite FK (order_line_sale_line_id_tenant_fkey (sale_line_id, tenant_id) → pos.sale_line(id, tenant_id), not bare). Link-don't-convert, mirroringorder_header.fulfilled_sale_id's own established asymmetry — no reciprocal column onpos.sale_line's side. See the dedicated section below. - A new terminal-status guard, added 2026-07-20 (Gap-Fill Batch, B10) —
order_payment.statusgained a 7th value,'forfeited', plus a new one-off trigger,trg_order_payment_guard_status, rejecting any transition OUT of'forfeited'(same-value re-writes tolerated). CHECK/trigger-only — no column or table added. See the dedicated section below.
Cross-Phase Foreign Keys (orders)
| Column | Target | Notes |
|---|---|---|
*.tenant_id (all 7 tables) |
platform.tenant |
NOT NULL on all 7 tables. |
order_header.site_id, order_fulfillment.site_id |
multi_loc.site.id |
NOT NULL on both. |
order_header.customer_id |
crm.customer.id |
Nullable — NULL only for anonymous quotes (order_type='quote'). |
order_header.entity_id |
platform.legal_entity.id |
NEW (Remediation Phase 4, Item 15). Nullable — which legal entity within the tenant this order belongs to, when a tenant operates multiple LLCs; NULL = the tenant's primary entity. Header-only (no entity_id on any orders line/child table — inherited via the header's FK). |
order_template.customer_id |
crm.customer.id |
Nullable — NULL = reusable across any customer. |
order_fulfillment.ship_to_address_id |
crm.address.id |
Nullable; REAL FK — crm.address exists live. Required (CHECK) when fulfillment_type='ship'. |
order_header.currency_code, order_line.currency_code, order_payment.currency_code |
shared.currency.iso_code |
NOT NULL on all 3, char(3). No hardcoded default on any of them (v1 had DEFAULT 'USD' on order_header — dropped in favor of service-resolved-from-site/customer, matching pos.sale's own no-default convention). |
order_line.variant_id, order_fulfillment_line.variant_id, order_template_line.variant_id |
inventory.item_variant.id |
NOT NULL on all 3; order_line.variant_id and order_line.substituted_from_variant_id's target, ON DELETE RESTRICT on order_line.variant_id. |
order_line.stock_reservation_id |
inventory.stock_reservation.id |
Nullable (NULL for backordered/non-stocked lines). REAL FK — the reservation seam, see dedicated section below. |
order_line.sale_line_id |
pos.sale_line(id, tenant_id) |
Nullable (NULL until fulfillment). NEW (Header/Line Remediation fix #6, 2026-07-10). Composite FK, not bare — order_line_sale_line_id_tenant_fkey. The prerequisite UNIQUE(id, tenant_id) on pos.sale_line was laid in the prior batch's POS reopen (fix #8) anticipating this exact fix. See dedicated section below. |
order_line.resolving_price_rule_id |
pricing.price_rule.id |
Nullable (NULL when no rule matched, bare base_price_cents used), ON DELETE RESTRICT. Part of Hard Contract 1. |
order_header.fulfilled_sale_id |
pos.sale.id |
Nullable, REAL FK, ON DELETE RESTRICT. The pos-fulfillment seam — link-don't-convert, no reciprocal column on pos's side (by design). |
order_payment.pos_sale_payment_id |
pos.sale_payment.id |
Nullable, REAL FK. |
order_id on order_line, order_payment, order_fulfillment |
orders.order_header.id |
NOT NULL on all 3. Intra-schema. |
order_fulfillment_id / order_line_id on order_fulfillment_line |
orders.order_fulfillment.id / orders.order_line.id |
NOT NULL on both. Intra-schema. |
order_template_id on order_template_line |
orders.order_template.id |
NOT NULL. Intra-schema. |
every *_actor_id column |
identity.actor |
Nullable throughout. Cross-schema, enforced. |
order_header.draft_po_id |
— (deferred forward-ref) | Plain nullable uuid, no FK — Purchasing schema doesn't exist in v2. |
order_payment.stripe_payment_intent_id |
— (deferred forward-ref) | Plain nullable text, no FK — Payments doesn't exist in v2. |
order_payment.charge_account_ref |
— (deferred forward-ref) | Plain nullable text, no FK — Billing doesn't exist in v2. |
order_fulfillment.tracking_number, .carrier |
— (deferred forward-refs) | Plain nullable text, no FK — a future Delivery/Shipping module (v1's own "Module 14" deferral, unchanged). |
orders.order_header (45 cols)
The root commercial-order record — quote, order, special order, or preorder. Full autonomy treatment: a quote can be agent-drafted; confirmation/cancellation are real financial/inventory commitments (needs_approval when agent-initiated).
Tenant-scoped.
tenant_id NOT NULLFK →platform.tenant. RLS enabled — permissive policyorder_header_tenant_isolation.Soft delete:
deleted_at timestamptz, nullable.
updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
site_id |
UUID | NOT NULL | — | FK → multi_loc.site |
order_number |
text | NOT NULL | — | Human-facing sequential reference |
order_type |
text | NOT NULL | 'order' |
CHECK IN (quote,order,special_order,preorder) |
customer_id |
UUID | nullable | — | FK → crm.customer; nullable only for anonymous quotes |
status |
text | NOT NULL | 'draft' |
CHECK IN (draft,quote_sent,confirmed,fulfilling,partially_fulfilled,fulfilled,closed,cancelled,expired) |
priority |
text | NOT NULL | 'standard' |
CHECK IN (standard,rush,vip) |
po_number |
text | nullable | — | Customer's PO (B2B) |
job_reference |
text | nullable | — | Contractor job reference |
subtotal_cents |
bigint | NOT NULL | 0 |
Service-maintained cache: SUM(order_line.line_subtotal_cents) |
discount_total_cents |
bigint | NOT NULL | 0 |
Service-maintained cache |
estimated_tax_cents |
bigint | NOT NULL | 0 |
Display-only — legal tax finalizes at POS |
estimated_total_cents |
bigint | NOT NULL | 0 |
Service cache: subtotal - discount + estimated_tax |
currency_code |
char(3) | NOT NULL | (none) | FK → shared.currency.iso_code; no hardcoded default (deviates from v1's DEFAULT 'USD') |
deposit_required_cents |
bigint | nullable | — | NULL = no deposit required |
balance_due_cents |
bigint | NOT NULL | 0 |
Service cache: estimated_total - SUM(order_payment.amount_paid_cents WHERE status='paid') |
quote_expires_at |
timestamptz | nullable | — | Required when order_type='quote' — real CHECK |
accepted_at |
timestamptz | nullable | — | |
accepted_by |
text | nullable | — | Preserved verbatim from v1 — free-form acceptance evidence |
accepted_by_actor_id |
UUID | nullable | — | NEW — FK → identity.actor; coexists with accepted_by, does not replace it |
requested_date |
date | nullable | — | |
expected_date |
date | nullable | — | |
draft_po_id |
UUID | nullable | — | DEFERRED forward-ref, no FK — Purchasing not built |
fulfilled_sale_id |
UUID | nullable | — | REAL FK → pos.sale, ON DELETE RESTRICT — the pos-fulfillment seam |
entity_id |
UUID | nullable | — | NEW (Remediation Phase 4, Item 15) — FK → platform.legal_entity; which legal entity within the tenant this order belongs to (multi-LLC tenants); NULL = tenant's primary entity |
idempotency_key |
text | nullable | — | API-driven order-creation dedup |
note |
text | nullable | — | |
attributes |
jsonb | nullable | '{}' |
Vertical/custom extension point — example shape TBD, see Open Items |
created_by_actor_id |
UUID | nullable | — | FK → identity.actor (retargeted from v1's created_by_user_id) |
confirmed_at |
timestamptz | nullable | — | Required once status reaches confirmed or later — widened beyond v1's literal single-state text (deliberate, see module_spec/orders.md DR) |
confirmed_by_actor_id |
UUID | nullable | — | FK → identity.actor (retargeted) |
cancelled_at |
timestamptz | nullable | — | |
cancelled_by_actor_id |
UUID | nullable | — | FK → identity.actor (retargeted) |
cancellation_reason |
text | nullable | — | |
closed_at |
timestamptz | nullable | — | |
automation_source |
text | NOT NULL | 'human' |
CHECK IN (human,agent,system,seed) |
review_status |
text | NOT NULL | 'not_required' |
CHECK IN (not_required,pending,approved,rejected) |
review_reason |
text | nullable | — | |
reviewed_by_actor_id |
UUID | nullable | — | FK → identity.actor; cannot equal created_by_actor_id when both set |
reviewed_at |
timestamptz | nullable | — | |
decision_provenance |
jsonb | nullable | — | memory_refs/delegated_by_actor_id extension, project-wide convention |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
Dropped vs. v1: search_vector (Search module doesn't exist in v2).
CHECK constraints (10): chk_order_header_order_type; chk_order_header_status; chk_order_header_priority; chk_order_header_automation_source; chk_order_header_review_status; chk_order_header_reviewer_not_creator; chk_order_header_quote_requires_expiry (order_type != 'quote' OR quote_expires_at IS NOT NULL — was v1 service-only, now real); chk_order_header_customer_required_unless_quote (order_type = 'quote' OR customer_id IS NOT NULL — was v1 service-only, now real); chk_order_header_confirmed_requires_timestamp (widened, see above); chk_order_header_cancelled_requires_actor_reason — the adversarial-caught fix: status != 'cancelled' OR (cancelled_at IS NOT NULL AND cancelled_by_actor_id IS NOT NULL AND cancellation_reason IS NOT NULL), requiring all 3 fields (an early draft only required 2 — caught before build, see PROJECT_DECISIONS #29).
Indexes: PK on id; plain index on tenant_id; (tenant_id, status, requested_date) — order dashboard filter; (tenant_id, customer_id) WHERE deleted_at IS NULL — customer's open orders; (tenant_id, quote_expires_at) WHERE order_type='quote' AND status='quote_sent' — quote-expiry sweep; order_header_tenant_id_order_number_unique (UNIQUE, tenant_id+order_number, WHERE deleted_at IS NULL); two-partial NULL-actor-safe idempotency uniques (matching pricing.price_rule's precedent): order_header_tenant_actor_idempotency_unique (tenant_id,created_by_actor_id,idempotency_key WHERE all NOT NULL AND deleted_at IS NULL), order_header_tenant_null_actor_idempotency_unique (tenant_id,idempotency_key WHERE created_by_actor_id IS NULL AND idempotency_key IS NOT NULL AND deleted_at IS NULL).
DEFERRED, not built: GIN (search_vector) (Search module doesn't exist); GIN (order_number gin_trgm_ops) (pg_trgm extension not enabled — same already-logged gap as inventory's identical deferral). Confirmed absent live.
orders.order_line (39 cols) — honors Pricing's Hard Contract 1 verbatim
One row per item on an order. Full autonomy treatment (upgraded from an earlier partial-pack draft after independent adversarial review found the partial version unjustified — see PROJECT_DECISIONS #29) — this table carries live financial judgment calls (price_override, a substitution proposal).
Pricing's locked Hard Contract 1, honored verbatim — the 6 required snapshot fields below are the historical record of what resolvePrice() returned at order-confirmation time, never re-derived from the live (possibly-since-superseded) price_rule row. Live-tested: after the resolving price_rule is superseded to a new price, order_line's snapshot amounts remain unchanged.
Tenant-scoped.
tenant_id NOT NULLFK →platform.tenant. RLS enabled — permissive policyorder_line_tenant_isolation.Soft delete:
deleted_at timestamptz, nullable.
updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
order_id |
UUID | NOT NULL | — | FK → orders.order_header |
variant_id |
UUID | NOT NULL | — | FK → inventory.item_variant, ON DELETE RESTRICT |
line_type |
text | NOT NULL | 'standard' |
CHECK IN (standard,comp,substitution) — preserved verbatim |
quantity |
numeric | NOT NULL | — | CHECK > 0 |
reserved_qty |
numeric | NOT NULL | 0 |
CHECK >= 0 |
fulfilled_qty |
numeric | NOT NULL | 0 |
CHECK >= 0 |
backordered_qty |
numeric | NOT NULL | 0 |
CHECK >= 0 |
cancelled_qty |
numeric | NOT NULL | 0 |
CHECK >= 0 |
fulfillment_status |
text | NOT NULL | 'pending' |
CHECK IN (pending,reserved,backordered,partially_fulfilled,fulfilled,cancelled) |
stock_reservation_id |
UUID | nullable | — | REAL FK → inventory.stock_reservation — the reservation seam; NULL for backordered/non-stocked lines |
sale_line_id |
UUID | nullable | — | NEW (Header/Line Remediation fix #6, 2026-07-10) — composite FK → pos.sale_line(id, tenant_id) (order_line_sale_line_id_tenant_fkey), not bare. NULL until fulfillment; link-don't-convert, no reciprocal column on pos.sale_line's side |
resolved_amount_minor_units |
bigint | NOT NULL | — | Hard Contract 1, field 1 |
charged_amount_minor_units |
bigint | NOT NULL | — | Hard Contract 1, field 2 |
currency_code |
char(3) | NOT NULL | — | Hard Contract 1, field 3. FK → shared.currency.iso_code |
tax_treatment |
text | NOT NULL | — | Hard Contract 1, field 4. CHECK IN (inclusive,exclusive) |
resolving_price_rule_id |
UUID | nullable | — | Hard Contract 1, field 5. FK → pricing.price_rule, ON DELETE RESTRICT; NULL when no rule matched |
resolved_quantity |
numeric | NOT NULL | — | Hard Contract 1, field 6. CHECK > 0 |
price_override |
boolean | NOT NULL | false |
Preserved from v1 — a capability pos.sale_line itself dropped; orders restores it |
price_override_reason |
text | nullable | — | Required when price_override=true — real CHECK |
discount_amount_cents |
bigint | NOT NULL | 0 |
|
line_subtotal_cents |
bigint | NOT NULL | 0 |
Service cache |
line_total_cents |
bigint | NOT NULL | 0 |
Service cache |
estimated_line_tax_cents |
bigint | NOT NULL | 0 |
Display-only |
substitution_allowed |
boolean | NOT NULL | true |
Preserved |
substituted_from_variant_id |
UUID | nullable | — | FK → inventory.item_variant, preserved |
expected_arrival_at |
timestamptz | nullable | — | |
note |
text | nullable | — | Required when line_type != 'standard' — real CHECK |
created_by_actor_id |
UUID | nullable | — | FK → identity.actor |
automation_source |
text | NOT NULL | 'human' |
CHECK IN (human,agent,system,seed) |
review_status |
text | NOT NULL | 'not_required' |
CHECK IN (not_required,pending,approved,rejected) |
review_reason |
text | nullable | — | |
reviewed_by_actor_id |
UUID | nullable | — | FK → identity.actor; cannot equal created_by_actor_id |
reviewed_at |
timestamptz | nullable | — | |
decision_provenance |
jsonb | nullable | — | |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
Removed vs. v1: unit_price_cents — replaced (additive, not a reduction) by the 6-field Hard Contract 1 snapshot.
CHECK constraints (13): chk_order_line_line_type; chk_order_line_quantity_positive; chk_order_line_qty_columns_nonneg; chk_order_line_fulfillment_status; chk_order_line_quantity_rollup (reserved_qty + fulfilled_qty + backordered_qty + cancelled_qty <= quantity — was v1 service-only, now real); chk_order_line_tax_treatment; chk_order_line_resolved_quantity_positive; chk_order_line_price_override_reason (NOT price_override OR price_override_reason IS NOT NULL — was v1 service-only, now real); chk_order_line_note_required_for_special_types (line_type = 'standard' OR note IS NOT NULL — was v1 service-only, now real); chk_order_line_amounts_nonnegative; chk_order_line_automation_source; chk_order_line_review_status; chk_order_line_reviewer_not_creator.
Indexes: PK on id; plain indexes on tenant_id, order_id; (order_id, fulfillment_status) — backordered/partial status per order; (tenant_id, variant_id) — cross-order lines by variant.
orders.order_payment (27 cols)
Payment schedule / installment record. One row per deposit, milestone, or balance payment. Full autonomy treatment — money-touching (Part C's C8: independent approval seam + reversibility, both required).
Tenant-scoped.
tenant_id NOT NULLFK →platform.tenant. RLS enabled — permissive policyorder_payment_tenant_isolation.Soft delete:
deleted_at timestamptz, nullable.
updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
order_id |
UUID | NOT NULL | — | FK → orders.order_header |
sequence |
integer | NOT NULL | — | Installment order (1 = deposit) |
payment_stage |
text | NOT NULL | — | CHECK IN (deposit,milestone,balance,final) |
status |
text | NOT NULL | 'scheduled' |
CHECK IN (scheduled,due,paid,failed,refunded,cancelled,forfeited) — 'forfeited' added 2026-07-20, Gap-Fill Batch B10; terminal, guarded by trg_order_payment_guard_status (see below) |
amount_due_cents |
bigint | NOT NULL | — | CHECK >= 0 |
amount_paid_cents |
bigint | NOT NULL | 0 |
CHECK >= 0 |
currency_code |
char(3) | NOT NULL | (none) | FK → shared.currency.iso_code; no hardcoded default |
due_at |
timestamptz | nullable | — | |
paid_at |
timestamptz | nullable | — | |
payment_method |
text | nullable | — | CHECK NULL OR IN (card,cash,check,ach,charge_account) — preserves v1's own exclusion of gift_card/store_credit (GAP F-1), consistent with pos's fail-closed decision on the same tenders |
stripe_payment_intent_id |
text | nullable | — | DEFERRED — Payments module not built |
charge_account_ref |
text | nullable | — | DEFERRED — Billing module not built |
pos_sale_payment_id |
UUID | nullable | — | REAL FK → pos.sale_payment |
refunded_amount_cents |
bigint | NOT NULL | 0 |
CHECK >= 0 AND <= amount_paid_cents |
note |
text | nullable | — | |
created_by_actor_id |
UUID | nullable | — | FK → identity.actor |
automation_source |
text | NOT NULL | 'human' |
CHECK IN (human,agent,system,seed) |
review_status |
text | NOT NULL | 'not_required' |
CHECK IN (not_required,pending,approved,rejected) |
review_reason |
text | nullable | — | |
reviewed_by_actor_id |
UUID | nullable | — | FK → identity.actor; cannot equal created_by_actor_id |
reviewed_at |
timestamptz | nullable | — | |
decision_provenance |
jsonb | nullable | — | |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
No columns dropped vs. v1 — pure addition (+7 autonomy pack).
CHECK constraints (10, count unchanged by the 2026-07-20 widening — chk_order_payment_status itself was widened in place, not replaced): chk_order_payment_payment_stage; chk_order_payment_status (widened 2026-07-20, Gap-Fill Batch B10, to add 'forfeited'); chk_order_payment_payment_method; chk_order_payment_amount_due_nonneg; chk_order_payment_amount_paid_nonneg; chk_order_payment_refunded_nonneg; chk_order_payment_refunded_le_paid; chk_order_payment_automation_source; chk_order_payment_review_status; chk_order_payment_reviewer_not_creator.
Trigger (new 2026-07-20, Gap-Fill Batch B10): trg_order_payment_guard_status (BEFORE UPDATE OF status, function orders.guard_order_payment_status()) — a no-op-plus-terminal-reject guard: a same-value re-write of status is tolerated (idempotent no-op), but any transition OUT of 'forfeited' is rejected. 'forfeited' is the only value this trigger polices; the other 6 order_payment statuses have no transition rule (they branch, not chain — see module_spec/orders.md §9 DR-K). See the dedicated section below.
Indexes: PK on id; plain indexes on tenant_id, order_id; order_payment_overdue_sweep_idx (tenant_id,status,due_at WHERE status IN ('scheduled','due')) — overdue-payment sweep.
orders.order_fulfillment (26 cols)
Fulfillment batch header. One row per fulfillment event for an order (a single order may span multiple batches — partial ship + later pickup). Full autonomy treatment — this is where a fulfillment delay or picking shortfall gets flagged as an anomaly, matching pos's own reasoning for why sale/register_session get the full pack.
Tenant-scoped.
tenant_id NOT NULLFK →platform.tenant. RLS enabled — permissive policyorder_fulfillment_tenant_isolation.Soft delete:
deleted_at timestamptz, nullable.
updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
order_id |
UUID | NOT NULL | — | FK → orders.order_header |
site_id |
UUID | NOT NULL | — | FK → multi_loc.site |
fulfillment_type |
text | NOT NULL | — | CHECK IN (pickup,delivery,ship,counter_handoff) |
status |
text | NOT NULL | 'pending' |
CHECK IN (pending,picking,staged,ready,handed_over,cancelled) |
staging_code |
text | nullable | — | |
picked_by_actor_id |
UUID | nullable | — | FK → identity.actor (retargeted from v1's picked_by) |
picked_at |
timestamptz | nullable | — | |
staged_at |
timestamptz | nullable | — | |
ready_at |
timestamptz | nullable | — | |
handed_over_at |
timestamptz | nullable | — | |
ship_to_address_id |
UUID | nullable | — | REAL FK → crm.address; required when fulfillment_type='ship' — real CHECK |
tracking_number |
text | nullable | — | DEFERRED — future Delivery/Shipping module |
carrier |
text | nullable | — | DEFERRED — same |
note |
text | nullable | — | |
created_by_actor_id |
UUID | nullable | — | FK → identity.actor |
automation_source |
text | NOT NULL | 'human' |
CHECK IN (human,agent,system,seed) |
review_status |
text | NOT NULL | 'not_required' |
CHECK IN (not_required,pending,approved,rejected) — the delay/picking-shortfall flagging surface |
review_reason |
text | nullable | — | |
reviewed_by_actor_id |
UUID | nullable | — | FK → identity.actor; cannot equal created_by_actor_id |
reviewed_at |
timestamptz | nullable | — | |
decision_provenance |
jsonb | nullable | — | |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
No columns dropped vs. v1 — pure addition (+7 autonomy pack).
CHECK constraints (6): chk_order_fulfillment_type; chk_order_fulfillment_status; chk_order_fulfillment_ship_to_required (fulfillment_type != 'ship' OR ship_to_address_id IS NOT NULL — was v1 service-only, now real); chk_order_fulfillment_automation_source; chk_order_fulfillment_review_status; chk_order_fulfillment_reviewer_not_creator.
Indexes: PK on id; plain indexes on tenant_id, order_id; order_fulfillment_status_idx (status) — all picking/staged/ready fulfillments across sites; order_fulfillment_site_id_status_idx (site_id,status) — staged/ready batches at a site, fulfillment dashboard.
orders.order_fulfillment_line (13 cols) — byte-for-byte unchanged from v1
One row per order line per fulfillment batch — tracks the split of a line's quantity across multiple batches and the picking outcome for each. Deliberately ZERO autonomy columns — a pure denormalized tracking/split table with no independent decision surface of its own beyond what its parent order_fulfillment already gates (matches crm.customer_note's DR-38 reasoning and pos_sync_conflict's reasoning for the same class of finding).
Tenant-scoped.
tenant_id NOT NULLFK →platform.tenant. RLS enabled — permissive policyorder_fulfillment_line_tenant_isolation.Soft delete:
deleted_at timestamptz, nullable.
updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
order_fulfillment_id |
UUID | NOT NULL | — | FK → orders.order_fulfillment |
order_line_id |
UUID | NOT NULL | — | FK → orders.order_line |
variant_id |
UUID | NOT NULL | — | FK → inventory.item_variant |
quantity_requested |
numeric | NOT NULL | — | CHECK > 0 |
quantity_picked |
numeric | NOT NULL | 0 |
CHECK >= 0 AND <= quantity_requested |
quantity_fulfilled |
numeric | NOT NULL | 0 |
CHECK >= 0 AND <= quantity_requested |
status |
text | NOT NULL | 'pending' |
CHECK IN (pending,picked,fulfilled,short,cancelled) |
note |
text | nullable | — | |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
Zero change vs. v1. CHECK constraints: chk_order_fulfillment_line_quantity_requested_positive; chk_order_fulfillment_line_quantity_picked_bounds; chk_order_fulfillment_line_quantity_fulfilled_bounds; chk_order_fulfillment_line_status.
Indexes: PK on id; plain indexes on tenant_id, order_fulfillment_id, order_line_id.
orders.order_template (16 cols)
Named recurring order template, applied via OrderService.applyTemplate() to create a new order_header pre-populated with lines. Full autonomy treatment — an agent could propose a template from a detected repeat-order pattern.
Prices are deliberately NOT stored here (preserved from v1) — resolved fresh by PricingService/resolvePrice() when the template is applied, never cached.
Tenant-scoped.
tenant_id NOT NULLFK →platform.tenant. RLS enabled — permissive policyorder_template_tenant_isolation.Soft delete:
deleted_at timestamptz, nullable.
updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
customer_id |
UUID | nullable | — | FK → crm.customer; NULL = reusable across any customer |
name |
text | NOT NULL | — | Unique per tenant while not deleted |
is_active |
boolean | NOT NULL | true |
|
note |
text | nullable | — | Carried to order on apply |
automation_source |
text | NOT NULL | 'human' |
CHECK IN (human,agent,system,seed) |
created_by_actor_id |
UUID | nullable | — | FK → identity.actor |
review_status |
text | NOT NULL | 'not_required' |
CHECK IN (not_required,pending,approved,rejected) |
review_reason |
text | nullable | — | |
reviewed_by_actor_id |
UUID | nullable | — | FK → identity.actor; cannot equal created_by_actor_id |
reviewed_at |
timestamptz | nullable | — | |
decision_provenance |
jsonb | nullable | — | |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
CHECK constraints: chk_order_template_automation_source; chk_order_template_review_status; chk_order_template_reviewer_not_creator.
Indexes: PK on id; plain index on tenant_id; (tenant_id, customer_id) WHERE is_active = true — active templates by customer; order_template_tenant_id_name_unique (UNIQUE, tenant_id+name, WHERE deleted_at IS NULL).
orders.order_template_line (9 cols) — byte-for-byte unchanged from v1
One row per item in an order template. Default quantities, adjustable at apply time. Prices not stored (see order_template). Deliberately ZERO autonomy columns — same reasoning as order_fulfillment_line.
Tenant-scoped.
tenant_id NOT NULLFK →platform.tenant. RLS enabled — permissive policyorder_template_line_tenant_isolation.Soft delete:
deleted_at timestamptz, nullable.
updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
order_template_id |
UUID | NOT NULL | — | FK → orders.order_template |
variant_id |
UUID | NOT NULL | — | FK → inventory.item_variant |
quantity |
numeric | NOT NULL | — | Default quantity, adjustable at apply time |
note |
text | nullable | — | |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
Zero change vs. v1. Indexes: PK on id; plain indexes on tenant_id, order_template_id.
The reservation seam
order_line.stock_reservation_id → inventory.stock_reservation is a real FK, not deferred — stock_reservation.source_type already includes 'order' as a CHECK-enum value, and stock_movement.source_module already includes 'orders' (confirmed live before this build) — this table was architecturally shaped to receive orders as a primary writer before orders itself existed. The reserve-vs-move distinction is inventory's own, unchanged: a reservation (at order confirmation) is a soft-hold that reduces stock.available_qty via reserved_qty without moving physical stock; fulfillment later writes a real stock_movement row (the actual decrement) — a service-layer concern, not this schema's. Reserve-on-confirm, not on quote (v1's own rule, preserved) — backordered lines correctly hold stock_reservation_id = NULL.
The pos-fulfillment seam
order_header.fulfilled_sale_id → pos.sale and order_payment.pos_sale_payment_id → pos.sale_payment are both real FKs, since both targets exist live. pos.md itself has zero mention of orders or this seam — CROSS_MODULE_CONTRACTS.md is the actual seam catalog and correctly documents it exactly as expected for a (at the time) not-yet-built consumer: "FK lives entirely on Orders' side, pos.sale needs no reciprocal column," "link-don't-convert: order links to the POS sale that fulfilled it, tax finalizes at POS." Live-tested: deleting a pos.sale row referenced by an order_header.fulfilled_sale_id is rejected (ON DELETE RESTRICT, 23503/order_header_fulfilled_sale_id_fkey) — proving this is a real, enforced FK, not an unvalidated forward-ref.
The line-level fulfillment-link seam (Header/Line Remediation fix #6, 2026-07-10)
order_line.sale_line_id → pos.sale_line extends the header-level pos-fulfillment seam above down to the line level — which sale_line a given order_line was actually fulfilled by, not just which sale fulfilled the order as a whole. Nullable (NULL until fulfillment happens), and a composite FK (order_line_sale_line_id_tenant_fkey (sale_line_id, tenant_id) → pos.sale_line(id, tenant_id)), never a bare single-column FK — the standing cross-tenant-FK rule this entire remediation effort exists to enforce. Link-don't-convert, matching order_header.fulfilled_sale_id's own established asymmetry exactly: no reciprocal column exists on pos.sale_line's side either, and none is planned unless a real "given a sale_line, find its originating order_line" query need emerges. The prerequisite UNIQUE(id, tenant_id) on pos.sale_line was laid in the prior batch's POS reopen (fix #8, PROJECT_DECISIONS #46) specifically anticipating this exact fix. Fully additive — nullable column, zero backfill, no existing order_line data touched. Live-tested: a same-tenant sale_line_id reference resolves via JOIN and succeeds; NULL succeeds (not-yet-fulfilled lines); a cross-tenant sale_line_id reference is rejected by the composite FK (23503/order_line_sale_line_id_tenant_fkey). See PROJECT_DECISIONS #50.
The forfeited-deposit terminal-status guard (Gap-Fill Batch B10, 2026-07-20)
order_payment.status gained a 7th CHECK-enum value, 'forfeited' (chk_order_payment_status widened in place — no new constraint, no column added), distinguishing a customer-forfeited deposit (special order cancelled by the customer, tenant keeps the deposit per stated policy) from 'cancelled' (payment never completed, no money changed hands) or 'refunded' (money returned) — a status meaning that did not previously exist.
A new function, orders.guard_order_payment_status(), and a matching BEFORE UPDATE OF status trigger, trg_order_payment_guard_status, enforce 'forfeited' as terminal: a same-value re-write ('forfeited' → 'forfeited') is tolerated as an idempotent no-op, but any transition OUT of 'forfeited' to any other value is rejected. This mirrors notifications.delivery_attempt's own established monotonic-guard shape — selected because neither order_header nor order_payment carried any status-transition trigger beyond platform.set_updated_at() before this pass, so there was no true precedent on this table itself to match. The other 6 order_payment statuses (scheduled,due,paid,failed,refunded,cancelled) remain entirely unguarded by this trigger — they branch rather than chain (e.g. a scheduled/due row can resolve to paid, failed, or cancelled; a paid row can later become refunded), so no single ordinal ranking exists to encode, and this fix's own scope was exactly one rule: forfeited is terminal. See module_spec/orders.md §9 (DR-K) and §15, and PROJECT_DECISIONS #74.
Deliberately NOT decided by this fix: whether a 'forfeited' payment still counts toward order_header.balance_due_cents's service-maintained derivation (§6/DR-4 of the module spec), and whether a forfeited deposit posts anywhere in billing, are both open questions left for OrderService's own future build — this fix adds only the status value and its terminal-transition guard.
Migration: packages/db/migrations/20260720000007_orders_reopen_forfeited_deposits.sql. Tests: orders-schema.spec.ts +4 (due→forfeited transition; forfeited→refunded rejected; same-value no-op tolerated; an unrelated non-forfeited row's normal transition unaffected), 51/51 for the file.
Pricing's Hard Contract 1 — honored verbatim on order_line
order_line carries all 6 fields Pricing's Hard Contract 1 requires (PROJECT_DECISIONS #26), verbatim-matching pos.sale_line's own field names/types: resolved_amount_minor_units, charged_amount_minor_units, currency_code, tax_treatment, resolving_price_rule_id, resolved_quantity. Live-tested: after the resolving price_rule row is superseded to a new price, order_line's snapshot amounts remain unchanged — the order line is the historical record of what was resolved at confirmation time, never re-derived from the live rule. This satisfies Hard Contract 1 from the orders side — mark that contract row as SATISFIED by both pos and orders in CROSS_MODULE_CONTRACTS.md.
orders — Design Patterns Summary
Column-count reconciliation
| Table | Cols |
|---|---|
order_header |
45 |
order_line |
39 |
order_payment |
27 |
order_fulfillment |
26 |
order_fulfillment_line |
13 |
order_template |
16 |
order_template_line |
9 |
| Total | 175 |
Verified live via information_schema.columns GROUP BY, schema orders: 7 tables, 175 columns (up from 174 at 2026-07-08/Remediation Phase 4, +1 from the 2026-07-10 Header/Line Remediation fix #6's order_line.sale_line_id; 173 at the original 2026-07-07 lock before that; unchanged again by the 2026-07-20 Gap-Fill Batch's B10 fix — CHECK/trigger-only, no column), matching the per-table sum exactly. 46 CHECK constraints total (verified live via pg_constraint, unchanged by fix #6 or by B10 — B10 widened chk_order_payment_status in place rather than adding a new CHECK), 45 FK constraints total (+1, order_line.sale_line_id → pos.sale_line, composite), RLS enabled on all 7 tables, platform.set_updated_at() firing on all 7, plus (new as of the 2026-07-20 Gap-Fill Batch) one additional one-off trigger — order_payment.trg_order_payment_guard_status (no new one-off trigger function had been needed by either of the two earlier reopens).
v1 deviations (full detail: PROJECT_DECISIONS #29)
Zero tables consolidated or dropped — pure addition (+40 cols across the same 7 tables). search_vector deferred (Search module doesn't exist in v2, matches every other module); the order_number pg_trgm fuzzy-search index deferred (extension not enabled, matches inventory's identical deferral); draft_po_id/stripe_payment_intent_id/charge_account_ref/tracking_number/carrier all remain forward-refs into not-yet-built modules (Purchasing/Payments/Billing/a future Delivery module). 6 v1 service-enforced-only rules promoted to real DB CHECKs. accepted_by preserved verbatim AND joined by a new accepted_by_actor_id (both, not either). 3 forward-refs upgraded from "documented as real" at v1's design time to actually-real FKs now that their targets exist live: fulfilled_sale_id→pos.sale, pos_sale_payment_id→pos.sale_payment, ship_to_address_id→crm.address.
Remediation Phase 4 (2026-07-08)
orders was reopened as 1 of 11 modules touched by Remediation Plan Phase 4 (Item 15 — Legal Entity; see PROJECT_DECISIONS #40). platform.legal_entity (1:N from platform.tenant, so a tenant can incorporate a 2nd LLC without splitting into two tenants) was added in this phase, along with a nullable entity_id FK column on 10 header tables across 8 modules independently derived for this rollout — orders.order_header is one of them.
order_header.entity_id (UUID, nullable, FK → platform.legal_entity.id) records which legal entity within the tenant an order belongs to, when the tenant operates multiple LLCs; NULL means the tenant's primary entity. This is a pure additive change — table count stays at 7, column count goes from 173 to 174 (order_header alone: 44 → 45).
Per the disclosed scoping rule established this same phase, entity_id was added ONLY to the orders module's header table (order_header), never to its line/child tables (order_line, order_payment, order_fulfillment, order_fulfillment_line, order_template, order_template_line) — unlike tenant_id, which this codebase applies uniformly to every table including line items, a line item's legal entity is always inherited via its header's FK and never needs its own column.
0 live order_header rows required backfill at build time (confirmed in the migration itself) — zero backfill risk. Migration: packages/db/migrations/20260709010000_phase4_item15_legal_entity.sql.
LIVE-REPRODUCED (per PROJECT_DECISIONS #40): order_header.entity_id confirmed nullable and FK-valid against platform.legal_entity, consistent with the other 9 header tables in this rollout.
Header/Line Remediation reopen (2026-07-10) — fix #6
Second reopen of the second batch of the coordinated "Header/Line Remediation" effort — Inventory landed first in this batch (fixes #5/#9, PROJECT_DECISIONS #49); orders is next, sequenced after Inventory and after POS specifically, per the confirmed ripple dependency in vrida-header-line-remediation-design-2026-07-10.md §6a (the prerequisite pos.sale_line UNIQUE(id, tenant_id) was laid in the first batch's POS reopen, fix #8). See PROJECT_DECISIONS #50 for the full record, including the pasted, attributed independent-verification summary.
orders.order_line.sale_line_id— new nullable column (+1 col), composite FK (order_line_sale_line_id_tenant_fkey) →pos.sale_line(id, tenant_id), not bare. Line-level fulfillment linkage — link-don't-convert, matchingorder_header.fulfilled_sale_id's own established asymmetry (no reciprocal column on POS's side either). Fully additive: nullable column, zero backfill, no existingorder_linedata touched.- Column-count impact: +1 column, 0 new tables — 174 → 175 columns, 7 tables unchanged — verified live via
information_schema.columns, matching the per-table sum exactly (see the Column-count reconciliation table above). - Independent verification (separate agent, adversarial, live-DB-checked against a fresh transaction) — pasted, attributed. Found: the core fix #6 deliverable is fully verified and correct — composite FK confirmed genuinely 2-column via
pg_constraint, the prerequisiteUNIQUE(id, tenant_id)onpos.sale_lineconfirmed present, all 3 live-reproduction scenarios (valid same-tenant reference succeeds, NULL succeeds, cross-tenant reference rejected byorder_line_sale_line_id_tenant_fkey) independently reproduced with matching error text, TS source confirmed clean (no dangling imports, no.references()on the raw column), grants unchanged, and the design's stated asymmetry (no reciprocal column onpos.sale_line) confirmed to actually hold. - 2 findings disclosed, neither invalidating fix #6 itself: (1) a test-suite bug caught DURING verification, not by the build itself — the new section's
afterAlloriginally attempted a hardDELETE FROM pos.sale_line, which unconditionally fails sincesale_lineis append-only-enforced (even for the superuser DB connection, per the prior POS-batch reopen's own trigger, fix #8) — since fixed:apps/api/src/orders/__tests__/orders-schema.spec.tsnow uses atryDelete()helper mirroringpos-schema.spec.ts's own established precedent for this exact append-only-table cleanup problem. (2) A new, previously undisclosed finding: 5 bare (non-composite) FKs remain insideordersitself —order_line.order_id,order_payment.order_id,order_fulfillment.order_id→order_header.id;order_fulfillment_line.order_fulfillment_id→order_fulfillment.id;order_template_line.order_template_id→order_template.id— none of whose parent tables (order_header/order_fulfillment/order_template) currently carryUNIQUE(id, tenant_id). This is the identical cross-tenant-write-exposure bug class this entire remediation effort exists to close, sitting inside Orders' own internals, immediately next to the column this fix just touched — not named anywhere in the design doc or any prior PROJECT_DECISIONS entry. Logged toOPEN_ITEMS.mdas a new row, cross-referenced to PROJECT_DECISIONS #50 — a future Orders reopen candidate, not fixed in this pass. - Migration:
packages/db/migrations/20260710040000_headerline_orders_fix6.sql. Schema file:packages/db/src/schema/orders/line.ts(orderLinegains thesale_line_idcolumn plus the table-level compositeforeignKey({...}), namedorder_line_sale_line_id_tenant_fkey— no.references()on the raw column). - Tests:
apps/api/src/orders/__tests__/orders-schema.spec.ts, new section M (3 tests: valid same-tenant reference resolves via JOIN; NULL succeeds; cross-tenant reference rejected). Full suite green: 46/46 (up from 43/43 before this fix).
Gap-Fill Batch reopen (2026-07-20) — B10 forfeited deposits
Third reopen of orders, this time as 1 of 5 modules touched by the 2026-07-20 Gap-Fill Batch — an architect-authorized fix pass against 6 confirmed gaps from the 2026-07-19 gap-validation report (vrida-gap-validation-2026-07-19.md), each with a pre-decided shape. orders' own named gap was B10 — forfeited deposits. See PROJECT_DECISIONS #74 for the full record, including the pasted collision-detection discipline and cross-module delta table.
orders.order_payment.status—chk_order_payment_statuswidened in place to add a 7th value,'forfeited', to the existing (scheduled,due,paid,failed,refunded,cancelled) set. No column added.- New function + trigger:
orders.guard_order_payment_status()/trg_order_payment_guard_status(BEFORE UPDATE OFstatus) — a no-op-plus-terminal-reject guard: same-value re-writes tolerated, any transition out of'forfeited'rejected. Mirrorsnotifications.delivery_attempt's own monotonic-guard shape (architect-selected — no closer precedent existed onorder_header/order_paymentthemselves). Deliberately scoped to just this one terminal-value rule, not a full ordinal state machine across all 7 statuses (they branch, not chain). See the dedicated section above ("The forfeited-deposit terminal-status guard"). - Column-count impact: none — 175 columns, 7 tables unchanged. This is a CHECK-widen-plus-trigger fix only.
- Named open question, deliberately not decided by this fix: whether
'forfeited'still counts towardorder_header.balance_due_cents, and whether it posts anywhere inbilling— left toOrderService's own future build. - Migration:
packages/db/migrations/20260720000007_orders_reopen_forfeited_deposits.sql. - Tests:
apps/api/src/orders/__tests__/orders-schema.spec.ts, +4 new tests (due→forfeited transition; forfeited→refunded transition rejected; same-value no-op tolerated; an unrelated non-forfeited row's normal transition unaffected). Full suite for the file: 51/51 (up from 46/46 before this fix).
Service layer
No OrderService exists yet — schema-only this pass, same pattern as every other module's deferred service layer.
JSONB columns
order_header.attributes (vertical/custom extension point — example shape not yet documented, logged to OPEN_ITEMS per a resolved design decision: deferred to build, real vertical-attribute needs not yet known). order_header.decision_provenance, order_line.decision_provenance, order_payment.decision_provenance, order_fulfillment.decision_provenance, order_template.decision_provenance (all following the codebase-wide decision_provenance convention — reason/evidence/confidence/memory_refs).
Open items carried forward (see OPEN_ITEMS.md for full text)
draft_po_idforward-ref — when Purchasing is designed/built.stripe_payment_intent_idforward-ref — when Payments is built.charge_account_refforward-ref — when Billing is built.tracking_number/carrierforward-refs — when a Delivery/Shipping module is built.search_vector— when Search is designed/built in v2.order_numberpg_trgm index — whenpg_trgmis enabled.- No
OrderServiceyet — schema-only this pass. - Cross-consumer oversell (order + POS both grab the last unit) — when
InventoryService.completeSale()/reserve()implement oversell detection (same trigger as pos's existing row — extend, don't duplicate). - Agent discount/margin ceiling cross-reference — same underlying
agent_duty_grant.spend_limit_centscumulative-tracking gap already logged against identity/pricing/ai. order_header.attributesJSONB example shape — when real vertical-attribute needs are known.- A potential future Files-module seam (signed quote/contract document) — registered, not built; v1 never had one either.
- New 2026-07-10 (Header/Line Remediation reopen, disclosure): 5 bare (non-composite) FKs remain inside
ordersitself —order_line.order_id/order_payment.order_id/order_fulfillment.order_id→order_header,order_fulfillment_line.order_fulfillment_id→order_fulfillment,order_template_line.order_template_id→order_template— the identical cross-tenant-write-exposure bug class this remediation effort exists to close, found by independent verification of fix #6, not named in the design doc. Trigger: a future Orders reopen — addUNIQUE(id, tenant_id)toorder_header/order_fulfillment/order_templatefirst, then upgrade all 5 FKs to composite form. - New 2026-07-20 (Gap-Fill Batch, B10, disclosure): does
order_payment.status = 'forfeited'still count towardorder_header.balance_due_cents? Does a forfeited deposit post anywhere inbilling? Deliberately NOT decided by the B10 fix itself. Trigger: whoever buildsOrderServicemust decide both before a'forfeited'row is used in any balance or revenue-recognition calculation. See PROJECT_DECISIONS #74.