# Session log — 2026-05-04 chốt session 8 — 2-stage dept approval + smart reject + lock edit

**Topic:** Migration 16 đóng bug anh Kiệt (FDC) báo: "tạo NV.PRO mới + tạo phiếu PE + duyệt gì duyệt được hết = phân quyền sai". Schema + logic 2-stage approval + smart reject + lock edit guards.

**Dev:** Claude (Opus 4.7) + user (pqhuy1987@gmail.com)
**Duration:** ~5 giờ (gồm Chunk A-D + verify LocalDB + Chunk E1 BE).
**Base commit:** `dfb43fc` (chốt session 7).

## Bối cảnh

User chia sẻ screenshot chat FDC-Anh Kiệt (Zalo):
- Anh Kiệt: "tạo tài khoản mới với vai trò là nhân viên, tạo phiếu mới, duyệt gì duyệt được hết — do anh phân quyền ko đúng hay sao em? user long.chau"
- User: "để e check" + "thêm 1 tầng nữa"

Ngầm yêu cầu: thêm 2-cấp duyệt mỗi phòng ban (NV Review → TPB Confirm) + setting bypass cho NV.

Cộng thêm 2 ràng buộc khác:
- "khi đưa lên duyệt thì không thay đổi được thông tin được nhé"
- "khi nào reject điều chỉnh lại thì trả về người trình và quay lại bước duyệt"

## Approach final (sau 4 vòng iterate plan)

User đề xuất "tách bảng riêng để lưu trạng thái duyệt của từng phòng ban" — đây là cách hay hơn 3 option Claude đề xuất ban đầu vì:
- KHÔNG touch workflow versioned hiện tại
- KHÔNG cần migrate HĐ/PE cũ
- Pattern mirror `PurchaseEvaluationDepartmentOpinion` (Migration 15) đã proven

3 ràng buộc gộp vào 1 migration để rollback atomic.

## Commits session 8

5 commit per-chunk theo plan:

- `5fe61cc` — Chunk A: Migration 16 schema (Domain + Infra)
- `14f3c9f` — Chunk B: Lock edit guards 17 handler (App)
- `9747f8c` — Chunk C: Smart reject + Resume after reject (3 module)
- `a532ba6` — Chunk D: PE 2-stage dept approval logic (Infra)
- (current) — Chunk E1: BE List endpoint + Notify TPB + Bypass-review toggle + Docs

## A. Schema — Migration 16

### 4 ALTER + 3 CREATE TABLE

```sql
-- Smart reject (3 bảng)
ALTER Contracts             ADD RejectedFromPhase int NULL
ALTER PurchaseEvaluations   ADD RejectedFromPhase int NULL
ALTER Budgets               ADD RejectedFromPhase int NULL

-- Bypass per-user
ALTER Users                 ADD CanBypassReview bit NOT NULL DEFAULT 0

-- 3 bảng DepartmentApprovals (mirror schema)
CREATE TABLE ContractDepartmentApprovals (...)
CREATE TABLE PurchaseEvaluationDepartmentApprovals (...)
CREATE TABLE BudgetDepartmentApprovals (...)
  UNIQUE (TargetId, PhaseAtApproval, DepartmentId, Stage)
  Columns: ApproverUserId, ApproverRoleSnapshot, Comment, ApprovedAt,
           IsBypassed bit + AuditableEntity (CreatedAt/By/...)
```

### Domain entities mới

- `Common/ApprovalStage` enum (1=Review NV, 2=Confirm TPB)
- `Contracts/ContractDepartmentApproval`
- `PurchaseEvaluations/PurchaseEvaluationDepartmentApproval`
- `Budgets/BudgetDepartmentApproval`

LƯU Ý: KHÁC `PurchaseEvaluationDepartmentOpinion` (Migration 15) — Opinion là sign-off block "Ý kiến 4 phòng ban" trên header phiếu. DepartmentApproval mới là 2-stage approval workflow per phase.

## B. Lock edit guards — 17 handler

| Module | Handler | Pattern |
|---|---|---|
| Contract | 15 (7 Add + 7 Update Detail × 7 type + 1 Delete) | Helper `EnsureContractType` extended |
| PE | 5 (Add/Update/Delete Detail + Upsert/Delete Quote) | Helper mới `PurchaseEvaluationDraftGuard` |
| Budget | 3 (Add/Update/Delete Detail) | Inline guard |

**KHÔNG lock** (intentional, đúng workflow):
- Contract Comment (cần được trong DangGopY phase 3)
- Contract Attachment Upload/Delete (Drafter scan ký ở DangInKy phase 5)
- PE OpinionUpsert (Ý kiến 4 PB là sign-off, có thể nhập sau khi trình)
- PE Attachment (báo giá NCC upload xuyên suốt workflow)

## C. Smart reject + Resume

### Reject
```csharp
if (decision == Reject) {
    entity.RejectedFromPhase = currentPhase;  // snapshot phase đang reject
    targetPhase = DangSoanThao;                // force về Drafter
}
// Approval row: FromPhase=X, ToPhase=DangSoanThao, Decision=Reject
```

