Skip to content

The store-grain count keeps a part roll: count and reconciliation quantities are Decimal(12,3) - #35

Merged
goetchstone merged 2 commits into
mainfrom
fix/store-count-decimal
Oct 17, 2026
Merged

goetchstone merged 2 commits into
mainfrom
fix/store-count-decimal

Conversation

@goetchstone

Copy link
Copy Markdown
Owner

Summary

The store-grain count (Inventory › Physical count, the variance report and reconciliation) stays as the check that a POS cutover counted right, and a store's stock includes part rolls. Its quantity columns were Int while the balance it is checked against is Decimal(12,3), so a part roll could not be counted. Exercised first against the Int columns, Prisma truncated silently:

  • a count of 12.5 yards was stored as 12;
  • two scans of 0.1 and 0.2 were stored as 0 and 0, so an accurate count read as 0.3 missing;
  • a count of "lots" reached Prisma as NaN and answered 500.

This is one of the follow-ups left by ledger-plan PR 4 (ROADMAP.md → "Handheld floor"). It does not depend on #34: that PR adds the count by place and leaves these models and routes alone.

What changes

  • Migration 20261021120000_store_count_decimal: PhysicalInventoryCount.quantity and Reconciliation's five expected, counted and variance columns become Decimal(12,3). The widening is in place (integer -> numeric), so existing rows keep their values.
  • lib/inventory/storeCount.ts:
  • parseCount (lib/inventory/qtyInput.ts) reads a typed count, and zero is a count. The count route and the "correct the count" prompt use it instead of Number() and parseInt. A bad count is now a 400 that says what is wrong.
  • Routes moved onto the helpers: variance report, accurate scans, product variance, the on-hand reports by department and by location, summary details, the variance export, scan history, reconciled items, reconcile and the count itself. The count screen's quantity box takes decimals.
  • Runbook corrections (docs/domains/inventory.md), checked against the source:
    • reconcile.ts does not write stock. It only stamps a Reconciliation row, and nothing reads that back into InventoryPosition.
    • The variance reports compare counts with InventorySnapshot, not InventoryPosition.

Trace / Verification

  • Trace (rule 45). No shared infrastructure is touched: nothing under lib/adapters, lib/auth, paymentService, secureUpload or automations. grep -rlnE "\.(physicalInventoryCount|reconciliation)\.(find|create|upsert|update|delete|count|aggregate|groupBy)" app/src finds 16 routes, all under pages/api/inventory. The 11 that read or send a quantity are changed here. The other 5 only delete rows (clear-all-data, clear-location, physical-count/[id], undo-reconciliation) or write a count of 1 (reconcile-unidentified), which a Decimal column takes as it is. The demo seed writes whole-number counts, which a Decimal column takes as they are.
  • Failing first. storeCountFractional.integration.test.ts ran against the Int columns before the change, and all 4 tests failed for the reasons in the summary. All 4 pass after it.
  • Mutation-checked. Each change was reintroduced, its test failed, and the file was restored:
    • removing the rounding from the scan sums;
    • sending scan history without converting it to numbers.
  • Migration. prisma migrate deploy of every migration into an empty scratch database, then prisma migrate diff --from-config-datasource --to-schema --exit-code: "No difference detected".
  • Self-heal sweep (rule 49). I searched the schema for other whole-number quantity columns. BuyerDraftItem.qty and ProposalLineItem.quantity can also receive a part roll. They are not changed here, and are tracked in ROADMAP.md → "Handheld floor". The rest are whole by nature: till counts, units per carton, item counts.
  • Unverified (rule 58). No browser check of the count and variance screens. The routes are exercised through their handlers against real Postgres.

Scan / gate state

npm run check:local stops at its integration step from an agent session: Prisma refuses the harness's db push --accept-data-loss without the owner's consent. Each step was run directly instead.

