[CLAUDE] Docs: S76 closeout — PE ngan sach ma tran 3 cot + bang luoi + badge quyen-NS

STATUS/HANDOFF (Mig 55->56, test 339->344, gotcha 69->70, bundle jOqxW4-p/DbsznVvR
Run #319, Phase +S76, In Progress->Recently Done) + gotcha #70 (FE absolute-set echo
stale-echo data-loss -> useIsFetching gate) + ef-core skill Mig 56 row + session log
2026-06-19-S76 + agent-memory harvest (impl-FE stray->canonical + 4 sub diary).
Curate-debt carry: reviewer 45KB + inv-codebase 35KB keep-floor-hit manual-condense.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-19 11:44:34 +07:00
parent 21d1f4ec43
commit 8f780b6237
10 changed files with 113 additions and 14 deletions

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,12 @@
---
## 🆕 S76 (2026-06-19) — PE budget MA TRẬN 3 cột + bảng lưới `<table>` + badge quyền NS (anh Kiệt FDC)
- **Part 1 mirror** (fe-user→fe-admin byte-identical, Python difflib slice-region): `PeBudgetSummaryTable` Block A → ma trận 3 cột (DỰ ÁN hiển-thị-only `—` / PRO `canEditPro` / CCM `canEditCcm`). Type `PeBudgetSummary` +`proInitialAmount`+`proAdjustmentAmount` cạnh `proEstimateAmount`(LEGACY). `proMut` body `{proInitialAmount,proAdjustmentAmount,proNote}`. proFull=proInit+proAdj.
- **Part 3 mirror:** `PeWorkflowPanel` approver `.join(' / ')` → map từng approver + badge `✎ NS PRO`(amber)/`✎ NS CCM`(sky) gate `a.canEditProBudget/canEditCcmBudget`. Type `PeCurrentApprovalLevelApprover` +2 cờ **CẢ 2 app** (purchaseEvaluation.ts types DIFFER giữa 2 app → sửa cả 2; PeDetailTabs/PeWorkflowPanel byte-identical).
- **Bảng lưới (em-main, anh phản hồi "chưa chia cột giống Excel"):** Block A flex→`<table border-collapse>` viền ô. `BudgetCell` xếp dọc (input full ô + Lưu dưới). `BudgetNoteRow``BudgetNoteCell` (td colSpan=3). **LESSON: flex+gap KHÔNG ra "bảng" — phải `<table>` viền ô mới giống spreadsheet.**
- **cwd-misland:** Part-1 `cd fe-admin` npm build → MEMORY rơi `fe-user/.claude` stray → em-main harvest về + `.gitignore` guard `fe-*/.claude/` (S76).
## 🆕 S73b (2026-06-18) — PE budget "Ghi chú từ CCM" mirror ×2 app (anh Kiệt FDC)
Thêm dòng "Ghi chú từ CCM" trong panel TỔNG HỢP NGÂN SÁCH TRÌNH KÝ (`PeBudgetSummaryTable`), mirror y hệt "Ghi chú từ PRO" nhưng gate `canEditCcm` + `ccmNoteText`/`setCcmNoteText` + save qua `ccmMut`. 2 file ×2 app: types/purchaseEvaluation.ts (`PeBudgetSummary` +`ccmNote:string|null` sau `adjustmentAmount`) + PeDetailTabs.tsx (state mirror proNoteText + `ccmMut` body type +`ccmNote` + **2 call-site echo `ccmNote:bs.ccmNote`** Ban hành+Hiệu chỉnh — endpoint `/budget/ccm` ABSOLUTE-SET cả 3 field, thiếu=null=CLEAR + block mới chèn SAU dòng V0/hiệu chỉnh TRƯỚC Ngân sách PRO, group CCM rows). **LESSON xác nhận S73: budget-table region 2 app byte-IDENTICAL → block mới mirror identical (diff awk-PeBudgetSummary + diff sed-region đều IDENTICAL). PeBudgetSummary type block 2 app identical (KHÁC PeDetailBundle Color-records divergence) → an toàn edit cùng content.** Build PASS ×2 (admin 1945mod `index-CCPIU9Wr.js` / user 1934mod `index-j5Zh9w96.js`, 0 TS err). NO ambiguity, full precedent. KHÔNG đụng BE (DTO `ccmNote` + endpoint body = em-main song song).

View File

