Files
solution-erp/.claude/skills/contract-workflow/SKILL.md
pqhuy1987 7e957a7654 [CLAUDE] Phase3: Workflow MVP — 9-phase state machine + code gen + FE Inbox/Detail
Backend Contracts domain (5 entities):
- Contract aggregate: Phase (9 enum), SlaDeadline, MaHopDong, BypassProcurementAndCCM, DraftData, SlaWarningSent
- ContractApproval: FromPhase → ToPhase, ApproverUserId (null = system auto-approve), Decision, Comment
- ContractComment: thread theo Phase current
- ContractAttachment: FileName + StoragePath + Purpose (DraftExport/ScannedSigned/SealedCopy)
- ContractCodeSequence: Prefix PK + LastSeq — atomic gen

EF configs:
- Unique MaHopDong filtered [MaHopDong] IS NOT NULL
- Indexes: Phase+IsDeleted, SupplierId, ProjectId, SlaDeadline, ContractId+ApprovedAt, ContractId+CreatedAt
- Cascade delete Approvals/Comments/Attachments khi Contract xoa
- Query filter IsDeleted
- Migration AddContractsWorkflow (DB 19 tables)

Workflow service:
- IContractWorkflowService.TransitionAsync:
  - Adjacency check qua Transitions Dict<(from,to), roles[]> (12 transitions)
  - Role guard: user phai co role ∈ allowed
  - Admin bypass (role Admin pass moi check)
  - System bypass (userId=null + Decision=AutoApprove → cho SLA job sau nay)
  - Bypass CCM: BypassProcurementAndCCM=true cho phep DangInKy → DangTrinhKy skip phase 6
  - Gen ma HD khi chuyen DangDongDau (idempotent — khong gen lai neu da co)
  - Reset SlaDeadline = UtcNow + PhaseSla
  - Insert ContractApproval row

Code generator (RG-001):
- 7 format theo ContractType: HDTP / HDGK / NCC / HDDV / MB + 2 framework (year prefix)
- BeginTransactionAsync(Serializable) + ContractCodeSequences UPSERT → atomic
- Idempotent: neu MaHopDong da co thi skip

CQRS (8 feature, ContractFeatures.cs):
- CreateContractCommand + Validator + Handler (set SlaDeadline = +7d)
- UpdateContractDraftCommand (chi khi Phase=DangSoanThao)
- TransitionContractCommand (delegate → WorkflowService)
- AddCommentCommand (phase = hien tai)
- ListContractsQuery (PagedResult + filter phase/supplier/project/search)
- GetMyInboxQuery (map Phase → actor roles, filter theo role user)
- GetContractQuery (detail + approvals + comments + attachments + resolve user names)
- DeleteContractCommand (soft, block > DangInKy)

Controller:
- ContractsController 8 endpoint: GET list/inbox/detail, POST create/transition/comment, PUT update, DELETE

Frontend fe-admin (2 page moi):
- types/contracts.ts: ContractPhase const + Label + Color maps + types
- components/PhaseBadge.tsx
- pages/contracts/ContractsListPage.tsx: filter phase + search + click → detail
- pages/contracts/ContractDetailPage.tsx: 2-col layout (info+comments | timeline), action dialog select target phase + comment

