wal: flush 20260717T1012
This commit is contained in:
@ -21,7 +21,7 @@
|
||||
- [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)
|
||||
- [x] INV-A (2-task ‖, wf_d9639faa-97a, 0-garble): T0 RED thật (`Failed 1/1` found 999tr @PeBudgetFreezeTests.cs:184, prod sạch — evidence sub-test-specialist-0.md) · FE 4 file (badge amber @ "Tổng hợp ngân sách trình ký" :1352, hash mirror `5534addb` ×2, npm build ×2 PASS) · errata spec: query = GetPurchaseEvaluationQuery (bundle prop BudgetSummary)
|
||||
- [ ] 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
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
> **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).
|
||||
> **Errata @INV-A (2026-07-17):** tên query detail-bundle ĐÚNG theo code = `GetPurchaseEvaluationQuery` (trả `PurchaseEvaluationDetailBundleDto.BudgetSummary`) — bản đầu spec ghi nhầm `GetPurchaseEvaluationDetailQuery` (test-specialist bắt khi viết T0; đã sửa toàn spec). Con số dòng `:905-973/:964` không đổi.
|
||||
|
||||
## ① Tính-năng / đề-bài
|
||||
|
||||
@ -21,7 +22,7 @@ Phiếu PE **Phase=DaDuyet bất biến số ngân sách** (R1 — anh Kiệt: "
|
||||
*(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ó).
|
||||
4. **Shared accumulator (chống drift 3-bản):** extract khối lũy kế + pair-load semantics từ `GetPurchaseEvaluationQuery` (`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).
|
||||
@ -29,7 +30,7 @@ Phiếu PE **Phase=DaDuyet bất biến số ngân sách** (R1 — anh Kiệt: "
|
||||
- **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ũ.
|
||||
- `GetPurchaseEvaluationQuery` (`: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]" }`.
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
# sub-implementer-frontend-1 — S133 STAGE A (FE types + badge frozen)
|
||||
|
||||
Role: implementer-frontend. Task: FE `budgetFrozen` field + badge (disjoint BE). 4 files only.
|
||||
|
||||
## Refuse self-check → PASS (proceed)
|
||||
- UX decision? NO — badge placement + style fully specified (pill cạnh header "Tổng hợp ngân sách trình ký", amber/slate rounded-full text-xs).
|
||||
- Schema design? NO — field `budgetFrozen: boolean` chốt sẵn spec ②.
|
||||
- BE touch? NO — chỉ FE 4 file.
|
||||
- First-time pattern? NO — pill badge proven (line 588/591/759 emerald "Đã ký"/"Đã duyệt").
|
||||
- Ambiguity? < 20% — spec chi tiết + MEMORY-PACK định vị chính xác.
|
||||
|
||||
## Recon (evidence file:line)
|
||||
- Baseline SHA both PeDetailTabs = `931025f240e04404a7d1821dbfd948f56da5eae5` (git hash-object) → khớp spec `931025f2`. 2 file byte-identical.
|
||||
- `PeBudgetSummary` type: fe-user `types/purchaseEvaluation.ts:314-333` · fe-admin `:312-331`. Kết thúc bằng `currentProposalTotal: number` + `}`. 2 file block NỘI-DUNG identical (chỉ lệch line-number → edit riêng, same old_string OK).
|
||||
- `bs = ev.budgetSummary` (`PeBudgetSummary|null`) @ PeDetailTabs.tsx:1226-1228. Null-guard `if(!bs) return` @:1298 → `bs` narrowed non-null tại render → `bs.budgetFrozen` typecheck OK.
|
||||
- Header render @ PeDetailTabs.tsx:1350-1352: `<div className="bg-[#1F7DC1] px-3 py-2 text-[12px] font-bold uppercase tracking-wide text-white">Tổng hợp ngân sách trình ký</div>`. className `bg-[#1F7DC1]...` LẶP @:1931 (winners section) → anchor phải gồm text "Tổng hợp ngân sách trình ký" (unique).
|
||||
- Pill pattern sync: `:588-594` header `flex items-center justify-between` + badge `inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-700`. Dùng amber variant cho frozen (blue header bar → amber pill đọc rõ).
|
||||
|
||||
## Plan 4 edit
|
||||
1. types fe-user + fe-admin: +`budgetFrozen: boolean` cuối type PeBudgetSummary (comment [S133]). Edit RIÊNG 2 file (same old_string).
|
||||
2. PeDetailTabs fe-user + fe-admin: header div → flex justify-between, wrap title `<span>`, +badge `{bs.budgetFrozen && ...}` amber pill "🔒 Ngân sách chốt tại thời điểm duyệt" (normal-case tracking-normal override header uppercase). Edit GIỐNG HỆT byte 2 file.
|
||||
|
||||
## Progress — ALL DONE ✅
|
||||
- [x] Edit types ×2: `budgetFrozen: boolean` @ fe-user types:333 · fe-admin types:331 (edit riêng, same old_string; comment [S133]).
|
||||
- [x] Edit PeDetailTabs ×2 (byte-identical): header div → `flex items-center justify-between gap-2`, title wrap `<span>`, +badge `{bs.budgetFrozen && (<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium normal-case tracking-normal text-amber-800">🔒 Ngân sách chốt tại thời điểm duyệt</span>)}` @ both :1352. `normal-case tracking-normal` override header `uppercase tracking-wide` để badge text đọc thường. Layout non-frozen giữ nguyên (1 span trong flex = left-align).
|
||||
- [x] git hash-object 2 PeDetailTabs EQUAL: `5534addb9a7887c00562a99ab1f52ed4ee77c0ca` (cả 2) — mirror giữ (baseline `931025f2` → post `5534addb`).
|
||||
- [x] grep budgetFrozen = 4 hit: fe-user types:333 + fe-user PeDetailTabs:1352 + fe-admin types:331 + fe-admin PeDetailTabs:1352.
|
||||
- [x] npm build fe-user EXIT 0 — 1939 modules, 19.84s, `index-DxCKQyJZ.js` 1534.07kB. Warn pre-existing (@import order + >500KB chunk + INEFFECTIVE_DYNAMIC_IMPORT realtime.ts).
|
||||
- [x] npm build fe-admin EXIT 0 — 1950 modules, 8.75s, `index-DTUy4uPK.js` 1627.72kB. Warn pre-existing (same 3).
|
||||
|
||||
## Isolation audit (git status --short)
|
||||
- Mine (4 target FE): fe-admin/fe-user × {PeDetailTabs.tsx, types/purchaseEvaluation.ts} — M ✓
|
||||
- Run-folder: sub-implementer-frontend-1.md — ?? ✓
|
||||
- NO src/Backend touched (grep Backend → none) ✓
|
||||
- ⚠️ `.claude/governance/.session-counter.json` M = harness/session-tooling artifact, KHÔNG do tôi ghi (mọi Bash tôi chạy read-only: build→dist/ gitignored, hash-object, grep, status). Flag để em main không quy cho STAGE A.
|
||||
|
||||
## Note cho STAGE B (BE — implementer-backend)
|
||||
- FE khai `budgetFrozen: boolean` (non-null) theo DTO chốt spec ②. BE PeBudgetSummaryDto phải append `bool BudgetFrozen = false` → serialize camelCase `budgetFrozen`. Field CHƯA ship BE → runtime `bs.budgetFrozen` undefined = falsy → badge ẩn an-toàn (KHÔNG vỡ UI, KHÔNG runtime error). Khi BE ship: DaDuyet+snapshot → true → badge hiện.
|
||||
@ -0,0 +1,62 @@
|
||||
# sub-test-specialist-0 — STAGE A T0 REPRODUCE (PE budget-freeze @DaDuyet)
|
||||
|
||||
**Role:** test-specialist · **Task:** viết ĐÚNG 1 test T0 REPRODUCE (test-before cho bug "phiếu DaDuyet đổi số ngân sách theo bảng LIVE"). Kỳ vọng **RED** trên code hiện tại.
|
||||
|
||||
**Boundary:** CHỈ ghi `tests/SolutionErp.Infrastructure.Tests/Services/PeBudgetFreezeTests.cs` + file này. KHÔNG chạm production (`src/Backend`, `fe-*`), KHÔNG sửa test cũ.
|
||||
|
||||
---
|
||||
|
||||
## 1. Investigation (code = single source of truth)
|
||||
|
||||
**Bug cơ chế (verified qua đọc code):**
|
||||
- Query `GetPurchaseEvaluationQuery` (KHÔNG phải `GetPurchaseEvaluationDetailQuery` — tên trong SPEC hơi lệch; query THẬT = `GetPurchaseEvaluationQuery`, trả `PurchaseEvaluationDetailBundleDto`). Handler `PurchaseEvaluationFeatures.cs:853`.
|
||||
- `PurchaseEvaluationFeatures.cs:911-912` — `pairRec` load **LIVE** từ `db.PeWorkItemBudgets.FirstOrDefaultAsync(b => b.ProjectId == e.ProjectId && b.WorkItemId == wiKey)` — KHÔNG phân biệt Phase.
|
||||
- `:964-972` — `PeBudgetSummaryDto` build từ `pairRec` (positional 17-field). Field cần assert:
|
||||
- `ProInitialAmount` = arg 16 ← `pairRec?.ProInitialAmount` (DTO def `PurchaseEvaluationDtos.cs:348`)
|
||||
- `InitialAmount` = arg 4 ← `pairRec?.InitialAmount` (CCM ban hành, DTO def `:335`)
|
||||
- Property bundle: `PurchaseEvaluationDetailBundleDto.BudgetSummary` (`PurchaseEvaluationDtos.cs:260`).
|
||||
- ⇒ mọi phiếu cùng cặp (ProjectId×WorkItemId) đọc CHUNG record live. Phiếu DaDuyet đổi số theo bản cập nhật mới nhất = BUG.
|
||||
|
||||
**Finalize path (đưa phiếu tới DaDuyet ĐÚNG cách — KHÔNG seed thẳng):**
|
||||
- `PurchaseEvaluationWorkflowService.cs:855-873` nhánh level-finalize: `if (matchingLevel.AllowApproverFinalize && applyLevelFinalize)` → `:861 evaluation.Phase = DaDuyet`. Đây là 1 trong 4 site DaDuyet (SPEC: :861/:899/:931/:1053). Hiện KHÔNG có snapshot ngân sách.
|
||||
- Mirror `PeApproverFinalizeTests` test #5 (AtLastSlot): workflow V2 1 Bước 1 Cấp, `AllowApproverFinalize=true`, approver duyệt + giá chốt (`approvedPriceAmount=400tr, source="Ncc"` — `ValidApprovedPriceSources` :954) → TỰ DaDuyet.
|
||||
- `TransitionAsync` sig `:37-52` (14 param, `applyLevelFinalize=true` default `:51`).
|
||||
|
||||
**Commands đổi pair (SAU khi duyệt — KHÔNG có phase-guard nên chạy được mọi phase):**
|
||||
- `UpdatePeBudgetProCommand(Guid PeId, decimal? ProInitialAmount, decimal? ProAdjustmentAmount, string? ProNote)` handler `(IApplicationDbContext, ICurrentUser)` — `PeWorkItemBudgetFeatures.cs:63/80`. Role-gate Admin|Procurement.
|
||||
- `UpdatePeBudgetCcmCommand(Guid PeId, decimal? InitialAmount, decimal? AdjustmentAmount, string? CcmNote)` handler `:134/151`. Role-gate Admin|CostControl. Absolute-set (null=clear). EnsureTrackedAsync reuse pair đã seed.
|
||||
|
||||
**Helpers reuse:** `IdentityFixture.CreateUserAsync(email, fullName, deptId, roles[])` · `TestCurrentUser(userId, fullName?, email?, params roles)` (Common) · `FixedDateTime` (Common/SqliteDbFixture.cs:71) · `NoOpNotificationService` (Services namespace, PurchaseEvaluationWorkflowServiceGuardTests.cs:188).
|
||||
|
||||
## 2. Test design (1 [Fact] T0 REPRODUCE)
|
||||
- (a) seed Project + WorkItem + PeWorkItemBudget pair (ProInitial=100tr, CCM InitialAmount=200tr).
|
||||
- (b) build PE ChoDuyet gắn cặp + workflow finalize; đưa tới DaDuyet QUA `svc.TransitionAsync` (approver duyệt, applyLevelFinalize default true, giá chốt 400tr/"Ncc").
|
||||
- (c) đổi pair LIVE: PRO 100tr→999tr (`UpdatePeBudgetProCommand`), CCM 200tr→888tr (`UpdatePeBudgetCcmCommand`), Admin. + sanity assert record live ĐÃ đổi (chống vacuous).
|
||||
- (d) query `GetPurchaseEvaluationQuery` bằng **drafter** (user thường, isDrafter=true qua authz `:874/:891`) → assert `BudgetSummary.ProInitialAmount==100tr` && `InitialAmount==200tr`.
|
||||
- Kỳ vọng **RED** (code hiện tại đọc live → 999tr/888tr).
|
||||
|
||||
## 3. Kết quả chạy — ✅ RED (đúng kỳ vọng T0)
|
||||
|
||||
**Lệnh:** `dotnet test SolutionErp.slnx --filter "FullyQualifiedName~PeBudgetFreeze" --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1`
|
||||
|
||||
**Build:** succeeded (test compile sạch → mọi API call/using đúng). **Kết quả:** `Failed! - Failed: 1, Passed: 0, Skipped: 0, Total: 1, Duration: 7 s`.
|
||||
|
||||
**Assert-failure nguyên văn (EVIDENCE):**
|
||||
```
|
||||
[xUnit.net 00:00:10.89] SolutionErp.Infrastructure.Tests.Services.PeBudgetFreezeTests.DaDuyet_BudgetSummary_KeepsSnapshotValue_DoesNotFollowLivePairUpdate [FAIL]
|
||||
Failed SolutionErp.Infrastructure.Tests.Services.PeBudgetFreezeTests.DaDuyet_BudgetSummary_KeepsSnapshotValue_DoesNotFollowLivePairUpdate [7 s]
|
||||
Error Message:
|
||||
Expected s.ProInitialAmount to be 100000000M because phiếu DaDuyet phải GIỮ ngân sách PRO tại thời điểm duyệt (100tr), KHÔNG chạy theo record live (999tr), but found 999000000.0M (difference of 899000000.0).
|
||||
Stack Trace:
|
||||
... at SolutionErp.Infrastructure.Tests.Services.PeBudgetFreezeTests.DaDuyet_BudgetSummary_KeepsSnapshotValue_DoesNotFollowLivePairUpdate() in ...\PeBudgetFreezeTests.cs:line 184
|
||||
Failed! - Failed: 1, Passed: 0, Skipped: 0, Total: 1, Duration: 7 s - SolutionErp.Infrastructure.Tests.dll (net10.0)
|
||||
```
|
||||
|
||||
**Đọc kết quả:**
|
||||
- Fail tại `PeBudgetFreezeTests.cs:184` = assert snapshot PRO `s.ProInitialAmount == 100tr` → **found 999tr** (record live). Bug tái hiện chính xác: phiếu DaDuyet chạy theo bảng live.
|
||||
- 2 assert TRƯỚC ĐÃ PASS (nếu fail sẽ báo dòng khác): `pe.Phase == DaDuyet` (finalize path chạy đúng, KHÔNG seed thẳng) + `liveRec.ProInitialAmount == 999tr`/`InitialAmount == 888tr` (update ăn → assert (d) không vacuous). Chứng minh flow đầy đủ, CHỈ thiếu hành-vi snapshot.
|
||||
- Assert CCM (`InitialAmount == 200tr`, dòng sau) chưa chạy tới vì FluentAssertions throw ở assert PRO đầu tiên — bình thường cho RED.
|
||||
- `Domain.Tests`: "No test matches filter" = đúng (test nằm ở Infrastructure.Tests). KHÔNG phải lỗi.
|
||||
|
||||
**Verdict:** T0 RED PASS-as-designed. KHÔNG PREMISE-FAIL (không GREEN bất ngờ). KHÔNG chạm production. Sẽ GREEN sau khi implementer thêm snapshot ngân sách tại 4 site DaDuyet + display-gate đọc cột snapshot.
|
||||
**Baseline:** 509 → 510 test (test này RED tới khi fix land — test-before đúng quy trình bug-fix docs/rules.md §7).
|
||||
Reference in New Issue
Block a user