Files
mercadodevida/work/artifacts/F-080/architect.md
2026-08-20 05:59:47 +02:00

44 lines
3.0 KiB
Markdown

# F-080 — Architect: PVP (IVA incl.) input locks decimal editing
## Root cause
`PricingSection.tsx` binds the PVP `<input>` to `value={grossEur}` where `grossEur` is **recomputed on every render** as `centsToEur(Math.round(netCents * (1 + ratePercent / 100)))`. Every keystroke in PVP fires `onChange` which writes to `net`; the next render immediately rewrites the PVP `value` based on the (new) net. The input shows a value that never matches what the user typed — feels "locked".
Secondary issue: `<input type="number">` ignores locale decimals. Browsers (Chrome/Safari) reject commas outright, and Firefox sometimes snaps to an integer. Sibling Neto input has the same `type="number"` but happens to feel usable because the value displayed in Neto is the actual state (`net[v.id]`), so the user controls the display string directly.
## Design
Treat PVP as a first-class controlled input, exactly like Neto:
1. Add a `gross` state alongside `net`: `Record<string, string>` holding the PVP value per variant (euros as a user-typed string).
2. On price load, populate `gross[v.id]` from the persisted net + IVA rate.
3. The PVP `<input>` `value` reads from `gross[v.id]`, **not** from a derived expression.
4. PVP `onChange`:
- stores the raw string in `gross`,
- re-derives `net` so the Neto cell stays in sync.
5. Neto `onChange`:
- stores the raw string in `net`,
- re-derives `gross` so the PVP cell stays in sync.
6. IVA `onChange`: re-derives `gross` from `net` using the new rate.
7. `savePrice` prefers `gross` (the customer-facing price) and converts it to net using the current IVA rate, so what the operator sees is what gets saved.
8. Switch both price inputs from `type="number"` to `type="text" inputMode="decimal"` so comma/dot typing and digit deletion work natively across browsers; the existing `eurToCents` already handles both separators.
9. Margin and gross column still derive their final numbers from `gross` state for display.
No backend changes. No new fields. Same payload to `setVariantPrice`.
## Risk
Low. UI behaviour change only. The round-trip `gross → net → gross` is now loss-tolerant because:
- gross is the canonical input,
- net is recomputed as `Math.round(gross / (1 + rate))`,
- gross displayed is the typed string (not re-derived mid-edit).
A rounding mismatch of ±1 cent between saved and displayed is acceptable because we already round to cents on save.
## Acceptance mapping
- "PVP input lets the operator type any digit and decimal separator" → free-text input + state-driven value.
- "Operator can delete individual digits and the decimal separator" → value is the raw state, never overridden mid-edit.
- "PVP value is parsed and stored as a real number with 2-decimal rounding" → `eurToCents` + `Math.round` chain.
- "On blur/save, PVP stays consistent with neto + IVA rate" → save uses `gross / (1 + rate)`.
- "Behavior matches Neto" → identical input component, parsing rules, controlled-state pattern.
- "verify.sh is green" → no schema/API changes; typecheck and unit tests unaffected.