[CLAUDE] App+Api+Docs: Chunk E1 — List endpoint + Bypass-review + Notify TPB + chốt session 8
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m15s

3 endpoint mới + Notify TPB + Docs update để chốt session 8.

Application:
- PurchaseEvaluationDepartmentApprovalFeatures.cs (NEW):
  * ListPeDepartmentApprovalsQuery + DTO PeDepartmentApprovalDto
  * Join Departments (lấy Name) + lookup Users.FullName denorm cho FE timeline
- UserFeatures.cs: SetUserBypassReviewCommand + Handler dùng UserManager.UpdateAsync
- IApplicationDbContext: thêm DbSet<User> Users + DbSet<Role> Roles (cần cho lookup)

Api:
- PurchaseEvaluationsController: GET /api/purchase-evaluations/{id}/department-approvals
- UsersController: PATCH /api/users/{id}/bypass-review (Authorize Users.Update)

Infra:
- PurchaseEvaluationWorkflowService: notify TPB cùng dept khi NV review.
  Query db.Users.Where(DeptId match + IsActive) → UserManager.GetRolesAsync
  filter DeptManager → notifications.NotifyAsync. Best effort fail non-critical.

Docs:
- STATUS.md: Recently Done thêm row session 8 + Phase header update
  count 52→55 tables, 15→16 migrations, 128→131 endpoints
- HANDOFF.md: TL;DR session 8 + 8 cảnh báo session 9 (FE chưa làm,
  test flow anh Kiệt, smart reject test, lock edit test, ...)
- migration-todos.md: Phase 9 done section đầy đủ 3 ràng buộc + pending Chunk E-bis
- CLAUDE.md: count 52→55 + migration 16 description
- session log: 2026-05-04-1230-chot-session-8-2-stage-dept-approval.md (full report)

Verify final:
- Build pass 0 warning 0 error
- 77 unit test pass (54 Domain + 23 Infra)
- Migration 16 applied LocalDB OK + schema verified

Total session 8 cumulative: 5 commit per-chunk:
- 5fe61cc (A: Migration 16 schema)
- 14f3c9f (B: Lock edit guards 17 handler)
- 9747f8c (C: Smart reject + Resume 3 module)
- a532ba6 (D: PE 2-stage logic)
- (current E1: List + Notify + Bypass + Docs)

Pending Chunk E-bis (defer cho session 9 sau UAT PE):
- FE Workflow Panel hiển thị 2-stage timeline
- FE UserManager toggle CanBypassReview
- HĐ + Budget 2-stage extension
- Tests Phase 3 mini cho 2-stage Service-layer logic

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-04 12:42:47 +07:00
parent a532ba6fc3
commit 3c4931687a
11 changed files with 504 additions and 7 deletions

View File

@ -50,7 +50,7 @@ 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ó 15 migration → 52 bảng** (Phase 8 thêm migration 15 `AddPurchaseEvaluationDepartmentOpinions` — 1 bảng UNIQUE PEId+Kind cho 4 box sign-off "Phê duyệt/CCM/MuaHàng/SM-PM")
- **Hiện có 16 migration → 55 bảng** (Phase 9 — Migration 16 `AddTwoStageDeptApprovalAndSmartReject` — 3 bảng `*DepartmentApprovals` + `Users.CanBypassReview` + 3 `RejectedFromPhase` cho smart reject + 2-stage NV/TPB approval per dept)
### Modules

View File

