feat(F-088): completed feature
This commit is contained in:
@@ -2,12 +2,36 @@
|
||||
"feature_id": "F-080",
|
||||
"verdict": "APPROVED",
|
||||
"trace": [
|
||||
{ "acceptance": "PVP input lets the operator type any digit and decimal separator (\",\" or \".\")", "result": "PASS", "evidence": "type=text inputMode=decimal; value bound to gross state; eurToCents handles both separators." },
|
||||
{ "acceptance": "Operator can delete individual digits and the decimal separator inside the value", "result": "PASS", "evidence": "Value is the user's raw string; no render-time derivation overwrites it." },
|
||||
{ "acceptance": "PVP value is parsed and stored as a real number with 2-decimal rounding", "result": "PASS", "evidence": "eurToCents() -> Math.round(*100) -> backend stores cents." },
|
||||
{ "acceptance": "On blur/save, PVP stays consistent with neto + IVA rate", "result": "PASS", "evidence": "savePrice derives net = round(gross / (1 + rate/100))." },
|
||||
{ "acceptance": "Behavior matches the Neto (sin IVA) input UX", "result": "PASS", "evidence": "Both use the same component, same parsing, same controlled-state pattern." },
|
||||
{ "acceptance": "verify.sh is green", "result": "PASS", "evidence": "tsc --noEmit exit 0, eslint exit 0." }
|
||||
{
|
||||
"acceptance": "PVP input lets the operator type any digit and decimal separator (\",\" or \".\")",
|
||||
"result": "PASS",
|
||||
"evidence": "type=text inputMode=decimal; value bound to gross state; eurToCents handles both separators."
|
||||
},
|
||||
{
|
||||
"acceptance": "Operator can delete individual digits and the decimal separator inside the value",
|
||||
"result": "PASS",
|
||||
"evidence": "Value is the user's raw string; no render-time derivation overwrites it."
|
||||
},
|
||||
{
|
||||
"acceptance": "PVP value is parsed and stored as a real number with 2-decimal rounding",
|
||||
"result": "PASS",
|
||||
"evidence": "eurToCents() -> Math.round(*100) -> backend stores cents."
|
||||
},
|
||||
{
|
||||
"acceptance": "On blur/save, PVP stays consistent with neto + IVA rate",
|
||||
"result": "PASS",
|
||||
"evidence": "savePrice derives net = round(gross / (1 + rate/100))."
|
||||
},
|
||||
{
|
||||
"acceptance": "Behavior matches the Neto (sin IVA) input UX",
|
||||
"result": "PASS",
|
||||
"evidence": "Both use the same component, same parsing, same controlled-state pattern."
|
||||
},
|
||||
{
|
||||
"acceptance": "verify.sh is green",
|
||||
"result": "PASS",
|
||||
"evidence": "tsc --noEmit exit 0, eslint exit 0."
|
||||
}
|
||||
],
|
||||
"regression_checks": [
|
||||
"IVA select still updates tax rate",
|
||||
@@ -17,5 +41,6 @@
|
||||
],
|
||||
"verdict_reason": "All acceptance criteria trace to PASS.",
|
||||
"reviewer": "qa",
|
||||
"reviewed_at": "2026-08-20T04:02:00Z"
|
||||
}
|
||||
"reviewed_at": "2026-08-20T04:02:00Z",
|
||||
"agent": "qa"
|
||||
}
|
||||
|
||||
@@ -2,16 +2,44 @@
|
||||
"feature_id": "F-080",
|
||||
"verdict": "APPROVED",
|
||||
"checks": [
|
||||
{ "name": "PVP value bound to state, not derived", "result": "PASS", "notes": "PVP <input> value reads from gross[v.id], not from centsToEur(... netCents * (1 + rate)) on each render." },
|
||||
{ "name": "type=text + inputMode=decimal", "result": "PASS", "notes": "All 4 price inputs (coste, PVP, oferta, neto) switched to type=text inputMode=decimal; comma and dot accepted." },
|
||||
{ "name": "net <-> gross cross-sync on edit", "result": "PASS", "notes": "PVP onChange derives net; Neto onChange derives gross; IVA onChange recomputes gross from net using new rate." },
|
||||
{ "name": "savePrice prefers gross and persists correctly", "result": "PASS", "notes": "savePrice now reads gross, converts to net via rate, posts to pricingApi.setVariantPrice. No API contract change." },
|
||||
{ "name": "Pre-existing lint cleaned up", "result": "PASS", "notes": "Removed unused PricingSectionProps interface, unused loadingPrices and prices states (pre-existing) so lint is green." },
|
||||
{ "name": "Margin still computed from gross", "result": "PASS", "notes": "calcMarginBruto(grossCents, costCents) uses eurToCents(gross[v.id]) — no longer recomputed on every render from netCents." }
|
||||
{
|
||||
"name": "PVP value bound to state, not derived",
|
||||
"result": "PASS",
|
||||
"notes": "PVP <input> value reads from gross[v.id], not from centsToEur(... netCents * (1 + rate)) on each render."
|
||||
},
|
||||
{
|
||||
"name": "type=text + inputMode=decimal",
|
||||
"result": "PASS",
|
||||
"notes": "All 4 price inputs (coste, PVP, oferta, neto) switched to type=text inputMode=decimal; comma and dot accepted."
|
||||
},
|
||||
{
|
||||
"name": "net <-> gross cross-sync on edit",
|
||||
"result": "PASS",
|
||||
"notes": "PVP onChange derives net; Neto onChange derives gross; IVA onChange recomputes gross from net using new rate."
|
||||
},
|
||||
{
|
||||
"name": "savePrice prefers gross and persists correctly",
|
||||
"result": "PASS",
|
||||
"notes": "savePrice now reads gross, converts to net via rate, posts to pricingApi.setVariantPrice. No API contract change."
|
||||
},
|
||||
{
|
||||
"name": "Pre-existing lint cleaned up",
|
||||
"result": "PASS",
|
||||
"notes": "Removed unused PricingSectionProps interface, unused loadingPrices and prices states (pre-existing) so lint is green."
|
||||
},
|
||||
{
|
||||
"name": "Margin still computed from gross",
|
||||
"result": "PASS",
|
||||
"notes": "calcMarginBruto(grossCents, costCents) uses eurToCents(gross[v.id]) — no longer recomputed on every render from netCents."
|
||||
}
|
||||
],
|
||||
"lint": { "errors_introduced": 0, "pre_existing_cleaned": true },
|
||||
"lint": {
|
||||
"errors_introduced": 0,
|
||||
"pre_existing_cleaned": true
|
||||
},
|
||||
"typecheck": "PASS",
|
||||
"verdict_reason": "PVP is now a free-text controlled input, mirrors Neto behavior, save uses gross. UI-only change, no API contract change.",
|
||||
"reviewer": "reviewer",
|
||||
"reviewed_at": "2026-08-20T04:01:00Z"
|
||||
}
|
||||
"reviewed_at": "2026-08-20T04:01:00Z",
|
||||
"agent": "reviewer"
|
||||
}
|
||||
|
||||
@@ -2,14 +2,27 @@
|
||||
"feature_id": "F-080",
|
||||
"verdict": "APPROVED",
|
||||
"checks": [
|
||||
{ "name": "XSS surface", "result": "PASS", "notes": "Inputs are plain text; values are sanitised by existing eurToCents parser." },
|
||||
{ "name": "Auth chain unchanged", "result": "PASS", "notes": "Same pricingApi.setVariantPrice endpoint, still gated." },
|
||||
{ "name": "No new dependencies", "result": "PASS", "notes": "No new packages." }
|
||||
{
|
||||
"name": "XSS surface",
|
||||
"result": "PASS",
|
||||
"notes": "Inputs are plain text; values are sanitised by existing eurToCents parser."
|
||||
},
|
||||
{
|
||||
"name": "Auth chain unchanged",
|
||||
"result": "PASS",
|
||||
"notes": "Same pricingApi.setVariantPrice endpoint, still gated."
|
||||
},
|
||||
{
|
||||
"name": "No new dependencies",
|
||||
"result": "PASS",
|
||||
"notes": "No new packages."
|
||||
}
|
||||
],
|
||||
"sast": "PASS",
|
||||
"dependency_review": "PASS",
|
||||
"secret_scan": "PASS",
|
||||
"verdict_reason": "Pure UX fix; no new attack surface.",
|
||||
"reviewer": "security",
|
||||
"reviewed_at": "2026-08-20T04:01:30Z"
|
||||
}
|
||||
"reviewed_at": "2026-08-20T04:01:30Z",
|
||||
"agent": "security"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user