Files
solution-erp/.claude/skills/contract-workflow/SKILL.md
pqhuy1987 fbca83264c
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m33s
[CLAUDE] Docs: chốt session Tier 3 feature-complete + versioned workflow
- Session log 2026-04-22-0300 (A→K): attachment, SignalR, form builder,
  PDF, dynamic + versioned workflow, nested menu, 3-panel permissions,
  seed master, brand identity, content polish, Gitea fix
- STATUS: Tier 3 feature-complete snapshot + cumulative stats (24 tables,
  ~50 endpoints, 8 migrations); next-up = UAT + Email SMTP (blocked) +
  rotate creds + SQL backup schedule
- HANDOFF: rewrite brief cho session mới — phase 5 prod done, Tier 3
  đóng gói, quick sanity-check 2 app, versioned workflow quick ref,
  file active hiện trạng, git state
- migration-todos: tick Tier 3 items (attachment/realtime/form builder/
  PDF/dynamic+versioned workflow/nested menu) + thêm iter-3 versioned
  workflow section + post-launch list
- schema-diagram: +5 table (Notifications, WorkflowTypeAssignments,
  WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers); indexes
  mới, cardinality FK restrict cho pinned policy, truy vấn tiêu biểu
- workflow-contract: +section 7bis resolution order, 7ter admin
  designer flow, updated data model + code pointers Tier 3
- PROJECT-MAP: module map post-Tier-3 (3 box mới Notification/
  Attachment/Branding + Infra/DevOps box), API namespace đầy đủ,
  architectural wins 5 điểm
- contract-workflow skill: versioned workflow section, policy
  resolution code snippet, admin designer flow, code pointers Tier 3,
  tier 4+ backlog
- gotchas +7 bẫy mới (#26-32): SignalR WebSocket headers, interceptor
  2-phase pattern, LibreOffice mirror 404, PS 5.1 UTF-16 GITHUB_PATH,
  PS 5.1 diacritics parse, Dialog size TS, NavLink end query-params

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 10:25:02 +07:00

14 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), User-kind approver runtime, warning 20% SLA.

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.

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)