measured at 56d2d327
Created: 2026-07-27 | Last Updated: 2026-07-27
Fifth filled instance of
module-scorecard-template.md(after PO, Receivings, Transfers, Distribution) — the last member of the inventory-movement family. Read-only analysis; recommendations only. Evidence isfile:line/ endpoint againstD:\BCPOSGit\qiiubon branchmainat commit56d2d327. Gathering source-of-record:artifact-adjustments-gathering-{repo,rmhrms,wids365,market}.md. Scored with all four source families (RMS/RMH + WIDS365 + modern market + QIIUB-original).
| Field | Value |
|---|---|
| Module name | Adjustments (Inventory Adjustments) |
| File | adjustments-scorecard.md (docs/modules/adjustments-scorecard.md) |
| Author / session | Claude (Opus) — module #5 |
| Created | 2026-07-27 |
| Last-Updated | 2026-07-27 |
| Repo commit (SHA) analyzed | 56d2d327 |
| Governing ADRs | 0081 (Inventory Hub IA), 0082 (PublicId/TypeID), 0083 (posted-document), 0023 (three-tier history), 0020 (LocationProduct not StockLevel), 0004 (bigint PK), 0062 (user attribution), 0067 (tax-holiday resolver) — evidence only; this scorecard creates none |
| Governing design docs | docs/design/inventory-supply-chain.md §4.5 / §5.2 |
| Status of this analysis | Adversarially-verified — Fable pass moved 76→63 (findings + resolutions in artifact-adjustments-fable-review.md); live-confirmed against the running app 2026-07-28 (§5c) |
Last-Updated= the measurement date, pinned tocommit:— bumped only on a re-score (.claude/rules/docs.mdcarve-out).
InventoryMovement ledger and updates LocationProduct.QuantityOnHand. It is the manual counterpart to the automatic stock changes produced by receiving, transfers, and stock counts.core (every retailer adjusts stock). Rx — core (pharmacies write off expired/damaged product; the reason-coded trail matters for controlled-substance shrinkage). Build — used (materials shrinkage/damage).SystemAdmin / Admin at the API (Roles("SystemAdmin","Admin")); the portal detail page also lets Manager transition (AdjustmentDetailPage.tsx:63). Surface: qiiub-portal (merchant back-office) + REST API. No POS or qiiub-admin surface — adjustments are a back-office operation, not a till action.InventoryMovement ledger + LocationProduct stock row (Inventory core), and shrinkage/variance reporting (Reports). All are marked Dependency in §3.| Module | Direction | What crosses the boundary | Scoring treatment |
|---|---|---|---|
| Reason Codes (cross-cutting reference data) | shared-infra | The ReasonCode catalog Adjustments requires on every header |
dependency — out of scope (ADJ-D01) |
| Stock Counts | upstream | Count finalize auto-creates a Completed adjustment from variance (reason CYCLE-COUNT) |
dependency (ADJ-D02) |
| Transfers | upstream | Damage-on-receive auto-creates a Completed adjustment (reason ADJ-XFR-DAMAGE) |
dependency (ADJ-D03) |
| Receivings | shared pattern | Receipt quantity/cost correction is a Receivings concern, not an adjustment | dependency (ADJ-D04) |
| Inventory core | shared-infra | InventoryMovement ledger + LocationProduct.QuantityOnHand that Complete writes |
dependency (ADJ-D05) |
| Reports | downstream | Shrinkage / variance-by-reason reporting | dependency (ADJ-D06) |
| Document sequencing | shared-infra | IDocumentSequenceService mints ADJ-###### |
dependency (ADJ-D07); the use (documents carry a number) is scored ADJ-F007 |
Boundary statement. Adjustments owns the InventoryAdjustment + InventoryAdjustmentLine tables, all 10 endpoints under Api/Features/Adjustments/, the Draft → Completed | Cancelled state machine, and the manual act of posting a reason-coded quantity change. It does not own the ReasonCode catalog, the InventoryMovement/LocationProduct tables it writes, or the two neighbor flows that create adjustment documents automatically (Stock Counts, Transfers). Those neighbors physically construct an InventoryAdjustment, but the semantic capability (cycle-count variance; transfer damage) belongs to them — single-owner rule.
Boundary-leak checklist.
FinalizeStockCountEndpoint.cs:118-237). Movements typed Count, not Adjustment, so the ledger distinguishes them.TransferDamageAdjustmentService.cs).Single-owner rule. Verified: none of the 27 scored features here is also scored in PO / Receivings / Transfers / Distribution / Stock Counts. The three auto-adjustment writers are Dependency rows, not scored.
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 Adjustment document) + modern inventory tooling (Square/Lightspeed/Shopify+Stocky/Clover/NetSuite/Cin7/Odoo) + QIIUB-original. A
Missingmeans "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.
Market shape (from the 4-source scan). Inventory adjustments split cleanly into two tiers: a table-stakes POS tier (manual single + reason, lifecycle, history, list) that every system has, and an ERP tier (value/cost revaluation, GL posting, back-date, reversal, approval) that pure POS systems omit. QIIUB has the full table-stakes tier and none of the ERP tier — which is exactly the T1-complete / T3-sparse shape below. Notably, adjustment approval workflow is rare even in ERP (NetSuite has no native approval), so building it would meet-or-beat the market.
| ID | Name | Description | Source | Status | Evidence | Notes |
|---|---|---|---|---|---|---|
| ADJ-F001 | Create adjustment (Draft) | Draft header (one location + reason) + optional signed lines | RMS+RMH+WIDS365+market | Implemented | CreateAdjustment/CreateAdjustmentEndpoint.cs:103 |
Resolves Location/ReasonCode/variant PublicIds; snapshots cost/price; mints ADJ-###### |
| ADJ-F002 | Signed quantity change per line | Per-line QuantityChange (+ increase / − decrease) |
RMS+RMH+WIDS365+market | Implemented | InventoryAdjustmentLine.cs:16; CreateAdjustmentEndpoint.cs:65-67 (NotEqual 0) |
Signed delta, not a target count — same model as WIDS365 |
| ADJ-F003 | Reason-coded (required) | Every adjustment carries a reason from the catalog | RMS+RMH+WIDS365+market | Implemented | InventoryAdjustment.cs:17 (non-null); CreateAdjustmentEndpoint.cs:118-125 |
Required by default — stronger than RMS/RMH (there it is config-driven "recommended") and matches WIDS365 (header-required) |
| ADJ-F004 | Complete → post to inventory | Complete writes InventoryMovement + upserts LocationProduct.QuantityOnHand, one transaction |
RMS+RMH+WIDS365+market | Implemented | CompleteAdjustmentEndpoint.cs:77,146-205,208-209 |
The core purpose. Atomic flip + per-line movement + QoH upsert + concurrency retry (#964) |
| ADJ-F005 | Line management | Add / update / remove / atomic bulk-remove lines on a Draft | RMS+RMH+WIDS365+market | Implemented | AddLine :60, UpdateLine :62, RemoveLine :30, RemoveLines batch #799 |
ProductVariant immutable on a line (remove+re-add); LineNumber auto max+1 |
| ADJ-F006 | Status lifecycle | Draft → Completed | Cancelled; only Draft is mutable |
RMS+RMH+WIDS365+market | Implemented | AdjustmentStatus.cs; 409 guards in 5 endpoints; atomic flips |
Both terminal states are atomic WHERE Status=Draft flips (no lost-update). Maps to WIDS365 Open→Closed / RMS Commit |
| ADJ-F007 | Document numbering | Human ADJ-###### per merchant |
RMS+RMH+WIDS365 | Implemented | CreateAdjustmentEndpoint.cs:156 via IDocumentSequenceService |
Unique (MerchantId, DocumentNumber). Sequence service = shared-infra (ADJ-D07) |
| ADJ-F008 | View adjustment detail | Full header + lines + names + attribution | RMS+RMH+WIDS365+market | Implemented | GetAdjustmentEndpoint.cs:84 |
Resolves Location/ReasonCode/Employee names + CreatedBy/UpdatedBy from CentralDb |
| ADJ-F009 | List / filter / sort / search | Paged list; filter location/status/reason/search; 7 sorts; Draft/Completed/Cancelled breakdown | RMS+RMH+WIDS365+market | Implemented | ListAdjustmentsEndpoint.cs:117; breakdown :147-156 (#633) |
Breakdown spans the full filtered set in one grouped round-trip |
| ID | Name | Description | Source | Status | Evidence | Notes |
|---|---|---|---|---|---|---|
| ADJ-F010 | Bulk / import / scan entry | Add many lines fast — CSV/clipboard import or barcode-scan, not one-by-one | WIDS365+RMS+market | Missing | Natural home: a bulk-add endpoint over InventoryAdjustmentLine + a portal import/scan surface |
QIIUB adds lines one at a time (bulk remove exists, #799; bulk add does not). WIDS365 has batch-JSON + clipboard/CSV; RMS Inventory Wizard 150; market has CSV + scan + mobile |
| ADJ-F011 | Role / permission gating | Only authorized roles can create/complete adjustments | RMS+RMH+market | Implemented | Roles("SystemAdmin","Admin") on every endpoint; portal canTransition = Admin||Manager (AdjustmentDetailPage.tsx:63) |
Coarse RBAC (no per-location or per-reason grant) but the market-standard control is present |
| ADJ-F012 | Reason categorization | Preset reason set (damage / shrink / theft / found / recount) | RMS+RMH+market | Partial | ReasonType='Adjustment' scopes the picker (ListReasonCodesEndpoint.cs:25; portal AdjustmentsListPage.tsx:70-74) |
What works: merchant-authored reasons scoped by ReasonType. Missing: no enforced category enum (shrinkage/theft/found are free-text, same as RMS/RMH). NB the ADJ-DAMAGE/ADJ-SHRINK codes ship only as DevSeed fixtures, not product presets |
| ADJ-F013 | Adjustment history / audit | Who/when + an append-only movement trail per adjustment | RMS+RMH+WIDS365+market | Implemented | InventoryMovement (append-only, ReferenceType=InventoryAdjustment); CreatedBy/UpdatedBy (BaseEntity.cs:14-15) surfaced in Get/List |
Ledger + user attribution = the ADR-0023 document-trail tier. (WIDS365 additionally uses temporal tables; QIIUB deliberately does not on the movement ledger — ADR-0023.) |
| ADJ-F014 | Operator attribution | Record the specific employee (or completing user) on the adjustment | RMS+RMH+WIDS365+market | Missing | EmployeeId/ClosedByEmployeeId modeled + surfaced but never written (InventoryAdjustment.cs:16,22) |
Reserved schema, no writer → Missing (status def). Aggravated: Complete/Cancel flip via ExecuteUpdateAsync, which bypasses the audit interceptor, so not even UpdatedByUserId is stamped on the transition (the base attribution credited to F013 covers create/edit only). See §8 |
| ADJ-F023 | Barcode-scan / mobile entry | Add adjustment lines by scanning, or from a mobile device | market | Missing | Line entry is manual desktop only (AdjustmentLineEntryBar.tsx); natural home: a scan handler + mobile surface |
Market table-stakes; unfolded from ADJ-F010 so the import/scan/mobile lines map 1:1 like the rest of the table |
| ADJ-F024 | Live on-hand at line entry | Show the item's current QuantityOnHand while entering the line | WIDS365+market | Missing | Entry bar takes qty blind; natural home: resolve LocationProduct.QuantityOnHand into the line-entry response |
WIDS365 surfaces available-on-hand at entry — lets the operator adjust to a known figure |
| ID | Name | Description | Source | Status | Evidence | Notes |
|---|---|---|---|---|---|---|
| ADJ-F015 | Value / cost revaluation | Adjust on-hand cost/value (revalue), not just quantity | market | Missing | Lines are quantity-only; LocationProduct.Cost never re-weighted at Complete (CompleteAdjustmentEndpoint.cs:180-183) |
ERP tier — Cin7 explicit revaluation, NetSuite worksheet, Odoo. Not in Square/Clover/Lightspeed/WIDS365 |
| ADJ-F016 | Reversal / undo | One-click reverse of a Completed adjustment (auto-offset) | market | Missing | No endpoint; Completed is terminal. Natural home: a "reverse" endpoint that mints an offsetting Draft | Today the operator manually creates an offsetting adjustment. Some ERP/mid-market offer a void/reverse |
| ADJ-F017 | Approval / threshold gating | Require sign-off (or a dollar threshold) before Complete | market | Missing | Complete open to any Admin/SystemAdmin; no threshold. Natural home: a status/approval gate on the transition | Rare even in ERP (NetSuite has no native approval) — building it would meet-or-beat the market |
| ADJ-F018 | Mandatory-note enforcement | Force a free-text note when the reason demands it | market+QIIUB-original | Missing | ReasonCode.RequiresNote exists (ReasonCode.cs:13) but no adjustment endpoint enforces it |
Reserved flag, unwired → Missing. Cheap quick-win over existing schema |
| ADJ-F019 | Attachment / photo evidence | Attach a photo/document to a damage/shrinkage write-off | market | Missing | Not in entity or endpoints. Natural home: a generic attachment table keyed by (ReferenceType, ReferenceId) |
Compliance-grade write-off evidence; present in some mid-market inventory apps |
| ADJ-F020 | GL / accounting posting | Post a journal entry (shrinkage expense / COGS) per adjustment | market | Missing | No accounting integration on adjustments | ERP tier (NetSuite/Cin7). RMS/RMH also lack per-adjustment GL (batch close-out export only) — QIIUB is at parity with the reference systems here, behind only ERP |
| ADJ-F021 | Back-date / effective date | Set an adjustment's effective date for period accounting | market+RMS | Missing | AdjustmentDate = UtcNow at create, not user-settable (CreateAdjustmentEndpoint.cs) |
ERP period-close nicety |
| ADJ-F022 | Discontinued-item write-off | Adjust/write-off a product that is inactive/discontinued | RMS+market | Implemented | Adjustment line variant resolution never joins the productActiveStatus filter (it sits on Product), so a killed SKU can still be lined; GetAdjustmentEndpoint.cs:98-99 keeps it visible in detail |
You can clear the last units of a discontinued SKU |
| ADJ-F025 | Printable adjustment document | Print / export the adjustment as a document | RMS+WIDS365 | Missing | No print/PDF surface (repo-wide ADR-0070 print gap); natural home: the ADR-0070 print pipeline | Internal doc — lower need than a PO/transfer slip, hence T3 |
| ADJ-F026 | Header valuation total | Show the adjustment's total quantity / value magnitude | WIDS365 | Missing | No aggregate on the header; natural home: a derived total in the Get/List projection | WIDS365 surfaces valuation totals on the header |
| ADJ-F027 | Damage quarantine / hold bin | Hold damaged stock in a non-sellable state, optionally return-to-main | RMS+RMH | Missing | QIIUB models damage as a destructive reason-coded write-off only; natural home: a location/stock-state flag | RMS/RMH InventoryOffline offers hold + return-to-main; QIIUB meets only the dispose half (reclassified from N/A after the Fable pass) |
Dependencies (excluded from score — listed so the boundary is visible):
| ID | Capability | Owner | Note |
|---|---|---|---|
| ADJ-D01 | ReasonCode catalog (CRUD, merchant-scoped) | Reason Codes (cross-cutting) | Features/ReasonCodes/**; shared with Return/Void/Discount/PriceOverride/AR (ReasonCode.cs:5-6) |
| ADJ-D02 | Cycle-count variance → auto-adjustment | Stock Counts | FinalizeStockCountEndpoint.cs:118-237 (movement Count, reason CYCLE-COUNT) |
| ADJ-D03 | Damage-on-receive write-off | Transfers | TransferDamageAdjustmentService.cs (reason ADJ-XFR-DAMAGE) |
| ADJ-D04 | Receiving quantity/cost correction | Receivings | Receipt Complete/Cancel posts its own Receipt movement |
| ADJ-D05 | InventoryMovement ledger + LocationProduct stock | Inventory core | InventoryMovement.cs; the stock row Complete upserts |
| ADJ-D06 | Shrinkage / variance-by-reason reporting | Reports | Adjustments supplies the filterable list (ADJ-F009); the aggregate $ report is Reports' |
| ADJ-D07 | Document sequence service | shared-infra | IDocumentSequenceService; the use is scored ADJ-F007 |
N/A — not applicable to QIIUB (excluded; reason given):
| ID | Capability | Why N/A |
|---|---|---|
| ADJ-N01 | Sync push/pull of the adjustment document (offline POS) | Adjustments are authored web-side (back-office); propagation of the resulting LocationProduct change to offline devices is the Sync module's job, not an adjustment feature. Met-differently-by-architecture. |
| ADJ-N02 | HQ/Central-originated adjustment (RMS Type=8) |
QIIUB runs one cloud database; a merchant-wide user adjusts any location directly, so there is no HQ/Central-vs-store adjustment split. Same call as the PO-Planner / Distribution HQ N/As. |
InventoryAdjustment (aggregate root) — src/QIIUB.Domain/Merchants/InventoryAdjustment.cs
BaseEntity (soft-deletable IsDeleted). Note: a Completed adjustment is logically immutable but is enforced only by status guards, not by BaseImmutableEntity — flagged in §8.PublicId adj_… (:13), DocumentNumber varchar(20) (:14), LocationId (:15), EmployeeId? reserved/unwired (:16), ReasonCodeId non-null (:17), Status tinyint (:18), AdjustmentDate datetime2(3) (:19), Notes nvarchar(500)? (:20), ClosedDate datetime2(3)? (:21), ClosedByEmployeeId? reserved/unwired (:22). Base columns per BaseEntity.cs:8-16.(MerchantId, Id); FK→Location + FK→ReasonCode both composite MerchantId-first, DeleteBehavior.Restrict; unique UQ_InventoryAdjustment_MerchantId_DocumentNumber; indexes on LocationId, ReasonCodeId.EmployeeId, ClosedByEmployeeId (→ ADJ-F014 Missing).InventoryAdjustmentLine (editable child) — InventoryAdjustmentLine.cs
PublicId adln_… (:12), InventoryAdjustmentId (:13), LineNumber (:14), ProductVariantId (:15), QuantityChange decimal(10,4) signed (:16), UnitCost decimal(18,4) snapshot (:17), UnitPrice decimal(18,2) snapshot (:18), Notes nvarchar(200)? (:19).UX_InventoryAdjustmentLine_MerchantId_InventoryAdjustmentId_PublicId (ADR-0082 §4.2.1 nested-route / route-forgery defense).Referenced (not owned): ReasonCode (→ Reason Codes doc), InventoryMovement + LocationProduct (→ Inventory core). MerchantId-first composite FKs verified throughout (Critical Rule #1).
| Surface | Verb + Route | Impl (file:line) | Realizes | Roles | Notes |
|---|---|---|---|---|---|
| API | POST /adjustments |
CreateAdjustmentEndpoint.cs:103 |
F001–F003, F007 | SysAdmin/Admin | Draft + optional lines; snapshots cost/price |
| API | POST /adjustments/{id}/lines |
AddAdjustmentLineEndpoint.cs:60 |
F005 | SysAdmin/Admin | 409 if not Draft |
| API | PUT /adjustments/{aid}/lines/{id} |
UpdateAdjustmentLineEndpoint.cs:62 |
F005 | SysAdmin/Admin | qty+notes; route-forgery check |
| API | DELETE /adjustments/{aid}/lines/{id} |
RemoveAdjustmentLineEndpoint.cs:30 |
F005 | SysAdmin/Admin | hard delete |
| API | POST /adjustments/{aid}/lines/batch-delete |
RemoveAdjustmentLinesEndpoint.cs |
F005 | SysAdmin/Admin | atomic bulk-remove, ≤100 (#799) |
| API | PUT /adjustments/{id} |
UpdateAdjustmentEndpoint.cs |
F003 | SysAdmin/Admin | header only (reason+notes); Location locked |
| API | POST /adjustments/{id}/complete |
CompleteAdjustmentEndpoint.cs:77 |
F004, F006 | SysAdmin/Admin | the posting op; 400 if 0 lines |
| API | POST /adjustments/{id}/cancel |
CancelAdjustmentEndpoint.cs |
F006 | SysAdmin/Admin | no inventory effect |
| API | GET /adjustments/{id} |
GetAdjustmentEndpoint.cs:84 |
F008, F022 | SysAdmin/Admin | discontinued-item safe |
| API | GET /adjustments |
ListAdjustmentsEndpoint.cs:117 |
F009 | SysAdmin/Admin | filter/sort/search + breakdown |
| portal | Inventory → Adjustments (list + detail + drawer + line bar + complete/cancel modals) | apps/qiiub-portal/src/features/adjustments/** |
F001,F004–F009,F011 | Admin/Manager | backend + UI in lockstep |
No over-build (every endpoint maps to a feature). UI gap: the header-edit endpoint (UpdateAdjustment) exists + useUpdateAdjustment is wired, but the detail page does not surface header edit (its comment "there is no Update endpoint" is stale — §8). Endpoint count (10) is size context, not a metric.
QuantityChange NotEqual 0; max-lengths matching DB columns (Notes 500/200); reason Code uppercased + resolved; batch-delete distinct + ≤100..Local-first LP guard (SQL-2601 / M-49).Absent — business validations (verified not present):
| Missing validation | Effect today | Tracked as |
|---|---|---|
RequiresNote enforcement |
a reason that "requires" a note can be used with none | ADJ-F018 |
| Approval / dollar-threshold on Complete | any Admin completes any adjustment, no ceiling | ADJ-F017 |
| Negative-on-hand guard | a decrease can drive QuantityOnHand below 0 unremarked |
§7 hardening bundle |
Score says "how complete vs a mature POS"; this says "how trustworthy is the built part." Testedness never enters §6.
tests/QIIUB.Tests.Integration/Features/Inventory/AdjustmentAndCountTests.cs + Features/Adjustments/ListAdjustmentsAggregatesTests.cs) · 0 unit · 0 e2e · 1 portal Vitest (AdjustmentsListPage.test.tsx). Counts are context.56d2d327), DB restored to zero residue afterward: Create Draft → Complete posted InventoryMovement (MovementType.Adjustment, −5) + upserted LocationProduct.QuantityOnHand 50→45 in one transaction (ADJ-000002, movementsCreated:1); a second Draft → Cancel wrote no movement and left QoH unchanged (ADJ-000003). Confirms ADJ-F001–F007 end-to-end. Test rows + the DocumentSequence.NextNumber counter fully reverted.| Flow | Tested |
|---|---|
| Create + Complete → LocationProduct updated (80−5=75) | ✓ (:216-245) |
| Two lines same variant, no existing LP → aggregate (SQL-2601 regression) + new LP Price NULL | ✓ (:247-307) |
| Complete of a 0-line draft → 400 | ✗ (not named) |
| Cancel: flips + no movements + already-Completed/Cancelled → 409 | ✓ (:875,:900,:925,:944) |
| List filter/sort/search + status breakdown | ✓ (:309–827) |
| Cross-merchant isolation (merchant B → 404) | ✓ (:696-712) |
| Get includes attribution | ✓ (:849) |
| AddLine / UpdateLine / RemoveLine happy+sad (direct) | ✗ (exercised only via create-with-lines) |
RequiresNote behavior |
✗ (feature not built) |
Named test gaps → one §7 "test hardening" row: direct line-management sad paths; explicit 0-line-Complete 400.
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) | 9 | 0 | 0 | — | 9 | 9.0 | 100.0 |
| T2 (×2) | 2 | 1 | 4 | — | 7 | 2.5 | 35.7 |
| T3 (×1) | 1 | 0 | 10 | — | 11 | 1.0 | 9.1 |
| Excluded | 7 Dep + 2 N/A |
overall% = 100 × ( 3×9.0 + 2×2.5 + 1×1.0 ) / ( 3×9 + 2×7 + 1×11 )
= 100 × ( 27 + 5 + 1 ) / ( 27 + 14 + 11 )
= 100 × 33 / 52 = 63.5% → 63%
Overall = 63% (T1 100 / T2 36 / T3 9). 27 scored features + 7 Dependency + 2 N/A = 36 mapped IDs. This is the post-Fable number — the first synthesis read 76%; the adversarial pass corrected the F014 double-count, three unreconciled source lines (F024/F026 + F023 unfold), and the over-generous damage-bin N/A (→ F027), dropping it to 63%.
Flat-Partial caveat: the one remaining T2 Partial (F012 reason categorization) is "core-need-met, refinement-missing" — not thin. Anti-gaming guards: (a) tiers by market necessity — the T3 sparsity mirrors the market's own ERP-only set; (b) no double-count — the Fable pass caught one: F014 had borrowed the base CreatedBy/UpdatedBy infra already credited to F013, and is now Missing (reserved EmployeeId alone can't earn Partial); bulk-remove is folded into F005, bulk-add is the separate gap F010; F007's sequence service is a dep, only its use is scored; (c) no Dependency/N/A hides an in-module gap — the damage-bin was reclassified N/A→Missing (F027); the two remaining N/As are architecture/scope; the three auto-adjustment writers genuinely belong to neighbors; (d) every Implemented has file:line, every Partial names what's missing; (e) §9 reconciles all four source families (fixed after the pass found three dropped lines); (f) single-owner verified vs the four movement-family siblings + Stock Counts.
Reproducibility honesty. The denominator (36 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 — third. Adjustments' entire table-stakes tier is built (T1 100), but the T2/T3 breadth (bulk/scan entry, live-on-hand, operator attribution, reversal, revaluation, approval) is where a mature POS pulls ahead.
| Rank | Gap (ID) | Tier | Impact | Recommendation | ADR? |
|---|---|---|---|---|---|
| 1 | ADJ-F014 operator attribution | T2 | No POS-employee accountability on write-offs (loss-prevention gap) | Quick win — write EmployeeId/ClosedByEmployeeId from context on create/complete; fields + response already exist |
No |
| 2 | ADJ-F018 mandatory-note | T2/T3 | "requires note" reasons don't actually require one | Quick win — enforce ReasonCode.RequiresNote in Create/Complete validators |
No |
| 3 | ADJ-F010 bulk / import / scan | T2 | Large write-offs / recounts are slow line-by-line | Add a bulk-add endpoint over InventoryAdjustmentLine + portal import/scan |
No (extends model) |
| 4 | ADJ-F016 reversal / undo | T3 | Fixing a wrong Completed adjustment is a manual offset | A reverse endpoint that mints an offsetting Draft referencing the original |
No (extends model) |
| 5 | ADJ-F017 approval / threshold | T3 | No control on high-value adjustments | Approval gate on the Draft→Completed transition (meets-or-beats market) | Requires ADR (new transition/state) |
| 6 | ADJ-F015 / F020 / F021 value-revaluation / GL / back-date | T3 | ERP-tier accounting not served | Bundle — revisit when an accounting/GL vertical is scoped | Requires ADR |
| 7 | Test hardening | — | Line-management sad paths + 0-line-Complete unguarded by name | Add the §5c ✗ flows | No |
Searched open issues (adjustment / reversal / revaluation / shrinkage / write-off) at measurement: no open issue tracks any of these gaps. #289 PublicId Audit Phase 2 is tangential (touches many modules). All Adjustments gaps are currently untracked — when a dev opens a ticket for one, reference the stable ID (e.g. ADJ-F014) in the issue body so this section can link gap ↔ ticket on the next re-score. Candidates to file first: the two quick wins (ADJ-F014, ADJ-F018).
AdjustmentDetailPage.tsx:53-55 says "There is no Update endpoint" — false; UpdateAdjustment (header) + useUpdateAdjustment exist. The UI just doesn't surface header edit. (Report only.)RemoveAdjustmentLinesEndpoint.cs:37 validator message says invadjln_; useAdjustments.ts:39 says adjln_; the actual prefix is adln (IdPrefixes.cs:82). Behavior correct (uses the constant); comments wrong.EmployeeId/ClosedByEmployeeId, but no endpoint writes them (→ ADJ-F014). Decide whether POS-employee attribution is in scope for the web back-office flow before wiring.Manager trigger Complete/Cancel (AdjustmentDetailPage.tsx:63, canTransition = Admin || Manager), but those API endpoints are restricted to SystemAdmin/Admin — a Manager who clicks Complete gets a 403. Report only (fix belongs to a PR, not this scorecard).ExecuteUpdateAsync, which bypasses the EF audit interceptor, so UpdatedByUserId is not written on the transition and Get surfaces a stale value. Feeds ADJ-F014 (Missing).BaseEntity, not BaseImmutableEntity; immutability rests on status guards only. Decision for a human: is that acceptable, or should Completed adjustments be type-immutable?Every capability in the 4-source inventory (artifact-adjustments-gathering-{rmhrms,wids365,market}.md) + the QIIUB-original sweep mapped to a disposition. scored = own §3 row; folded = subsumed; N/A = out of scope; dep = neighbor.
| Source inventory line | Disposition | Where |
|---|---|---|
| Manual single-item quantity adjustment | scored | ADJ-F001/F002 |
| Signed delta vs target count | scored | ADJ-F002 |
| Reason code required on adjustment | scored | ADJ-F003 |
| Post to on-hand + movement ledger | scored | ADJ-F004 |
| Add/edit/remove lines | scored | ADJ-F005 |
| Open→Closed / Draft→Commit lifecycle (WIDS365/RMS) | scored | ADJ-F006 |
| Document number (WIDS365 AdjustmentHeader, RMS) | scored | ADJ-F007 |
| View / list adjustments | scored | ADJ-F008/F009 |
| Batch/JSON + clipboard/CSV import (WIDS365); Inventory Wizard 150 (RMS) | scored (Missing) | ADJ-F010 |
| Barcode-scan / mobile adjustment entry (market) | scored (Missing) | ADJ-F023 |
| Live available-on-hand shown at line entry (WIDS365) | scored (Missing) | ADJ-F024 |
| Header quantity / valuation totals (WIDS365) | scored (Missing) | ADJ-F026 |
| Printable adjustment document (RMS/WIDS365 DevExpress report) | scored (Missing) | ADJ-F025 |
| Role / permission gating (market table-stakes) | scored | ADJ-F011 |
| Preset reason categories damage/theft/loss/recount (market); RMS/RMH free-text type-2 | scored (Partial) | ADJ-F012 |
| Adjustment history / audit trail; WIDS365 temporal tables | scored | ADJ-F013 |
| Operator/cashier on the adjustment (all) | scored (Missing) | ADJ-F014 |
| Value/cost revaluation (Cin7/NetSuite/Odoo) | scored (Missing) | ADJ-F015 |
| Reversal / void of a posted adjustment | scored (Missing) | ADJ-F016 |
| Approval workflow (rare; NetSuite none) | scored (Missing) | ADJ-F017 |
| Mandatory note per reason | scored (Missing) | ADJ-F018 |
| Attachment / photo evidence | scored (Missing) | ADJ-F019 |
| GL / journal posting per adjustment (ERP) | scored (Missing) | ADJ-F020 |
| Back-date / effective date (ERP/RMS) | scored (Missing) | ADJ-F021 |
| Adjust discontinued/inactive item | scored | ADJ-F022 |
| Reason-code catalog / configuration (RMS Options, WIDS365 LK_AdjustmentReason) | dep | ADJ-D01 |
| Physical/cycle count → variance posting (RMS Calculate→Commit; market count→adjust) | dep | ADJ-D02 |
| Damage-on-receive write-off | dep | ADJ-D03 |
| Damage hold/quarantine bin + return-to-main (RMS/RMH Offline Inventory) | scored (Missing) | ADJ-F027 |
| Receiving cost/qty correction | dep | ADJ-D04 |
| Movement ledger (RMS InventoryTransferLog Type 5–9; WIDS365 InventoryTransferLog) | dep | ADJ-D05 |
| Shrinkage / variance reporting (market shrinkage reports) | dep | ADJ-D06 — contestable: the in-module list gives a status breakdown (F009); a by-reason/$ shrinkage report is assigned to Reports. Scoring it in-module Missing T2 would drop the overall ~3pts. Kept Dependency per single-owner (Reports owns reporting); flagged for a human |
| HQ/Central adjustment (RMS Type=8, Worksheet 307) | N/A | ADJ-N02 |
| Sync of the document to offline POS | N/A | ADJ-N01 |
| Auto-reconcile concurrent movement during count (RMH refinement) | dep | ADJ-D02 (Stock Counts) |
No source line is left unreconciled across the 4 source families (RMS/RMH + WIDS365 + market + QIIUB-original) — the Fable pass added four previously-dropped lines (ADJ-F023/F024/F025/F026) and reclassified the damage bin (ADJ-F027).
inventory-supply-chain.md §4.5; note changes since Last-Updated.Implemented/Partial file:line still resolves.EmployeeId fields get wired, re-score the affected statuses (denominator stays frozen at 36).