@ -1,6 +1,38 @@
# HANDOFF — Brief 5 phút cho session tiếp theo
**Last updated:** 2026-04-30 (Session 6**MD audit + compact -288 dòng + 3 skill refresh + 2 rule mới timing test + audit định kỳ**)
**Last updated:** 2026-05-04 (Session 8**Migration 16: 2-stage dept approval + smart reject + lock edit. Đóng bug anh Kiệt báo: NV duyệt được hết phase PE.**)
## TL;DR Session 8 (04/05 — code lớn, 5 commit per-chunk)
**Output session 8** — đóng bug anh Kiệt + thêm 3 ràng buộc workflow:
-**Migration 16** `AddTwoStageDeptApprovalAndSmartReject` — 4 ALTER (3 RejectedFromPhase int + Users.CanBypassReview bit) + 3 CREATE TABLE (`Contract/PE/Budget DepartmentApprovals` UNIQUE (TargetId, Phase, Dept, Stage)) + 12 indexes.
-**Lock edit** 17 handler thêm guard Phase != DangSoanThao (Contract Detail × 15 qua helper, PE Detail × 5 qua helper mới, Budget Detail × 3 inline).
-**Smart reject + Resume** 3 module — Reject = lưu phase nguồn + force về DangSoanThao. Resume = jump straight tới phase đã reject (skip phase trung gian, bypass policy guard).
-**PE 2-stage logic** trong `PurchaseEvaluationWorkflowService` — TPB/CanBypass → Confirm; NV → Review only, BLOCK transition cho đến khi TPB confirm.
-**3 endpoint mới**: `GET /pe/{id}/department-approvals` (List), `PATCH /users/{id}/bypass-review` (toggle), Notify TPB cùng dept khi NV review.
-**Verify**: Build + 77 test pass mỗi commit. Migration applied LocalDB OK. Schema verified.
-**6 commit pushed** (2 docs S7 + 5 code S8).
## ⚠️ CẢNH BÁO session tiếp (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 block đúng (BE), nhưng UX chưa hiển thị 2-stage progress. User test sẽ thấy phase không đổi mà không hiểu tại sao "stuck". Phải UAT với hint cho user trước khi code FE.
3. **FE UserManager toggle CanBypassReview chưa làm** — tạm thời SET qua HTTP PATCH:
```
PATCH /api/users/{userId}/bypass-review
Authorization: Bearer <admin>
{ "canBypassReview": true }
```
4. **Test thực tế bug fix flow**:
- Login `phuong.nguyen` (NV.PRO, role=Procurement, DeptId=PRO) tạo phiếu PE type A
- Trình DangSoanThao → ChoPurchasing
- phuong.nguyen click Duyệt phase ChoPurchasing → expect: phase KHÔNG đổi, có row Stage=Review
- `tra.bui` (TPB.PRO, role=DeptManager) click Duyệt → expect: phase chuyển ChoCCM
5. **Notify TPB cùng dept** dùng `UserManager.GetRolesAsync` filter `DeptManager`. Best effort, fail OK.
6. **Cron audit định kỳ 2026-05-01** đã quá hạn 3 ngày, vẫn EMPTY runtime. Cần manual trigger.
7. **Smart reject test**: Reject phase ChoCCM → DangSoanThao + RejectedFromPhase=ChoCCM. Drafter sửa Detail + trình lại → jump straight tới ChoCCM (skip ChoPurchasing).
8. **Lock edit test**: HĐ ở Phase=DangGopY → cố sửa Detail → expect 409 Conflict "đã trình duyệt, không thể chỉnh sửa".
## TL;DR Session 6 (30/04 — không code, chỉ docs)

View File