@ -70,6 +70,10 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte
## 📅 Recent activity (FIFO — older → archive/git)
- **2026-06-19 (S76 P2+P3 — budget-edit-role BADGE insert-point map, designer+fe-user-flow, on-disk):** ⭐ **Display-only "✎ NS PRO/CCM" badge per approver — BE change = SMALL both DTOs.** **(A) Designer fe-admin `ApprovalWorkflowsV2Page.tsx`:** read-only render `DefinitionCard:446-454` (level group → approver `{approverUserName}` + `({approverEmail})`); DTO `LevelDto:37-54` (approverUserId/userName/email + 7 Allow* flag, **NO role/dept field**). Feed = `GetAwAdminOverview` (`/approval-workflows-v2`). **Insert badge → `:447-452`** cạnh `approverUserName`. **(B) fe-user `PeDetailTabs.tsx`:** approvalFlow render `LevelOpinionsSectionV2:588` (signed-only) — но live flow tree = `currentApproval.approvers` :131 + Panel3 separate. `PeApprovalFlow` DTO `purchaseEvaluation.ts` + BE `PurchaseEvaluationApprovalLevelApproverDto` (`PurchaseEvaluationDtos.cs:129-132` = UserId/FullName/Email, **NO role**). **(C) Role-resolve for LIST userId:** codebase uses `userManager.GetRolesAsync(u)` (per-user, N+1 risk) OR `GetUsersInRoleAsync(role)` (reverse, `PeUrgentFeatures.cs:74`). `IApplicationDbContext` exposes `DbSet<Role> Roles` :29 but **NO UserRoles join-table DbSet** → efficient batch = either (a) `userManager.GetUsersInRoleAsync(Procurement/CostControl)`→2 HashSet<Guid>, mark approver if id∈set (NO N+1, 2 queries total); or (b) add `DbSet<IdentityUserRole<Guid>>` to interface for join. **BE build site `PurchaseEvaluationFeatures.cs:964-972`** already batches `approverInfos` via `userManager.Users.Where(Contains(allApproverIds))` — extend SELECT or post-join 2 role-sets here; handler has both `db`+`userManager` :750-751. **(D) Change size = SMALL:** +2 bool field (canEditProBudget/canEditCcmBudget) per approver DTO + 2 GetUsersInRoleAsync calls. Designer side: `GetAwAdminOverview` query needs same 2-set lookup (admin-only, cheap). Gate semantics ALREADY proven `:800-801` (canEditPro=Admin||Procurement, canEditCcm=Admin||CostControl). **(E) REC:** minimal = compute 2 HashSet once (proFans/ccmFans via GetUsersInRoleAsync), pass into approver-DTO map both sites; badge = pure display `id∈proFans→"✎ NS PRO"` `id∈ccmFans→"✎ NS CCM"`. RISK low (display-only, no authz touch) — only watch: a user can hold BOTH roles → show both badges; Admin holds neither role explicitly unless seeded → may need OR Admin note. Tag `[s76, budget-role-badge, designer+pe-flow, getusersinrole-batch-no-n1, approver-dto-add-2bool, display-only]`.
- **2026-06-19 (PE Block-A budget editable-gate audit — submission-count lock NEXISTS, on-disk):** ⭐ **Gate = PURE ROLE, KHÔNG phase, KHÔNG số-lần-trình.** BE `PurchaseEvaluationFeatures.cs:800-801` `canEditPro=isAdmin||Procurement` · `canEditCcm=isAdmin||CostControl` (DTO arg :856). Handler `PeWorkItemBudgetFeatures.cs`: PRO `:86-91` CCM `:152-157` fail-closed ForbiddenException role-only TRƯỚC side-effect; comment `:18-20` ghi RÕ "KHÔNG ràng Phase (bảng NS = tài-liệu-sống chỉnh bất-kỳ-lúc-nào như Excel)". Validator chỉ `>=0` (Initial :136, Adjustment cho-ÂM :138), absolute-set null=clear. **FE `PeDetailTabs.tsx:1060 PeBudgetSummaryTable`:** ô "Ban hành lần đầu" :1173 + ô "hiệu chỉnh V0" :1188 dùng **CÙNG biến `bs.canEditCcm`** — ZERO phân-biệt 2 ô, ZERO lock-after-first. `drafterEditable:1066`=`!readOnly&&isEditablePhase` chỉ áp row3/row8 (drafter NS-kỳ-này), KHÔNG áp Block-A. **(b) submission-count lock = KHÔNG TỒN TẠI:** grep `submitCount|lanTrinh|firstSubmit|lockInitial|hasSubmitted|soLanTrinh` toàn `src/Backend`=0 + FE=0. Entity `PeWorkItemBudget.cs` 6 field plain, KHÔNG cờ `IsInitialLocked`/`SubmitCount`; record per-cặp(Project×WorkItem) share mọi phiếu KHÔNG track lần-trình. **Kết luận: yêu-cầu chị Trà/anh Kiệt (khóa Initial sau lần-trình-đầu, mở Adjustment) = FEATURE MỚI — cần field track first-submit-done + TÁCH gate 2 ô (Initial vs Adjustment), HIỆN cùng `canEditCcm` không tách được.** Tag `[pe-block-a-gate, role-only-no-phase, submission-count-lock-NEXISTS, initial-vs-adjustment-same-gate, fdc-feature-new]`.
- **2026-06-18 (PE price-model recon FDC "Giá chào thầu" PRO-Min/Max + CCM-proposed, on-disk):** ⭐ **"Giá chào thầu" mục c = DERIVED, KHÔNG stored column** = `WinnerQuoteTotal` = SUM(Quote.ThanhTien WHERE supplierRows==SelectedSupplierId). Computed 3 nơi đồng-predicate: submit-guard `PurchaseEvaluationWorkflowService.cs:188-192` · detail-GET `PurchaseEvaluationFeatures.cs:818-826`(→`CurrentProposalTotal`) · CEO-threshold `:833`. DTO `WinnerQuoteTotal` `PurchaseEvaluationDtos.cs:244`. **ALL money fields:** Quote(NCC) `BgVat/ChuaVat/ThanhTien` decimal non-null `PurchaseEvaluationQuote.cs:12-14` · PE-header `BudgetPeriodAmount`(row3 drafter)/`ExpectedRemainingAmount`(row8) decimal? `PurchaseEvaluation.cs:40-41` · PeWorkItemBudget(per cặp Project×WorkItem) PRO `ProEstimateAmount:27` + CCM `InitialAmount`/`AdjustmentAmount`(ÂM-OK) `:29-30` decimal? · Detail dự-toán `KhoiLuong/DonGia/ThanhTienNganSach` `PurchaseEvaluationDetail.cs:15-18`. **PRO-min/max + CCM-proposed = KHÔNG tồn-tại** (grep Min|Max|Proposed|Suggest|BidPrice|GiaChaoThau PE-entities=0) → field MỚI. **Role-gate mirror-được (`PeWorkItemBudgetFeatures.cs`):** 2 cmd tách `UpdatePeBudgetProCommand:61`+`UpdatePeBudgetCcmCommand:126`; handler fail-closed `ForbiddenException` TRƯỚC side-effect — PRO `:86-91`(`Admin||Procurement`) CCM `:150-155`(`Admin||CostControl`); capability-flag BE-computed `canEditPro/canEditCcm` `PurchaseEvaluationFeatures.cs:783-784`→DTO `PeBudgetSummaryDto:290-291`; auto-create race-safe `PeWorkItemBudgetEnsurer.EnsureTrackedAsync:34`; KHÔNG ràng Phase. NO AutoMapper (DTO project tay). **FE (fe-user `src/`; fe-admin PeDetailTabs.tsx = SHA-identical `diff -q`):** mục-c `components/pe/PeDetailTabs.tsx:1406-1417`(helper `computeGiaChaoThau` def:71 call:1393) · budget-table `PeBudgetSummaryTable:1062-`(rows:1110-1128, host `ChonNccSection:1383`) · giá-gói+CEO-threshold `:311-313` · create `PeWorkspaceCreateView.tsx` · header `PeHeaderForm.tsx`. FE type `types/purchaseEvaluation.ts` `PeBudgetSummary:292-307`+`winnerQuoteTotal:445`. ⚠️ fe-admin types DIFFER (sync cả 2). **Surprise:** PRO-Min/Max-chốt + CCM-proposed = semantic MỚI (giá-người-duyệt ≠ giá-NCC-báo); gắn PeWorkItemBudget(per-cặp role-gate-sẵn) vs column-PE(per-phiếu) = em-main quyết. Mig 53 CeoApprovalThreshold+cờ-gấp đã có khung CEO-duyệt-theo-ngưỡng. Tag `[pe-price-model, gia-chao-thau-derived, pro-minmax-ccm-proposed-NEW, role-gate-mirror, fdc]`.
- **2026-06-18 (S71 PART-C audit — run-trace vs checklist-v2 FLAT + detector-refine, on-disk):** ⭐ **2 GAP THẬT (trung-thực, không inflate):** (1) **C1/C2/C8 = SUBFOLDER, canonical-v2 = FLAT → migration NEEDED, chưa làm.** `find runs/` cho thấy MỖI run-folder có `sub-md/`+`harvest/` SUBDIR (5 run: h10-{invest,implement,review}+h910-{finalize,curate}) — đúng cấu-trúc CŨ broadcast-delta phát-bỏ. ZERO flat-awareness: grep `phẳng|flat|cùng cấp` trong `.claude/workflows/`+`.claude/commands/`=0 hit. SE-adoption-commit `8c47bd0`(06-18) TRƯỚC broadcast-flat cùng-ngày → SE chưa biết. README/hmw.js/session-end đều mô-tả subfolder. C8 dual-form-acceptance close-gate cũng chưa. (2) **REFINE(b) detector = MISSING HOÀN-TOÀN.** `find .claude -name *.js/*.ps1`=CHỈ `hmw.js`(=engine ≠ detector). `.claude/hooks`+`.claude/scripts` KHÔNG tồn-tại. Repo-wide grep `bypass|scan.*runs` script=0. SE KHÔNG có bộ-dò chống-lách-engine → 3-function (whitelist/path-variants/launch-key-anchor)+relation-acceptance = n-a. **MET (đừng nhạ oan):** C3 committed THẬT — `git check-ignore runs`=exit1(NOT-ignored)+`git ls-files runs`=22 file (cả hai nấc). C4 per-turn real (`invest-synthesis.md` 43-dòng). C5 3-layer wired: L1 README:51(convention em-main) · L2 `session-start.md:71` orphan-scan `runs/*/` closed=⏳+harvest-rỗng · L3 `session-end.md:51` close-gate idempotent 5-trục. C6 ledger 2-beat (`_ledger.md:7`, 5 run đều CLOSE-beat+wf_). C7 caveat present (README §69-73 no-overclaim/fragile/G-015 TRACKED≠enforced). ⚠️ sub-md/ chỉ `.gitkeep` (read-only sub→em-main scribe, design KHÔNG phải miss). Tag `[s71, part-c-audit, subfolder-not-flat, detector-MISSING, c3-committed-real]`.

