Wiki 🔍 Search
QIIUB · Module completeness

Stock Counts

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

measured at 3bfe3b23

39%
Overall
T182%
T210%
T30%

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

Sixth filled instance of module-scorecard-template.md (after PO, Receivings, Adjustments, Transfers, Distribution) — the last member of the inventory-movement family. Read-only analysis; recommendations only. Evidence is file:line / endpoint against D:\BCPOSGit\qiiub on branch main at commit 3bfe3b23. Gathering source-of-record: artifact-stock-counts-gathering-{repo,rmhrms,wids365}.md + artifact-stock-counts-market-{pos,retail,erp}.md. Scored with all four source families (RMS/RMH + WIDS365 + modern market + QIIUB-original).

Read the headline honestly — two caveats. (1) Stock Counts is a backend-only module: 4 API endpoints, zero portal/admin UI (§5/§7). Its only presence in any user-facing surface is a greyed-out "Coming soon" tile. A merchant cannot perform a physical count through any QIIUB interface today. (2) The 39% is the lowest in the movement family, and unlike its five siblings T1 is not 100% — the count document cannot be revised or abandoned once created (§3). The posting engine at the end of the flow is genuinely solid; almost everything a merchant would touch on the way there is absent.

Léelo así. Stock Counts es el conteo físico: se abre un conteo en una ubicación, se registran las cantidades contadas, y al cerrarlo QIIUB calcula la diferencia contra el inventario del sistema y la postea automáticamente como un ajuste. Ese motor de cierre funciona bien y es transaccional. El problema es todo lo demás: no hay pantalla (solo un tile "Coming soon"), el conteo se envía completo de una sola vez y no se puede corregir ni cancelar, la merma se contabiliza a costo cero (así que no se puede saber cuánto dinero se perdió), y no se registra quién contó.


0. Document control

Field Value
Module name Stock Counts (Physical / Cycle Inventory Counts)
File stock-counts-scorecard.md (docs/modules/stock-counts-scorecard.md)
Author / session Claude (Opus) — module #6
Created 2026-07-28
Last-Updated 2026-07-28
Repo commit (SHA) analyzed 3bfe3b23
Governing ADRs None dedicated. Incidental only: 0081 (Inventory Hub IA — specifies the unbuilt counts UI and mis-states the endpoint), 0082 (PublicId/TypeID — scnt/scln), 0020 (LocationProduct not StockLevel), 0065 (product status filter), 0062 (user attribution). ADR-0027 governs ShiftCount, a different module. Evidence only; this scorecard creates none
Governing design docs docs/design/inventory-supply-chain.md §496; docs/design/offline-function-matrix.md:80; docs/design/21-pos-client.md:79 (the latter two contradicted by code — §8)
Status of this analysis Adversarially-verified — Fable pass (findings + resolutions in artifact-stock-counts-fable-review.md); live-confirmed against the running app 2026-07-28 (§5c)

Last-Updated = the measurement date, pinned to commit: — bumped only on a re-score (.claude/rules/docs.md carve-out).


1. Module identity


2. Dependency map

Module Direction What crosses the boundary Scoring treatment
Adjustments downstream Finalize constructs an InventoryAdjustment (born Completed) as the correction artifact dependency (SC-D01) — the document type is theirs; the count→variance→correction capability is scored here (mirror of ADJ-D02)
Reason Codes (cross-cutting reference data) shared-infra The CYCLE-COUNT reason the auto-adjustment carries dependency (SC-D02); the lazy auto-create of that row on first finalize is scored here (SC-F006)
Inventory core shared-infra InventoryMovement ledger + LocationProduct.QuantityOnHand that finalize writes, and the Movements Log UI that surfaces Count movements dependency (SC-D03, SC-D04)
Reports downstream Aggregate shrinkage / variance-by-period reporting dependency (SC-D05) — contestable, see §9
Document sequencing shared-infra IDocumentSequenceService mints CNT-###### and the second ADJ-###### dependency (SC-D06); the use (documents carry a number) is scored SC-F007
Tax shared-infra ILocationProductTaxResolver assigns TaxGroupId on a newly-created LocationProduct dependency (SC-D07)
Sync / POS shared-infra Offline transport — exists but is not wired for counts dependency (SC-D08); the in-module gap (no offline count capture) is scored SC-F039
Employees upstream The Employee identity the reserved EmployeeId would point at dependency (SC-D09)
Platform (ADR-0062) shared-infra CreatedByUserId / UpdatedByUserId base columns dependency (SC-D10)
Locations / Products upstream The Location scope and the ProductVariant being counted referenced, not re-specified (§4)

Boundary statement. Stock Counts owns the StockCount + StockCountItem tables, all 4 endpoints under Api/Features/StockCounts/, the InProgress → Completed transition, the expected-on-hand snapshot and variance model, and — decisively — the semantic capability that a counted difference becomes a posted correction. It does not own the InventoryAdjustment document type it instantiates, the ReasonCode catalog it writes one row into, the InventoryMovement/LocationProduct tables it mutates, or the aggregate reporting over the result.

Boundary-leak checklist.

Single-owner rule. Verified: none of the 40 scored features here is also scored in PO / Receivings / Transfers / Distribution / Adjustments. Conversely, Adjustments' ADJ-D02 and its parked line "Auto-reconcile concurrent movement during count (RMH refinement)" (adjustments-scorecard.md:328) both land here — the first as Implemented (SC-F006), the second as Missing (SC-F022).


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 (whose physical-inventory feature is one of their richest areas) + WIDS365 + modern inventory tooling (Square/Clover/Lightspeed/Shopify+Stocky/Cin7/NetSuite/Odoo) + QIIUB-original. A Missing means "that world has it and QIIUB doesn't (yet)," not a commitment.

