wal: flush 20260717T0958
This commit is contained in:
@ -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.
|
||||
|
||||
@ -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<pe.CreatedAt; submitted = Phase IN (10,7) SUM(BudgetPeriodAmount)+COUNT; selected = Phase=7 AND EXISTS supplier IsWinner=1 → COUNT + SUM(q.ThanhTien WHERE q.IsSelected qua join Suppliers→Quotes)), ApprovedBudgetSnapshotAt = SYSUTCDATETIME() FROM PurchaseEvaluations pe LEFT JOIN PeWorkItemBudgets b ON b.ProjectId=pe.ProjectId AND b.WorkItemId=pe.WorkItemId AND b.IsDeleted=0 WHERE pe.Phase=7 AND pe.ApprovedBudgetSnapshotAt IS NULL AND pe.IsDeleted=0`
|
||||
*(predicate PHẢI khớp `PurchaseEvaluationFeatures.cs:917-943` — reviewer đối chiếu 3 bản: SQL ↔ accumulator ↔ Features cũ; enum: ChoDuyet=10, DaDuyet=7)*
|
||||
- Down: DropColumn ×11 (data-restore no-op mirror Mig 61/65).
|
||||
- Apply local: `dotnet ef database update` cả `SolutionErp_Dev` (--connection override) + `SolutionErp_Design` (per skill ef-core-migration + feedback_designtime_runtime_db).
|
||||
4. **Shared accumulator (chống drift 3-bản):** extract khối lũy kế + pair-load semantics từ `GetPurchaseEvaluationDetailQuery` (`PurchaseEvaluationFeatures.cs:917-951`) ra helper static mới `Application/PurchaseEvaluations/PeBudgetAccumulator.cs` (input: db, projectId, workItemId, peId, peCreatedAt → output record {PrevSubmittedTotal/Count, PrevSelectedTotal/Count}) — dùng chung: GetDetail nhánh live + service snapshot. GetDetail refactor gọi accumulator (0 đổi hành vi — so output trước/sau bằng test hiện có).
|
||||
5. **Service snapshot helper** `Infrastructure/Services/PurchaseEvaluationWorkflowService.cs` — +`private async Task ApplyBudgetSnapshotOnFinalizeAsync(PurchaseEvaluation evaluation, CancellationToken ct)`:
|
||||
- Load pair `AsNoTracking` theo (ProjectId, WorkItemId) — null-safe (WorkItemId null / pair thiếu → các cột số = null, VẪN set `ApprovedBudgetSnapshotAt` = UtcNow làm marker).
|
||||
- Gọi accumulator cho 4 số lũy kế. **LUÔN overwrite** (re-finalize sau un-terminal/TraLai = re-snapshot lần DaDuyet cuối — khớp "bản cập nhật gần nhất" áp cho thời điểm duyệt).
|
||||
- Comment rule ngay trên helper: `// RULE: MỌI nhánh set Phase=DaDuyet PHẢI gọi helper này ngay trước assignment — reviewer grep-check.`
|
||||
- **Gọi tại ĐÚNG 4 site, ngay trước `evaluation.Phase = PurchaseEvaluationPhase.DaDuyet`:** `:861` (level-finalize) · `:899` (CCM-threshold) · `:931` (all-steps-done) · `:1053` (V1 legacy — nhánh này hiện KHÔNG gọi ApplyApprovedPriceOnFinalize, đừng copy nhầm behavior giá-chốt vào V1; CHỈ thêm budget-snapshot).
|
||||
6. **DTO + display-gate** `Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs` + `PurchaseEvaluationFeatures.cs`:
|
||||
- `PeBudgetSummaryDto` (positional record, hiện 17 field, 1 construction site `:964`) — **append cuối** `bool BudgetFrozen = false`.
|
||||
- `GetPurchaseEvaluationDetailQuery` (`:905-973`): `var frozen = e.Phase == PurchaseEvaluationPhase.DaDuyet && e.ApprovedBudgetSnapshotAt != null;` → nhánh frozen: build DTO từ cột `ApprovedBudget*` (fullAmount/FullIsEstimate dùng CÙNG công thức `:957-962` nhưng input = snapshot cols; `CanEditPro=CanEditCcm=false`; `CurrentProposalTotal` vẫn từ own quotes — tự-frozen bởi edit-guard; `BudgetFrozen=true`); nhánh else: live y nguyên + `BudgetFrozen=false`. Fallback an toàn: DaDuyet mà SnapshotAt null (không xảy ra sau backfill) → live như cũ.
|
||||
7. **Write-guards (OD2 — block CẢ Admin trên terminal):**
|
||||
- `PeWorkItemBudgetFeatures.cs` `SetPeCcmBudgetPeriodCommandHandler` (`:228+`): +`ConflictException` khi `pe.Phase ∈ {DaDuyet, TuChoi}` (đặt TRƯỚC role-check side-effects, sau NotFound).
|
||||
- `PurchaseEvaluationFeatures.cs` `AdjustBudgetCommand` (`:419-422`): +Conflict terminal-phase TRƯỚC nhánh `else { actorTag = "[Admin]" }`.
|
||||
- **KHÔNG đụng** `UpdatePeBudgetPro/CcmCommandHandler` (pair live = R2, giữ không phase-guard).
|
||||
8. **FE ×2 app (sau khi BE DTO chốt — nhưng shape đã cố định ở spec, làm song song được):**
|
||||
- `fe-user/src/types/purchaseEvaluation.ts` (~:314-349) + `fe-admin/src/types/purchaseEvaluation.ts` (~:312) — 2 file KHÔNG mirror, sửa riêng: +`budgetFrozen: boolean` vào PeBudgetSummary type.
|
||||
- `fe-user` + `fe-admin` `src/components/pe/PeDetailTabs.tsx` (**SHA-mirror — sửa GIỐNG HỆT, verify `git hash-object` 2 file bằng nhau sau sửa**; baseline hiện `931025f2` identical): +badge "🔒 Ngân sách chốt tại thời điểm duyệt" khi `bs.budgetFrozen` (cạnh header bảng TỔNG HỢP NGÂN SÁCH). KHÔNG đổi logic render khác (input đã tự khoá qua `canEditPro/canEditCcm=false` từ BE).
|
||||
- `npm run build` ×2 app PASS.
|
||||
9. **Tests** (Infrastructure.Tests, SQLite fixture — mig `Sql()` không replay qua EnsureCreated = safe, precedent Mig 65):
|
||||
- **T0 REPRODUCE (test-before, viết + chạy TRƯỚC khi BE fix land):** phiếu DaDuyet + pair; `UpdatePeBudgetPro` đổi số; GetDetail → bs **phải giữ số cũ** → hiện RED (đọc live) → GREEN sau fix. *(Test này compile trên code HIỆN TẠI — không cần cột mới.)*
|
||||
- T1-T4: snapshot-set đủ **4 nhánh finalize** (level-finalize / CCM-threshold / all-steps / V1-legacy): `ApprovedBudget* == pair values` + `SnapshotAt != null`.
|
||||
- T5-T6 guards: `SetPeCcmBudgetPeriod` trên DaDuyet → Conflict (cả admin) · `AdjustBudget` admin trên DaDuyet → Conflict.
|
||||
- T7: display-gate else-live (phiếu ChoDuyet sau pair-đổi → số MỚI + `BudgetFrozen=false`; frozen DTO có CanEdit*=false).
|
||||
- T8: null-safe (DaDuyet WorkItemId null → budgetSummary null không NRE; pair thiếu → frozen số null + SnapshotAt set).
|
||||
- T9: re-finalize overwrite (un-terminal → sửa pair → duyệt lại → snapshot = số MỚI).
|
||||
10. **Docs closeout (em main):** STATUS (Mig 67, tables giữ 89) + HANDOFF + session-log + gotchas nếu phát sinh; slnx KHÔNG đổi (accumulator nằm trong csproj glob — vẫn verify `git status`).
|
||||
|
||||
## ③ Checklist per-vai (acceptance ĐO ĐƯỢC)
|
||||
|
||||
**test-specialist (stage A — REPRODUCE trước):**
|
||||
- [ ] File test mới `PeBudgetFreezeTests` có T0; chạy `dotnet test --filter PeBudgetFreeze` trên code HIỆN TẠI → **RED** (log output làm evidence), commit-ready.
|
||||
- [ ] KHÔNG đụng src/ production.
|
||||
|
||||
**implementer-frontend (stage A — song song, disjoint):**
|
||||
- [ ] `grep -rn budgetFrozen fe-user/src fe-admin/src` ≥ 4 hit (2 types + 2 PeDetailTabs).
|
||||
- [ ] `git hash-object fe-user/src/components/pe/PeDetailTabs.tsx fe-admin/src/components/pe/PeDetailTabs.tsx` → 2 hash **BẰNG NHAU** (baseline `931025f2` đang identical).
|
||||
- [ ] `npm run build` PASS cả 2 app (log exit 0).
|
||||
|
||||
**implementer-backend (stage B — sau stage A):**
|
||||
- [ ] Mig 67 3-file: `git status` Migrations/ = 3 file (mới ×2 + ModelSnapshot); `grep -c "AddColumn" <Mig67>.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.
|
||||
@ -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<pe.CreatedAt ...) oa
|
||||
WHERE pe.Phase=7 AND pe.ApprovedBudgetSnapshotAt IS NULL -- idempotent
|
||||
```
|
||||
LEFT JOIN → null-safe (WorkItemId null / thiếu pair → marker set, số null → FE hiển thị "—" như hiện tại). Down = DropColumn ×11 (schema reversible); data-restore no-op (mirror Mig 61/65).
|
||||
|
||||
## G. EDGE
|
||||
- **TuChoi:** terminal khoá (enum :26) + action GỠ S60 (service :101-106 chặn cả admin) → phiếu TuChoi mới KHÔNG phát sinh; phiếu cũ ít. **Khuyến nghị: freeze DaDuyet-only** (display-gate `Phase==DaDuyet`); extend TuChoi sau = 1 điều kiện nếu owner muốn.
|
||||
- **DaDuyet có đường thoát ngược:** CÓ — EDGE-5 hard-lock chỉ chặn non-admin (`:67-68`); **Admin + System un-terminal được** (comment :62-63). Precedent: `ApprovedPrice*` KHÔNG BAO GIỜ clear (grep `ApprovedPriceAmount = null` = 0 hit ngoài default-param) → **mirror: KHÔNG clear snapshot khi un-terminal** — snapshot nằm im (display-gate theo Phase tự chuyển về live), re-finalize **OVERWRITE** (helper luôn ghi đè = "re-snapshot lần DaDuyet cuối").
|
||||
- **TraLai → sửa → duyệt lại:** như trên — overwrite tại lần finalize mới. Đúng ngữ nghĩa "ngân sách đúng nhất là bản cập nhật gần nhất" áp cho THỜI ĐIỂM DUYỆT cuối.
|
||||
- **WorkItemId null / thiếu pair row:** helper set SnapshotAt + số null; GetDetail hiện tại đã trả `budgetSummary=null` khi WorkItemId null (:905-906) → hành vi giữ nguyên (banner "chưa gắn Hạng mục" fe-user :1298-1303).
|
||||
|
||||
## H. DECISION-time vs DISPLAY-time (GIỮ LIVE — không đụng)
|
||||
- Submit-guard `WorkflowService.cs:222-228` — lúc trình, LIVE đúng semantics ✓
|
||||
- CCM-threshold so `winnerQuoteTotal` vs `aw.CeoApprovalThreshold` (:878+) — không đọc budget ✓
|
||||
- winnerQuoteTotal = SUM IsSelected (Dtos :269 + Features ~:1219) — quote-derived, tự-frozen ✓
|
||||
- **CreateContractFromEvaluation KHÔNG đọc PeWorkItemBudgets** (grep `Budget` file = :106-107 carry `pe.BudgetPeriodAmount`→BudgetManualAmount + :183 projection; GiaTri = SUM báo giá chọn — xác nhận S114/S117) ✓
|
||||
|
||||
## I. TEST đề xuất (Infrastructure.Tests, SqliteDbFixture — mig `Sql()` skip qua EnsureCreated ✓ precedent Mig 65)
|
||||
1. **test-before REPRODUCE (RED hiện tại):** phiếu DaDuyet + pair; UpdatePeBudgetPro đổi số; GetDetail → bs phải GIỮ số cũ (nay FAIL vì đọc live) → GREEN sau fix.
|
||||
2. Snapshot-set đủ 4 nhánh finalize (level-finalize / ccm-threshold / all-steps / V1-legacy): ApprovedBudget* == pair values tại lúc duyệt + SnapshotAt != null.
|
||||
3. Guard: SetPeCcmBudgetPeriod trên DaDuyet → Conflict; AdjustBudget admin trên DaDuyet → Conflict (theo decision #2).
|
||||
4. Display-gate else-live: phiếu ChoDuyet sau khi pair đổi → bs = số MỚI + BudgetFrozen=false; frozen DTO có CanEditPro/Ccm=false.
|
||||
5. Null-safe: DaDuyet WorkItemId null → budgetSummary null, không NRE; pair thiếu → frozen số null.
|
||||
6. Re-finalize overwrite: admin un-terminal → sửa pair → duyệt lại → snapshot = số MỚI.
|
||||
**Ước lượng:** BE 6 file (PurchaseEvaluation.cs + Configuration + Features + WorkflowService + PeWorkItemBudgetFeatures + Dtos) + mig 3-file + tests ~2 file + FE 4 file (2 app × types+PeDetailTabs) ≈ 15 file / ~400 LOC. **Rủi ro:** positional-record append (1 construction site :964 — thấp) · mirror SHA PeDetailTabs 2 app (diff-check sau sửa) · OUTER APPLY backfill đúng filter (IsDeleted=0, Phase IN (7,10) cho submitted-row1) · nhánh finalize MỚI tương lai quên gọi helper (mitigate: comment rule + reviewer grep 4/4).
|
||||
|
||||
## Open decisions (≤3, kèm khuyến nghị)
|
||||
1. **Snapshot 4 cột lũy kế (row1/2)?** REC **CÓ** (conf 0.7) — r5/r7 + so-sánh derive từ row1; không chụp thì phiếu DaDuyet vẫn đổi số khi peer transition (vector khác PeWorkItemBudgets nhưng cùng triệu chứng Kiệt thấy). Bỏ = 7 cột, chấp nhận drift lũy kế.
|
||||
2. **Terminal-guard `SetPeCcmBudgetPeriod`+`AdjustBudget` block cả Admin?** REC **CÓ** (conf 0.75) — "không cho thay đổi gì hết"; đường chính thống = admin un-terminal (EDGE-5 exempt) → sửa → duyệt lại (re-snapshot).
|
||||
3. **Freeze TuChoi?** REC **KHÔNG** (conf 0.8) — action gỡ S60, extend sau 1 dòng.
|
||||
|
||||
---
|
||||
|
||||
# SPEC-DRAFT (H21 3 heading — lead verify rồi lead ghi file spec)
|
||||
|
||||
## ① Tính-năng / đề-bài
|
||||
Phiếu PE **Phase=DaDuyet bất biến số ngân sách**: chụp (snapshot) bộ số ngân-sách-gói-thầu (`PeWorkItemBudgets` matrix PRO/CCM + lũy kế) vào 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`), phiếu chưa duyệt tiếp tục đọc LIVE. Bảng `PeWorkItemBudgets` GIỮ NGUYÊN là nguồn "bản cập nhật gần nhất" (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. Backfill phiếu DaDuyet hiện hữu = số live hiện tại (số gốc không khôi phục được — changelog attribution gap, honest-note). Nguồn yêu cầu: UAT anh Kiệt FDC 2026-07-16 17:24-17:27 (R1/R2/R3).
|
||||
|
||||
## ② Cách implement (ordered)
|
||||
1. **Domain** `src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs`: +11 props nullable `ApprovedBudget{ProInitial,ProAdjustment,CcmInitial,CcmAdjustment}Amount` decimal? · `ApprovedBudget{Pro,Ccm}Note` string? · `ApprovedBudgetPrev{Submitted,Selected}{Total decimal?,Count int?}` · `ApprovedBudgetSnapshotAt` DateTime?.
|
||||
2. **Config** `Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs`: HasPrecision(18,2) ×6 decimal + HasMaxLength(1000) ×2 note (pattern :115-116).
|
||||
3. **Mig 67** `dotnet ef migrations add AddPeApprovedBudgetSnapshot` (3-file rule): AddColumn ×11 + `Sql()` backfill idempotent `WHERE Phase=7 AND ApprovedBudgetSnapshotAt IS NULL` (LEFT JOIN pair IsDeleted=0 + OUTER APPLY lũy kế same-predicate Features :921-943; ActiveProvider SqlServer-guard mirror Mig 61/65) + Down DropColumn ×11.
|
||||
4. **Shared accumulator (chống mirror-drift):** extract lũy kế (peers submitted/selected + proposal) từ `GetPurchaseEvaluationDetailQuery` (:917-951) ra helper static `Application/PurchaseEvaluations/PeBudgetAccumulator.cs` — dùng chung GetDetail (live path) + service snapshot (runtime path). (Backfill SQL vẫn bản dịch SQL của cùng predicate — reviewer đối chiếu.)
|
||||
5. **Service** `Infrastructure/Services/PurchaseEvaluationWorkflowService.cs`: +`ApplyBudgetSnapshotOnFinalizeAsync(evaluation, ct)` (load pair AsNoTracking theo ProjectId×WorkItemId, null-safe; gọi accumulator; LUÔN overwrite; comment rule "mọi nhánh set DaDuyet PHẢI gọi") — gọi tại **4 site** ngay trước `Phase = DaDuyet`: :861 · :899 · :931 · :1053.
|
||||
6. **DTO + display-gate** `Application/PurchaseEvaluations/{Dtos/PurchaseEvaluationDtos.cs,PurchaseEvaluationFeatures.cs}`: `PeBudgetSummaryDto` +`bool BudgetFrozen` (append cuối :349); GetDetail :905-973 → nhánh `frozen` build DTO từ cột snapshot (CanEditPro/Ccm=false, lũy kế + notes từ snapshot, CurrentProposalTotal vẫn từ own quotes), else live như cũ.
|
||||
7. **Write-guards**: `PeWorkItemBudgetFeatures.cs` SetPeCcmBudgetPeriodHandler +Conflict terminal (:230+); `PurchaseEvaluationFeatures.cs` AdjustBudget +Conflict terminal trước admin-bypass (:419-422). KHÔNG đụng UpdatePeBudgetPro/Ccm (R2).
|
||||
8. **FE ×2 app**: types +`budgetFrozen: boolean` (fe-user :314+, fe-admin ~:312+ — file KHÔNG SHA-mirror, sửa từng cái); `PeDetailTabs.tsx` +badge frozen (SHA-mirror — sửa giống hệt 2 app, diff-check).
|
||||
9. **Tests** (mục I trên — test-before RED trước khi code fix, rule bug-fix=test-before).
|
||||
10. **Docs closeout**: STATUS.md (Mig 67, bảng giữ 89) + schema-diagram migration-table + gotchas nếu phát sinh + session log. slnx không đổi (không file .cs mới ngoài accumulator — VẪN check).
|
||||
|
||||
## ③ Checklist per-vai (acceptance ĐO ĐƯỢC)
|
||||
**implementer-backend**
|
||||
- [ ] Mig 67 3-file: `git status` Migrations/ = 3 file mới; `grep -c AddColumn <mig>.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.
|
||||
|
||||
Reference in New Issue
Block a user