# Session log — 2026-04-29 (Phase 8 — Budget FE complete + PE feature gap đóng + Tests Phase 1-2)

**Topic:** FE Budget pages 3-panel + PE/HD-Budget integration + PE Detail UI restructure + PE Workflow Designer + Ý kiến 4 phòng ban + Tests Phase 1-2 (Domain + Infra) + CI gate.

**Commits (6 total session 5):**
- `df12fb1` — FE Budget pages 3-panel cả 2 app
- `61e5d4d` — PE/Contract → Budget integration + cột "So với ngân sách" PE matrix
- `7e36241` — PE Detail UI restructure theo spec PHIẾU TRÌNH KÝ (4 section đánh số)
- `5d94bb4` — PE Workflow Designer admin UI + Ý kiến 4 phòng ban (Migration 15)
- `d3f9346` — Tests Phase 1: Domain unit tests + CI gate
- `df5988b` — Tests Phase 2: Code generator format + sequence (SQLite in-memory)
- (Session log này — commit chốt MD session 5)

**Migration:** 15 `AddPurchaseEvaluationDepartmentOpinions` — 1 bảng UNIQUE PEId+Kind cho 4 phòng ban sign-off.

## A. FE Budget pages 3-panel (commit `df12fb1`)

Mirror pattern PE 3-panel cho cả `fe-admin` + `fe-user`:

| File | Mô tả |
|---|---|
| `types/budget.ts` | BudgetPhase 5-state enum + DTO types (BudgetListItem/DetailRow/Approval/Bundle/DetailBody) |
| `pages/budgets/BudgetsListPage.tsx` | 3-panel `[340px_1fr_360px]` + filter Phase + Năm + `?phase=Pending` alias filter ChoCCM/ChoCEO + readOnly mode (menu Duyệt) + BudgetDetailPage fullpage mobile |
| `pages/budgets/BudgetCreatePage.tsx` | Form Header (Tên/Năm/Dự án/Phòng ban/Mô tả). Edit mode khóa Project+Department |
| `components/budgets/BudgetDetailTabs.tsx` | Section Thông tin Header + Section Hạng mục table CRUD inline (Add/Edit/Delete dialog auto-compute ThanhTien=KL×ĐG). Export `BudgetApprovalsSection` + `BudgetHistorySection` cho Panel 3 reuse |
| `components/budgets/BudgetWorkflowPanel.tsx` | Panel 3 timeline activePhases visual + nextPhases buttons (Approve xanh / Reject đỏ / Trả về vàng) + Dialog xác nhận có comment + Approvals + Changelog |

App.tsx 3 route mới `/budgets`, `/budgets/new`, `/budgets/:id` cả 2 app. Layout resolver `Bg_List`/`Bg_Create`/`Bg_Pending` + root `Budgets` icon Wallet.

## B. PE/Contract → Budget integration (commit `61e5d4d`)

### BE (6 file)

- `Budgets/Dtos/BudgetDtos.cs`: + `BudgetSummaryDto` (compact header snapshot reuse cho PE & Contract)
- `PE Dtos + Features`: + `BudgetId?` + `Budget?` vào DetailBundle. Create/Update PE commands + validate cùng Project + Phase=DaDuyet
- `Contract Dtos + Features`: same pattern
- `CreateContractFromEvaluation`: carry forward `pe.BudgetId → contract.BudgetId`

### FE (10 file)

- PE & Contract Create form: Select **"Ngân sách"** filter Phase=DaDuyet & Project match (BE-side filter qua `/budgets?projectId=&phase=4`)
- PE InfoTab + Contract Edit: hiển thị Budget link clickable `→ /budgets?id=`
- PE ItemsTab matrix: cột **"NS link · Δ"** chỉ hiện khi `ev.budgetId`, match per-row qua key `groupCode|itemCode` (fetch `/budgets/{id}` riêng), footer aggregate (xanh dưới / đỏ vượt / xám khớp)