### Resume after reject
```csharp
if (decision == Approve
    && fromPhase == DangSoanThao
    && entity.RejectedFromPhase != null) {
    targetPhase = entity.RejectedFromPhase!.Value;  // jump straight
    entity.RejectedFromPhase = null;                // clear flag
    // Skip policy guard (Drafter đã trình lại sau khi sửa)
}
```

Approval history giờ track đầy đủ cycle reject→sửa→resume:
1. `Approval 1`: DangGopY → DangSoanThao, Decision=Reject (CCM reject)
2. (Drafter sửa Header/Detail)
3. `Approval 2`: DangSoanThao → DangGopY, Decision=Approve (Drafter resume)

## D. PE 2-stage dept approval logic

**Logic flow trong `PurchaseEvaluationWorkflowService.TransitionAsync`:**

1. Detect approving phase với role thuộc phòng ban:
   - `decision == Approve` + `target != DangSoanThao && != TuChoi`
   - Không reject + không resume + không admin/system
   - `actorUserId != null` + `actor.DepartmentId != null`

2. Stage detection:
   - `DeptManager` (TPB) → `Stage=Confirm` trực tiếp
   - `User.CanBypassReview=true` → `Stage=Confirm` + `IsBypassed=true`
   - Else (NV) → `Stage=Review` only

3. Upsert `PurchaseEvaluationDepartmentApproval` row (UNIQUE (PEId, Phase, Dept, Stage))

4. Check `Stage=Confirm` tồn tại cho `(PEId, fromPhase, deptId)`:
   - Yes → tiếp tục normal phase transition logic (phase đổi)
   - No → BLOCK transition:
     * Insert PEApproval row (FromPhase=ToPhase=fromPhase, Decision=Approve, Comment="[Review NV] ...")
     * Insert Changelog "NV X đã review phase Y, chờ TPB confirm"
     * Notify TPB cùng dept (best effort)
     * Return early — Phase KHÔNG đổi

5. Skip 2-stage hoàn toàn khi:
   - Decision=Reject (Chunk C đã handle)
   - Resume after reject (target đã pinned)
   - Admin role hoặc System (auto-approve)
   - actorUserId == null hoặc actor.DepartmentId == null

### Bug fix verified theo flow anh Kiệt
- User `long.chau` (NV.PRO, role=Procurement, DepartmentId=PRO) duyệt phase ChoPurchasing:
  - role=Procurement (không có DeptManager) → Stage=Review
  - hasConfirm=false → BLOCK transition ✅
- TPB.PRO (`tra.bui` có role DeptManager + DeptId=PRO) duyệt:
  - role=DeptManager → Stage=Confirm
  - hasConfirm=true → ALLOW transition ✅

## E. Endpoint mới

### List PE Department Approvals
```http
GET /api/purchase-evaluations/{id}/department-approvals
Response: List<PeDepartmentApprovalDto> (Id, PhaseAtApproval, DepartmentId,
          DepartmentName, Stage, ApproverUserId, ApproverName,
          ApproverRoleSnapshot ("TPB"/"NV"/"NV(bypass)"), Comment,
          ApprovedAt, IsBypassed)
```

FE Workflow Panel sẽ render dạng timeline 2-stage progress.

### Set User Bypass Review
```http
PATCH /api/users/{id}/bypass-review
Body: { canBypassReview: true|false }
[Authorize(Policy = "Users.Update")]
```

Admin toggle cho 1 user. Khi `true`, NV được duyệt thay TPB ở 2-stage (skip Stage Review, đẩy thẳng Stage Confirm).

## F. Notify TPB cùng dept

Khi NV insert `Stage=Review` mà chưa có Confirm → service query `db.Users` filter `DepartmentId == deptId && IsActive`, dùng `UserManager.GetRolesAsync` filter role `DeptManager`, push notification "Phiếu chờ TPB confirm" (best effort, fail non-critical).

## G. Verify thực tế

```
✓ Build pass mỗi commit (2 warning DocxRenderer cũ)
✓ 77 unit test pass mỗi commit (54 Domain + 23 Infra)
✓ Migration 16 applied LocalDB SolutionErp_Design OK
✓ Schema verified qua sqlcmd:
  - 3 bảng mới: ContractDepartmentApprovals, PEDepartmentApprovals, BudgetDepartmentApprovals
  - 4 cột mới: Users.CanBypassReview (bit) + 3 RejectedFromPhase (int)
✓ API startup không error (warning query filter là pattern intentional)
✓ Push 6 commit lên Gitea (2 docs session 7 + 4 code session 8)
```

## H. Files touched session 8

