Wiki πŸ” Search
QIIUB Β· Module completeness

Transfers

38 scored features 13 dependencies + N/A area: Inventory & Supply Chain

measured at 5d527ffb

60%
Overall
T1100%
T240%
T30%

Created: 2026-07-26 | Last Updated: 2026-07-26

Third filled instance of module-scorecard-template.md (after po-scorecard.md and receivings-scorecard.md). Read-only analysis; recommendations only. Evidence is file:line / endpoint against D:\BCPOSGit\qiiub on branch main at commit 5d527ffb. 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 mutate LocationProduct β€” tables the Inventory module owns β€” while the damage write-off is delegated to the Adjustments module via TransferDamageAdjustmentService (ADR-0083 explicitly rejected a private TransferDamage movement 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.


0. Document control

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).


1. Module identity

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.

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.


1b. Canonical flow β€” how stock moves between locations

  1. Create (Draft) β€” 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).
  2. Build lines β€” 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.)
  3. Ship β€” 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.
  4. Receive β€” 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.
  5. Close β€” 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.
  6. Cancel β€” 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.)


2. Dependency map

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):


3. Functional taxonomy

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 Missing means "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.

T1 β€” Core (module unusable without) β€” weight Γ—3

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)

T2 β€” Standard (market expects) β€” weight Γ—2

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

T3 β€” Advanced / Differentiator β€” weight Γ—1

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.

4. Storage specification

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().

Table: TransferOrder β€” 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.

Table: TransferOrderLine β€” 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.

Table: TransferConfirmation β€” 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.

Table: TransferConfirmationLine β€” 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.


5. API surface

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

5b. Validations inventory

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

5c. Test surface & confidence

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:

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.


6. Scoring rubric & result

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%

Transfers module completeness: β‰ˆ 60% overall β€” T1 100%, T2 40%, T3 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:


7. Gap list & recommendations

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.


7a. GHES issue coverage β€” is each gap already ticketed?

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.

8. Open questions & contradictions

Contradictions (from the gathering artifacts):

  1. C1 β€” design doc 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.
  2. C2 β€” print is a split, unreconciled story. ADR-0083 (accepted) shipped a client-side 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).
  3. C3 β€” silent-ledger dependency (PROVEN LIVE 2026-07-26): the damage write-off silently no-ops if the 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.)
  4. C4 β€” dead enum values & columns: Submitted/Shipped/Received (TransferOrderStatus) and Rejected (TransferConfirmationType) are defined but never emitted by the happy path; RequestedByEmployeeId/ClosedByEmployeeId/TransferConfirmation.EmployeeId are never written.
  5. C5 β€” index-vs-PROGRESS drift: CLEAN for the transfer ADRs (0022/0083 README matches front-matter, both accepted). PROGRESS Phase-5 counts predate the Phase-22 PublicId sweep (expected staleness, not a contradiction).
  6. C6 — cross-doc orphan (Fable F-7, registry-level, not a Transfers gap): TRF-N05 dispatches vendor-return-as-transfer-out to "PO/Receivings", but 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:


9. Source reconciliation (denominator integrity)

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.


Glossary

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.