Check Result
validate (lint, typecheck, format) clean; 0 errors (warnings are the existing ones)
unit tests 3,953 pass (20 skipped), including storeCount.test.ts
integration 102 files, 885 pass (5 skipped), 0 failing, run one file at a time against fbc_test_db with this migration's SQL applied (no schema push)
markdownlint clean
Semgrep 0 findings
OSV (security:deps) no issues (existing suppressions only)
CodeQL runs in CI on this PR

Test plan

  • On /app/inventory/physical-count, count a fabric product with quantity 12.5. Scan history shows 12.5.
  • Generate a snapshot, then open the variance report for that store. A part roll shows its fractional expected, counted and variance.
  • "Correct" a variance with 12.375. Reconciled items shows 12.375.
  • Type -1 as a count. It is refused with "The count must be a number, zero or more."

🤖 Generated with Claude Code

…econciliation quantities are Decimal(12,3)

The count, its variance and its reconciliation were Int while the balance
is Decimal(12,3) and the snapshot a float. Exercised first against the Int
columns (storeCountFractional.integration.test.ts), Prisma truncated
silently: a count of 12.5 yards was stored as 12, and two scans of 0.1 and
0.2 as 0 and 0, so an accurate count read as 0.3 missing. A count of "lots"
reached Prisma as NaN and answered 500.

- Migration 20261021120000_store_count_decimal widens the six columns in
  place (integer -> numeric), so existing rows keep their values.
- lib/inventory/storeCount.ts: scans summed and the variance taken to the
  stock precision (float noise is not a variance), and every count and
  reconciliation sent as numbers (JSON sends a Decimal as text).
- parseCount (lib/inventory/qtyInput.ts) reads a typed count; zero is a
  count. The count route and the corrected-count prompt use it, in place of
  Number() and parseInt.
- The inventory runbook no longer says reconcile.ts writes stock (it only
  stamps a Reconciliation row), or that the variance reports read
  InventoryPosition (they read the snapshot).

Tests: storeCountFractional.integration.test.ts (4, all failing on the Int
columns; removing the rounding or the conversion each fails one),
storeCount.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…store-count-decimal

# Conflicts:
#	docs/domains/inventory.md
@goetchstone
goetchstone merged commit f2bff6d into main Oct 17, 2026
12 checks passed
goetchstone added a commit that referenced this pull request Oct 17, 2026
…oposal quantity are Decimal(12,3) (#36)

* A buyer's draft and a B2B proposal keep a part roll: BuyerDraftItem.qty and ProposalLineItem.quantity are Decimal(12,3)

The last two whole-number quantity columns a part roll can reach (found
by #35's rule-49 sweep). Exercised first against the Int columns
(draftAndProposalQtyFractional.integration.test.ts, 6 of 7 failing): a
draft typed as 12.5 yards was stored as 1, a real PO for 12.5 yards was
drafted as 12, a buy priced 12 yards (96, not 100), a proposal line of
12.5 was stored and ordered as 12, and a typed "lots" answered 500.

- Migration 20261022120000_draft_and_proposal_qty_decimal: both columns
  widen integer -> numeric in place, default 1.
- Writers read a typed quantity with parseQty. A draft qty is zero or
  more (zero was already a legitimate draft qty); create still falls back
  to 1, edit refuses with a 400. The historical PO import keeps the
  ordered yards instead of truncating. A proposal quantity must be greater
  than zero, else 400.
- Readers convert with toQty: draftItemForClient / draftSpend
  (lib/buyerDraftQty.ts) and proposalLineForClient (lib/proposalQty.ts)
  on every route that sends or prices a row; exports, the performance
  report, convert-to-order and the PDF convert inline.
- The PDF's item page shows "each x qty = line" for any quantity but 1
  (it was > 1, so half a yard showed a yard's price as the line).
- Barcode quick-add reads its qty with parseQty, not parseInt; the draft
  wizard and proposal quantity boxes take decimals.
- Runbooks: buyer-drafts.md (the int truncation is gone; zero-quantity
  import lines are skipped, not clamped to 1 as it said) and the
  proposals section of sales-orders.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* buyerDraftQty: say where the conversions live

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@goetchstone
goetchstone deleted the fix/store-count-decimal branch October 17, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant