[CLAUDE] Docs: S63 closeout S60-62 — re-tier STATUS/HANDOFF + count-flush (Mig 50, 88 bang, 263 test, 64 gotcha) + reconcile stray reviewer + gotcha #63/#64
Viet bu docs cho S60/S61/S62 (ship code prod-verified nhung chua closeout — drift bat o /session-start S63 qua git log). - Reconcile stray reviewer cwd-misland: MOVE 2 file con fe-admin/.claude -> canonical + pointer (no overwrite 31KB) + xoa stray - Commit harvest S61/S62: cicd-monitor MEMORY (Run #286) + gotcha #63 (EF RenameColumn sai-semantics) + #64 (Design-DB vs Dev-DB data-migrate) - Count-flush 4 file: Mig 49->50, tables 93->88, test 240->263 (45D+218I), gotcha 62->64, menu 57->53, Budget module REMOVED->PeWorkItemBudgets - Session-log bu 2026-06-12-S60-S62-pe-budget-workitem-softwarning.md. Docs-only -> CI skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -47,10 +47,10 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve
|
||||
- **Gitea:** `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` · workflow `.gitea/workflows/deploy.yml` · paths-ignore `['docs/**','**/*.md','.claude/skills/**']`
|
||||
- **Prod:** api/admin/eoffice `.solutions.com.vn` · SSH `ssh vietreport-vps` (Administrator, id_ed25519) · IIS site phys paths (S42 verified): API `C:\inetpub\solution-erp\api` · admin `\fe-admin` · user `\fe-user` (3 sites Started). DB `.\SQLEXPRESS`/`SolutionErp`/`vrapp` SQL-auth. **Conn string key = `ConnectionStrings.Default` (NOT `DefaultConnection`!)** — read pw from prod appsettings.Production.json when `$env:PROD_DB_PASSWORD` empty.
|
||||
- **SSH→PS quoting (S42 lesson):** nested bash→ssh→powershell mangles `$var`/`\"`. Use `iconv UTF-16LE | base64` → `powershell -EncodedCommand $B64`. Single-quote literal paths.
|
||||
- **Tests baseline:** **228 PASS** (S56 Run #379 sha a20cde8; Domain 58 + Infra 170 = +12 golive-harden `ItTicketReassignAuthzTests`/`LeaveBalanceTests`/`WorkflowAppApproveV2Tests`/`DocxRendererTests` vs prev 216). CI gate runs both test projects BEFORE build/deploy → status=success ⟹ test gate passed (`tasks` endpoint reports terminal as `status:success`, `conclusion` field NOT populated). Local grep undercounts (Theory/InlineData) — trust CI conclusion. Phase 9 UAT mode skip per chunk OK.
|
||||
- **Mig latest repo:** **Mig 48 `20260609020759_AddProjectMasterFields`** (S55; AddColumn-only, Project +Year/Investor/Location/Package nullable, NO new table; kèm `SeedRealMasterDataAsync` ungated). Prev Mig 47 FilterMasterCatalog... + 46 AddSlaFieldsToItTicket. Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`. ⚠️ Table-count: `sys.tables` (is_ms_shipped=0) count = **93** (S56 Run #379 verified, Mig 48 col-only no delta); narrative also 93 now — reconciled. Don't FAIL on 92↔93 convention diff.
|
||||
- **Tests baseline:** **263 PASS** (S62 Run #286 sha 7926c21 spec; 45 Domain + 218 Infra — em-main supplied; supersedes prev 228/240/256). CI gate runs both test projects BEFORE build/deploy → status=success ⟹ test gate passed (`tasks` endpoint reports terminal as `status:success`, `conclusion` field NOT populated). Local grep undercounts (Theory/InlineData) — trust CI conclusion. Phase 9 UAT mode skip per chunk OK.
|
||||
- **Mig latest repo:** **Mig 50 `20260612173224_ReplaceBudgetModuleWithPeWorkItemBudgets`** (S61; DROPS old Budget module tables + adds PeWorkItemBudgets — schema net-reduce). Prev Mig 49 `AddWorkItemToPurchaseEvaluation` + 48 AddProjectMasterFields. Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/` (50 mig .cs non-designer total). Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`. ⚠️ Table-count: `sys.tables` (is_ms_shipped=0, excl mighist) = **88** (S62 Run #286 verified — S61 Budget-replace DROPPED tables 93→88). Narrative-93 is STALE pre-S61 — when commit touches no schema, 88 is correct, don't FAIL on 88↔93. Always cross-ref COMMIT scope vs ambient count.
|
||||
- **Bearer:** admin `admin@solutions.com.vn/Admin@123456` (full) · UAT `nv.test@solutions.com.vn/TestUser@123456` (Drafter CCM, gotcha #44 check)
|
||||
- **Bundle hash live S59 (CLOSE-OUT FINAL-v2):** admin `B1DtNT9C` · user `D6uF3Mln` (Run #282 sha 792c030, ROTATED from #280 `BKy_8OO9`/`XcZ6PRyA` — supersede-chain #281`80b64dd`cancelled→#282 ships paymentTerms-removal + "+Thêm hạng mục" drop ×2). Bundle size ~800KB/750KB gz. ⚠️ S50 mid-deploy transient lesson: pre-success snapshot can show intermediate FE copy in-flight — re-confirm hash AFTER status=success ALWAYS (anti-pattern #3).
|
||||
- **Bundle hash live S62:** admin `0xKYGhhf` · user `C81ZdG9G` (Run #286 sha 7926c21, ROTATED from S61 `DsGZlNzT`/`DTL_bjzQ` — PE budget soft-warning allow-negative FE×2). Prev-prev S59 `B1DtNT9C`/`D6uF3Mln` (now 2 deploys stale). ⚠️ S50 mid-deploy transient lesson: pre-success snapshot can show intermediate FE copy in-flight — re-confirm hash AFTER status=success ALWAYS (anti-pattern #3).
|
||||
- **DB pw (S42, when `$PROD_DB_PASSWORD` empty):** `vrapp/buKL3TGBkD0wDDbYVw65QeX9` read from `C:\inetpub\solution-erp\api\appsettings.Production.json`→`ConnectionStrings.Default`. ⚠️ Skill-doc path `C:\inetpub\apps\SolutionErp\Api` is STALE → real path `C:\inetpub\solution-erp\api`. sqlcmd over SSH works direct (no UTF-16 encode needed). ⚠️ sys-catalog string-concat queries hit collation conflict (`Latin1_General_CI_AS_KS_WS` vs `SQL_Latin1_General_CP1_CI_AS`) → add `COLLATE DATABASE_DEFAULT` per concatenated column.
|
||||
|
||||
## 🔑 Critical config (flag commit nếu tái xuất)
|
||||
@ -68,6 +68,7 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code /
|
||||
|
||||
## 📅 Recent runs (FIFO — older → archive/git)
|
||||
|
||||
- **2026-06-13 S62 Run #286 (run_number 286, id400) sha=`7926c21` PASS ~4m41s (PE "vượt ngân sách" → SOFT-WARNING: gỡ chặn số âm — CROSS-STACK 1 BE validator-rule-removal + 2 FE PeDetailTabs ×2 + 1 test flip, NO migration):** Push `79ef8da..7926c21` 4 files: BE `PurchaseEvaluationFeatures.cs` (gỡ 1 FluentValidation rule `ExpectedRemainingAmount >= 0` trong `AdjustPurchaseEvaluationBudgetCommandValidator`) + `PeDetailTabs.tsx` ×2 app (allowNegative row8 + banner "Vượt ngân sách") + `PeWorkItemBudgetTests.cs` (flip 1 test). `.cs`+`.tsx` → full pipeline RAN. GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod `appsettings.Production.json`→`ConnectionStrings.Default`. Run IN-PROGRESS first poll (running 11:15) — correctly did NOT FAIL/verify-bundle-mid-flight (anti-pattern #3), polled iter5 status=success (started 11:14:00 → success 11:18:41 ≈4m41s). CI gate (both test proj pre-deploy ⟹ status=success ⟹ test gate **263** baseline (45 Domain + 218 Infra) passed; `tasks` endpoint reports terminal as `status:success`, `conclusion` empty — trust success). **Bundle ROTATE BOTH (load-bearing FE×2, verified AFTER status=success +re-confirm STABLE no transient — anti-pattern#3): admin `DsGZlNzT→0xKYGhhf` + user `DTL_bjzQ→C81ZdG9G`** ✓ both touched (FE changed both apps). Title "Solutions ERP · Admin" preserved. Health live+ready **200/200** + admin/eoffice root 200. Smoke: PE unauth **401** (auth gate real) + control `/api/zzz-not-a-route` **404** (routing live, 401 not catch-all). **NO migration** — prod `__EFMigrationsHistory` top = `20260612173224_ReplaceBudgetModuleWithPeWorkItemBudgets` (= S61 "Mig 50", Budget→PeWorkItemBudgets replace) == repo HEAD, GIỮ NGUYÊN ✓ (commit 0 migration files; repo 50 mig .cs total). sys.tables(excl mighist)=**88** (S61 replace-mig DROPPED Budget tables 93→88; convention/count shift from S61 NOT this commit — FE+validator-only cannot alter schema). 0 regression. **LESSON: validator-rule-removal (negative-allow soft-warning) = internal handler-pipeline behavior — cannot curl-assert "now accepts negative ExpectedRemainingAmount" without authed multi-step adjust flow → rely on +flip PeWorkItemBudgetTests in CI gate 263 passing + bundle-rotate-both (FE banner shipped). Table count 88 (not 93) is S61 Budget-replace aftermath, not regression — always cross-ref what the COMMIT touched vs ambient schema state.** Tag `[s62, run286, pass, pe-budget-soft-warning, allow-negative, cross-stack, bundle-rotate-both, no-mig, test263, tables88-s61-aftermath]`.
|
||||
- **2026-06-12 S60 Run #283 (run_number 283) sha=`37122f0` PASS ~5m (PE guard 4-thông-tin mục 3 khi gửi duyệt + bypass người-soạn-trong-chuỗi-duyệt + rename heading "Đơn vị NCC/TP được chọn" — CROSS-STACK 1 BE service + 2 FE PeDetailTabs ×2 + 1 NEW test file):** Push `792c030..37122f0` 7 files: `PurchaseEvaluationWorkflowService.cs` (BE submit-guard + drafter-bypass) + `PeDetailTabs.tsx` ×2 app + `PeSubmitGuardAndBypassTests.cs` (NEW, +14 → 240→**254** expected) + 3 agent-memory `.md` (harvest-curator/investigator-codebase/test-specialist — `.claude/agent-memory/**` matches `**/*.md` glob → ignored, but `.cs`+`.tsx` present ⟹ whole-range builds, Discovery #3). GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod `appsettings.Production.json`→`ConnectionStrings.Default`. Run IN-PROGRESS first poll (running 11:55) — correctly did NOT FAIL, polled iter5 status=success (started ~11:54 → success 11:59:26 ≈5m). CI gate (both test proj pre-deploy ⟹ status=success ⟹ test gate 254 passed; `tasks` endpoint reports terminal as `status:success`, `conclusion` empty — trust success). **Bundle ROTATE BOTH (load-bearing, verified AFTER status=success — anti-pattern#3): admin `B1DtNT9C→akytoBnc` + user `D6uF3Mln→BzSdQmN0`** ✓ both touched (FE changed both apps). Brand `1F7DC1` preserved both HTML. Health live+ready **200/200** + admin/eoffice root 200. Smoke: PE unauth **401** + contracts unauth **401** + control `/api/zzz-not-a-route` **404** (auth gates real, routing live). **NO migration** — prod `__EFMigrationsHistory` top = Mig 49 `AddWorkItemToPurchaseEvaluation` == repo, GIỮ NGUYÊN ✓ (PE.MaPhieu col, not Code). sys.tables(excl mighist)=**92** (convention diff vs narrative-93, no new table — col/logic-only). **DATA INTACT (no-touch verify, sqlcmd): PE_count=3 · PE/2026/A/001 EXISTS (=1, phiếu UAT thật giữ nguyên ✓) · Suppliers=23 · WorkItems=71** — counts vs S59 (PE was 1 #275, Suppliers 22 #278) GREW from legit ongoing-UAT (this commit = FE+BE-service, NO DbInitializer/seed change → cannot resurrect/wipe; growth is user activity not deploy-induced). 0 regression. **LESSON (cross-stack submit-guard + drafter-bypass = ship-proof via run-success + test 254 + bundle-rotate-both + PE-data-preserved; the guard/bypass logic is internal handler behavior — cannot curl-assert "block submit when mục-3 incomplete" or "skip drafter in chain" without authed multi-step flow → rely on +14 PeSubmitGuardAndBypassTests in CI gate passing). SSH→sqlcmd via `iconv UTF-16LE|base64`→`powershell -EncodedCommand` (nested bash→ssh→PS strips `$vars`/mangles quotes); PE code column = `MaPhieu` NOT `Code`.** Tag `[s60, run283, pass, pe-submit-guard, drafter-bypass-in-chain, cross-stack, bundle-rotate-both, no-mig, test254, pe-a001-preserved]`. **↳đợt2 (14:14): Run #284 (run_number 284, id398) sha=`6db195d` PASS ~4m31s — GỠ hành động "Từ chối" khỏi quy trình PE (chỉ còn Duyệt/Trả lại; CROSS-STACK Domain `PurchaseEvaluationPolicy.cs` + Infra `PurchaseEvaluationWorkflowService.cs` guard + FE `PeWorkflowPanel.tsx` ×2 app + 2 NEW test `PurchaseEvaluationPolicyTests`/`PurchaseEvaluationWorkflowServiceGuardTests`, +2 → 254→256 expected: 59 Domain + 197 Infra). Push `37122f0..6db195d` 6 files (.cs+.tsx → full pipeline). Tokens empty → anon Gitea API + prod appsettings DB pw `ConnectionStrings.Default`. Run IN-PROGRESS first poll (running 14:31) — correctly did NOT FAIL, polled iter5 status=success (14:30:51→14:35:22 ≈4m31s; CI both-proj-pre-deploy ⟹ success ⟹ 256-gate passed, `conclusion` empty trust success). Bundle ROTATE BOTH (verified AFTER status=success — anti#3): admin `akytoBnc→DSvM8h3A` + user `BzSdQmN0→Cs2Tt5n6` ✓ both touched. Health live+ready 200/200 + admin/eoffice root 200 + PE unauth 401 + control /api/zzz-not-a-route 404. NO migration — prod top=Mig 49 `AddWorkItemToPurchaseEvaluation`==repo GIỮ ✓. sys.tables(excl mighist)=92. DATA INTACT: PE_count=4 (grew from 3 @#283 — legit ongoing-UAT; BE-policy+FE-only NO seed change → cannot resurrect/wipe) · PE/2026/A/001 EXISTS (=1 phiếu UAT thật giữ ✓). 0 regression. LESSON: "Từ chối"-removal = internal policy/handler behavior, cannot curl-assert "reject action gone" without authed multi-step flow → rely on +2 PolicyTests/GuardTests in CI gate passing. Tag `[s60-dot2, run284, pass, pe-remove-reject-action, cross-stack, bundle-rotate-both, no-mig, test256, pe-a001-preserved]`.**
|
||||
- **2026-06-11 S59-CLOSE Run #280 (run_number 280) sha=`69997da` PASS ~4m24s (FINAL đóng sổ session — FE-only ×2 PeDetailTabs+PeHeaderForm bỏ ô "Tên ngân sách" manual budget UAT vòng4):** Push `f21c55d..69997da` 4 `.tsx` (PeDetailTabs+PeHeaderForm ×2 app). **Run #279 (id393) sha=`f21c55d` (NCC table-fixed UAT vòng3) = `cancelled` @18:22:33 — supersede-BENIGN:** #280 push @18:22:34 (1s gap → Gitea concurrency-cancel in-flight) + `git merge-base --is-ancestor f21c55d 69997da`=TRUE ✓ (f21c55d preserved trong HEAD, ships via #280 — verified diff f21c55d→69997da chỉ +4 PeDetail/Header file, không re-touch 12 file vòng3). Tokens empty → anon Gitea API + prod appsettings DB. Poll iter4 status=success (18:22:34→18:26:58). **Bundle ROTATE BOTH FINAL (verified AFTER success +re-confirm STABLE no transient — anti#3): admin `BSh2fG2X→BKy_8OO9` + user `D22KfpPc→XcZ6PRyA`** ✓ session-close hash, brand `1F7DC1`+"Solutions ERP" preserved ×2. Health live+ready **200/200** + admin/eoffice root 200 + PE unauth 401 + control 404. **NO migration** (FE-only, Mig 49 held). LESSON (mirror Run #385 supersede-chain): same-SHA `cancelled` mid-flight = concurrency-supersede bởi newer push (1s HEAD-move), KHÔNG build/deploy-fault → ancestor-check TRUE = benign, verify prod qua SUCCESSFUL run #280 (NOT cancelled #279), KHÔNG escalate. Tag `[s59-close, run280-pass, run279-cancelled-benign, supersede-chain, fe-budget-name-remove-x2, bundle-rotate-both-FINAL, no-mig]`. **↳FINAL-v2 (tối): `80b64dd` (Run #281 cancelled-BENIGN) gỡ "Điều khoản thanh toán" 3-form ×2 → superseded bởi `792c030` Run #282 PASS ~4m (UAT vòng6 bỏ nút "+Thêm hạng mục" PeDetailTabs ×2). Ancestor 80b64dd⊂792c030=TRUE ✓ (792c030 chỉ re-touch PeDetailTabs, KHÔNG đụng PeHeaderForm/PeWorkspaceCreateView → paymentTerms-removal survives). Verify qua #282-success. Bundle ROTATE BOTH ĐÓNG-SỔ-THẬT (AFTER success +re-confirm STABLE no transient): admin `BKy_8OO9→B1DtNT9C` + user `XcZ6PRyA→D6uF3Mln` ✓, brand `1F7DC1` ok. Health live+ready 200/200 + 2 FE root 200. NO mig (FE-only). Lần thứ 3 liên tiếp supersede-chain (#279/#281 cancelled-benign) — pattern stable.**
|
||||
- **2026-06-11 Run #278 (run_number 278) sha=`9c330d2` PASS ~3m45s (S59-đợt6 CROSS-STACK — BE SuppliersController POST hạ `[Authorize(Roles="Admin,CatalogManager")]` → class-level `[Authorize]` any-auth (anh chốt quick-add NCC đi-thầu phát sinh liên tục), PUT/DELETE GIỮ khóa Admin+CatalogManager; FE×2 PeDetailTabs AddSupplierDialog SearchableSelect+quick-create+upload-multi + PeWorkflowPanel ẩn Trả-lại/Từ-chối khi drafterUserId==currentUser):** Push `faed59f..9c330d2` 5 files: 4 FE `.tsx` (PeDetailTabs+PeWorkflowPanel ×2 app) + `SuppliersController.cs`. `.cs`+`.tsx` → full pipeline RAN. GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod appsettings.Production.json→`ConnectionStrings.Default` (`vrapp/buKL3...`). Run IN-PROGRESS first poll (running) — polled iter6 status=success. **Bundle ROTATE BOTH (load-bearing FE×2, verified AFTER status=success +re-confirm stable ×2 NO transient — anti-pattern#3): admin `ex7Tc92G→BSh2fG2X` + user `DzUeSk96→D22KfpPc`** ✓ both touched. Brand `1F7DC1`+"Solutions ERP" preserved. Health live+ready **200/200** + admin/eoffice root 200. **NO migration** — prod `__EFMigrationsHistory` top = Mig 49 `AddWorkItemToPurchaseEvaluation` == repo ✓. **★ AUTHZ PROBE 4-điểm (the change-point — asymmetric POST-open/DELETE-locked verify) ALL PASS:** (a) unauth POST /api/suppliers (no token) = **401** ✓ (vẫn phải login, KHÔNG anonymous — class `[Authorize]` giữ); (b1) nv.test (Drafter non-admin) POST `{code:ZZCICD-TEST,type:1}` = **201** ✓ id `bc64c0c0-...` (quick-add mở OK); (b2) nv.test DELETE same-id CÙNG token = **403** ✓ (Sửa/Xóa vẫn khóa Admin+CatalogManager — method-level attr giữ); cleanup admin DELETE = **204** + GET = **404** ✓ (probe gỡ sạch). Spot sqlcmd ground-truth: **WorkItems active=71 HELD** ✓ (no resurrect) + **Suppliers active=22** ✓ (==pre-probe; DELETE là SOFT `IsDeleted=1` → active count về 22, total=23 với 1 tombstone ZZCICD `IsDeleted=1` — by-design audit, KHÔNG leak: list/GET ẩn nó). ⚠️ API `/api/suppliers` list trả 20 (paginated/filtered default page — KHÔNG authoritative, dùng sqlcmd cho count thật). Test gate (CI both proj pre-deploy ⟹ success=passed). 0 regression. **LESSON: cross-stack authz-relax verify = probe CẢ asymmetry — (i) unauth vẫn 401 (relax ≠ anonymous), (ii) target-role action mở (201), (iii) SIBLING action vẫn locked (403 same token), (iv) cleanup soft-delete → active-count về baseline + tombstone total+1 (soft-delete ≠ rác nếu list/GET ẩn). API list-count KHÔNG tin (pagination) → sqlcmd `WHERE IsDeleted=0` cho count thật.** Tag `[s59-dot6, run278, pass, supplier-post-authz-relax, asymmetric-probe-401-201-403, soft-delete-cleanup, bundle-rotate-both, no-mig, wi71-held]`.
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
> Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53).
|
||||
> Full verbatim history pre-S40 → git `d2f52ba` + `archive/2026-05-q1..q2.md`.
|
||||
|
||||
## 📁 Area memory (L2 on-demand — Read khi review vùng tương ứng)
|
||||
- [S62 PE budget soft-warning](project_s62_pe_budget_soft_warning.md) — PASS: hard-block→soft-warning; submit-guard intact + validator giữ `BudgetPeriodAmount>0`, row8 negative-safe (additive-only). Validator class `PurchaseEvaluationFeatures.cs:317` (reconcile S63: stray cwd-misland → canonical, anchor verified).
|
||||
- [Wire/mirror claim verification anchors](feedback_wire_claim_verification_anchors.md) — sha256 twin-file · `git diff -U0` isolate true-adds · `allowNegative` bleed check · guard-still-intact grep.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Role baseline
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
---
|
||||
name: wire-claim-verification-anchors
|
||||
description: Adversarial verification recipe for FE+BE mirror changes — sha256 twin-file check, git diff -U0 to isolate true adds, allowNegative bleed check
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
# Verification anchors that caught/cleared issues in adversarial review
|
||||
|
||||
**Rule:** For changes claiming FE-admin/FE-user mirror + a validation relaxation, run these independent checks rather than trusting the spec's self-description.
|
||||
|
||||
**Why:** Specs say "2 files byte-identical" and "added allowNegative to ONE field" — both are claims to verify, not facts. The diff context can show pre-existing sibling code (e.g. another `allowNegative` field) that looks like part of the change but isn't.
|
||||
|
||||
**How to apply:**
|
||||
- **Twin-file identity:** `sha256sum` both mirrored files — equal hash proves byte-identical (don't eyeball). S62: both PeDetailTabs.tsx = same sha256.
|
||||
- **Isolate true additions:** `git diff -U0 -- <file> | grep '^\+' | grep -i <token>` shows ONLY added lines, filtering out unchanged context. S62: spec mentioned `allowNegative` but full-context grep showed 2 occurrences — `-U0` proved only 1 was actually added (the other, `bs.adjustmentAmount` CCM row, was pre-existing and already negative-by-design). Prevented a false "scope bleed" flag.
|
||||
- **allowNegative bleed:** when a field gains `allowNegative`, confirm sibling currency inputs that must stay positive (e.g. budget input) do NOT have it. S62: row8 has it (1268), row3 budget input (1189) does not. Correct.
|
||||
- **Guard-still-intact:** when relaxing one validation rule, grep the related submit/transition guard separately and read ±4 lines to confirm it wasn't loosened in the same edit.
|
||||
@ -0,0 +1,21 @@
|
||||
---
|
||||
name: s62-pe-budget-soft-warning
|
||||
description: S62 review PASS — PE "vượt ngân sách" hard-block→soft-warning; validator drop ExpectedRemaining>=0, FE amber banner + allowNegative row8
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# S62 — PE budget over-spend: hard-block → soft-warning (PASS)
|
||||
|
||||
Reviewed 2026-06-13. Anh Kiệt FDC directive: phiếu PE khi giá trị NCC vượt ngân sách → ô "Giá trị thực hiện dự kiến còn lại" (row8) ra âm → bị chặn lưu. Chốt: cho lưu, chỉ cảnh báo mềm.
|
||||
|
||||
**Why:** Adversarial review of a small (4-file, ~20 LOC) validation-relaxation change. Verdict PASS.
|
||||
|
||||
**How to apply:** When a future change touches PE budget validation or `ExpectedRemainingAmount`, recall these verification anchors:
|
||||
- BE submit guard `PurchaseEvaluationWorkflowService.cs:198` = `BudgetPeriodAmount is null || <= 0` → adds "chưa nhập Ngân sách kỳ này". This is the budget>0 enforcement; must NOT be loosened when relaxing row8.
|
||||
- FE mirror of that guard: `PeDetailTabs.tsx:178` (`budgetPeriodAmount == null || <= 0`). BE+FE predicate kept in sync (S61 lineage).
|
||||
- Validator `AdjustPurchaseEvaluationBudgetCommandValidator` keeps `BudgetPeriodAmount.GreaterThan(0)`, drops `ExpectedRemainingAmount.GreaterThanOrEqualTo(0)`.
|
||||
|
||||
**Negative-safety chain (row8 can now be negative — verified no break):** `row8 = expectedRemainingAmount ?? row7`; `row9 = row4 + row8`; `cmpFull = full - row9`; `cmpPeriod = row3 - row4`. All pure additive — no division by row8, no sqrt, no unsigned cast. BE projection (PurchaseEvaluationFeatures.cs:1038) + CreateContractFromEvaluation clone (line 155) pass the value through with zero arithmetic.
|
||||
|
||||
**Precedent:** mirrors LeaveBalance allowing negative balance (cited in code comment as the in-repo precedent for a domain quantity going negative).
|
||||
@ -17,7 +17,7 @@ Skill này là tài liệu chuyên biệt để Claude (và developer khác) dù
|
||||
| Skill | Mục đích | Trigger ví dụ | Trạng thái |
|
||||
|---|---|---|---|
|
||||
| `dependency-audit-erp` | Scan CVE NuGet + npm 2 FE, respect pin constraint (MediatR 12.4.1, Swashbuckle 6.9.0) | "npm audit", "dotnet vulnerable", "deps scan", "nâng cấp package" | ✅ New Tier 3 |
|
||||
| `ef-core-migration` | Tạo/revert EF Core 10 migration, 3-file rule, DesignTimeDbContextFactory, **49 migration history** (Init → AddWorkItemToPurchaseEvaluation Mig 49) | "thêm migration", "EF migration", "schema update", "snapshot lỗi" | ✅ Updated S58 (Mig 49 PE WorkItem) |
|
||||
| `ef-core-migration` | Tạo/revert EF Core 10 migration, 3-file rule, DesignTimeDbContextFactory, **50 migration history** (Init → ReplaceBudgetModuleWithPeWorkItemBudgets Mig 50) | "thêm migration", "EF migration", "schema update", "snapshot lỗi" | ✅ Updated S61 (Mig 50 Budget→PeWorkItemBudgets) |
|
||||
| `iis-deploy-runbook` | 3 IIS site + win-acme cert + gitea-runner + LibreOffice + debug 500/502/SignalR prod + **G-084 IPv4/IPv6 hardening** | "prod 500", "IIS fail", "cert hết hạn", "restart app pool", "deploy IIS", "port hijack" | ✅ Updated (G-084) |
|
||||
|
||||
## Format chuẩn 1 skill
|
||||
@ -87,5 +87,5 @@ when-to-use:
|
||||
## Related
|
||||
|
||||
- `docs/CLAUDE.md` — quick rules + full stack context
|
||||
- `docs/gotchas.md` — 60 bẫy đã gặp (latest #60 Identity seed CreateAsync silent-fail vs prod password-policy → population Dev ≠ prod, S58)
|
||||
- `docs/gotchas.md` — 64 bẫy đã gặp (latest #64 `dotnet ef database update` áp Design-DB 0-rows ≠ Dev-DB → data-migrate untested-before-prod, S61; #63 EF scaffold RenameColumn sai-semantics)
|
||||
- `docs/changelog/migration-todos.md` — roadmap 5 phase + Tier 3
|
||||
|
||||
@ -150,6 +150,6 @@ Lưu vào `docs/changelog/deps-audit-{YYYY-MM-DD}.md` nếu có action.
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/gotchas.md` — 60 bẫy package compat / CI / IIS / Identity / per-NV refactor / SQLite tie-break đã gặp
|
||||
- `docs/gotchas.md` — 64 bẫy package compat / CI / IIS / Identity / per-NV refactor / SQLite tie-break đã gặp
|
||||
- `docs/changelog/migration-todos.md` Phase 5.1 — checklist deps scan CI
|
||||
- `SolutionErp.slnx` + `global.json` — .NET version pin
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: ef-core-migration
|
||||
description: Tạo/sửa/revert EF Core 10 migration cho SOLUTION_ERP. Dùng khi thêm entity mới, thay đổi schema, rollback migration, debug DesignTimeDbContextFactory fail. Đã có 49 migration sẵn (Init → AddWorkItemToPurchaseEvaluation Mig 49, S57bis). Snapshot + Designer + Migration 3-file rule bắt buộc commit đủ.
|
||||
description: Tạo/sửa/revert EF Core 10 migration cho SOLUTION_ERP. Dùng khi thêm entity mới, thay đổi schema, rollback migration, debug DesignTimeDbContextFactory fail. Đã có 50 migration sẵn (Init → ReplaceBudgetModuleWithPeWorkItemBudgets Mig 50, S61). Snapshot + Designer + Migration 3-file rule bắt buộc commit đủ.
|
||||
when-to-use:
|
||||
- "thêm migration"
|
||||
- "EF Core migration"
|
||||
@ -16,7 +16,7 @@ when-to-use:
|
||||
|
||||
> **Context:** .NET 10 + EF Core 10 + SQL Server. DbContext: `ApplicationDbContext` ở `Infrastructure/Persistence/`. Startup: `SolutionErp.Api`.
|
||||
|
||||
## Migration history (49 migration hiện có)
|
||||
## Migration history (50 migration hiện có)
|
||||
|
||||
| # | Name | Tables added / changed |
|
||||
|---|---|---|
|
||||
@ -69,8 +69,9 @@ when-to-use:
|
||||
| **47** | **`FilterMasterCatalogUniqueIndexesByIsDeleted`** | **🎯 S53 — Department + Supplier + Project Code UNIQUE filtered `WHERE [IsDeleted]=0` (gotcha #57 EXT Master, 6× cumulative). Index-only.** |
|
||||
| **48** | **`AddProjectMasterFields`** | **🎯 S55 — Project +4 cột nullable (Year int · Investor 250 · Location 500 · Package 300). AddColumn-only, no new table. Kèm `SeedRealMasterDataAsync` ungated nạp 62 dự án + 71 hạng mục + 3 NCC real từ Excel.** |
|
||||
| **49** | **`AddWorkItemToPurchaseEvaluation`** | **🎯 S57bis — PE.WorkItemId `Guid?` loose-Guid (KHÔNG FK vật lý — convention PE: ProjectId/SelectedSupplierId đều loose) + `IX_PurchaseEvaluations_WorkItemId`. AddColumn+CreateIndex-only, no new table. Guard = validator NotEmpty (create) + handler AnyAsync IsActive→Conflict.** |
|
||||
| **50** | **`ReplaceBudgetModuleWithPeWorkItemBudgets`** | **🎯 S61 (2026-06-13) — bảng mới `PeWorkItemBudgets` (1 record/cặp Dự án × Hạng mục, UNIQUE filtered `[IsDeleted]=0`) + **DROP module Budget cũ** (Budgets/BudgetDetails/BudgetApprovals/BudgetChangelogs…) + PE/Contracts DROP `BudgetId` + **backfill `BudgetManualAmount→BudgetPeriodAmount` TRƯỚC DropColumn** (phiếu UAT giữ số) + DELETE menu/permission `Bg_*` IN-list children-first. ⚠️ database-agent advise: KHÔNG FK vật lý PE/Contracts→Budgets → no DropForeignKey · DropIndex TRƯỚC DropColumn (SQL 5074) · IN-list thay LIKE `Bg_%`. Ngân sách giờ per-gói-thầu nhập theo role PRO/CCM. **gotcha #63** (EF scaffold RenameColumn SAI-semantics → Add+UPDATE+Drop) + **#64** (`dotnet ef database update` áp Design-DB 0-rows ≠ Dev-DB → backfill chạy thật lần đầu trên prod).** |
|
||||
|
||||
Total: **93 bảng** dbo + `__EFMigrationsHistory` (re-ground S56 `sys.tables` — last Mig 49 AddWorkItemToPurchaseEvaluation, column+index-only). Xem `docs/database/schema-diagram.md` migration table + §11-15 module ERD (§16+ Mig 27-49 chi tiết pending).
|
||||
Total: **88 bảng** dbo + `__EFMigrationsHistory` (re-ground S62 cicd `sys.tables` — last Mig 50 ReplaceBudgetModuleWithPeWorkItemBudgets: DROP module Budget + CREATE PeWorkItemBudgets → net 93→88). Xem `docs/database/schema-diagram.md` migration table + §11-15 module ERD (§16+ Mig 27-50 chi tiết pending).
|
||||
|
||||
## N-stage workflow pattern (Mig 18-20 — Session 12-13)
|
||||
|
||||
|
||||
14
CLAUDE.md
14
CLAUDE.md
@ -50,20 +50,20 @@ Kiến trúc: **.NET 10 Clean Architecture + 2 React FE (admin + user) + SQL Ser
|
||||
- Audit fields: `CreatedAt`, `UpdatedAt`, `CreatedBy`, `UpdatedBy` (`BaseEntity`)
|
||||
- Soft delete: `IsDeleted`, `DeletedAt`, `DeletedBy` (`AuditableEntity`)
|
||||
- Migrations: `dotnet ef migrations add <Name> --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api`
|
||||
- **Hiện có 49 migration → 93 bảng** (Phase 10 COMPLETE + Phase 11 P11-A→F done — Mig 34-42 HRM/Office/WorkflowApps/Attendance + Contract V2 (32-33) + WireWorkflowApps V2 (41) + LeaveBalance (42) + Holiday filtered-unique (43, S45) + Vehicle/Driver catalog (44, S51) + HRM-catalog filtered-unique 3× (45, S51) + ItTicket SLA (46, S52) + Master filtered-unique 3× (47, S53 gotcha #57 EXT) + Project master fields Year/Investor/Location/Package (48, S55 — AddColumn no new table, kèm nạp 62 dự án + 71 hạng mục + 3 NCC real data từ Excel qua `SeedRealMasterDataAsync` ungated idempotent) + PE gắn Hạng mục công việc WorkItemId loose-Guid KHÔNG FK vật lý (49, S57bis — AddColumn+CreateIndex, no new table). V2 schema history S29-era bên dưới giữ nguyên — Mig 32+33 Plan B Contract V2 cookie-cutter mirror PE Mig 22-26 (S29). Mig 26 `AddPeLevelOpinionsForV2`: bảng mới `PurchaseEvaluationLevelOpinions` UNIQUE composite (PEId, LevelId), FK Cascade Pe + Restrict Level. Section 5 "Ý kiến cấp duyệt" V2 dynamic theo workflow đã pin: forEach Step (Phòng) → forEach Level (Cấp) → forEach NV → 1 OpinionBox. Service `ApproveV2Async` UPSERT auto khi NV duyệt — Q1=1B (sync gắn với Duyệt, KHÔNG form input rời). SignedByUserId track signer thật, FE banner "Admin duyệt thay" khi !== ApproverUserId. Comment empty → "(duyệt — không ý kiến)" placeholder. Phiếu V1 legacy fallback Mig 15 4 box readOnly (data history). Mig 25 `AddIsUserSelectableToApprovalWorkflows`: ALTER `ApprovalWorkflows` +`IsUserSelectable bit` (admin pin/unpin workflow nào cho user pick lúc create phiếu, multi-select độc lập IsActive). Backfill `WHERE IsActive=1 SET 1` giữ behavior cũ. Designer +badge "Cho user chọn" + button Ghim/Bỏ ghim. Workspace filter dropdown chỉ workflows `IsUserSelectable=true`. Mig 22-24 V2 schema (Session 17): `ApprovalWorkflows`/Steps/Levels — Quy trình > Bước (Phòng) > Cấp (N NV cụ thể qua ApproverUserId, OR-of-N cùng cấp). PE.ApprovalWorkflowId pin V2. PE.CurrentApprovalLevelOrder track. State machine 5 trạng thái: Nháp / Đã gửi duyệt / Trả lại (Phase riêng TraLai=98) / Từ chối / Đã duyệt. PE Service V2 wire match `actor.Id == ApproverUserId`. Contract V2 ĐÃ WIRE (Mig 32+33 Plan B S29 — cookie-cutter mirror PE V2: `ApproveV2Async` + `ContractLevelOpinions` UPSERT + Workspace V2 Select dropdown). Mig 21 V1 flat workflow vẫn live cho phiếu cũ.)
|
||||
- **Hiện có 50 migration → 88 bảng** (Phase 10 COMPLETE + Phase 11 P11-A→F done — Mig 34-42 HRM/Office/WorkflowApps/Attendance + Contract V2 (32-33) + WireWorkflowApps V2 (41) + LeaveBalance (42) + Holiday filtered-unique (43, S45) + Vehicle/Driver catalog (44, S51) + HRM-catalog filtered-unique 3× (45, S51) + ItTicket SLA (46, S52) + Master filtered-unique 3× (47, S53 gotcha #57 EXT) + Project master fields Year/Investor/Location/Package (48, S55 — AddColumn no new table, kèm nạp 62 dự án + 71 hạng mục + 3 NCC real data từ Excel qua `SeedRealMasterDataAsync` ungated idempotent) + PE gắn Hạng mục công việc WorkItemId loose-Guid KHÔNG FK vật lý (49, S57bis — AddColumn+CreateIndex, no new table) + **Mig 50 `ReplaceBudgetModuleWithPeWorkItemBudgets` (S61, 2026-06-13) — XÓA module Budget cũ, thay bằng `PeWorkItemBudgets` ngân sách per-gói-thầu (1 record/cặp Dự án × Hạng mục, nhập theo role PRO/CCM, vượt ngân sách = cảnh báo mềm cho lưu S62); backfill `BudgetManualAmount→BudgetPeriodAmount` TRƯỚC DropColumn (phiếu UAT giữ số); net bảng 93→88; gotcha #63/#64**. V2 schema history S29-era bên dưới giữ nguyên — Mig 32+33 Plan B Contract V2 cookie-cutter mirror PE Mig 22-26 (S29). Mig 26 `AddPeLevelOpinionsForV2`: bảng mới `PurchaseEvaluationLevelOpinions` UNIQUE composite (PEId, LevelId), FK Cascade Pe + Restrict Level. Section 5 "Ý kiến cấp duyệt" V2 dynamic theo workflow đã pin: forEach Step (Phòng) → forEach Level (Cấp) → forEach NV → 1 OpinionBox. Service `ApproveV2Async` UPSERT auto khi NV duyệt — Q1=1B (sync gắn với Duyệt, KHÔNG form input rời). SignedByUserId track signer thật, FE banner "Admin duyệt thay" khi !== ApproverUserId. Comment empty → "(duyệt — không ý kiến)" placeholder. Phiếu V1 legacy fallback Mig 15 4 box readOnly (data history). Mig 25 `AddIsUserSelectableToApprovalWorkflows`: ALTER `ApprovalWorkflows` +`IsUserSelectable bit` (admin pin/unpin workflow nào cho user pick lúc create phiếu, multi-select độc lập IsActive). Backfill `WHERE IsActive=1 SET 1` giữ behavior cũ. Designer +badge "Cho user chọn" + button Ghim/Bỏ ghim. Workspace filter dropdown chỉ workflows `IsUserSelectable=true`. Mig 22-24 V2 schema (Session 17): `ApprovalWorkflows`/Steps/Levels — Quy trình > Bước (Phòng) > Cấp (N NV cụ thể qua ApproverUserId, OR-of-N cùng cấp). PE.ApprovalWorkflowId pin V2. PE.CurrentApprovalLevelOrder track. State machine 5 trạng thái: Nháp / Đã gửi duyệt / Trả lại (Phase riêng TraLai=98) / Từ chối / Đã duyệt. PE Service V2 wire match `actor.Id == ApproverUserId`. Contract V2 ĐÃ WIRE (Mig 32+33 Plan B S29 — cookie-cutter mirror PE V2: `ApproveV2Async` + `ContractLevelOpinions` UPSERT + Workspace V2 Select dropdown). Mig 21 V1 flat workflow vẫn live cho phiếu cũ.)
|
||||
|
||||
### Modules
|
||||
|
||||
| Module | Namespace | Migration | Trạng thái |
|
||||
|---|---|---|---|
|
||||
| Contract (HĐ) | `Domain/Contracts/` | 1-11 | Feature-complete (7 ContractType × 9 phase) |
|
||||
| PurchaseEvaluation (Duyệt NCC tiền-HĐ) | `Domain/PurchaseEvaluations/` | 12, 13, 15 | Feature-complete — chỉ Export PDF còn pending (không quan trọng) |
|
||||
| Budget (Ngân sách dự án) | `Domain/Budgets/` | 14 | **Feature-complete** — BE + FE 3-panel + integration với PE/HD |
|
||||
| PurchaseEvaluation (Duyệt NCC tiền-HĐ) | `Domain/PurchaseEvaluations/` | 12,13,15,49,50 | Feature-complete — +Hạng mục (Mig 49) +ngân sách per-gói-thầu role PRO/CCM (Mig 50, vượt=cảnh báo mềm S62). Export PDF pending |
|
||||
| ~~Budget (Ngân sách dự án)~~ | — | 14 → **Mig 50 DROP** | ⚠️ **REMOVED S61** — module Budget cũ XÓA, thay bằng PE-budget-per-gói-thầu (`PeWorkItemBudgets`). FE pages/types/menu `Bg_*` gỡ hết |
|
||||
| Master (Supplier/Project/Department) | `Domain/Master/` | 2, 10 | Feature-complete |
|
||||
| Identity (User/Role/Permission/MenuItem) | `Domain/Identity/` | 1, 3, 11 | Feature-complete (30 demo user — 16 sample + 14 Solutions thật) |
|
||||
| Forms (Template + Clause) | `Domain/Forms/` | 4 | Feature-complete |
|
||||
| Notifications | `Domain/Notifications/` | 6 | In-app + SignalR OK, email SMTP TODO |
|
||||
| **Tests** | `tests/SolutionErp.{Domain,Infrastructure}.Tests/` | — | **240 test pass** (58 Domain + 182 Infra) — CI gate + path filter docs-only skip |
|
||||
| **Tests** | `tests/SolutionErp.{Domain,Infrastructure}.Tests/` | — | **263 test pass** (45 Domain + 218 Infra) — CI gate + path filter docs-only skip |
|
||||
|
||||
### Commit convention
|
||||
|
||||
@ -84,7 +84,7 @@ tests/
|
||||
└── Application/ (6 test - PeWorkflowDefinition versioning)
|
||||
```
|
||||
|
||||
**240 unit test pass** (58 Domain + 182 Infra). CI gate + path filter live. (S57bis +12 `PeWorkItemGuardTests`; S56 +12 golive-harden; S52 +14→200; S53 +3 `MasterCatalogFilteredUniqueTests` — gotcha #57 EXT Master DONE Mig 47.)
|
||||
**263 unit test pass** (45 Domain + 218 Infra). CI gate + path filter live. (S61 +22 `PeWorkItemBudgetTests` −14 `BudgetPolicyTests` −1 → 263; S60 +14 `PeSubmitGuardAndBypassTests` +2 spec → 256; S57bis +12 `PeWorkItemGuardTests`. Domain giảm 58→45 do drop BudgetPolicyTests cùng module Budget.)
|
||||
|
||||
```bash
|
||||
dotnet test SolutionErp.slnx # chạy cả 2 test project
|
||||
@ -128,9 +128,9 @@ Quy tắc:
|
||||
| [`docs/workflow-contract.md`](docs/workflow-contract.md) | State machine 9 phase HĐ + role matrix |
|
||||
| [`docs/forms-spec.md`](docs/forms-spec.md) | Catalog 8 form + quy định mã HĐ RG-001 |
|
||||
| [`docs/database/database-guide.md`](docs/database/database-guide.md) | DB conventions + migration workflow + cheatsheet |
|
||||
| [`docs/database/schema-diagram.md`](docs/database/schema-diagram.md) | ⭐ ERD + luồng DB + data flow 93 table (+ §11 PE + §12 Budget + §13 PEDeptOpinions + §14 Contract V2 LevelOpinions; §16+ Mig 32-49 pending) |
|
||||
| [`docs/database/schema-diagram.md`](docs/database/schema-diagram.md) | ⭐ ERD + luồng DB + data flow 88 table (+ §11 PE + §12 ~~Budget~~ DROP + §13 PEDeptOpinions + §14 Contract V2 LevelOpinions; §16+ Mig 32-50 pending) |
|
||||
| [`docs/flows/README.md`](docs/flows/README.md) | Index 6 flow (auth, permission, contract, form, SLA) |
|
||||
| [`docs/gotchas.md`](docs/gotchas.md) | ⭐ 62 bẫy đã gặp — đọc trước khi debug tương tự |
|
||||
| [`docs/gotchas.md`](docs/gotchas.md) | ⭐ 64 bẫy đã gặp — đọc trước khi debug tương tự |
|
||||
| [`.claude/skills/`](.claude/skills/README.md) | 6 skill: contract-workflow, form-engine, permission-matrix, dependency-audit-erp, ef-core-migration, iis-deploy-runbook |
|
||||
| [`docs/guides/vps-setup.md`](docs/guides/vps-setup.md) | ⭐ Master runbook deploy VPS shared với VIETREPORT |
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,68 @@
|
||||
# S60–S62 (2026-06-12 → 06-13) — PE workflow polish + ngân sách per-gói-thầu (Mig 50, XÓA module Budget) + soft-warning vượt ngân sách
|
||||
|
||||
> **Closeout muộn (viết bù S63 2026-06-15):** 3 session product S60/S61/S62 ship CODE prod-verified nhưng KHÔNG closeout docs cùng lúc (UAT realtime deadline anh Kiệt FDC — đúng pattern "code committed, docs mù" P1). Drift lộ ở `/session-start` S63 khi `git log` cho thấy docs dừng S59 (`6bf28bf`) còn HEAD = `7926c21` (S62) + Mig 50. Nguồn log: 4 commit `37122f0`→`7926c21` + cicd-monitor MEMORY (Run #286) + reviewer stray reconcile.
|
||||
>
|
||||
> **Driver xuyên suốt:** anh Kiệt (FDC) UAT realtime trên eoffice prod — chuỗi chỉnh PE module theo phản hồi thực tế. (PE module "CÒN CHỈNH NHIỀU" như CLAUDE.md cảnh báo.)
|
||||
|
||||
**Net state sau S62:** Mig **50** · **88** bảng (was 93 — Mig 50 XÓA module Budget) · **263** test (45 Domain + 218 Infra) · **64** gotcha (#63/#64) · menu **53** (was 57 — gỡ 4 Bg_*) · bundle admin **`0xKYGhhf`** / user **`C81ZdG9G`** (Run #286).
|
||||
|
||||
---
|
||||
|
||||
## S60 (2026-06-12) — 2 commit: ràng buộc gửi duyệt + gỡ "Từ chối"
|
||||
|
||||
### `37122f0` (11:53) — Ràng buộc đủ 4 thông tin mục 3 + bypass người soạn trong chuỗi duyệt
|
||||
- **Rename mục 3** "Chọn NCC / TP thắng thầu" → **"Đơn vị NCC/TP được chọn"** (anh Kiệt chốt chữ) ×2 app + wording phụ nhất quán.
|
||||
- **Guard gửi duyệt đủ CẢ 4** (anh chốt): đơn vị được chọn + giá chào thầu >0 + ngân sách (Budget link HOẶC nhập tay) + bảng so sánh đính kèm.
|
||||
- BE `ConflictException` gộp mọi mục thiếu 1 lần, áp **cả Admin** (`TransitionAsync` submit branch).
|
||||
- FE pre-check `missingForApproval` cùng predicate → disable nút + tooltip liệt kê đủ (`computeGiaChaoThau` extract single-source).
|
||||
- **Bypass drafter-in-chain** (luật GENERIC theo cấp, anh chốt): V2-only, **BƯỚC ĐẦU only** — người soạn là approver cấp k → auto qua Cấp 1..k khi gửi.
|
||||
- Audit 3 tầng: Approval row `AutoApprove` per cấp + LevelOpinion CHỈ slot chính chủ (KHÔNG gắn chữ ký NV bị skip) + Changelog.
|
||||
- Pointer: k<max → Cấp k+1; hết bước → Bước 2 Cấp 1; workflow 1 bước → terminal DaDuyet. TraLai resubmit áp lại idempotent (opinion UPSERT).
|
||||
- **Test:** +14 `PeSubmitGuardAndBypassTests` (240 → **254** PASS). ⚠️ Reviewer die mid-run (gotcha #53 class) → em main self-gate evidence-checklist PASS 0 blocker.
|
||||
|
||||
### `6db195d` (14:30) — Gỡ hành động "Từ chối" (chỉ còn Duyệt / Trả lại)
|
||||
- **Domain policy:** xóa MỌI transition → `TuChoi` ở cả 4 policy (NccOnly + NccWithPlan + ForV2Schema + FromDefinition) → `NextPhases` hết trả TuChoi, nút FE tự biến mất.
|
||||
- **Service guard S60:** chặn `targetPhase=TuChoi` mọi caller kể cả Admin (đứng TRƯỚC mọi branch — spec bỏ hẳn, không escape hatch); message hướng dẫn dùng Trả lại / Xóa nháp.
|
||||
- **FE ×2 app:** filter `next.filter(p != TuChoi)` PeWorkflowPanel (SHA256 identical); dialog/isCancel giữ dead-safe để flip lại dễ.
|
||||
- Enum TuChoi + phiếu TuChoi cũ + tab filter "Từ chối" **GIỮ display** (data cũ render bình thường). SlaExpiryJob chỉ Contract — PE không auto-TuChoi, không ảnh hưởng.
|
||||
- **Test 254 → 256** (59 Domain + 197 Infra). Spec-change tests cùng commit (Domain flip `BothPolicies_TuChoi_Removed...` + NEW V2SchemaPolicy fact; Infra NEW `TargetTuChoi_WithRejectDecision_Throws_TuChoiRemoved_S60`; guard #45 test cũ giữ PASS).
|
||||
|
||||
---
|
||||
|
||||
## S61 (2026-06-13 01:07) — `79ef8da` Mig 50: ngân sách gói thầu theo Excel anh Kiệt + XÓA module Budget cũ
|
||||
|
||||
- **Mig 50 `ReplaceBudgetModuleWithPeWorkItemBudgets`:** bảng mới `PeWorkItemBudgets` (1 record/cặp Dự án × Hạng mục, UNIQUE filtered `[IsDeleted]=0`) + **drop module Budget cũ** + PE/Contracts **drop `BudgetId`** + **backfill `BudgetManualAmount→BudgetPeriodAmount` TRƯỚC DropColumn** (phiếu UAT giữ số) + DELETE menu/permission `Bg_*` IN-list children-first.
|
||||
- **BE:** `PUT {id}/budget/pro` (role Procurement) + `{id}/budget/ccm` (role CostControl, Adjustment cho phép ÂM) **fail-closed Forbidden-TRƯỚC-side-effect** + `EnsureTrackedAsync` race-safe (catch unique → re-fetch winner, lỗi khác rethrow) + auto-create record khi tạo phiếu + `budgetSummary` DTO (lũy kế trình-trước/chọn-thầu-trước/đề-xuất-kỳ-này + full fallback dự-trù-PRO + canEdit flags) + submit-guard (3) đổi predicate `BudgetPeriodAmount` → "chưa nhập Ngân sách kỳ này" + PATCH budget-adjust absolute-set 2 field mới + Contract GIỮ `BudgetManual*` (HĐ nhập tay không đổi) + kế thừa HĐ map `BudgetPeriodAmount`.
|
||||
- **FE ×2 app SHA256 identical:** bảng "TỔNG HỢP NGÂN SÁCH TRÌNH KÝ" — block A (full đầm + ban hành + V0 hiệu chỉnh + dự trù PRO + ghi chú, editable theo `canEditPro`/`canEditCcm`) + block B 9 dòng công thức Excel (5=1+3, 6=2+4, 7=full−5, 8 tự nhập default 7, 9=4+8) + tô màu vượt ngân sách `#C00000` / âm đỏ / red-soft row8>row7 + "Chưa chọn" khi count=0 + banner phiếu chưa gắn Hạng mục + ô "Ngân sách kỳ này" ở create/header + **XÓA pages/components/types budgets + routes + menuKeys + Layout staticMap (4-place)**.
|
||||
- **Test:** +22 `PeWorkItemBudgetTests` (auto-create ×3, ensure/race ×2, authz matrix PRO ×5 + CCM ×3, budgetSummary aggregates ×5, adjust ×4) − 14 `BudgetPolicyTests` (xóa theo module) − 1 test via-BudgetId → **263 PASS** (45 Domain + 218 Infra, 0 fail).
|
||||
- **database-agent advise adopted:** không FK vật lý PE/Contracts→Budgets (DropColumn không cần DropForeignKey) + DropIndex TRƯỚC DropColumn (SQL 5074) + IN-list thay LIKE `Bg_%` (underscore wildcard + miss root) + không Serializable wrap (nested-tx conflict codegen).
|
||||
- **Reviewer PASS-with-minor 0 blocker** (verdict-first survived); 2 minor đã sửa trước commit (comment adjustMut absolute-set + dead key budgetId). Note: F4 approver-edit-budget UI entry tạm drafter-only, BE vẫn cho approver scope — chờ UAT anh Kiệt.
|
||||
- **⚠️ Scaffold-bug caught → 2 gotcha NEW:**
|
||||
- **#63** EF tự sinh `RenameColumn(BudgetManualAmount→ExpectedRemainingAmount)` SAI semantics (drop+add cùng type → EF heuristic đoán rename) → thay bằng `AddColumn` + `Sql(UPDATE backfill)` + `DropColumn`. SQLite test (`EnsureCreated` từ model) KHÔNG bắt được.
|
||||
- **#64** `dotnet ef database update` áp **Design DB** (`DesignTimeDbContextFactory`, 0 rows) ≠ runtime Dev DB → `Sql()` backfill CHƯA TỪNG chạy trên data thật trước prod. Guard: cicd brief BẮT BUỘC mục DATA-PRESERVE spot-check sau deploy.
|
||||
|
||||
---
|
||||
|
||||
## S62 (2026-06-13 11:13) — `7926c21`: vượt ngân sách = cảnh báo mềm (cho lưu số âm row 8)
|
||||
|
||||
- **Root cause:** ô "Giá trị thực hiện dự kiến còn lại" (row 8 bảng Tổng hợp) khi giá trị NCC vượt ngân sách → số dư còn lại ra ÂM; BE validator `ExpectedRemainingAmount>=0` + FE `VndInlineEdit` không bật `allowNegative` → chặn cứng "âm không lưu được" (testing báo qua anh Kiệt).
|
||||
- **BE:** `AdjustPurchaseEvaluationBudgetCommandValidator` GỠ rule `ExpectedRemainingAmount.GreaterThanOrEqualTo(0)` → cho lưu số âm (mirror tiền lệ LeaveBalance `AllowsNegativeRemaining`). GIỮ `BudgetPeriodAmount>0` + submit-guard "đã nhập NS kỳ này" không đổi. (`PurchaseEvaluationFeatures.cs:317` validator.)
|
||||
- **FE ×2 app SHA256 identical:** (a) `allowNegative` cho VndInlineEdit row 8; (b) banner amber "Vượt ngân sách — vẫn lưu & gửi duyệt được" trong `PeBudgetSummaryTable` khi `cmpPeriod<0 || cmpFull<0`. Tô màu đỏ cũ GIỮ NGUYÊN.
|
||||
- **Spec change:** flip test `AdjustBudget_Validator_ExpectedRemainingNegative_FailsValidation` → `_PassesValidation` (âm giờ hợp lệ); test `BudgetPeriodZero_FailsValidation` GIỮ (budget>0 vẫn enforced).
|
||||
- **Build FE ×2 PASS + test 263 PASS** (45 Domain + 218 Infra, 0 fail/skip). **Reviewer PASS 0 issue** (row8 âm an toàn arithmetic additive-only — row9=row4+row8, cmpFull=full−row9, no division/sqrt/unsigned-cast; submit guard nguyên; mirror byte-identical; no scope creep).
|
||||
- **cicd Run #286** sha `7926c21` PASS ~4m41s — bundle ROTATE admin `DsGZlNzT→0xKYGhhf` + user `DTL_bjzQ→C81ZdG9G` (FE×2 changed, đúng). DATA-PRESERVE spot-check 8/8 phiếu UAT giữ số (gồm phiếu 1.243.820.600 đ anh Kiệt).
|
||||
|
||||
---
|
||||
|
||||
## Quality + lessons
|
||||
|
||||
- **3 session deadline-driven KHÔNG closeout docs** = drift S59→S62 (Mig/table/test/gotcha/menu/bundle đều lệch). Bắt được ở S63 `/session-start` qua `git log` (session-log disk + MEMORY count đều lag). **Lesson H1: đầu session luôn `git log` trước, đừng tin MEMORY count.**
|
||||
- **Reviewer cwd-relative mis-land (S62):** reviewer cd `fe-admin/` → Write MEMORY relative-path → 3 file rơi `fe-admin/.claude/agent-memory/reviewer/` (pattern `feedback_agent_cwd_relative_memory_misland` S54). Reconcile S63: MOVE 2 file con vào canonical + MERGE pointer (KHÔNG overwrite 31KB) + xóa stray.
|
||||
- **EF migration data-migrate (gotcha #63/#64):** test xanh + "applied-local OK" ≠ migration đúng — SQLite test dựng từ model (không replay migration), `ef database update` áp Design-DB 0-rows. Guard duy nhất = đọc file migration sau scaffold + cicd DATA-PRESERVE spot-check sau deploy.
|
||||
|
||||
## Carry / NEXT (cho session sau)
|
||||
- **test-after guard** (deadline trade-off): `PeWorkItemBudgetTests` đã có; cân nhắc thêm guard `LockDemoSampleUsersAsync` (S58) + suppliers asymmetric authz (S59) — vẫn pending.
|
||||
- **F4 approver-edit-budget UI** tạm drafter-only (BE cho approver scope) — chờ anh Kiệt UAT chốt mở UI cho approver.
|
||||
- **schema-diagram §16+** Mig 32-50 ERD debt (giờ +Mig 50 Budget-drop) — monthly audit 2026-07-01.
|
||||
- **cicd-monitor L1 MEMORY 63.6KB** over-cap lần 5 → curate L2 gấp (H2 flag).
|
||||
- **Bundle/Run** S60/S61 run numbers (#283-#285) chưa truy verbatim — nếu cần, đọc cicd-monitor MEMORY recent entries.
|
||||
@ -1122,6 +1122,30 @@ for h in resp.points: # ← .points không phải iterable trực tiếp
|
||||
|
||||
**References:** `scripts/s59-rename-workitems-pmh.sql` · `DbInitializer.cs` SeedRealMasterDataAsync · gotcha #57 họ hàng (soft-delete vs UNIQUE filtered) · Run #276 cicd verdict.
|
||||
|
||||
### 63. EF scaffold tự đoán `RenameColumn` SAI SEMANTICS khi drop + add cột cùng type — review migration trước khi tin, test xanh KHÔNG bắt được (Session 61)
|
||||
|
||||
**Triệu chứng (đã né):** Mig 50 drop `BudgetManualAmount` + add `BudgetPeriodAmount`/`ExpectedRemainingAmount` (đều `decimal(18,2)` nullable) → `dotnet ef migrations add` scaffold sinh `RenameColumn(BudgetManualAmount → ExpectedRemainingAmount)`. Nếu tin scaffold: số ngân sách nhập tay của 8 phiếu UAT prod rơi vào cột "Giá trị thực hiện dự kiến còn lại" (row 8) thay vì "Ngân sách kỳ này" (row 3) — **data đúng chỗ SAI semantics, không lỗi runtime nào báo**.
|
||||
|
||||
**Root cause:** EF model-diff heuristic map cột-bị-xóa ↔ cột-mới cùng type/nullability thành RENAME (tối ưu giữ data) — máy không biết semantics nghiệp vụ.
|
||||
|
||||
**Fix đúng (S61 proven, prod-verified Run #285):** bỏ RenameColumn → `AddColumn` cả 2 cột mới → `Sql("UPDATE ... SET BudgetPeriodAmount = BudgetManualAmount WHERE ... IS NOT NULL")` → `DropColumn` cột cũ. Precedent cùng shape: Mig `20260513130144` (add→backfill→drop).
|
||||
|
||||
**Vì sao test không bắt:** SQLite test dựng schema từ MODEL hiện tại (`EnsureCreated`), KHÔNG chạy migration — mọi sai trong migration operations vô hình với 263 test xanh. Guard duy nhất = đọc file migration sau scaffold + cicd spot-check data sau deploy.
|
||||
|
||||
**References:** `Migrations/20260612173224_ReplaceBudgetModuleWithPeWorkItemBudgets.cs` Up() comment đầu · reviewer S61 verify "KHÔNG còn RenameColumn" grep · gotcha #64 (cặp đôi — backfill chưa test local).
|
||||
|
||||
### 64. `dotnet ef database update` áp lên **Design DB** (DesignTimeDbContextFactory) — KHÔNG phải runtime Dev DB; Sql() data-migrate trong Up() có thể CHƯA TỪNG chạy trên data thật trước prod (Session 61)
|
||||
|
||||
**Triệu chứng:** claim "Mig 50 applied local OK" sau `dotnet ef database update` — đúng nhưng là DB `SolutionErp_Design` (pin tại `DesignTimeDbContextFactory.cs:14`, **0 rows**). Runtime `SolutionErp_Dev` (appsettings.Development.json) vẫn ở Mig 49. Nguy hiểm thật: mọi `Sql()` backfill/UPDATE trong Up() **chưa từng chạy trên DB có data** — prod deploy là lần ĐẦU TIÊN data-migrate chạy thật.
|
||||
|
||||
**Cơ chế 2 DB:** `dotnet ef` CLI → DesignTimeDbContextFactory → Design DB · runtime app start → `DbInitializer.cs:64 MigrateAsync()` tự heal Dev/prod DB. 2 đường KHÁC NHAU, đừng lẫn.
|
||||
|
||||
**Guard (S61 áp dụng):** migration có data-migrate ⟹ (1) ghi rõ trong commit "backfill lần đầu chạy trên prod"; (2) cicd-monitor brief BẮT BUỘC mục DATA-PRESERVE spot-check sau deploy (S61: `SELECT MaPhieu, BudgetPeriodAmount ...` → 8/8 phiếu giữ số, gồm phiếu 1.243.820.600 đ của anh Kiệt ✓); (3) optional: `dotnet run` local 1 lần trước push để Dev DB có data chạy thử backfill.
|
||||
|
||||
**Credit:** 🟥 reviewer S61 catch (đào connection-string mới lộ — "claim applied-local trên DB 0-rows = backfill untested"). Họ hàng S53 database-agent catch "committed-but-unapplied-local".
|
||||
|
||||
**References:** `DesignTimeDbContextFactory.cs:14` · `DbInitializer.cs:64` · reviewer S61 MINOR #1 · cicd S61 self-verify BACKFILL 8/8.
|
||||
|
||||
---
|
||||
|
||||
## Checklist debug bug mới
|
||||
|
||||
Reference in New Issue
Block a user