measured at 5d527ffb
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 isfile:line/ endpoint againstD:\BCPOSGit\qiiubon branchmainat commit5d527ffb. 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.
| 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".
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.
core for chains with a central warehouse; not relevant for a single-store merchant.used (central-fill / branch replenishment), same lot/expiry caveat as the rest of the supply chain (not modeled).used (yard → job-site/truck replenishment).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.
POST /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.)POST /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.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.
| 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):
LocationProduct; "distributing moves the stock" is the distribution's outcome → scored here (DST-F005).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
Missingmeans "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), NOTMissing— 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.
| 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 |
| 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 |
| 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. |
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.
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.
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.
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.
| 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 | — | — |
apps/ for distributions/Distributions returns zero route/page/component/API-client hits — only a read-only movement-type badge in the Movements Log (inventory.ts:151,161, MovementsLogPage.tsx:68,79). A merchant admin cannot run a distribution without calling the API directly. This is a surface gap, not a capability gap — §6 scores capability, so it does not lower the %, but it is the #1 practical concern (§7).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 total — AllocatedQuantity 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 |
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:
LocationProduct 50 → 10 (−40); existing dest Loc 2 17 → 42 (+25); Loc 3 had no LocationProduct → auto-created with QuantityOnHand = 15, Cost = 239.02, Price = NULL (inherit) — confirms the auto-create + cost-to-new-LP + Price-inherit path.InventoryMovement rows, all MovementType.Distribution (=7), ReferenceType="Distribution", DocumentNumber=DST-000002: Loc1 −25, Loc2 +25, Loc1 −15, Loc3 +15 (warehouse-OUT + store-IN per allocation).Allocated → Completed; AllocatedQuantity summed to 40.LastReceivedDate) afterward — zero DB residue.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.)
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%
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:
artifact-distribution-fable-review.md). Four CONFIRMED corrections: +DST-F027 over-distribution guard scored (F-1, it maps to a concrete WIDS365 source + is material, so a feature not just a §5b note); F009 + F010 demoted T1→T2 (F-2/F-3, their source rows are T2 — they were score-favorable promotions; both stay Implemented so T1 remains 100%); +DST-F028 temporal history scored Partial (F-4, QIIUB has no temporal tables — partial-equivalent via the movement trail). Evidence layer verified clean; arithmetic exact.backend-only badge on the dashboard card, and flagging (for JJ) a template amendment on whether an API client satisfies "reaches the intended actor". The no-UI gap stays in §5/§7, not the score.MovementType=7; no fan-out into Transfers → zero overlap with the Transfers scorecard. Do NOT fold TRF-F032 here (receipt-required blocks receipt-less multi-dest).| 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.
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 bypart:*+ 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— untrackedrow 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.
Contradictions / findings:
inventory-supply-chain.md §4.3) is frozen at Draft 2026-03-13 and predates the PublicId sweep + tax resolver.DistributionStatus.Shipped(2)/Cancelled(4) never set; DocumentType.Distribution(2) defined but not wired (movements use the "Distribution" string); EmployeeId/ClosedByEmployeeId/AcknowledgedByEmployeeId never written.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).AllocatedQuantity is summed but never checked against TotalQuantity; a shipping BCPOS sibling (WIDS365) does guard this.Decisions needed from a human:
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).
| 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. |