Files
solution-erp/docs/flows/contract-approval-flow.md
pqhuy1987 49a5f57a50 [CLAUDE] Docs: database-guide + 6 flow diagrams
docs/database/database-guide.md:
- Conventions (naming, data types, audit fields, soft delete)
- Schema hien tai (Identity tables sau migration Init) + seed 12 role + admin
- Schema planned: Phase 1 dot 2 (Supplier/Project/Department + Permission Matrix)
- Schema planned: Phase 3 (Contract + Approval + Comment + Attachment + Template + Clause + CodeSequence)
- Mermaid ERD cho tung phase
- Migration workflow (create/apply/revert)
- Index strategy + unique indexes
- Backup/restore SQL
- Common pitfalls + SQL cheatsheet

docs/flows/ — 6 flow documentation:
- README.md: index
- auth-flow.md: login/refresh/me/logout (IMPLEMENTED, sequence + edge cases + security checklist)
- permission-flow.md: Phase 1 dot 2 - Role x MenuKey x CRUD resolution + FE guard + BE policy
- contract-creation-flow.md: Phase 2 - Drafter flow chon template -> fill -> preview -> save draft
- contract-approval-flow.md: Phase 3 - state machine 9 phase chi tiet + reject flow + timeline UI
- form-render-flow.md: Phase 2 - OpenXml + ClosedXML + LibreOffice PDF convert
- sla-expiry-flow.md: Phase 3 - BackgroundService auto-approve qua SLA + warning notify

Update references:
- CLAUDE.md (root): them 2 row Tai lieu quan trong
- docs/CLAUDE.md: update project layout voi flows/ + database/
- docs/STATUS.md: log docs addition
- docs/changelog/migration-todos.md: tick Phase 0 docs items

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

12 KiB

Contract Approval Flow — State Machine 9 Phase

Status: 📝 Planned (Phase 3) Spec gốc: ../workflow-contract.md (state machine + role matrix) Entry: User click "Chuyển phase tiếp" ở /contracts/{id} detail page

1. Tổng quan state machine

stateDiagram-v2
    [*] --> DangChon: Tạo (mặc định)
    DangChon --> DangSoanThao: PB chọn NCC
    DangSoanThao --> DangGopY: Drafter submit
    DangGopY --> DangDamPhan: Nhận xong comment
    DangDamPhan --> DangInKy: Thỏa thuận xong
    DangInKy --> DangKiemTraCCM: Đã ký nháy, chuyển CCM
    DangKiemTraCCM --> DangTrinhKy: CCM duyệt
    DangTrinhKy --> DangDongDau: BOD ký (GEN mã HĐ!)
    DangDongDau --> DaPhatHanh: HRA đóng dấu
    DaPhatHanh --> [*]

    DangSoanThao --> TuChoi: Drafter hủy
    DangGopY --> DangSoanThao: Revise
    DangKiemTraCCM --> DangSoanThao: CCM reject
    DangTrinhKy --> DangSoanThao: BOD reject
    TuChoi --> [*]

SLA mỗi phase: xem ../workflow-contract.md §4. Tổng ~19 ngày.

2. Transition API — POST /api/contracts/{id}/transitions

Request

{
  "targetPhase": "DangGopY",
  "decision": "Approve",
  "comment": "Đã hoàn thiện draft, chuyển góp ý."
}

Response

{
  "contractId": "c5d6-...",
  "oldPhase": "DangSoanThao",
  "newPhase": "DangGopY",
  "slaDeadline": "2026-04-28T10:00:00Z",
  "actor": { "id": "u1-...", "fullName": "Nguyen Van A" },
  "notifiedUsers": ["u2-...", "u3-...", "u4-..."]
}

3. Handler flow

