Files
solution-erp/.claude/workflows/runs/2026-07-13-pe-negative-quote/spec-pe-negative-quote-13-07-2026.md
2026-07-13 16:37:04 +07:00

9.7 KiB
Raw Blame History

Spec — PE cho phép nhập SỐ ÂM ô "Số tiền báo giá cho hạng mục" (2026-07-13, S116)

Engine: /fable-clone investigator-codebase 4-lane ensemble (wf_eaf52972-d18, 4/4 clean 0-garble) → lead verify + synthesize. Owner anh Kiệt (Zalo ⇄ Duy Nguyen 2026-07-13).

① Tính-năng / đề-bài

Ô "Số tiền báo giá cho hạng mục" (React state form.thanhTienQuote.ThanhTien) trong dialog "Thêm NCC vào phiếu" + dialog sửa giá (QuoteDialog) của module PE hiện không nhập được số âm. Yêu cầu: cho nhập âm (NCC có phát-sinh-giảm giá trị do có làm trước).

Rule chốt (Duy Nguyen xác nhận 11:51): số âm = khoản trừ/phát-sinh-giảm của riêng NCC; NGÂN SÁCH GIỮ NGUYÊN — quote âm KHÔNG cộng/trừ vào ngân sách dự án.

Ngữ cảnh nghiệp-vụ (anh Kiệt S116): số âm là giá-trị THẬT (vd NCC hoàn tiền cho mình / phát-sinh-giảm), KHÔNG phải lỗi → cấm coi âm là lỗi hay cảnh-báo-đỏ. Nguyên-tắc "có giá là được": có ≥1 giá thật (âm/dương) → gửi được; chỉ chưa-nhập-gì (selected toàn 0) mới chặn. ⇒ nền cho R2-revised + R1 (cho HĐ âm).

② Cách implement — FE + 1 điều-kiện BE (0 migration · 0 bảng/cột mới)

Đã verify (invest 4-lane hội-tụ): BE + DB đã chấp nhận âm sẵnUpsertPurchaseEvaluationQuoteCommand (PurchaseEvaluationDetailFeatures.cs:272) KHÔNG validator, cột ThanhTien decimal(18,2) (PurchaseEvaluationConfiguration.cs:131) 0 CHECK constraint. Ngân sách (PeWorkItemBudgets) chỉ ghi bởi command budget role-gated → quote âm không đụng ngân sách (rule thỏa bằng kiến-trúc, 0 code). ⇒ Sửa FE (gỡ 3 chốt chặn âm) + nới 1 điều-kiện submit-guard BE (R2-revised).

5 thay đổi (mirror 2 app fe-user + fe-admin byte-identical — SHA-mirror bắt buộc):

  1. Input quote — AddSupplierDialog PeDetailTabs.tsx:2365 — thêm neg state + nút toggle ±, hiển thị formatVndInput(Math.abs(thanhTien)), lưu neg ? -parseVnd : parseVnd. Tái dùng pattern có sẵn VndInlineEdit:1003 (prop allowNegative + neg state :1015 + toggle :1025-1037 + save neg?-n:n :1019). KHÔNG reinvent.
  2. Input quote — QuoteDialog :2917 — cùng pattern; neg init từ dấu của existing.thanhTien (mở lại quote âm phải hiện đúng, không trắng ô).
  3. Submit gate — AddSupplierDialog :2226 if (detailId && form.thanhTien > 0)!== 0 (âm tạo quote; rỗng/0 vẫn skip = "chưa báo giá"). Toast :2239 cùng sửa.
  4. KHÔNG đụng parseVnd/formatVndInput (dùng-chung ô ngân sách cùng file :51-52) + KHÔNG truyền allowNegative cho ô ngân-sách thanhTienNganSach:2830 (giữ ≥0).
  5. Submit-guard R2-revised (BE + FE mirror): BE PurchaseEvaluationWorkflowService.cs:206 selectedThanhTien.Sum() <= 0selectedThanhTien.All(x => x == 0) (hoặc !Any(x => x != 0)). Chặn CHỈ khi chưa nhập giá nào; có ≥1 giá thật (âm/dương, kể cả 1 NCC 1 hạng mục âm) → CHO gửi. Message giữ. Count==0 (chưa chọn NCC) vẫn chặn riêng.
    • 🔴 FE = 2 mirror (em-main gate S116, KHÔNG phải 1): :201 (missingForApproval, gia <= 0) VÀ :235 (submitChecklist label "Giá chào thầu > 0"). CẢ HAI dùng computeGiaChaoThauev.winnerQuoteTotal=SUM (:75-80) — SUM KHÔNG phân-biệt all-zero vs net-zero (+100,100=0) → phải iterate list quote đã-chọn ev.suppliers[].quotes, predicate "có ≥1 selected quote thanhTien !== 0". Đổi CẢ :201 (block khi !hasReal) + :235 (ok khi hasReal, label → "Đã nhập giá chào thầu"). Net-zero-có-giá-thật = submittable.

