management: createStockAdjustment has no schema; the modal previews a change it silently cannot perform #69
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by
Author59while fixing #59/#60 on PR #68, filed in its corrected form after the author retracted its own first version. The original report claimed a stock/ledger divergence; that was a measurement error and is withdrawn. See PR #68 comment 3231 for the full retraction. This issue is the narrower claim that survived measurement.What was retracted, so nobody re-derives it
The first report said
adjustStockdoesstock + deltain SQL and therefore silently rounds a fractional delta — measured as stock 12 + delta 1.5 landing on 14 whilestock_movements.quantityrecorded 1.5.That measurement used a SQL numeric literal:
Drizzle does not do that. It sends a bound parameter, and an int4 parameter cannot carry
1.5:There is no rounding and no ledger divergence. Literal-vs-bound-parameter is the entire difference, and it is worth recording because the wrong version is more alarming and would have been easy to file.
The two real defects
1.
createStockAdjustmenthas no schema at allIt is the only inventory write path with no zod validation whatsoever. Every other path was bounded by PR #68 (
sell_price,buy_price,stock,weight_grams). This one is protected today only by the browser's implicitstep="1", which arrives free withmin="0"on<input type="number">.That protection is accidental, not designed, and it evaporates under any of:
step="0.01"or any explicit non-integer stepnoValidateon the formThe int4 column is the only thing left, and it produces a bare 500 (
invalid input syntax for type integer) rather than a readable rejection — the same shape #60 closed everywhere else.2. The modal displays an outcome it cannot perform, then does nothing
Measured through the real modal: typing
1.500inJumlahmakes the modal's own preview offerwith save enabled. Clicking save hits native
stepMismatchand the submit is blocked. No server call, no mutation, no error shown — stock stayed 12 with 0 movements.So the operator is shown a specific result, allowed to commit to it, and then silently gets nothing. That is worse than a visible rejection: the preview actively asserts the value is understood.
Why this is not a duplicate of #59 or #60
Berat (gram)widget on the two inventory forms. Fixed in PR #68.buy_priceandstockon the REST and MCP write paths, and explicitly treats the adjustment path as stock's legitimate owner — which is why PR #68 correctly did not widen into it.Scope
createStockAdjustmentboundingdeltaas an integer within int4, rejecting readably and naming the field, leaving the row untouched on rejection — matching what PR #68 established for the other four paths. Reuse the existing constant rather than declaring a second int4 literal.1.500is 1500 to an Indonesian typist and1,5is genuinely decimal, so any rule rescuing one input misreads the other.Explicitly not this issue
The missing API error handler — thrown errors flatten to bare 500s across every endpoint, including making an unauthenticated REST write answer 500 instead of 401. Pre-existing, out of scope for #60, and filed separately.
Verification
Jumlahin the real modal with1.500,1,500,abc, empty,-1and a 10-digit value. Readinventory.stock,stock_movementsandinventory_lotsback from Postgres each time.createStockAdjustmentdirectly, bypassing the browser, with a fractional and a 10-digit delta. Assert a readable 4xx and an untouched row — this is the path the accidentalstep="1"does not cover.Acceptance
createStockAdjustmentvalidates its input server-side and rejects readably without mutating the row; no adjustment input relies on a browser default for safety; the modal never previews an outcome it cannot perform; a direct non-browser call cannot produce a bare 500.Added scope: the negative resulting stock invariant
Routed here by #71 (its "Explicitly not this issue" section says the negative-resulting-stock invariant "belongs in
adjustStockunder #69"), and flagged byReviewer68r5during round 5 of PR #68 — because this issue's own Scope and Acceptance did not cover it, so closing #69 on its stated criteria would have left the gap untracked whileupdate_inventory_item's tool description points here.The gap, measured
AdjustmentDeltaSchema(PR #68) bounds the delta. It does not bound the result. A negative delta is legitimate — it is a removal — so a schema cannot be where this is fixed:apps/management/lib/catalog.ts:80-81derivesrow.stock > 0 ? "ready" : row.allowIndent ? "indent" : "habis", so a negative count reaches buyers as one of exactly two states — there is no third:allow_indentavailabilityfalsehabistrueindentThe
indentbranch is the harmful one and is the easy one to miss.apps/store/app/products/[slug]/add-to-cart.tsx:38-40disables the cart button forhabisonly;indentfalls through to the full cart flow and writesindent: trueonto the line.apps/store/lib/facets.ts:49-50states the same rule.One related claim corrected, so it is not inherited
update_inventory_item's description says the negative count is one "that FIFO then drives further down". Measured, that is conditional, not unconditional:apps/management/lib/orders/service.ts:53-59sizes its guard fromsum(inventory_lots.qty_remaining)and never readsinventory.stock, whileadjustStockmovesinventory.stockand never touches a lot.So the two numbers are independent, and that is the deeper problem:
inventory.stockand the lot ledger can disagree without either being wrong on its own terms.Scope for this part
inventory.stocknegative, at the service layer inadjustStock, so every caller inherits it — REST, MCP, the dashboard action, and the bot node from #71. A schema cannot do this: it sees the delta, not the current row.habisor orderable.adjustStock: decide whether it should also move the lot ledger, or whetherinventory.stockdiverging fromsum(qty_remaining)is intended. Note the opening balance has never had a lot or anINmovement on any path including the seed — recorded on PR #68 — so the ledger cannot currently reconstructstockat all.Acceptance for this part
No caller of
adjustStockcan commit a negativeinventory.stock; the rejection is readable and leaves the row unchanged; and if a negative is ever permitted, the publicavailabilityit produces is a stated decision rather than a side effect ofallowIndent.