[CLAUDE] PurchaseEvaluation: luy ke TAM TINH vs CHINH XAC — ComputePendingAsync + 5-field DTO + FE 2-app sub-dong amber/banner man duyet + D4 audit-note (UAT anh Kiet 15-07, 0 mig, test 520→528)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m27s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m27s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,32 @@
|
||||
# run — 2026-07-17-S134-pe-luyke-tam-tinh
|
||||
|
||||
- **engine:** `/fable-real investigator-codebase` (H21 lệnh-A, alias "invest" anh gõ) → lead verify + ghi spec 3-heading → `/fable-real reviewer` (alias "review" anh gõ — soi "cách làm chi tiết - checklist") → **hmw implement Opus 4.8 MAX** (anh chỉ định "Sau đó hwm cho Opus 4.8 MAX xử lý nhé") → reviewer pre-commit → deploy + cicd.
|
||||
- **goal:** UAT anh Kiệt FDC (chat 15/07, chốt 16:10 **"Vậy em xử ký theo hướng này đi"**): lũy kế PE hiện chỉ bắt phiếu **DaDuyet** (nguyên tắc Kenny S117 "duyệt rồi mới bắc") → phiếu chưa duyệt KHÔNG góp lũy kế phiếu sau. Vì **cung đặc thù** (phiếu sau trình khi phiếu trước chưa duyệt), anh Kiệt cần: **vẫn cho bắt lên và tính toán, note màu** để biết phiếu trước chưa duyệt, để bước duyệt cuối (CEO/người duyệt thay) biết **cần duyệt phiếu trước phiếu này**. Hướng chốt (em đề xuất, anh Kiệt duyệt ♥): chia 2 số — **"lũy kế TẠM TÍNH" = tất cả phiếu** (kể cả chưa duyệt, đánh dấu màu phần từ phiếu-chưa-duyệt) ⟂ **"lũy kế CHÍNH XÁC" = chỉ phiếu đã duyệt** (semantics hiện tại GIỮ NGUYÊN). Informational — KHÔNG hard-block duyệt (xác nhận ở invest).
|
||||
- **session:** S134 · lead: Fable 5 (owner-choice) · HMW consent: anh chỉ định hmw trong lệnh.
|
||||
- **Tương tác S133 (freeze-NS):** phiếu DaDuyet đọc snapshot `ApprovedBudgetPrev*` bất biến — tạm-tính dự kiến CHỈ có nghĩa trên phiếu chưa-terminal; KHÔNG đụng snapshot đã chốt (invest xác nhận).
|
||||
|
||||
## taskList snapshot
|
||||
|
||||
1. `[fable-real:invest-luyke-tam-tinh]` role=`investigator-codebase` · **model=`fable`** (spawn-param THẮNG frontmatter-pin — H23 đo S126) — single deep-pass:
|
||||
- Predicate lũy kế hiện tại: 3 bản PHẢI KHỚP (accumulator `PeBudgetAccumulator.cs` · live `PurchaseEvaluationFeatures.cs:909/:959` · snapshot `PurchaseEvaluationWorkflowService.cs:1013` · backfill SQL Mig 67 `:99`) — filter Phase nằm đâu, file:line.
|
||||
- FE matrix "Tổng hợp ngân sách trình ký" rows 5/6 (`PeDetailTabs.tsx:1318-1590` ×2 app) + types `:325/:327` + màn duyệt (PeWorkflowPanel/inbox) — chỗ chèn số tạm-tính + note màu.
|
||||
- Phase taxonomy đầy đủ → đề xuất tập "tạm tính" (TuChoi? Nháp? TraLai?) + trade-off.
|
||||
- Danh sách "phiếu trước cùng cặp CHƯA duyệt" (mã + phase) — khả thi trong projection? cần endpoint mới?
|
||||
- Edge: WorkItemId null · multi-winner D4 · giá âm S116 · Min-Max S117 · PRO-split · CcmBudgetPeriod · budgetFrozen S133.
|
||||
- Propose spec 3-mục + checklist per-vai acceptance ĐO ĐƯỢC + phân rã BE/FE/test cho hmw.
|
||||
- 🔴 Ghi-đĩa-TRONG-lúc-làm: `sub-investigator-codebase-0.md` (chống #53 — S133 file 22.5KB cứu trọn).
|
||||
|
||||
## taskList snapshot — implement (append @spec-v2 chốt, sau fable-real review GO_WITH_FIXES)
|
||||
|
||||
- INV-2.A `[impl:be-full]` implementer-backend — spec ② items 1-4 (ComputePendingAsync + DTO + live-branch fill + D4 changelog-in-helper shape M1) + checklist A1-A6.
|
||||
- INV-2.B `[impl:fe-2app]` implementer-frontend — spec ② items 5-7 (types ×2 KHÔNG-mirror + PeDetailTabs sub-dòng amber SHA-mirror + PeWorkflowPanel banner gate M2 + dialog-block) + checklist B1-B4.
|
||||
- INV-3.C `[impl:test-full]` test-specialist — checklist C1-C9 (pattern PeBudgetFreezeTests seed :277-332).
|
||||
|
||||
## Stages
|
||||
|
||||
- [x] 1. invest fable-real — #53 garble return, **sub-file 8-phần cứu trọn** (157 dòng COMPLETE); phát hiện lật-khung: row1 ĐÃ gồm ChoDuyet, gap thật = row2
|
||||
- [x] 2. lead verify 7/7 claim + 2 errata (path `Dtos/`, hash-value) + chốt D1-D5 → ghi spec 3-heading
|
||||
- [x] 3. fable-real reviewer — #53 garble return, sub-file cứu (lần 3) · **GO_WITH_FIXES 0C/2M/9n** (M1 changelog-shape đầy-đủ Action=Update né #49 · M2 banner gate phase===ChoDuyet; D4-seeder CLEAR — set trực tiếp không gọi helper) → lead áp TRỌN → spec v2
|
||||
- [!] 4. hmw implement Opus 4.8 MAX — INV-2 [A‖B] đang chạy (args.run RUN-TRACE)
|
||||
- [ ] 5. INV-3 [TEST-C] + test full + npm ×2 + reviewer pre-commit → commit/push + cicd verify
|
||||
- [ ] 6. docs closeout → /pause (anh dặn)
|
||||
@ -0,0 +1,109 @@
|
||||
# SPEC — PE lũy kế TẠM TÍNH vs CHÍNH XÁC (17-07-2026, run S134)
|
||||
|
||||
> Engine: `/fable-real investigator-codebase` propose (sub-investigator-codebase-0.md, 8 phần) → **lead verified 7/7 claim + 2 errata** (path DTO = `PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs`; hash-value dùng số lead đo: PeDetailTabs 2-app IDENTICAL `5534addb`, PeWorkflowPanel IDENTICAL `a83a58e1`, types KHÁC `2bf773`≠`ab4a2e`) → lead ghi. Decisions D1-D5 lead chốt (flag anh ở báo cáo cuối).
|
||||
|
||||
## ① Tính năng / đề bài
|
||||
|
||||
UAT anh Kiệt FDC (chat 15/07, chốt 16:10 "Vậy em xử ký theo hướng này đi"): vì **"cung đặc thù"** — phiếu sau trình khi phiếu trước cùng gói thầu (ProjectId × WorkItemId) CHƯA duyệt — cần:
|
||||
|
||||
- **Lũy kế TẠM TÍNH**: "bắt lên và tính toán" cả phần góp từ phiếu chưa duyệt; phần này **nhận diện được** (màu/badge riêng — anh Kiệt: "note nó màu gì đó để biết rằng phiếu trước đó chưa duyệt").
|
||||
- **Lũy kế CHÍNH XÁC**: semantics HIỆN TẠI — **GIỮ NGUYÊN 100%, không đổi 1 số nào** (nguyên tắc Kenny S117 "duyệt rồi mới bắc").
|
||||
- **Người duyệt cuối (CEO/người duyệt thay) NHÌN THẤY** danh sách "phiếu trước chưa duyệt [mã — trạng thái]" ngay lúc duyệt — **informational nudge, KHÔNG hard-block** (D3).
|
||||
|
||||
**Ground-truth thu hẹp đề (invest verified):** row 1 "NS trình duyệt trước" = `Phase ∈ {ChoDuyet, DaDuyet}` — **ĐÃ gồm phiếu đang trình**. Gap thật: **row 2 "Kỳ trước đã chọn thầu"** (`DaDuyet + IsWinner` only → phiếu chưa duyệt đã chọn NCC không góp) + row 6 dẫn xuất; row 1 thiếu TraLai/legacy/nháp.
|
||||
|
||||
**Predicate hiện tại (bản RUNTIME duy nhất — `PeBudgetAccumulator.ComputeAsync`, lead re-đọc):**
|
||||
- peers = cùng `(ProjectId, WorkItemId)`, `Id != this`, `CreatedAt < this` (`PeBudgetAccumulator.cs:38-40`)
|
||||
- PrevSubmitted = peers `Phase ∈ {ChoDuyet=10, DaDuyet=7}` → Count + SUM(`BudgetPeriodAmount ?? 0`) (`:42-48`)
|
||||
- PrevSelected = peers `DaDuyet && Suppliers.Any(IsWinner)` → Count + SUM(quote `IsSelected` `ThanhTien`) (`:50-60`)
|
||||
- Live-path gọi helper (`PurchaseEvaluationFeatures.cs:962`) · snapshot-path gọi helper (`PurchaseEvaluationWorkflowService.cs:1018`, 5 call-site) · backfill Mig 67 = SQL one-time đã chạy. **⇒ KHÔNG đụng `ComputeAsync` = snapshot/backfill/chính-xác bất động.**
|
||||
|
||||
**Phase enum:** DangSoanThao=1 · DaDuyet=7 · ChoDuyet=10 · TraLai=98 · TuChoi=99 (+legacy 2-6 deprecated, Dev DB còn row sống phase 3/6 → predicate viết dạng LOẠI-TRỪ).
|
||||
|
||||
**Decisions CHỐT (D1-D5):**
|
||||
- **D1** Nháp (DangSoanThao=1): vào **LIST cảnh báo** (mã + badge "Nháp") nhưng **KHÔNG cộng delta tiền** (S89 nháp riêng-tư `Features.cs:893-898` + số chưa chốt = nhiễu). ⚑ flag anh.
|
||||
- **D2** Delta row 1 LÀM (TraLai + legacy 2-6 có BudgetPeriodAmount) — cùng query, nhất quán "tất cả".
|
||||
- **D3** Plain-info: KHÔNG disable nút, KHÔNG hard-block BE.
|
||||
- **D4** Audit-note Changelog lúc finalize khi còn pending — **1 chỗ duy nhất trong helper snapshot** (KHÔNG đụng 5 call-site).
|
||||
- **D5** TuChoi=99 exclude tuyệt đối (không bao giờ thành "chính xác").
|
||||
|
||||
**Tập predicate MỚI (chốt):**
|
||||
- `pendingPriorPes` (list cảnh báo) = peers `Phase NOT IN (7, 99)` → `{Id, MaPhieu, Phase}` order CreatedAt.
|
||||
- `pendingSelectedTotal/Count` (delta row 2/6) = peers `Phase NOT IN (1, 7, 99)` && `Suppliers.Any(IsWinner)` → Count + SUM(quote IsSelected ThanhTien) — mirror shape `:50-60`.
|
||||
- `pendingSubmittedTotal/Count` (delta row 1/5) = peers `Phase NOT IN (1, 7, 10, 99)` → Count + SUM(`BudgetPeriodAmount ?? 0`) (10/7 đã nằm trong row 1 chính xác).
|
||||
- Phiếu FROZEN (`budgetFrozen=true`): pending = 0/empty — **R1-S133 phiếu DaDuyet bất biến, KHÔNG số động mới**.
|
||||
|
||||
## ② Cách implement
|
||||
|
||||
**BE — 0 migration, 0 endpoint mới, 0 đụng `ComputeAsync`/snapshot-path/backfill:**
|
||||
|
||||
1. **`PeBudgetAccumulator.cs`** — THÊM (không sửa cũ):
|
||||
- `public readonly record struct PePendingAccumulation(decimal PendingSubmittedTotal, int PendingSubmittedCount, decimal PendingSelectedTotal, int PendingSelectedCount, IReadOnlyList<PePendingPriorPeDto> PriorPes);`
|
||||
- `public static async Task<PePendingAccumulation> ComputePendingAsync(IApplicationDbContext db, Guid projectId, Guid workItemId, Guid peId, DateTime peCreatedAt, CancellationToken ct)` — peers window Y HỆT `:38-40` (`CreatedAt < this` — GIỮ mốc, khác window = 2 tập peer hết so sánh được); 3 predicate theo ①; list order `CreatedAt`.
|
||||
2. **`Dtos/PurchaseEvaluationDtos.cs`** — `public record PePendingPriorPeDto(Guid Id, string? MaPhieu, int Phase);` (N2 — entity `MaPhieu` nullable `PurchaseEvaluation.cs:10`, FE fallback "(chưa có mã)") + `PeBudgetSummaryDto` append CUỐI sau `BudgetFrozen` (pattern S133 an-toàn): `decimal PendingSubmittedTotal = 0, int PendingSubmittedCount = 0, decimal PendingSelectedTotal = 0, int PendingSelectedCount = 0, IReadOnlyList<PePendingPriorPeDto>? PendingPriorPes = null` — 2 construction site set EXPLICIT: frozen `:946` → `0,0,0,0,null` · live `:975` → fill từ `ComputePendingAsync`.
|
||||
3. **`PurchaseEvaluationFeatures.cs` nhánh LIVE (`:957-984`)** — gọi `ComputePendingAsync` sau `ComputeAsync`, fill 5 field. Nhánh FROZEN (`:930-955`): defaults.
|
||||
4. **D4 audit-note — TRONG `ApplyBudgetSnapshotOnFinalizeAsync` (`PurchaseEvaluationWorkflowService.cs:998-1041`), 1 chỗ, TRONG nhánh `WorkItemId != null`:** sau khi set snapshot, gọi `ComputePendingAsync` (1 lần); nếu `PriorPes.Count > 0` → **`.Add(...)`** (sync — pattern codebase `LogTransitionAsync:1145`; KHÔNG `AddAsync`) 1 row với **SHAPE ĐẦY ĐỦ (M1 — KHÔNG thêm param vào signature helper, giữ A6 0-đổi-call-site)**:
|
||||
```csharp
|
||||
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog {
|
||||
PurchaseEvaluationId = evaluation.Id,
|
||||
EntityType = PurchaseEvaluationEntityType.Workflow,
|
||||
Action = ChangelogAction.Update, // KHÔNG Transition — caller đã ghi row Transition riêng (LogTransitionAsync:1128-1155); 2 row Transition = UI audit hiểu nhầm 2 lần chuyển phase (#49)
|
||||
PhaseAtChange = PurchaseEvaluationPhase.DaDuyet,
|
||||
UserId = null, UserName = "Hệ thống", // helper không có actor — audit actor tái dựng từ row Transition cùng batch
|
||||
Summary = "Duyệt khi còn phiếu trước cùng gói chưa duyệt",
|
||||
ContextNote = "[S134 pending-prior] Duyệt khi N phiếu trước cùng gói chưa duyệt: A/0xx (Đã gửi duyệt), …",
|
||||
});
|
||||
```
|
||||
KHÔNG SaveChanges trong helper (caller lo — V2/V1 `TransitionAsync:301`, admin-override `:316`). KHÔNG đụng 5 call-site. (Chi phí: ~3 query `ComputePendingAsync` mỗi finalize — N4, chấp nhận; tần suất thấp.)
|
||||
|
||||
**FE — 2 app (fe-user + fe-admin):**
|
||||
|
||||
5. **types `purchaseEvaluation.ts` ×2 app** (⚠️ 2 file KHÁC nhau `2bf773`≠`ab4a2e` — edit block `PeBudgetSummary` TỪNG app, KHÔNG copy nguyên file): +5 field `pendingSubmittedTotal: number; pendingSubmittedCount: number; pendingSelectedTotal: number; pendingSelectedCount: number; pendingPriorPes: { id: string; maPhieu: string | null; phase: number }[] | null`.
|
||||
6. **`PeDetailTabs.tsx`** (2-app SHA-mirror `5534addb` — sửa 1 nội dung áp 2 file, `git hash-object` phải IDENTICAL sau sửa):
|
||||
- Gate TOÀN BỘ render tạm-tính: `!bs.budgetFrozen && (bs.pendingSubmittedCount > 0 || bs.pendingSelectedCount > 0 || (bs.pendingPriorPes?.length ?? 0) > 0)`.
|
||||
- Row 2 (:1503-vùng): sub-dòng **amber** khi `pendingSelectedCount > 0`: `Tạm tính: {fmt(row2 + delta2)} (+{fmt(delta2)} từ {pendingSelectedCount} phiếu chưa duyệt)` — **N8: n = count TƯƠNG ỨNG delta, KHÔNG dùng `pendingPriorPes.length`** (list ⊇ delta-set — dùng nhầm sẽ "từ 3 phiếu" khi delta chỉ từ 1).
|
||||
- Row 6 (:1579-1586): sub-dòng amber `row6_tạm = row6 + delta2` (n = pendingSelectedCount).
|
||||
- Row 1 (:1498-vùng): sub-dòng amber khi `pendingSubmittedCount > 0` (+delta1, n = pendingSubmittedCount); Row 5 (:1569-1578) per-cột `r5_tạm = r5 + delta1`.
|
||||
- Dưới matrix: danh sách pending `{maPhieu ?? "(chưa có mã)"} — badge phase-label màu` — **N3: dùng map sẵn có `PurchaseEvaluationPhaseLabel`** (types cả 2 app; PeWorkflowPanel đang dùng `:465`), màu ChoDuyet=amber · TraLai=orange · Nháp=gray · legacy=amber. **N9: plain-text + badge, KHÔNG render link** (approver click link phiếu Nháp → 403 S89 guard `Features.cs:893-898` → toast lỗi khó hiểu). Số tạm-tính LUÔN kèm nhãn "Tạm tính" — không bao giờ thay số chính xác.
|
||||
7. **`PeWorkflowPanel.tsx`** (2-app SHA-mirror `a83a58e1` — mirror y vậy):
|
||||
- **Banner amber** cạnh V2-banner (:437-vùng) khi `(evaluation.budgetSummary?.pendingPriorPes?.length ?? 0) > 0 && !evaluation.budgetSummary?.budgetFrozen && !readOnly && evaluation.phase === 10 /* ChoDuyet — M2: phiếu Nháp/TraLai/TuChoi đi nhánh live cũng có pending fill, banner "cần duyệt phiếu trước" chỉ có nghĩa khi phiếu ĐANG chờ duyệt */`: `⚠ {n} phiếu trước cùng gói thầu CHƯA duyệt: A/012 (Đã gửi duyệt), … — cần duyệt phiếu trước trước phiếu này` (n = `pendingPriorPes.length` — đây là LIST, đúng nghĩa banner).
|
||||
- **Block NHẮC LẠI trong Dialog duyệt** khi `isApproveAction` (vùng :509-514, TRÊN bộ chọn giá — mọi cấp duyệt thấy; bước cuối `shouldPickPrice :510` là tập con nên chắc chắn thấy trước "Xác nhận"). **KHÔNG disable nút Xác nhận** (D3).
|
||||
|
||||
**KHÔNG đụng:** `ComputeAsync` · snapshot 11 cột · Mig 67 backfill · frozen branch số cũ · submit-guard · HĐ GiaTri · multi-winner derive (#81) · endsBeforeCeo.
|
||||
|
||||
## ③ Checklist per-vai + acceptance ĐO ĐƯỢC
|
||||
|
||||
**implementer-backend (IMPL-A):**
|
||||
- [ ] A1 `ComputePendingAsync` + 2 record mới; acceptance: `git diff` vùng `ComputeAsync` thân method = **0 dòng đổi**; `dotnet build SolutionErp.slnx` 0 error/0 warning mới.
|
||||
- [ ] A2 DTO append-cuối + 2 construction site explicit; acceptance: grep `PendingPriorPes` đúng 2 site set + build xanh.
|
||||
- [ ] A3 Live-branch fill; acceptance: **dựng data trước — Dev DB hiện 0 cặp multi-phiếu (N5): tạo 2 phiếu cùng Dự án × Hạng mục (qua API/UI hoặc seed test), phiếu A ChoDuyet + winner + quote IsSelected** → GET phiếu B: JSON `pendingSelectedTotal` = SUM IsSelected của A, `pendingPriorPes` chứa `{maPhieu A, phase:10}`.
|
||||
- [ ] A4 Frozen → defaults; acceptance: phiếu DaDuyet+SnapshotAt → `pendingPriorPes` null/[], 4 số = 0, JSON số chính-xác Y NGUYÊN trước-sau (R1-S133).
|
||||
- [ ] A5 KHÔNG migration; acceptance: `git status` — `ApplicationDbContextModelSnapshot.cs` + `Migrations/` **0 file đổi**.
|
||||
- [ ] A6 D4 changelog-note 1-chỗ-trong-helper; acceptance: finalize phiếu có pending → +1 row `PurchaseEvaluationChangelogs` ContextNote chứa `[S134 pending-prior]` + mã phiếu; finalize KHÔNG pending → 0 row mới loại này; **0 đổi 5 call-site** (`git diff` các vùng `:311/:865/:904/:937/:1112` chỉ được phép 0-dòng).
|
||||
**implementer-frontend (IMPL-B, ×2 app):**
|
||||
- [ ] B1 types ×2; acceptance: `grep -c pendingPriorPes fe-user/src/types/purchaseEvaluation.ts fe-admin/src/types/purchaseEvaluation.ts` = 1 mỗi file.
|
||||
- [ ] B2 PeDetailTabs sub-rows + list + gate frozen; acceptance: `git hash-object` 2 file IDENTICAL; string "Tạm tính" xuất hiện trong source; phiếu frozen KHÔNG render (gate đầu nhánh).
|
||||
- [ ] B3 PeWorkflowPanel banner + dialog-block; acceptance: hash 2 file IDENTICAL; nút Xác nhận KHÔNG thêm điều kiện disable (`git diff` vùng disabled-condition = 0).
|
||||
- [ ] B4 `npm run build` fe-user + fe-admin đều PASS.
|
||||
**test-specialist (TEST-C — pattern seed `PeBudgetFreezeTests.cs:277-306/:311-332`, CreatedAt override thủ công `:332`):**
|
||||
- [ ] C1 peer ChoDuyet+IsWinner+IsSelected → delta2 = tổng ThanhTien; có mặt trong PriorPes.
|
||||
- [ ] C2 peer TuChoi → delta 0 + KHÔNG trong list; peer DaDuyet → delta 0 (bên chính xác) + KHÔNG trong list.
|
||||
- [ ] C3 peer Nháp → TRONG list, delta tiền = 0 (D1); peer TraLai có BudgetPeriodAmount → delta1 đúng (D2).
|
||||
- [ ] C4 peer-window: peer `CreatedAt > this` → loại hoàn toàn (mirror `:435-451`).
|
||||
- [ ] C5 quote ÂM trong delta2 (signed-sum S116).
|
||||
- [ ] C6 REGRESSION: `ComputeAsync` số cũ Y NGUYÊN khi thêm peer non-terminal (chính-xác không đổi).
|
||||
- [ ] C7 frozen phiếu → DTO pending 0/empty.
|
||||
- [ ] C8 D4: finalize có pending → changelog row đúng format; không pending → không row.
|
||||
- [ ] C9 `dotnet test SolutionErp.slnx` FULL PASS (520 + N mới, 0 fail).
|
||||
|
||||
**Phân rã hmw staged:** INV xong (sub-file) → **INV-2 = [IMPL-A ‖ IMPL-B]** (song song — DTO shape + JSON field names chốt sẵn trong spec) → **INV-3 = [TEST-C]** (sau A land). Lead verify giữa các invocation + reviewer pre-commit cuối.
|
||||
|
||||
## Edge cases (phủ trong implement + test)
|
||||
1. WorkItemId null → `budgetSummary` null toàn cục (gate `:912`) — tạm-tính không tồn tại, banner "gắn hạng mục" giữ.
|
||||
2. Multi-winner D4/IsSelected: predicate mirror `:50-60` — 0 quy ước mới.
|
||||
3. Giá âm: SUM signed nguyên trạng, delta có thể âm — render như mọi số tiền.
|
||||
4. Min–Max/ApprovedPrice: KHÔNG feed lũy kế — orthogonal.
|
||||
5. PRO-split/CcmBudgetPeriodAmount: giữ nguyên quy ước row1 = `BudgetPeriodAmount` only (drift-vector sẵn có S133 — NGOÀI scope).
|
||||
6. Own-phase terminal/nháp (TuChoi/TraLai/DangSoanThao): matrix VẪN hiển thị tạm-tính (thông tin); **banner PeWorkflowPanel ẨN bằng gate tường minh `evaluation.phase === 10` (M2 — KHÔNG tự ẩn; phiếu non-ChoDuyet vẫn có pending fill từ nhánh live)**; dialog-block tự gate qua `isApproveAction`.
|
||||
7. Legacy phase 2-6 còn row sống (Dev {3,6}): predicate loại-trừ tự cover.
|
||||
8. **N7 (pre-existing, ghi nhận để không ngạc nhiên UAT):** phiếu DaDuyet mà SnapshotAt null (`Features.cs:926-928` fallback defense-in-depth) → đi nhánh live → tạm-tính HIỆN trên phiếu DaDuyet dị dạng — sau backfill Mig 67 + 5 site + seeder, lý thuyết 0 row loại này; KHÔNG fix trong scope.
|
||||
@ -0,0 +1,35 @@
|
||||
# sub-implementer-backend-0 — IMPL-A BE lũy kế TẠM TÍNH (S134)
|
||||
|
||||
> Ghi-đĩa-trong-lúc-làm (chống #53). Append từng bước. 4 file code + build verify.
|
||||
|
||||
## Scope (KHÔNG ĐỤNG)
|
||||
ComputeAsync thân · snapshot 11 cột ApprovedBudget* · Mig 67/Migrations/ModelSnapshot · frozen số cũ · submit-guard :206 · fe-*/** · tests/** · KHÔNG commit/push/git-add.
|
||||
|
||||
## Baseline (đo trước khi sửa)
|
||||
- `git status --porcelain` = CLEAN (branch main).
|
||||
- DTO namespace = `SolutionErp.Application.PurchaseEvaluations.Dtos` (:4) ; accumulator namespace = `SolutionErp.Application.PurchaseEvaluations` → CẦN thêm `using ...Dtos;` vào accumulator.
|
||||
- `PurchaseEvaluation.MaPhieu` = `string?` (:10) → DTO PePendingPriorPeDto MaPhieu nullable ✓ (khớp reviewer N2).
|
||||
- Phase enum: DangSoanThao=1 · legacy 2-6 (ChoPurchasing/ChoDuAn/ChoCCM/ChoCEODuyetPA/ChoCEODuyetNCC) · DaDuyet=7 · ChoDuyet=10 · TraLai=98 · TuChoi=99.
|
||||
- Changelog entity fields: PurchaseEvaluationId/EntityType/EntityId?/Action/PhaseAtChange?/UserId?/UserName?/Summary?/FieldChangesJson?/ContextNote?. EntityType.Workflow=5, ChangelogAction.Update=2 (ContractChangelog.cs:41). LogTransitionAsync (:1128-1155) dùng `.Add(...)` sync + Action=Transition.
|
||||
- Reviewer GO_WITH_FIXES: M1 (changelog shape) + N1 (.Add not AddAsync) + N2 (MaPhieu nullable) đã incorporate vào task spec item 4 — tôi follow.
|
||||
- 2 construction site verified: Features.cs:946 (frozen) + :975 (live). Helper 5 call-site :311/:865/:904/:937/:1112 — KHÔNG đổi signature.
|
||||
|
||||
## Tiến độ
|
||||
- [x] Chunk 1: PeBudgetAccumulator.cs — +using Dtos (:3); +PePendingAccumulation record struct (5-field, sau PeBudgetAccumulation); +ComputePendingAsync (window Y HỆT ComputeAsync; PendingSubmitted loại {1,7,10,99}; PendingSelected loại {1,7,99}+winner mirror join :50-60; PriorPes loại {7,99} order CreatedAt Select PePendingPriorPeDto). CHỈ đọc AsNoTracking, KHÔNG mutate. ComputeAsync thân KHÔNG đổi.
|
||||
- [x] Chunk 2: PurchaseEvaluationDtos.cs — +PePendingPriorPeDto(Guid Id, string? MaPhieu, int Phase) trước PeBudgetSummaryDto; extend PeBudgetSummaryDto append-CUỐI sau BudgetFrozen: +PendingSubmittedTotal=0/Count=0/SelectedTotal=0/Count=0/PendingPriorPes=null.
|
||||
- [x] Chunk 3: PurchaseEvaluationFeatures.cs — live-branch insert ComputePendingAsync sau ComputeAsync; live construction (:975) fill 5 field từ pending; frozen construction (:946) thêm 5 default 0m/0/0m/0/null (BudgetFrozen: true GIỮ). ComputeAsync call-site KHÔNG đổi (chỉ THÊM call ComputePendingAsync sau nó).
|
||||
- [x] Chunk 4: PurchaseEvaluationWorkflowService.cs — D4 trong ApplyBudgetSnapshotOnFinalizeAsync nhánh wiKey SAU set 4 lũy kế; gọi ComputePendingAsync 1 lần; PriorPes.Count>0 → .Add(changelog Action=Update, EntityType=Workflow, PhaseAtChange=DaDuyet, UserId=null, UserName="Hệ thống"); local switch PendingPhaseLabel (1→Nháp,10→Đã gửi duyệt,98→Trả lại,_→Đang duyệt); KHÔNG đổi signature, KHÔNG SaveChanges.
|
||||
- [x] Build slnx: **Build succeeded. 0 Warning(s) 0 Error(s)** (58.84s).
|
||||
- [x] git status verify: Migrations/ + ModelSnapshot = **NONE changed (PASS)**. 4 BE file mine đổi. 4 FE file (fe-admin/fe-user PeDetailTabs+types) + sub-implementer-frontend-1.md = của agent implementer-frontend-1 CHẠY SONG SONG (INV-2 A‖B) — KHÔNG phải tôi.
|
||||
|
||||
## Verify acceptance (đo được)
|
||||
- **A1 (ComputeAsync thân = 0 đổi):** PeBudgetAccumulator.cs diff = `68 insertions(+), 0 deletions`. `grep '^-'` = NO REMOVED LINES → ComputeAsync body byte-nguyên. PASS.
|
||||
- **A6 (5 call-site helper = 0 đổi):** WorkflowService.cs diff = `30 insertions(+), 0 deletions` (insertion-only). 5 call-site giữ signature `(evaluation, ct)` tại :311/:865/:904/:937/:1142 (site cuối dịch :1112→:1142 = +30 do CHÈN trong THÂN helper, nội-dung call bất động). Signature helper :998 KHÔNG đổi. PASS.
|
||||
- **A2 (đúng 2 construction site):** `grep 'new PeBudgetSummaryDto' src/Backend/` = ĐÚNG 2 (Features.cs:946 frozen + :985 live; :985 = :975 cũ dịch +10 do frozen+4 & live-insert+6). PASS.
|
||||
- **Frozen số cũ bất động:** Features.cs diff — nhánh frozen chỉ THÊM 5 default (`0m,0,0m,0,null`) vào construction :946; các dòng `e.ApprovedBudget*` = context KHÔNG đổi. 2 dòng "removed" = `BudgetFrozen: true);`/`false);` chỉ là đóng-ngoặc mở-rộng (giá-trị BudgetFrozen giữ). PASS.
|
||||
- **submit-guard :206:** KHÔNG trong diff (grep `selectedThanhTien`/`.All(x=>x==0)` trong diff = rỗng). PASS.
|
||||
- **DTO append-CUỐI:** DTOs.cs diff `14 insertions, 1 deletion` (deletion = mở-rộng `bool BudgetFrozen = false);` → +5 field). Pure-append. PASS.
|
||||
|
||||
## Note (bàn giao FE / risk)
|
||||
- Contract JSON: `PendingPriorPes` = **null** (nhánh frozen / không-cặp) hoặc list (live) → FE PHẢI null-guard `?? []` (reviewer N2/N9 — plain text mã + fallback "(chưa có mã)", KHÔNG render link). FE = implementer-frontend-1 lo.
|
||||
- `(int)p.Phase` projection EF-translate: spec-pinned + reviewer trục-4 validated; enum→int underlying = EF standard. Runtime chưa chạy được vì Dev DB 0 multi-pair (reviewer N5) — build compile PASS, logic mirror ComputeAsync đã proven.
|
||||
@ -0,0 +1,42 @@
|
||||
# SUB implementer-frontend-1 — S134 PE lũy-kế TẠM TÍNH (FE 2 app)
|
||||
|
||||
> Run: 2026-07-17-S134-pe-luyke-tam-tinh · role implementer-frontend · IMPL-B (items 5-7)
|
||||
> Ghi-đĩa-trong-lúc-làm (chống #53). Append từng bước.
|
||||
> ⚠️ NAMING: task top-line nói `sub-implementer-frontend-0.md` NHƯNG RUN-TRACE + subMdPath return nói `-1.md` (×4). Resolve = `-1.md` (khớp return contract). Flag cho em-main reconcile.
|
||||
|
||||
## Baseline (git hash-object PRE-edit) — VERIFIED
|
||||
- PeDetailTabs.tsx ×2 app: `5534addb9a7887c00562a99ab1f52ed4ee77c0ca` IDENTICAL (khớp task) → SHA-mirror protocol: edit fe-user → cp fe-admin → hash verify.
|
||||
- PeWorkflowPanel.tsx ×2 app: `a83a58e1d8e17ed56660a7061c755d58fa6ad27e` IDENTICAL (khớp task) → same protocol.
|
||||
- types/purchaseEvaluation.ts: fe-admin `ab4a2ec6` vs fe-user `2bf77367` DIVERGED (KHÔNG mirror, S117 confirmed) → edit RIÊNG từng app, cùng old_string (block PeBudgetSummary content-identical).
|
||||
- PurchaseEvaluationPhaseLabel exported types:35 · imported PeDetailTabs:29 + PeWorkflowPanel:29 · PurchaseEvaluationPhase.ChoDuyet=10 types:29.
|
||||
- fmtVnd PeDetailTabs:1002 · cn imported cả 2 file.
|
||||
|
||||
## Plan (3 items)
|
||||
- Item 5 types ×2: +5 field sau budgetFrozen (pendingSubmittedTotal/Count, pendingSelectedTotal/Count, pendingPriorPes[]|null).
|
||||
- Item 6 PeDetailTabs: +sub `sub?` prop cho BudgetSharedNumCell(:1184)+BudgetColValue(:1197); showPending gate; sub-dòng amber row1/2/5/6 (N8 count tương ứng); block liệt kê pendingPriorPes dưới matrix (:1656).
|
||||
- Item 7 PeWorkflowPanel: derived vars sau :132; banner amber sau V2-banner (:437); reminder dialog isApproveAction sau Dialog-open (:524, trên price-picker :696). KHÔNG đụng priceMissing:514/footer:522.
|
||||
|
||||
## Progress
|
||||
- [x] Read + ground-truth 6 file + anchors (DONE)
|
||||
- [x] Item 5 types ×2 (fe-user+fe-admin, same old_string budgetFrozen block) — grep pendingPriorPes = 1/1 ✓
|
||||
- [x] Item 6 PeDetailTabs — 8 edit fe-user → cp fe-admin → hash `d28eaa16` ×2 IDENTICAL ✓
|
||||
- import +type ReactNode; BudgetSharedNumCell+BudgetColValue +sub?:ReactNode prop
|
||||
- pending vars + showPending gate + pendingSub helper sau row6
|
||||
- row1(:1498) row2(:1503) sub withDetail=true; row5(pro/ccm.r5) row6 sub withDetail=false (N8 count tương ứng)
|
||||
- block liệt kê pendingPriorPes dưới matrix (border-t amber, N9 plain+badge phase-color 10/98/1/else)
|
||||
- [x] Item 7 PeWorkflowPanel — 3 edit fe-user → cp fe-admin → hash `bebc0ada` ×2 IDENTICAL ✓
|
||||
- derived: pendingPriors + showPendingBanner(phase===ChoDuyet M2) + pendingPriorsText (sau :132)
|
||||
- banner amber sau V2-banner (:437 vùng)
|
||||
- reminder dialog isApproveAction && showPendingBanner sau Dialog-open (trên price-picker); KHÔNG đụng disabled Button (B3)
|
||||
- [x] npm build ×2 — BOTH PASS 0 TS err (tsc -b && vite gate): fe-user 1939mod `index-BVoJX7QA.js` 20.94s · fe-admin 1950mod `index-mcHk0pmo.js` 9.26s. Warn pre-existing @import CSS + >500KB + realtime INEFFECTIVE_DYNAMIC_IMPORT.
|
||||
|
||||
## VERIFY FINAL (checklist B1-B4)
|
||||
- B1 types ×2 (KHÔNG mirror, DIVERGE): grep -c pendingPriorPes = 1/1 ✓. Both build clean.
|
||||
- B2 PeDetailTabs SHA-mirror: `git hash-object` ×2 = `d28eaa1656e1697f65483e91d6ad8cf381881d65` IDENTICAL ✓ (pre `5534addb`). Frozen phiếu KHÔNG render tạm-tính (showPending = !bs.budgetFrozen && ...) ✓.
|
||||
- B3 PeWorkflowPanel SHA-mirror: `git hash-object` ×2 = `bebc0ada6afc342f6c22bb2bf12c9f0317d7ade9` IDENTICAL ✓ (pre `a83a58e1`). Disable nút Xác nhận UNTOUCHED — `git diff | grep ^[+-] disabled|priceMissing|shouldPickPrice|transition.isPending` = 0 line ✓.
|
||||
- B4 diff scope: 6 FE file mine (fe-{admin,user}/{PeDetailTabs,PeWorkflowPanel,types}). 4 Backend file = implementer-backend ∥ (code-disjoint, KHÔNG phải mine). Sub-trace `-1.md` in run-folder.
|
||||
|
||||
## NAMING RESOLVED
|
||||
Run-folder có `sub-implementer-backend-0.md` (∥ agent) + mine `sub-implementer-frontend-1.md` → em-main index fan-out-GLOBAL (backend=0, frontend=1). `-1.md` ĐÚNG; top-line task `-0.md` = stale artifact. Return subMdPath = `-1.md`.
|
||||
|
||||
## STATUS: COMPLETE — no ambiguity, full precedent (S133 pattern reused).
|
||||
@ -0,0 +1,157 @@
|
||||
# SUB investigator-codebase-0 — S134 PE lũy-kế TẠM TÍNH (ground-truth + propose spec)
|
||||
|
||||
> Run: 2026-07-17-S134-pe-luyke-tam-tinh · engine fable-real single-deep-pass · ghi-đĩa-trong-lúc-làm (chống #53)
|
||||
> Status: IN-PROGRESS — append từng phần
|
||||
|
||||
## PHẦN 1 — Predicate lũy kế hiện tại (Claim 1) ✅
|
||||
|
||||
### Kết luận Claim 1: PARTIAL — quan trọng: "chỉ bắt DaDuyet" KHÔNG đúng cho cả 4 số
|
||||
|
||||
**Helper `PeBudgetAccumulator.cs` (extracted S133, behavior-preserving):**
|
||||
- Peers = cùng `(ProjectId, WorkItemId)`, `Id != this`, `CreatedAt < this` (`PeBudgetAccumulator.cs:38-40`) + HasQueryFilter !IsDeleted tự áp.
|
||||
- **PrevSubmitted (Total/Count)** = peers `Phase ∈ {ChoDuyet, DaDuyet}` → Count + SUM(`BudgetPeriodAmount ?? 0`) — `:42-48`. **⟵ ĐÃ bao gồm ChoDuyet (phiếu đang trình chưa duyệt)!**
|
||||
- **PrevSelected (Total/Count)** = peers `Phase == DaDuyet` AND `Suppliers.Any(IsWinner)` (existence) — `:50-52`; total = SUM `ThanhTien` quote `IsSelected` của các peer đó — `:53-60`. **⟵ Đây mới là chỗ DaDuyet-only ("duyệt rồi mới bắc" Kenny S117).**
|
||||
- **CurrentProposalTotal** = own quotes IsSelected — `:62-65`.
|
||||
|
||||
**Số bản predicate phải khớp — thực tế HIỆN TẠI là 2 bản sống + 1 bản one-time:**
|
||||
- (a) Helper `PeBudgetAccumulator.ComputeAsync` — bản RUNTIME duy nhất.
|
||||
- (b) Live-path `PurchaseEvaluationFeatures.cs:962-963` — **GỌI helper, KHÔNG duplicate predicate** (comment :959-961: "Row 1 = đã trình (ChoDuyet + DaDuyet); Row 2 = đã chọn thầu (DaDuyet + IsWinner). TraLai/DangSoanThao KHÔNG tính").
|
||||
- (c) Snapshot-path `PurchaseEvaluationWorkflowService.cs:998` (`ApplyBudgetSnapshotOnFinalizeAsync`) — `:1018` **GỌI helper, KHÔNG duplicate**. Helper được gọi từ 5 site: `:311` (submit-bypass?), `:865/:904/:937` (finalize branches), `:1112` (admin-override) — cần verify chi tiết từng site.
|
||||
- (d) Backfill SQL Mig 67 `20260717032812_AddPeApprovedBudgetSnapshot.cs:99-150` — bản DỊCH SQL one-time (đã chạy, không chạy lại — idempotent `WHERE SnapshotAt IS NULL`). Xác nhận enum số: `Phase IN (10, 7)` = ChoDuyet=10, DaDuyet=7 (`:128`), selected = `Phase = 7 + EXISTS IsWinner` (`:135-137`).
|
||||
|
||||
**TẠM TÍNH có đụng các bản này không?** → KHÔNG bắt buộc đụng semantics cũ. Cách sạch nhất: THÊM computation mới (nhánh/method mới trong helper hoặc method riêng) tính "phần góp từ phiếu chưa-terminal". Snapshot-path (c) + backfill (d) KHÔNG đụng — tạm-tính KHÔNG snapshot (đề bài chốt). Live-path (b) chỉ THÊM field vào DTO.
|
||||
|
||||
### Frozen gate S133 (Claim 3 partial — evidence)
|
||||
- Gate: `PurchaseEvaluationFeatures.cs:929` — `frozen = Phase == DaDuyet && ApprovedBudgetSnapshotAt != null`.
|
||||
- Frozen branch `:930-955`: serve 11 cột `ApprovedBudget*` từ PE row; row 4 "Giá trị kỳ này" VẪN live từ own quotes (`:932-936` — SPEC S133); `BudgetFrozen: true` (`:955`); canEditPro/Ccm = false-false (`:950`).
|
||||
- Live branch `:957-984`: pairRec `PeWorkItemBudgets` + accumulator; `BudgetFrozen: false` (`:984`).
|
||||
- DTO: `PurchaseEvaluationDtos.cs:353` — `bool BudgetFrozen = false` (cần đọc full record để map field position).
|
||||
- Entity: `PurchaseEvaluation.cs:116-132` — 11 cột ApprovedBudget* + SnapshotAt.
|
||||
- Seeder demo cũng set snapshot: `DbInitializer.cs:1327-1340`.
|
||||
|
||||
## PHẦN 2 — Cụm 4 số lũy kế + FE matrix (Claim 2) ✅
|
||||
|
||||
**BE DTO `PeBudgetSummaryDto` (`PurchaseEvaluationDtos.cs:331-353`, 18 field):**
|
||||
- `PreviousSubmittedTotal/Count` (:342-343) = SUM `BudgetPeriodAmount` peers Phase ∈ {ChoDuyet, DaDuyet} — "Ngân sách trình duyệt trước"
|
||||
- `PreviousSelectedTotal/Count` (:344-345) = SUM quote ThanhTien IsSelected của peers DaDuyet+IsWinner — "Kỳ trước đã chọn thầu"
|
||||
- `CurrentProposalTotal` (:346) = own quotes IsSelected
|
||||
- `BudgetFrozen` (:353) — append CUỐI default false (pattern append-an-toàn construction site)
|
||||
|
||||
**FE type `PeBudgetSummary` (`fe-user/src/types/purchaseEvaluation.ts:314-334`; block lũy kế :327-333).** ⚠️ types 2-app KHÔNG SHA-mirror (fe-user `173529b` vs fe-admin `5631542` — S117 confirmed lại): sửa type = edit CẢ 2 file riêng.
|
||||
|
||||
**FE matrix `PeDetailTabs.tsx` (2-app SHA IDENTICAL `47c97df1`):**
|
||||
- Công thức: `row1 = bs.previousSubmittedTotal` (:1315) · `row2 = bs.previousSelectedTotal` (:1316) · `row4 = bs.currentProposalTotal` (:1317) · **`row6 = row2 + row4`** (:1318 — "6. Lũy kế thực hiện = 2 + 4", render :1579-1586) · **`r5 = row1 + r3`** per-cột trong `colCalc` (:1323-1335, :1325 — "5. Lũy kế ngân sách đã sử dụng = 1 + 3", render :1569-1578). r7 = colFull − r5 (:1326), r9 = row4 + r8 (:1328), cmp56 = r5 − row6 (:1332).
|
||||
- Row 1/2 CHUNG 3 cột (`BudgetSharedNumCell colSpan=3`, :1498/:1503) — hiển thị null khi Count==0.
|
||||
- Row 3 PER-cột: PRO = `ev.budgetPeriodAmount ?? bs.proInitialAmount` (:1516) / CCM = `ev.ccmBudgetPeriodAmount ?? bs.initialAmount` (:1531 — Mig 59).
|
||||
- Frozen badge "🔒 Ngân sách chốt tại thời điểm duyệt" (:1352-1356). WorkItemId null → `bs` null → banner nhắc gắn hạng mục (:1297-1304).
|
||||
- ⚠️ Nuance sẵn có: row1 (accumulator) SUM `BudgetPeriodAmount ?? 0` — peer chưa nhập row3 góp 0, trong khi FE row3 phiếu đó hiển thị fallback proInitialAmount (drift-vector S133 đã ghi nhận, TẠM TÍNH kế thừa nguyên trạng — không sửa trong scope này).
|
||||
|
||||
## PHẦN 3 — Phase taxonomy (Claim 4) + snapshot helper (Claim 3 evidence)
|
||||
|
||||
**Enum `PurchaseEvaluationPhase.cs:15-27`:**
|
||||
- `DangSoanThao = 1` (Nháp) · `ChoPurchasing=2/ChoDuAn=3/ChoCCM=4/ChoCEODuyetPA=5/ChoCEODuyetNCC=6` [LEGACY deprecated] · **`DaDuyet = 7`** (terminal thành công) · **`ChoDuyet = 10`** (đã gửi duyệt) · **`TraLai = 98`** (trả lại — Drafter sửa gửi lại) · **`TuChoi = 99`** (terminal khoá).
|
||||
|
||||
**Snapshot helper `ApplyBudgetSnapshotOnFinalizeAsync` (`PurchaseEvaluationWorkflowService.cs:998-1041`):**
|
||||
- WorkItemId có → load pair `PeWorkItemBudgets` 6 cột (:1004-1011) + accumulator 4 cột lũy kế (:1018-1023); WorkItemId null → 10 cột null (:1027-1037); LUÔN set `ApprovedBudgetSnapshotAt` (:1040).
|
||||
- 5 call-site: `:311` + `:865/:904/:937` + `:1112` (RULE comment :997 "MỌI nhánh set Phase=DaDuyet PHẢI gọi helper NGAY TRƯỚC assignment").
|
||||
- Display-gate frozen: `PurchaseEvaluationFeatures.cs:929` (`Phase==DaDuyet && SnapshotAt != null`); frozen branch :930-955 serve snapshot + `BudgetFrozen:true`; row 4 "giá trị kỳ này" VẪN live (:932-936 spec S133).
|
||||
|
||||
## PHẦN 4 — Màn duyệt (Claim 5) — PeWorkflowPanel (2-app SHA IDENTICAL `9d88e671`)
|
||||
|
||||
- Component: `fe-user/src/components/pe/PeWorkflowPanel.tsx` (+ fe-admin mirror same SHA). Panel 3 = timeline + transition buttons + history.
|
||||
- Final-approver detect ĐÃ CÓ SẴN: `currentIsFinalApprover = lastFlowLevel?.status === 'Current'` (:112-115) · CCM-done eligible `ccmDelegationEligible` (:109-111) · per-level finalize `approverFinalizeEligible` (:119) · `shouldPickPrice = isApproveAction && (currentIsFinalApprover || finalizeByCcm || (approverFinalizeEligible && applyLevelFinalize))` (:510) — ĐÂY chính là predicate "đang duyệt CUỐI" tái dụng được cho cảnh báo.
|
||||
- V2 banner "Đến lượt bạn" :418-437 · action buttons :439-487 · Dialog transition :494-530 (footer Xác nhận :522, `priceMissing` disable :514) · mutation POST `/purchase-evaluations/{id}/transitions` :161-216.
|
||||
- Chỗ chèn cảnh báo "phiếu trước chưa duyệt" khả thi: (i) TRONG Dialog duyệt khi `isApproveAction` (cạnh :509-514, trước bộ chọn giá) — approver PHẢI nhìn thấy trước Xác nhận; (ii) banner Panel cạnh V2 banner :437; (iii) badge/note trong matrix PeDetailTabs rows 1/5/6.
|
||||
- Data: bundle DTO (`evaluation`) hiện KHÔNG có danh sách phiếu-trước-chưa-duyệt (chỉ có count gộp ChoDuyet+DaDuyet trong PreviousSubmittedCount — không tách). ⇒ cần field DTO MỚI; endpoint MỚI: KHÔNG cần — bơm vào GetPurchaseEvaluationQuery (đã query peers sẵn).
|
||||
|
||||
## PHẦN 5 — 5 call-site snapshot định danh + DB ground + tests
|
||||
|
||||
**5 site `ApplyBudgetSnapshotOnFinalizeAsync` (đọc context thật):**
|
||||
- `:311` — Admin manual override (site 5, reviewer-catch S133; assignment qua biến targetPhase)
|
||||
- `:865` — level-finalize (AllowApproverFinalize + applyLevelFinalize opt-out S96; set `EndedByLevelFinalize=true`)
|
||||
- `:904` — CCM duyệt-done miễn CEO (threshold guard winnerQuoteTotal < ceoThreshold)
|
||||
- `:937` — all-steps-done terminal (CEO/cấp cuối V2)
|
||||
- `:1112` — V1 legacy final (KHÔNG ApplyApprovedPriceOnFinalize)
|
||||
|
||||
**Entity fields:** `PurchaseEvaluation.cs:10` MaPhieu · `:16` WorkItemId loose-Guid · `:40` BudgetPeriodAmount · `:49` CcmBudgetPeriodAmount. Bundle root DTO `BudgetSummary` field `PurchaseEvaluationDtos.cs:260`; construction site duy nhất `PurchaseEvaluationFeatures.cs:1253`. FE bundle type `budgetSummary: PeBudgetSummary | null` (`fe-user/src/types/purchaseEvaluation.ts:470`) ⇒ PeWorkflowPanel đọc được qua `evaluation.budgetSummary` — KHÔNG cần prop/endpoint mới.
|
||||
|
||||
**DB ground:** Dev LocalDB `SolutionErp_Dev`: Phase distribution = {1:1, 3:1, 6:1, 7:1} — **phase LEGACY 3/6 CÒN ROW SỐNG** (seed cũ); multi-pair (cùng ProjectId+WorkItemId ≥2 phiếu) = 0 trong Dev. Prod: UNVERIFIED run này (SSH password extraction fail ×2 — không chặn spec; predicate viết dạng loại-trừ tự cover).
|
||||
|
||||
**Tests `PeBudgetFreezeTests.cs`:** peer-seed helper `:311-332` — CreatedAt override THỦ CÔNG (`peer.CreatedAt = createdAt` :332; TestApplicationDbContext KHÔNG wire audit interceptor); test lũy kế `:435-451` seed 2 peer CreatedAt sớm hơn `baseT`, phiếu xét `pe.CreatedAt = baseT` :451. Own-quote seed helper `:277-306` (IsWinner :287 + IsSelected :306).
|
||||
|
||||
## PHẦN 6 — Phase-set TẠM TÍNH trade-offs (Claim 4)
|
||||
|
||||
| Phase | Vào tạm-tính? | Trade-off |
|
||||
|---|---|---|
|
||||
| ChoDuyet=10 | ✅ CORE | "cung đặc thù" — đã trình, sẽ duyệt. Với row2-delta là gap chính; row1 ĐÃ gồm sẵn |
|
||||
| TraLai=98 | ✅ khuyến nghị | đang sửa để gửi lại — sẽ quay lại luồng duyệt; badge trạng thái để người duyệt tự cân |
|
||||
| DangSoanThao=1 | ⚠️ FLAG ANH | "tất cả lun" (lời em chat) NHƯNG: (i) nháp = RIÊNG TƯ S89 (`PurchaseEvaluationFeatures.cs:893-898` chặn approver mở nháp) — cộng tiền nháp vào matrix phiếu khác = lộ thông tin nháp; (ii) số nháp chưa chốt → nhiễu. Khuyến nghị: list-only (hiện MÃ + badge "nháp" trong danh sách cảnh báo, KHÔNG cộng delta tiền) |
|
||||
| TuChoi=99 | ❌ | terminal — không bao giờ thành "chính xác"; nghịch mục đích "số sẽ thành chính xác khi phiếu trước duyệt" |
|
||||
| DaDuyet=7 | — | đã nằm trong CHÍNH XÁC |
|
||||
| Legacy 2-6 | ✅ gộp | Dev DB CÓ row (3, 6) — predicate loại-trừ `Phase NOT IN (DaDuyet, TuChoi)` tự cover, an toàn hơn liệt kê dương |
|
||||
|
||||
**Khuyến nghị tập mặc định:** pendingPriorPes list = peers `Phase NOT IN (7, 99)` (mọi non-terminal, kể cả nháp — có badge); **delta-TIỀN row2** = non-terminal TRỪ nháp (`NOT IN (1, 7, 99)`) + IsWinner-existence + SUM IsSelected; **delta-TIỀN row1** = phases chưa nằm trong row1 hiện tại TRỪ nháp (= TraLai + legacy 2-6). Lead quyết + flag anh điểm nháp.
|
||||
|
||||
## PHẦN 7 — Edge cases (Claim 7) — từng edge tạm-tính xử ra sao
|
||||
|
||||
1. **WorkItemId null**: `budgetSummary` null toàn cục (gate `Features.cs:912`) → tạm-tính + warning KHÔNG TỒN TẠI (đúng — không định nghĩa được "cùng cặp"); banner "gắn hạng mục" cũ giữ (`PeDetailTabs.tsx:1297-1304`).
|
||||
2. **Multi-winner D4/IsSelected (S114)**: delta dùng ĐÚNG predicate hiện hành (IsWinner existence + SUM IsSelected — mirror accumulator :50-60) → nhất quán cardinality, 0 quy ước mới.
|
||||
3. **Giá âm (S116)**: SUM signed nguyên trạng → delta có thể ÂM; FE render dấu như mọi số tiền; KHÔNG guard thêm.
|
||||
4. **Min–Max (S117)**: `ApprovedPrice*` KHÔNG feed lũy kế (lũy kế = BudgetPeriodAmount + quote-sums; HĐ GiaTri cũng không đọc giá chốt) → orthogonal, 0 tác động.
|
||||
5. **PRO-split Mig 56**: `proInitialAmount` chỉ là FALLBACK DISPLAY row3 (`PeDetailTabs:1516`), accumulator dùng `BudgetPeriodAmount ?? 0` — tạm-tính giữ nguyên quy ước (không sửa drift-vector sẵn có trong scope này).
|
||||
6. **CcmBudgetPeriodAmount Mig 59**: row1 CHỈ SUM `BudgetPeriodAmount` (PRO-side); CCM-kỳ-này per-phiếu KHÔNG vào lũy kế hiện tại → delta CŨNG KHÔNG (thêm = đổi semantics CHÍNH XÁC, ngoài scope).
|
||||
7. **Mốc CreatedAt**: tạm-tính GIỮ NGUYÊN peer-window `CreatedAt < this.CreatedAt` (khác window = 2 tập peer khác nhau — số chính xác và tạm tính hết so sánh được). Phiếu bị đè CreatedAt (seed/import) nằm ngoài/trong window y hệt semantics hiện tại. Test seed mirror `PeBudgetFreezeTests:311-332/:435-451`.
|
||||
8. **Own-phase TuChoi/TraLai**: live branch vẫn compute; FE ẩn NUDGE màn duyệt khi phiếu terminal (duyệt không còn); matrix vẫn hiển thị (thông tin).
|
||||
9. **Audit gap (phiếu sau duyệt trước phiếu trước)**: snapshot CHỈ chốt 4 số CHÍNH XÁC; tạm-tính không snapshot → sau này không tái dựng "lúc duyệt có cảnh báo N phiếu". Optional-recommended: nối `ContextNote` LogTransitionAsync tại finalize "Duyệt khi N phiếu trước chưa duyệt: [mã…]" — 0 cột mới, Changelogs pattern Mig 61 đã có.
|
||||
|
||||
## PHẦN 8 — PROPOSED SPEC 3-mục
|
||||
|
||||
### ① Tính năng / đề bài
|
||||
UAT anh Kiệt FDC 15/07 (chốt 16:10): vì "cung đặc thù" (phiếu sau trình khi phiếu trước cùng gói thầu CHƯA duyệt), lũy kế PE phải: (a) "bắt lên và tính toán" cả phần góp từ phiếu chưa duyệt = **Lũy kế TẠM TÍNH**, phần góp NHẬN DIỆN ĐƯỢC (note màu); (b) **Lũy kế CHÍNH XÁC** = semantics hiện tại GIỮ NGUYÊN 100%; (c) người duyệt cuối (CEO/duyệt thay) NHÌN THẤY danh sách "phiếu trước chưa duyệt [mã — trạng thái]" lúc duyệt — informational nudge, KHÔNG hard-block.
|
||||
**Ground-truth thu hẹp đề:** row1 "NS trình duyệt trước" ĐÃ gồm ChoDuyet sẵn (accumulator :43-44). Gap thật = **row2 "Kỳ trước đã chọn thầu" (DaDuyet-only) + row6 dẫn xuất**, và (tuỳ tập chốt) row1 thiếu TraLai/nháp/legacy.
|
||||
|
||||
### ② Cách implement
|
||||
**BE (0 migration, 0 endpoint mới, 0 đụng snapshot-path):**
|
||||
1. `PeBudgetAccumulator.cs` — THÊM record `PePendingAccumulation(PendingPrevSubmittedTotal, PendingPrevSubmittedCount, PendingPrevSelectedTotal, PendingPrevSelectedCount, PendingPriorPes)` + method MỚI `ComputePendingAsync(db, projectId, workItemId, peId, peCreatedAt, ct)`. Predicate: peers cùng cặp + `CreatedAt <` + `Phase NOT IN (DaDuyet, TuChoi)`; delta-selected mirror :50-60 (IsWinner-existence + SUM IsSelected); delta-submitted = phases ngoài {ChoDuyet, DaDuyet} theo tập chốt. `PePendingPriorPeDto(Id, MaPhieu, Phase)` cho danh sách. **KHÔNG sửa `ComputeAsync`** (snapshot-path + Mig 67 bất động).
|
||||
2. `PeBudgetSummaryDto` (`PurchaseEvaluationDtos.cs:331-353`) — append CUỐI sau `BudgetFrozen`: 4 số pending (default 0) + `IReadOnlyList<PePendingPriorPeDto>? PendingPriorPes = null` (pattern append-an-toàn như BudgetFrozen S133).
|
||||
3. `GetPurchaseEvaluationQuery` — nhánh LIVE (`Features.cs:957-984`): gọi `ComputePendingAsync`, fill fields. Nhánh FROZEN (`:930-955`): pending = 0/null/empty — **R1-S133 giữ nguyên: phiếu DaDuyet là bảng snapshot bất biến, KHÔNG thêm số động** (tạm-tính trên phiếu đã duyệt vô nghĩa + vi phạm freeze).
|
||||
4. *(Optional-recommended, đóng audit-gap)*: tại 4 finalize-site + admin-site, nếu pending priors tồn tại → nối vào comment/ContextNote của `LogTransitionAsync` "Duyệt khi N phiếu trước cùng gói chưa duyệt: [mã…]" — 0 cột, Changelogs pattern sẵn.
|
||||
|
||||
**FE (2 app):**
|
||||
5. `types/purchaseEvaluation.ts` ×2 app (KHÔNG SHA-mirror — edit block `PeBudgetSummary` TỪNG app): +5 field mới.
|
||||
6. `PeDetailTabs.tsx` (SHA-mirror 2 app `47c97df1` — sửa 1 copy 2): matrix gate `!bs.budgetFrozen`:
|
||||
- Row 2 + Row 6: sub-dòng amber khi `pendingPrevSelectedCount > 0`: "Tạm tính: {row2+delta2} (+{delta2} từ {n} phiếu chưa duyệt)"; row6_tạm = row6 + delta2.
|
||||
- Row 1 + Row 5 (nếu delta1 bật): tương tự với delta1; r5_tạm per-cột = r5 + delta1.
|
||||
- Danh sách mã pending (MaPhieu + PhaseLabel badge màu per-phase) — render nhỏ dưới matrix hoặc tooltip.
|
||||
7. `PeWorkflowPanel.tsx` (SHA-mirror `9d88e671`): (a) banner amber cạnh V2-banner (:437) khi `evaluation.budgetSummary?.pendingPriorPes?.length > 0 && !readOnly`: "⚠ N phiếu trước cùng gói thầu CHƯA duyệt: A/012 (Đã gửi duyệt), … — cần duyệt phiếu trước trước phiếu này"; (b) NHẮC LẠI block đó TRONG Dialog duyệt khi `isApproveAction` (vùng :509-514, trên bộ chọn giá) — người duyệt cuối (`shouldPickPrice` :510 = predicate CEO/duyệt-thay sẵn có) chắc chắn thấy trước "Xác nhận". KHÔNG disable nút.
|
||||
|
||||
### ③ Checklist per-vai + acceptance ĐO ĐƯỢC
|
||||
**implementer-backend:**
|
||||
- [ ] `ComputePendingAsync` mới; acceptance: `git diff` vùng `ComputeAsync` = 0 dòng; build xanh.
|
||||
- [ ] DTO append-cuối; acceptance: 2 construction site (:946/:975) set explicit; không site nào khác gãy.
|
||||
- [ ] Frozen → pending 0/empty; acceptance: GET phiếu DaDuyet+SnapshotAt → `pendingPriorPes=[]`, 4 số 0.
|
||||
- [ ] Live → pending đúng; acceptance: curl GET phiếu B (peer A ChoDuyet+winner) → `pendingPrevSelectedTotal` = SUM quote IsSelected của A, list chứa `{maPhieu A, phase 10}`.
|
||||
- [ ] KHÔNG migration: snapshot `ApplicationDbContextModelSnapshot.cs` diff = 0.
|
||||
- [ ] (opt) Changelog note finalize; acceptance: transition finalize khi có pending → Changelogs row ContextNote chứa mã A.
|
||||
**implementer-frontend (×2 app):**
|
||||
- [ ] types 2 app; acceptance: `grep pendingPriorPes fe-{user,admin}/src/types/purchaseEvaluation.ts` = 2 hit.
|
||||
- [ ] PeDetailTabs sub-rows + list; acceptance: SHA 2 app IDENTICAL sau sửa; phiếu frozen KHÔNG render tạm-tính.
|
||||
- [ ] PeWorkflowPanel banner + dialog-block; acceptance: SHA 2 app IDENTICAL; nút Xác nhận KHÔNG bị disable bởi pending.
|
||||
- [ ] UAT flow đo được: A ChoDuyet → mở B: matrix row2 hiện tạm-tính + banner liệt kê A; duyệt A → DaDuyet → refetch B: tạm-tính biến mất, row2 chính-xác tăng ĐÚNG delta cũ.
|
||||
**test-specialist (pattern `PeBudgetFreezeTests` seed :277-306/:311-332):**
|
||||
- [ ] ComputePendingAsync: peer ChoDuyet+IsWinner+IsSelected → delta2 = tổng; peer TuChoi → 0; peer DaDuyet → 0 (bên chính xác); TraLai/nháp theo tập chốt.
|
||||
- [ ] Peer-window: peer CreatedAt > this → loại (mirror :435-451).
|
||||
- [ ] Quote âm trong delta (signed).
|
||||
- [ ] Frozen phiếu → pending rỗng.
|
||||
- [ ] (opt) finalize ghi Changelog note.
|
||||
|
||||
### Phân rã hmw staged (kiểu S133): INV done (file này) → **IMPL-A** backend (items 1-4) → **IMPL-B** frontend 2-app (5-7, sau A merge DTO shape) → **TEST-C** (sau A; FE acceptance UAT tay). A/B song song được nếu chốt DTO-shape trước; C sau A.
|
||||
|
||||
## DECISION-POINTS (lead quyết / flag anh)
|
||||
1. **DangSoanThao (nháp) vào delta-TIỀN?** A=include-all (đúng verbatim "tất cả lun") / B=list-only mã+badge, KHÔNG cộng tiền (S89 nháp-riêng-tư + số chưa chốt) — **tao khuyến nghị B, flag anh Kiệt**.
|
||||
2. **Delta row1** (TraLai+legacy BudgetPeriodAmount) làm luôn hay chỉ row2? Khuyến nghị: LÀM (nhất quán "tất cả") — chi phí ~0 (cùng query).
|
||||
3. **Nudge mức nào:** plain-info (khuyến nghị) / +confirm-checkbox FE "tôi biết N phiếu trước chưa duyệt" (rẻ, không BE) / hard-block BE (KHÔNG khuyến nghị — nghịch "cung đặc thù"; nếu anh muốn thì guard ở 4 finalize-site + admin bypass).
|
||||
4. **Changelog audit note** lúc finalize (optional-recommended — đóng gap "về sau không chứng minh được lúc duyệt đã cảnh báo").
|
||||
5. **TuChoi exclude** — khuyến nghị chắc, nêu để anh confirm.
|
||||
|
||||
## STATUS: COMPLETE — return digest gửi kèm.
|
||||
@ -0,0 +1,66 @@
|
||||
# sub-reviewer-0 — adversarial spec review PE lũy kế tạm-tính (S134)
|
||||
|
||||
> Ghi-đĩa-trong-lúc-làm (chống #53). Append từng issue khi tìm thấy. Return = digest.
|
||||
|
||||
## Trạng thái
|
||||
- [x] Đọc spec + investigator
|
||||
- [x] Đọc code BE (accumulator :29-71 / features :880-1009 / workflow service :280-321, :840-949, :998-1041, :1112, :1128-1155 / seeder :1290-1352 / entity changelog / DTO :331-353)
|
||||
- [x] Đọc code FE (PeDetailTabs :1290-1600 / PeWorkflowPanel :95-134, :400-539 + SHA tự đo 6 file)
|
||||
- [x] 8 trục verdict — DONE, xem cuối file
|
||||
|
||||
## VERDICT: GO_WITH_FIXES (0 Critical / 2 Major spec-fix / 9 nits — Major đều sửa bằng chữ trong spec trước khi hmw chạy)
|
||||
|
||||
## Issues (append dần)
|
||||
|
||||
### Trục 3 (D4) — kết quả soi từng nghi vấn
|
||||
- **(a) CLEAR:** seeder `DbInitializer.cs:1328-1341` set snapshot **TRỰC TIẾP** (mirror thủ công, KHÔNG gọi helper) → D4 changelog-in-helper KHÔNG gây changelog rác lúc seed/restart. #75-class không xảy ra.
|
||||
- **(b) CLEAR:** 5 call-site đều là finalize thật (Phase → DaDuyet): `:311` admin-override chỉ gọi helper khi `targetPhase == DaDuyet`; `:865` level-finalize; `:904` CCM-miễn-CEO; `:937` all-steps-done; `:1112` V1 legacy. Note "Duyệt khi N phiếu trước chưa duyệt" đúng ngữ cảnh cả 5.
|
||||
- **(c) SaveChanges:** mọi path qua helper đều save — V2/V1 qua `TransitionAsync:301`, admin-override `:316`. OK.
|
||||
- **[M1] D4 thiếu shape đầy đủ cho changelog row:** helper `ApplyBudgetSnapshotOnFinalizeAsync(evaluation, ct)` KHÔNG có `actorUserId`. `PurchaseEvaluationChangelog` (mirror `LogTransitionAsync:1145-1155`) cần `EntityType/Action/PhaseAtChange/UserId/UserName/Summary/ContextNote`. Spec chỉ nói ContextNote. Nếu hmw tự thêm param actor vào helper → ĐỔI 5 call-site → vi phạm chính acceptance A6 "0 đổi 5 call-site". Spec PHẢI chốt: UserId=null + UserName="Hệ thống" (audit actor tái dựng từ row Transition cùng batch) + EntityType/Action/PhaseAtChange value cụ thể — nếu không hmw sẽ tự quyết 2 hướng ngược nhau.
|
||||
- **[N1] `AddAsync` sai pattern codebase:** `LogTransitionAsync:1145` dùng `db.PurchaseEvaluationChangelogs.Add(...)` (sync). Spec ghi "AddAsync" — hoạt động nhưng lệch pattern; nên ghi `.Add(...)`.
|
||||
- **(d) Chi phí:** ComputePendingAsync ~3 query (list + delta1 + delta2), spec ghi "+1 query" là under-count — vẫn chấp nhận được (finalize tần suất thấp). Note.
|
||||
|
||||
### Trục 2 — construction site
|
||||
- Grep `new PeBudgetSummaryDto` toàn repo = ĐÚNG 2 site `Features.cs:946/:975` (khớp spec A2). Session log S74 xác nhận pattern grep này (gotcha #65). Tests KHÔNG construct DTO (chỉ 1 comment `PeBudgetFreezeTests.cs:21`) → append-cuối không gãy compile chỗ nào.
|
||||
- **[M2] Banner PeWorkflowPanel KHÔNG tự ẩn trên phiếu terminal — mâu thuẫn nội tại spec:** predicate banner spec item 7 = `pendingPriorPes.length > 0 && !budgetFrozen` — KHÔNG có gate `readOnly`/phase. Phiếu TraLai/TuChoi/Nháp đi nhánh live (`Features.cs:957`) → pending fields FILL → banner "cần duyệt phiếu trước trước phiếu này" hiện trên phiếu ĐÃ từ chối/trả lại = sai ngữ cảnh. Edge-6 spec claim "banner duyệt tự ẩn (không còn action duyệt)" — SAI với predicate đã cho (banner không phụ thuộc action; V2-banner :418 có `!readOnly`, action buttons :439 có `next.length > 0` — banner spec không kế thừa gate nào). Fix: thêm `!readOnly && evaluation.phase === ChoDuyet` (hoặc tối thiểu `!readOnly && next.length > 0`) vào predicate banner + sửa câu edge-6 cho khớp. Dialog-block đã tự gate qua `isApproveAction` — không dính.
|
||||
|
||||
### Verify PASS (positive — chống trượt kép)
|
||||
- **SHA tự đo (git hash-object):** PeDetailTabs 2-app IDENTICAL `5534addb9a78...` ✅ khớp spec; PeWorkflowPanel IDENTICAL `a83a58e1d8e1...` ✅; types KHÁC `2bf7736`≠`ab4a2ec` ✅ (spec errata lead đúng — số investigator cũ `47c97df1`/`9d88e671` stale). Measured-label gate PASS.
|
||||
- **Trục 1 (chính-xác bất động):** ComputeAsync untouched ⇒ mọi consumer bất động — live `:962`, snapshot `:1018`, backfill Mig 67 one-time, seeder direct-set `:1334-1340`. ComputePendingAsync chỉ ĐỌC (AsNoTracking window y hệt) + fill field MỚI + AddAsync changelog — không mutate field cũ nào. Không có đường lệch số cũ.
|
||||
- **Soft-delete peer:** `PurchaseEvaluationConfiguration.cs:84` HasQueryFilter !IsDeleted ✅ tự áp cho ComputePendingAsync (cùng DbSet).
|
||||
- **FE công thức khớp:** `row6 = row2 + row4` (:1318), `r5 = row1 + r3` per-cột (:1325) → row5_tạm = r5+delta1, row6_tạm = row6+delta2 đúng đại số. Render sites lũy kế toàn FE = CHỈ PeDetailTabs (:1315-1316/:1498/:1503 + r5 qua colCalc) — grep 2 app không còn site nào khác (không print/export/list) → spec liệt đủ, S89 miss-2-sites không tái diễn.
|
||||
- **Trục 4 predicate:** delta1 NOT IN (1,7,10,99) = {98, legacy 2-6} đúng (10/7 đã trong row1 chính-xác — không double-count); delta2 NOT IN (1,7,99) ⊃ {10 có winner} = gap chính đề bài ✓ + {98, legacy} có winner hợp lý; pendingPriorPes NOT IN (7,99) ⊇ cả 2 delta-set + nháp list-only (D1) — 3 tập nhất quán. EF translate: `!array.Contains(p.Phase)` → NOT IN, enum non-null → không bẫy null-semantics.
|
||||
- **Trục 5 dialog:** block chèn TRÊN bộ chọn giá chỉ RENDER thêm — `priceMissing` (:514) + footer disabled (:522) không đổi → acceptance B3 "diff vùng disabled = 0" đo được.
|
||||
- **D4 mọi path SaveChanges:** V2/V1 → `TransitionAsync:301`; admin-override → `:316`. Changelog entity + DbSet có sẵn (`IApplicationDbContext.cs:63`).
|
||||
- **Baseline test 520 khớp** (S133 suite 520/0). A5/A6/B1/B2/B3/B4/C1-C9 đều có lệnh đo được.
|
||||
|
||||
### Trục 6 — acceptance đo được: PASS có 1 gap
|
||||
- **[N5] A3 live-JSON verify cần DATA chưa tồn tại:** Dev DB multi-pair (cùng ProjectId+WorkItemId ≥2 phiếu) = 0 (invest PHẦN 5). A3/A4 curl cần dựng 2 phiếu cùng cặp trước — spec nên thêm 1 dòng "tạo 2 phiếu cùng Dự án × Hạng mục qua UI/API trước khi curl" để hmw không bịa acceptance đã-pass.
|
||||
- B2 "phiếu frozen KHÔNG render (gate đầu nhánh)" hơi code-read-feel — chấp nhận được vì C7 (test DTO defaults) + gate biến grep được; có thể thêm "grep điều kiện `budgetFrozen` xuất hiện trong block tạm-tính".
|
||||
|
||||
### Trục 7 — edges THÊM ngoài 7 edge spec
|
||||
- **Tie CreatedAt bằng nhau:** strict `<` → 2 phiếu cùng timestamp không thấy nhau — pre-existing y hệt ComputeAsync (nhất quán, không mới). Không cần fix.
|
||||
- **[N2] MaPhieu nullable:** entity `string? MaPhieu` (:10, gen at create `Features.cs:158` nhưng data cổ/nullable schema). DTO spec `PePendingPriorPeDto(Guid Id, string MaPhieu, int Phase)` non-null → nên `string?` + FE fallback "(chưa có mã)".
|
||||
- **[N9] Danh sách pending KHÔNG render link:** approver click link phiếu Nháp → 403 S89 guard (`Features.cs:893-898`) → toast lỗi khó hiểu. Spec nên chốt "plain text mã + badge, KHÔNG link".
|
||||
- **Peer có winner nhưng 0 quote IsSelected:** delta2 = 0 nhưng count đếm peer → sub-dòng "+0đ từ 1 phiếu" — mirror y semantics PrevSelectedCount hiện tại (chấp nhận, nhất quán).
|
||||
- **DaDuyet-no-snapshot fallback [N7]:** `Features.cs:926-928` phiếu DaDuyet mà SnapshotAt null → nhánh live → BudgetFrozen=false → tạm-tính HIỆN trên phiếu DaDuyet dị dạng này. Pre-existing class (nhánh live đã hiện số live cho row1/2 từ S133) — sau backfill Mig 67 + 5 site + seeder về lý thuyết không còn row nào — ghi nhận để không ngạc nhiên UAT, không fix.
|
||||
- **Performance:** ComputePendingAsync ~3 query (list + delta1 + delta2) mỗi GET detail non-frozen + mỗi finalize — bảng PE nhỏ, peers per cặp ít, chấp nhận. Spec ghi "+1 query" là under-count [N4].
|
||||
- **List dài không cap:** thực tế peers/cặp < 10 — không cần cap; nếu muốn phòng thì `.Take(20)` — optional.
|
||||
|
||||
### Trục 8 — mâu thuẫn nội tại / thiếu cho hmw
|
||||
- M1 (D4 shape) + M2 (banner gate vs edge-6) = 2 mâu thuẫn/thiếu chính — đã ghi trên.
|
||||
- **[N1]** Spec ghi `AddAsync` — pattern codebase = `db.PurchaseEvaluationChangelogs.Add(...)` (`:1145`). Dùng Add cho khớp.
|
||||
- **[N3]** Spec "map inline nhỏ nếu chưa có map dùng chung" — map ĐÃ CÓ: `PurchaseEvaluationPhaseLabel` (types cả 2 app, PeWorkflowPanel đang dùng :465) → chỉ thẳng dùng nó, khỏi inline.
|
||||
- **[N6]** Cite "mirror pattern LogTransition `:1073`-vùng" — thực tế `LogTransitionAsync` ở `:1128-1155`. Số dòng lệch (hmw grep ra được nhưng sửa cho sạch).
|
||||
- **[N8]** Sub-dòng "{n} phiếu chưa duyệt": chốt n = count TƯƠNG ỨNG delta (row1/5 → pendingSubmittedCount; row2/6 → pendingSelectedCount), KHÔNG dùng `pendingPriorPes.length` (list ⊇ delta-set, dùng nhầm → "từ 3 phiếu" khi delta chỉ từ 1).
|
||||
- **INV-2 [A‖B] song song:** DTO field names + JSON shape đã pin cứng trong spec → B không chờ A, race DTO-shape KHÔNG thật. OK.
|
||||
|
||||
## CHECKLIST-8-TRỤC (verdict per trục)
|
||||
1. Chính-xác bất động: **PASS** — ComputeAsync untouched + 4 consumer enumerated + ComputePendingAsync read-only.
|
||||
2. R1-S133 frozen: **PASS** (2 site construction verified duy nhất; DTO append-cuối an toàn; fallback DaDuyet-no-snapshot = pre-existing note N7).
|
||||
3. D4-trong-helper: **PASS-WITH-M1** — seeder direct-set (không gọi helper) nên KHÔNG changelog rác; 5 site đều finalize thật + SaveChanges đủ; NHƯNG spec thiếu shape row (actor/EntityType/Action/vị trí trong nhánh wiKey) → M1.
|
||||
4. Predicate mới: **PASS** — 3 tập nhất quán, không double-count, EF translate OK.
|
||||
5. FE: **PASS-WITH-M2** — công thức + vị trí + priceMissing không đụng; banner thiếu gate readOnly/phase → M2.
|
||||
6. Acceptance: **PASS** — đo được trừ A3 cần seed data (N5).
|
||||
7. Edge sót: 5 edge mới soi — không cái nào block; 3 thành nits (N2/N7/N9).
|
||||
8. Mâu thuẫn/thiếu: **2 Major + 5 nits** — đều sửa được bằng chữ trong spec, không cần đổi thiết kế.
|
||||
@ -0,0 +1,43 @@
|
||||
# sub-test-specialist-0 — TEST-C: PE lũy kế TẠM TÍNH (S134)
|
||||
|
||||
## Nhiệm vụ
|
||||
Viết test cho code BE ĐÃ LAND (INV-2): `PeBudgetAccumulator.ComputePendingAsync` + `PeBudgetSummaryDto` +5 field + D4 changelog on-finalize. C1-C9. KHÔNG touch prod.
|
||||
|
||||
## Bước 1 — Đọc code under test (DONE)
|
||||
- `tests/.../Services/PeBudgetFreezeTests.cs` — seed-helper pattern :277-336 (SeedWinnerQuoteAsync, SeedPeerAsync CreatedAt-override thủ công :332), T0 flow finalize path, T7 frozen query :730, C8-mirror pattern (Changelog discriminator).
|
||||
- `src/Backend/SolutionErp.Application/PurchaseEvaluations/PeBudgetAccumulator.cs`:
|
||||
- `ComputePendingAsync` :92-139 — peers window Y HỆT ComputeAsync (:100-102: ProjectId+WorkItemId, Id!=peId, CreatedAt<peCreatedAt).
|
||||
- PendingSubmitted :104-112 = Phase NOT IN {DangSoanThao, DaDuyet, ChoDuyet, TuChoi} → Count + SUM(BudgetPeriodAmount ?? 0).
|
||||
- PendingSelected :114-126 = Phase NOT IN {DangSoanThao, DaDuyet, TuChoi} AND Suppliers.Any(IsWinner) → Count + SUM(ThanhTien WHERE IsSelected).
|
||||
- PriorPes :128-133 = Phase NOT IN {DaDuyet, TuChoi} orderby CreatedAt → PePendingPriorPeDto(Id, MaPhieu, (int)Phase).
|
||||
- `PePendingAccumulation` record struct :31-36.
|
||||
|
||||
## Bước 2 — DTO + query handler + D4 (DONE)
|
||||
- `PePendingPriorPeDto(Guid Id, string? MaPhieu, int Phase)` @ `Dtos/PurchaseEvaluationDtos.cs:324` (ns `SolutionErp.Application.PurchaseEvaluations.Dtos`).
|
||||
- `PeBudgetSummaryDto` +5 field cuối :362-366 (PendingSubmittedTotal/Count, PendingSelectedTotal/Count, PendingPriorPes default null).
|
||||
- Query handler `PurchaseEvaluationFeatures.cs`: nhánh frozen :930-959 (Pending 4 số = 0 + PendingPriorPes null); nhánh live :961-1000 (fill từ ComputePendingAsync :972).
|
||||
- D4 `ApplyBudgetSnapshotOnFinalizeAsync` @ `PurchaseEvaluationWorkflowService.cs:998` — :1025-1053 nếu ComputePendingAsync.PriorPes.Count>0 → +1 Changelog {EntityType=Workflow, Action=Update, PhaseAtChange=DaDuyet, UserId=null, UserName="Hệ thống", ContextNote chứa "[S134 pending-prior]" + priorList (MaPhieu + label)}.
|
||||
- Phase enum: DangSoanThao=1, legacy 2-6, DaDuyet=7, ChoDuyet=10, TraLai=98, TuChoi=99.
|
||||
- BaseEntity.CreatedAt = plain DateTime no-default → phải override thủ công + peCreatedAt muộn hơn.
|
||||
- Helpers khả dụng cùng ns Services: NoOpNotificationService, FixedDateTime, TestCurrentUser, IdentityFixture.
|
||||
|
||||
## Bước 3 — viết PePendingAccumulationTests.cs (DONE)
|
||||
File MỚI `tests/SolutionErp.Infrastructure.Tests/Services/PePendingAccumulationTests.cs` — 8 [Fact] C1-C8, mirror seed-helper PeBudgetFreezeTests. Helper copy: CreateService, SeedProjectAndWorkItemAsync, SeedPairAsync (CCM-only), SeedWinnerQuoteAsync, SeedPeerAsync (return peer), SeedFinalizeWorkflowAsync, BuildPeAtFinalizeSlot, ApproveWithPriceAsync, ComputePendingAsync convenience (synthetic curPeId + CurCreatedAt=noon).
|
||||
|
||||
| Test | Method | Kết quả |
|
||||
|---|---|---|
|
||||
| C1 | `ChoDuyetWinner_CountsInPendingSelected_AndListedInPriorPes` :158 | PendingSelected=70tr/1, Submitted=0, PriorPes Id+MaPhieu+Phase=10 |
|
||||
| C2 | `TuChoiAndDaDuyetPeers_ContributeNothingToPending_AndAbsentFromPriorPes` :185 | delta all 0, PriorPes empty |
|
||||
| C3 | `DraftPeer_ListOnly_TraLaiPeer_CountsInPendingSubmitted` :208 | Submitted=60tr(TraLai)/1 (Nháp 40tr list-only), PriorPes 2 order CreatedAt |
|
||||
| C4 | `PeerCreatedAfterCurrent_ExcludedFromWindow` :234 | before lọt (25tr/1), after loại (NotContain) |
|
||||
| C5 | `NegativeSelectedQuote_ProducesSignedPendingSelectedTotal` :257 | PendingSelected=-500k signed |
|
||||
| C6 | `Compute_And_ComputePending_AreComplementary_NoDoubleCount` :278 | ComputeAsync PrevSelected=90tr/1 PrevSubmitted=130tr/2 bất động; Pending Selected=50tr/1 Submitted=0 |
|
||||
| C7 | `FrozenQuery_ReturnsZeroPendingAndNullPriorPes_EvenWithPendingPeer` :312 | BudgetFrozen=true, Pending 4=0, PriorPes null (dù có peer ChoDuyet) |
|
||||
| C8 | `Finalize_WithPendingPriorPeer_WritesAuditChangelog_ContrastNoPeerWritesNone` :355 | +1 D4 Update/Workflow/UserId=null/Hệ thống/ContextNote⊇MaPhieu A; contrast=0 |
|
||||
|
||||
## Bước 4 — chạy suite (DONE)
|
||||
- Filtered `~PePendingAccumulationTests`: **Passed 8/8, Failed 0** (3s).
|
||||
- FULL `dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1`:
|
||||
- Domain **45 PASS** 0 fail; Infra **483 PASS** 0 fail → **528 total, 0 fail**.
|
||||
- Baseline 520 (45D+475I) → **528 (45D+483I) = +8 Infra**. C9 PASS.
|
||||
- Build 0 error. KHÔNG touch production. KHÔNG git add/commit/push. NO prod bug found.
|
||||
Reference in New Issue
Block a user