## C. PE Detail UI restructure (commit `7e36241`)

Match form chính thức **PHIẾU TRÌNH KÝ CHỌN TP/NCC** với 4 section đánh số:

```
1. Thông tin gói thầu       (a Tên + b Dự án + Địa điểm/Mô tả compact)
2. Chọn NCC / TP             (a NCC chọn / b Ngân sách / c Giá chào thầu auto-compute / d Bản so sánh)
3. NCC / TP tham gia ({n})   (table 5 cột UX web)
4. Hạng mục + Báo giá ({n})  (matrix + cột "NS link · Δ")
5. Ý kiến 4 phòng ban (sign-off)  (2x2 grid OpinionBox — Section thêm trong commit kế)
```

**Section 2.c "Giá chào thầu"** auto-compute: lookup `winnerSupplierRowId` qua `selectedSupplierId`, sum quotes có `purchaseEvaluationSupplierId === winnerRowId`. Hiện "—" khi chưa chọn NCC hoặc chưa nhập báo giá.

**Section 2.d "Bản so sánh"** dùng filter `purchaseEvaluationSupplierId === null` (file tổng, không gắn NCC cụ thể).

`FormRow` helper mới (label 176px + value flex) thay cho dl grid 2-col cũ. Drop helper `Field` cũ.

## D. PE Workflow Designer admin UI + Ý kiến 4 phòng ban (commit `5d94bb4`)

### PE Workflow Designer

- BE `Application/PurchaseEvaluations/PeWorkflowAdminFeatures.cs` ~250 LOC mirror Contract WorkflowAdminFeatures (GetOverview + CreateNewVersion deactivate cũ atomic)
- BE `Api/Controllers/PeWorkflowsController.cs` 2 endpoint reuse policy `Workflows.Read` + `Workflows.Create`
- FE `pages/system/PeWorkflowsPage.tsx` ~500 LOC: Landing 2-card grid → TypePanel khi pick code → DefinitionCard active+history + PeWorkflowDesigner dialog (clone từ existing + edit Code/Name/Description + add/remove steps + +Role / +User approvers)
- App.tsx route `/system/pe-workflows` + `/system/pe-workflows/:typeCode`. Resolver `PeWf_<Code>` từ session 3 đã trỏ.

### Ý kiến 4 phòng ban PE

**Migration 15** `AddPurchaseEvaluationDepartmentOpinions`:

```sql
CREATE TABLE PurchaseEvaluationDepartmentOpinions (
  Id, PurchaseEvaluationId FK Cascade,
  Kind INT NOT NULL,    -- 1=PheDuyet, 2=Ccm, 3=MuaHang, 4=SmPm
  Opinion NVARCHAR(2000) NULL,
  SignedAt DATETIME2 NULL,
  UserId UNIQUEIDENTIFIER NULL,
  UserName NVARCHAR(200) NULL,
  -- AuditableEntity columns
);
CREATE UNIQUE INDEX ON (PurchaseEvaluationId, Kind);  -- max 1 row mỗi loại phòng ban per phiếu
```

- Domain entity `PurchaseEvaluationDepartmentOpinion` + enum `PeDepartmentKind`
- Application `PeDepartmentOpinionFeatures.cs`: Upsert pattern (sign=true → set SignedAt+UserId+UserName denorm, sign=false → giữ chữ ký cũ chỉ update text), Delete admin override
- API `POST /api/purchase-evaluations/{id}/opinions` + `DELETE .../opinions/{kind}`
- FE Section "5. Ý kiến 4 phòng ban (sign-off)" 2x2 grid `OpinionBox`:
  - Read mode (menu Duyệt readOnly): hiển thị text + chữ ký
  - Edit mode: textarea + 2 button "Lưu text" / "Lưu & Ký"
  - Badge "Đã ký" emerald + tên người ký + ngày

## E. Tests Phase 1 — Domain unit tests + CI gate (commit `d3f9346`)

