management: Berat (gram) input silently misreads 1.500 as 1.5 (same type=number defect as price) #59

Closed
opened 2026-08-27 16:12:00 +00:00 by yuris · 0 comments
Owner

Found by Author43b while fixing the price-field blocker on PR #56, and correctly declined there as out of scope. Filing so it is not lost.

The defect

Berat (gram) in apps/management/app/(dashboard)/stok/inventory-edit-form.tsx is still an <input type="number">, which carries the same misread that made the price field dangerous:

  • 1.500 — ordinary Indonesian thousands formatting for 1500 g — is parsed by the browser as the decimal 1.5, not 1500.
  • abc, 1,500 or a pasted value with a unit blanks the field, and a Number(raw) || 0 style coercion turns that into 0.

Why it is lower severity than the price case (and why it still matters)

Lower, because weightGrams is not a published price: a wrong weight does not advertise a 1000×-wrong number to buyers, and PR #56 already bounded the field server-side with .int().positive(), so the trust boundary holds and the column cannot take a fractional or negative value.

Still matters, because weight is what docs/store-overhaul-spec.md identifies as the input to the ongkir question — "Ongkir. Buyer's first question. Needs item weight + destination." A weight silently stored as 1.5 instead of 1500 (or rejected to 0) feeds a wrong shipping quote, which is settled over WhatsApp and therefore lands on the operator rather than being caught by the site.

Fix

Reuse what PR #56 built rather than inventing a second parser. That PR replaced the price field with inputMode="numeric" text plus a parseRupiahInput that refuses anything which is not a digit run, marks aria-invalid, swaps the hint, and disables submit. Apply the same shape here:

  • drop type="number" so the raw string survives and an emptied field stays distinguishable from unparseable input
  • refuse rather than reinterpret, for the reason recorded on #56: 1.500 is 1500 to a typist and 1.5 to the browser, so any rule that rescues one input silently misreads another
  • keep the server-side .int().positive() as the guarantee; the widget is convenience only

Check whether the parser generalises cleanly — if it does, share it; if the unit differs enough to need its own bounds, say so.

Also worth checking in the same pass

Any other type="number" field in the two inventory forms (add-inventory-modal.tsx, inventory-edit-form.tsx). This is the third instance of this misread found in this codebase, so audit rather than patch one field.

Verification

  • Enter 1.500, 1,500, abc, an empty string and a pasted "1500 g" in the real form against a real database; read weight_grams back from Postgres each time and confirm no value was silently reinterpreted or zeroed.
  • Confirm a crafted REST/MCP call cannot land a fractional, negative or zero weight.
  • Confirm an emptied field is distinguishable from unparseable input, as #56 established for price.

Acceptance

No weight input is ever silently reinterpreted or coerced to 0; invalid input is refused visibly; the server-side bound remains the guarantee; every type="number" field in both inventory forms is audited and listed here.

Found by `Author43b` while fixing the price-field blocker on PR #56, and correctly declined there as out of scope. Filing so it is not lost. ## The defect `Berat (gram)` in `apps/management/app/(dashboard)/stok/inventory-edit-form.tsx` is still an `<input type="number">`, which carries the **same misread** that made the price field dangerous: - `1.500` — ordinary Indonesian thousands formatting for 1500 g — is parsed by the browser as the decimal `1.5`, not `1500`. - `abc`, `1,500` or a pasted value with a unit blanks the field, and a `Number(raw) || 0` style coercion turns that into `0`. ## Why it is lower severity than the price case (and why it still matters) Lower, because `weightGrams` is **not a published price**: a wrong weight does not advertise a 1000×-wrong number to buyers, and PR #56 already bounded the field server-side with `.int().positive()`, so the trust boundary holds and the column cannot take a fractional or negative value. Still matters, because weight is what `docs/store-overhaul-spec.md` identifies as the input to the **ongkir** question — *"Ongkir. Buyer's first question. Needs item weight + destination."* A weight silently stored as `1.5` instead of `1500` (or rejected to `0`) feeds a wrong shipping quote, which is settled over WhatsApp and therefore lands on the operator rather than being caught by the site. ## Fix Reuse what PR #56 built rather than inventing a second parser. That PR replaced the price field with `inputMode="numeric"` text plus a `parseRupiahInput` that refuses anything which is not a digit run, marks `aria-invalid`, swaps the hint, and disables submit. Apply the same shape here: - drop `type="number"` so the raw string survives and an emptied field stays distinguishable from unparseable input - refuse rather than reinterpret, for the reason recorded on #56: `1.500` is 1500 to a typist and `1.5` to the browser, so any rule that rescues one input silently misreads another - keep the server-side `.int().positive()` as the guarantee; the widget is convenience only Check whether the parser generalises cleanly — if it does, share it; if the unit differs enough to need its own bounds, say so. ## Also worth checking in the same pass Any other `type="number"` field in the two inventory forms (`add-inventory-modal.tsx`, `inventory-edit-form.tsx`). This is the third instance of this misread found in this codebase, so audit rather than patch one field. ## Verification - Enter `1.500`, `1,500`, `abc`, an empty string and a pasted `"1500 g"` in the real form against a real database; read `weight_grams` back from Postgres each time and confirm no value was silently reinterpreted or zeroed. - Confirm a crafted REST/MCP call cannot land a fractional, negative or zero weight. - Confirm an emptied field is distinguishable from unparseable input, as #56 established for price. ## Acceptance No weight input is ever silently reinterpreted or coerced to `0`; invalid input is refused visibly; the server-side bound remains the guarantee; every `type="number"` field in both inventory forms is audited and listed here.
yuris closed this issue 2026-08-28 08:31:57 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/garage#59
No description provided.