Files
solution-erp/.claude/skills/contract-workflow/SKILL.md
pqhuy1987 7c83ac8f17 [CLAUDE] Docs+Skill: chốt Session 14 wrap-up — PE 3-button workflow + Task 2 defer
Session 14 (2026-05-07) docs/skill update:

STATUS.md:
- Last updated + Phase summary count (95→96 test, 20 mig, 57 bảng, 41 gotcha)
- Recently Done row Session 14 chi tiết (3-button + Task 2 in-progress + DesignTime/Runtime DB gotcha)

HANDOFF.md:
- TL;DR Session 14 prepend với 1 commit + Task 2 defer
- 5 cảnh báo Session 15+: TraLai phase orphan / Task 2 sample seed / DesignTime
  vs Runtime DB / Budget N-stage defer / schema-diagram §17-19 defer

migration-todos.md: Phase 9 + Session 14 block 4 sub-task done + 2 defer task

Session log NEW `2026-05-07-2500-3-button-workflow.md`:
- Bối cảnh + spec 3-button (Duyệt/Trả lại/Từ chối) + implementation chi tiết
  (Domain policy expand + Service tách reject + FE button + dialog warning)
- Tests update (95→96 với +1 NEW Reject_To_TuChoi_Locks_Permanently)
- Task 2 in-progress: DesignTime vs Runtime DB gotcha + API exit 255 sớm
- Plan organization sau S14

Skill ef-core-migration:
- description + heading: 17→20 migration
- Bảng migration history thêm Mig 18-19 (PE) + Mig 20 (Contract)
- Section MỚI "N-stage workflow pattern (Mig 18-20)" — architecture decision
  với filtered unique trick + per-module migration packaging guideline
- Phase 8 update: 83→96 test breakdown

Skill contract-workflow:
- Section MỚI "Phase 9+ done (Mig 18-20 — Session 12/13/14)":
  * N-stage workflow PE + Contract (entity + filtered unique split + service
    refactor + tests + FE Designer + UsersPage cấp + API)
  * PE 3-button approval (Duyệt/Trả lại/Từ chối) Session 14
  * Defer: Budget N-stage / Phase TraLai=98 orphan

CLAUDE.md root: 16→20 migration + 55→57 bảng + 83→96 test
docs/rules.md §7: Phase 9 active 83→96 test

Verify: dotnet test 96 pass + npm build (skip — pure docs/skill update).

🎉 Session 14 wrap-up complete. Pushed 1 task (3-button) + Task 2 defer.
Cumulative since session start (13h17): 13 commit (1 button removal +
6 PE N-stage Chunk A-F + 5 Contract N-stage Chunk A,B,C,D,F + 1 3-button).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:40:00 +07:00

19 KiB
Raw Blame History

name, description, when-to-use
name description when-to-use
contract-workflow 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.
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 SEPARATEPurchaseEvaluationPolicy + 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.

Phase 9 cross-ref (Migration 16 — Session 8/9): 3 mở rộng cross-cut 3 module (Contract + PurchaseEvaluation + Budget):

  • 2-stage dept approval (đóng bug anh Kiệt FDC): NV Review → TPB Confirm. *DepartmentApprovals table UNIQUE (TargetId, Phase, Dept, Stage). Service inject UserManager → check role DeptManager / CanBypassReview → upsert row + block transition cho đến khi Stage=Confirm. 6 test PE 2-stagetests/.../Services/PeTwoStageApprovalTests.cs (IdentityFixture reusable).
  • Smart reject + Resume jump-back: Reject → set RejectedFromPhase snapshot
    • force targetPhase=DangSoanThao. Resume (Drafter trình lại từ DangSoanThao với RejectedFromPhase != null) → jump straight tới phase đã reject, bypass policy guard. Áp dụng 3 module.
  • Lock edit guards 17 handler: Phase != DangSoanThao → throw 409 ConflictException. KHÔNG lock Comment + Attachment + Opinion (workflow design intent).

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

// 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.csGetWorkflowAdminOverviewQuery + 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 HĐ 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 HĐ đã qua DangInKy
  • Versioned pin: Contract.WorkflowDefinitionId pinned at create → không update sau đó. FK restrict → admin không xóa được def nếu HĐ còn tham chiếu

