Files
solution-erp/.claude/skills/contract-workflow/SKILL.md
pqhuy1987 7ca6c914fa
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m55s
[CLAUDE] Docs: chốt session 2 — PE skeleton + G-084 + skill audit
User feedback: "phần Duyệt NCC chưa xong đâu đấy nhé, còn chỉnh nhiều"
→ mark PE module skeleton (not feature-complete), liệt kê chi tiết chức
năng/UX/edge-case còn missing cho session tiếp.

Update 7 file:
 - STATUS.md — phase = "PE skeleton + refinement WIP", In Progress liệt
   kê 4 nhóm: A Chức năng MISSING (9 item), B UX/Polish (6 item),
   C Edge case (4 item), D Deploy/Ops (1 item). +G-084 row Recently Done.
 - HANDOFF.md — TL;DR "PE skeleton, còn chỉnh nhiều" + Priority 0 section
   cho session tiếp (9 task PE refinement) + cảnh báo runner + G-084.
 - migration-todos.md — Phase 7 checklist (A/B/C/D nhóm) trước Phase 8
   post-launch. Pending migrations: PaymentTermFields + DepartmentOpinions
   + CodeSequences.
 - architecture.md — Section 9 PurchaseEvaluation module (ERD + workflow
   A/B + kế thừa HĐ flow).
 - CLAUDE.md (root) — 5 file đọc đầu (thêm HANDOFF), Modules table, 12
   migration 46 bảng, +PurchaseEvaluation commit scope.
 - .claude/skills/ — 4 skill cross-ref Phase 6:
   * README: trạng thái updated với Phase 6 note
   * contract-workflow: note PE workflow tách table riêng
   * permission-matrix: +Pe_*/PeWf_* menu keys + TODO grant non-admin
   * ef-core-migration: 12 migration history + Phase 7 pending
 - docs/changelog/sessions/2026-04-23-2359-chot-session-pe-skeleton.md —
   session log full commits + MD files updated + session tiếp priorities
   + notes (PE là skeleton, runner check, G-084 rule, MaPhieu format).
2026-04-23 17:46:41 +07:00