Frontend fe-user (4 page moi + 14 file shared):
- cp 14 file shared tu fe-admin (menuKeys, types/*, DataTable, PhaseBadge, Dialog, Textarea, Select, apiError, usePermission, PermissionGuard)
- AuthContext update: load menu tu /menus/me + cache
- Layout: menu fixed 3 muc + user info + roles display
- InboxPage: list HD cho role user xu ly (sort theo SLA)
- ContractCreatePage: form chon loai + template + NCC + du an + gia tri + bypass CDT
- ContractDetailPage: duplicate fe-admin pattern (convention)
- MyContractsPage: list HD cua toi
- App.tsx: 4 route moi

E2E verified:
- Setup Supplier + Project
- POST /contracts → 201 + phase=2
- POST /contracts/{id}/transitions x7 → di het 9 phase
- Final: MaHopDong = "FLOCK 01/HĐGK/SOL&PVL2026/01" dung format RG-001
- Approvals: 7 rows audit day du

Docs:
- .claude/skills/contract-workflow/SKILL.md: placeholder → full spec voi state machine, SLA table, role matrix, 7 code format, code pointers, API, E2E workflow, pitfalls
- docs/changelog/sessions/2026-04-21-1330-phase3-workflow.md: session log
- docs/STATUS.md: Phase 3 MVP done, next Phase 4
- docs/HANDOFF.md: update phase status + file tree + commit log + testing points
- docs/changelog/migration-todos.md: tick Phase 3 MVP items + add iteration 2 list

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:26:09 +07:00

186 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: contract-workflow
description: State machine 9 phase cho hợp đồng TP/NCC/Tổ đội — transition guard, role check, SLA deadline, auto-gen mã HĐ RG-001. Dùng khi debug chuyển phase, 403 forbidden, code sai format, bypass Chủ đầu tư.
when-to-use:
- "transition contract"
- "chuyển phase hợp đồng"
- "HĐ quá hạn auto-approve"
- "role không duyệt được"
- "reject contract về draft"
- "mã HĐ sai format"
- "bypass CCM chủ đầu tư"
---
# Contract Workflow Skill
> **Status:** Phase 3 IMPLEMENTED (MVP — state transitions + code gen). Còn thiếu: SLA hosted service, email notify, in-app realtime.
## Domain entities (implemented)
```
Contract ─────< ContractApproval (lịch sử mỗi transition)
─────< ContractComment (thread góp ý)
─────< ContractAttachment (scan signed/sealed)
ContractCodeSequence (Prefix PK, LastSeq) — gen mã HĐ atomic
```
## 9 phase state machine
```
DangChon(1) → DangSoanThao(2) → DangGopY(3) → DangDamPhan(4) → DangInKy(5) →
DangKiemTraCCM(6) → DangTrinhKy(7) → DangDongDau(8) → DaPhatHanh(9)
Alternates:
DangSoanThao → TuChoi(99)
DangGopY → DangSoanThao (revise)
DangKiemTraCCM → DangSoanThao (CCM reject)
DangTrinhKy → DangSoanThao (BOD reject)
Bypass (HĐ Chủ đầu tư, BypassProcurementAndCCM=true):
DangInKy → DangTrinhKy (skip CCM)
```
## SLA mặc định (sau transition, set `Contract.SlaDeadline = UtcNow + sla`)
| Phase | SLA |
|---|---|
| DangSoanThao | 7d |
| DangGopY | 7d |
| DangDamPhan | 7d |
| DangInKy | 1d |
| DangKiemTraCCM | 3d |
| DangTrinhKy | 1d |
| DangDongDau | none |
| DaPhatHanh | none |
## Role × Phase guard matrix
Xem `ContractWorkflowService.Transitions` dictionary. Tóm tắt:
| Phase hiện tại → target | Roles được phép |
|---|---|
| DangSoanThao → DangGopY | Drafter, DeptManager |
| DangSoanThao → TuChoi | Drafter, DeptManager |
| DangGopY → DangDamPhan | Drafter, DeptManager |
| DangGopY → DangSoanThao | ProjectManager, Procurement, CostControl, Finance, Accounting, Equipment |
| DangDamPhan → DangInKy | Drafter, DeptManager, ProjectManager |
| DangInKy → DangKiemTraCCM | Drafter, DeptManager, ProjectManager |
| DangInKy → DangTrinhKy (bypass) | Drafter, DeptManager, ProjectManager (chỉ khi `BypassProcurementAndCCM=true`) |
| DangKiemTraCCM → DangTrinhKy | CostControl |
| DangKiemTraCCM → DangSoanThao | CostControl |
| DangTrinhKy → DangDongDau | Director, AuthorizedSigner |
| DangTrinhKy → DangSoanThao | Director, AuthorizedSigner |
| DangDongDau → DaPhatHanh | HrAdmin |
**Admin bypass:** user có role `Admin` → pass mọi guard. Dùng để test flow nhanh.
## Mã HĐ gen (RG-001)
Xem `ContractCodeGenerator.GenerateAsync()`. Format theo loại HĐ:
| Type | Format |
|---|---|
| HopDongThauPhu | `{ProjectCode}/HĐTP/SOL&{SupplierCode}/{Seq:D2}` |
| HopDongGiaoKhoan | `{ProjectCode}/HĐGK/SOL&{SupplierCode}/{Seq:D2}` |
| HopDongNhaCungCap | `{ProjectCode}/NCC/SOL&{SupplierCode}/{Seq:D2}` |
| HopDongDichVu | `{ProjectCode}/HĐDV/SOL&{SupplierCode}/{Seq:D2}` |
| HopDongMuaBan | `{ProjectCode}/MB/SOL&{SupplierCode}/{Seq:D2}` |
| HopDongNguyenTacNCC | `{Year}/NCC/SOL&{SupplierCode}/{Seq:D2}` ← framework |
| HopDongNguyenTacDichVu | `{Year}/HĐDV/SOL&{SupplierCode}/{Seq:D2}` ← framework |
**Transactional:** `BeginTransactionAsync(IsolationLevel.Serializable)` + `ContractCodeSequences` row UPDATE. Tránh race condition khi 2 HĐ cùng prefix gen song song.
**Gen khi nào:** transition sang `DangDongDau`. Nếu `MaHopDong` đã có (reject rồi approve lại) → giữ nguyên, không gen lại.
## Code pointers
**Backend:**
- `Domain/Contracts/Contract.cs` — aggregate root
- `Domain/Contracts/ContractApproval.cs` — history
- `Domain/Contracts/ContractComment.cs` — thread
- `Domain/Contracts/ContractAttachment.cs` — files
- `Domain/Contracts/ContractCodeSequence.cs` — seq table
- `Application/Contracts/Services/IContractWorkflowService.cs` + `IContractCodeGenerator.cs`
- `Infrastructure/Services/ContractWorkflowService.cs` — state + role guard
- `Infrastructure/Services/ContractCodeGenerator.cs` — transactional gen
- `Application/Contracts/ContractFeatures.cs` — CQRS (Create, Update draft, Transition, AddComment, List, Inbox, GetDetail, Delete)
- `Api/Controllers/ContractsController.cs` — REST endpoints
**Frontend:**
- `fe-admin/src/pages/contracts/ContractsListPage.tsx` — full list admin view
- `fe-admin/src/pages/contracts/ContractDetailPage.tsx` — detail + timeline + action
- `fe-user/src/pages/InboxPage.tsx` — HĐ chờ role tôi xử lý
- `fe-user/src/pages/contracts/ContractCreatePage.tsx` — tạo HĐ draft
- `fe-user/src/pages/contracts/ContractDetailPage.tsx` — duplicate có chủ đích
- `fe-user/src/pages/contracts/MyContractsPage.tsx` — HĐ của tôi
- `fe-admin/src/types/contracts.ts` + `fe-user/src/types/contracts.ts` — type mirror
- `fe-admin/src/components/PhaseBadge.tsx` — badge màu theo phase
## API endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | `/api/contracts` | List với filter phase/supplier/project + paging |
| GET | `/api/contracts/inbox` | HĐ chờ role của user xử lý |
| GET | `/api/contracts/{id}` | Detail + approvals + comments + attachments |
| POST | `/api/contracts` | Tạo draft (Phase = DangSoanThao) |
| PUT | `/api/contracts/{id}` | Update draft (chỉ khi Phase = DangSoanThao) |
| POST | `/api/contracts/{id}/transitions` | Chuyển phase (body: `{targetPhase, decision, comment}`) |
| POST | `/api/contracts/{id}/comments` | Thêm comment vào thread |
| DELETE | `/api/contracts/{id}` | Soft delete (chỉ < DangInKy) |
## Guard Rules đã implement
- **State adjacency:** chỉ cho chuyển giữa các (from, to) đã khai báo trong `Transitions` dict
- **Role check:** role của actor phải allowed roles của transition đó
- **Admin bypass:** role `Admin` pass mọi check
- **System bypass:** `actorUserId == null` + `Decision = AutoApprove` cho phép (dành cho SLA job Phase 3.2)
- **Bypass CCM:** `Contract.BypassProcurementAndCCM=true` cho phép `DangInKy → DangTrinhKy` (skip CCM). Default false phải qua CCM
- **Self-delete:** không cho xóa đã qua `DangInKy`
## Workflow tạo HĐ end-to-end (testable)
```bash
# 1. Setup master data
POST /api/suppliers { code: "PVL", name: "...", type: 1 }
POST /api/projects { code: "FLOCK 01", name: "..." }
# 2. Tạo HĐ
POST /api/contracts { type: 2, supplierId, projectId, giaTri: 150000000, tenHopDong: "..." }
# → Phase = DangSoanThao, SlaDeadline = +7d
# 3. Submit góp ý
POST /api/contracts/{id}/transitions { targetPhase: 3, decision: 1, comment: "..." }
# 4. Chuyển qua các phase (với admin)
4 DangDamPhan → 5 DangInKy → 6 DangKiemTraCCM → 7 DangTrinhKy
# 5. BOD ký → gen mã HĐ
8 DangDongDau
# contract.MaHopDong = "FLOCK 01/HĐGK/SOL&PVL/01"
# 6. HRA đóng dấu + phát hành
9 DaPhatHanh
```
## Common pitfalls (xem gotchas.md)
- **Admin check mọi phase** đôi khi không catch role-scope bug. Test với user không phải Admin.
- ** gen 2 lần** (sau reject rồi approve) generator check `if (MaHopDong is null)` trước khi gen.
- **Race condition gen song song** dùng `IsolationLevel.Serializable`, không skip.
- **SLA Deadline không reset khi reject** `TransitionAsync` luôn reset theo target phase, kể cả reject.
- **Comment phase sai** `AddCommentCommand` luôn lấy phase hiện tại tại thời điểm comment.
- **FE hiển thị next phase button** map `NEXT_PHASES` FE phải match BE `Transitions`. Nếu BE đổi, FE quên update user click 403.
## Phase 3 iteration 2 (còn thiếu)
- [ ] `SlaExpiryJob` BackgroundService auto-approve khi quá hạn (xem `docs/flows/sla-expiry-flow.md`)
- [ ] Warning notification khi còn 20% SLA
- [ ] Email notification (MailKit) khi chuyển phase
- [ ] In-app notification badge SignalR push
- [ ] Upload attachment endpoint + FE (multipart)
- [ ] RowVersion optimistic concurrency (2 user cùng duyệt)
- [ ] ContractClause appendix attach khi export trọn gói
- [ ] Audit log riêng (`AuditLogs` table) ngoài `ContractApprovals`