Wiki 🔍 Search
QIIUB · Module completeness

Distribution

⚙︎ Backend-only — no UI yet
28 scored features 11 dependencies + N/A area: Inventory & Supply Chain

measured at 5d527ffb

58%
Overall
T1100%
T228%
T30%

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

Fourth filled instance of module-scorecard-template.md (after PO, Receivings, Transfers). 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-distribution-gathering-{repo,decisions,market}.md. First module scored with WIDS365 as a source from the start (4 sources: RMS/RMH + WIDS365 + modern market + QIIUB-original).

Read the headline honestly. Distribution is a backend-only module — 5 API endpoints, zero portal/admin UI (§5/§7). The ≈58% measures capability completeness vs the yardstick; it does not mean the module is operable by a merchant today. A merchant admin cannot run a distribution without direct API calls. The strong T1 (100%) is real at the API surface; the missing surface + operational breadth are the story.


0. Document control

Field Value
Module name Distribution (Distribution + DistributionLine + DistributionAllocation)
File distribution-scorecard.md (target: docs/modules/distribution-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 No Distribution-specific ADR exists. Rides on: 0020 (LocationProduct — write target), 0021 (separate documents), 0023 (three-tier history / movement trail), 0037 (datetime2(3)), 0081 (inventory hub), 0082 (PublicId — header dist/line dstl; DistributionAllocation is Cat-3, no PublicId). Transfers ADRs 0022/0083 are boundary markers only.
Governing design docs docs/design/inventory-supply-chain.md §4.3 (Draft, frozen 2026-03-13 — stale, predates PublicId Phase 22 + tax-resolver Phase 20c)
Status of this analysis Adversarially-verified — Fable pass (62→58; findings + resolutions in artifact-distribution-fable-review.md); live-confirmed against the running app 2026-07-27 (API-only, §5c)

Léelo así (en simple). Foto del 2026-07-26 (commit 5d527ffb) de cuánto del módulo de distribución (una bodega/HQ repartiendo mercancía recibida a varias tiendas en un solo documento) está construido, medido contra POS de retail establecidos + WIDS365 + mercado moderno + el roadmap de QIIUB. El núcleo funciona y está probado (crear el documento, asignar por tienda, completar → mueve el stock de la bodega a cada tienda, 17 tests) — pero es solo backend, sin UI, y le falta casi toda la amplitud operacional (editar/cancelar, métodos de auto-asignación, en-tránsito, aprobación, impresión, reportes). ~58% mide capacidad vs la vara, no "listo para usar".


1. Module identity

Purpose. Distribution pushes stock from one warehouse/HQ location out to many stores in a single document, sourced from a receiving event. A Distribution header (source warehouse + a required PurchaseReceiptId) has DistributionLines (one per item, with a total quantity) each split into DistributionAllocations (one per destination store, with that store's quantity). Completing the distribution is the event that moves stock: in one transaction it decrements the source warehouse and increments each destination store, writing paired InventoryMovement rows (MovementType.Distribution = 7). Unlike Transfers, it does not fan out into child TransferOrders — it moves stock directly and one-directionally (warehouse → stores).

Vertical relevance.

Actors & roles. All 5 endpoints require SystemAdmin | Admin (no Manager, no Cashier). Surface: API only (FastEndpoints). No portal, no admin, no POS surface — the feature exists solely as HTTP endpoints; it surfaces to a human only as a read-only movement-type badge in the inventory Movements Log (§5/§7).

Not-in-this-module. Lateral store↔store rebalancing is Transfers (two-sided, TransferOut/In). The LocationProduct on-hand/cost table and the InventoryMovement ledger are Inventory-owned (Distribution writes them, scored here as outcomes). The upstream PurchaseReceipt it sources from is Receivings. Getting the per-store increments to physically-offline stores is a future Sync concern. See §2.


1b. Canonical flow — how stock is distributed

  1. CreatePOST /distributions. Name the source warehouse (SourceLocationId) and a required source receipt (PurchaseReceiptId); add lines (one per item, TotalQuantity) each with inline allocations (per-store {LocationPublicId, Quantity}). A DST-000xxx number is minted. Status is set to Allocated if a line carries allocations, else Draft (CreateDistributionEndpoint.cs:266-267). (There is no separate allocate step, no edit endpoint, and no cancel endpoint.)
  2. CompletePOST /distributions/{id}/complete. Draft/Allocated → Completed, in one transaction (CompleteDistributionEndpoint.cs:76-255): per line, per allocation — write a warehouse-OUT InventoryMovement (−qty, MovementType=7) and a store-IN movement (+qty), decrement the source warehouse LocationProduct.QuantityOnHand, and increment/auto-create the destination store LocationProduct (Cost = line UnitCost, Price = NULL → inherit). Two-sided, atomic, RowVersion-guarded.
  3. Acknowledge (optional, metadata only) — POST /distributions/{id}/lines/{line}/allocations/{location}/acknowledge. Sets AcknowledgedDate on that store's allocation. No stock effect, not required for the stock move, 409 if already acknowledged.

State machine: Draft(0)/Allocated(1) → Completed(3). Shipped(2) and Cancelled(4) exist in the enum but are never set by any endpoint (dead values). There is no in-transit stage — stock moves instantly and completely at Complete.


2. Dependency map

Module Direction What crosses the boundary Scoring treatment
Receivings (PurchaseReceipt) upstream Distribution requires a PurchaseReceiptId source; optional per-line PurchaseReceiptLineId dependency — the receipt is Receivings-owned; distributing its goods is scored here
Inventory — LocationProduct (on-hand/cost) upstream Complete decrements the warehouse + increments/auto-creates each store dependency — table owned upstream; the distribution-triggered writes are scored here (DST-F005)
Inventory — InventoryMovement ledger shared-infra Complete writes paired OUT/IN MovementType.Distribution rows dependency — ledger owned upstream; the distribution's movement trail is scored here (DST-F011)
Document Sequencing shared-infra mints the DST- number shared-infra; the use is DST-F007, the service is a dependency
Catalog / Inventory master data upstream Location, ProductVariant, TaxGroup referenced dependency — tables owned upstream
Sync engine (offline-first) downstream/future per-store increments must reach physically-offline stores eventually dependency — NOT wired; a correctness concern for offline-first (see §7)
Transfers sibling (NOT a dependency) lateral store↔store movement out of scope — different flow; Distribution does not use Transfers

Boundary statement. Distribution owns the Distribution + DistributionLine + DistributionAllocation tables, all 5 endpoints under Api/Features/Distributions/, the forward state machine, and the act of distributing received stock (the warehouse-decrement + per-store-increment + paired movements that Complete performs). It does not own the source receipt, the LocationProduct/InventoryMovement tables it writes, or the sync that delivers stock to offline stores.

Single-owner rule. Complete (CompleteDistributionEndpoint.cs:148-250) writes Inventory's LocationProduct + InventoryMovement directly, using its own MovementType.Distribution = 7 (distinct from TransferOut=4/TransferIn=5). That code lives here, but the tables are Inventory's → scored once here as the distribution's outcome (DST-F005/F011); Inventory's own table CRUD is DST-D01/D02. Distribution does NOT fan out into TransferOrders (verified: zero TransferOrder references) — so there is no overlap with the Transfers scorecard. No Distribution feature is scored in a neighbor's doc.

Boundary-leak checklist (to be audited by Fable):


3. Functional taxonomy

Status legend: Implemented / Partial / Missing / Dependency (excluded). Source: RMS / RMH / WIDS365 / market / QIIUB-original.

Comparison map, not a backlog. Universe = RMS/RMH + WIDS365 (a BCPOS production POS with a first-class Distribution module) + modern allocation tooling (Square/Lightspeed/NetSuite/Cin7 + Toolio/Uphance) + QIIUB-original. A Missing means "that world has it and QIIUB doesn't (yet)," not a commitment.

Framing decision (JJ, 2026-07-26 — applied here): RMS/RMH do not have a first-class "Distribution" document. They achieve distribution through the HQ Worksheet / P/O Planner mechanism (Style 340/330/308) — an artifact of RMS/RMH's disconnected HQ↔store databases (worksheet → approve → download → convert into per-store POs/transfers). QIIUB runs one cloud source and moves stock directly, so those RMS/RMH worksheet-mechanism capabilities are scored N/A (same call as the PO-Planner N/A in po-scorecard.md), NOT Missing — a module is not penalized for lacking another system's disconnected-DB machinery. Capabilities that are also in WIDS365/market (e.g. auto-allocation methods) stay in scope.

T1 — Core (module unusable without) — weight ×3

ID Name Description Source Status Evidence Notes
DST-F001 Distribution document Header + per-item lines + per-store allocations WIDS365+market Implemented Distribution/DistributionLine/DistributionAllocation entities; POST /distributions — CreateDistributionEndpoint Three-table shape (header→line→allocation)
DST-F002 Source warehouse designation The doc names the sending warehouse WIDS365+market Implemented Distribution.SourceLocationId (required, Distribution.cs:16); composite FK MerchantId-first
DST-F003 Allocate one item across N stores Per-item, per-store quantity grid in one document WIDS365+market Implemented DistributionAllocation per store ({DistributionLineId, LocationId, Quantity}); unique (MerchantId, LineId, LocationId) = one alloc per store per line Multi-item via multiple lines. Explicit store list (group-inherited targeting = N/A, DST-N05)
DST-F004 Manual per-store quantities Operator types each store's quantity WIDS365+RMS+market Implemented Inline CreateAllocationRequest {LocationPublicId, Quantity} (:31-35); stored as allocations Manual is the only method (auto-allocation = DST-F013, Missing)
DST-F005 Complete → move stock Decrement warehouse + increment each store, paired movements WIDS365+market Implemented CompleteDistributionEndpoint.cs:143-250 — 2 InventoryMovement (MovementType.Distribution=7) per allocation + warehouse QoH -= + store QoH +=/auto-create; one txn Two-sided, atomic. Achieves the fan-out outcome directly — no child docs (unlike RMS/RMH/WIDS365 mechanism)
DST-F006 Forward state machine Draft/Allocated → Completed, atomic guard WIDS365+market Implemented DistributionStatus (0/1/3 live); atomic ExecuteUpdateAsync flip → 409 if already completed (:96-116) Shipped(2)/Cancelled(4) are dead enum values — never set. Edit/cancel = DST-F012 (Missing)
DST-F007 Document numbering Human DST-000xxx per merchant WIDS365+RMS Implemented "DST" sequence via IDocumentSequenceService; unique (MerchantId, DocumentNumber) Sequence service = shared-infra (DST-D04)
DST-F008 Cross-merchant isolation Every distribution op tenant-scoped QIIUB-original (tenancy) Implemented Composite PK/FK MerchantId-first on all 3 entities; 3 cross-merchant tests (DistributionTests.cs:497,526,554) Critical Rule #1; MerchantId-first verified in every endpoint
DST-F011 Movement ledger trail Immutable paired in/out movement rows per allocation WIDS365+QIIUB-original Implemented Warehouse-OUT :148-160 + store-IN :163-175, ReferenceType="Distribution", ReferenceId=dist.Id, DocumentNumber The ADR-0023 document-trail tier for distribution. Table is Inventory-owned (DST-D02). Distinct from F005 (the stock write) — different capability, different source row

T2 — Standard (market expects) — weight ×2

ID Name Description Source Status Evidence Notes
DST-F012 Edit / cancel a draft Revise or void a distribution before/without posting WIDS365+market Missing No Update, no Cancel, no delete endpoint (only Create/Get/List/Complete/Acknowledge). Cancelled(4) enum value is dead Once created, a distribution can only be Completed or left dangling. Natural home: an UpdateDistribution + CancelDistribution endpoint
DST-F013 Auto-allocation methods Compute per-store qty (even-split / fixed / min-max·reorder / sales-history) instead of manual market+RMS+RMH Missing Manual only; AllocatedQuantity is just the sum of manual allocations (:264); no allocation engine anywhere Bundled (even-split / fixed / min-max / recently-sold — not split, anti-gaming b). Even-split is the table-stakes half; sales/min-max are the richer half. WIDS365 is also manual-only → this is a market gap, not a WIDS365 one
DST-F014 Catalog-filter line population Add lines by dept/category/supplier filter, not one-by-one RMS+RMH+WIDS365 Missing Lines are supplied explicitly in the Create body; no filter-populate
DST-F015 Source-stock reservation Reserve the qty at the warehouse between create and post WIDS365+market Missing LocationProduct.QuantityCommitted is never read/written by the distribution flow (verified); no reservation WIDS365 reserves via ItemDynamic.QuantityCommitted; Cin7 "allocated vs available". QIIUB does not
DST-F016 In-transit / mark-as-sent Stock left the warehouse but not yet received at the store market Missing No in-transit state; stock moves instantly & completely at Complete; Shipped enum dead Elevated for QIIUB's offline-first model — a store that's offline can't receive synchronously (see §7 + DST-D06)
DST-F017 Per-store receipt with variance Destination confirms received qty vs sent market+RMS Partial AcknowledgeAllocationEndpoint sets AcknowledgedDate per store (:119) — a receipt acknowledgment exists Partial: the ack is metadata only — no received-quantity, no variance, no stock effect (stock already moved at Complete). What works = per-store ack timestamp; missing = qty variance + discrepancy
DST-F018 Approval / multi-state workflow Open → Pending → Approved → Posted routing market Missing Only Draft/Allocated → Completed; no approval state WIDS365 also lacks it (Open/Closed only)
DST-F019 Cost & price carry / valuation Line cost/price travels to the destination WIDS365+market Partial New destination LP gets Cost = line.UnitCost, Price = NULL (inherit) (:234-249) Partial: cost carries to a new store LP; an existing store LP is not re-averaged (:216-220 adds qty only). Same limitation as Transfers F014
DST-F020 Per-store pick-list / label print Printable pick sheet / packing slip / labels per destination WIDS365+market Missing No print path (and no UI at all) WIDS365 has per-store report + label printing. Pairs with ADR-0070
DST-F021 Per-store distribution report Report of what each store received (qty + extended value) WIDS365+market Missing No reporting; distribution surfaces only as a movement-log badge WIDS365 web_DistributionStoreRpt
DST-F022 Status monitoring (ordered vs received) Track fulfillment per store across the distribution RMS+market Missing No per-store fulfillment dashboard; ack date is the only per-store signal
DST-F023 Notes / remarks Free-text on the distribution WIDS365+market Implemented Distribution.Notes nvarchar(500) (:20, config :14)
DST-F009 View + list / filter / search Full detail; paginated list by source-location / status / doc-number WIDS365 Implemented GET /distributions/{id} (GetDistributionEndpoint) + GET /distributions (ListDistributionsEndpoint, sort/filter :112-121) T2 (Fable F-3): source row 35 is T2 — demoted from T1 to match its source tier (was a score-favorable promotion)
DST-F010 Distribute received goods Source a distribution from a warehouse receiving WIDS365+RMS+market Implemented PurchaseReceiptId required FK (Distribution.cs:15); per-line optional PurchaseReceiptLineId validated to belong to the receipt (:203-223) QIIUB requires a receipt (WIDS365 optional). T2 (Fable F-2): source row 23 is T2 — demoted from T1 to match source tier
DST-F027 Over-distribution guard Reject allocating more than the line's total (Σ allocations ≤ TotalQuantity) WIDS365+market Missing AllocatedQuantity is summed but never validated against TotalQuantity (CreateDistributionEndpoint.cs:264); a line can over-allocate Scored (Fable F-1): maps to a concrete source (WIDS365 web_DistributionPost @diffCount guard) + raised as material (§8 C5) → a feature, not just a §5b note. Natural home: a Create validation
DST-F028 Temporal / audit history Full change history of the header & lines WIDS365 Partial InventoryMovement trail (F011) + CreatedByUserId/UpdatedByUserId attribution exist; no system-versioned temporal tables (verified — QIIUB has none for distribution) Partial (Fable F-4): partial-equivalent — movement trail + who/when attribution cover part of the need; full row-level temporal history (WIDS365 DistributionHeaderHistory) is absent

T3 — Advanced / Differentiator — weight ×1

ID Name Description Source Status Evidence Notes
DST-F024 Landed-cost / freight distribution Spread freight/landed cost across distributed lines by qty/value/weight market+RMS Missing No charge model; cost = bare line UnitCost Rare — ERP tier (Cin7 Core). Sibling of TRF-F027/RCV-F020. Not in Square/Lightspeed/WIDS365
DST-F025 Store-attribute / cluster allocation Weight per-store qty by size/demographics/climate/cluster market Missing Not built (no allocation engine at all) Advanced allocation tooling (Toolio/Buyer's Toolbox)
DST-F026 AI demand-forecast auto-replenishment System proposes distributions from forecast/rate-of-sale market Missing Not built Advanced/future; depends on cross-store sales history via sync

Dependencies (excluded from score) — listed for reproducibility:

ID Name Owner module Status Note
DST-D01 LocationProduct on-hand/cost table Inventory Dependency Written by Complete; table CRUD owned upstream (the writes are DST-F005)
DST-D02 InventoryMovement ledger Inventory Dependency Distribution writes MovementType=7; ledger owned upstream (trail = DST-F011)
DST-D03 Source PurchaseReceipt Receivings Dependency Required source; Receivings-owned
DST-D04 Document sequence service Shared-infra Dependency Mints DST-; the use is DST-F007
DST-D05 Master data (Location/Variant/TaxGroup) Catalog / Inventory Dependency Referenced; tables owned upstream
DST-D06 Offline sync of per-store increments Sync (future) Dependency Not wired — Complete writes every store synchronously in one txn; offline stores need eventual sync (correctness concern, §7)

N/A — not applicable to QIIUB (excluded; RMS/RMH disconnected-DB worksheet-mechanism artifacts + arch mismatches):

ID Name Why N/A (not Missing)
DST-N01 HQ worksheet broadcast (Worksheet + WorksheetStore fan-out) RMS/RMH disconnected-DB artifact. RMS/RMH broadcast a numbered worksheet to a store-set because HQ and stores run on separate DBs. QIIUB's single cloud Distribution document targeting an explicit store list is the fan-out, done a cleaner way (JJ's framing — same call as the PO-Planner N/A).
DST-N02 Master-doc / child-order linking (Master PO, child transfers) QIIUB moves stock directly. RMS WorksheetHeader_MasterPO and WIDS365's fan-into-child-transfers both create child documents to link. QIIUB's Complete mutates LocationProduct directly with no child docs → nothing to link. Superseded by the direct-movement design.
DST-N03 Allocation scheduling across dates (APOP) RMS/RMH-only refinement. RMH POD_OrderAllocation splits one order's lines by required-date/location — a scheduling nicety inside the disconnected-DB order flow, not a QIIUB distribution capability.
DST-N04 Bulk quantity formula (column mark-up/down) RMS/RMH worksheet UI convenience for the disconnected worksheet grid; a future front-end helper at most, not a market-defining capability.
DST-N05 Group-inherited allocation defaults / templates No group inheritance in QIIUB (store config is per-store; MerchantGroup is loyalty/gift-card only — [[feedback_store_config_independent]]). Targeting an explicit store list works (DST-F003); inheriting allocation templates from a store group does not apply. Fable-flagged residual (documented, not hidden): saved store-group/region targeting (one-click pick a LocationGroup as recipients) is distinct from config-inheritance and is arguably Missing T2 (LocationGroup exists but Distribution doesn't consume it) — kept N/A because targeting any explicit set already works (F003); flipping it would be ≈56%. A JJ call.

4. Storage specification

Three tables are owned: Distribution, DistributionLine, DistributionAllocation. All extend BaseEntity (soft-deletable operational documents). Composite PK (MerchantId, Id); Distribution/DistributionLine carry PublicId varchar(32) (prefixes dist/dstl); DistributionAllocation is Cat-3 (no PublicId) — addressed via parent-line PublicId + destination LocationPublicId.

Table: Distribution — DistributionConfiguration.cs

Column SQL type Null Business purpose
Id, MerchantId bigint, int no Composite PK; tenancy
PublicId varchar(32) no External identity (prefix dist)
DocumentNumber varchar(20) no Human DST-000xxx; unique per merchant
PurchaseReceiptId bigint no Required FK → source receipt
SourceLocationId bigint no FK → warehouse/HQ source
EmployeeId bigint yes Reserved / dead — never written
Status tinyint no Draft/Allocated/Shipped/Completed/Cancelled (0–4; 2 & 4 dead)
DistributionDate datetime2(3) no When distributed
Notes nvarchar(500) yes Free-text
ClosedDate datetime2(3) yes Set at Complete
ClosedByEmployeeId bigint yes Reserved / dead — never written

Indexes: unique (MerchantId, DocumentNumber); (MerchantId, PurchaseReceiptId), (MerchantId, SourceLocationId). FKs (composite, MerchantId-first): PurchaseReceipt / SourceLocation — Restrict.

Table: DistributionLine — DistributionLineConfiguration.cs

Column SQL type Null Business purpose
Id, MerchantId bigint, int no Composite PK; tenancy
PublicId varchar(32) no External identity (prefix dstl)
DistributionId bigint no FK → parent
PurchaseReceiptLineId bigint yes Optional link to the source receipt line
ProductVariantId bigint no What is distributed
TotalQuantity decimal(10,4) no Total to distribute across stores
AllocatedQuantity decimal(10,4) no Accumulator = sum of allocation quantities (not validated ≤ TotalQuantity)
UnitCost decimal(18,4) no Cost carried to destination
UnitPrice decimal(18,2) no Captured; not written to store LocationProduct.Price

Indexes: (MerchantId, DistributionId); unique (MerchantId, DistributionId, PublicId) (ADR-0082 nested route). FKs: Distribution / PurchaseReceiptLine / ProductVariant — Restrict.

Table: DistributionAllocation — DistributionAllocationConfiguration.cs

Column SQL type Null Business purpose
Id, MerchantId bigint, int no Composite PK; tenancy (no PublicId — Cat-3)
DistributionLineId bigint no FK → parent line
LocationId bigint no Destination store
Quantity decimal(10,4) no That store's allocated quantity
AcknowledgedDate datetime2(3) yes Set when the store acknowledges (metadata only)
AcknowledgedByEmployeeId bigint yes Reserved — no FK configured

Indexes: unique (MerchantId, DistributionLineId, LocationId)one allocation per store per line. FKs: DistributionLine / Location — Restrict.

Reserved/unwired: Distribution.EmployeeId, Distribution.ClosedByEmployeeId, DistributionAllocation.AcknowledgedByEmployeeId (attribution is via BaseEntity.CreatedByUserId/UpdatedByUserId). Absent entirely: any in-transit / reservation / freight / lot / approval / allocation-method columns; DocumentType field (movements use the "Distribution" string + "DST" sequence, not the DocumentType=2 enum).

Referenced (dependency-owned): PurchaseReceipt/PurchaseReceiptLine, Location, ProductVariant, LocationProduct (+ QuantityOnHand/Cost/LastReceivedDate; QuantityCommitted unused), InventoryMovement, TaxGroup, DocumentSequence.


5. API surface

Surface Verb+Route Impl Realizes Roles
API POST /distributions CreateDistributionEndpoint DST-F001, F002, F003, F004, F007, F010, F023 Admin/SysAdmin
API GET /distributions/{PublicId} GetDistributionEndpoint DST-F009 "
API GET /distributions ListDistributionsEndpoint DST-F009 "
API POST /distributions/{PublicId}/complete CompleteDistributionEndpoint DST-F005, F006, F011, F019 "
API POST /distributions/{id}/lines/{line}/allocations/{location}/acknowledge AcknowledgeAllocationEndpoint DST-F017 (Partial) "
portal / admin — none — NO UI EXISTS

5b. Validations inventory

Present — field-level (FluentValidation, every endpoint): PublicId prefix checks (receipt/location/variant/receipt-line/line); Lines NotEmpty; per-line positive-decimal column checks; per-allocation location + positive qty; Notes max 500; list page/size 1-100 + sort dir + optional source-location prefix.

Present — business-rule (handlers): all PublicIds resolved MerchantId-first with 400 on unknown; receipt-line-belongs-to-receipt check (:203-223); dup-document-number 409 (:278-281); atomic Complete transition (Draft/Allocated only → 409 otherwise); composite route-forgery defense on acknowledge (line must belong to the distribution); already-acknowledged 409; one-allocation-per-store-per-line (unique index).

Absent — business validations:

Missing validation Effect today Tracked as
Allocated ≤ TotalQuantity (over-distribution guard) A line can allocate more than its totalAllocatedQuantity is summed, never checked (:264). WIDS365 has this guard (@diffCount) scored DST-F027 (Missing) — Fable F-1 elevated it from a §5b note to a feature
Source warehouse has enough stock Complete decrements even if it drives the warehouse LP negative (only warns if the LP is missing) §7 hardening
Active-location / same-merchant checks on each destination store A distribution to an inactive store may be allowed §7 hardening

5c. Test surface & confidence

Second axis, orthogonal to the score. Testedness never enters §6.

Inventory (at 5d527ffb): 17 integration (tests/QIIUB.Tests.Integration/Features/Inventory/DistributionTests.cs, 17 [Fact], 0 [Theory]) · 0 unit · 0 e2e. No frontend tests (no UI). Counts are context.

Critical-flow coverage:

Flow Tested
Create with allocations (201, status Allocated, dist_/dstl_ prefixes, counts)
Complete → move stock (MovementsCreated=4, warehouse 100→30, Store1=40 / Store2=30) ✓ (:266)
Two lines same variant/store, no existing dest LP (SQL-2601 dedup, Price stays NULL) ✓ (:327)
Acknowledge (sets date; double-ack → 409) ✓ (:385)
List + filter by source location
Cross-merchant isolation (get / complete / acknowledge) ✓ (3, :497/:526/:554)
404 / 409 / route-forgery / wrong-prefix (complete non-existent, already-completed, ack line-not-in-dist, wrong prefixes)
Over-allocation vs TotalQuantity rejected ✗ (no such guard — §5b)
InventoryMovement row content asserted (beyond count)
Warehouse-goes-negative / insufficient-source guard
Dead states (Shipped/Cancelled), Edit, Cancel ✗ (no endpoints)
Dedicated unit tests ✗ (0)

Named test gaps → §7 "test hardening": the over-allocation guard, movement-content assertions, insufficient-source, and zero unit coverage. Reasonable core coverage (17 tests incl. the mandatory 3 cross-merchant) for a backend-only module.

Live confirmation (2026-07-27). Drove the full chain against the running app (local dotnet run API + SQL, magic-link auth, API-only — no portal) on merchant 1 (TechZone): created a distribution (source Loc 1, PurchaseReceiptId = a Draft receipt as the required FK) of variant 76 total 40 → allocated Loc 2 = 25 + Loc 3 = 15, then completed. Observed:

This live-drives the T1 core (F005/F011 + auto-create + lifecycle) on a running system. (The source was a stocked store + a Draft receipt-for-FK — a mechanical confirmation of the Complete write path, which is identical regardless of whether the source is labeled a warehouse; the receipt→distribute business chain is exercised end-to-end in the 17 integration tests.)


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) 9 0 0 0 9 9.0 100.0%
T2 (×2) 3 3 10 0 16 4.5 28.1%
T3 (×1) 0 0 3 0 3 0.0 0.0%

T1 scored (9): all Implemented (F001–F008, F011) → 9.0 / 9 = 100.0%. T2 scored (16): Implemented F009/F010/F023; Partial F017/F019/F028; Missing F012/F013/F014/F015/F016/F018/F020/F021/F022/F027. → 3 + 3×0.5 + 10×0 = 4.5 / 16 = 28.1%. T3 scored (3): all Missing (F024–F026). → 0.0 / 3 = 0.0%. Excluded: 6 Dependency (DST-D01–D06) + 5 N/A (DST-N01–N05).

Overall weighted:

numerator   = 3×9.0 + 2×4.5 + 1×0.0 = 27 + 9 + 0 = 36
denominator = 3×9   + 2×16  + 1×3   = 27 + 32 + 3 = 62
overall%    = 36 / 62 = 58.1%

Distribution module completeness: ≈ 58% overall — T1 100%, T2 28%, T3 0%.

Interpretation. The distribution core is complete and tested (T1 100%): a warehouse's received goods can be allocated across N stores in one document and posted, moving stock warehouse→stores with a paired movement trail, tenant-isolated, over 17 integration tests. But the module is backend-only (no UI) and operationally thin (T2 28%): no edit/cancel, no auto-allocation methods (manual only — even WIDS365 is manual), no source reservation, no in-transit stage, no approval, no print, no reporting, no over-distribution guard, and the per-store "receipt" is a bare timestamp with no variance. T3 is 0% (no landed cost / cluster / AI). ≈58% sits just under Transfers (60%) — read it with the surface caveat: the number is carried by the T1 weight, and the module is not usable by a merchant without direct API calls. The honest one-liner: a solid, tested engine with no cockpit and few instruments.

Sensitivity / anti-gaming notes:


7. Gap list & recommendations

Rank Gap (ID) Tier Impact Recommendation ADR?
1 No portal/admin surface (§5) Highest — practical. The module is not operable by a merchant; only an API caller can distribute Build a qiiub-admin (or portal) Distribution list/create/complete UI over the existing 5 endpoints No (UI over shipped API)
2 DST-F016 In-transit + DST-D06 offline sync T2 High — correctness for offline-first. Complete writes every store synchronously; an offline store can't receive in-transaction Add an in-transit stage + route per-store increments through the sync engine (eventual) requires new ADR (offline distribution model)
3 DST-F012 Edit / cancel T2 Medium — a created distribution can't be revised or voided Add Update (Draft-only) + Cancel endpoints; wire the dead Cancelled state No
4 DST-F013 Auto-allocation methods T2 Medium — manual-only is tedious for a real chain Add even-split first (table-stakes), then min-max/sales-history from per-store reorder + sales data Likely new ADR (allocation engine)
5 Over-allocation guard (§5b) Medium — a line can distribute more than its total Validate Σ allocations ≤ TotalQuantity in Create (WIDS365 has this guard) No
DST-F017 receipt-with-variance, F015 reservation, F018 approval T2 Medium Extend acknowledge into a real per-store receipt (qty + variance); reserve QuantityCommitted at create; add an approval state some need ADR
DST-F020 print, F021 report, F022 monitoring T2 Low-Med Per-store pick-list (ADR-0070) + a distribution report + ordered-vs-received view Promote ADR-0070 for print
DST-F024 landed cost, F025 cluster, F026 AI T3 Low now Defer (ERP-tier) new ADR each

Quick wins: #5 over-allocation guard, DST-F012 edit/cancel, wire the dead *EmployeeId columns. Net-new: #1 UI, #2 in-transit + sync, #4 allocation engine, 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 table is generated by the render tool at build time (query GHES by part:* + feature-ID convention), never hand-maintained — an issue that closes updates the badge on the next render, exactly like the staleness stamp. Shown here as a one-time demo of how it looks. A — untracked row is a gap with no open issue — a prompt for a human to file one (the scorecard flags, it does not auto-create).

Gap (ID) Tracked by (open GHES issue)
Whole module — design/paradigm + no ADR (§8 C2) #303 "Distribution — decide Paradigm A/B/C + FK + ADR" (part:adr, P2)
#2 In-transit + offline sync (F016 / D06) #303 (the paradigm decision covers the in-transit/movement model) — partial
#1 No portal/admin UI (§5) untracked
#3 Edit / cancel (F012) untracked
#4 Auto-allocation methods (F013) untracked
#5 Over-distribution guard (F027) untracked
#6 Print (F020) / reports (F021) / monitoring (F022) untracked (print pairs with the proposed ADR-0070, not an issue)
Reservation (F015), approval (F018), landed cost (F024), cluster (F025), AI (F026) untracked
(adjacent, other modules — shows cross-refs the tool would surface) #807 "DevSeed coverage — populate empty feature areas" (↔ the C3-class seed gaps); #902 "supplier minimum order amount" (↔ PO-F056)

Read: only the module's design decision is ticketed (#303); every concrete build-gap is untracked. That is the actionable signal — a dev sees at a glance that shipping Distribution for real means filing issues for the UI, in-transit, auto-allocation, and the operational breadth, none of which exist yet.


8. Open questions & contradictions

Contradictions / findings:

  1. C1 — no operable surface: a shipped backend module (5 endpoints, 17 tests) with zero UI in either app. Capability-complete at T1, unusable in practice. (§5/§7 #1.)
  2. C2 — no Distribution-specific ADR: a shipped module governed entirely by cross-cutting ADRs — the design was never ratified. The only design coverage (inventory-supply-chain.md §4.3) is frozen at Draft 2026-03-13 and predates the PublicId sweep + tax resolver.
  3. C3 — dead enum values & columns: DistributionStatus.Shipped(2)/Cancelled(4) never set; DocumentType.Distribution(2) defined but not wired (movements use the "Distribution" string); EmployeeId/ClosedByEmployeeId/AcknowledgedByEmployeeId never written.
  4. C4 — offline-first tension: Complete mutates every destination store's LocationProduct in one synchronous server transaction — but stores are offline-first. Getting the increment to an offline store is an unaddressed sync concern (DST-D06).
  5. C5 — over-allocation not guarded: AllocatedQuantity is summed but never checked against TotalQuantity; a shipping BCPOS sibling (WIDS365) does guard this.
  6. C6 — index-vs-PROGRESS: CLEAN (feature landed in the Phase-5 bulk commit; only PR #149 PublicId is tracked — expected).

Decisions needed from a human:


9. Source reconciliation (denominator integrity)

Every capability in the 4-source inventory (artifact-distribution-gathering-market.md, 37 unified rows) + QIIUB-originals mapped to a disposition. scored = own §3 row; folded = subsumed; N/A = out of scope; dep = neighbor.

Source capability Disposition
1 Distribution document (header/detail) scored DST-F001
2 Source warehouse/HQ designation scored DST-F002
3 Allocate one item across N stores scored DST-F003
4 Multi-item per distribution folded into DST-F001 (multiple lines)
5 Manual per-store quantity entry scored DST-F004
6 Draft / edit / delete before posting split — Draft exists (folded DST-F006); edit/cancel → DST-F012 (Missing)
7 Document numbering / sequence scored DST-F007
8 Auto-create child store orders/transfers on post folded into DST-F005 (QIIUB achieves the fan-out outcome by moving stock directly — no child docs; the child-doc mechanism is DST-N02 N/A)
9 Source-warehouse stock deduction on post scored DST-F005
10 Per-store stock increment on post scored DST-F005
11 Inventory movement ledger scored DST-F011
12 Over-distribution guard scored DST-F027 (Missing) — Fable F-1: maps to a concrete WIDS365 source + material, so a feature
13 Even / equal-split allocation scored within DST-F013 (bundled)
14 Fixed-quantity per store folded into DST-F013
15 Min-max / reorder-point allocation folded into DST-F013
16 Sales-history proportional allocation folded into DST-F013
17 Store-attribute / cluster allocation scored DST-F025
18 AI demand-forecast auto-replenishment scored DST-F026
19 Allocation scheduling across dates (APOP) N/A DST-N03
20 Bulk quantity formula N/A DST-N04
21 Recipient store-set selection split — explicit store list folded into DST-F003; group-inherited defaults → N/A DST-N05
22 Item selection by dept/category/supplier filter scored DST-F014
23 Create distribution from a receiving scored DST-F010
24 Deliver-to-warehouse-for-later-disbursement folded into DST-F010 (QIIUB's model is receive-at-warehouse-then-distribute)
25 Source-stock reservation / commit scored DST-F015
26 In-transit / mark-as-sent state scored DST-F016
27 Per-store receipt with variance scored DST-F017 (Partial)
28 Approval / multi-state workflow scored DST-F018
29 Master doc linking child docs N/A DST-N02 (QIIUB moves stock directly, no children)
30 Cost & price carry on lines scored DST-F019 (Partial)
31 Landed-cost / freight distribution scored DST-F024
32 Per-store pick-list / packing slip / label scored DST-F020
33 Per-store distribution report scored DST-F021
34 Distribution status monitoring scored DST-F022
35 List / search / filter distributions scored DST-F009
36 Temporal / audit history scored DST-F028 (Partial) — Fable F-4: partial-equivalent (movement trail + attribution) but no temporal tables → own row
37 Multi-location PO → central receive → fan-out folded into DST-F010 (the "multi-location PO" half is PO's; the central-receive→distribute half is DST-F010)
(RMS/RMH) HQ worksheet broadcast mechanism N/A DST-N01 (disconnected-DB artifact)
(QIIUB-original) Cross-merchant isolation scored DST-F008
(QIIUB-original) Notes/remarks scored DST-F023
(QIIUB-original, future) Offline sync of increments dep DST-D06

No source line is left unreconciled (37 market/legacy rows across 4 source families — RMS/RMH + WIDS365 + market + QIIUB-original).


Glossary

Term In plain words
Distribution Sending received stock from a warehouse out to several stores in one document.
Allocation How much of an item each store gets (one row per store).
Source warehouse The location the stock is sent from (where it was received).
Complete The step that actually moves the stock: warehouse down, each store up, with a movement trail. Before that it's just a plan.
Acknowledge A store marking "I got it" — today just a timestamp, with no quantity check and no effect on stock.
Fan-out (and why QIIUB doesn't do it) Other systems turn a distribution into one child transfer/order per store. QIIUB skips that and moves the stock directly — simpler, one document.
In-transit Stock that has left the warehouse but not yet arrived — QIIUB has no such stage; stock moves instantly at Complete.
Auto-allocation Letting the system compute each store's quantity (even split, by sales, by min/max) instead of typing them — not built; QIIUB is manual-only.
Reservation (QuantityCommitted) Holding stock aside at the warehouse between planning and posting — not done here.
Worksheet / P/O Planner How legacy HQ systems distribute — a broadcast worksheet that generates per-store orders because HQ and stores are on separate databases. QIIUB doesn't need it (one cloud database), so those features are N/A, not missing.
Single-owner rule Each capability is scored in one module. Distribution writes Inventory's tables but "distributing stock" is scored here, once; it does not overlap Transfers.
Dependency (in this doc) A capability owned by a neighbor (Inventory, Receivings, Sync), so it's not counted in this %.
N/A (in this doc) A legacy capability that doesn't fit QIIUB's single-cloud model (HQ broadcast, master-child docs), so it's not a gap and not counted.
Backend-only The feature works through its API but has no screen — a merchant can't use it without a developer calling the endpoints.