306 lines
15 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, versioned workflow admin-configurable per ContractType. Dùng khi debug chuyển phase, 403 forbidden, code sai format, bypass Chủ đầu tư, workflow policy resolution.
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ư"
- "versioned workflow"
- "quy trình mới HĐ cũ giữ cũ"
- "WorkflowDefinition pin"
- "admin workflow designer"
---
# Contract Workflow Skill
> **Status:** Tier 3 FEATURE-COMPLETE — State transitions + code gen + SLA job + attachment + realtime notify + versioned workflow admin-configurable. Còn thiếu: email outbox (SMTP), warning 20% SLA.
>
> **Phase 6 cross-ref (2026-04-23):** Module `PurchaseEvaluation` (tiền-HĐ) có
> **workflow SEPARATE** — `PurchaseEvaluationPolicy` + registry +
> `FromDefinition` mirror pattern này nhưng tách table riêng (3 bảng
> `PurchaseEvaluationWorkflow*`) vì Phase enum khác. Xem:
> - `Domain/PurchaseEvaluations/PurchaseEvaluationPolicy.cs`
> - `Infrastructure/Services/PurchaseEvaluationWorkflowService.cs`
> - Kế thừa HĐ từ phiếu `DaDuyet` qua `CreateContractFromEvaluationCommand` — pin `Contract.WorkflowDefinitionId` theo ContractType user chọn.
## Domain entities (implemented)
```
Contract ─────< ContractApproval (lịch sử mỗi transition)
─────< ContractComment (thread góp ý)
─────< ContractAttachment (scan signed/sealed)
─────> WorkflowDefinition (PINNED at create-time, nullable FK)
ContractCodeSequence (Prefix PK, LastSeq) — gen mã HĐ atomic
WorkflowDefinition ─────< WorkflowStep ─────< WorkflowStepApprover
(Code + Version + IsActive + ContractType) (Kind=Role|User + AssignmentValue)
WorkflowTypeAssignment (admin override legacy, fall back khi Contract.WorkflowDefinitionId == null)
```
## 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)
Policy variants (hardcoded fallback, dùng khi không có WorkflowDefinition pin):
- Standard (8 phase full CCM) — Thầu phụ, Giao khoán, NCC
- SkipCcm (7 phase bỏ CCM) — Dịch vụ, Mua bán, Nguyên tắc NCC, Nguyên tắc DV
```
## Versioned workflow (Tier 3) — policy resolution runtime
```csharp
// ContractWorkflowService.LoadPolicyAsync(contractId):
// 1. Load contract
var c = await db.Contracts.FindAsync(contractId);
// 2. If pinned — nạp từ DB
if (c.WorkflowDefinitionId != null) {
var def = await db.WorkflowDefinitions
.Include(d => d.Steps).ThenInclude(s => s.Approvers)
.FirstAsync(d => d.Id == c.WorkflowDefinitionId);
return WorkflowPolicyRegistry.FromDefinition(def);
// → xây policy runtime từ Steps.Approvers
// → Role-kind → allowedRoles
// → User-kind (data model ready, iter sau enable guard)
}
// 3. Nếu không pin — check admin override WorkflowTypeAssignments
var assignment = await db.WorkflowTypeAssignments.FirstOrDefaultAsync(a => a.ContractType == c.Type);
if (assignment != null) return WorkflowPolicyRegistry.ByName(assignment.PolicyName);
// 4. Fallback hardcoded
return WorkflowPolicyRegistry.For(c.Type); // Standard or SkipCcm
```
## Admin designer flow (Tạo version mới)
```
Admin → /system/workflows → grid 7 type card
→ click "HĐ Mua bán" → /system/workflows/MuaBan
→ thấy QT-MB-v01 active + history
→ click "Tạo phiên bản mới" (có thể Clone từ v01)
→ Designer modal:
Code: QT-MB (auto-fill)
Version: v02 (auto-compute max+1)
Name + Description
Steps (repeatable, reorderable):
Step 1: Phase=2 (DangSoanThao) SLA=7d
Approvers: +Role Drafter, +Role DeptManager
Step 2: Phase=3 (DangGopY) SLA=7d
Approvers: +Role ProjectManager, +User {userId alice}
...
→ Save → POST /api/workflows
BE atomically:
UPDATE WorkflowDefinitions SET IsActive=0 WHERE ContractType=5 AND IsActive=1;
INSERT WorkflowDefinitions (Id, Code='QT-MB', Version=2, IsActive=1, ...);
INSERT WorkflowSteps / WorkflowStepApprovers batch;
→ trở về /system/workflows/MuaBan → v02 active badge, v01 archived "N HĐ còn chạy"
→ HĐ cũ pin v01 KHÔNG BỊ ẢNH HƯỞNG (Contract.WorkflowDefinitionId = v01.Id)
→ HĐ mới tạo sau đó pick active → pin v02
```
## SLA mặc định (khi pinned def không có SlaDays → fallback)
| Phase | SLA fallback |
|---|---|
| DangSoanThao | 7d |
| DangGopY | 7d |
| DangDamPhan | 7d |
| DangInKy | 1d |
| DangKiemTraCCM | 3d |
| DangTrinhKy | 1d |
| DangDongDau | none |
| DaPhatHanh | none |
Nếu pinned WorkflowStep có `SlaDays > 0` → ưu tiên value của step đó.
## Role × Phase guard matrix (hardcoded Standard)
Xem `WorkflowPolicies.Standard.Transitions`. 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.
**Versioned override:** Nếu HĐ có `WorkflowDefinitionId` pin → allowed roles sẽ lấy từ `WorkflowStep.Approvers` (Role-kind) thay vì hardcoded. Admin có thể config 2 role bất kỳ cho step 3, guard theo đó.
## 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 UPSERT. 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 (Tier 3 updated)
**Backend Domain:**
- `Domain/Contracts/Contract.cs` — aggregate root (+ `WorkflowDefinitionId?`)
- `Domain/Contracts/ContractApproval.cs` — history
- `Domain/Contracts/ContractComment.cs` — thread
- `Domain/Contracts/ContractAttachment.cs` — files
- `Domain/Contracts/ContractCodeSequence.cs` — seq table
- `Domain/Contracts/WorkflowPolicy.cs` — record + `WorkflowPolicies.Standard/SkipCcm` + `WorkflowPolicyRegistry.{For, FromDefinition, ByName}`
- **`Domain/Contracts/WorkflowDefinition.cs`** — versioned policy header
- **`Domain/Contracts/WorkflowStep.cs`** — step trong definition
- **`Domain/Contracts/WorkflowStepApprover.cs`** — Role/User approver (+ `ApproverKind` enum)
- `Domain/Contracts/WorkflowTypeAssignment.cs` — legacy admin override
**Backend Application:**
- `Application/Contracts/Services/IContractWorkflowService.cs` + `IContractCodeGenerator.cs`
- `Application/Contracts/ContractFeatures.cs` — CQRS (Create pin WorkflowDefId, Update draft, Transition, AddComment, List, Inbox, GetDetail, Delete)
- `Application/Contracts/ContractAttachmentFeatures.cs` — Upload/Download/Delete CQRS
- **`Application/Contracts/WorkflowAdminFeatures.cs`** — `GetWorkflowAdminOverviewQuery` + `CreateWorkflowDefinitionCommand`
**Backend Infrastructure:**
- `Infrastructure/Services/ContractWorkflowService.cs` — resolve policy (pinned → override → fallback), state + role guard
- `Infrastructure/Services/ContractCodeGenerator.cs` — transactional gen
- `Infrastructure/Services/NotificationService.cs` — write to DbContext (caller atomicity)
- `Infrastructure/Persistence/Interceptors/NotificationPushInterceptor.cs` — auto-push via SignalR
**Backend Api:**
- `Api/Controllers/ContractsController.cs` — REST endpoints
- **`Api/Controllers/WorkflowsController.cs`** — admin overview + create new version
- `Api/Hubs/NotificationHub.cs` + `Api/Realtime/SignalRNotifier.cs`
**Frontend Admin:**
- `fe-admin/src/pages/contracts/{ContractsListPage, ContractCreatePage, ContractDetailPage}.tsx`
- `fe-admin/src/pages/system/WorkflowsPage.tsx` — URL-driven landing + per-type
- `fe-admin/src/components/workflow/{WorkflowDesigner, DefinitionCard}.tsx`
- `fe-admin/src/components/{PhaseBadge, WorkflowSummaryCard, ContractAttachmentsSection, DynamicForm, SlaTimer}.tsx`
**Frontend User:**
- `fe-user/src/pages/InboxPage.tsx` — filter `?type=X`
- `fe-user/src/pages/contracts/{ContractCreatePage, ContractDetailPage, MyContractsPage}.tsx`
- `fe-user/src/components/{Layout, ContractAttachmentsSection, SlaTimer, NotificationBell}.tsx`
## API endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | `/api/contracts` | List với filter phase/supplier/project/type + paging + pendingMe |
| GET | `/api/contracts/inbox` | HĐ chờ role của user xử lý |
| GET | `/api/contracts/{id}` | Detail + approvals + comments + attachments + pinned workflow |
| POST | `/api/contracts` | Tạo draft — pin `WorkflowDefinitionId = active version for type` |
| 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 |
| POST | `/api/contracts/{id}/attachments` | Upload file (multipart, 20MB, MIME whitelist) |
| GET | `/api/contracts/{id}/attachments/{aid}` | Download stream |
| DELETE | `/api/contracts/{id}/attachments/{aid}` | Delete (+ cleanup file) |
| DELETE | `/api/contracts/{id}` | Soft delete (chỉ < DangInKy) |
| **GET** | **`/api/workflows`** | Admin: overview per ContractType (active + history + "N còn chạy") |
| **GET** | **`/api/workflows/{type}`** | Per-type definitions + steps + approvers |
| **POST** | **`/api/workflows`** | Create new version (auto-increment + deactivate old) |
## Guard Rules đã implement
- **State adjacency:** chỉ cho chuyển giữa các (from, to) đã khai báo trong `policy.Transitions` (pinned def hoặc hardcoded)
- **Role check:** role của actor phải allowed roles của transition đó (từ Role-kind approvers hoặc hardcoded)
- **Admin bypass:** role `Admin` pass mọi check
- **System bypass:** `actorUserId == null` + `Decision = AutoApprove` cho phép (dành cho SLA job)
- **Bypass CCM:** `Contract.BypassProcurementAndCCM=true` cho phép `DangInKy → DangTrinhKy` (skip CCM)
- **Self-delete:** không cho xóa đã qua `DangInKy`
- **Versioned pin:** `Contract.WorkflowDefinitionId` pinned at create không update sau đó. FK restrict admin không xóa được def nếu còn tham chiếu
## Workflow tạo HĐ end-to-end (testable, Tier 3)
```bash
# 1. Setup master data (auto-seeded: 5 supplier + 3 project + 9 dept)
# 2. Admin tạo version mới cho HĐ Mua bán
POST /api/workflows
{
"code": "QT-MB",
"name": "Quy trình Mua bán v02",
"contractType": 5,
"steps": [
{ "order": 1, "phase": 2, "name": "Soạn thảo", "slaDays": 7,
"approvers": [{ "kind": 1, "assignmentValue": "Drafter" }, { "kind": 1, "assignmentValue": "DeptManager" }] },
{ "order": 2, "phase": 3, "name": "Góp ý", "slaDays": 7,
"approvers": [{ "kind": 1, "assignmentValue": "ProjectManager" }, { "kind": 2, "assignmentValue": "{userId}" }] },
...
]
}
# → Version=02, IsActive=1, v01 deactivated
# 3. User tạo HĐ Mua bán → pin WorkflowDefinitionId = v02.Id
POST /api/contracts { type: 5, supplierId, projectId, giaTri: ..., tenHopDong: "..." }
# → Phase=DangSoanThao, SlaDeadline=+7d, WorkflowDefinitionId=v02
# 4. Transition — guard load từ v02.Steps.Approvers
POST /api/contracts/{id}/transitions { targetPhase: 3, decision: 1, comment: "..." }
# 5. Chuyển qua các phase
4 DangDamPhan → 5 DangInKy → (skip CCM nếu SkipCcm policy)7 DangTrinhKy
# 6. BOD ký → gen mã HĐ
8 DangDongDau
# contract.MaHopDong = "FLOCK 01/MB/SOL&PVL/01"
# 7. 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** RESOLVED Tier 3. FE dùng `contract.workflow.nextPhases` từ BE (pinned policy single source of truth).
- **WorkflowDefinition cascade delete** NÊN restrict FK. Nếu cascade sẽ xóa Contract data loss. Đã fix trong migration.
- **User-kind approver không enforce runtime** designer cho chọn nhưng guard v1 chỉ check Role. Iter 2 cần wire `step.Approvers.Where(a => a.Kind == User)` vào check.
## Tier 4+ (còn thiếu / future)
- [ ] Warning notification 20% SLA (`SlaWarningSent` flag đã )
- [ ] User-kind approver runtime guard (data model ready)
- [ ] Email notification (MailKit) khi chuyển phase BLOCKED SMTP
- [ ] RowVersion optimistic concurrency (2 user cùng duyệt 409)
- [ ] ContractClause appendix attach khi export trọn gói
- [ ] Audit log riêng (`AuditLogs` table) ngoài `ContractApprovals`
- [ ] MediatR `AuditBehavior` log mọi command
- [ ] E-signature integration (VNPT/FPT CA)