---
name: project-pe-negative-quote-correctness-review
description: PE negative-quote FE-only spec review (correctness-wire lens) — PASS_WITH_FIXES; sign-plumbing pitfall replicating deferred-parse pattern into shared-form dialog
metadata:
  type: project
---

# PE cho phép nhập số ÂM ở quote — correctness-wire lens review (owner anh Kiệt)

**Verdict: PASS_WITH_FIXES.** Plan FE-only (0 BE, 0 mig) sound; site inventory COMPLETE; R1/R2 no-op verified accurate on disk. 1 must-fix = sign-plumbing pitfall.

## Verified accurate (spec claims match disk)
- R1 (Contract.GiaTri âm cho phép): `CreateContractFromEvaluationFeatures.cs:88-100` GiaTri = Sum(IsSelected quotes/winner) NO clamp → negative flows through. `ContractFeatures.cs:46` GreaterThanOrEqualTo(0) = separate MANUAL path (unaffected). No-op correct.
- R2 (submit-guard net<=0 giữ): BE `PurchaseEvaluationWorkflowService.cs:206` `selectedThanhTien.Sum() <= 0` + FE `PeDetailTabs.tsx:201` `gia <= 0` — both SUM SIGNED → pure-neg blocks, mixed net>0 passes. No-op correct.
- Submit gate empty→skip: `:2226` parseVnd('')=0, `0!==0` false, `-0!==0` false. Semantics preserved.
- BE accepts neg: UpsertQuoteCommand `PurchaseEvaluationDetailFeatures.cs:272-280` no validator; ThanhTien decimal(18,2) `PurchaseEvaluationConfiguration.cs:131` no CHECK.
- Mirror byte-identical NOW (sha256 fd9a68a5...). 4 test files exist.

## MUST-FIX (correctness gap in terse plan)
Sign-loss pitfall. `form.thanhTien` is read DIRECTLY as saved value at POST body `:2232`(AddSupplierDialog) + `:2875`(QuoteDialog), submit gate `:2226`, toast `:2239`. VndInlineEdit (`:1014-1021`) works because it keeps abs in LOCAL `text` state + computes signed ONLY at its own "Lưu" onSave — these 2 dialogs have NO separate amount-save button. So spec MUST specify: store SIGNED in form.thanhTien (display Math.abs), and toggle +/- handler MUST re-sign form.thanhTien (not just flip a `neg` flag). If toggle only flips neg + user submits w/o further keystroke → POST sends wrong sign (positive) → neg quote silently saved positive = feature fails.

## Consumer inventory (Q4 — none break, all neg-safe)
Table cell `:2695/:2699` fmtMoney · giaChaoThau `:1935` toLocaleString · winnerQuoteTotal+threshold `:344/:346/:966` fmtMoney+`<` · submit checklist `:235` gia>0 · budget soft-warning `:1329-1352` uses PRO/CCM suggested NOT quotes. Only blanking helper formatVndInput(`:52` n>0?…:'') confined to 3 inputs; spec correctly leaves global untouched → budget input `:2830` stays >=0.

## Lesson
Replicating a DEFERRED-PARSE widget (local state + parse-at-own-save) into a SHARED-FORM dialog (value read by outer submit/POST) = sign/magnitude split must reach the shared field at EVERY mutation incl. the toggle, not just the widget's save. Grep every reader of the shared field (POST body + gate + toast) before trusting "reuse pattern X".