```
tests/SolutionErp.Domain.Tests/
├── Contracts/WorkflowPolicyTests.cs              (~17 test)
├── PurchaseEvaluations/PurchaseEvaluationPolicyTests.cs (~17 test)
├── Budgets/BudgetPolicyTests.cs                  (~13 test)
└── SolutionErp.Domain.Tests.csproj               (xUnit 2.9.3 + FluentAssertions 7.2)
```

**Coverage:**
- Standard 9-phase + SkipCcm 7-phase (HĐ): role transitions, CCM check, BOD signing, terminal phase, reject paths
- Registry: DefaultPolicyName per ContractType (7 type) + bypass flag override Standard→SkipCcm
- FromDefinition versioned: build từ ordered steps + reject path back to draft + TuChoi auto-add + UserKindApprover populate UserTransitions
- NccOnly (PE-A) 3-step + NccWithPlan (PE-B) 5-step: skip ChoDuAn/ChoCEODuyetPA chỉ ở B
- BudgetPolicy Default 3-step + DaDuyet/TuChoi terminal + SLA spec verify (5d/3d/2d)

**CI gate** `.gitea/workflows/deploy.yml`:

```yaml
- name: Run unit tests (Domain)
  run: dotnet test tests/SolutionErp.Domain.Tests/...
  # Fail → exit $LASTEXITCODE → no deploy
```

54 test pass / 6.4s. Verify local: `dotnet test SolutionErp.slnx`.

## F. Tests Phase 2 — Code generator format + sequence (commit `df5988b`)

```
tests/SolutionErp.Infrastructure.Tests/
├── Common/SqliteDbFixture.cs                     (Test fixture + TestApplicationDbContext)
├── Services/ContractCodeGeneratorTests.cs        (~10 test)
├── Services/PurchaseEvaluationCodeGeneratorTests.cs (~7 test)
└── SolutionErp.Infrastructure.Tests.csproj       (+ EF Sqlite 10)
```

### Setup SQLite in-memory

- `TestApplicationDbContext` subclass override `OnModelCreating` — iterate model, replace bất kỳ column type chứa "max" → "TEXT" (SQLite không support `nvarchar(max)`)
- `FixedDateTime` stub `IDateTime` (control year boundary deterministic)
- Shared `SqliteConnection` open trong fixture, EnsureCreated() từ DbContext model

### Coverage

- Format RG-001 cho 5 ContractType (HĐTP/HĐGK/NCC/HĐDV/MB) — `{Project}/{TypeCode}/SOL&{Supplier}/{Seq}`
- Framework HĐ (NguyenTacNCC + NguyenTacDV) đặc biệt — `{Year}/{TypeCode}/SOL&{Supplier}/{Seq}` (year scope thay vì project)
- Sequence increment per prefix (`/01` → `/02` → `/03`)
- Different prefixes (project / supplier khác) có sequence độc lập
- Year boundary cho framework — sang 2027 prefix mới reset seq
- PE format `PE/{YYYY}/{A|B}/{Seq:D3}` — 3-digit padding test với 12 phiếu
- PE Type A và B sequence độc lập trong cùng năm
- PersistsSequenceRow LastSeq + UpdatedAt verification

**CI gate** thêm step thứ 2 — fail-fast tương tự Domain.

17 test pass / 2s. Total session 5 test count = **71** (54 Domain + 17 Infra).

## G. Stats sau session 5

| | Trước S5 | Sau S5 |
|---|---:|---:|
| BE LOC | ~11750 | **~13050** (+1300 PE WF Designer + Opinion + Budget integration) |
| DB tables | 51 | **52** (+1 PEDeptOpinions) |
| API endpoints | ~124 | **~128** (+2 PE WF + 2 Opinion) |
| Migrations | 14 | **15** |
| FE pages | ~26 | **~31** (+5 Budget + 1 PeWorkflowsPage) |
| **Tests** | 0 | **71** (54 Domain + 17 Infra) |
| CI gate | 1 (build) | **2 step test gate + build** |
| Commits | ~75 | **~82** (+6 session 5 + chốt MD) |

