# F-080 — Architect: PVP (IVA incl.) input locks decimal editing ## Root cause `PricingSection.tsx` binds the PVP `` 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: `` 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` 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 `` `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.