Semantics giữ: rỗng/0 = "chưa báo giá" (skip tạo quote); âm = quote thật (≠ rỗng). formatVndInput(-n)→'' là lý do PHẢI tách magnitude+dấu (không chỉ nới parseVnd).

Tests (regression-lock invariant tài chính — test-specialist):

  1. Ngân sách giữ nguyên: upsert quote âm → assert PeWorkItemBudget amounts UNCHANGED (PeWorkItemBudgetTests).
  2. winnerQuoteTotal = SUM mixed +/- selected (PeMultiWinnerTests).
  3. submit-guard R2-revised: all-zero → blocked (giữ test :104); thuần-âm / net-âm → SUBMITTABLE (có giá thật, kể cả 1 NCC 1 hạng mục âm); mixed net-any → submittable (PeSubmitGuardAndBypassTests).
  4. CCM-threshold với total giảm do âm (PeCcmThresholdFinalizeTests).

Owner-decisions (anh Kiệt CHỐT S116 via AskUser)

  • R1 — Contract.GiaTri âm: CHO PHÉP (khuyến nghị). Luồng PE→HĐ giữ số âm; KHÔNG thêm clamp/floor; validator tạo-tay ContractFeatures.cs:46 (GiaTri≥0) GIỮ nguyên (đường khác). → code no-op (allow = không chặn thêm).
  • R2 — submit-guard: NỚI (anh Kiệt refine S116) — đổi chặn Sum()<=0 → chặn CHỈ khi TẤT CẢ selected = 0 (All(==0)). Có ≥1 giá thật (âm/dương, kể cả 1 NCC 1 hạng mục âm) → CHO gửi. Lý do: rào cũ Sum()<=0 bắt nhầm ca 1-NCC-âm → mâu thuẫn R1 (cho HĐ âm nhưng không gửi được phiếu tạo ra nó). → BE :206 + FE mirror :201 (fix nhỏ 2-tier).
  • R3 (default): BgVat/ChuaVat giữ nguyên (FE chỉ expose input âm cho thanhTien).
  • R-CEO — ngưỡng CEO: DÙNG NET (anh Kiệt chốt S116) — CeoApprovalThreshold giữ tính trên winnerQuoteTotal net (dòng âm giảm tổng → có thể kéo dưới ngưỡng → CCM finalize bỏ CEO; owner chấp nhận, đúng ý "duyệt theo net cam-kết"). → code no-op (giữ hiện tại). → R1 + R-CEO no-op + R2 nới-guard ⇒ implement = 4 thay-đổi FE + 1 điều-kiện BE (submit-guard) + 4 test.

🔍 Review-reconcile Stage 2 (fable-clone 3-lane wf_4824f158-b66 — Lane1 PASS_WITH_FIXES · Lane2/3 PASS)