Workflow tạo HĐ end-to-end (testable, Tier 3)

# 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 phase4 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ành9 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.
  • Mã HĐ gen 2 lần (sau reject rồi approve) → generator check if (MaHopDong is null) trước khi gen.
  • Race condition gen mã song song → dùng IsolationLevel.Serializable, không skip.
  • SLA Deadline không reset khi rejectTransitionAsync luôn reset theo target phase, kể cả reject.
  • Comment ở phase saiAddCommentCommand 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 cũ → 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.

Phase 9 done (Mig 16 — Session 8/9)

  • 2-stage dept approval xuyên 3 module — NV Review BLOCK / TPB Confirm ALLOW
  • Smart reject + Resume jump-back — RejectedFromPhase snapshot + bypass policy
  • Lock edit guards 17 handler khi Phase != DangSoanThao
  • CanBypassReview toggle per User — admin UI + audit IsBypassed=true

Phase 9+ done (Mig 18-20 — Session 12/13/14)

N-stage workflow (PE Mig 18-19 + Contract Mig 20)

  • N-stage approval Phòng × PositionLevel (NV/PP/TP) cấu hình động per WorkflowStep cha. Mỗi inner step = 1 cấp duyệt (Order asc, sequential).
  • PositionLevel enum Domain/Identity (NV=1, PP=2, TP=3) + User.PositionLevel int?
  • InnerStep entity + ALTER *DepartmentApproval.InnerStepId Guid? per module
  • Filtered unique split (Mig 19 cho PE / gộp Mig 20 cho Contract): legacy WHERE InnerStepId IS NULL (Stage Review/Confirm) + N-stage WHERE InnerStepId IS NOT NULL (per inner step)
  • Service refactor TransitionAsync — load InnerSteps eager + reject branch clear N-stage rows + dept approval block split: hasInnerSteps→N-stage logic / else→legacy 2-stage. Match firstPending Order asc + (exact level OR canBypass + level≥). Bypass batch upsert NV+PP+TP cùng dept ≤ actor.
  • 6 test PE N-stage + 6 test Contract N-stage (PeNStageApprovalTests, ContractNStageApprovalTests). Pattern reusable.
  • FE Designer (PeWorkflowsPage + WorkflowsPage) sub-section "Cấp duyệt nhỏ trong phòng" drag-list { Phòng × Cấp + required }
  • UsersPage cột Cấp + cycle button null→1→2→3→null
  • API PATCH /users/{id}/position-level

Backward compat 100%: workflow no InnerSteps → service fallback legacy 2-stage Mig 16. Data legacy InnerStepId=null vẫn enforce unique cũ qua filtered index.

PE 3-button approval (Session 14 — 0d77698)

UI distinguishment 3 hành động cho approver:

  • Duyệt = forward (decision=Approve)
  • Trả lại = về DangSoanThao + Drafter sửa (decision=Reject + target=DangSoanThao → smart reject pattern Mig 16 + clear N-stage rows + jump-back)
  • Từ chối = Phase=TuChoi (decision=Reject + target=TuChoi → phiếu khoá vĩnh viễn 17 handler Mig 16 lock edit, Drafter phải tạo phiếu mới)

Domain policy expand: NccOnly + NccWithPlan + FromDefinition thêm (X → TuChoi) transition cho mọi phase trung gian (trước chỉ DangSoanThao→TuChoi).

Service Reject branch tách 2 case:

  • target=TuChoi → giữ nguyên (KHÔNG override + KHÔNG set RejectedFromPhase + KHÔNG clear N-stage)
  • target khác (DangSoanThao) → smart reject (force DangSoanThao + RejectedFromPhase + clear N-stage)

Defer

  • Budget N-stage — cần migration AddBudgetVersionedWorkflow trước (Budget hardcoded BudgetPolicy.Default, chưa có versioned WorkflowDefinition).
  • Phase TraLai = 98 (Domain enum từ S11+++++++) — orphan, KHÔNG wire (user Session 14 chốt không cần phase trung gian).

Tier 4+ (còn thiếu / future)

  • Warning notification 20% SLA (SlaWarningSent flag đã có)
  • 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 HĐ 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)