[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>
This commit is contained in:
pqhuy1987
2026-04-21 11:15:28 +07:00
parent 702411fcc8
commit 49a5f57a50
12 changed files with 1982 additions and 2 deletions

View File

@ -0,0 +1,278 @@
# 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