About the reference systems. RMS (Microsoft Dynamics Retail Management System) is the older base; RMH (Retail Management Hero) is its current, actively-developed successor — built on the RMS database to keep upgrades smooth, and extending it with additional tables plus visual and integration extensions. RMH is one of the products BCPOS resells. (RMS's Headquarters / HQ is called Central in RMH.) They are used here as a completeness yardstick, not a competitive ranking: where QIIUB does something differently, that reflects its cloud / single-database architecture, not a judgment that RMS/RMH is inferior. QIIUB is a newer evolution alongside them.

WIDS365 has no physical-count feature at all — a well-evidenced absence, and a wider subset margin than in the previous five modules. Sweeps of sys.tables (183 + 29), sys.procedures, and its 20-row Report catalog return no count entity, no variance report, and no store-facing inventory screen; counting is done through the generic AdjustmentHeader/AdjustmentDetail document, where Detail.Quantity is a signed delta so the operator computes variance by hand. Its AnalyticsItemStockCount dashboard tile is a name collisionSUM(ItemDynamic.Quantity), an on-hand aggregate — and is marked N/A (SC-N04), not credited. What WIDS365 does contribute to the yardstick is generic document infrastructure QIIUB lacks: structured bulk ingest (web_AdjustmentDetailUpsertByJson — MERGE-idempotent, resolves by ItemID or ItemCode, per-row named errors, fed by clipboard paste and .txt/.csv/.tsv drop) and temporal audit on header and detail.

Market shape (from the 3-segment scan: POS / retail-ecom / ERP). Counting splits into a table-stakes tier (open a count, capture quantities, auto-variance, post to on-hand, scan/mobile capture, scope filters, count sheet, CSV round-trip, who-counted, valued variance, uncounted→zero-or-leave) and an ERP tier (count schedules, ABC-class selection, tolerance/approval routing, transaction freeze, GL posting, serial/lot/expiry counting, bin-level counting). QIIUB has the posting engine and essentially none of the table-stakes operational tier — which is why T2 reads 10% rather than the family-typical 22–46%.

Three calibrations from the scan materially shaped the tiering, and each is worth stating because they cut against the intuitive reading:

  • Nothing is universal across the POS tier: Clover ships no stocktake at all in its core product (no count/audit/adjustment resource in its developer docs; its own blog points merchants to third-party apps, and the "Inventory Audits" feature belongs to Clover Sport, a different product line). The honest POS benchmark is Square ∩ Lightspeed, with Clover as the floor — so QIIUB having a count engine at all already clears the floor.
  • No product freezes or snapshots expected quantity at count start except NetSuite. Shopify/Stocky, Cin7 and Odoo all compare against live on-hand; Cin7's answer to mid-count movement is to lock the location and stop trading, which is not available to a POS. QIIUB's persisted SystemQuantity baseline (SC-F003) is therefore ahead of the market norm, not catching up to it — a point in its favor that the 39% does not convey (though see SC-F003 for why it is a sound design choice rather than a delivered advantage).
  • Blind counting, tolerance thresholds and scheduled counts are documented by almost nobody below the ERP tier. Blind in particular is absent from every product scanned and from RMS/RMH (which achieve it only by omitting the expected-quantity column from the printed sheet), so it is scored as QIIUB-original whitespace (SC-F021, T3) rather than a market parity gap.

One dated market note, recorded as a point-in-time fact rather than a prescription: Shopify is retiring Stocky on 2026-08-31, after which the go-forward Shopify baseline is thinner (a CSV round-trip plus a ≤1,000-item "Quick count"). A future re-score should not benchmark against Stocky's richer feature set.

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

ID Name Description Source Status Evidence Notes
SC-F001 Open a count (scoped to a location) Create a count document against exactly one location RMS+RMH+market Implemented StartStockCountEndpoint.cs:67, :115-123; StockCountConfiguration.cs:18-22 Location required and resolved from PublicId; 400 if unknown (:89-93). Per-location partial counts are possible
SC-F002 Capture counted quantity per item Record what the counter physically found, per product variant RMS+RMH+WIDS365+market Implemented StartStockCountEndpoint.cs:125-137; StockCountItem.cs:16 decimal(18,4). Submitted in the opening POST, not entered over time — see SC-F011
SC-F003 Snapshot expected on-hand Persist the system quantity each counted line is measured against RMS+RMH+market Implemented StockCountItem.cs:15; StartStockCountEndpoint.cs:111-113, :128; test AdjustmentAndCountTests.cs:406 Real decimal(18,4) column, written once from LocationProduct.QuantityOnHand. A variant with no stock row snapshots as 0 (:128), so a never-stocked item can be counted. Rarer than it looks: of the products scanned only NetSuite also snapshots at count start — Shopify/Stocky, Cin7 and Odoo all compare against live on-hand. Read that as a sound design choice, not a delivered advantage: because the whole count arrives in one POST (SC-F011), the baseline and the counted figure are captured at the same instant, so the snapshot's value is limited to the create→finalize window — real, but narrower than the count-open-to-count-close window RMS/RMH protect. And the window is left unguarded (SC-F022), so the baseline is a good foundation with nothing built on it
SC-F004 Automatic variance calculation Compute counted − expected per line RMS+RMH+market Implemented StartStockCountEndpoint.cs:135; StockCountItemConfiguration.cs:13 Stored, not derived. Finalize re-reads the stored value rather than recomputing (FinalizeStockCountEndpoint.cs:111-112) — the drift consequence is SC-F022
SC-F005 Post the count to stock + ledger Finalize moves on-hand and writes an append-only movement per variance line RMS+RMH+WIDS365+market Implemented FinalizeStockCountEndpoint.cs:178-188 (movement), :210-213/:220-228 (QoH upsert), :73:239 (one transaction) The module's core purpose, and its strongest part: all-or-nothing transaction with the rationale documented at :66-72, MovementType.Count (:183) distinct from manual Adjustment, .Local-first upsert fixing SQL-2601 (:199-208), and QoH RowVersion retry (:233)
SC-F006 Variance becomes a posted correction The difference auto-creates a Completed reason-coded adjustment document RMS+RMH+market Implemented FinalizeStockCountEndpoint.cs:147-157, :232; reason find-or-create :126-143; test AdjustmentAndCountTests.cs:435 RMS/RMH's Calculate → Commit. Born Completed, skipping Draft — no review step (see SC-F030). Lazily mints the CYCLE-COUNT reason row on first use. This is the capability Adjustments cedes as ADJ-D02
SC-F007 Document numbering Human CNT-###### per merchant RMS+RMH Implemented StartStockCountEndpoint.cs:107; S12_DocumentSequencesSeed.cs:29; unique UQ_StockCount_MerchantId_DocumentNumber (StockCountConfiguration.cs:25-27) A second number (ADJ-######) is minted for the auto-adjustment (FinalizeStockCountEndpoint.cs:145). Sequence service is shared-infra (SC-D06)
SC-F008 View count detail with variance Header + lines showing system / counted / variance and product identity RMS+RMH+market Implemented GetStockCountEndpoint.cs:57, :69-94 Deliberately opts out of the productActiveStatus filter (:70, rationale :66-68) so discontinued SKUs — a primary write-off case — still display
SC-F009 List / filter / sort / search counts Find a count among many RMS+RMH+market Partial ListStockCountsEndpoint.cs:66, filters :77-80, 6 sorts :86-95 What works: paging, filter by location + status, 6 sort keys. Missing: no free-text search on document number, no date-range filter, no aggregates (total variance / lines counted), no created-by surfaced, and SortBy has no allowlist so an unknown value silently falls through to the default (:94) — all of which the Adjustments list has
SC-F010 Status lifecycle The count's states and the guarded transitions between them RMS+RMH+WIDS365+market Partial StockCountStatus.cs; CAS flip FinalizeStockCountEndpoint.cs:93-105; test AdjustmentAndCountTests.cs:586 What works: InProgress → Completed is a genuine compare-and-swap (Status == InProgress is the predicate), so two racing finalizes cannot both post; 409 on repeat; a deliberate 404/409 split (:75-89). Missing: Cancelled = 2 (StockCountStatus.cs:10) is a dead enum member with no writer anywhere in src/ — you cannot abandon a started count, and the list can filter by a status that can never occur. No reopen either
SC-F011 Revise the count while open Add / edit / remove counted lines before posting RMS+RMH+market Missing No UpdateStockCount, AddStockCountItem, UpdateStockCountItem or RemoveStockCountItem folder in src/QIIUB.Api/Features/StockCounts/. Natural home: siblings of FinalizeStockCount/, cf. Features/Adjustments/{Update,AddLine,UpdateLine,RemoveLine,RemoveLines} A count is immutable from the instant it is created. The whole count arrives in the opening POST and the only subsequent action is finalize. Every yardstick system treats a count as a working document filled and corrected over time (RMS/RMH Open → entry → CalculateClose, with ad-hoc add/delete). A mis-keyed quantity in a 500-line count is uncorrectable — the only remedy is to finalize the wrong count, posting wrong stock and a wrong adjustment, then manually post a corrective Adjustment. Scored T1 for parity with Adjustments, where the same capability is T1 Implemented (ADJ-F005)

T2 — Standard (market broadly expects) — weight ×2

ID Name Description Source Status Evidence Notes
SC-F012 Server-side count-scope generator Build the count list from a scope (all-at-location / category / supplier / brand / not-counted-since) RMS+RMH+market Missing Caller must enumerate every variant — Items is NotEmpty() (StartStockCountEndpoint.cs:44) and the handler counts only what was passed (:125). No scope column exists (InitialMerchantSchema.cs:1209-1239). Natural home: a scope object on the start request + a server-side variant query RMS/RMH generate the count worksheet from exactly these criteria (incl. blank sheets and not-counted-since). Without it, "count this store" means the client must first fetch and post every SKU
SC-F013 Full-count mode Treat uncounted items as zero, not as untouched RMS+RMH+market Missing Finalize only touches submitted lines (FinalizeStockCountEndpoint.cs:110-113); nothing enumerates un-submitted variants at the location This is precisely what separates a full physical inventory from a cycle count. QIIUB supports only the cycle-count shape — corroborated by the design doc naming a second "Physical Count" reason that no code creates (§8-C6)
SC-F014 Print / export a count sheet Produce the paper or PDF sheet counters work from RMS+RMH+market Missing No print/PDF surface (repo-wide ADR-0070 print gap); natural home: the ADR-0070 print pipeline Table-stakes in both reference systems (RMH adds configurable layout — SC-F036)
SC-F015 Import counted quantities from a file Load counts from CSV/delimited file or a handheld export RMS+RMH+WIDS365+market Missing Only POST /stock-counts JSON (StartStockCountEndpoint.cs:67); no import endpoint. Natural home: a bulk-ingest endpoint modelled on WIDS365's web_AdjustmentDetailUpsertByJson (MERGE-idempotent, per-row named errors) RMS/RMH accept delimited + XML; RMH adds merge modes and an error preview. The single most reusable idea from WIDS365
SC-F016 Barcode-scan capture Count by scanning items rather than typing variant identifiers RMS+RMH+market Missing Request accepts ProductVariantPublicId only (StartStockCountEndpoint.cs:22) — no barcode resolution, although ProductBarcode exists and is even sync-enabled (SyncConstants.cs:13) Scan entry is how counts are actually performed. Market table-stakes
SC-F017 Valued variance (shrinkage in money) Cost the difference so the loss has a dollar figure RMS+RMH+market Missing grep "UnitCost|UnitPrice" src/QIIUB.Api/Features/StockCounts/zero hits. Both InventoryAdjustmentLine.UnitCost (:17) and InventoryMovement.UnitCost (:18) are non-nullable decimal, so every count-generated line and movement persists at 0.0000. The manual path does it right — CreateAdjustmentEndpoint.cs:186 UnitCost = lpCost.Cost ?? variant.Cost The module's most consequential defect. "How much inventory did we lose to shrink this quarter" — the single most-asked question of a physical count — is unanswerable from QIIUB data. RMH values variance by cost explicitly; the RMS/RMH Type=7 count journal carries cost. In-module gap, not a Reports gap (§2)
SC-F018 Reason coding on the variance Explain why each counted difference occurred RMS+RMH Missing No reason field on StockCountItem (:12-17). Natural home: a nullable ReasonCodeId on the count line, resolved per line at finalize instead of one blanket header reason Demoted from Partial by the adversarial pass (guard b). The only code that would have earned the half-credit is the fixed CYCLE-COUNT constant (FinalizeStockCountEndpoint.cs:127, :133) — and that is already the credited behavior of SC-F006, so crediting it again here double-counts. The actual capability is wholly absent: RMS/RMH carry ReasonCodeID per count entry plus a store-level "reason required" rule, so a count there can separate breakage from theft from mis-scan; QIIUB cannot distinguish them at all. Same correction the Adjustments pass applied to ADJ-F014
SC-F019 Who counted / who closed Attribute the count and its closure to a person RMS+RMH+market Missing EmployeeId (InitialMerchantSchema.cs:1218) and ClosedByEmployeeId (:1223) exist as columns with no FK (schema-data.json IsForeignKey:false) and no writergrep EmployeeId src/QIIUB.Api/Features/StockCounts/ → zero hits; FinalizeStockCountEndpoint.cs:95-97 sets only Status + ClosedDate. Wired siblings for reference: ClosePurchaseOrderEndpoint.cs:108, CloseShiftEndpoint.cs:142 Reserved schema with no logic ⇒ Missing per the status definition and the Adjustments precedent (adjustments-scorecard.md:249). RMS/RMH stamp CashierID on 100% of the 40,120 Type=7 count-journal rows sampled. No accountability today, and it blocks any counter-accuracy metric
SC-F020 Recount / second pass Re-count discrepant items and supersede the first figure RMH+market Missing No pass/attempt/recount column on StockCountItem (:12-17), and decisively no endpoint can revise a line (SC-F011). Natural home: a pass number on the line + a recount endpoint Today a recount means starting an entirely new count document
SC-F022 Concurrent-movement handling Detect, warn about, or reconcile stock that moves while the count is open RMH+market Missing Finalize reads the Variance stored at start (FinalizeStockCountEndpoint.cs:110-113) and never re-reads on-hand; nothing detects drift, warns, offers a re-snapshot, or blocks the movement The arithmetic happens to survive because posting is a relative delta (:212 +=) and deltas compose — but there is no way to express "the shelf is authoritative, set it to 98", and no audit signal at all that the count window was contaminated. An all-day count with the register open silently blends counted and transacted stock. This is exactly the RMH 3.9.19 refinement Adjustments parks at adjustments-scorecard.md:328
SC-F023 Last-counted tracking per item Record when each item was last counted, to drive coverage and scheduling RMS+RMH Missing No such column on LocationProduct; IX_StockCountItem_MerchantId_ProductVariantId exists (InitialMerchantSchema.cs:4759-4760) but no endpoint queries counts by variant RMS/RMH maintain Item.LastCounted; it is what makes "not counted since " scoping (SC-F012) and cycle-count scheduling (SC-F028) possible
SC-F024 Multi-counter / team counting Split one count across several people, each with their own sheet RMS+RMH Missing One nullable EmployeeId on the header (SC-F019), nothing per line (StockCountItem.cs:12-17) RMS/RMH support copying a count per counter and then reconciling
SC-F025 Variance-only review Show just the discrepant lines before committing RMH+market Missing GetStockCountEndpoint.cs:84-93 returns all lines undifferentiated; no variance filter or flag on the projection RMH filters to discrepancies and highlights them. Finalize internally filters Variance != 0 (:111) but nothing exposes that view to a human first
SC-F026 Role gating Only authorized roles can open and finalize counts RMS+RMH+market Implemented Roles("SystemAdmin","Admin") on all four endpoints Coarse RBAC — the market-standard control is present, matching how Adjustments scores the same capability (ADJ-F011). But a Manager cannot count at all, and RMH has a dedicated count permission; noted in §7 rather than scored separately
SC-F027 Count audit trail An append-only, attributable record of what the count did RMS+RMH+WIDS365+market Partial Movement trail carries ReferenceType="StockCount", ReferenceId, DocumentNumber back to the count (FinalizeStockCountEndpoint.cs:185-187); structured logs at StartStockCountEndpoint.cs:142-143 and FinalizeStockCountEndpoint.cs:241-243 What works: the ledger is traceable to the originating count and is append-only. Missing: no attribution is exposed on any response (GetStockCountEndpoint.cs:13-26 has no created-by/closed-by — Adjustments exposes both), and the finalize flip uses ExecuteUpdateAsync, which bypasses the EF audit interceptor, so UpdatedByUserId is not stamped on the transition and a reader sees the stale create-time value (reproduced live — §5c). RowVersion was removed from this row's credit by the adversarial pass: it is inherited base infrastructure (SC-D10), not in-module work. This is the softest Partial in the scorecard — demoting it to Missing would give 38%.

T3 — Advanced / Differentiator — weight ×1

ID Name Description Source Status Evidence Notes
SC-F021 Blind count Withhold the expected quantity from the person counting QIIUB-original Missing GetStockCountEndpoint.cs:90 always projects SystemQuantity; no mode field on the start request (:13-24), no column (InitialMerchantSchema.cs:1209-1239), no gated projection. Natural home: a CountMode column + a gated projection variant Whitespace, not catch-up. Scored QIIUB-original because no scanned product documents an enforced blind mode (Clover/Stocky/Cin7/Odoo absent; Square/Lightspeed/NetSuite unevidenced) and RMS/RMH achieve it only by omitting the expected-quantity column from the printed sheet. The expectation is internal: the platform already has the vocabulary on the cash side — ADR-0027 "Three shift close patterns (Full, Blind, Deferred Count)" — and never applied it to stock. Tiered T3 on market prevalence; a reviewer could argue T2 on audit necessity
SC-F028 Scheduled / recurring cycle counts Auto-generate counts on a cadence so coverage is systematic market Missing No schedule entity anywhere; natural home: a count-schedule table + a background job. Needs SC-F023 first ERP tier (NetSuite plan templates + due-date engine; Odoo per-location Inventory Frequency auto-setting the next count date). Absent from RMS/RMH and from every POS-tier product scanned. Contested tier, resolved against the source: the ERP gathering artifact explicitly recommended scoring this "top of T2", on the grounds that both ERPs converge on count-due scheduling by different mechanisms. That recommendation is rebutted here — T2 is defined as "the modern market broadly expects it," and the expectation fails at the POS tier entirely (unevidenced in Square and Lightspeed, absent in Clover, Stocky and Cin7) as well as in both reference systems. Two ERPs converging is evidence of ERP consensus, not of broad market expectation. The adversarial pass reviewed and upheld T3
SC-F029 ABC-class / risk-driven selection Choose what to count by value class or last-counted date market Missing No ABC or velocity classification on Product/ProductVariant; natural home: a classification column feeding SC-F012 ERP tier; also absent from RMS/RMH beyond the not-counted-since filter
SC-F030 Variance approval / tolerance threshold Require sign-off, or auto-accept only within a tolerance band market Missing Finalize posts immediately (FinalizeStockCountEndpoint.cs:93-97) and the adjustment is born Completed (:153), skipping Draft review entirely. :111 filters Variance != 0 with no tolerance band Not purely ERP tier — the adversarial pass caught the original "ERP tier" framing being falsified by this scorecard's own source: Square ships a confirmed approval gate on stocktakes, and Cin7 has a Draft→Complete confirm step. It is absent from RMS/RMH, and tolerance thresholds remain unevidenced anywhere. Kept T3 on overall prevalence, but the honest read is "present at the top of the POS tier and in ERP, absent in the reference systems" — the most defensible control gap for an auditable count
SC-F031 Freeze transactions during a count Block or quarantine movement at the location while counting market Missing No freeze column (InitialMerchantSchema.cs:1209-1239); StockCount is referenced by no write path outside its own feature folder ERP tier; absent from RMS/RMH. The heavyweight alternative to SC-F022's detect-and-warn
SC-F032 GL / journal posting of the write-off Post shrinkage expense to accounting market Missing No accounting integration; blocked behind SC-F017 (a $0 variance cannot be journalled) ERP tier (NetSuite/Odoo). RMS/RMH also lack per-count GL posting — parity with the reference systems
SC-F033 Bin / shelf granularity Count by bin or shelf location within a store RMS+RMH+market Missing StockCountItem has no bin column (InitialMerchantSchema.cs:2315-2332); no bin concept exists in the platform (cf. the damage-bin also Missing at adjustments-scorecard.md:249) RMS/RMH store BinLocation per count entry (though not as a scope criterion). Needs a platform-level bin model first
SC-F034 Serial / lot / expiry-aware counting Count identified units or track expiry while counting market Missing No serial/lot model on ProductVariant; platform-level prerequisite ERP tier; absent from RMS/RMH. Matters for the Rx vertical (expiry) more than Shop
SC-F035 Merge multiple counts Combine several count documents into one reconciliation RMH Missing No merge endpoint; a count is standalone (FinalizeStockCountEndpoint.cs operates on one countInfo.Id) RMH-specific refinement, paired with multi-counter counting (SC-F024)
SC-F036 Configurable sheet layout + count reports Tailor the printed sheet and produce detailed/summary count reports RMH Missing Blocked behind SC-F014 (no print surface at all) RMH adds layout configuration and Detailed/Summary reports on top of the base sheet
SC-F037 Count-progress tracking Show how much of the intended scope has been counted market Missing ListStockCountsResponse (ListStockCountsEndpoint.cs:22-28) carries ItemCount but no expected-scope denominator — without SC-F012 there is no scope to measure against Market convenience for long counts
SC-F038 Reverse / unpost a finalized count Undo a posted count in one action market Missing No inverse of the CAS flip (FinalizeStockCountEndpoint.cs:93-97); Completed is terminal. Natural home: a compensating-adjustment endpoint Mirrors ADJ-F016. Today the remedy is a hand-built offsetting Adjustment
SC-F039 Offline count capture on the terminal Count on a POS device with no connectivity, sync later QIIUB-original Missing StockCount/StockCountItem are absent from SyncConstants.EntityTypes (SyncConstants.cs:9-33), which gates every sync endpoint via IsAllowedEntity (:37); grep -rni "stockcount" apps/qiiub-pos/src → zero hits QIIUB's own stated ambition, and its clearest differentiator opportunity — counting a back room is exactly where connectivity fails. Two design docs already claim this works (offline-function-matrix.md:80, 21-pos-client.md:79); it does not (§8-C4/C5). Absent from RMS/RMH and from POS-tier market products
SC-F040 Count in a different unit of measure Count a case or box and have it convert to the stocking unit RMH+market Missing The count line carries a bare decimal with no UoM reference (StockCountItem.cs:16); natural home: a CountUoMId + conversion snapshot on the line, mirroring PurchaseOrderLine.PurchasingUoMId + UnitsPerPurchaseUoM Added by the adversarial pass, which correctly held that ceding this to the Units of Measure module would break the PO precedent: PO scores line-level UoM in-module (PO-F020 Implemented, with only the catalog as a dependency). A counter facing a pallet of 24-packs must do the arithmetic by hand

Dependencies (excluded from score — listed so the boundary is visible):

ID Capability Owner Note
SC-D01 InventoryAdjustment document type + manual Draft→Completed machine Adjustments adjustments-scorecard.md:70; finalize instantiates it, does not own it
SC-D02 ReasonCode catalog (CRUD, merchant-scoped) Reason Codes ReasonCode.cs:5-6; shared with Return/Void/Discount/AR. The lazy auto-create is scored SC-F006
SC-D03 InventoryMovement append-only ledger + Movements Log UI Inventory core InventoryMovement.cs; MovementsLogPage.tsx surfaces Count movements
SC-D04 LocationProduct.QuantityOnHand row + QoH concurrency helper Inventory core (ADR-0020) MerchantDbContextConcurrencyExtensions.cs; shared by 5+ writers
SC-D05 Aggregate shrinkage / variance-by-period report Reports Contestable — see §9. No such report exists on either side (grep -rni "stockcount|variance|shrink" src/QIIUB.Api/Features/Reports/ → zero hits)
SC-D06 DocumentSequence service shared-infra DocumentSequenceService.cs:22; the use is scored SC-F007
SC-D07 Tax-group resolution for a newly-created LocationProduct Tax ILocationProductTaxResolver; FinalizeStockCountEndpoint.cs:161, :216-218
SC-D08 Sync push/pull transport Sync src/QIIUB.Api/Features/Sync/ exists; simply not wired for counts. The in-module gap is SC-F039
SC-D09 Employee identity (table, PIN) Employees What the reserved EmployeeId (InitialMerchantSchema.cs:1218) would reference
SC-D10 User attribution base columns (ADR-0062) Platform BaseMerchantEntityConfiguration.cs:34-36; inherited infra, not in-module credit

N/A — not applicable to QIIUB (excluded; reason given):

ID Capability Why N/A
SC-N01 HQ/Central count-worksheet distribution + push-back (RMS/RMH Worksheet 501, Task 190, Worksheet 307) QIIUB runs one cloud database; a merchant-wide user counts any location directly, so there is no HQ/Central-to-store worksheet broadcast or reconciliation tier. Met-differently-by-architecture — the same call as the PO-Planner / Distribution / Adjustments HQ N/As
SC-N02 Central replication / consolidation of count documents Same single-database reason as SC-N01. RMS/RMH need it because store and Central are separate databases
SC-N03 Central item-management lockdown during an open count (RMH) A control that exists to protect the HQ↔store replication window SC-N01/N02 removes
SC-N04 WIDS365 AnalyticsItemStockCount dashboard tile Name collision, not a count featurefn_GetItemStockCountAnalytics is SUM(ItemDynamic.Quantity), an on-hand aggregate. Crediting it would be a false positive
SC-N05 Matrix / kit-specific count behavior Not documented in any source family — the RMS/RMH sweep found it specified in neither system, and no market product documents it. With no source expecting it there is nothing to measure against. Flagged for a human: if the in-flight product bundle-types work lands a real kit model, this should become a T3 row on the next re-score rather than staying N/A

4. Storage specification

StockCount (aggregate root) — src/QIIUB.Domain/Merchants/StockCount.cs

StockCountItem (child line) — StockCountItem.cs

Referenced (not owned): InventoryAdjustment + InventoryAdjustmentLine (→ Adjustments), ReasonCode (→ Reason Codes), InventoryMovement + LocationProduct (→ Inventory core), Location, ProductVariant. MerchantId-first composite FKs and tenant-first composite PKs verified throughout (Critical Rule #1).


5. API surface

Surface Verb + Route Impl (file:line) Realizes Roles Notes
API POST /api/stock-counts StartStockCountEndpoint.cs:67 F001–F004, F007 SysAdmin/Admin Opens the count and submits every counted quantity in the same call; snapshots system qty; computes and stores variance; 201 + Location header
API GET /api/stock-counts ListStockCountsEndpoint.cs:66 F009 SysAdmin/Admin Paged; filter location + status; 6 sorts
API GET /api/stock-counts/{PublicId} GetStockCountEndpoint.cs:57 F008 SysAdmin/Admin Header + lines; discontinued-product safe (:70)
API POST /api/stock-counts/{PublicId}/finalize FinalizeStockCountEndpoint.cs:50 F005, F006, F010 SysAdmin/Admin The posting op: CAS flip + movements + QoH upsert + auto-adjustment, one transaction

5b. Validations inventory

Absent — business validations (verified not present):

Missing validation Effect today Tracked as
Duplicate-variant de-duplication on start Two lines for the same variant both persist with the same SystemQuantity, so variance is double-counted and posted twice. .Distinct() is applied only to the resolution lookup (StartStockCountEndpoint.cs:97); the persist loop iterates raw req.Items (:125). Acknowledged in a test comment (AdjustmentAndCountTests.cs:472-473 "StartStockCount does not dedup") — the SQL-2601 symptom was fixed, the data defect was not §7 hardening bundle
CountedQuantity >= 0 A negative counted quantity is accepted, producing a nonsensical variance §7 hardening bundle
Cap on Items per request A full-store count can post an unbounded request body (40k lines) in one call; contrast the explicit cap on adjustment batch-delete (AdjustmentAndCountTests.cs:1927) §7 hardening bundle
SortBy allowlist An unknown SortBy silently falls through to the default instead of 400 folded into SC-F009 Partial

Per the framework rule these are bundled as one §7 hardening row rather than split into micro-features — splitting would pad the denominator. The duplicate-variant item is the materially dangerous one: it silently corrupts posted stock.


5c. Test surface & confidence

The §6 % says "how complete vs a mature POS"; this says "how trustworthy is the built part." Testedness never enters §6.

Flow Tested
Start → snapshots SystemQuantity ✓ (:406)
Finalize with variance → creates adjustment + movement + QoH ✓ (:435)
Two lines same variant, no existing LocationProduct → aggregates (SQL-2601 regression) ✓ (:470)
Zero-variance finalize → no adjustment, no movement ✓ (:514)
Cross-tenant: foreign PublicId → 404 on Get and Finalize ✓ (:537, :554)
Already-finalized → 409 ✓ (:586)
Wrong-prefix PublicId → 400 (4 endpoints) ✓ (:605, :616, :631, :646, :679)
List filter by location + invalid sort direction ✓ (:656, :689)
Cross-merchant isolation on the list endpoint (seed A and B, assert B sees only B) ✗ — only forged-PublicId 404s exist. The Adjustments half of the same file does have it (Adjustment_CrossMerchantIsolation, :697). This is a rule-level gap.claude/rules/csharp.md §Testing Requirements makes cross-merchant isolation mandatory per merchant-scoped feature
Transaction rollback (side effect throws → status flip reverts) ✗ — untested despite being the stated reason the transaction exists (FinalizeStockCountEndpoint.cs:66-72)
UnitCost on generated lines/movements ✗ — which is exactly why SC-F017 survived; the Adjustments path has these assertions (:1080, :1101)
CYCLE-COUNT reason auto-created on first use, reused on second ✗ — the find-or-create branch (:129-143) is unasserted
Concurrent finalize (the retry path at :233)
Duplicate-variant data correctness (not just the QoH aggregate)

Named test gaps → one §7 "test hardening" row, led by the mandatory cross-merchant isolation test and the UnitCost assertions.


6. Scoring rubric (mechanical)

Weights T1=3, T2=2, T3=1; Implemented=1.0, Partial=0.5, Missing=0.0; Dependency/N/A excluded.

Tier Impl Partial Missing Dep-excl Scored Σ value tier%
T1 (×3) 8 2 1 11 9.0 81.8
T2 (×2) 1 1 13 15 1.5 10.0
T3 (×1) 0 0 14 14 0.0 0.0
Excluded 10 Dep + 5 N/A
overall% = 100 × ( 3×9.0 + 2×1.5 + 1×0.0 ) / ( 3×11 + 2×15 + 1×14 )
         = 100 × ( 27 + 3 + 0 ) / ( 33 + 30 + 14 )
         = 100 × 30 / 77 = 39.0%  → 39%

Overall = 39% (T1 82 / T2 10 / T3 0). 40 scored features + 10 Dependency + 5 N/A = 55 mapped IDs.

This is the post-adversarial number — the first synthesis read 41%, and the pass corrected two things: SC-F018 reason-coding was demoted Partial → Missing (its only credited code was the CYCLE-COUNT constant already credited to SC-F006 — a guard-(b) double-count, the same error the Adjustments pass caught in ADJ-F014), and multi-UoM counting was reclassified from a Dependency on Units of Measure to an in-module gap (new SC-F040), because PO scores line-level UoM in-module (PO-F020) and ceding it here would have let a real gap hide behind a neighbor. The pass also upheld the two contested tier calls (SC-F021 blind → T3, SC-F028 schedules → T3) and the T1 parity argument, and rejected demoting SC-F003.

T1 is not 100% — the first module in the family where that is true, and it is the honest read rather than a harsher rubric. Two T1 capabilities that Adjustments scores as Implemented are not present here: line management (ADJ-F005 Implemented → SC-F011 Missing) and a complete status lifecycle (ADJ-F006 Implemented → SC-F010 Partial, since Cancelled has no writer). Scoring them any higher would require treating the same capability differently in two sibling scorecards.

Flat-Partial caveat. Only three Partials survive the pass, and two are thin: SC-F009 (list missing search, dates and aggregates) and SC-F027 (a trail with no attribution surfaced — the softest 0.5 in the document; demoting it would give 38%). SC-F010 is the one genuine "core-need-met, refinement-missing" Partial. The defensible range is 38–39%; read the headline as the top of that range, not a midpoint.

Anti-gaming guards (each checked):

Reproducibility honesty. The denominator (55 mapped rows) freezes at this first synthesis; a re-score updates statuses only. Family ranking: PO 74 / Rcv 67 / Adj 63 / Trf 60 / Dist 58 / Stock Counts 39 — last, by 19 points. The shape is unusual: a genuinely well-engineered posting core (transactional, CAS-guarded, concurrency-retried, ledger-typed, and snapshotting a baseline most of the market does not) attached to almost no operational module. Stock Counts is best read as a finalize endpoint that works, and a count workflow that was never built.


7. Gap list & recommendations

Rank Gap (ID) Tier Impact Recommendation ADR?
1 No portal surface (§5) Highest — practical. No merchant can count without direct API calls; the hub already advertises the feature as "Coming soon" Build the portal list + detail + count-entry pages ADR-0081 already specifies (/inventory/counts, /inventory/counts/$id) over the existing 4 endpoints — but F011 first, since a UI over a single-shot immutable POST is barely usable No (UI over shipped API)
2 SC-F011 revise the count while open T1 A mis-keyed quantity is uncorrectable; the count must be assembled perfectly client-side in one shot Add UpdateStockCount (header) + add/update/remove item endpoints, mirroring Features/Adjustments/ one-for-one. The unused UX_StockCountItem_…_PublicId index already exists for exactly this route No (extends model)
3 SC-F017 valued variance T2 Shrinkage cannot be measured in money — the primary business output of a physical count Quick win — snapshot UnitCost/UnitPrice onto the adjustment lines and movements at finalize, exactly as CreateAdjustmentEndpoint.cs:186 does. Fields already exist and are non-nullable No
4 SC-F010 cancel / abandon a count T1 A started count can never be closed except by posting it Quick win — a CancelStockCount endpoint flipping to the already-modelled Cancelled status, mirroring CancelAdjustmentEndpoint.cs No
5 SC-F019 who counted / who closed T2 No accountability on a stock-affecting operation; blocks any counter-accuracy metric Quick win — write EmployeeId / ClosedByEmployeeId from context and add the two FKs. Note the finalize flip must stop bypassing the audit interceptor No
6 Validation hardening bundle (§5b) Duplicate variants silently double-post stock; negative counts accepted; unbounded request body Dedup or reject repeated variants on start; >= 0 rule; Items cap; SortBy allowlist No
7 SC-F012 / F013 scope generator + full-count mode T2 "Count this store" is not expressible; only the cycle-count shape exists A scope object on the start request (all-at-location / category / supplier / not-counted-since) + a zero-out-uncounted mode. Needs SC-F023 last-counted for the date criterion Requires ADR (new count semantics — and there is no ADR at all today)
8 SC-F015 / F016 import + scan capture T2 Counting by typing variant identifiers is not a real workflow Bulk-ingest endpoint modelled on WIDS365's per-row-error JSON MERGE; barcode resolution over the existing ProductBarcode No (extends model)
9 SC-F022 concurrent-movement detection T2 An all-day count silently blends counted and transacted stock with no audit signal At finalize, re-read on-hand and flag lines whose baseline moved; surface it rather than silently posting No (extends model)
10 SC-F020 / F025 / F021 recount, variance review, blind count T2/T3 The count is not defensible as an audit control Add a pass number + recount endpoint; a variance-only view; a blind mode gating the SystemQuantity projection. Blind is whitespace, not catch-up — no scanned competitor documents it, so building it would differentiate rather than close a gap Requires ADR (count semantics)
11 SC-F030 approval / tolerance T3 No control on a high-value variance; the adjustment is born Completed with no review Approval gate before posting — would meet-or-beat both RMS/RMH and most of the market Requires ADR (new state)
12 SC-F039 offline count capture T3 QIIUB's own differentiator, already claimed by two design docs Add StockCount/StockCountItem to SyncConstants.EntityTypes + a POS count surface; or correct the two design docs Requires ADR (sync entity set)
13 SC-F018 / SC-F040 per-line reason code + count UoM T2/T3 A count cannot say why a line differs (breakage vs theft vs mis-scan), and a counter facing 24-packs converts by hand Both are one nullable column plus resolution on StockCountItemReasonCodeId (mirroring the header pattern) and CountUoMId + conversion snapshot (mirroring PurchaseOrderLine.PurchasingUoMId). Best done together with #2, since neither is reachable without line-level editing No (extends model)
14 Test hardening (§5c) The mandatory cross-merchant isolation test is missing; UnitCost unasserted (which is why #3 survived) Add the isolation test, UnitCost assertions, rollback fault-injection, and the reason-code find-or-create cases No
15 No ADR governs count semantics (§8) The module's most consequential behavior lives only in inline comments Write the missing ADR when #7/#10 are picked up — and amend ADR-0081, which mis-states the endpoint and over-claims delivered scope Requires ADR

7a. GHES issue coverage

At measurement (3bfe3b23) a search for stock count / physical count / cycle count / shrinkage / variance returned no open issue tracking any of these gaps — every one was untracked. #289 PublicId Audit Phase 2 is tangential (touches many modules).

Two issues were filed from this analysis before merge, so the four highest-value gaps are now tracked:

Issue Type Covers Priority
#1030 bug The §5b duplicate-variant double-postStartStockCount persists raw req.Items while .Distinct() applies only to the resolution lookup, so duplicate lines share a baseline and post the variance twice. Filed separately because it is the only finding that silently corrupts posted stock P1
#1031 tech-debt SC-F017 valued variance · SC-F010 cancel / dead Cancelled enum · SC-F019 counter attribution — three small, mostly-already-modelled gaps one PR can close together P2

Everything else in §7 remains untracked, including the whole T3 tier and the §7 #1 no-portal-surface gap. The stable IDs are the join key: reference them (e.g. SC-F017) in any future issue body so this section can link gap ↔ ticket on the next re-score, without which this table rots into a manual snapshot.


8. Open questions & contradictions

Eight doc-vs-code contradictions, both sides cited. The headline finding is a negative one: no ADR governs how physical counts work in QIIUB — not blind vs informed, not snapshot vs recompute, not freeze semantics, not partial vs full scope, and not the variance→adjustment pipeline, which is the module's most consequential behavior and exists only as inline comments (FinalizeStockCountEndpoint.cs:66-72, :190-198) and an entity XML doc (StockCount.cs:8-9). For a module whose finalize mutates on-hand stock and mints two documents, that is itself worth surfacing.

Decisions needed from a human:


9. Source reconciliation (denominator integrity)

Every capability in the 4-source inventory mapped to a disposition. scored = own §3 row; folded = subsumed; N/A = out of scope; dep = neighbor. Sources: artifact-stock-counts-gathering-{rmhrms,wids365,repo}.md + artifact-stock-counts-market-{pos,retail,erp}.md.

Source inventory line Disposition Where
RMS/RMH — table-stakes in both
Count document + Open → Calculated → Closed lifecycle scored (Partial) SC-F010
Count reference number + description scored SC-F007 (number); description folded into SC-F001 Notes
Scoped generation: all / dept / category / supplier / filter / not-counted-since / blank sheet scored (Missing) SC-F012
Ad-hoc add and delete of count lines scored (Missing) SC-F011
Manual quantity entry scored SC-F002
Scan entry scored (Missing) SC-F016
Delimited / XML import scored (Missing) SC-F015
Per-line count time folded covered by SC-F027 (audit trail); no per-line timestamp beyond CreatedAtUtc
Per-line bin scored (Missing) SC-F033
Per-line cost scored (Missing) SC-F017
Print count sheet scored (Missing) SC-F014
Calculate variance scored SC-F004
Commit posts the differential to on-hand scored SC-F005
InventoryTransferLog Type=7 count journal scored SC-F005 (movement) + dep SC-D03 (the ledger table)
Who-committed on the journal (CashierID, 100% populated) scored (Missing) SC-F019
Cost on the journal scored (Missing) SC-F017
Reason code per count line + store-level "reason required" rule scored (Missing) SC-F018
Copy a count per counter scored (Missing) SC-F024
Count history scored (Partial) SC-F027
Item.LastCounted scored (Missing) SC-F023
HQ/Central Worksheet 501 + Task 190 reconciliation + discrepancy report N/A SC-N01
Worksheet 307 push-back to stores N/A SC-N01
RMS/RMH — advanced (mostly RMH)
Merge of multiple counts scored (Missing) SC-F035
Recount / second pass scored (Missing) SC-F020
Discrepancy-only filter + highlighting scored (Missing) SC-F025
Variance valued by cost (explicit in RMH) scored (Missing) SC-F017
Configurable sheet layout + Detailed/Summary reports scored (Missing) SC-F036
CSV export for handhelds folded covered by SC-F015 (import/export of counted quantities)
Import merge modes + error preview folded covered by SC-F015
Exclude-inactive option (RMH 3.11.16) folded covered by SC-F012 (scope generator); QIIUB's inverse behavior — deliberately including discontinued products — is credited in SC-F008
Auto-reconciliation of movement while the count is open (RMH 3.9.19) scored (Missing) SC-F022 — the line Adjustments parks at adjustments-scorecard.md:328
Central item-management lockdown during Worksheet 501 N/A SC-N03
Named count role permission (RMH) folded covered by SC-F026 (role gating); QIIUB's RBAC is coarser, noted there and in §7
RMS/RMH — absent in both (so not a parity gap; market-driven only)
Enforced blind count scored (Missing) SC-F021 — sourced market, explicitly noted as absent from RMS/RMH
Per-line who-counted attribution scored (Missing) SC-F024 (team counting) + SC-F019 (header attribution)
Variance tolerance / approval routing scored (Missing) SC-F030
Scheduled / recurring cycle counts scored (Missing) SC-F028
Sales freeze during a count scored (Missing) SC-F031
Serial / lot / expiry-aware counting scored (Missing) SC-F034
Bin/aisle as a scope criterion folded covered by SC-F033 (bin granularity) + SC-F012 (scope)
Count sheets distributed from HQ/Central N/A SC-N01
Count documents replicated / consolidated centrally N/A SC-N02
Counting from the POS or a native mobile app scored (Missing) SC-F039 (offline POS capture) + SC-F016 (scan)
WIDS365
No physical/cycle count entity at all (18 evidence-backed negatives) Contributes no capability line to the denominator; the negative result is stated in §3 and is why WIDS365 is a wider subset here than in the previous five modules
Counting done via generic AdjustmentHeader/AdjustmentDetail, signed delta, operator computes variance dep SC-D01 — QIIUB's equivalent is the auto-generated adjustment, scored SC-F006 (strictly ahead of WIDS365 here)
Structured bulk ingest web_AdjustmentDetailUpsertByJson (MERGE-idempotent, ItemID-or-ItemCode, per-row errors, clipboard + .txt/.csv/.tsv) scored (Missing) SC-F015 — named as the recommended model
Temporal system-versioned audit on header and detail scored (Partial) SC-F027; QIIUB uses the movement ledger + RowVersion instead of temporal tables (ADR-0023)
RecordDeletedLog deletion audit with full payload folded covered by SC-F027; QIIUB has no count-delete path at all (SC-F011), so there is nothing to audit yet
Granular per-action permissions (Approve declared but unimplemented) folded covered by SC-F026; the unimplemented Approve corroborates SC-F030 being rare below ERP
AnalyticsItemStockCount dashboard tile N/A SC-N04 — name collision, an on-hand SUM, not a count feature
Market — table-stakes tier
Open a count / stocktake document scored SC-F001
Capture counted quantity scored SC-F002
Automatic variance calculation scored SC-F004
Post to on-hand (adjustment record / movement ledger / direct set) scored SC-F005 + SC-F006
Partial / cycle count vs full stocktake scored (Missing for full) SC-F013; the cycle shape is SC-F001
Count scope filters (category / brand / supplier / tag / location) scored (Missing) SC-F012
Mobile app / barcode-scanner count entry scored (Missing) SC-F016
Count-sheet print / PDF scored (Missing) SC-F014
CSV import / export of counted quantities scored (Missing) SC-F015
Variance valuation (cost impact / shrinkage $) scored (Missing) SC-F017
Count history / audit trail scored (Partial) SC-F027
Who-counted attribution scored (Missing) SC-F019
Recount / second pass scored (Missing) SC-F020
Blind vs informed counting scored (Missing) SC-F021
Reconciling sales that happen mid-count scored (Missing) SC-F022
Multi-user simultaneous counting scored (Missing) SC-F024
Reason codes on variance scored (Missing) SC-F018
Count-progress tracking (% counted) scored (Missing) SC-F037
"Uncounted items → zero or leave alone" policy scored (Missing) SC-F013
Role / permission gating scored SC-F026
Multi-location rollups folded covered by SC-F012 scope + SC-D05 reporting; QIIUB's single database makes cross-location querying native, so there is no rollup mechanism to miss
API access to counts scored SC-F001/F005/F008/F009 — the API is QIIUB's only surface (§5)
Offline counting scored (Missing) SC-F039
Market — advanced / ERP tier
Scheduled / recurring cycle counts + count schedules scored (Missing) SC-F028
ABC-class / last-counted-driven selection scored (Missing) SC-F029
Variance approval workflow + tolerance thresholds scored (Missing) SC-F030
Freezing transactions during a count scored (Missing) SC-F031
GL / journal posting of the write-off scored (Missing) SC-F032
Serial / lot / expiry-aware counting scored (Missing) SC-F034
Bin-level counting scored (Missing) SC-F033
Count templates / persisted count plan (NetSuite) folded covered by SC-F012 (a saved scope is a template)
Reverse / unpost a posted count scored (Missing) SC-F038
Vendor / family / group / subsidiary / department scope (NetSuite) folded covered by SC-F012
Include zero-quantity rows in the count (NetSuite) folded covered by SC-F012; QIIUB's GetValueOrDefault(variantId, 0) (StartStockCountEndpoint.cs:128) already tolerates a never-stocked variant, credited in SC-F003
Bin-order / count-sequence sorting of the sheet (NetSuite) folded covered by SC-F033 (bin granularity) + SC-F014 (sheet)
Inventory-status-aware counting (NetSuite) folded covered by SC-F034 (serial/lot/status-aware counting)
Recalculate Snapshot after movement (NetSuite) folded covered by SC-F022 — NetSuite's answer to the exact drift problem QIIUB leaves unguarded
Item locked from a second concurrent count (NetSuite) folded covered by SC-F031 (freeze/lock); QIIUB has no lock of any kind
Location locked / trading stopped during stocktake (Cin7) folded covered by SC-F031; noted in §3 as unavailable to a POS
Multi-UoM per counted line (NetSuite); RMH purchase/stocking unit split scored (Missing) SC-F040 — reclassified from a Units-of-Measure dependency by the adversarial pass, per the PO-F020 in-module precedent
Negative on-hand permitted at count (Lightspeed R) folded covered by the §5b hardening bundle — QIIUB's inverse gap is that it accepts a negative counted quantity
"Zero All" / "Zero out missed" bulk actions (Stocky) folded covered by SC-F013 (full-count mode)
Activity log with user + timestamp (Cin7) scored (Partial/Missing) SC-F027 (trail) + SC-F019 (who-counted)
Variance Financials tab + GL account (Cin7) scored (Missing) SC-F017 (valued variance) + SC-F032 (GL posting)
Clover core ships no stocktake (count/audit belongs to Clover Sport, a separate product) Contributes no capability line; recorded because it sets the POS-tier floor and is why "nothing is universal across the POS tier" (§3)
Stocky retirement 2026-08-31 shrinking the Shopify baseline Dated market fact, not a capability; recorded in §3 and the Appendix so a future re-score does not benchmark against a withdrawn product
QIIUB-original sweep
Offline-first count capture on the terminal (offline-function-matrix.md:80, 21-pos-client.md:79) scored (Missing) SC-F039 — and the docs contradict the code (§8-C4/C5)
Auto-generated correction document from variance (StockCount.cs:8-9; inventory-supply-chain.md:496) scored SC-F006 — QIIUB does this automatically where WIDS365 requires hand-computed deltas
Movement ledger typed Count, distinct from manual Adjustment (ADR-0023 document-trail tier) scored SC-F005
Portal counts UI specified by ADR-0081 :47-48 Surface, not capability — §5/§7 #1 per the framework and the Distribution precedent; deliberately not a taxonomy row so the score stays comparable across modules
AI-assisted count (variance anomaly detection) N/A Not in any ADR, design doc, or the agents spec (ADRs 0072-0079) for this module — no source claims it, so it does not enter the denominator
Single cloud database (no HQ↔store count replication) N/A SC-N01/N02

Folds added by the adversarial pass (guard (e) sweep — the pass found 18 dropped source lines: 8 RMS/RMH, 4 ERP, 2 market and 4 WIDS365 positives. None moves the score, but an unmapped line silently shrinks the denominator, so each is reconciled below. They resolve into 16 rows, not 18: NetSuite's tally-scanning and GS1/UPC-parsing lines share one row, and the WIDS365 date-range list filter has no row here because it is already reconciled inside SC-F009, which names it as one of that row's missing pieces.):

Source inventory line Disposition Where
Offline-inventory quantities reconciled inside the count (QuantityToOffline / QuantityFromOffline, InventoryOffline) folded covered by SC-F022 (movement during the count); the hold-bin itself is Adjustments' (ADJ-F027), not this module's
Multiple counts open concurrently at one location (both RMS and RMH) folded covered by SC-F001 — QIIUB has this (nothing constrains concurrent InProgress counts). Deliberately folded with a note rather than given its own Implemented row: crediting an unconstrained absence as a feature would be the flattering read, and NetSuite treats the opposite (locking an item from a second count) as the capability — see SC-F031
Bulk "Set Date" across count lines (RMS) folded covered by SC-F011 (no line revision of any kind)
Unsaved-changes prompt (RMH 3.11.1) folded UI behavior — §5 surface gap, not a capability
Guided variance-investigation checklist folded covered by SC-F025 (variance review); a documentation practice rather than a feature
Zone map / aisle assignment folded covered by SC-F033 (bin granularity) + SC-F012 (scope)
Effective-dated scheduling of the reconciliation worksheet folded covered by SC-N01 — it schedules the HQ/Central worksheet, already N/A
Matrix / kit-specific count behavior N/A SC-N05
Annual full-count date configuration (NetSuite) folded covered by SC-F028 (scheduling)
Count / row user assignment (NetSuite, Odoo) folded covered by SC-F019 (who-counted) + SC-F024 (team counting)
Tally scanning + GS1/UPC parsing (NetSuite) folded covered by SC-F016 (scan capture)
Paid-tier feature gating (Square Inventory Plus) N/A Commercial packaging, not a capability — QIIUB's module/feature-flag system is a separate concern (CentralDB ModuleDefinition)
Informed counting = present (the other half of the blind/informed pair) folded covered by SC-F008 — QIIUB has informed counting; only the blind half (SC-F021) is missing, so the pair must not read as wholly absent
Header quantity / valuation totals (WIDS365) folded covered by SC-F009 (the missing list/detail aggregates) + SC-F017 (valuation)
TransferID link from an adjustment to its source document (WIDS365) dep SC-D01 — QIIUB's equivalent traceability is on the movement (ReferenceType/ReferenceId), credited in SC-F027
PDF of the finished count document (WIDS365) folded covered by SC-F014 (print / export)

Every line item in all four source families now maps to a scored ID, a named fold, an N/A, or a dependency. The first synthesis claimed this prematurely; the claim holds only as of the adversarial pass.


Appendix — reproducibility checklist for the next session