Skip to content

Decision 0018 — Inbound Cardinality, Receipt And Reversal Authority

  • Status: ACCEPTED
  • Authority: RATIONALE_ONLY
  • Date: 2026-08-26
  • Source baseline: qa@49fdcb61963a9e475bb61ec3d6d48b5790329adb
  • Requires: 0017-tenant-runtime-authority.md
  • Implementation handoff: ../tasks/NS2-019.md

Context

Current contracts accept many commercial lines and many fulfillment items, and the MySQL writer validates that a fulfillment item references a line in its selected commercial order. The active inbound path is still single-line in practice:

  • the UI creates one item and executes only detail.items[0];
  • receipt policy rejects an inbound order unless its total line count is exactly one;
  • UQ_NS2_LOT_SOURCE_ITEM permits only one active lot/receipt per fulfillment line;
  • receipt finalization marks the entire order complete after one line;
  • Scale ticket selection is order-level, no-scale eligibility is unresolved, and receipt reversal has a capability but no command/writer;
  • the receipt transaction already creates Lot and Ledger together, while Warehouse Map already owns UUID-backed placement separately.

The accepted database has multi-line records, immutable ledger rows and active placement identity. The missing contract is cardinality and orchestration, not a missing generic workflow platform.

Decision

Cardinality And Conservation

Commercial Purchase 1 -> many Commercial Lines
Commercial Line 1 -> many Inbound Fulfillment Lines
Inbound Fulfillment Order 1 -> many Fulfillment Lines
Fulfillment Line 1 -> many Receipt Events
Receipt Event 1 -> 1 Inventory Lot -> many immutable Ledger Entries
Inventory Lot 1 -> 0 or 1 active Warehouse Map Placement

Each fulfillment line references exactly one commercial line from the selected purchase. Summed active fulfillment quantity for a commercial line cannot exceed ordered quantity. Summed finalized receipt quantity cannot exceed the fulfillment line. Receipt net weight follows the user-entered physical evidence; the previous fulfillment weight is not an upper bound. A duplicate command UUID with the same normalized payload is idempotent; a different payload conflicts.

ordered = open fulfillment + received - reversed and received = lot opening ledger + receipt corrections + receipt reversals are binding readbacks. Quantity over receipt is rejected in Phase 2; this does not impose a planned-weight cap.

2026-09-10 direct user clarification (NS3-101/104): user-entered weight is authoritative; do not reject it merely for exceeding the previous line weight. Do not add mandatory planned/actual fields, a tolerance gate or a new schema for this purpose. Existing quantity, evidence, ledger and correction/reversal contracts remain.

Typed Inbound Policy

The existing policy/Runtime owner adds two code-owned typed settings:

  • INBOUND_SCALE_POLICY: REQUIRED or CERTIFIED_NO_SCALE_ALLOWED;
  • INBOUND_PARTIAL_RECEIPT_POLICY: ALLOWED or FULL_ONLY.

The two acceptance profiles are semantic policy combinations, not customer names or numeric IDs:

  • scale required + partial allowed;
  • certified no-scale allowed + full only.

Missing, invalid or unavailable policy blocks the command. No-scale requires explicit certified quantity/net-weight evidence, reference and reason; planned fulfillment weight is not physical proof.

Scale Work And Evidence

No durable queue or new package is added. The Scale work queue is a read projection of open inbound fulfillment lines plus effective policy. A fulfillment line is the work owner. The existing Scale ticket lifecycle is the weigh session; its completed gross/tare result is the immutable evidence projection. The ticket gains an exact fulfillment-line UUID relationship so an order-level ticket cannot satisfy the wrong line.

Scale correction/void stays owned by @ctc/scale. Receipt finalization locks and validates the chosen completed evidence in the same MySQL transaction but does not mutate or delete it.

Receipt Transaction Owner