## H. Cảnh báo session 6

1. **CI test gate active** — mỗi commit push trigger 2 step `dotnet test`. Test fail → NO deploy. Workflow mới: code → `dotnet test SolutionErp.slnx` local → commit → push.
2. **Chưa xóa binding cũ `.huypham.vn`** — fallback active. Sau verify stable: `.\migrate-domains.ps1 -RemoveOld -SkipCert`.
3. **win-acme scheduled task "unhealthy"** — fix trước cert expire 2026-06-18.
4. **Login email default** vẫn `admin@solutionerp.local` (chỉ rebrand demo user).
5. **Atomic sequence Budget chưa chốt** — Random.Shared race risk thấp nhưng không zero.
6. **Versioned workflow Budget chưa có** — hardcoded `BudgetPolicy.Default` 3-step.
7. **Export phiếu PDF/Excel PE** pending vô thời hạn (user nói không quan trọng lắm).

## I. Lessons learned session 5

1. **Test pyramid bottom-heavy hợp solo dev** — 71 test (54 Domain pure + 17 Infra SQLite) chạy < 3s. Bỏ E2E, tránh maintenance overhead.

2. **SQLite in-memory cho code generator test** đủ test format + sequence + year boundary, KHÔNG đủ test race condition thực (cần SQL Server thật cho `IsolationLevel.Serializable` strict). Workaround tốt cho CI runner không có LocalDB.

3. **TestApplicationDbContext subclass override** — pattern sạch nhất để adapt SQL Server-specific model (`nvarchar(max)`) cho SQLite mà không touch production DbContext.

4. **CI gate ROI cao cho solo dev** — bug Domain logic recurring (workflow phase, role transition) sẽ block deploy ngay. ~3.5 ngày upfront cho 5 phase, có thể stop ở Phase 2 (1.5 ngày) nếu Phase 3-5 ROI chưa rõ.

5. **PE Detail UI restructure theo spec form giấy** — match 4 section đánh số PHIẾU TRÌNH KÝ giúp UX consistent giữa fill web và print PDF (tương lai).

6. **Sign-off 4 phòng ban dùng table riêng thay vì 12 column header** — UNIQUE(PEId, Kind) bảo vệ max 1 row mỗi phòng, dễ query "phòng nào chưa ký", dễ extend nếu thêm kind.

7. **Workflow user mới end-of-task** = `dotnet test SolutionErp.slnx` → commit → push. CI tự run test gate trước build/deploy.

## J. Files touched session 5