🔴 MUST bake (Lane-1 sign-plumbing): 2 dialog commit form.thanhTien THẲNG ở POST (:2232 AddSupplier · :2875 QuoteDialog), KHÔNG có nút "Lưu số tiền" riêng như VndInlineEdit ⇒ nút ± PHẢI đảo dấu CHÍNH form.thanhTien NGAY (setForm({...form, thanhTien: -form.thanhTien})), display formatVndInput(Math.abs(form.thanhTien)), onChange áp dấu hiện tại. Chỉ flip cờ neg → bấm ± rồi Thêm không gõ thêm = gửi sai dấu (dương) = mất-dấu thầm lặng. Lane-2 nits (bake): (a) Toast :2239 dùng CÙNG predicate !== 0 với gate :2226. (b) neg = useState RỜI, KHÔNG nhét vào form object (QuoteDialog :2921 setForm({thanhTien}) thay-thế object → nuốt neg). (c) Re-verify mirror SAU implement: git diff --no-index 2 app PeDetailTabs = exit 0 (4 chèn × 2 app = drift-risk cao nhất). Lane-3 test notes: T1 (budget unchanged) hơi tautological (0 đường ghi budget từ quote) — giữ regression-lock, bề-mặt rò thật = T2. T3 cần seed ≥2 quote TRÁI DẤU/1 winner → mở rộng helper SeedWinnerWithQuoteAsync (PeSubmitGuardAndBypassTests.cs:106-136, hiện 1 quote/winner); base :104 không đủ nửa "mixed net>0 submittable". Owner-confirm CEO-threshold: anh Kiệt CHỐT dùng NET (giữ hiện tại, no-op) — chấp nhận dòng âm có thể kéo phiếu dưới ngưỡng CEO.

Cổng cuối — em-main-solo gate (fable-real #53 garble recovery, S116)

Fable-real deep-pass (wf_9648d207-21b, model=claude-fable-5 — Fable UP) chạy 124K tok/35 tool-call NHƯNG #53 return-garble (cut mid-investigation ở CEO-threshold, 0 verdict trong transcript). Per playbook feedback_agent_return_garble_recoverem-main-solo gate:

  • Verdict: GO-WITH-FIXES. 7 agent (invest 4 + review 3) hội-tụ sạch; sign-plumbing (Lane-1) baked; R-CEO verified trên đĩa (fable-real transcript đọc đúng winnerQuoteTotal SUM :884-889).
  • 1 FIX em-main gate bắt thêm: R2-revised FE = 2 mirror (:201+:235) iterate-quotes-KHÔNG-sum (bake change #5) — điều fable-real chưa kịp soi.
  • Còn lại SOUND → GO implement.

③ Checklist (vai · deliverable · acceptance ĐO ĐƯỢC)

# Vai Deliverable Acceptance đo được
C1 implementer-frontend ± toggle + neg-state cho 2 input quote (:2365, :2917) × 2 app Nhập -5.000.000 → ô hiện -5.000.000 (KHÔNG trắng); reopen quote âm hiện đúng dấu; SHA fe-user==fe-admin
C2 implementer-frontend Submit gate :2226 >0!==0 + toast :2239 × 2 app Thêm NCC với quote âm → tạo quote (không bị nuốt im lặng); rỗng/0 vẫn "chỉ tạo NCC"
C3 implementer-frontend Ô ngân-sách :2830 GIỮ ≥0 (không allowNegative) Nhập - vào ngân sách hạng mục → bị chặn/không nhận
C3b implementer-backend + implementer-frontend Submit-guard R2-revised: BE :206 Sum()<=0All(==0) + 2 FE mirror :201+:235 (iterate quotes, KHÔNG sum) × 2 app Phiếu 1 NCC 1 hạng mục âm → GỬI được; net-zero-có-giá → gửi được; selected toàn 0 → vẫn chặn "chưa báo giá"
C4 test-specialist 4 test regression-lock (ngân-sách-giữ-nguyên + winnerTotal + submit-guard + CCM-threshold) dotnet test xanh; test #1 chứng budget bất-biến khi quote âm
C5 reviewer + cicd build 2-app + deploy npm build 2-app pass; bundle rotate; prod nhập âm được (anh UAT incognito)

Trạng thái sau implement dự kiến: Mig 65 (GIỮ) · 89 bảng (GIỮ) · test 486→~490 · FE 2-app bundle rotate + BE 1 điều-kiện guard. → CI chạy (path-filter không skip).