[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:
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