@ctc/inventory owns the finalize/correct/reverse command contract and conservation policy. @ctc/db-mysql implements one transaction per command and owns numeric compatibility, locks, schema checks and authoritative UUID reread.

Finalize locks tenant, fulfillment line, prior receipt totals and optional Scale evidence; inserts an immutable receipt event, creates one lot, appends its opening ledger entry and updates line/order status from reconciled totals. The order becomes complete only when every active line is complete.

The route remains authentication -> effective capability -> parse -> command -> response. React selects a line and typed evidence; it owns no business invariant or SQL.

Placement

Warehouse Map remains the sole placement effect owner. Finalize creates the lot as HOLD when it is unplaced. The existing UUID-targeted Warehouse Map placement command creates/updates the active placement and only then opens the lot. Placement failure leaves a truthful retryable HOLD lot; it does not roll back a committed physical receipt or fabricate success.

Correction And Reversal

Correction never rewrites history. It appends a reasoned receipt correction and compensating ledger delta under optimistic version/conservation checks. Reversal marks the receipt event reversed, appends the exact negative ledger delta, closes/releases active placement, reopens fulfillment line/order totals and retains Scale evidence.

An active or progressed downstream allocation blocks receipt correction/reversal until its owning allocation reversal completes. Lots and ledger rows are never deleted. Unknown commit results are recovered only by authoritative receipt UUID reread and normalized payload comparison.

Capability And Exposure

Existing inventory.receipt.finalize and inventory.receipt.reverse capabilities remain the action gates, independent from menus. Module/policy readiness gates execute server-side. The existing Inbound Fulfillment surface becomes line-aware; no new Product surface is approved.

Alternatives

Alternative 1 — Patch The Current One-Line Scale-Required Flow

Keep one lot per source line, remove only the UI items[0] assumption and continue completing an order from the first receipt. This is the smallest initial diff but cannot support partial receipts, multi-line completion, no-scale policy or reversal. Rejected because it preserves the blocking data model and causes a second rewrite.

Alternative 2 — Add A Workflow Service, Queue And Event Bus

Create generic workflow records and asynchronous events across commercial, fulfillment, scale, inventory and placement. This increases packages, dependencies, failure states and unknown-result coordination before any immediate consumer needs them. Rejected as speculative infrastructure.

Alternative 3 — Compose Existing Owners With One Inventory Command

Keep commercial intent, fulfillment work, Scale evidence, inventory conservation and Warehouse Map placement in their current semantic owners. Add only the receipt event/cardinality data required by partial receipt and reversal, with one MySQL transaction per command and authoritative reread. Accepted as the smallest boundary that satisfies every required invariant.

Consequences

  • NS2-019 must remove every inbound business use of items[0] and make the UI line-aware.
  • The one-active-lot-per-source-line index must be replaced by receipt-event identity and explicit active-total constraints; migration requires exact preflight/readback/rollback.
  • Typed inbound policy becomes part of effective Runtime and Admin readiness diagnostics.
  • Receipt and placement remain separate commands with truthful HOLD recovery between them.
  • No new package, service, queue, event bus, Product surface, customer backend or architecture exception is approved.

Expected implementation structure: existing module roots, Web route/UI/composition, db-mysql adapter, one forward/rollback migration and focused tests. Public/domain/application identities remain UUID.

Revisit

Revisit only if measured throughput requires an asynchronous queue, one receipt must span multiple fulfillment lines, regulatory evidence requires a separate weigh-session record, or Warehouse Map moves behind an external service. Any revisit requires a new accepted ADR and migration plan.

Rollback before implementation is to supersede this decision. After implementation, run the paired schema rollback only after proving zero multi-receipt/correction/reversal rows and restoring the previous one-line caller boundary; never drop receipt or ledger evidence opportunistically.

---

Source: new-system/docs/decisions/0018-inbound-authority.md — ctc repository · This page is an English translation of the source document. Identifiers, file paths and code symbols are unchanged.