[CLAUDE] PurchaseEvaluation: cờ gấp PRO/CCM + CCM duyệt-final theo ngưỡng giá trị (Mig 53) + 14 test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m41s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m41s
Yêu cầu anh Kiệt FDC (sau họp sếp). Mig 53 AddPeUrgentAndCeoApprovalThreshold — 3 AddColumn, no new table (Mig 52→53). Rollout an toàn: cột nullable, ngưỡng null = giữ luồng duyệt cũ 100% cho tới khi admin set.
B — CCM duyệt-final theo NGƯỠNG GIÁ TRỊ ("gói CEO phân quyền theo giá trị"):
- ApprovalWorkflow += CeoApprovalThreshold (decimal?, admin nhập trong Workflow Designer).
- ApproveV2Async: actor role CostControl (CCM) + winnerQuoteTotal (tổng giá NCC được chọn) < ngưỡng → DaDuyet luôn (bỏ CEO); ≥ ngưỡng → đẩy lên CEO như cũ. Ngưỡng null = luồng tuyến tính cũ. Q4 chốt nhận diện theo ROLE người duyệt.
- reviewer PASS 0 blocker: cascade-safe (Off/role không lan), tested load-bearing (CCM dưới ngưỡng → DaDuyet skip CEO).
A — cờ gấp per-vai (visibility-only, Q3 KHÔNG đổi luồng):
- PE += IsUrgentByPro (PRO đỏ) / IsUrgentByCcm (CCM xanh).
- Endpoint PUT /purchase-evaluations/{id}/urgent role-gated (Procurement→ByPro, CostControl→ByCcm, Admin→cả 2, khác→Forbidden) + notify CEO (Director) khi MỚI bật (best-effort).
FE ×2 app: Workflow Designer ô "Ngưỡng giá trị gói CEO" (fe-admin) + PE detail nút bật/tắt cờ gấp đỏ/xanh theo role + badge GẤP + hint "giá trị gói vs ngưỡng → CCM duyệt-final/cần CEO" + PE list badge gấp.
DTO: PE detail += isUrgentByPro/Ccm + winnerQuoteTotal + ceoApprovalThreshold; list += isUrgentByPro/Ccm; workflow V2 += ceoApprovalThreshold.
+14 test (292→306): PeCcmThresholdFinalizeTests 5 (B routing) + PeUrgentToggleAuthzTests 9 (A authz). Build slnx 0/0 · npm build ×2 0 err · dotnet test 306 PASS.
C (sau duyệt xong chuyển phiếu đến dự án) — chờ anh Kiệt làm chi tiết form, CHƯA làm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -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 292 tests = 292 PASS (45 Domain + 247 Infra) ← 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 306 tests = 306 PASS (45 Domain + 261 Infra) ← 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,8 @@ Test theo CODE (single source truth), document mismatch header comment + report.
|
||||
|
||||
## 📅 Recent activity (last 10 FIFO)
|
||||
|
||||
- **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-wf→DaDuyet skip-CEO pointers-null + chỉ CCM-slot opinion no CEO-opinion / (2) ==ngưỡng→advance 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ưỡng→advance (chỉ CostControl trigger, nhận-diện-theo-role) / (5) CCM-at-last-slot<ngưỡng→DaDuyet 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):** role→cờ: PRO→IsUrgentByPro / CCM→IsUrgentByCcm / Admin→CẢ2 / else→ForbiddenException. 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 / Drafter→Forbidden+no-mutation / Finance→Forbidden / PRO-turn-off clears-only-Pro Ccm-preserved / **multi-role PRO+CCM no-Admin→else-if short-circuit chỉ ByPro (LOCK behavior)** / unknown-PE→NotFound. **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].
|
||||
|
||||
- **2026-06-17 (S69 Office golive permission-seed regression — test-after SECURITY invariant, public Văn phòng số):** +6 test `tests/.../Application/OfficeModulePermissionSeedTests.cs` → **286→292 PASS** (45 Domain + Infra 241→247, 0 fail). Mirror `HrmProfilePermissionSeedTests` (S67) — SAME reflection harness (invoke 2 private-static `RevokeTemporarilyHiddenModulesAsync` + `SeedAllRolesOfficeModulePermissionsAsync` qua `GetMethod(name, NonPublic|Static).Invoke(null, [db, rm, NullLogger.Instance])`; SqliteDbFixture/IdentityFixture; seed MenuItem rows TRƯỚC Permission FK Cascade). **KHÁC HRM:** Office grant mở **CanRead AND CanCreate** (HRM read-only) trên allow-list **16 key**; HRM chỉ 2 key. Chain = revoke (StartsWith("Off")→all false non-Admin) → office-grant (allow-list→read+create, upgrade-only). **Cover:** (1) chain non-Admin allow-list-16 → read+create=true + **excluded-3 stay hidden** (`OffPhongHopManage`/`OffAttendanceReport`/`OffChamCong` ⭐ LOAD-BEARING security assert) / (2) allow-list Update+Delete stay false / (3) no-leak HRM-dashboard+Personal stay hidden / (4) Admin not-revoked keeps all incl excluded-3 / (5) create-missing-row read+create=true update/delete=false + excluded NOT created / (6) **upgrade-only preserves admin-raised Update/Delete=true** (office-grant chỉ đụng Read/Create, KHÔNG hạ). **No prod bug** — seed logic đúng spec (excluded-3 confirmed hidden, upgrade-only không phá quyền admin). Tag [s69, office-golive, permission-seed, security-invariant, excluded-3-hidden, read+create-grant, upgrade-only, reflection-private-static, test-after].
|
||||
|
||||
- **2026-06-12 (S60 UAT anh Kiệt — 2 feature PE submit branch, test-after build PASS):** +14 test `tests/.../Services/PeSubmitGuardAndBypassTests.cs` → **240→254 PASS** (58 Domain + Infra 182→196, 0 fail). Mirror `PurchaseEvaluationWorkflowServiceGuardTests` (IdentityFixture+SQLite, reuse `NoOpNotificationService` internal). **F1 Section 3 guard (8):** submit branch (DangSoanThao/TraLai→ChoDuyet) build `missing` list 4 mục → ConflictException msg gộp prefix `'Chưa đủ thông tin mục 3 "Đơn vị NCC/TP được chọn"...'` + join `' · '`. Cover: thiếu cả 4 / winner-only / winner+quote=0 / budget (cả null+manual=0) / comparison / **attachment gắn NCC (PES_Id!=null) KHÔNG đếm bảng so sánh = vẫn Conflict** (predicate PES_Id==null) / đủ-4-manual-budget→ChoDuyet / đủ-4-BudgetId→ChoDuyet. **F2 drafter-bypass (6, V2-only `ApplyDrafterBypassOnSubmitAsync`):** k=drafterSlots.Max(Order) bước đầu → auto Cấp 1..k. Cover: drafter=TP(2/2)+2bước→StepIdx=1/Lvl=1+opinion 1 row slot TP+2 AutoApprove / drafter=NV(1/2)→Lvl=2 cùng bước+opinion slot NV / drafter ngoài bước đầu→KHÔNG bypass StepIdx=0 Lvl=1 0-auto / 1-bước+drafter cấp cuối→DaDuyet pointers null SLA null / V1(awId null)→submit OK no-bypass no-crash / TraLai-resubmit→bypass áp lại opinion UPSERT 1 row + approval cộng dồn 2 vết. **⚠️ GUARD-FIRST:** mọi bypass-test PHẢI dựng PE đủ 4 ĐK Section 3 (winner+quote>0+manual-budget+comparison-attach) qua guard. **Seed pattern S60:** `SeedWinnerWithQuoteAsync`(PES+Detail+Quote ThanhTien) map winner→quote sum · `SeedComparisonAttachment`(PES_Id=null) · `SeedWorkflowAsync(Guid[][] stepApprovers)` build multi-step V2 1-NV/cấp. **Opinion-only-ownSlot invariant:** bypass cấp NV skip KHÔNG ghi opinion (chỉ Approval AutoApprove + Changelog vết); assert `opinions.HaveCount(1)` + `ApprovalWorkflowLevelId==drafterSlot.Id`. **No prod bug** — code đúng spec, test theo CODE (S34 rule). Tag [s60, pe-submit-guard, section3-completeness, drafter-bypass, v2-only, guard-first, opinion-ownslot-only, test-after].
|
||||
@ -66,8 +68,6 @@ Test theo CODE (single source truth), document mismatch header comment + report.
|
||||
|
||||
- **2026-06-08 (S52 P11-E + P11-F WorkflowApps/Attendance test-after):** +5 test → **191 PASS** (Infra 128→133). 2 file `tests/.../Application/`: **ItTicketCodeGenTests** (3 — MaTicket regex `^IT/\d{4}/\d{3}$` + sequential 001→002 cùng prefix `IT/{year}` LastSeq++ + per-year-prefix 2027 reset 001) + **AttendanceReportTests** (2 — full aggregate day-type/weighted + DepartmentId filter). **⭐ Serializable-on-SQLite GOTCHA = NON-ISSUE (confirmed):** `WorkflowAppCodeGen.GenerateMaDonTuAsync` dùng `BeginTransactionAsync(IsolationLevel.Serializable)` chạy SẠCH trên SQLite — provider map isolation level gracefully (no throw), format+seq+per-year đều hold KHÔNG cần try/skip. Đã proven sẵn bởi WorkflowAppApproveV2Tests (DT/LR path). Handler `CreateItTicketHandler(db, cu, clock)` = 3 dep MediatR. **Day-type test pattern (P11-E core):** holiday check chạy TRƯỚC weekend/weekday → seed 2026-06-01 (thứ Hai) vào holidaySet → assert phân **Holiday** dù là weekday (override day-of-week). Holiday.Date=DateOnly → `BuildHoliday` dùng `DateOnly.FromDateTime`. OtWeighted = 2×1.5+3×2.0+1×3.0=12.0m. DepartmentId filter: seed 2 Department row + 2 user khác dept → query deptA chỉ trả 1 row (handler join Users `u.DepartmentId==deptId`, userMeta dùng `DefaultIfEmpty` nên dept row optional nhưng seed cho DepartmentName assert). No prod bug. **⚠️ MSBuild OOM** chạy full parallel → dùng `-maxcpucount:1 -p:BuildInParallel=false` (env resource, KHÔNG test fail). Tag [s52, p11-e, p11-f, codegen, day-type, serializable-sqlite-ok, test-after].
|
||||
- **2026-06-08 (S51 P11-C HMW Wave2 filtered-unique gotcha #57):** +4 test `tests/.../Application/HrmConfigFilteredUniqueTests.cs` → **185 total = 183 PASS + 2 RED** (Infra 123→127). Mirror HolidayTests Case 7 (seed soft-deleted Code-slot → Create same Code → assert success + active==1 + all==2). **2 GREEN** Vehicle+Driver (Mig 44 config ĐÃ filtered → 2 catalog mới đúng). **2 RED INTENTIONAL = gotcha #57 REPRODUCED** (test-before): `CreateLeaveType_OnSoftDeletedCodeSlot...` → `SQLite Error 19 UNIQUE constraint failed: LeaveTypes.Code` + `CreateShift_OnSoftDeletedCodeSlot...` → `ShiftPatterns.Code` (bare `.IsUnique()` đếm cả row soft-deleted; handler app-check `!IsDeleted` PASS → Add+SaveChanges → DbUpdateException). NOT test lỗi — REPORTED em main fix Mig 45 `.HasFilter("[IsDeleted]=0")` cho 2 config → flip GREEN. **⚠️ Soft-delete trong test (giống Holiday):** AuditingInterceptor (prod soft-delete Deleted→Modified+IsDeleted=true) KHÔNG wire trong SqliteDbFixture → `Remove+SaveChanges` = HARD delete (không test được). PHẢI seed row `IsDeleted=true` thủ công để mô phỏng slot bị chiếm. Handlers chỉ cần IApplicationDbContext → `new CreateXxxHandler(db)`. Tag [s51, p11-c, gotcha-57, filtered-unique, test-before].
|
||||
- **2026-05-30 (S43 P11-B Wave3 LeaveBalance):** +8 test `tests/.../Application/LeaveBalanceTests.cs` → **152 PASS** (Infra 86→94). Deduction hook (ApproveLeaveRequestHandler terminal) full: deduct single-level (create row from DaysPerYear), only-at-terminal multi-level (advance no-deduct + 1× terminal), accumulate UPSERT (5+2=7 no new row), negative allowed (Used20>Entitled12 → Remaining−8 no throw), Reject+Return no-deduct (split 5a/5b), GetMyLeaveBalances lazy synth (2 active type filter inactive), AdjustLeaveBalance upsert. **⚠️ FOUND + FIXED 2 pre-existing RED** in S42 template (`Approve_LastLevel_TransitionsToDaDuyet` + `Approve_EmptyComment_StoresPlaceholder`): Wave 1 deduction hook (uncommitted, prod) làm terminal insert LeaveBalance FK→LeaveTypes Restrict FAIL vì BuildLeave dùng `LeaveTypeId=Guid.NewGuid()`. **NOT prod bug** (prod đơn luôn pin LeaveType thật) — fix tại test: BuildLeave +optional leaveTypeId, seed LeaveType ở 2 test đó. Baseline thật trước S43 = 142-pass/2-RED (KHÔNG phải 144-green). REPORTED em main.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Anti-patterns (DO NOT)
|
||||
|
||||
Reference in New Issue
Block a user