measured at 5d527ffb
Created: 2026-07-26 | Last Updated: 2026-07-26
Third filled instance of
module-scorecard-template.md(afterpo-scorecard.mdandreceivings-scorecard.md). Read-only analysis; recommendations only. Evidence isfile:line/ endpoint againstD:\BCPOSGit\qiiubon branchmainat commit5d527ffb. Gathering source-of-record:artifact-transfers-gathering-repo.md+-decisions.md+-market.md.Why Transfers was scored third. It is the third node of the supply-chain pipeline (PO β Receivings β Transfers) and the sharpest remaining test of the framework's single-owner rule on a shared ledger: Ship/Receive write
InventoryMovement(TransferOut/TransferIn) and mutateLocationProductβ tables the Inventory module owns β while the damage write-off is delegated to the Adjustments module viaTransferDamageAdjustmentService(ADR-0083 explicitly rejected a privateTransferDamagemovement type to avoid forking Adjustment). Those cross-module writes are scored here as the transfer's semantic outcomes; the tables' and the Adjustment's own CRUD stay dependencies. If the rubric survives a module that writes three neighbors' tables, the boundary rules are proven.
| Field | Value |
|---|---|
| Module name | Transfers (TransferOrder + TransferConfirmation) |
| File | transfers-scorecard.md (target: docs/modules/transfers-scorecard.md) |
| Author / session | Claude (Opus 4.8), coordinated multi-agent analysis |
| Created | 2026-07-26 |
| Last-Updated | 2026-07-26 |
| Repo commit (SHA) analyzed | 5d527ffb (branch main) |
| Governing ADRs | 0022 (Two-Sided Transfers with Confirmation β the foundational contract), 0083 (TransferConfirmation as Posted Document + Damage fold-in). Inherited: 0020 (LocationProduct), 0021 (separate documents), 0023 (three-tier history), 0024 (ref enums), 0062 (attribution), 0082 (PublicId β tcf/tcln prefixes), 0037 (datetime2(3)), 0081 (Inventory hub), 0033. ADR-0070 (QuestPDF "Transfer Slip") is proposed, Phase 2 β not shipped. |
| Governing design docs | docs/design/inventory-supply-chain.md Β§4.4/Β§5/Β§7 (Draft, frozen 2026-03-13 β pre-ADR-0083, stale); docs/design/system-inventory-answers.md (generated inventory) |
| Status of this analysis | Adversarially-verified β Fable pass (findings + resolutions in artifact-transfers-fable-review.md; score 63β60); live-confirmed against the running app 2026-07-26 (Β§5c) |
LΓ©elo asΓ (en simple). Foto del 2026-07-26 (commit
5d527ffb) de cuΓ‘nto del mΓ³dulo de traslados entre ubicaciones (mover mercancΓa de una tienda/bodega a otra) estΓ‘ construido, medido contra el estΓ‘ndar de POS de retail establecidos + mercado moderno + el roadmap propio de QIIUB. El nΓΊcleo de dos lados (enviar baja el stock en origen, recibir lo sube en destino, con documentos de confirmaciΓ³n y detecciΓ³n de daΓ±os) estΓ‘ hecho y probado; lo que falta es amplitud operacional (bucket de en-trΓ‘nsito, fletes, motivos, reportes) y todo lo avanzado (landed cost, serie/lote, cantidades sugeridas, aprobaciΓ³n, multi-destino).
Purpose. The Transfers module moves stock between two of a merchant's own locations as a two-sided document pair (ADR-0022): a TransferOrder (the sender's intent + running per-line totals) and one or more TransferConfirmation posted documents (one per Ship event, one per Receive event, each carrying per-line deltas β ADR-0083). Shipping decrements the source on-hand and posts a TransferOut movement; receiving increments the destination on-hand and posts a TransferIn movement. Damaged units received are written off to inventory via an auto-InventoryAdjustment. The unit cost travels with the goods; the destination selling price is deliberately left to inherit the variant base.
Vertical relevance.
core. Multi-location retailers rebalance stock between stores/warehouses constantly.used. Inter-branch movement is real, but lot/batch traceability across locations (TRF-F029) is absent β relevant for controlled-substance chain-of-custody, though less hard-blocking than the receiving-side lot gap.core. Moving materials between yards/jobs/trucks.Actors & roles. Every transfer endpoint requires SystemAdmin | Admin (no Manager, no Cashier) β Roles("SystemAdmin","Admin") on all 13. Surfaces: API (FastEndpoints, complete) and portal (qiiub-portal β list, detail hub, dedicated full-page receive flow, create/line-edit drawers, confirmation print drawer). No POS, no qiiub-admin, no mobile surface. Not location-scoped β an admin transfers between any two of the merchant's locations; there is no per-operator "my store" restriction (TransfersListPage/receive are role-gated only).
Not-in-this-module. The InventoryAdjustment that records damaged units is Adjustments-owned (the transfer only triggers it β Β§2). The LocationProduct on-hand/cost table and the InventoryMovement ledger are Inventory-owned (the transfer writes them, scored here as outcomes). Reorder/replenishment that reads transfer history is Purchasing. Offline sync of transfers is a future Sync dependency. Vendor returns / out-to-supplier live in PO/Receivings, not here. See Β§2.
POST /transfer-orders. Pick From and To locations (must differ β CreateTransferOrderEndpoint validator :59-60); a TRF-000xxx DocumentNumber is minted. Zero-line drafts are legal (:64-71).POST /transfer-orders/{id}/lines while Draft (the surface the portal uses; TransferDrawer.tsx:117 always creates with lines: []). On the AddLine path each line's UnitCost/UnitPrice is snapshotted from the sender's LocationProduct (fallback catalog variant) at add-time (AddTransferOrderLineEndpoint.cs:155-156), with guards: duplicate-variant reject, SKU-not-stocked-at-sender reject, and qty > Available (OnHand β Reserved) reject (:114,:138,:146-153). Lines editable/removable (single + atomic bulk-delete) while Draft. (Exception: the create-with-inline-lines path CreateTransferOrderEndpoint.cs:166-167 writes client-supplied UnitCost/UnitPrice verbatim and skips those guards β an API-only hole the portal never exercises; see Β§5b.)POST /transfer-orders/{id}/ship. Draft/Submitted β InTransit. Stock is re-validated against Available before any mutation (:163-183); then, in one transaction: set QuantityShipped, decrement sender on-hand, write a TransferOut movement (Quantity = βshipped), and create a Shipped TransferConfirmation (+ per-line deltas). Ship quantity may be less than requested (partial ship in a single event); it can never exceed Available. Concurrency: SaveChangesWithStockConcurrencyRetryAsync β 409 on RowVersion conflict.POST /transfer-orders/{id}/receive. InTransit/PartiallyReceived β PartiallyReceived or Closed. Per line, capture received + damaged; over-receipt (received+damaged > shipped-so-far) is rejected (400). In one transaction: increment destination on-hand by received+damaged, write a TransferIn movement, auto-create the destination LocationProduct if absent (Cost = line UnitCost; Price = NULL β inherit), create a Received confirmation (+ deltas), and β for damaged units β spawn a Completed InventoryAdjustment (βdamaged, reason ADJ-XFR-DAMAGE) so net destination gain = intact/sellable. When every line is fully accounted, Receive auto-collapses straight to Closed.POST /transfer-orders/{id}/close. Manual seal of a PartiallyReceived (or legacy Received) order β Closed (write-off of a known short/lost-in-transit gap). No inventory side effects.POST /transfer-orders/{id}/cancel. Draft only β Cancelled; never moves stock. Once InTransit there is no reverse-transit path β only Receive (full/partial) then Close.State machine: Draft(0)/Submitted(1) β InTransit(3) β PartiallyReceived(4) β Closed(6); full receive jumps InTransit β Closed; Draft β Cancelled(7). Every transition is transactional or an atomic ExecuteUpdate guarded on the source status (409 otherwise). (Submitted, Shipped, Received enum values are largely vestigial β the happy path skips them.)
| Module | Direction | What crosses the boundary | Scoring treatment |
|---|---|---|---|
| Adjustments β damage write-off | downstream/peer | Receive spawns a Completed InventoryAdjustment (βdamaged) via TransferDamageAdjustmentService |
dependency β the Adjustment doc/CRUD is Adjustments-owned; the transfer-triggered write-off outcome is scored here (TRF-F013, single-owner) |
| Inventory β LocationProduct (on-hand/cost) | upstream | Ship decrements source, Receive increments dest, auto-creates dest LP, cost travels | dependency β table owned upstream; the transfer-triggered on-hand/cost writes are scored here (TRF-F005/F006/F014) |
| Inventory β InventoryMovement ledger | shared-infra | Ship writes TransferOut, Receive writes TransferIn |
dependency β ledger owned upstream; the transfer's movement writes are the audit outcome scored here (TRF-F016) |
| Inventory β Reserved / Available | upstream | Ship/AddLine gate on Available = OnHand β Reserved | dependency β reservations owned by Inventory |
| Document Sequencing | shared-infra | mints TRF- (order) + TCF- (confirmation) numbers |
shared-infra; the transfer's use is TRF-F011, the service is a dependency |
| Catalog / Inventory master data | upstream | Location, ProductVariant, UoM, TaxGroup referenced | dependency β tables owned upstream |
| Purchasing β reorder engine | downstream | may read transfer movement history for replenishment | dependency β out of scope (PO-F025) |
| Sync engine (offline-first) | downstream/future | bidirectional push/pull of transfer docs | dependency β not wired for transfers at this commit |
Boundary statement. Transfers owns the TransferOrder + TransferOrderLine + TransferConfirmation + TransferConfirmationLine tables, every endpoint under Api/Features/Transfers/, the two-sided state machine, and the act of moving stock (the source-decrement-on-ship and destination-increment-on-receive, plus the movement and cost writes those perform). It does not own the LocationProduct/InventoryMovement tables it writes, the InventoryAdjustment its damage path spawns, the reservation math it reads, reorder, or offline sync.
Single-owner rule (the headline test β a shared ledger). Ship (ShipTransferOrderEndpoint.cs:212-226) and Receive (ReceiveTransferOrderEndpoint.cs:236-278) physically write Inventory's LocationProduct and InventoryMovement. That code lives in this module, but "moving stock between locations" is the transfer's semantic outcome β scored once here (TRF-F005/F006/F016). Inventory's doc scores the table CRUD, not the transfer-triggered write. Symmetrically, the damage write-off's InventoryAdjustment is Adjustments-owned; ADR-0083 deliberately reused the Adjustment type and reason code rather than fork a TransferDamage movement β so the transfer's damage outcome is TRF-F013 here, and the adjustment document itself is TRF-D01 (Dependency), never re-scored. This is exactly the inflation the rule prevents.
Shared-infra split. DocumentSequence mints TRF-/TCF- numbers β the service is TRF-D04 (dependency); the transfer's use ("documents carry a human number") is TRF-F011 (scored). Same split PO/Receivings applied.
Boundary-leak checklist (to be audited in the Fable pass):
Cost column; "cost moves with the goods" scored here (TRF-F014, Partial β no avg recompute).Status legend: Implemented / Partial / Missing / Dependency (excluded). Source: RMS / RMH / market / QIIUB-original.
Comparison map, not a backlog. The universe is drawn from RMS/RMH (Store Ops + HQ Worksheet 330) + the modern market (Square/Clover/Lightspeed/NetSuite/Cin7) + QIIUB's own ADRs. A
Missingmeans "that world has it and QIIUB doesn't (yet)," not a commitment. Legacy artifacts (HQ-push, commit-vs-save, GL postings) are N/A candidates. The decided work is the prioritized Β§7 gaps.
| ID | Name | Description | Source | Status | Evidence | Notes |
|---|---|---|---|---|---|---|
| TRF-F001 | Transfer order document | Header + lines representing one inter-location transfer | RMS+RMH+market | Implemented | POST /transfer-orders β CreateTransferOrderEndpoint; TransferOrder/TransferOrderLine entities |
Mints TRF- number; zero-line draft legal |
| TRF-F002 | Origin/destination + sourceβ dest | Pick From/To location; block same-location | RMS+RMH+market | Implemented | FromLocationId/ToLocationId; validator not-same-location (CreateTransferOrderEndpoint.cs:59-60); composite FKs MerchantId-first |
|
| TRF-F003 | Line items + qty + sender on-hand | Add variant + qty; see sender OnHand/Reserved/Available | RMS+RMH+market | Implemented | AddTransferOrderLineEndpoint; Available shown per line (GetTransferOrderEndpoint.cs:225-253); TransferLineEntryBar live sender-stock (search name/SKU/barcode) |
Quick-scan-on-entry (market #19) folded here |
| TRF-F004 | Two-sided state machine | DraftβInTransitβPartial/Closed (+ auto-close, Cancel) | RMS+RMH+market | Implemented | TransferOrderStatus enum; Ship/Receive/Close/Cancel endpoints; atomic ExecuteUpdate guards |
Cancel-draft + auto-close folded here (no double-count) |
| TRF-F005 | Deduct source on ship | Source on-hand drops + TransferOut movement |
RMS+RMH+market | Implemented | ShipTransferOrderEndpoint.cs:212 (decrement) + :214-226 (TransferOut, Quantity=βshipped, cost/price carried) |
Inside a txn; stock validated before mutation |
| TRF-F006 | Add destination on receive | Destination on-hand rises + TransferIn movement; auto-create dest LP |
RMS+RMH+market | Implemented | ReceiveTransferOrderEndpoint.cs:236,:251 (increment received+damaged) + :266-278 (TransferIn); auto-create LP :245-262 (.Local dedup :223-232) |
Dest LP Cost=UnitCost, Price=NULL (inherit) |
| TRF-F007 | Partial ship | Ship fewer than requested (single ship event) | RMS+RMH+market | Implemented | ShipTransferOrderEndpoint.cs:121-127,:211 β body Lines[].QuantityShipped may be < requested; capped at Available |
Single event only; ship-in-installments is TRF-F035 (Missing) |
| TRF-F008 | Partial receipt | Receive less than shipped; order stays PartiallyReceived | RMS+RMH+market | Implemented | Receive β PartiallyReceived (:294-329); test PartialReceive_ThenManualClose_WritesOffGap (TransferOrderTests.cs:423) |
Over-receipt rejected 400 (:145-152) |
| TRF-F009 | Two-sided posted confirmations | Per-event TransferConfirmation + delta lines (Ship/Receive) |
QIIUB-original+market | Implemented | ADR-0022/0083; TransferConfirmation(+Line) with QuantityDelta/QuantityDamagedDelta; Shipped conf ShipTransferOrderEndpoint.cs:195-239, Received conf in Receive |
Summing deltas reproduces the running line totals |
| TRF-F010 | Cross-merchant isolation | Every transfer op tenant-scoped | QIIUB-original (tenancy) | Implemented | Composite PK/FK MerchantId-first on all 4 entities; 6+ cross-merchant tests (TransferOrderTests.cs:912,930,984,1106,1142,1208 + StatCard :116) |
Critical Rule #1; verified MerchantId-first in every endpoint |
| TRF-F011 | Document numbering | Human TRF-000xxx (order) + TCF-000xxx (confirmation) |
RMS+RMH+market | Implemented | IDocumentSequenceService "TRF"/"TCF"; unique (MerchantId, DocumentNumber) on both tables |
Sequence service itself is shared-infra (TRF-D04) |
| ID | Name | Description | Source | Status | Evidence | Notes |
|---|---|---|---|---|---|---|
| TRF-F012 | Short-close / write-off gap | Manually seal a partial transfer, writing off the lost-in-transit remainder | RMS+RMH+market | Implemented | CloseTransferOrderEndpoint.cs:84-90 (PartiallyReceived/Received β Closed); positive short-close path TransferOrderTests.cs:423; still-Draft-409 guard :1125 |
No inventory effect β the gap is simply not received |
| TRF-F013 | Discrepancy + damage capture | Capture damaged units per line/event and write them off to the ledger | RMS+RMH+QIIUB-original | Partial | TransferOrderLine.QuantityDamaged + TransferConfirmationLine.QuantityDamagedDelta (capture β always works); TransferDamageAdjustmentService.cs:87-140 (Completed Adjustment βdamaged + movement, when the reason code exists) |
Partial (Fable-conservative, live-evidenced): the capture half (damaged qty on line + confirmation delta) works unconditionally; the ledger write-off half is not reliably provisioned β ADJ-XFR-DAMAGE is seeded only in DevSeed S02 (S02_ReferenceDataSeed.cs:88), no production path, and proven skipped live 2026-07-26 (0 InventoryAdjustment, dest overstated 27 vs 25). Alternative: Implemented if you count a fresh DevSeed as the provisioning reality (QIIUB is pre-real-merchant) β a one-flip decision (see Β§6 sensitivity). Short/never-arrived captured implicitly as the shipped-minus-accounted gap |
| TRF-F014 | Transfer cost carry / valuation | Move item at cost; set/recompute destination cost | RMS+RMH+market | Partial | Cost travels: line UnitCost snapshot β both movements; auto-created dest LP Cost = UnitCost (ReceiveTransferOrderEndpoint.cs:252) |
Partial: cost carries to a new dest LP, but an existing dest LP's cost is not re-averaged (:236-237 updates qty only β live-confirmed 2026-07-26: existing dest cost stayed NULL). Weighted-average-at-destination = missing |
| TRF-F015 | Confirmation print (packing slip) | Printable ship/receive confirmation (acta / packing slip) | RMS+RMH+market | Implemented | TransferConfirmationPrintDrawer.tsx:49-54 (window.print(), @media print) off GET /transfer-confirmations/{id} (GetTransferConfirmationEndpoint) |
Client-side print only. Branded server-side QuestPDF "Transfer Slip" (ADR-0070 Phase 2) is proposed, not shipped β see Β§8 |
| TRF-F016 | Transfer history / audit trail | Per-line movement log (who/when/qty/cost) + document trail | RMS+RMH+market | Implemented | TransferOut/TransferIn movements carry UnitCost/ReferenceType/DocumentNumber; ADR-0062 attribution; confirmations = ADR-0023 document-trail tier |
GetTransferOrder resolves audit user names (:257-275) |
| TRF-F017 | In-transit inventory bucket | Issued-but-not-received stock visible as its own bucket / items-in-transit report | market+RMS | Missing | Status InTransit exists but no in-transit quantity row/bucket; derived only as shipped β received β damaged (design Β§7 GL-in-transit :918 not implemented) |
Units leave source at Ship and reappear at dest at Receive β nowhere stocked in between. Natural home: a derived report or a clearing pseudo-location. Tier (guard-a, Fable F-5): the source-of-record tiers "in-transit tracking" T1, but the module is demonstrably usable without a bucket (the T1-worthy half β the InTransit state β is F004, Implemented); the distinct bucket/report is a T2 visibility refinement, so the Missing residual is scored T2, not T1 |
| TRF-F018 | Freight / shipping header fields | Ship via, FOB, freight, est. shipping, other fees on the order | RMS+RMH | Missing | No freight/ship-via/FOB columns on TransferOrder (verified) |
Evergreen home: nullable columns on TransferOrder. RMS Header tab |
| TRF-F019 | Reason codes on transfer | Categorize the transfer (rebalance / return / consumed / lost) | RMS+RMH | Missing | No reason-code FK on TransferOrder; ReasonCode used only by the internal damage adjustment |
RMS InventoryTransferLog.ReasonCodeID |
| TRF-F020 | Barcode scan-to-receive | Scan units to reconcile received quantities | market | Partial | Receive page has client-side search over name/SKU/barcode + keyboard fill (Enter/+) (TransferReceiveCreatePage.tsx:126-136,:183-223) |
Partial: search-assisted receiving present; no true scan-to-increment reconcile loop. What works = find-by-barcode + fill; missing = scan-count reconciliation |
| TRF-F021 | Notifications | In-transit / received / discrepancy alerts | market | Missing | No notification wiring for transfer events | Pairs with a future events/notifications module |
| TRF-F022 | Transfer reporting | Items-in-transit / status / valuation reports | RMS+RMH+market | Partial | List StatCards: Draft/InTransit/Closed counts over the full filtered set (ListTransferOrdersEndpoint.cs:157-167) |
Partial: document-count status summary exists; dedicated items-in-transit (units) + valuation reports absent (pairs with TRF-F017) |
| TRF-F023 | Notes / remarks | Free-text remarks on order + confirmation | RMS+RMH+market | Implemented | TransferOrder.Notes (500), TransferConfirmation.Notes (500) |
|
| TRF-F024 | Effective / required dates | Hold-until effective date + requested delivery date | RMS+RMH | Missing | Only RequestDate/ShippedDate/ReceivedDate/ClosedDate β no effective/required-by |
RMS Worksheet 330 effective date |
| TRF-F025 | Requisitioner / people fields | Who requested / who confirms, as first-class fields | RMS+RMH | Missing | RequestedByEmployeeId/ClosedByEmployeeId/TransferConfirmation.EmployeeId columns exist but are never written (reserved/dead) β attribution goes via CreatedByUserId/UpdatedByUserId |
Reserved-unwired = Missing (template Β§3). Evergreen: wire the existing columns |
| TRF-F038 | Reject shipment at receive | Receiver refuses a shipment so goods return to sender (vs absorbing/damaging) | QIIUB-original (ADR-0022)+market | Missing | TransferConfirmationType.Rejected enum value exists but is never emitted; no reject path β once InTransit the only exits are Receive (absorb) and Close (write off). Natural home: ReceiveTransferOrderEndpoint |
Fable F-1 (guard-e miss): ADR-0022:35 explicitly promises "Receiver can reject or partially accept" β partially accept = TRF-F008 (Impl), reject was unscored. T2 (the governing ADR names it; two-sided market systems support it); T3 arguable |
| ID | Name | Description | Source | Status | Evidence | Notes |
|---|---|---|---|---|---|---|
| TRF-F026 | Discrepancy investigation status | Flag/track resolution of issued-vs-received variances (Match tab) | RMS | Missing | No investigation/resolution flag; variance is only the implicit line gap | RMS HQ "Status of quantity discrepancies" |
| TRF-F027 | Landed cost on transfer | Add freight/duty/handling on top of the item cost so the destination's cost reflects the true landed cost of moving it, not the bare unit cost | RMS+market | Missing | No charge model on the transfer; the item moves at the bare line UnitCost (live: travelled at 239.02, no fees). Natural home: a TransferCharge table + a cost-distribution method |
Rare β ERP/legacy-HQ tier, hence T3. Supported on transfers specifically by: established-HQ retail POS (transfer Receive tab β Cost+Tax+Shipping+Other Fees + distribution method) and ERP/mid-market (NetSuite, Cin7/DEAR). Not in Square for Retail / Clover / Lightspeed Retail, and not in WIDS365 (bare-cost carry). Sibling of RCV-F020 β but landed cost is more expected on receiving (T2 there) than on inter-location transfer (T3 here) |
| TRF-F028 | Serialized item handling | Capture serial numbers at ship + receive | RMS+RMH+market | Missing | No serial fields on transfer entities | Differentiator (NetSuite/Cin7/Lightspeed) |
| TRF-F029 | Lot / batch tracking | Carry lot/batch numbers through the transfer | RMH+market | Missing | No lot/batch fields | Rx-relevant (controlled-substance chain-of-custody); pairs with RCV-F019 |
| TRF-F030 | Suggested / auto transfer qty | Compute qty from reorder point / sales history / fixed | RMS+RMH | Missing | No worksheet/suggested-qty engine | RMS HQ Worksheet 330 auto-suggest |
| TRF-F031 | Auto-replenishment transfers | Automatic inter-location rebalancing (min/max, velocity) | market | Missing | Not built | NetSuite/Cin7/Lightspeed analytics tier |
| TRF-F032 | Multi-destination / bulk transfer | One worksheet fanning to many receiving locations | RMS+market | Missing | One From / one To per order (single ToLocationId) |
RMS HQ multi-store worksheet; this is Distribution's territory in QIIUB β candidate to fold to Distribution's scorecard when scored |
| TRF-F033 | Request / approval workflow | Store requests; approver authorizes before issue | RMS+market | Missing | No approval step; any Admin ships directly | NetSuite/Cin7 |
| TRF-F034 | ASN / shipment notice | Advance notice of inbound to the destination | market | Missing | No ASN concept | ERP-tier; pairs with RCV-F029 |
| TRF-F035 | Multi-shipment (installments) | Ship a transfer in several shipments over time | market | Missing | Ship transitions Draft/Submitted β InTransit once; no top-up ship | QIIUB models one ship event; partial ship (TRF-F007) is single-shot |
| TRF-F036 | UoM / master-pack qty | Transfer in supplier pack multiples (MPQ) | RMS | Missing | Transfer lines have no purchasing-UoM/MPQ (unlike PurchaseReceiptLine) | RMS "Use Supplier MPQ" toggle |
| TRF-F037 | Reverse a completed transfer | Un-ship / reverse an in-transit or closed transfer | market | Missing | No reverse-transit path; workaround = a counter-transfer | Cancel is Draft-only (folded TRF-F004); reversing after ship is absent. Tier (guard-a, Fable F-6): source row 34 "reversal/cancel" is tiered T2, but most of that expectation is void-draft (which exists β F004); reversing a completed transfer is the rarer, ERP-tier half β T3. Demotion argued, not silent |
Dependencies (excluded from score) β listed for reproducibility:
| ID | Name | Owner module | Status | Note |
|---|---|---|---|---|
| TRF-D01 | Damage write-off document | Adjustments | Dependency | Code lives here (TransferDamageAdjustmentService) but the InventoryAdjustment doc/CRUD is Adjustments-owned; the transfer outcome is TRF-F013 |
| TRF-D02 | LocationProduct on-hand/cost table | Inventory | Dependency | Written by Ship/Receive; table CRUD owned upstream (transfer writes scored TRF-F005/F006/F014) |
| TRF-D03 | InventoryMovement ledger | Inventory | Dependency | Transfer writes TransferOut/TransferIn; ledger owned upstream (audit outcome = TRF-F016) |
| TRF-D04 | Document sequence service | Shared-infra | Dependency | Mints TRF-/TCF-; the transfer's use is TRF-F011 |
| TRF-D05 | Reserved / Available math | Inventory | Dependency | Ship/AddLine gate on OnHand β Reserved; reservations owned upstream |
| TRF-D06 | Master data (Location/Variant/UoM/TaxGroup) | Catalog / Inventory | Dependency | Referenced; tables owned upstream |
| TRF-D07 | Reorder generation from transfer data | Purchasing | Dependency | May read transfer history (PO-F025) |
| TRF-D08 | Offline sync of transfer docs | Sync (future) | Dependency | Not wired for transfers at 5d527ffb |
N/A β not applicable to QIIUB (excluded; listed so a reader sees they were considered):
| ID | Name | Why N/A (not Missing) |
|---|---|---|
| TRF-N01 | HQ-initiated transfer (central push) | Architecture mismatch. QIIUB stores are configured per-store with no group inheritance (store-config-is-per-store); there is no central-office push model. A future MerchantGroup rebalance flow could revive it, but it is not the inter-location transfer module's job today. |
| TRF-N02 | Auto-create counterpart order via batch sync | Dissolves in the two-sided single-source model. RMS/RMH auto-materialize a counterpart order because HQ and stores run on disconnected DBs synced on a schedule. QIIUB's TransferConfirmation is the counterpart, on one cloud source β there is no separate order to auto-create. |
| TRF-N03 | Approve-lock / commit-vs-save | Superseded by architecture. RMS's explicit Commit (subtract-from-DB) + Approve-lock are UI patterns; QIIUB's Draft β InTransit state machine + soft-delete + RowVersion is that distinction, met a cleaner way (same call as RCV-N01). |
| TRF-N04 | GL / accounting postings on transfer | Billing/accounting is external to QIIUB. RMH posts Stock/Freight control-account entries; QIIUB treats GL as an external integration concern, not a transfer-module capability. |
| TRF-N05 | Vendor-return-as-transfer-out + tax-on-transfer | Belongs to another module. RMH conflates vendor returns (Credit Note) and tax with Transfer Out. QIIUB models vendor returns in PO/Receivings; the inter-location transfer module is store-to-store only. |
Four tables are owned: TransferOrder, TransferOrderLine, TransferConfirmation, TransferConfirmationLine. All extend BaseEntity (soft-deletable β a transfer is an operational document, not a posted-immutable financial record). Composite PK (MerchantId, Id), PublicId varchar(32) unique per merchant, RowVersion, datetime2(3) timestamps β all from BaseMerchantEntityConfiguration<T>. GlobalId default NEWSEQUENTIALID().
TransferOrderConfiguration.cs| Column | SQL type | Null | Business purpose |
|---|---|---|---|
| Id, MerchantId | bigint, int | no | Composite PK; tenancy |
| PublicId | varchar(32) | no | External identity (order prefix, ADR-0082) |
| DocumentNumber | varchar(20) | no | Human TRF-000xxx; unique per merchant |
| FromLocationId | bigint | no | FK β Location (source) |
| ToLocationId | bigint | no | FK β Location (destination) |
| RequestedByEmployeeId | bigint | yes | Reserved / dead β never written |
| Status | tinyint | no | Draft/Submitted/Shipped/InTransit/PartiallyReceived/Received/Closed/Cancelled (0β7) |
| RequestDate | datetime2(3) | no | When drafted |
| ShippedDate | datetime2(3) | yes | Set at Ship |
| ReceivedDate | datetime2(3) | yes | Set at full receive / auto-close |
| ClosedDate | datetime2(3) | yes | Set at Close / auto-close |
| ClosedByEmployeeId | bigint | yes | Reserved / dead β never written |
| Notes | nvarchar(500) | yes | Free-text |
Indexes: unique (MerchantId, DocumentNumber); (MerchantId, FromLocationId, Status), (MerchantId, ToLocationId, Status). FKs (composite, MerchantId-first): From/To Location β both Restrict.
TransferOrderLineConfiguration.cs| Column | SQL type | Null | Business purpose |
|---|---|---|---|
| Id, MerchantId | bigint, int | no | Composite PK; tenancy |
| PublicId | varchar(32) | no | External identity (prefix trln) |
| TransferOrderId | bigint | no | FK β parent order |
| LineNumber | int | no | Ordinal (max+1 on add) |
| ProductVariantId | bigint | no | What moves |
| QuantityRequested | decimal(10,4) | no | Draft intent |
| QuantityShipped | decimal(10,4) | no | Running total shipped |
| QuantityReceived | decimal(10,4) | no | Running total received (intact) |
| QuantityDamaged | decimal(10,4) | no | Running total received-damaged |
| UnitCost | decimal(18,4) | no | Snapshot from sender LP at add-time (cost travels) |
| UnitPrice | decimal(18,2) | no | Snapshot; not written to dest LocationProduct.Price |
Indexes: (MerchantId, TransferOrderId); unique (MerchantId, TransferOrderId, PublicId) (ADR-0082 Β§4.2.1 nested route). FKs: TransferOrder / ProductVariant β Restrict.
TransferConfirmationConfiguration.cs| Column | SQL type | Null | Business purpose |
|---|---|---|---|
| Id, MerchantId | bigint, int | no | Composite PK; tenancy |
| PublicId | varchar(32) | no | External identity (prefix tcf) |
| DocumentNumber | varchar(20) | no | Human TCF-000xxx; unique per merchant (ADR-0083) |
| TransferOrderId | bigint | no | FK β parent order |
| ConfirmationType | tinyint | no | Shipped/Received/PartialReceived/Rejected (0β3; Rejected never emitted) |
| EmployeeId | bigint | yes | Reserved / dead β never written |
| LocationId | bigint | no | FK β Location (From on Ship, To on Receive) |
| ConfirmationDate | datetime2(3) | no | Event time |
| Notes | nvarchar(500) | yes | Free-text |
Indexes: (MerchantId, TransferOrderId, ConfirmationDate DESC); unique (MerchantId, DocumentNumber). FKs: TransferOrder / Location β Restrict.
TransferConfirmationLineConfiguration.cs| Column | SQL type | Null | Business purpose |
|---|---|---|---|
| Id, MerchantId | bigint, int | no | Composite PK; tenancy |
| PublicId | varchar(32) | no | External identity (prefix tcln) |
| TransferConfirmationId | bigint | no | FK β parent confirmation (Cascade delete β the only cascade in the module) |
| TransferOrderLineId | bigint | no | FK β order line (Restrict) |
| QuantityDelta | decimal(10,4) | no | Per-event delta: shipped (Ship) or received-intact (Receive) |
| QuantityDamagedDelta | decimal(10,4) | no | Per-event damaged delta (Receive) |
Indexes: (MerchantId, TransferConfirmationId); unique (MerchantId, TransferConfirmationId, PublicId).
Reserved/unwired: TransferOrder.RequestedByEmployeeId, TransferOrder.ClosedByEmployeeId, TransferConfirmation.EmployeeId (audit is via BaseEntity.CreatedByUserId/UpdatedByUserId). Absent entirely: any serial / lot / batch / expiry / freight / reason-code / effective-date / in-transit-bucket columns.
Referenced (dependency-owned): Location, ProductVariant, LocationProduct (+ QuantityOnHand/QuantityReserved/Cost/LastReceivedDate), InventoryMovement, InventoryAdjustment(+Line), ReasonCode, TaxGroup, UnitOfMeasure, DocumentSequence.
| Surface | Verb+Route | Impl | Realizes | Roles |
|---|---|---|---|---|
| API | POST /transfer-orders |
CreateTransferOrderEndpoint | TRF-F001, F002, F011 | Admin/SysAdmin |
| API | GET /transfer-orders/{PublicId} |
GetTransferOrderEndpoint | TRF-F003, F016 | " |
| API | GET /transfer-orders |
ListTransferOrdersEndpoint | TRF-F022 (StatCards) | " |
| API | PUT /transfer-orders/{PublicId} |
UpdateTransferOrderEndpoint | TRF-F023 (Notes) | " |
| API | POST /transfer-orders/{PublicId}/lines |
AddTransferOrderLineEndpoint | TRF-F003, F014 (cost snapshot) | " |
| API | PUT /transfer-orders/{id}/lines/{PublicId} |
UpdateTransferOrderLineEndpoint | TRF-F003 | " |
| API | DELETE /transfer-orders/{id}/lines/{PublicId} |
RemoveTransferOrderLineEndpoint | TRF-F003 | " |
| API | POST /transfer-orders/{id}/lines/batch-delete |
RemoveTransferOrderLinesEndpoint | TRF-F003 | " |
| API | POST /transfer-orders/{PublicId}/ship |
ShipTransferOrderEndpoint | TRF-F005, F007, F009, F016 | " |
| API | POST /transfer-orders/{PublicId}/receive |
ReceiveTransferOrderEndpoint | TRF-F006, F008, F009, F013 (+ TRF-D01 damage adj) | " |
| API | POST /transfer-orders/{PublicId}/close |
CloseTransferOrderEndpoint | TRF-F012 | " |
| API | POST /transfer-orders/{PublicId}/cancel |
CancelTransferOrderEndpoint | TRF-F004 (Cancel) | " |
| API | GET /transfer-confirmations/{PublicId} |
GetTransferConfirmationEndpoint | TRF-F009, F015 | " |
| portal | List + detail hub + full-page receive + create/line drawers + confirmation print drawer | apps/qiiub-portal/src/features/transfers/ |
TRF-F001βF016 (UI) | merchant Admin |
LocationProduct/InventoryMovement (Inventory tables) and Receive spawns an InventoryAdjustment (Adjustments) β all scored as the transfer's outcomes (F005/F006/F013/F016), with the tables' own CRUD as dependencies.Rejected enum has no path), and all Missing T3.GET /api/inventory/location-products (Inventory) for sender-stock display β an Inventory endpoint, correctly a dependency.Present β field-level (FluentValidation, every endpoint): PublicId prefix checks (to_/trln_/tcf_/loc_/pvar_ via MustBePublicId); decimal precision/scale/sign bounds on quantities; Notes/DocumentNumber max-lengths matching DB columns; sort-whitelist + direction on list; batch-delete cap 100.
Present β business-rule (handlers): same-location reject on create (:59-60); duplicate-variant reject on add-line (:114); SKU-not-stocked-at-sender reject (:138); qty > Available (OnHand β Reserved) reject on add/update/ship (:146-153, ship :163-183); Draft-only edit/line-mutation guards (409); atomic status-transition predicates (Ship from Draft/Submitted, Receive from InTransit/PartiallyReceived, Close from Received/PartiallyReceived, Cancel from Draft β 409 otherwise); empty-line ship guard (409); over-receipt reject (400, :145-152); route-forgery checks on nested line routes; RowVersion + stock-concurrency retry β 409.
Absent β business validations:
| Missing validation | Effect today | Tracked as |
|---|---|---|
ADJ-XFR-DAMAGE reason-code presence guard |
Damaged units silently skip the ledger β the code is seeded only in DevSeed, so this is the normal state outside dev (TransferDamageAdjustmentService.cs:76-82) |
TRF-F013 (Partial) / Β§7 #1 (ADR-0083 flags a future IsSystem seed-protect) |
| Ship qty β€ requested check | A line can be shipped more than requested (only capped at Available) | Β§7 hardening |
| Create-with-inline-lines cost/guards (Fable F-3) | CreateTransferOrderEndpoint.cs:166-167 writes client-supplied UnitCost/UnitPrice and skips the stocked-at-sender / qtyβ€Available guards that AddLine enforces (API-only; portal always sends lines: []) |
Β§7 hardening |
| Active-location / same-merchant-location check | A transfer to/from an inactive location may be allowed | Β§7 hardening |
Second axis, orthogonal to the score. Testedness never enters Β§6.
Inventory (at 5d527ffb, method counts): β55 integration (tests/QIIUB.Tests.Integration/Features/Inventory/TransferOrderTests.cs β52 + Features/Transfers/ListTransferOrdersAggregatesTests.cs 3) Β· 0 dedicated unit Β· 0 e2e. Frontend: 7 Vitest blocks across 2 files (TransfersListPage.test.tsx 2 + useTransfers.test.ts 5). Counts are context; the signal is the flow matrix.
Critical-flow coverage (flow granularity):
| Flow | Tested |
|---|---|
| Create (valid, column-max 400, same-location 400, wrong-prefix 400, barcode on line) | β |
| AddLine/UpdateLine guards (not-stocked 409, qty>available 409, all-reserved 409, route-forgery 404, qty formatting) | β |
| Ship guards (variant-not-stocked 409, qty>OnHand 409, qty==OnHand ok, all-reserved 409, wrong-prefix 400) | β |
| Full receive β auto-close (source decrement, dest = intact, 2 damaged not stocked, 2 confirmations, second-close 409) | β (:360) |
| Partial receive β manual short-close (30 of 40 β PartiallyReceived β Close) | β (:423) |
Receive edge: two lines same variant, no existing dest LP (.Local dedup) |
β (:1040) |
| Status guards (cancel-already-shipped 409, receive-not-receivable 409, close-still-draft 409, 404s) | β |
| Bulk-delete (removes, not-Draft 409, line-of-another-transfer 400 nothing-deleted, over-cap 400) | β |
| List sort/filter + StatCard counts span all pages | β |
| Cross-merchant isolation (ship/receive/cancel/close/get-confirmation/statcards) | β (6+) |
| Damage write-off skipped when reason code absent (the silent-ledger risk) | β automated (no negative-seed test) β but live-proven, see below |
| Confirmation print drawer / GET confirmation content (beyond 200/400) | β (endpoint 200/wrong-prefix only) |
| Dedicated unit tests (validators, delta math) | β (0 unit files) |
Named test gaps β Β§7 "test hardening": the absent-reason-code damage path, a ship-more-than-requested assertion, and zero dedicated unit coverage. Thin-coverage signal overall β β55 integration tests concentrated in one 1546-line file, no unit layer.
Live confirmation (2026-07-26). Drove the full transfer chain against the running app (local dotnet run API + SQL, magic-link auth) on merchant 1 (TechZone): created TRF, added variant 76 qty 10 (Loc 1 β Loc 2), shipped full, received 8 intact + 2 damaged. Observed:
LocationProduct 50 β 40 (β10); InventoryMovement TransferOut β10 @Loc1 cost 239.02; status β InTransit; confirmation TCF-000002 (Shipped) β confirms TRF-F005/F009.received+damaged=10, the full physical arrival); InventoryMovement TransferIn +10 @Loc2 cost 239.02 (cost travelled β TRF-F014 mechanism); confirmation TCF-000003 (Received); status β Closed with autoClosed=true β confirms TRF-F006/F008/F009 + auto-close (F004).NULL); after receive its cost stayed NULL β not re-averaged. Cost carries to a new LP but an existing one is untouched. Exactly the Partial claim.DamageAdjustmentDocumentNumber: null and zero InventoryAdjustment rows were created, so the 2 damaged units were never written off: the destination ended at 27 (incl. the 2 damaged as if sellable) instead of 25. Cause in this environment: the merchant DB carries reason codes only through ADJ-SHRINK (SortOrder 6); the service's required ADJ-XFR-DAMAGE (TransferDamageAdjustmentService.cs:38, seeded at S02_ReferenceDataSeed.cs:88, SortOrder 7) is absent because the DB was seeded before that entry was added and DevSeed's S02 does not back-fill it on re-seed. The seed source is correct; the idempotency is not β and the runtime consequence is a silent inventory overstatement. Direct evidence for TRF-F013's risk note and Β§7 gap #1.LocationProduct rows restored to pre-state (LP151β50, LP152β17 + LastReceivedDate) afterward β zero DB residue.This is the confidence axis's strongest tier: the core (F005/F006/F009), the auto-close (F004), the cost-carry boundary (F014), and the sharpest gap (C3) are all live-driven, not merely code-read.
Formula (fixed, template Β§6): weights T1Γ3/T2Γ2/T3Γ1; Implemented=1.0, Partial=0.5, Missing=0.0; Dependency + N/A excluded.
| Tier | Implemented | Partial | Missing | Dep+N/A excluded | Scored count | Ξ£ statusValue | Tier % |
|---|---|---|---|---|---|---|---|
| T1 (Γ3) | 11 | 0 | 0 | 0 | 11 | 11.0 | 100.0% |
| T2 (Γ2) | 4 | 4 | 7 | 0 | 15 | 6.0 | 40.0% |
| T3 (Γ1) | 0 | 0 | 12 | 0 | 12 | 0.0 | 0.0% |
T1 scored (11): all Implemented (F001βF011) β 11.0 / 11 = 100.0%. T2 scored (15): Implemented F012/F015/F016/F023; Partial F013/F014/F020/F022; Missing F017/F018/F019/F021/F024/F025/F038. β 4Γ1 + 4Γ0.5 + 7Γ0 = 6.0 / 15 = 40.0%. T3 scored (12): all Missing (F026βF037). β 0.0 / 12 = 0.0%. Excluded: 8 Dependency (TRF-D01βD08) + 5 N/A (TRF-N01βN05).
Overall weighted:
numerator = 3Γ11.0 + 2Γ6.0 + 1Γ0.0 = 33 + 12 + 0 = 45
denominator = 3Γ11 + 2Γ15 + 1Γ12 = 33 + 30 + 12 = 75
overall% = 45 / 75 = 60.0%
Interpretation. The transfer core is complete and well-tested (T1 100%): a merchant can draft a two-sided transfer, ship it (decrementing source stock with a movement trail and a posted confirmation), receive it partially or fully (incrementing destination stock, auto-creating the destination product row), and close it β all tenant-isolated, guarded by an atomic state machine, and covered by β55 integration tests. What's missing is operational breadth (T2 40%): the damage write-off to the ledger is not reliably provisioned (F013 Partial β proven skipped live), no in-transit inventory bucket, no freight/reason-code/effective-date fields, no receiver-reject path (which ADR-0022 itself promises β F038), no notifications, thin reporting β and all advanced features (T3 0%): landed cost, serial/lot, suggested/auto transfers, approval, multi-destination, ASN. β60% sits just below Receivings (67%) and PO (74%), a coherent read: transfers has the strongest core mechanics of the three (two-sided posting) but the least operational and advanced surface built on top.
Sensitivity / anti-gaming notes:
artifact-transfers-fable-review.md.po-scorecard.md mentions transfers once (as N/A, separate module); receivings-scorecard.md scores only receipt-triggered writes β no cross-doc double-count either direction. Open obligation: the future Inventory/Adjustments scorecards MUST carry Dependency rows back-pointing at TRF-F005/F006/F013/F016 (registry note).| Rank | Gap (ID) | Tier | Impact | Recommendation | ADR? |
|---|---|---|---|---|---|
| 1 | TRF-F013 hardening β absent reason code (C3, proven live) | T2 | High β silent ledger. Damaged units silently overstate destination stock when ADJ-XFR-DAMAGE is absent (live: 27 vs 25) |
(a) Make DevSeed S02 idempotently back-fill system reason codes; (b) add the IsSystem reason-code protection ADR-0083 flags, or fail Receive loud when the code is missing |
No β closes an ADR-0083 open item |
| 2 | TRF-F017 In-transit inventory bucket / report | T2 | High β visibility. Stock in motion is invisible except as a derived line gap; a lost-in-transit transfer is untracked until manual close | Add an items-in-transit report (derived) or a clearing pseudo-location; design Β§7 already anticipates it | Possibly (if a clearing location) |
| 3 | TRF-F014 Weighted-average cost at destination | T2 | Medium β valuation. An existing destination LP's cost is not re-averaged on receipt, so cost drifts | Recompute destination Cost as a weighted average on receive (mirrors the costing decision RCV-F024 raises) |
requires new ADR (costing model) |
| 4 | TRF-F029 Lot / batch tracking on transfer | T3βT2 for Rx | Medium β Rx chain-of-custody | Carry lot/batch through ship + receive; pairs with RCV-F019 | requires new ADR |
| 5 | TRF-F015 Branded server-side print (Transfer Slip) | T2 | Medium β the current print is client-side window.print() only |
Implement ADR-0070 Phase-2 "Transfer Slip" (QuestPDF) or explicitly ratify the client-side drawer as sufficient (Β§8) | Promote/close ADR-0070 |
| 6 | TRF-F038 Receiver-reject path | T2 | Medium β ADR-0022 promises "receiver can reject"; today refusing a shipment is impossible (only absorb-or-close) | Emit TransferConfirmationType.Rejected + a reject transition that returns goods to sender (reverse the in-transit units) |
Likely new ADR (reverse-transit model) |
| β | TRF-F018 freight, F019 reason codes, F024 effective dates, F025 people fields | T2 | Low-Med | Nullable columns on TransferOrder (+ wire the existing dead *EmployeeId columns) |
No |
| β | Validation + test hardening (Β§5b/Β§5c) | β | Low-Med | Ship-qty β€ requested; active-location check; unseeded-reason-code test; first unit tests | No |
| β | TRF-F026βF037 (landed cost, serial, suggested/auto, approval, multi-dest, ASN, installments, MPQ, reverse) | T3 | Low now, high at ERP-tier | Defer; each needs an ADR (multi-destination likely belongs to Distribution) | new ADR each |
Quick wins: #1 reason-code guard, TRF-F018/F019 columns, wire the dead *EmployeeId columns (F025).
Net-new: #2 in-transit bucket, #3 avg costing, #4 lot/batch, and all T3.
Point-in-time snapshot (GHES
Qiiub/qiiub, queried 2026-07-27; 103 open issues). In the promoted framework this is generated by the render tool at build time, never hand-maintained.β untracked= a gap with no open issue.
| Gap (ID) | Tracked by (open GHES issue) |
|---|---|
| Transfers roadmap (posted receipts / supplier RMA / cross-docking) | #304 "Transfers β Themes 3/5/6" β partial (roadmap themes, not the specific gaps below) |
TRF-F013 damage write-off seed / ADJ-XFR-DAMAGE (C3) |
related to #807 "DevSeed coverage β populate empty feature areas" β adjacent (seed idempotency) |
| TRF-F017 in-transit bucket, F038 receiver-reject, F019 reason codes, F018 freight, F024 effective dates, F025 people fields, F029 lot/batch, F015 branded print, all T3 | β untracked |
Read: only a broad roadmap theme (#304) touches Transfers; every concrete gap β including the C3 silent-ledger risk proven live and the receiver-reject that ADR-0022 promises β is untracked.
Contradictions (from the gathering artifacts):
inventory-supply-chain.md is frozen at Draft/2026-03-13 and pre-dates the shipped model. It still shows TransferConfirmation with no DocumentNumber and no TransferConfirmationLine, and a Receive flow where TransferIn = received only (48) that silently drops damaged units (:433-451). The shipped/ADR-0083 model writes TransferIn = received+damaged (50) then an Adjustment βdamaged. The canonical design doc contradicts shipped code + the accepted ADR-0083 and was never back-updated.window.print() drawer; ADR-0070 (proposed, Phase 2) specifies a branded QuestPDF "Transfer Slip" that is not built and is partly pre-empted by the drawer. The two print strategies are unreconciled (TRF-F015 / Β§7 #5).ADJ-XFR-DAMAGE reason code is absent (TransferDamageAdjustmentService.cs:76-82). The seed source is correct (S02_ReferenceDataSeed.cs:88, SortOrder 7), but the seeded merchant-1 DB stops at ADJ-SHRINK (SortOrder 6) β it was seeded before that entry and DevSeed S02 does not back-fill on re-seed (idempotency gap). Live result: receiving 2 damaged units created no InventoryAdjustment and left the destination at 27 (incl. 2 damaged) instead of 25 β a silent inventory overstatement. ADR-0083 itself flags a future IsSystem reason-code protection as the real fix (Β§7 #1). (Two related, separable defects: the runtime silent-skip, and the DevSeed non-idempotency that triggered it here.)Submitted/Shipped/Received (TransferOrderStatus) and Rejected (TransferConfirmationType) are defined but never emitted by the happy path; RequestedByEmployeeId/ClosedByEmployeeId/TransferConfirmation.EmployeeId are never written.accepted). PROGRESS Phase-5 counts predate the Phase-22 PublicId sweep (expected staleness, not a contradiction).receivings-scorecard.md has zero return-to-vendor rows and po-scorecard.md only tracks Credit-NoteβAP. So RTV (return-to-vendor) β which every legacy system has β is N/A'd here and absent from both neighbors' denominators. N05 is correct for Transfers (no in-module gap hidden), but Receivings owes a "RTV / return to vendor" row. Flag for the registry reconciliation, not fixable in this doc.Decisions needed from a human:
*EmployeeId columns' intended owner (or drop them).Every capability in the legacy+market inventory (artifact-transfers-gathering-market.md, 42 unified rows) + the QIIUB-original sweep (ADRs 0022/0083) mapped to a disposition. scored = own Β§3 row; folded = subsumed; N/A = out of scope; dep = neighbor module.
| Source capability | Disposition |
|---|---|
| 1 Transfer order document | scored TRF-F001 |
| 2 Origin/destination selection | scored TRF-F002 |
| 3 Transfer direction (In/Out) | folded into TRF-F004/F009 (QIIUB models both sides via ship/receive) |
| 4 Line items with qty + Qty OH | scored TRF-F003 |
| 5 Lifecycle states | scored TRF-F004 |
| 6 Deduct stock at source on send | scored TRF-F005 |
| 7 Add stock at destination on receive | scored TRF-F006 |
| 8 Partial issue / partial ship | scored TRF-F007 |
| 9 Partial receipt | scored TRF-F008 |
| 10 Close / short-close | scored TRF-F012 |
| 11 In-transit tracking | split β state β TRF-F004 (Impl); bucket/report β TRF-F017 (Missing) |
| 12 Discrepancy (issued vs received) | scored TRF-F013 |
| 13 Discrepancy investigation status | scored TRF-F026 |
| 14 Transfer cost carry / valuation | scored TRF-F014 (Partial) |
| 15 Landed cost on transfer | scored TRF-F027 |
| 16 Freight / shipping fields | scored TRF-F018 |
| 17 Reason codes | scored TRF-F019 |
| 18 Packing slip / print | scored TRF-F015 |
| 19 Barcode / scan entry (create) | folded into TRF-F003 |
| 20 Barcode scan-to-receive | scored TRF-F020 (Partial) |
| 21 Serialized item handling | scored TRF-F028 |
| 22 Lot / batch tracking | scored TRF-F029 |
| 23 Suggested / auto transfer qty | scored TRF-F030 |
| 24 Auto-replenishment transfers | scored TRF-F031 |
| 25 Multi-destination / bulk transfer | scored TRF-F032 (candidate to fold to Distribution) |
| 26 HQ-initiated transfer | N/A TRF-N01 (arch mismatch β per-store config) |
| 27 Store-initiated (inter-store) | folded into TRF-F001/F002 (QIIUB's default model) |
| 28 Local / external transfer | N/A TRF-N05 (vendor return β PO/Receivings) |
| 29 Auto-create counterpart order | N/A TRF-N02 (cloud-dissolved; the confirmation IS the counterpart) |
| 30 Request / approval workflow | scored TRF-F033 |
| 31 Approve-lock / immutability | N/A TRF-N03 (superseded by DraftβInTransit state machine) |
| 32 Effective / required dates | scored TRF-F024 |
| 33 Requisitioner / confirming-to | scored TRF-F025 |
| 34 Transfer reversal / cancel | split β cancel-draft β TRF-F004 (Impl); reverse-completed β TRF-F037 (Missing) |
| 35 ASN / shipment notice | scored TRF-F034 |
| 36 Notifications | scored TRF-F021 |
| 37 Transfer history / audit log | scored TRF-F016 |
| 38 Transfer reporting | scored TRF-F022 (Partial) |
| 39 Tax rate on transfer | N/A TRF-N05 (folded with vendor-return flavor) |
| 40 Remarks / notes | scored TRF-F023 |
| 41 GL / accounting postings | N/A TRF-N04 (external billing) |
| 42 UOM / master-pack qty | scored TRF-F036 |
| (QIIUB-original, ADR-0022/0083) Two-sided posted confirmations | scored TRF-F009 |
| (QIIUB-original, ADR-0022:35) Receiver reject shipment | scored TRF-F038 (Missing) β Fable F-1: was the one guard-(e) leak; now reconciled |
| (QIIUB-original) Cross-merchant isolation | scored TRF-F010 |
| (QIIUB-original) Document numbering TRF+TCF | scored TRF-F011 |
| (QIIUB-original) Damage units β ledger auto-adjustment | scored within TRF-F013 (Partial; dep doc = TRF-D01) |
| (QIIUB-original observation) Multi-shipment installments | scored TRF-F035 |
| (QIIUB-original, future) Offline sync of transfers | dep TRF-D08 |
No source line is left unreconciled (42 market/legacy rows + 7 QIIUB-originals; the ADR-0022 receiver-reject leak found by Fable is now TRF-F038).
WIDS365 sweep (2026-07-26 β 4th source, artifact-transfers-wids365-sweep.md). WIDS365 (a BCPOS production POS) has a first-class Transfer module (TransferHeader/TransferDetail, web_TransferPost.sql) plus a Distribution fan-out (DistributionHeader/Detail/DetailStore). Reconciliation verdict: WIDS365 is a strict subset of this taxonomy β it adds no new denominator row and changes no tier. Every WIDS365 capability maps to an existing row: single-step atomic post β folded into TRF-F004/F005/F006 (QIIUB's two-sided model is a superset β WIDS365 has no ship-then-receive phase); cost-carry-without-recompute β reinforces TRF-F014 (Partial, same limitation); ItemQtyOnTransfor views β reinforce TRF-F017 (in-transit bucket, still Missing in QIIUB); Distribution fan-out β reinforces TRF-F032 (multi-destination, Missing β and it's Distribution's territory) + the TRF-N01 HQ-push N/A. WIDS365 also lacks partial ship/receipt, damage/discrepancy, reason codes, freight, serial/lot, landed cost, approval, and reverse β so it reinforces, never expands, QIIUB's gap list. (Notably, WIDS365 carries seeded-but-unwired two-phase statuses β the same dead-enum pattern QIIUB has with Submitted/Shipped/Received/Rejected.) Guard (e) now spans four source families: RMS/RMH + modern market + QIIUB-original(ADRs) + WIDS365.
| Term | In plain words |
|---|---|
| Transfer (TransferOrder) | The document that moves stock from one of your locations to another. |
| Two-sided / confirmation | Two events, each posted as its own document: the sender ships, the receiver confirms β QIIUB records both (a TransferConfirmation each). |
| In-transit | The stock has left the source but not yet arrived. In QIIUB it's a status, not a counted bucket β the amount "in flight" is inferred (shipped β received β damaged). |
| Ship | The step that lowers source stock and starts the transfer moving (writes a TransferOut movement). |
| Receive | The step that raises destination stock (writes a TransferIn movement); can be partial. |
| Damaged units | Units that arrive broken β recorded, then written off inventory via an automatic adjustment so they aren't counted as sellable. |
| Auto-close | When everything shipped is accounted for on receive, the transfer closes itself. |
| Short-close / write-off | Manually closing a transfer that came up short (lost in transit) β the missing units are simply never received. |
| Cost travels | The item's cost moves with the goods (the destination inherits the source's cost), vs re-computing a blended cost at the destination (which QIIUB doesn't do β TRF-F014). |
| Landed cost | Adding freight/duty on top of the item cost when it arrives β not built for transfers. |
| Lot / batch | Tracking which production batch each unit belongs to as it moves β needed for pharmacy traceability, not built. |
| Multi-destination transfer | One document fanning stock out to several receiving stores β this is really Distribution's job in QIIUB. |
| Single-owner rule | Each capability is scored in exactly one module. Ship/Receive write Inventory's tables and spawn an Adjustment, but "moving stock" is scored here, once. |
| Dependency (in this doc) | A capability owned by a neighbor module (Inventory, Adjustments, Sync), so it's not counted in this %. |
| N/A (in this doc) | A legacy capability that doesn't fit QIIUB's model (HQ-push, GL postings, approve-lock), so it's not a gap and not counted. |