```
src/Backend/SolutionErp.Domain/Common/ApprovalStage.cs                       (NEW)
src/Backend/SolutionErp.Domain/Contracts/ContractDepartmentApproval.cs       (NEW)
src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluationDepartmentApproval.cs (NEW)
src/Backend/SolutionErp.Domain/Budgets/BudgetDepartmentApproval.cs           (NEW)
src/Backend/SolutionErp.Domain/Contracts/Contract.cs                         (mod: +RejectedFromPhase + nav)
src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs     (mod: +RejectedFromPhase + nav)
src/Backend/SolutionErp.Domain/Budgets/Budget.cs                             (mod: +RejectedFromPhase + nav)
src/Backend/SolutionErp.Domain/Identity/User.cs                              (mod: +CanBypassReview)

src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/
  20260504051025_AddTwoStageDeptApprovalAndSmartReject.cs                    (NEW migration)
  *.Designer.cs + ApplicationDbContextModelSnapshot.cs                       (3-file rule)
src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs   (mod: +3 DbSet)
src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/
  DepartmentApprovalsConfiguration.cs                                        (NEW: 3 config)
  ContractConfiguration.cs / PurchaseEvaluationConfiguration.cs / BudgetConfiguration.cs
                                                                             (mod: +RejectedFromPhase HasConversion)

src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs (mod: +5 DbSet)
src/Backend/SolutionErp.Application/Contracts/ContractDetailsFeatures.cs     (mod: helper Phase guard)
src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationDetailFeatures.cs (mod: helper Phase guard)
src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs                (mod: 3 inline Phase guard + smart reject Budget)
src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationDepartmentApprovalFeatures.cs (NEW)
src/Backend/SolutionErp.Application/Users/UserFeatures.cs                    (mod: +SetUserBypassReviewCommand)

src/Backend/SolutionErp.Infrastructure/Services/ContractWorkflowService.cs   (mod: smart reject + resume)
src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs (mod: smart reject + 2-stage logic + notify TPB)

src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs     (mod: +1 endpoint)
src/Backend/SolutionErp.Api/Controllers/UsersController.cs                   (mod: +1 endpoint)

docs/STATUS.md                                                               (mod: Recently Done + Phase header)
docs/HANDOFF.md                                                              (mod: cảnh báo session 9)
docs/changelog/migration-todos.md                                            (mod: Phase 9 done section)
docs/CLAUDE.md                                                               (mod: count 52→55, 15→16)
docs/changelog/sessions/2026-05-04-1230-chot-session-8-*.md                  (NEW: file này)
```

## I. Cảnh báo session 9

1. **Bug fix anh Kiệt** chỉ áp PE workflow. HĐ + Budget 2-stage scope **defer** cho khi UAT PE OK.
2. **FE Workflow Panel** chưa update — workflow vẫn hoạt động đúng (BE block transition khi NV review chưa có TPB confirm), nhưng UX chưa hiển thị 2-stage progress. User test sẽ thấy phase không đổi mà không biết tại sao.
3. **FE UserManager toggle CanBypassReview** chưa làm — admin SET qua Postman/curl tạm:
   ```
   PATCH /api/users/{id}/bypass-review
   Authorization: Bearer <admin token>
   Content-Type: application/json
   { "canBypassReview": true }
   ```
4. **Notify TPB cùng dept** dùng `UserManager.GetRolesAsync` filter `DeptManager`. Cần verify với production user có role DeptManager đúng không.
5. **Tests Phase 1 (Domain) chưa update** — không có Domain policy thay đổi. Tests Service-layer 2-stage logic cần `UserManager` + `IDateTime` DI helper, defer Phase 3 mini.
6. **Cron audit định kỳ 2026-05-01** đã quá hạn 3 ngày, vẫn EMPTY runtime (CronList trống). Cần manual trigger hoặc recreate cron.

## J. Lessons learned

1. **Iterate plan với user trước khi code lớn** — 4 vòng review (Claude propose → user push back "tách bảng riêng") tránh implement sai approach. Schema kết quả simple hơn cả 3 option Claude đề xuất.

2. **Per-chunk commit pattern** — 5 chunk small commits (A-B-C-D-E1) thay vì 1 commit monolithic giúp:
   - Build + test pass mỗi chunk → bug khu trú dễ
   - Rollback granular nếu chunk nào sai
   - Code review easier (each commit < 100 LOC change)

3. **Smart reject với jump-back** đơn giản hơn dự đoán — chỉ thêm 1 nullable field `RejectedFromPhase` + 2 if branch trong service. Bypass policy guard ở resume case là key.

4. **Helper extract pattern (EnsureContractType extend)** — 14 handler share 1 guard logic. DRY + 1 nơi maintain.

5. **2 file `User` ở 2 namespace** — Domain.Identity.User vs SolutionErp.Domain.Identity.User — cần explicit `Domain.Identity.User` khi disambiguate.

## K. Stats sau session 8

| | Trước S8 | Sau S8 |
|---|---:|---:|
| BE LOC | ~13050 | ~13750 (+700) |
| DB tables | 52 | **55** (+3 DepartmentApprovals) |
| Migrations | 15 | **16** |
| API endpoints | ~128 | **~131** (+3) |
| FE pages | ~31 | ~31 (FE chưa update) |
| Tests | 77 | 77 (chưa thêm) |
| Gotchas | 41 | 41 |
| Demo user | 30 | 30 |
| Commits S8 | 0 | **5** (A-B-C-D-E1) |
| Session log | 18 | **19** |