sequenceDiagram
    actor U as User<br/>(role cụ thể)
    participant FE as fe-user
    participant API as ContractsController<br/>.Transition
    participant M as TransitionContractCommandHandler
    participant WF as IContractWorkflowService
    participant CG as IContractCodeGenerator
    participant NS as INotificationService
    participant DB

    U->>FE: Click "Chuyển phase" + nhập comment
    FE->>API: POST /api/contracts/{id}/transitions<br/>{targetPhase, decision, comment}
    API->>M: Send(TransitionContractCommand)

    M->>DB: SELECT Contract WHERE Id = ?
    alt Not found
        M-->>API: throw NotFoundException
    end

    M->>WF: ValidateTransition(contract, targetPhase, userRole)
    WF->>WF: Check state rule:<br/>1. currentPhase allows transition to targetPhase?<br/>2. user role đủ quyền ở phase này?<br/>3. bypass rule với Chủ đầu tư?
    alt Invalid transition
        WF-->>M: throw ForbiddenException("không được phép")
    end

    alt targetPhase == DangDongDau (BOD ký)
        M->>CG: GenerateAsync(project, type, supplier)
        CG->>DB: BEGIN TRAN SERIALIZABLE
        CG->>DB: SELECT LastSeq WITH UPDLOCK
        CG->>DB: UPDATE LastSeq + 1
        CG->>DB: COMMIT
        CG-->>M: "FLOCK 01/HĐGK/SOL&PVL/03"
        M->>DB: UPDATE Contract SET MaHopDong = ?
    end

    M->>DB: INSERT ContractApproval<br/>(ContractId, Phase=currentPhase,<br/>ApproverUserId, Decision, Comment)
    M->>DB: UPDATE Contract SET Phase = targetPhase,<br/>SlaDeadline = UtcNow + PhaseSla

    M->>NS: NotifyPhaseChangeAsync(contract, oldPhase, newPhase)
    NS->>NS: Query users trong role eligible<br/>for newPhase
    NS->>NS: Send email (MailKit) + in-app notify
    Note over NS: (Phase 3 Iteration 2) SignalR<br/>push notification real-time

    M-->>API: TransitionResultDto
    API-->>FE: 200

    FE->>FE: Invalidate TanStack query<br/>['contracts', id] + toast success
    FE->>FE: Refresh timeline UI

4. Guard rules (IContractWorkflowService)