@ -2,9 +2,9 @@
> **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`.
**Last updated:** 2026-04-30 (Session 6**MD audit + compact -288 dòng + 3 skill refresh + rule timing test + rule audit định kỳ §6.4**)
**Last updated:** 2026-05-04 (Session 8**2-stage department approval + smart reject + lock edit guards (Migration 16). Đóng bug anh Kiệt báo: NV duyệt được hết phase PE workflow.**)
## 📍 Phase hiện tại: **Phase 9 active — UAT + Ops + carry over** — 52 DB tables, 15 migrations, ~128 API endpoints, 31 FE pages. **77 unit test pass** (54 Domain + 17 Infra + 6 PE WF Application) — CI fail-fast. Path filter docs-only skip 0s. 41 gotcha. 30 demo user. **6 skill (3 refresh)**. Doc audit định kỳ §6.4 chốt cron 2026-05-01 fire mai.
## 📍 Phase hiện tại: **Phase 9 active — UAT + Ops + 2-stage dept approval** — **55 DB tables (52+3), 16 migrations, ~131 API endpoints (+3 dept-approvals/bypass-review), 31 FE pages (FE 2-stage chưa update). 77 unit test pass** (54 Domain + 23 Infra). 41 gotcha. 30 demo user. 6 skill.
### 🌐 Production URLs
@ -44,14 +44,23 @@
- [ ] **Phase 4** — API smoke tests qua WebApplicationFactory ~7 test
- [ ] **Phase 5** — FE Vitest cho lib utility (queryMatches, fmtMoney) ~10 test
### E. Audit định kỳ (cron tự fire)
### E. 2-stage dept approval — Chunk E-bis (FE + extend)
- [ ] **2026-05-01** (mai) — `solution-erp-skill-audit-monthly` cron fire 9:00. Combined audit theo §6.4 + §9.4 (skill staleness + doc drift + count consistency). Log → `docs/changelog/skill-audit-2026-05.md`
- [ ] **FE Workflow Panel update** (cả fe-admin + fe-user) — hiển thị progress 2-stage timeline per phase × dept. Dùng endpoint `GET /pe/{id}/department-approvals`.
- [ ] **FE UserManager toggle** `CanBypassReview` checkbox per user. Endpoint `PATCH /users/{id}/bypass-review` đã sẵn.
- [ ] **HĐ 2-stage** mở rộng: `ContractWorkflowService.TransitionAsync` thêm 2-stage logic + endpoint List `ContractDepartmentApprovals` (sau verify PE OK).
- [ ] **Budget 2-stage** mở rộng tương tự (low priority, ít user duyệt budget per dept).
- [ ] **Tests Phase 3 mini** cho 2-stage logic ở `PurchaseEvaluationWorkflowService` (cần UserManager DI helper).
### F. Audit định kỳ (cron tự fire)
- [ ] **2026-05-01** (đã quá hạn 3 ngày, cần manual trigger hoặc recreate cron) — `solution-erp-skill-audit-monthly` Combined audit theo §6.4 + §9.4. Log → `docs/changelog/skill-audit-2026-05.md`
## ✅ Recently Done (newest on top)
| Ngày | Ai | Task | Commit |
|---|---|---|---|
| 2026-05-04 | Claude | **Session 8 — Migration 16: 2-stage dept approval + smart reject + lock edit (đóng bug anh Kiệt)** — Anh Kiệt báo: NV.PRO tạo phiếu PE → duyệt được hết phase = phân quyền sai. Schema mới: 3 bảng `*DepartmentApprovals` (Contract/PE/Budget) UNIQUE (TargetId, Phase, Dept, Stage). 4 cột mới: `Users.CanBypassReview` bit + 3 `RejectedFromPhase` int. Logic 2-stage trong `PurchaseEvaluationWorkflowService.TransitionAsync`: user.DepartmentId != null → DeptManager (TPB) Stage=Confirm; CanBypassReview=true → Stage=Confirm+IsBypassed; else NV → Stage=Review only, BLOCK transition cho đến khi TPB confirm. Smart reject: Decision=Reject → set RejectedFromPhase, force về DangSoanThao. Resume sau reject: Drafter trình lại từ DangSoanThao + RejectedFromPhase != null → jump straight tới phase đã reject (skip phase trung gian). Lock edit: 17 handler thêm guard Phase != DangSoanThao (Contract Detail × 15, PE Detail × 5, Budget Detail × 3). 3 endpoint mới: `GET /pe/{id}/department-approvals` (FE Workflow Panel hiển thị progress) + `PATCH /users/{id}/bypass-review` (admin toggle) + Notify TPB cùng dept khi NV review. **HĐ + Budget 2-stage scope defer** (chỉ PE first đóng bug). FE update + Tests defer Chunk E-bis. | `5fe61cc` (A) · `14f3c9f` (B) · `9747f8c` (C) · `a532ba6` (D) · (current) |
| 2026-04-30 | Claude | **Session 6 — MD audit + compact + 3 skill refresh + 2 rule mới** — Compact 3 file core (-288 dòng): STATUS -27%, HANDOFF -32%, migration-todos -35%. Archive 51 row Recently Done Phase 0-7 → `changelog/recently-done-archive-2026-04.md`. Refresh 3 skill stale: `form-engine` (Phase 2 MVP → Tier 3 feature-complete + bỏ section duplicate gen mã HĐ), `permission-matrix` (12 menu → ~60 menu key + Bg_*/Pe_*/PeWf_* + inheritance roots), `ef-core-migration` (24 DbSet → 52 bảng + ERD update). Rule mới `rules.md §7 Khi nào viết test — timing rule` (5-row table compact, sau khi rút gọn từ 70 dòng overkill). Rule mới `rules.md §6.4 Audit + compact MD định kỳ` (cadence + checklist + anti-pattern, KHÔNG rewrite toàn bộ). `rules.md §9.4 Skill audit` mở rộng cross-ref §6.4. | (current) |
| 2026-04-29 | Claude | **Tests Phase 3 mini + 3 gotcha CI mới (#39 #40 #41)**`tests/.../Application/PeWorkflowAdminTests.cs` 6 test versioning logic (CreatePeWorkflowDefinition: first version IsActive=true, second deactivates first, different EvaluationType independent, persists steps ordered + approvers per step, third version increments to v3). Total **77 test** (54 Domain + 17 Infra + 6 PE WF Application). Gotcha #39 act_runner github.com TCP timeout 21s + manual checkout fix. #40 npm junction cache fail `tsc not found` rolled back. #41 paths-ignore behavior + workflow file exclusion. | `b874743` |
| 2026-04-29 | Claude | **CI Path filter docs-only skip live**`paths-ignore` trong on:push lookup `docs/**`/`**/*.md`/`.claude/skills/**`/`.gitignore`. Commit chỉ touch docs SKIP CI hoàn toàn (saving ~196s/commit, ~30% commit thuộc loại này). Verify `512880c` (docs-only) → Gitea NO trigger run #113. | `29eb5d9` · `a21790d` · `512880c` |

View File

@ -157,6 +157,35 @@ Session log: `2026-04-28-chot-session-4-budget.md`.
## 📝 Phase 9 — UAT + Ops + carry over (Session 6+ active)
### ✅ Session 8 done (2026-05-04) — Migration 16: 2-stage dept approval + smart reject + lock edit
**Bối cảnh:** Anh Kiệt (FDC) báo bug PE workflow: NV.PRO tạo phiếu → duyệt được hết phase. Phân quyền sai vì policy chỉ check role, không check Stage 2-cấp.
**3 ràng buộc gộp 1 migration:**
- [x] **Lock edit khi Phase != DangSoanThao** — 17 handler thêm guard (Contract Detail × 15 qua helper `EnsureContractType`, PE Detail × 5 qua helper mới `PurchaseEvaluationDraftGuard`, Budget Detail × 3 inline). KHÔNG lock Comment + Attachment + Opinion (workflow design intent).
- [x] **Smart reject + Resume**`Decision=Reject``entity.RejectedFromPhase = currentPhase` + force `targetPhase=DangSoanThao`. Resume: `Drafter trình từ DangSoanThao + RejectedFromPhase != null → jump tới phase đã reject + clear field`. Bypass policy guard ở resume.
- [x] **2-stage dept approval (PE only v1)** — User.DepartmentId != null + role guard:
- DeptManager (TPB) → Stage=Confirm trực tiếp
- User.CanBypassReview=true → Stage=Confirm + IsBypassed=true
- Else (NV) → Stage=Review only, BLOCK transition cho đến khi TPB confirm
- Schema: 3 bảng `*DepartmentApprovals` UNIQUE (TargetId, Phase, Dept, Stage)
- [x] **Migration 16** `AddTwoStageDeptApprovalAndSmartReject` — 4 ALTER + 3 CREATE TABLE + 12 indexes + FK Cascade
- [x] **Endpoint mới**: `GET /api/purchase-evaluations/{id}/department-approvals` (List), `PATCH /api/users/{id}/bypass-review` (toggle)
- [x] **Notify TPB cùng dept** khi NV review (best effort, fail non-critical)
- [x] **Verify**: Build pass + 77 test pass + Migration applied LocalDB OK + schema verified qua sqlcmd
- [x] 5 commit per-chunk: `5fe61cc` (A) · `14f3c9f` (B) · `9747f8c` (C) · `a532ba6` (D) · current (E1)
Session log: `2026-05-04-1230-chot-session-8-2-stage-dept-approval.md`.
**Pending Chunk E-bis (defer):**
- [ ] FE Workflow Panel hiển thị progress 2-stage timeline
- [ ] FE UserManager toggle `CanBypassReview` checkbox
- [ ] HĐ 2-stage mở rộng (`ContractWorkflowService` thêm 2-stage logic + endpoint List)
- [ ] Budget 2-stage mở rộng (low priority)
- [ ] Tests 2-stage logic Service-layer (cần UserManager DI helper)
### ✅ Session 6 done (2026-04-30 — pure docs work)
- [x] **MD audit + compact** — STATUS -27%, HANDOFF -32%, migration-todos -35%, archive 51 row Phase 0-7 cũ

View File

@ -0,0 +1,273 @@
# 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** |

View File

@ -220,6 +220,15 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
await mediator.Send(new DeletePeDepartmentOpinionCommand(id, kind), ct);
return NoContent();
}
// ========== 2-stage department approvals (Phase 9 — Migration 16) ==========
// List approvals progress per phase × dept × stage. FE Workflow Panel
// hiển thị timeline: phase nào đã review/confirm, ai duyệt khi nào.
[HttpGet("{id:guid}/department-approvals")]
public async Task<ActionResult<List<PeDepartmentApprovalDto>>> ListDepartmentApprovals(
Guid id, CancellationToken ct)
=> Ok(await mediator.Send(new ListPeDepartmentApprovalsQuery(id), ct));
}
public record OpinionBody(PeDepartmentKind Kind, string? Opinion, bool Sign);

View File

@ -62,7 +62,18 @@ public class UsersController(IMediator mediator) : ControllerBase
await mediator.Send(new UnlockUserCommand(id), ct);
return NoContent();
}
// 2-stage department approval (Phase 9): admin toggle bypass-review per user.
[HttpPatch("{id:guid}/bypass-review")]
[Authorize(Policy = "Users.Update")]
public async Task<IActionResult> SetBypassReview(
Guid id, [FromBody] SetBypassReviewBody body, CancellationToken ct)
{
await mediator.Send(new SetUserBypassReviewCommand(id, body.CanBypassReview), ct);
return NoContent();
}
}
public record AssignRolesBody(List<string> Roles);
public record ResetPasswordBody(string NewPassword);
public record SetBypassReviewBody(bool CanBypassReview);

View File

@ -23,6 +23,8 @@ public interface IApplicationDbContext
DbSet<WorkItem> WorkItems { get; }
DbSet<MenuItem> MenuItems { get; }
DbSet<Permission> Permissions { get; }
DbSet<User> Users { get; }
DbSet<Role> Roles { get; }
DbSet<ContractTemplate> ContractTemplates { get; }
DbSet<ContractClause> ContractClauses { get; }
DbSet<Contract> Contracts { get; }

View File

@ -0,0 +1,81 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Common;
using SolutionErp.Domain.PurchaseEvaluations;
namespace SolutionErp.Application.PurchaseEvaluations;
// 2-stage department approval list (Phase 9 — Migration 16).
// Query để FE hiển thị progress: phase nào × dept nào đã review/confirm.
//
// FE Workflow Panel sẽ render dạng timeline:
// - Phase ChoPurchasing (PRO):
// - Stage Review: long.chau (NV) — 14:30
// - Stage Confirm: tra.bui (TPB) — 14:35 → unlock transition
//
// Insertion + Block logic ở PurchaseEvaluationWorkflowService.TransitionAsync.
public record ListPeDepartmentApprovalsQuery(Guid PurchaseEvaluationId) : IRequest<List<PeDepartmentApprovalDto>>;
public record PeDepartmentApprovalDto(
Guid Id,
int PhaseAtApproval,
Guid DepartmentId,
string? DepartmentName,
ApprovalStage Stage,
Guid ApproverUserId,
string? ApproverName,
string? ApproverRoleSnapshot, // "TPB" / "NV" / "NV(bypass)"
string? Comment,
DateTime ApprovedAt,
bool IsBypassed);
public class ListPeDepartmentApprovalsQueryHandler(IApplicationDbContext db)
: IRequestHandler<ListPeDepartmentApprovalsQuery, List<PeDepartmentApprovalDto>>
{
public async Task<List<PeDepartmentApprovalDto>> Handle(
ListPeDepartmentApprovalsQuery request, CancellationToken ct)
{
// Join với Departments để lấy Name. Join với Users để lấy ApproverName denorm.
var rows = await (
from a in db.PurchaseEvaluationDepartmentApprovals.AsNoTracking()
join d in db.Departments.AsNoTracking() on a.DepartmentId equals d.Id into deptJoin
from d in deptJoin.DefaultIfEmpty()
where a.PurchaseEvaluationId == request.PurchaseEvaluationId
orderby a.PhaseAtApproval, a.Stage, a.ApprovedAt
select new
{
a.Id,
a.PhaseAtApproval,
a.DepartmentId,
DepartmentName = d != null ? d.Name : null,
a.Stage,
a.ApproverUserId,
a.ApproverRoleSnapshot,
a.Comment,
a.ApprovedAt,
a.IsBypassed,
}).ToListAsync(ct);
// Lookup approver names (separate query to avoid Identity user join complexity)
var userIds = rows.Select(r => r.ApproverUserId).Distinct().ToList();
var users = await db.Users.AsNoTracking()
.Where(u => userIds.Contains(u.Id))
.Select(u => new { u.Id, Name = u.FullName ?? u.Email ?? "" })
.ToDictionaryAsync(u => u.Id, u => u.Name, ct);
return rows.Select(r => new PeDepartmentApprovalDto(
r.Id,
r.PhaseAtApproval,
r.DepartmentId,
r.DepartmentName,
r.Stage,
r.ApproverUserId,
users.TryGetValue(r.ApproverUserId, out var n) ? n : null,
r.ApproverRoleSnapshot,
r.Comment,
r.ApprovedAt,
r.IsBypassed)).ToList();
}
}

View File

@ -267,3 +267,23 @@ public class UnlockUserCommandHandler(UserManager<User> userManager) : IRequestH
await userManager.ResetAccessFailedCountAsync(user);
}
}
// ========== SET BYPASS REVIEW (Phase 9 — Migration 16) ==========
// Admin toggle CanBypassReview cho 1 user. Khi true, user (NV) được duyệt
// thay TPB ở 2-stage department approval (skip Stage Review, đẩy thẳng
// Stage Confirm). Mặc định false (an toàn).
public record SetUserBypassReviewCommand(Guid Id, bool CanBypassReview) : IRequest;
public class SetUserBypassReviewCommandHandler(UserManager<User> userManager)
: IRequestHandler<SetUserBypassReviewCommand>
{
public async Task Handle(SetUserBypassReviewCommand request, CancellationToken ct)
{
var user = await userManager.FindByIdAsync(request.Id.ToString())
?? throw new NotFoundException("User", request.Id);
user.CanBypassReview = request.CanBypassReview;
var result = await userManager.UpdateAsync(user);
if (!result.Succeeded)
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description)));
}
}

View File

@ -176,7 +176,38 @@ public class PurchaseEvaluationWorkflowService(
ContextNote = comment,
});
// TODO Chunk E: notify TPB cùng dept để confirm.
// Notify TPB cùng dept để confirm. Best effort — fail OK.
try
{
var managers = await db.Users.AsNoTracking()
.Where(u => u.DepartmentId == deptId && u.Id != actorUid && u.IsActive)
.Select(u => u.Id)
.ToListAsync(ct);
if (managers.Count > 0)
{
// Filter: chỉ notify user có role DeptManager (TPB).
// Không có direct join với UserRoles ở IApplicationDbContext —
// dùng UserManager để filter từng user.
foreach (var mgrId in managers)
{
var mgr = await userManager.FindByIdAsync(mgrId.ToString());
if (mgr is null) continue;
var roles = await userManager.GetRolesAsync(mgr);
if (!roles.Contains(AppRoles.DeptManager)) continue;
await notifications.NotifyAsync(
mgrId,
NotificationType.ContractPhaseTransition,
title: $"Phiếu {evaluation.MaPhieu ?? evaluation.TenGoiThau} chờ TPB confirm",
description: $"NV {reviewerName} đã review phase {fromPhase}. Vui lòng vào confirm để workflow tiếp tục.",
href: $"/purchase-evaluations/{evaluation.Id}",
refId: evaluation.Id,
ct: ct);
}
}
}
catch { /* notification fail non-critical */ }
await db.SaveChangesAsync(ct);
return;
}