Repository navigation
A buyer's draft and a B2B proposal keep a part roll: draft qty and proposal quantity are Decimal(12,3) - #36
Merged
Conversation
…ty 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
goetchstone
enabled auto-merge (squash)
October 17, 2026 11:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fabric is drafted, quoted and ordered by the yard, but a buyer's draft and a B2B proposal held their quantities as
Int.BuyerDraftItem.qtyandProposalLineItem.quantitywere the last two whole-number columns a part roll can reach, found by #35's sweep for the same bug shape (rule 49). I exercised them first against theIntcolumns, and each lost the fraction without an error:optionalIntswaps any non-integer for the default), and an edit to 12.5 was refused;Math.trunc), so the buy priced 12 yards: $96, not $100;NaNand answered 500.This is one of the follow-ups left by ledger-plan PR 4 ("Handheld floor" in the roadmap). It does not depend on #34 or #35, which leave these models and routes alone.
What changes
20261022120000_draft_and_proposal_qty_decimal: both columns becomeDecimal(12,3), default 1. The widening happens in place (integer -> numeric), so existing rows keep their values. The timestamp comes after The store-grain count keeps a part roll: count and reconciliation quantities are Decimal(12,3) #35's20261021120000.parseQty(lib/inventory/qtyInput.ts):buyerDraftRequestBody.test.tspins it, and home-accessory imports draft backorders at 0).validatedQty), a blank, garbage or negative qty is a 400. A blank qty used to be stored as 0.toQty, because JSON sends a Decimal as text (the A stock position or transfer reaches the screen as a number: the warehouse routes convert the Decimal with toQty instead of letting JSON send it as text #29 bug shape):draftItemForClientanddraftSpend(lib/buyerDraftQty.ts) on the item list, create, get and edit, the draft PO, the buy rollup and the archive;proposalLineForClient(lib/proposalQty.ts) on the proposal and on the line add and edit routes;> 1, so half a yard showed a yard's price as the line. The text comes fromitemPriceText, which has a unit test.parseQtyinstead ofparseInt, with a decimal keypad.step="any").docs/domains/buyer-drafts.md: the qty is fractional, and the int truncation is gone. Its edge-case list said zero or negative import lines were "clamped to 1".buildImportFromPurchaseOrderskips them, so the runbook now says that.docs/domains/sales-orders.md.Trace / Verification
lib/adapters,lib/auth,paymentService,secureUploadorautomations.grep -rlnE "\.(buyerDraftItem|proposalLineItem)\." app/srcfinds 19 files. I read every route that reaches the two models through a nested include as well (draft PO, buy, archive, performance, exports, proposal GET, PDF and convert-to-order).linked-pos, reorder and images.buildItemCreateData, so they take the new parsing.draftAndProposalQtyFractional.integration.test.tscalls the real routes behind their real permission gates; only next-auth's session is mocked. Against theIntcolumns, 6 of its 7 tests failed, for the reasons listed in the summary. After the change, all 7 pass.proposalLineForClientfails withExpected: 12.375, Received: "12.375";Math.truncon the imported qty fails withExpected: "12.5", Received: "12".prisma migrate deployof every migration into an empty scratch database, thenprisma migrate diff --from-config-datasource --to-schema --exit-code. It reported "No difference detected".Intcolumn named like a quantity can receive a part roll. The two that remain are whole by nature:Product.cartonQty(units per carton) andTillCount.quantity(cash).GET /buys/[id]and the archive. So a CANCELLED draft still counts against the buy's budget, even though the runbook's fix for a double count is "cancel the forward-flow drafts". This PR keeps that behaviour, and it is tracked inROADMAP.md→ "Handheld floor".Scan / gate state
npm run check:localstops at its integration step from an agent session, because Prisma refuses the harness'sdb push --accept-data-losswithout the owner's consent. I ran each step directly instead.buyerDraftRequestBody,historicalPoImportand newproposalPdftestsfbc_test_db, with this migration's SQL applied and no schema push, the same wayscripts/run-integration-tests.shruns them. Two existing tests failed on the first run because they compared the Decimalqtywith a number (buyerDraftCrud,homeAccessoryOrderCommit). Their assertions now readqty.toNumber(), as those files already do forcost, and both pass.security:deps)Test plan
/app/admin/buyer-drafts, add a fabric item with qty12.5. The card showsQty 12.5, and the buy's bar adds 12.5 × cost.2.375. A toast reads "Added 2.375 × …".12.5, then download the PDF. The item page reads "each x 12.5 = …".🤖 Generated with Claude Code