# Contract Approval Flow — State Machine 9 Phase

> **Status:** 📝 Planned (Phase 3)
> **Spec gốc:** [`../workflow-contract.md`](../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

```mermaid
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`](../workflow-contract.md). Tổng ~19 ngày.

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

### Request

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

### Response

```json
{
  "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

```mermaid
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)

```csharp
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)

```mermaid
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 |
|---|---|---|
| `DangSoanThao` → `DangGopY` | 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):

```csharp
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ừ `DangSoanThao` → `DangKiemTraCCM` (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

- [`../workflow-contract.md`](../workflow-contract.md) — spec gốc
- [`sla-expiry-flow.md`](sla-expiry-flow.md) — auto-approve job
- [`contract-creation-flow.md`](contract-creation-flow.md) — tạo HĐ trước khi vào flow này
- [`form-render-flow.md`](form-render-flow.md) — render file khi chuyển phase