public class ContractWorkflowService : IContractWorkflowService
{
    // Adjacency + role matrix — xem workflow-contract.md
    private static readonly Dictionary<(ContractPhase From, ContractPhase To), string[]> _transitions = new()
    {
        [(ContractPhase.DangChon, ContractPhase.DangSoanThao)] = [AppRoles.Drafter, AppRoles.DeptManager],
        [(ContractPhase.DangSoanThao, ContractPhase.DangGopY)] = [AppRoles.Drafter],
        [(ContractPhase.DangSoanThao, ContractPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.Admin],
        [(ContractPhase.DangGopY, ContractPhase.DangDamPhan)] = [AppRoles.Drafter],
        [(ContractPhase.DangGopY, ContractPhase.DangSoanThao)] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl],
        [(ContractPhase.DangDamPhan, ContractPhase.DangInKy)] = [AppRoles.Drafter, AppRoles.DeptManager],
        [(ContractPhase.DangInKy, ContractPhase.DangKiemTraCCM)] = [AppRoles.Drafter],
        [(ContractPhase.DangKiemTraCCM, ContractPhase.DangTrinhKy)] = [AppRoles.CostControl],
        [(ContractPhase.DangKiemTraCCM, ContractPhase.DangSoanThao)] = [AppRoles.CostControl],
        [(ContractPhase.DangTrinhKy, ContractPhase.DangDongDau)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
        [(ContractPhase.DangTrinhKy, ContractPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
        [(ContractPhase.DangDongDau, ContractPhase.DaPhatHanh)] = [AppRoles.HrAdmin],
    };

    // Bypass CĐT: HopDongChuDauTu → có thể nhảy từ DangInKy thẳng tới DangTrinhKy (bỏ CCM)
    public void ValidateTransition(Contract contract, ContractPhase target, IReadOnlyList<string> userRoles)
    {
        // 1. Check adjacency
        var key = (contract.Phase, target);
        if (!_transitions.TryGetValue(key, out var allowedRoles))
            throw new ForbiddenException($"Không thể chuyển {contract.Phase} → {target}");

        // 2. Check role
        if (!userRoles.Any(r => allowedRoles.Contains(r)) && !userRoles.Contains(AppRoles.Admin))
            throw new ForbiddenException("Role không đủ quyền duyệt phase này");

        // 3. Bypass rule
        if (contract.BypassProcurementAndCCM && contract.Phase == ContractPhase.DangInKy && target == ContractPhase.DangTrinhKy)
            return; // OK skip CCM

        // 4. Business rule: không cho skip phase
        // (adjacency table đã enforce — chỉ transition nào khai báo mới pass)
    }
}

5. Reject flow (ví dụ CCM reject)

sequenceDiagram
    actor CCM
    participant FE
    participant API
    participant M as TransitionHandler
    participant NS
    participant DB

    CCM->>FE: Click "Yêu cầu sửa" + nhập comment
    FE->>API: POST /contracts/{id}/transitions<br/>{targetPhase: "DangSoanThao", decision: "Reject", comment: "Điều khoản 5 cần rõ hơn"}
    API->>M: Send

    M->>DB: INSERT ContractApproval<br/>(Phase=DangKiemTraCCM, Decision=Reject, Comment)
    M->>DB: UPDATE Contract.Phase = DangSoanThao<br/>SlaDeadline = UtcNow + 7d

    Note over M: KHÔNG xóa lịch sử approval các phase cũ<br/>— giữ history

    M->>NS: NotifyRejection(contract, drafter)
    NS->>NS: Email drafter + in-app badge
    M-->>API: 200

Lưu ý: Drafter nhận thông báo + comment thread tự động có entry mới. Họ fix, rồi submit lại → re-trigger DangGopY (quay lại loop comment → duyệt).

6. Comment thread

Endpoint riêng (không phải transition):

POST /api/contracts/{id}/comments
Body: { content: "NCC này từng trễ giao hàng HĐ trước, cân nhắc thêm điều khoản phạt" }

Ghi nhận:

  • ContractComments.Phase = contract.Phase hiện tại
  • Hiển thị cùng timeline với approval history

7. Timeline UI (fe-user /contracts/{id})

┌────────────────────────────────────────────────────────┐
│  HĐ: FLOCK 01/HĐGK/SOL&PVL/03                         │
│  NCC: Công ty PVL · Dự án: FLOCK 01                   │
│  Giá trị: 150,000,000 VND                              │
│  Phase hiện tại: 🟡 Đang kiểm tra CCM                 │
│  SLA: còn 2 ngày 4 giờ                                 │
├────────────────────────────────────────────────────────┤
│  Timeline                                              │
│                                                        │
│  ● DangSoanThao  2026-04-15 10:00  Nguyen Van A        │
│  │   Drafter tạo draft                                 │
│  │                                                      │
│  ● DangGopY      2026-04-16 08:30  Tran Thi B (PM)    │
│  │   "Scope work cần chi tiết hơn mục 3"               │
│  ●                  Le Van C (CCM)                     │
│  │   "OK giá"                                          │
│  │                                                      │
│  ● DangDamPhan   2026-04-17 14:20  Nguyen Van A        │
│  ● DangInKy      2026-04-18 09:15  NCC đã ký           │
│                                                        │
│  🟡 DangKiemTraCCM  (đang ở phase này)                │
│     Chờ CCM kiểm tra                                   │
│                                                        │
│  ⚪ DangTrinhKy                                        │
│  ⚪ DangDongDau                                        │
│  ⚪ DaPhatHanh                                         │
├────────────────────────────────────────────────────────┤
│  Action (tùy role):                                    │
│  [✅ Duyệt → TrinhKy]  [❌ Yêu cầu sửa]               │
│                                                        │
│  Comment:                                              │
│  [Textarea]                                            │
│  [Gửi comment]                                         │
└────────────────────────────────────────────────────────┘

8. Notifications

Event Recipient Channel
DangSoanThaoDangGopY Tất cả role góp ý (PD, PM, PRO, CCM, FIN, ACT) Email + in-app
Chuyển DangKiemTraCCM Mọi user role CostControl Email + in-app
Chuyển DangTrinhKy Mọi user role Director + AuthorizedSigner Email (high priority) + in-app
SLA còn 20% thời gian Role đang giữ phase In-app warning
SLA hết → auto-approve Drafter + role giữ phase Email + in-app
Reject → quay về DangSoanThao Drafter (người tạo) Email + in-app
Mã HĐ được gen khi BOD ký Drafter In-app

9. Audit trail

Mọi transition tạo 1 row trong ContractApprovals. Ngoài ra, nếu cần log granular hơn, Phase 4 thêm AuditLogs table ghi mọi command (không chỉ workflow):

public class AuditBehavior<TReq, TRes> : IPipelineBehavior<TReq, TRes>
{
    public async Task<TRes> Handle(TReq request, RequestHandlerDelegate<TRes> next, CancellationToken ct)
    {
        var response = await next();
        // Log: command name, user, payload, timestamp
        _db.AuditLogs.Add(new AuditLog { ... });
        await _db.SaveChangesAsync(ct);
        return response;
    }
}

10. Edge cases

Case Xử lý
User A và User B cùng role CCM click "Duyệt" đồng thời Optimistic concurrency với RowVersion — người sau nhận 409 Conflict
Drafter cố submit từ DangSoanThaoDangKiemTraCCM (skip phase) Adjacency table reject → 403
BOD reject ở phase DangTrinhKy sau khi mã HĐ đã gen Giữ mã HĐ (không revert seq) + update Phase=DangSoanThao. Khi BOD duyệt lại → vẫn dùng mã cũ (không gen lại)
User role không map đúng phase (vd Finance cố duyệt DangKiemTraCCM) Guard reject 403
HĐ với Chủ đầu tư (BypassProcurementAndCCM=true) Từ DangInKy → cho phép nhảy DangTrinhKy bỏ qua CCM
Xóa HĐ ở phase > DangInKy Block — soft delete cũng không cho (business rule)

11. Liên quan