[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

@ -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** |