View File

@ -61,6 +61,8 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
## 📅 Recent activity (FIFO — older → archive/git)
- **2026-06-19 (S76 Part2+3 PE budget-edit BADGE display-only — Lens-2 FE badges+render-safety, PASS, 0 blocker):** 2 cờ bool CanEditProBudget/CanEditCcmBudget vào 2 DTO approver (AwLevelDto designer + PurchaseEvaluationApprovalLevelApproverDto PE-flow), suy từ ROLE (KHÔNG đổi authz). Badge "✎ NS PRO" (amber) / "✎ NS CCM" (sky). **PASS Lens-2:** (1) **role-set KHỚP gate**`proBudgetEditors=GetUsersInRoleAsync(Procurement)Admin` / `ccmBudgetEditors=CostControlAdmin` (ApprovalWorkflowV2AdminFeatures.cs:155-157 + PurchaseEvaluationFeatures.cs:976-978) đảo-chiều set-lookup 3-query/req no-N+1; khớp 1:1 gate thật canEditPro/canEditCcm `:800-801`=isAdmin‖Procurement / isAdmin‖CostControl; AppRoles consts verified (`AppRoles.cs:5,9,10`). (2) **role-set DEFINE trước cả 2 site** — PE-flow define `:978` < approvalFlow `:1045` < currentApproval `:1064` (cùng V2-branch scope); designer define `:155` < ToDto `:184`. (3) **DTO flag flows CẢ flow+current** `PurchaseEvaluationApprovalFlowLevelDto.Approvers` (`:152`) + `PurchaseEvaluationCurrentApprovalDto`-path (`:145`) đều dùng `PurchaseEvaluationApprovalLevelApproverDto` badge hiện Panel-flow lẫn current. (4) **PeWorkflowPanel `.join('/')→map`** giữ separator "/" (`{i>0 && <span>/</span>}`), giữ case rỗng `length===0?'(chưa cấu hình)'`, key=`a.userId` SAFE (validator `HaveNoDuplicateApproverInSameLevel:289` chặn dup ApproverUserId trong 1 level no React key-collision). (5) **render-safety** designer wrapper `flex``flex flex-wrap` + panel `flex flex-wrap gap-x-1 gap-y-0.5` badge KHÔNG vỡ layout khi nhiều approver/badge. (6) **mirror 2-app PERFECT** PeWorkflowPanel fe-admin==fe-user byte-IDENTICAL cả base lẫn now (diff empty); types +2 cờ CẢ 2 app (admin:235-236/user:238-239), block diff-identical (chỉ pre-existing inline-comment `//0-based` lệch = noise KHÔNG do change này). (7) **FE typecheck CLEAN** cả 2 app (`tsc --noEmit` exit 0). no mock/alert/TODO. ** SPEC-vs-DIFF MISMATCH (em-main framing wrong, NOT a code bug):** spec nói "KHÔNG migration" nhưng diff BUNDLES S76 Part1 (migration `AddProBudgetSplitToPeWorkItemBudget` + PeWorkItemBudget domain + PeBudgetSummaryDto + PeDetailTabs 306-LOC matrix rewrite WIRED PUT /budget/pro). Part1 spot-check sound (mig 3-file OK pure-ASCII gotcha#30-respect, PUT wired real not-mock). **🟡 MAJOR race STILL PRESENT (carry-over Part1, line 64 entry):** PeDetailTabs 2 PRO cell (`:1283` proInitial + `:1305` proAdjust) cùng `proMut.mutate` echo sibling từ `bs` server-snapshot, `invalidate()` fire-and-forget (`:1161` không await) double-Save<refetch wipes sibling. Sev MAJOR data-loss tiềm ẩn, prob thấp. **LEARNED:** display-only capability-flag review = (a) confirm flag-compute KHỚP gate-thật bit-for-bit (đảo-chiều set-lookup must mirror the forward Roles.Contains check) + (b) confirm DEFINE-before-all-consumer-site trong cùng scope + (c) which DTO carries flag determines which UI surface shows badge (flow vs current both here). For `.join→map` refactor verify separator+empty-case preserved + key-uniqueness backed by a BE validator. SURPRISE: adversarial value here = catching the spec's "KHÔNG migration" claim is FALSE (diff is combined Part1+2+3) don't trust em-main's scope framing, read the actual changed-set. Tag [s76-part23, pe-budget-badge, display-only-capability-flag, role-set-mirrors-gate, join-to-map-separator-preserved, key-uniqueness-validator-backed, mirror-2app-byte-identical, spec-vs-diff-mismatch, major-race-carryover].
- **2026-06-19 (S76 Part1 PE budget MA-TRẬN 3 cột Mig 56 uncommitted, PASS w/ 1 MAJOR race + 2 MINOR):** Form ngân sách 1-cộtma-trận [Dự án|PRO|CCM]. Entity +ProInitialAmount/+ProAdjustmentAmount (cột PRO mirror CCM Initial/Adjustment); ProEstimateAmountLEGACY, Mig 56 Sql() UPDATE migrate idempotent (`WHERE ProEstimate NOT NULL AND ProInitial NULL` chạy-1-lần-safe). **PASS:** authz fail-closed Forbidden TRƯỚC side-effect 2 handler (PRO=Admin‖Procurement `:90`, CCM=Admin‖CostControl `:160`; Admin nhập cả 2 đúng ý; neither-role blocked); compute `fullAmount`=CCM nếu hasCcm else proFull(ProInit+ProAdj) `:852`, migrate-value flow đúng (legacy ProEstimateProInitialhasPro=true→fullAmount); `fullIsEstimate` `!hasCcm``!hasCcm&&hasPro` (improve, no badge khi empty); DTO 17-arg positional khớp def (2 new appended last, build-PASS compiler-checked); Block B 9-row dùng `full` authoritative INTACT; Mig 3-file OK (.cs+Designer+snapshot 18,2); FE 2-app SHA-twin `a93c8aa0`; 32 PeWorkItemBudget tests PASS (+5 S76: set-both-neg, validator neg-initial-fail/neg-adjust-pass, full-proFull-150, neg-proAdjust-70); no mock; anti-fiddle clean. **🟡 MAJOR race (pre-existing pattern, S76 WORSENS):** BudgetCell cross-field echo từ `bs` (server snapshot) KHÔNG local-state PRO "Ban hành" save gửi `proAdjustmentAmount: bs.proAdjustmentAmount`, "V0" save gửi `proInitialAmount: bs.proInitialAmount`. `invalidate()` fire-and-forget (`:1170` không await refetch). 2 PRO cell nay đồng-cột click Save cell-2 trong window [onSuccess fires (isPendingfalse, btn re-enabled) refetch lands] đè cell-1 về STALE (vd: lưu Ban-hành=100 ngay lưu V0=50 trước refetch Ban-hành WIPED null). Trước S76 PRO chỉ 1 số nên window này hại; ma-trận 2-cột-PRO làm reachable. Sev MAJOR (data-loss tài-chính tiềm ẩn) nhưng prob thấp (cần double-click <refetch-latency); fix gợi-ý: disable sibling-cell Save khi mut.isPending HOẶC onMutate optimistic-merge HOẶC await invalidate. **🔵 MINOR:** (a) `parseVnd` strip `.` "1.5"→15 (input cho `.` nhưng VND whole-number nên harmless); (b) stray `fe-user/.claude/agent-memory/implementer-frontend/MEMORY.md` NOT-gitignored (cwd-misland gotcha) em main ĐỪNG `git add -A`. **LEARNED:** cross-field echo-from-server an-toàn khi 1-field/cột; thành race khi N-field cùng-cột share 1 mutation + fire-and-forget invalidate window mở SAU isPending=false (btn enable) chứ không phải lúc in-flight; load-bearing = đếm field-cùng-cột share mutation + check invalidate awaited. Tag [s76, pe-budget-matrix, mig56-migrate-idempotent, cross-field-echo-race-worsened, fullIsEstimate-improve, cwd-misland-stray, parseVnd-dot-minor].
- **2026-06-18 (S72ter-WIRE Mig 54 cross-stack-wire + verify-fix lane uncommitted priceMissing, PASS no-new-deadlock):** Complement to S72ter-AUTHZ below (same fix, deadlock-lens). Fix = `priceMissing` old `length>0 && !source` new `(length===0 || !source)`, 2-app SHA-twin `4d6c89d9`. **No new deadlock — 4-fact:** (1) fix CHỈ THÊM disable-cond `length===0` lên branch đã unreachable-by-invariant (submit-guard `:194` hard-block winnerQuoteTotal<=0 ALL-paths Ncc candidate luôn 1 ChoDuyet) không sinh lockout mới; (2) giáchọnsource set`priceMissing=false`nút "Xác nhận" mởduyệt OK; (3) empty (giả định)→nút khoá + amber `:537` "nhập PRO/CCM hoặc chọn NCC" = lối-thoát , setter-path KHÔNG phase-gated (mirror-budget) cho nhập giá bất kỳ lúccandidate xuất hiệnmở lại (no hard-lock); (4) intermediate-approve `shouldPickPrice=false` (chỉ `currentIsFinalApprover||finalizeByCcm`)→nút mở bình thường, khớp BE ApplyApprovedPrice chỉ terminal `:885`+CCM-deleg `:853` (intermediate advance `:870/:893` không gọi). 7-layer threading 0-drop re-confirm (ctrl `:129/:337`cmd `:462`handler `:515`iface `:30`svc `:47`ApproveV2 `:822`). OR-of-N `currentIsFinalApprover` true mọi viewer cấp cuối (ComputeLevelStatus `:987` position-based) nhưng nút dialog `disabled=blockedByV2Level`+`!isDisabled&&setTarget` `:310/:320`non-approver không mởprice-selector hại. **LEARNED:** "fix tạo deadlock?" = THÊM-disable lên branch-unreachable-by-invariant không thể sinh lockout; verify lối-thoát = setter KHÔNG phase-gated (giá nhập-được bất kỳ lúc) + amber-message user luôn thoát empty-state. Tag [s72ter-wire, verify-fix, no-new-deadlock, escape-hatch-amber, 7layer-0drop].
- **2026-06-18 (S72ter Mig 54 AUTHZ+SECURITY lane double-check uncommitted priceMissing FE-fix + committed 1d86abc re-verify, PASS, 0 issue):** anh giao 3 lane laser (a setters / b CCM-finalize bypass / c controller authz) on commit 1d86abc (deployed Run #313) + 1 uncommitted FE-fix. **Uncommitted diff = 2 LOC product only** (`priceMissing` both apps, SHA-identical `4d6c89d9`) + memory/ledger noise em main đúng kỷ luật chỉ-touch-2-file. **(a) PASS** `PeSuggestedPriceFeatures.cs` cả 2 setter ForbiddenException TRƯỚC mọi mutate+SaveChanges (load+NotFoundrole-gate `:40-41`/`:109-110`mutate); role đúng PRO=Admin‖Procurement, CCM=Admin‖CostControl; AppRoles consts tồn tại (`:5,9,10`). Phase-guard cố-tình-thiếu, documented mirror-budget S61 (non-regression). **(b) PASS no-bypass 3 gate trực-giao chặn non-CostControl finalize-bỏ-CEO, TẤT CẢ throw TRƯỚC `Phase=DaDuyet`(:854):** (1) approver-match `:702-713` non-admin phải pendingLevel.ApproverUserId else Forbidden forged-caller-not-at-level KHÔNG tới được finalize block; (2) `finalizeByCcmDelegation:830-851` threshold-nullConflict / roleCostControlForbidden / `winnerQuoteTotal>=ceoThreshold` strict-`<`Conflict 3 throw trước set; (3) block `return` no-fallthrough. `winnerQuoteTotal` recompute server-side từ Suppliers+Quotes.ThanhTien của SelectedSupplier (`:839-847`) KHÔNG trust client; threshold từ DB `aw.CeoApprovalThreshold`. skipToFinal+finalizeByCcm combo safe (skipToFinal `:818` return non-last-slot HOẶC `:797` no-op fall-through last-slot finalize once, 3 guard vẫn áp). **(c) PASS** class `[Authorize]:14` 2 endpoint mới inherit any-auth, fine-grained handler Forbidden (gotcha#44-safe KHÔNG class-Policy-overstrict). **FE-fix sound strict-tightening:** old `length>0 && !source` để nút ENABLED khi candidates-empty click BE Conflict "Chọn 1 giá chốt"; new `(length===0 || !source)` disable nút khớp amber empty-state `:537` (trước fix message-hiện-cùng-nút-enabled = UX mâu thuẫn). `winnerQuoteTotal:number` non-null candidates-non-empty thực tế (submit-guard >0), fix thuần defensive nhưng đúng. **LEARNED:** "finalize-bypass?" load-bearing proof = đếm guard giữa caller-entry và state-mutation + xác nhận MỖI guard throw TRƯỚC mutation đầu tiên (đây Phase=DaDuyet) + recompute-vs-trust-client của giá-trị-so-ngưỡng (winnerQuoteTotal server-Sum, không nhận body) → 3 gate độc lập (approver-match ∩ role ∩ amount<threshold) mạnh hơn 1; client chỉ chọn-source-label, BE tự tính amount-vs-threshold. **SURPRISE:** uncommitted-fix chỉ edge defensive (candidates thực tế luôn 1 do submit-guard) nhưng vẫn đáng xoá UX-mâu-thuẫn enabled-button-cùng-amber-empty + chống regression nếu submit-guard nới sau này. Tag [s72ter, mig54-authz-lane, finalize-bypass-3gate-proof, server-recompute-not-trust-client, fe-fix-strict-tighten, phase9-uat-pass].

View File

@ -15,7 +15,7 @@ WRITE specialist độc quyền `tests/**`. xUnit + FluentAssertions 7.2 + EF SQ
- ❌ NOT: production code `src/Backend/**` + `fe-*/**` → test reveal bug → REPORT em main, KHÔNG fix
- ❌ NOT: decide WHAT to test (test plan) → em main + reviewer chốt priority
## 📊 Baseline 339 tests = 339 PASS (45 Domain + 294 Infra) ← S74 +5 PE Mig 55 CcmNote (compile-fix arity + test-after): UpdatePeBudgetCcmCommand record +`string? CcmNote` (4 param) → fix 3 existing `new UpdatePeBudgetCcmCommand(...)` thêm `null` arg4 (PeWorkItemBudgetTests.cs line 388/407/421); NEW 5 test mirror ProNote: CostControl-set / Admin-set / null=clear-absolute-set (pre-seed prove not vốn-dĩ-null) / Procurement→Forbidden no-mutate (fail-closed role-gate TRƯỚC EnsureTrackedAsync+side-effect, AsNoTracking re-read) / Initial+Adjustment+CcmNote all-persist-together. **Handler ctor `(db, ICurrentUser)` 2-dep UNCHANGED** — chỉ COMMAND record +CcmNote. No prod bug. Prev 334 ← S72 +28 PE Mig 54 (spec-change+test-after): PeCcmThresholdFinalizeTests 6→11 (AUTO→OPT-IN finalizeByCcmDelegation) + NEW PeApprovedPriceFinalizeTests 10 (giá chốt) + NEW PeSuggestedPriceSetterAuthzTests 13 (2 setter role-gate). Prev 306 ← S69b +14 PE 2 feature anh Kiệt FDC (test-before-merge SECURITY/FINANCIAL): `PeCcmThresholdFinalizeTests.cs` (5, Services ns, value-threshold CCM-finalize ApproveV2Async) + `PeUrgentToggleAuthzTests.cs` (9, Application ns, urgent-toggle role authz). Prev 292 ← S69 +6 Office golive permission-seed (`OfficeModulePermissionSeedTests.cs`, test-after, mirror HrmProfilePermissionSeedTests S67). Prev 286 ← S67 +23 HRM test-after [DepartmentTreeTests 8 cycle-guard/rollup/orphan + PeHoSoLinkTests 9 absolute-set (⚠spec-drift: HoSoLink gửi null=CLEAR, KHÔNG null-safe như Budget*/WorkItemId) + HrmProfilePermissionSeedTests 6 reflection private-static revoke→seed chain]. **em main PROXY-RECORD** — return truncated #53 (chết lúc update MEMORY), 3 file delivered + `dotnet test` 286 PASS verify-on-disk. Prev 263 (S61 +22 PeWorkItemBudget 14 BudgetPolicy; Domain 58→45 drop Budget module). Pre = 254 (S60).
## 📊 Baseline 344 tests = 344 PASS (45 Domain + 299 Infra) ← S76 +5 PE Mig 56 PRO-column-split (spec-change+compile-fix arity): UpdatePeBudgetProCommand record 3→4 param `(PeId, ProInitialAmount, ProAdjustmentAmount, ProNote)`; handler set ProInitial+ProAdjust absolute-set (KHÔNG còn ProEstimateAmount). Vá 5 PRO call-site `new UpdatePeBudgetProCommand(...)` thêm arg ProAdjust + rename assert `.ProEstimateAmount`→`.ProInitialAmount` (handler giờ set ProInitial). Capability `FullAmount`: hasCcm?CCM:proFull(ProInitial+ProAdjust); FullIsEstimate=!hasCcm&&hasPro; DTO +2 field cuối ProInitial/ProAdjust. NEW 5: PRO set both incl ProAdjust ÂM persist / validator ProInitial<0 invalid + ProAdjust ÂM valid (mirror CCM no-sign-constraint) / full=proFull(100+50=150)+DTO-surface / full=proFull ÂM-adjust(100-30=70 no-clamp). Full-fallback seed `ProEstimateAmount=500`→`ProInitialAmount=500` (capability đọc ProInitial). **Handler ctor `(db, ICurrentUser)` 2-dep UNCHANGED**. ProEstimateAmount=LEGACY (Mig 56 backfill→ProInitial, FE bỏ). No prod bug. Prev 339 ← S74 +5 PE Mig 55 CcmNote (compile-fix arity + test-after): UpdatePeBudgetCcmCommand record +`string? CcmNote` (4 param) → fix 3 existing `new UpdatePeBudgetCcmCommand(...)` thêm `null` arg4 (PeWorkItemBudgetTests.cs line 388/407/421); NEW 5 test mirror ProNote: CostControl-set / Admin-set / null=clear-absolute-set (pre-seed prove not vốn-dĩ-null) / Procurement→Forbidden no-mutate (fail-closed role-gate TRƯỚC EnsureTrackedAsync+side-effect, AsNoTracking re-read) / Initial+Adjustment+CcmNote all-persist-together. **Handler ctor `(db, ICurrentUser)` 2-dep UNCHANGED** — chỉ COMMAND record +CcmNote. No prod bug. (compile-fix arity + test-after): UpdatePeBudgetCcmCommand record +`string? CcmNote` (4 param) → fix 3 existing `new UpdatePeBudgetCcmCommand(...)` thêm `null` arg4 (PeWorkItemBudgetTests.cs line 388/407/421); NEW 5 test mirror ProNote: CostControl-set / Admin-set / null=clear-absolute-set (pre-seed prove not vốn-dĩ-null) / Procurement→Forbidden no-mutate (fail-closed role-gate TRƯỚC EnsureTrackedAsync+side-effect, AsNoTracking re-read) / Initial+Adjustment+CcmNote all-persist-together. **Handler ctor `(db, ICurrentUser)` 2-dep UNCHANGED** — chỉ COMMAND record +CcmNote. No prod bug. Prev 334 ← S72 +28 PE Mig 54 (spec-change+test-after): PeCcmThresholdFinalizeTests 6→11 (AUTO→OPT-IN finalizeByCcmDelegation) + NEW PeApprovedPriceFinalizeTests 10 (giá chốt) + NEW PeSuggestedPriceSetterAuthzTests 13 (2 setter role-gate). Prev 306 ← S69b +14 PE 2 feature anh Kiệt FDC (test-before-merge SECURITY/FINANCIAL): `PeCcmThresholdFinalizeTests.cs` (5, Services ns, value-threshold CCM-finalize ApproveV2Async) + `PeUrgentToggleAuthzTests.cs` (9, Application ns, urgent-toggle role authz). Prev 292 ← S69 +6 Office golive permission-seed (`OfficeModulePermissionSeedTests.cs`, test-after, mirror HrmProfilePermissionSeedTests S67). Prev 286 ← S67 +23 HRM test-after [DepartmentTreeTests 8 cycle-guard/rollup/orphan + PeHoSoLinkTests 9 absolute-set (⚠spec-drift: HoSoLink gửi null=CLEAR, KHÔNG null-safe như Budget*/WorkItemId) + HrmProfilePermissionSeedTests 6 reflection private-static revoke→seed chain]. **em main PROXY-RECORD** — return truncated #53 (chết lúc update MEMORY), 3 file delivered + `dotnet test` 286 PASS verify-on-disk. Prev 263 (S61 +22 PeWorkItemBudget 14 BudgetPolicy; Domain 58→45 drop Budget module). Pre = 254 (S60).
> Pattern S67: private-static seed/init → invoke qua REFLECTION (`GetMethod(name, NonPublic|Static)` + `Invoke(null, [db, roleManager, NullLogger.Instance])`); seed MenuItem rows TRƯỚC Permission (FK MenuKey→MenuItem.Key Cascade, SQLite Error 19 nếu thiếu). Cycle-guard test: SqliteDbFixture đủ (no User); rollup-count test cần IdentityFixture (đếm User.DepartmentId active).
Run: `dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1` (MSBuild OOM → serialize build)
@ -54,6 +54,7 @@ Test theo CODE (single source truth), document mismatch header comment + report.
## 📅 Recent activity (last 10 FIFO)
- **2026-06-19 (S76 PE Mig 56 PRO-column-split SPEC-CHANGE + compile-fix arity + test-after):** +5 test `PeWorkItemBudgetTests.cs` **339→344 PASS** (45 Domain + Infra 294299, 0 fail). BE done+builds (Mig 56 `AddProBudgetSplitToPeWorkItemBudget`, em main). **SPEC:** `UpdatePeBudgetProCommand` record 3→**4 param** `(PeId, ProInitialAmount, ProAdjustmentAmount, ProNote)` (mirror CCM Initial/Adjustment); handler set `rec.ProInitialAmount + rec.ProAdjustmentAmount` absolute-set (KHÔNG còn `ProEstimateAmount`). Validator `ProInitialAmount>=0` when HasValue, `ProAdjustmentAmount` KHÔNG ràng dấu (ÂM OK), ProNote max1000. ** Handler ctor `(IApplicationDbContext db, ICurrentUser)` 2-dep UNCHANGED** chỉ COMMAND record đổi arity. Capability `PeBudgetSummaryDto.FullAmount` (PurchaseEvaluationFeatures.cs:849-864): `hasPro = ProInitial∥ProAdjust not-null`; `proFull = ProInitial+ProAdjust`; `full = hasCcm ? CCM(Initial+Adjustment) : proFull`; `FullIsEstimate = !hasCcm && hasPro`. DTO +2 field cuối `ProInitialAmount`,`ProAdjustmentAmount`. **Compile-fix:** 5 PRO call-site `new UpdatePeBudgetProCommand(...)` thêm arg ProAdjust (regex `\([^,]+,[^,]+,[^,)]+\)` = 0 hit sau xác nhận hết 3-arg). **Assertion-vá:** 5 PRO-handler test `.ProEstimateAmount``.ProInitialAmount` (handler giờ set ProInitial). **Full-fallback seed-vá:** capability đọc ProInitial `ProEstimateAmount=500m``ProInitialAmount=500m` (2 chỗ region 5 + 1 CanEdit seed). **NEW 5:** (1) PRO set both ProInitial+ProAdjust ÂM(-20tr)+note all-persist 1-lệnh / (2) validator ProInitial<0 invalid (khác ProAdjust) / (3) validator ProAdjust ÂM valid (mirror CCM no-sign-constraint) / (4) full=proFull(100+50=150)+DTO surface ProInitial/ProAdjust riêng / (5) full=proFull ÂM-adjust(100-30=70 no-clamp = tổng đại số). CCM-present test thêm seed PRO(500+20) chứng minh PRO bị bỏ qua trong full. **No prod bug** code đúng spec (PRO mirror CCM, ProAdjust ÂM intentional, ProEstimateAmount=LEGACY Mig 56 backfillProInitial FE bỏ). Tag [s76, pe-mig56, pro-column-split, spec-change, compile-fix-arity, proadjust-negative-allowed, profull-no-clamp, mirror-ccm, full-fallback, test-after].
- **2026-06-18 (S74 PE Mig 55 CcmNote compile-fix arity + test-after):** +5 test `PeWorkItemBudgetTests.cs` **334→339 PASS** (45 Domain + Infra 289294, 0 fail). Mig 55 additive: `PeWorkItemBudget.CcmNote` (string? nvarchar(1000)) + `UpdatePeBudgetCcmCommand` record 3→**4 param** `(PeId, InitialAmount, AdjustmentAmount, CcmNote)` + DTO `PeBudgetSummaryDto +CcmNote` (slot after AdjustmentAmount). ** Handler ctor `UpdatePeBudgetCcmCommandHandler(IApplicationDbContext db, ICurrentUser)` 2-dep UNCHANGED** chỉ COMMAND record đổi arity, KHÔNG đụng handler signature. **Compile-fix:** grep `new UpdatePeBudgetCcmCommand` toàn tests/ = 3 hit (line 388/407/421 PeWorkItemBudgetTests), thêm `null` arg4 mỗi cái. **NEW 5 test (region 4b, mirror ProNote pattern S61):** (1) CostControl set CcmNotepersist / (2) Admin setpersist / (3) **null=clear absolute-set** (pre-seed CcmNote="cũ" null request BeNull, chứng minh CLEAR không phải vốn-dĩ-null) / (4) **Procurement→ForbiddenException + record no-mutate** (fail-closed: role-gate line 152-157 TRƯỚC WorkItemId-check + EnsureTrackedAsync + side-effect; AsNoTracking re-read assert CcmNote/Initial/Adjustment giữ nguyên) / (5) Initial+Adjustment+CcmNote all-persist-together 1 lệnh. Handler `rec.CcmNote = request.CcmNote` absolute-set; changelog "ghi chú CCM cập nhật" khi `oldCcmNote != request.CcmNote`. **No prod bug** code đúng spec (mirror ProNote, fail-closed role-gate giống Initial/Adjustment). Tag [s74, pe-mig55, ccmnote, compile-fix-arity, absolute-set-null-clear, fail-closed-no-mutate, mirror-pronote, test-after].
- **2026-06-18 (S72 PE Mig 54 anh Kiệt FDC SPEC-CHANGE + 2 test-after FINANCIAL):** +28 test **306→334 PASS** (45 Domain + Infra 261289, 0 fail). BE Mig 54 committed+builds. Mig 54 fields PE: `ProSuggestedMin/MaxPrice` + `CcmSuggestedPrice` + `ApprovedPriceAmount` + `ApprovedPriceSource`; `TransitionAsync`/`ApproveV2Async` +3 param `finalizeByCcmDelegation`(bool) + `approvedPriceAmount`(decimal?) + `approvedPriceSource`(string?). ** SPEC-CHANGE CCM-finalize AUTOOPT-IN UPDATE `PeCcmThresholdFinalizeTests` 611 (Services ns):** S69b AUTO (gói<ngưỡng+CCM tự DaDuyet im-lặng) NAY chỉ finalize khi `finalizeByCcmDelegation=true` + đủ ĐK fail-closed (check theo thứ tự code 832-851: threshold-nullConflict / roleCostControlForbidden / `winnerQuoteTotal>=ceoThreshold`Conflict, strict-`<` giữ nguyên) ApplyApprovedPriceOnFinalize(BẮT BUỘC giá chốt human)→DaDuyet. ApproveAsync helper +3 optional param. Cover: flag-true+<ngưỡng+giáDaDuyet skip-CEO + bind ApprovedPrice / **flag-FALSE+<ngưỡng→KHÔNG finalize advance-CEO (đổi-chính)** / flag-true ==ngưỡng→Conflict + >ngưỡng→Conflict (no-mutation reload-assert) / flag-true non-CCM→Forbidden / flag-true threshold-null→Conflict / flag-true slot-cuối+giá→DaDuyet 1-Approval no-double / flag-true đủ-ĐK null-giá→Conflict("giá chốt"). **② NEW `PeApprovedPriceFinalizeTests` 10 (Services ns) — ApplyApprovedPriceOnFinalize (private-static):** terminal DaDuyet normal-advance human valid-price→bind / null-price human→Conflict NOT-finalized(ChoDuyet reload) / garbage-source→Conflict / Theory 4 valid-source {Ncc,ProMin,ProMax,Ccm} each-set + **isSystem null-price→no-throw no-set qua REFLECTION** (⚠OBSERVATION report: isSystem KHÔNG reachable qua public ApproveV2Async — approve-branch gate `decision==Approve` còn isSystem cần `AutoApprove`; PE no SLA-auto-job chỉ Contract SlaExpiryJob → isSystem-exempt = defensive/dead qua V2-approve; test contract mức UNIT) + đối-chứng isSystem-false→Conflict + isSystem-true amount+garbage-source→Conflict. Reflection unwrap TargetInvocationException→inner. **③ NEW `PeSuggestedPriceSetterAuthzTests` 13 (Application ns) — 2 setter handler 2-dep db+ICurrentUser mirror PeWorkItemGuard:** PRO-cmd Procurement/Admin set Min/Max·CostControl/Drafter→Forbidden+no-set·validator Min>Max invalid+negative invalid+single/null valid·unknown-PE→NotFound(existence TRƯỚC authz). CCM-cmd CostControl/Admin set·Procurement→Forbidden·validator negative invalid·NotFound. **No prod bug** — code đúng spec (OPT-IN finalize an-toàn-hơn AUTO, fail-closed order intentional). FakeCurrentUser configurable-roles. Tag [s72, pe-mig54, ccm-finalize-opt-in, spec-change, approved-price, applyapprovedprice-private-static-reflection, issystem-unreachable-observation, suggested-price-setter-authz, fail-closed, test-after].
- **2026-06-17 (S69b PE 2 feature anh Kiệt FDC — test-before-merge SECURITY+FINANCIAL workflow):** +14 test → **292→306 PASS** (45 Domain + Infra 247→261, 0 fail). BE done+builds, mirror harness PeSubmitGuardAndBypassTests/PeWorkItemGuardTests. **FEATURE B value-threshold CCM-finalize (`PeCcmThresholdFinalizeTests.cs` 5, Services ns, ApproveV2Async line 816-854):** NV duyệt role=CostControl + `aw.CeoApprovalThreshold!=null` + `winnerQuoteTotal < ngưỡng` + chưa-slot-cuối → Phase=DaDuyet bỏ CEO + pointers/SLA null. **⭐ BOUNDARY load-bearing: predicate `winnerQuoteTotal < ceoThreshold` STRICT-less-than (line 838)** → gói==đúng-ngưỡng = KHÔNG finalize = advance. Cover: (1)⭐LOAD-BEARING CCM<ngưỡng mid-wfDaDuyet skip-CEO pointers-null + chỉ CCM-slot opinion no CEO-opinion / (2) ==ngưỡngadvance Bước2(CEO) stays-ChoDuyet SLA+7d / (2b) >ngưỡng→advance / (3) threshold-null→advance kể-cả-CCM+gói-1đ (backward-compat) / (4) non-CCM(PRO)<ngưỡngadvance (chỉ CostControl trigger, nhận-diện-theo-role) / (5) CCM-at-last-slot<ngưỡngDaDuyet via NORMAL-advance (guard `!(idx==last&&lvl==max)` skip finalize-branch, nhánh advance terminal cũng DaDuyet no double, 1 Approve row). Harness: dựng PE TRỰC TIẾP ChoDuyet pin pointer slot CCM (skip submit guard) + drive 1 Approve; `SeedWorkflowAsync(stepApprovers, ceoThreshold)`. **FEATURE A urgent-toggle authz (`PeUrgentToggleAuthzTests.cs` 9, Application ns, SetPurchaseEvaluationUrgentCommandHandler 4-dep db+ICurrentUser+UserManager+INotificationService):** rolecờ: PROIsUrgentByPro / CCMIsUrgentByCcm / AdminCẢ2 / elseForbiddenException. Notify-CEO best-effort try/catch KHÔNG assert (NoOpNotificationService nuốt; CreateUserAsync idempotent-register role nên GetUsersInRoleAsync(Director) no-throw). Cover: PRO-only-ByPro(Ccm-untouched) / CCM-only-ByCcm / Admin-both / DrafterForbidden+no-mutation / FinanceForbidden / PRO-turn-off clears-only-Pro Ccm-preserved / **multi-role PRO+CCM no-Admin→else-if short-circuit chỉ ByPro (LOCK behavior)** / unknown-PENotFound. **No prod bug** cả 2 feature code đúng spec (strict-`<` intentional rollout-safe, else-if priority Admin>PRO>CCM intentional). FakeCurrentUser configurable-roles ctor. Reuse NoOpNotificationService internal qua `using ...Tests.Services`. Tag [s69b, pe-ccm-threshold-finalize, value-threshold, strict-less-than-boundary, role-based-routing, urgent-toggle-authz, forbidden-no-mutation, else-if-short-circuit, test-before-merge].