# 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.
