management: buy_price and stock are unvalidated; a negative stock silently publishes an item as habis #60
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
Reviewer56r1during round 3 of PR #56, proved by measurement rather than inferred, and deliberately not fixed there to avoid voiding a clean verdict on that PR's final round.Two claims that were assumed safe and are not
While reviewing #56, the deferral rationale for leaving
buy_priceandstockonNumber(x) || 0in the inventory forms was recorded as "not published, server-bounded". Both halves are false.1. Neither field is bounded at the trust boundary
buy_priceandstockarez.number().optional()with no.int(),.nonnegative()or.max(). Measured consequence:PATCH {"buy_price": 9999999999}→ bare 500 (int4 overflow surfacing as an unhandled error)This is the same defect class PR #56 just closed for
sell_price, which now carries.int().nonnegative().max(MAX_RUPIAH)on all four write paths withMAX_RUPIAH = 2_147_483_647single-sourced atlib/.../currency.ts:19.2.
stockdoes reach the storefrontThis is the part that matters. Measured:
PATCH {"stock": -5}→ 200, persisted, and the item's publicavailabilityflipped to"habis"That is #42's derived-availability rule working exactly as designed —
stock > 0 ? "ready" : allowIndent ? "indent" : "habis"— fed a value that nothing rejects. So a negative stock silently marks a real, in-stock product as unavailable to buyers, with no operator-visible error. The reviewer restored the affected row to12.stockwas excluded from the #43 form work for a good and still-valid reason: it belongs to the FIFO ledger and moves throughadjustment-modal.tsx. But "does not belong in that form" is not the same as "cannot be written to a nonsense value" — the REST and MCP paths write it directly.Why this is filed rather than fixed in #56
#56's round-3 verdict was clean (0 blockers, 0 shoulds) and widening it would have voided that verdict on its fourth round. The deferral may well remain the right call for the form fields. What was wrong was the justification, and an inaccurate "this is safe" note is worse than an honest "this is deferred" one — it stops the next reader from checking.
Scope
.int().nonnegative()and an int4.max()tobuy_priceandstockacross every write path — both REST schemas and both MCP tools — reusing theMAX_RUPIAHconstant #56 established rather than declaring a second literal.stockshould be writable at all through REST/MCP given the FIFO ledger owns it. If it stays writable, a negative value must be rejected, not merely bounded. If a negative stock is ever legitimate (a correction?), then the derived-availability rule needs to say what it means publicly, because today it meanshabis.Explicitly NOT this issue
Berat (gram)— see #59. That one is atype=numberinput misread, andweight_gramsgenuinely is.int().positive()server-side, so it really was bounded. The two defects cannot share a justification, which is exactly why they are separate issues.Verification
PATCHand MCP-writebuy_priceandstockwith: a 10-digit value, a negative value, a fractional value, and zero. Assert a readable 4xx, not a 500, and assert the row is untouched on rejection.stockwrite, readavailabilityback from/api/public/catalogand confirm it reflects a state the operator actually intended.2147483647appears; the constant must stay single-source.Acceptance
No
buy_priceorstockwrite can produce a bare 500 or a silently wrong publicavailability; every rejection is readable and leaves the row unchanged; the decision on whetherstockis writable outside the ledger is recorded here.