```
src/Backend/SolutionErp.Domain/PurchaseEvaluations/
  ├── PurchaseEvaluation.cs                              (mod: + DepartmentOpinions navigation)
  └── PurchaseEvaluationDepartmentOpinion.cs             (NEW — entity + enum PeDepartmentKind)

src/Backend/SolutionErp.Application/Contracts/
  ├── ContractFeatures.cs                                (mod: + BudgetId in commands + GetQuery load Budget)
  └── Dtos/ContractDtos.cs                               (mod: + BudgetId + Budget? trong ContractDetailDto)

src/Backend/SolutionErp.Application/PurchaseEvaluations/
  ├── PurchaseEvaluationFeatures.cs                      (mod: + BudgetId + DepartmentOpinions trong bundle)
  ├── PeWorkflowAdminFeatures.cs                         (NEW — ~250 LOC mirror Contract pattern)
  ├── PeDepartmentOpinionFeatures.cs                     (NEW — Upsert + Delete)
  ├── CreateContractFromEvaluationFeatures.cs            (mod: carry forward BudgetId)
  └── Dtos/PurchaseEvaluationDtos.cs                     (mod: + BudgetId + DepartmentOpinions list)

src/Backend/SolutionErp.Application/Budgets/
  └── Dtos/BudgetDtos.cs                                 (mod: + BudgetSummaryDto)

src/Backend/SolutionErp.Application/Common/Interfaces/
  └── IApplicationDbContext.cs                          (mod: + DbSet PEDepartmentOpinions)

src/Backend/SolutionErp.Infrastructure/Persistence/
  ├── ApplicationDbContext.cs                            (mod: + DbSet)
  ├── Configurations/PurchaseEvaluationConfiguration.cs  (mod: + DepartmentOpinions cascade + UNIQUE)
  └── Migrations/20260429041117_AddPurchaseEvaluationDepartmentOpinions.{cs,Designer.cs} (NEW)

src/Backend/SolutionErp.Api/Controllers/
  ├── PurchaseEvaluationsController.cs                   (mod: + 2 opinion endpoints)
  └── PeWorkflowsController.cs                           (NEW)

fe-admin/src/                                            (FE mod files mirror lên fe-user)
  ├── types/budget.ts                                    (NEW)
  ├── types/contracts.ts                                 (mod: + BudgetSummary)
  ├── types/purchaseEvaluation.ts                        (mod: + Budget + DepartmentOpinion + Kind enum)
  ├── pages/budgets/BudgetsListPage.tsx                  (NEW)
  ├── pages/budgets/BudgetCreatePage.tsx                 (NEW)
  ├── pages/contracts/ContractCreatePage.tsx             (mod: + Budget select)
  ├── pages/pe/PurchaseEvaluationCreatePage.tsx          (mod: + Budget select)
  ├── pages/system/PeWorkflowsPage.tsx                   (NEW)
  ├── components/budgets/BudgetDetailTabs.tsx            (NEW)
  ├── components/budgets/BudgetWorkflowPanel.tsx         (NEW)
  ├── components/pe/PeDetailTabs.tsx                     (mod: 4 section restructure + Budget col + Opinion section)
  ├── App.tsx                                            (mod: + 5 route Budget + 2 PE WF)
  └── components/Layout.tsx                              (mod: + Bg_* resolver)

tests/SolutionErp.Domain.Tests/                          (NEW Phase 1)
  ├── SolutionErp.Domain.Tests.csproj
  ├── Contracts/WorkflowPolicyTests.cs                   (~17 test)
  ├── PurchaseEvaluations/PurchaseEvaluationPolicyTests.cs (~17 test)
  └── Budgets/BudgetPolicyTests.cs                       (~13 test)

tests/SolutionErp.Infrastructure.Tests/                  (NEW Phase 2)
  ├── SolutionErp.Infrastructure.Tests.csproj            (+ EF Sqlite 10)
  ├── Common/SqliteDbFixture.cs                          (TestApplicationDbContext + FixedDateTime)
  ├── Services/ContractCodeGeneratorTests.cs             (~10 test)
  └── Services/PurchaseEvaluationCodeGeneratorTests.cs   (~7 test)

SolutionErp.slnx                                         (mod: + folder /tests/ với 2 project)
.gitea/workflows/deploy.yml                              (mod: + 2 step dotnet test gate)

docs/STATUS.md                                           (mod: header + 6 row Recently Done + cumulative cột S5)
docs/HANDOFF.md                                          (mod: TL;DR + Cảnh báo S6 + Priority 0 S6 + Phase status)
docs/changelog/migration-todos.md                        (mod: Phase 8 done + Phase 9 active S6)
docs/architecture.md                                     (mod: + §11 Testing strategy + renumber §12 Liên quan)
docs/database/schema-diagram.md                          (mod: Migration 15 row + 52 tables + §13 PEDeptOpinions)
docs/CLAUDE.md (root + docs/)                            (mod: modules table + Tests row + Roadmap S5/S6 + Tests section)
.claude/skills/ef-core-migration/SKILL.md                (mod: 15 migrations + 52 tables + Phase 8 update)
docs/changelog/sessions/2026-04-29-chot-session-5-budget-fe-pe-tests.md (NEW — file này)
~/.claude/projects/.../memory/project_solution_erp.md    (mod: Phase 8 summary + Session 6 priority)
```
