From 9f404d3f2d6c6a5078e2c6718db6cc528a3a03ac Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 17 Jul 2026 09:58:40 +0700 Subject: [PATCH] wal: flush 20260717T0958 --- .claude/WAL.md | 21 ++- .../investigator-codebase/MEMORY.md | 1 + .../run.md | 16 +- ...pec-pe-budget-freeze-daduyet-17-07-2026.md | 85 +++++++++ .../sub-investigator-codebase-0.md | 170 ++++++++++++++++-- 5 files changed, 267 insertions(+), 26 deletions(-) create mode 100644 .claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/spec-pe-budget-freeze-daduyet-17-07-2026.md diff --git a/.claude/WAL.md b/.claude/WAL.md index 4da7aae..037b379 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -1,17 +1,20 @@ # WAL — auto-generated, không sửa tay updated: 2026-07-17 | session: S133 | branch: main -goal: PE budget FREEZE-at-DaDuyet (UAT anh Kiệt 07-16 17:24-17:27): phiếu đã duyệt BẤT BIẾN, NS live = bản cập nhật gần nhất; pipeline /fable-real invest → lead spec → hmw Opus MAX implement → review → deploy +goal: PE budget FREEZE-at-DaDuyet (UAT anh Kiệt 07-16): phiếu DaDuyet bất biến NS, live = bản mới nhất; pipeline /fable-real invest → spec → hmw Opus MAX implement → review → deploy chain: -[!] 1. wf: hmw 1-task fable-real investigator-codebase (tier fable) — run 2026-07-17-S133-pe-budget-freeze-daduyet · wf_348f6155-167 -[ ] 2. lead verify findings (grep chéo read-sites PeWorkItemBudgets + finalize choke-point) + ghi spec-pe-budget-freeze-daduyet-17-07-2026.md -[ ] 3. hmw implement Opus MAX theo spec (BE Mig+service ‖ FE 2-app mirror → test-specialist) -[ ] 4. reviewer pre-commit (financial + mig) → commit push → cicd-monitor verify prod -[ ] 5. harvest per-turn + closeout +[x] 1. wf: hmw fable-real investigator-codebase — wf_348f6155-167 · #53 return-fail → disk sub-file 22.5KB cứu · lead verify 7/7 claim PASS (finalize 4-site :861/:899/:931/:1053 · pair-live :911-912 · DTO 17-field :964 · guards :228-242/:419-422 · PeDetailTabs hash identical) +[x] 2. spec ghi: runs/2026-07-17-S133-pe-budget-freeze-daduyet/spec-pe-budget-freeze-daduyet-17-07-2026.md (Mig 67 AddPeApprovedBudgetSnapshot 11 cột · OD1 CÓ lũy-kế · OD2 CÓ block-admin · OD3 KHÔNG TuChoi) +[!] 3. wf: hmw INV-A 2-task (test-specialist T0-REPRODUCE RED ‖ implementer-frontend types+badge 2-app) — wf_d9639faa-97a · note: hook nén-17.1K bắn lần nữa trên inv-cb 19.7KB (carry hook-vs-budget-cap — KHÔNG tự nén, dưới cap anh 25.6K/30.7K) +[ ] 4. wf: hmw INV-B 1-task implementer-backend (Domain+Config+Mig67 3-file+accumulator+helper 4-site+display-gate+2 guard+build slnx+T0 GREEN) +[ ] 5. wf: hmw INV-C 1-task test-specialist (T1-T9 + full suite PASS) +[ ] 6. reviewer pre-commit (mig+financial) → em main commit push → cicd-monitor verify prod +[ ] 7. harvest + docs closeout -next: chờ wf invest return → lead verify → spec +next: chờ INV-A return → verify (RED evidence + FE hash-equal + npm build ×2) → phóng INV-B verify: - ls .claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/ -- git log --oneline -3 -- grep -rn "PeWorkItemBudget" src/Backend/SolutionErp.Application/PurchaseEvaluations/ | head -20 +- git status --porcelain (sau INV-A: tests/ mới + fe-* đổi; src/Backend PHẢI sạch tới INV-B) +- dotnet test --filter PeBudgetFreeze (RED trước INV-B, GREEN sau) +- grep -rn "Phase = PurchaseEvaluationPhase.DaDuyet" src/Backend/ (4 hit, sau INV-B mỗi hit có snapshot-call kề) diff --git a/.claude/agent-memory/investigator-codebase/MEMORY.md b/.claude/agent-memory/investigator-codebase/MEMORY.md index 08215a0..8cb3552 100644 --- a/.claude/agent-memory/investigator-codebase/MEMORY.md +++ b/.claude/agent-memory/investigator-codebase/MEMORY.md @@ -18,6 +18,7 @@ Read-only INTERNAL audit SOLUTION_ERP codebase. Tools: Read, Grep, Glob, Bash + --- ## 🆕 Recent invest (S112-S114 stubs — verbatim → git history + `archive/2026-07.md`) +- **S133 (07-17) PE budget FREEZE-at-DaDuyet invest [engine: fable-real-single `wf_348f6155-167`; #53 return-fail (no StructuredOutput) → sub-file 22.5KB cứu trọn, lead ghi entry on-behalf sau verify-đĩa 7/7]:** retro-change 2 đường: BE detail-bundle load pair LIVE `PurchaseEvaluationFeatures.cs:911-912` (DTO :964 — 1 construction site duy nhất) + FE fallback `?? bs.proInitialAmount`/`?? bs.initialAmount` (`PeDetailTabs.tsx:1511/:1526` — draft cite :1288 lệch dòng = class cite-drift S108, bản chất đúng). Finalize = ĐÚNG 4 assignment DaDuyet ALL `PurchaseEvaluationWorkflowService.cs:861/:899/:931/:1053`; `ApplyApprovedPriceOnFinalize` (def :955 static) chỉ 3-call V2 — V1 :1053 KHÔNG gọi ⇒ choke-point = helper MỚI 4-call, KHÔNG tái dụng hook giá-chốt. Vector per-slip: `SetPeCcmBudgetPeriod` NO-phase-guard (:228-242) · `AdjustBudget` admin-bypass (:419-422). Lũy kế row1/2 peer-derived (:917-943) = drift-vector thứ 2 → chụp luôn. Design: 11 cột `ApprovedBudget*` trên PE + display-gate `BudgetFrozen` + 2 terminal-guard + backfill=live-now (Changelogs CÓ log value NHƯNG attribution-gap: log gắn phiếu-đang-mở-lúc-sửa → không tái dựng số lúc duyệt). PeDetailTabs 2-app hash IDENTICAL `931025f2` · types 2-app KHÁC (S117). Tag `[s133, pe-budget-freeze, fable-real, ghi-dia-cuu-2/2, attribution-gap-changelog]` - **S129 (07-16 đêm) adap 2 errata EOL 3-lane `wf_3fccd7d1`:** T0 sweep = SE **0 defect-site** (counter duy-nhất `agent-frontmatter-eol-check.ps1` byte-level pre-satisfy fix①③; pattern hỏng chỉ ở mention-site). T1: 19 `i/-text` đều declared-binary 0-suspect — **filter mẫu erratum thiếu docx/xlsx = 13 dương-giả trên SE** (tailor-per-class!); generator-drift thật duy-nhất `mfe-eval.ps1:204` Set-Content UTF8 = **BOM+CRLF** (blob mang BOM 3-byte); W0.6 = EOL-only A/B, trục-quote UNDETERMINED — đừng over-read. T2: nấc thật = **4 gated + 3 discipline** (đừng kê "7/7" phẳng — R1 MAJOR). 2 bẫy tự-mắc đáng nhớ: `git log -- | tail -1` = **first-touch KHÔNG phải policy-landing** (.gitattributes 08c7036 06-15 vs 92de58f 07-15) · story "40 = +1 agent mới" BỊA — số scoped=39, delta=`skills/README.md` ngoài glob SKILL.md (meta-count class). #53 WRITE-lane 2/3 lane — journal cứu trọn, em-main scribe. Tag `[s129, adap-errata-eol, byte-counter-pre-satisfy, bom-in-blob, tail1-first-touch-trap]` - **S114 (07-12) PE multi-winner PER-HANG-MUC (fable-clone):** Mig58/59 multi-winner = WHOLE-SUPPLIER (`PurchaseEvaluationSupplier.IsWinner`); `Quote.IsSelected` per hang-muc×NCC = latent-hook Mig12 nay LIVE (Mig65 backfill re-key winner-truth IsWinner→IsSelected = CARDINALITY-CHANGE #2). winnerQuoteTotal=SUM(Quote WHERE IsSelected) `PeFeatures.cs:1188`. #1 open: N-win 1 hang-muc split KHOI-LUONG hay full-price (schema thieu per-winner volume). Tag `[s114, pe-multiwinner-per-hangmuc, quote-isselected-live]`. - **S113 (07-12) Supplier import v2 (5-lane ensemble):** selection consumers qua ListSuppliersQuery (`PeDetailTabs:2222`+`ContractCreatePage:321`+dashboard); chot IsPublic-bool + dedup-MST+Code + fail-closed default. Prod 37 NCC. 1 lane #53-garble→em-main recover. Tag `[s113, supplier-import-v2, ma-ncc=code]`. diff --git a/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/run.md b/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/run.md index d49157e..7d9dfa3 100644 --- a/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/run.md +++ b/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/run.md @@ -18,7 +18,17 @@ ## Stages -- [ ] 1. invest fable-real (wf: `wf_348f6155-167`) -- [ ] 2. lead verify + ghi `spec-pe-budget-freeze-daduyet-17-07-2026.md` -- [ ] 3. hmw implement Opus MAX theo spec (BE ‖ FE → test) +- [x] 1. invest fable-real (wf: `wf_348f6155-167`) — #53 return-fail, disk sub-file 22.5KB cứu; lead verify 7/7 claim PASS +- [x] 2. lead verify + ghi `spec-pe-budget-freeze-daduyet-17-07-2026.md` (OD1 CÓ · OD2 CÓ · OD3 KHÔNG) +- [ ] 3. hmw implement Opus MAX theo spec — 3 invocation staged: + - [ ] INV-A (2-task ‖): test-specialist T0 REPRODUCE (RED evidence, KHÔNG đụng src/) ‖ implementer-frontend (types ×2 riêng + PeDetailTabs SHA-mirror badge + npm build ×2) + - [ ] INV-B (1-task): implementer-backend (Domain +11 col · Config · Mig 67 3-file + backfill idempotent · PeBudgetAccumulator · ApplyBudgetSnapshotOnFinalizeAsync 4-site · display-gate BudgetFrozen · 2 terminal-guard · build slnx · T0 GREEN) + - [ ] INV-C (1-task): test-specialist T1-T9 + full suite - [ ] 4. reviewer pre-commit → commit/push → cicd verify prod + +## taskList snapshot — implement (append @spec-chốt) + +- INV-A.1 `[impl:t0-reproduce]` test-specialist — PeBudgetFreezeTests T0: đưa phiếu tới DaDuyet QUA service finalize (mirror PeApproverFinalizeTests, KHÔNG seed thẳng Phase), đổi pair qua UpdatePeBudgetPro, GetDetail assert giữ số cũ → RED hiện tại (evidence log). +- INV-A.2 `[impl:fe-badge-types]` implementer-frontend — types 2 app +budgetFrozen (file KHÔNG mirror, sửa riêng) + PeDetailTabs 2 app badge 🔒 (SHA-mirror, hash-object bằng nhau) + npm build ×2. +- INV-B.1 `[impl:be-full]` implementer-backend — theo spec ② bước 1-7 + checklist BE. +- INV-C.1 `[impl:test-full]` test-specialist — T1-T9 + dotnet test full PASS. diff --git a/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/spec-pe-budget-freeze-daduyet-17-07-2026.md b/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/spec-pe-budget-freeze-daduyet-17-07-2026.md new file mode 100644 index 0000000..df49e71 --- /dev/null +++ b/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/spec-pe-budget-freeze-daduyet-17-07-2026.md @@ -0,0 +1,85 @@ +# SPEC — PE budget FREEZE-at-DaDuyet (S133, 2026-07-17) + +> **Provenance:** UAT anh Kiệt FDC chat 2026-07-16 17:24-17:27 (screenshot anh gửi) → `/fable-real investigator-codebase` deep-pass `wf_348f6155-167` (#53 return-fail → disk-truth `sub-investigator-codebase-0.md` 22.5KB) → **lead verify từ đĩa 2026-07-17** (finalize-map 4/4 hit khớp grep · pair-load :911-912 khớp · guard-vectors :228-242/:419-422 khớp · PeDetailTabs 2-app hash IDENTICAL `931025f2` · types 2-app hash KHÁC) → lead ghi spec này. +> **Owner-resolve 3 open-decision (theo khuyến nghị invest, veto được):** OD1 **CÓ** snapshot 4 cột lũy kế · OD2 **CÓ** terminal-guard block cả Admin · OD3 **KHÔNG** freeze TuChoi (DaDuyet-only). + +## ① Tính-năng / đề-bài + +Phiếu PE **Phase=DaDuyet bất biến số ngân sách** (R1 — anh Kiệt: "các phiếu đã duyệt thì không cho thay đổi gì hết"): chụp **snapshot** bộ số ngân-sách-gói-thầu (matrix PRO/CCM từ `PeWorkItemBudgets` + 4 số lũy kế peer-derived) vào **11 cột mới trên `PurchaseEvaluations`** tại thời điểm phiếu chuyển DaDuyet. Phiếu đã duyệt hiển thị/serve từ snapshot (display-gate BE + flag `budgetFrozen` trong DTO); phiếu chưa duyệt tiếp tục đọc **LIVE** (R2 — "ngân sách đúng nhất là ngân sách cập nhật gần nhất": bảng `PeWorkItemBudgets` giữ nguyên semantics + cơ chế nhập PRO/CCM KHÔNG đổi). Kèm **2 write-guard** chặn sửa field ngân-sách per-slip trên phiếu terminal (block cả Admin — OD2; đường chính thống = un-terminal EDGE-5 → sửa → duyệt lại → re-snapshot overwrite). **Backfill** phiếu DaDuyet hiện hữu = số live hiện tại (số-tại-thời-điểm-duyệt KHÔNG khôi phục được — changelog có log value nhưng attribution-gap: log gắn phiếu-đang-mở-lúc-sửa, honest-note). DB delta = AddColumn-only, **không bảng mới** (R3 — "tức lại database tí là oki"). + +**Cơ chế bug (đã verify):** mọi phiếu cùng cặp (ProjectId × WorkItemId) đọc CHUNG 1 record `PeWorkItemBudgets` LIVE — BE `PurchaseEvaluationFeatures.cs:911-912` (detail-bundle → `PeBudgetSummaryDto` :964, 1 construction site duy nhất) + FE fallback `ev.budgetPeriodAmount ?? bs.proInitialAmount` / `ev.ccmBudgetPeriodAmount ?? bs.initialAmount` (`PeDetailTabs.tsx:1511/:1526`) ⇒ PRO/CCM sửa NS → phiếu DaDuyet đổi số retroactive. Lũy kế row1/2 (`:917-943` peers) = vector drift thứ 2 (đổi khi phiếu KHÁC transition) → OD1 chụp luôn. + +## ② Cách implement (ordered — worker làm ĐÚNG thứ tự trong vai mình) + +1. **Domain** `src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs` — +11 props nullable: + `ApprovedBudgetProInitialAmount` `ApprovedBudgetProAdjustmentAmount` `ApprovedBudgetCcmInitialAmount` `ApprovedBudgetCcmAdjustmentAmount` `ApprovedBudgetPrevSubmittedTotal` `ApprovedBudgetPrevSelectedTotal` (decimal?) · `ApprovedBudgetProNote` `ApprovedBudgetCcmNote` (string?) · `ApprovedBudgetPrevSubmittedCount` `ApprovedBudgetPrevSelectedCount` (int?) · `ApprovedBudgetSnapshotAt` (DateTime? — marker frozen + audit). + *(Mapping nguồn: ProInitial/ProAdjust/ProNote ← pair.ProInitialAmount/ProAdjustmentAmount/ProNote · CcmInitial/CcmAdjust/CcmNote ← pair.InitialAmount/AdjustmentAmount/CcmNote — chú ý pair dùng tên `InitialAmount`/`AdjustmentAmount` cho CCM.)* +2. **EF Config** `Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs` — HasPrecision(18,2) ×6 decimal + HasMaxLength(1000) ×2 note (mirror pattern cột ApprovedPrice*/Note hiện có). +3. **Mig 67** `AddPeApprovedBudgetSnapshot` (`dotnet ef migrations add ... --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api` — 3-file rule: Migration + Designer + ModelSnapshot commit đủ): + - Up: AddColumn ×11 + `Sql()` backfill **idempotent** (guard `ActiveProvider` SqlServer-only mirror Mig 61/65): + `UPDATE pe SET ApprovedBudget* = b.*, ApprovedBudgetPrevSubmittedTotal/Count + PrevSelectedTotal/Count = (OUTER APPLY peers: cùng ProjectId+WorkItemId, Id<>pe.Id, CreatedAt.cs` = 11; Down có 11 DropColumn; backfill `Sql()` có `WHERE pe.Phase=7 AND pe.ApprovedBudgetSnapshotAt IS NULL`. +- [ ] `grep -n "ApplyBudgetSnapshotOnFinalizeAsync" PurchaseEvaluationWorkflowService.cs` = **5 hit** (1 def + 4 call); `grep -n "Phase = PurchaseEvaluationPhase.DaDuyet" src/ -r` = 4 hit, MỖI hit có snapshot-call trong ≤6 dòng trước. +- [ ] `dotnet build SolutionErp.slnx` 0 error (build FULL slnx — gotcha #65, DTO record đổi chữ ký đụng tests compile). +- [ ] `dotnet ef database update` ×2 DB local không lỗi; sqlcmd Dev: `SELECT COUNT(*) FROM PurchaseEvaluations WHERE Phase=7 AND ApprovedBudgetSnapshotAt IS NULL` = **0**. +- [ ] `dotnet test --filter PeBudgetFreeze` → T0 **GREEN**. + +**test-specialist (stage C — sau BE):** +- [ ] T1-T9 đủ (≥9 test mới tổng cả T0); `dotnet test SolutionErp.slnx` FULL PASS (baseline 509 + mới, 0 fail/0 skip). +- [ ] Đối chiếu accumulator: GetDetail output lũy kế TRƯỚC/SAU refactor bằng nhau (test hiện có không đổi kết quả). + +**reviewer (pre-commit — schema mig + financial BẮT BUỘC):** +- [ ] Xác nhận KHÔNG có phase-guard mới trên `UpdatePeBudgetPro/Ccm` (R2 giữ live). +- [ ] Display-gate đúng điều kiện `Phase==DaDuyet && SnapshotAt!=null`; nhánh frozen KHÔNG rò field live nào (đối chiếu 17+1 field từng cái). +- [ ] Đối chiếu **3 bản predicate lũy kế** (SQL backfill ↔ PeBudgetAccumulator ↔ Features cũ :917-943) khớp từng điều kiện (Phase set, CreatedAt<, Id<>, IsDeleted, IsSelected join-path). +- [ ] EDGE-5 interplay: un-terminal → snapshot dormant (không clear) → re-finalize overwrite; grep xác nhận KHÔNG chỗ nào set `ApprovedBudget* = null`. +- [ ] Grep `canEditPro|canEditCcm` FE: nhánh frozen không còn đường edit; 2 terminal-guard có test. + +**cicd-monitor (post-deploy):** +- [ ] Run Gitea xanh; prod sqlcmd: `__EFMigrationsHistory` top = `AddPeApprovedBudgetSnapshot`; `SELECT COUNT(*) FROM PurchaseEvaluations WHERE Phase=7 AND ApprovedBudgetSnapshotAt IS NULL` = 0; sys.tables = **89** (no new table). +- [ ] Bundle FE rotate ×2 app + curl-grep byte-level `budgetFrozen` + "Ngân sách chốt" PRESENT trong bundle mới (#77). +- [ ] Smoke 200×3 + PE endpoint 401-secured. +- [ ] UAT script cho anh Kiệt: mở phiếu DaDuyet → ghi số matrix → sửa NS PRO/CCM từ phiếu nháp cùng cặp (hoặc màn NS) → reload phiếu DaDuyet **incognito** → số KHÔNG đổi + badge 🔒; phiếu nháp cùng cặp thấy số MỚI. + +**Migration:** Mig 67 `AddPeApprovedBudgetSnapshot`. **File đụng (~15):** Domain PurchaseEvaluation.cs · PurchaseEvaluationConfiguration.cs · Migrations ×3 · PurchaseEvaluationFeatures.cs · PeWorkItemBudgetFeatures.cs · PurchaseEvaluationDtos.cs · +PeBudgetAccumulator.cs (mới) · PurchaseEvaluationWorkflowService.cs · tests ~2 · fe-user types+PeDetailTabs · fe-admin types+PeDetailTabs. diff --git a/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/sub-investigator-codebase-0.md b/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/sub-investigator-codebase-0.md index 82c01c5..0336062 100644 --- a/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/sub-investigator-codebase-0.md +++ b/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/sub-investigator-codebase-0.md @@ -1,21 +1,163 @@ # sub-investigator-codebase-0 — PE budget FREEZE-at-DaDuyet invest > RUN: 2026-07-17-S133-pe-budget-freeze-daduyet · engine /fable-real single deep-pass -> STATUS: IN-PROGRESS (ghi-đĩa-trong-lúc-làm chống #53 — bài S123/S128) +> STATUS: DONE — mọi claim grep/đọc từ đĩa lúc chạy (2026-07-17) ## Đề bài -R1 phiếu DaDuyet bất-biến số NS · R2 PeWorkItemBudgets = live cho phiếu chưa duyệt · R3 DB delta nhỏ (AddColumn, no new table, reversible, idempotent backfill). +R1 phiếu DaDuyet bất-biến số NS · R2 PeWorkItemBudgets = live "bản cập nhật gần nhất" cho phiếu chưa duyệt · R3 DB delta nhỏ (AddColumn bảng sẵn có, no new table, reversible Down, backfill idempotent). -## Tiến độ -- [ ] A READ-MAP PeWorkItemBudgets consumers -- [ ] B WRITE-MAP command budget -- [ ] C per-slip fields sẵn có semantics -- [ ] D FINALIZE-MAP Phase=DaDuyet -- [ ] E thiết kế snapshot -- [ ] F backfill changelog -- [ ] G edge -- [ ] H decision vs display -- [ ] I test list -- [ ] J spec-draft H21 +--- -## Findings (append trong lúc làm) +## A. READ-MAP — mọi site BE load/JOIN `PeWorkItemBudgets` (grep toàn `src/Backend` = 4 file ngoài mig/config) + +| # | Site | file:line | Serve phiếu DaDuyet? | +|---|---|---|---| +| 1 | CreatePE auto-create pair record (race-safe) | `PurchaseEvaluationFeatures.cs:105-134` | KHÔNG serve số — write-path | +| 2 | **DETAIL BUNDLE** `GetPurchaseEvaluationDetailQuery` → `PeBudgetSummaryDto` | `PurchaseEvaluationFeatures.cs:911-912` (load pairRec LIVE) → :964-972 (DTO) | **CÓ — display-after-decision. ĐÂY là site retro-change duy nhất serve số pair ra phiếu DaDuyet** | +| 3 | Submit-guard `effectiveBudget = BudgetPeriodAmount ?? pair.ProInitialAmount` | `PurchaseEvaluationWorkflowService.cs:222-228` | KHÔNG — decision-time lúc TRÌNH (phiếu còn DangSoanThao/TraLai) → GIỮ LIVE | +| 4 | `PeWorkItemBudgetEnsurer.EnsureTrackedAsync` (load-or-create) | `PeWorkItemBudgetFeatures.cs:39-57` | KHÔNG — write-path helper | + +List/inbox projection KHÔNG join pair (grep `BudgetPeriodAmount` Features chỉ :151/:218/:295/:428 — cột per-slip). Dashboard/Contract KHÔNG đọc PeWorkItemBudgets (grep 0 hit ngoài 4 file trên). Workflow Designer không đụng budget. + +**Số trong PeBudgetSummaryDto (Dtos.cs:331-349, 17 field) chia 3 nhóm:** +- **Từ pairRec LIVE (đổi retro khi PRO/CCM sửa)**: ProEstimateAmount(legacy, FE không dùng — fe-user types :316), ProNote, InitialAmount(CCM), AdjustmentAmount(CCM), CcmNote, ProInitialAmount, ProAdjustmentAmount, FullAmount+FullIsEstimate (derived :957-962), CanEditPro/CanEditCcm (role-derived :908-909). +- **Từ peers (drift theo transition phiếu KHÁC, không theo pair)**: PreviousSubmittedTotal/Count (:921-927 — peers CreatedAt< this, Phase ∈ {ChoDuyet,DaDuyet}, SUM BudgetPeriodAmount), PreviousSelectedTotal/Count (:931-943 — peers DaDuyet + quote IsSelected). +- **Từ chính phiếu (tự-frozen sau DaDuyet)**: CurrentProposalTotal (:947-951 — own IsSelected quotes; edit-guard `PurchaseEvaluationDetailFeatures.cs:28-31` + :49-98 chặn mọi sửa Section-2/chọn-thầu ngoài DangSoanThao/TraLai/ChoDuyet-có-quyền → DaDuyet = Conflict :97-98). + +**FE render (mirror 2 app `components/pe/PeDetailTabs.tsx`)**: row3 PRO `draftRow3 = ev.budgetPeriodAmount ?? bs.proInitialAmount` (fe-user :1288-1290) · row3 CCM `draftCcmRow3 = ev.ccmBudgetPeriodAmount ?? bs.initialAmount` (:1294-1295) — **fallback đọc pairRec LIVE = đường retro-change hiển thị thứ 2**; proFull/ccmFull (:1309-1310); r5/r7/r8/r9 + 3 so-sánh (colCalc :1323-1335); cờ vượt NS (:1344-1346). effBudget mirror submit-guard (:220/:244 cả 2 app). + +## B. WRITE-MAP `PeWorkItemBudgets` +- `UpdatePeBudgetProCommandHandler` `PeWorkItemBudgetFeatures.cs:80-130` — role PRO|Admin, absolute-set :105-107, **KHÔNG ràng Phase** (comment :20-22 "tài liệu sống per cặp"). Endpoint `PUT /purchase-evaluations/{id}/budget/pro` (controller :72). +- `UpdatePeBudgetCcmCommandHandler` :151-200 — role CCM|Admin, absolute-set :175-177, KHÔNG ràng Phase. `PUT .../budget/ccm` (controller :80). +- Key shared `(ProjectId × WorkItemId)` (`EnsureTrackedAsync` :36-58; UNIQUE filtered index Mig 50) → **mọi phiếu cùng gói đọc chung 1 record = cơ chế retro-change xác nhận (AC2 = read-site :911-912 + fallback FE :1288/:1294)**. + +**Vector retro-change per-slip (ngoài pair):** +- `SetPeCcmBudgetPeriodCommandHandler` :224-257 ghi `PE.CcmBudgetPeriodAmount` **KHÔNG phase-guard** (:228-242) → CCM sửa được row3-CCM trên chính phiếu DaDuyet. `PATCH .../budget/ccm-period` (controller :122). +- `AdjustBudgetCommand` (`PurchaseEvaluationFeatures.cs:319-429`) ghi `PE.BudgetPeriodAmount/ExpectedRemainingAmount` :425-429 — non-admin bị chặn phase khác (:412-417) nhưng **Admin bypass mọi phase** (:419-422) → admin sửa được row3-PRO phiếu DaDuyet. + +## C. PER-SLIP fields sẵn có (semantics thật — soi #73) +- `PE.BudgetPeriodAmount` (Mig 50, "NS kỳ này" PRO row3): set Create :151 · UpdateDraft :294-295 (null-safe GIỮ, chỉ DangSoanThao/TraLai per :326-327) · AdjustBudget :428 (absolute-set, scope Drafter-nháp/Approver-đang-duyệt-có-AllowApproverEditBudget/Admin). KHÔNG bị sync theo live. **Tái dụng làm snapshot? KHÔNG** — semantics khác (số drafter/approver NHẬP per-slip, không phải bản chụp pair matrix; matrix vẫn hiển thị pair riêng). +- `PE.CcmBudgetPeriodAmount` (Mig 59, row3 CCM): SetPeCcmBudgetPeriod :242 absolute-set — cần thêm terminal-guard (xem G). +- `PurchaseEvaluationDetails.DonGiaNganSach/ThanhTienNganSach` (`PurchaseEvaluationDetail.cs:17-18`): per-line item, ghi qua Add/Update detail (controller :195-206), lock edit-guard sau trình → tự-frozen. KHÔNG liên quan pair matrix. +- **Kết luận C: KHÔNG có chỗ chứa sẵn cho bản chụp pair matrix → thêm cột snapshot mới sạch hơn** (trade-off: +11 cột nullable vs conflate semantics 2 field cũ — chọn cột mới, theo precedent `ApprovedPrice*` Mig 54 "amount = snapshot lúc duyệt" comment WorkflowService :951). + +## D. FINALIZE-MAP (grep `Phase\s*=\s*PurchaseEvaluationPhase\.DaDuyet` toàn `src` = **đúng 4 hit, ALL trong PurchaseEvaluationWorkflowService.cs** — không seed/initializer/controller nào set) +1. `:861` V2 **level-finalize** (AllowApproverFinalize opt-in, `:859` EndedByLevelFinalize=true, `:860` ApplyApprovedPriceOnFinalize) +2. `:899` V2 **CCM-threshold** (CeoApprovalThreshold `:878`, `:898` ApplyApprovedPriceOnFinalize) +3. `:931` V2 **all-steps-done** (`:930` ApplyApprovedPriceOnFinalize) +4. `:1053` **V1 legacy** last-step (`ApproveV1LegacyAsync` — **KHÔNG gọi** ApplyApprovedPriceOnFinalize) + +**Choke-point đề xuất:** KHÔNG dùng `ApplyApprovedPriceOnFinalize` (miss nhánh V1 :1053; static/sync không db-access :955-956). → Helper MỚI `ApplyBudgetSnapshotOnFinalizeAsync(evaluation, ct)` trong cùng service, gọi ngay TRƯỚC mỗi `Phase = DaDuyet` tại **4 site**. Rủi ro nhánh sót tương lai: guard = unit test 4 nhánh + reviewer grep-check "mỗi assignment DaDuyet có snapshot call kề" (hiện 4/4; nhánh mới thêm sau này phải kèm — ghi rule vào comment helper). + +## E. THIẾT KẾ (phương án NHỎ NHẤT thỏa R1-R3) +**(i) Bộ số phiếu DaDuyet đang hiển thị hôm nay** (soi FE matrix + BE DTO): +- Block A pair: ProInitial, ProAdjust, ProNote, CcmInitial(=pair.InitialAmount), CcmAdjust(=pair.AdjustmentAmount), CcmNote → proFull/ccmFull FE tự cộng (:1309-1310). +- Row1/2 lũy kế: PrevSubmittedTotal/Count, PrevSelectedTotal/Count (peer-derived — drift riêng). +- Row3/8: per-slip sẵn (BudgetPeriodAmount/CcmBudgetPeriodAmount/ExpectedRemainingAmount) — frozen bằng WRITE-GUARD, không cần cột. +- Row4/r5/r7/r9 + so-sánh + cờ vượt: FE derive từ trên. +- ProEstimateAmount legacy: FE không dùng (fe-user types :316 "[LEGACY ≤S75]") → KHÔNG snapshot. + +**(ii) Bảng đặt cột: `PurchaseEvaluations`** — 1 PE : 1 WorkItemId (`PE.WorkItemId Guid?` Mig 49) : 1 pair record → cardinality 1-1 per phiếu, đúng đơn vị bị freeze. KHÔNG bảng mới (R3). + +**(iii) Cột mới (Mig 67 `AddPeApprovedBudgetSnapshot`) — 11 cột nullable:** +``` +ApprovedBudgetProInitialAmount decimal(18,2)? ApprovedBudgetCcmInitialAmount decimal(18,2)? +ApprovedBudgetProAdjustmentAmount decimal(18,2)? ApprovedBudgetCcmAdjustmentAmount decimal(18,2)? +ApprovedBudgetProNote nvarchar(1000)? ApprovedBudgetCcmNote nvarchar(1000)? +ApprovedBudgetPrevSubmittedTotal decimal(18,2)? ApprovedBudgetPrevSubmittedCount int? +ApprovedBudgetPrevSelectedTotal decimal(18,2)? ApprovedBudgetPrevSelectedCount int? +ApprovedBudgetSnapshotAt datetime2? ← marker frozen + audit thời điểm chụp +``` +Naming theo precedent `ApprovedPrice*` (Mig 54/66 — snapshot-lúc-duyệt trên PE). Core tối thiểu = 7 (bỏ 4 lũy kế) nếu owner chọn open-decision #1 = KHÔNG. + +**(iv) Display-gate:** `GetPurchaseEvaluationDetailQuery` — `frozen = e.Phase==DaDuyet && e.ApprovedBudgetSnapshotAt!=null` → build `PeBudgetSummaryDto` từ cột snapshot, **CanEditPro=CanEditCcm=false**, +field mới `bool BudgetFrozen` (append cuối record :349 — chỉ 1 construction site :964 phải sửa + 1 site frozen mới); else nhánh live y nguyên + BudgetFrozen=false. Fallback an toàn: DaDuyet mà SnapshotAt null (không thể sau backfill) → live như cũ. +**FE 2 app touch TỐI THIỂU** (input đã gate sẵn qua `bs.canEditPro/canEditCcm` — fe-admin :1403-1459/:1524, fe-user tương tự; row3/row8 đã gate `drafterEditable` :1236 = isEditablePhase loại DaDuyet): +- `fe-user/src/types/purchaseEvaluation.ts:314-349` + `fe-admin/src/types/purchaseEvaluation.ts` (~:312, KHÔNG SHA-mirror — S117): +`budgetFrozen: boolean`. +- `fe-{user,admin}/src/components/pe/PeDetailTabs.tsx` (SHA-mirror — sửa giống hệt): badge "Ngân sách chốt tại thời điểm duyệt" khi `bs.budgetFrozen` (cạnh header matrix ~:1306); KHÔNG đổi logic render. + +**Write-guards kèm (R1 "không cho thay đổi gì hết"):** +- `SetPeCcmBudgetPeriodCommandHandler` (PeWorkItemBudgetFeatures.cs:228+): +Conflict khi `pe.Phase ∈ {DaDuyet, TuChoi}`. +- `AdjustBudgetCommand` (PurchaseEvaluationFeatures.cs:419-422): +Conflict terminal-phase TRƯỚC nhánh admin (block cả Admin — open-decision #2). +- `UpdatePeBudgetPro/Ccm` pair: **GIỮ NGUYÊN không phase-guard** (R2 — bảng live vẫn là "bản cập nhật gần nhất"; phiếu frozen không bị ảnh hưởng nhờ display-gate). + +## F. BACKFILL phiếu DaDuyet hiện hữu +`PurchaseEvaluationChangelogs` CÓ log giá trị budget-edit (Summary "Ngân sách gói thầu (PRO): ban hành PRO Xđ → Yđ" — PeWorkItemBudgetFeatures.cs:109-126/:179-196) **NHƯNG KHÔNG tái dựng được số tại-thời-điểm-duyệt**: (a) log gắn `PurchaseEvaluationId` = phiếu ĐANG MỞ lúc sửa — sửa từ trang phiếu B không xuất hiện trong changelog phiếu A cùng cặp (attribution gap); (b) giá trị nằm trong string N0-format phải parse; (c) dev DB hiện 0 budgetLogs / 0 pairRows (sqlcmd 2026-07-17; prod [UNVERIFIED] chưa scan). → **Backfill = copy LIVE hiện tại + honest-note "không khôi phục được số gốc tại thời điểm duyệt"** (mirror pattern Mig 61 #79 — đúng bảng, đúng kỳ vọng). SQL trong Mig 67 Up (ActiveProvider==SqlServer guard như Mig 61/65): +```sql +UPDATE pe SET ApprovedBudget... = b...., ApprovedBudgetPrevSubmittedTotal = oa.st, ..., + ApprovedBudgetSnapshotAt = SYSUTCDATETIME() +FROM PurchaseEvaluations pe +LEFT JOIN PeWorkItemBudgets b ON b.ProjectId=pe.ProjectId AND b.WorkItemId=pe.WorkItemId AND b.IsDeleted=0 +OUTER APPLY (SELECT ... peers CreatedAt.cs` = 11; Down có 11 DropColumn. +- [ ] `grep -n "ApplyBudgetSnapshotOnFinalizeAsync" PurchaseEvaluationWorkflowService.cs` = 5 hit (1 def + 4 call :861/:899/:931/:1053-vùng). +- [ ] `grep -n "Phase = PurchaseEvaluationPhase.DaDuyet" src/ -r` = 4 hit, mỗi hit có snapshot-call trong ≤5 dòng trước. +- [ ] Backfill idempotent: `dotnet ef database update` ×2 không lỗi; sqlcmd Dev `SELECT COUNT(*) FROM PurchaseEvaluations WHERE Phase=7 AND ApprovedBudgetSnapshotAt IS NULL` = 0. +- [ ] Guard: curl PATCH `/budget/ccm-period` + `/budget-adjust` trên phiếu DaDuyet → 409 (cả token admin nếu decision #2 = CÓ). +- [ ] `dotnet build` 0 error/0 warning mới. +**implementer-frontend** +- [ ] `grep -rn budgetFrozen fe-user/src fe-admin/src` ≥ 4 hit (types + PeDetailTabs mỗi app). +- [ ] PeDetailTabs 2 app SHA-identical sau sửa: `git hash-object fe-user/.../PeDetailTabs.tsx fe-admin/.../PeDetailTabs.tsx` bằng nhau (nếu baseline đang identical — verify trước). +- [ ] 2 app `npm run build` pass. +**test-specialist** +- [ ] Test REPRODUCE commit TRƯỚC fix, RED (bs đổi theo pair trên phiếu DaDuyet); sau fix GREEN. +- [ ] 4 test snapshot per-nhánh finalize + 2 guard + null-safe + else-live + re-finalize-overwrite (≥8 test mới). +- [ ] `dotnet test SolutionErp.slnx` full pass (baseline STATUS.md + mới). +**reviewer** +- [ ] Adversarial: xác nhận KHÔNG thêm phase-guard vào UpdatePeBudgetPro/Ccm (R2); xác nhận display-gate điều kiện `Phase==DaDuyet && SnapshotAt!=null`; đối chiếu predicate backfill SQL vs accumulator vs Features :921-943 (3 bản khớp); soi EDGE-5 interplay (un-terminal → dormant → overwrite). +- [ ] Grep `canEditPro|canEditCcm` FE — frozen path không còn đường edit hiện. +**cicd-monitor** +- [ ] Run xanh; prod sqlcmd `__EFMigrationsHistory` top = `AddPeApprovedBudgetSnapshot`; `SELECT COUNT(*) ... Phase=7 AND ApprovedBudgetSnapshotAt IS NULL` = 0; bundle FE rotate + curl-grep `budgetFrozen` trong bundle (verify-deployed-artifact byte-level). +- [ ] UAT script: mở phiếu DaDuyet → ghi số matrix → sửa NS PRO từ phiếu nháp cùng cặp → reload phiếu DaDuyet → số KHÔNG đổi + badge frozen; phiếu nháp thấy số MỚI. + +**Migration đề xuất:** Mig 67 `AddPeApprovedBudgetSnapshot`. **File đụng (15):** Domain/PurchaseEvaluations/PurchaseEvaluation.cs · Infrastructure/.../PurchaseEvaluationConfiguration.cs · Migrations ×3 · Application/.../PurchaseEvaluationFeatures.cs · .../PeWorkItemBudgetFeatures.cs · .../Dtos/PurchaseEvaluationDtos.cs · +Application/.../PeBudgetAccumulator.cs (mới → **update slnx? KHÔNG cần** — file trong csproj glob; vẫn verify) · Infrastructure/Services/PurchaseEvaluationWorkflowService.cs · tests ×~2 · fe-user types+PeDetailTabs · fe-admin types+PeDetailTabs.