# Schema Diagram — Luồng DB SOLUTION_ERP
> ERD đầy đủ + mối quan hệ 19 table sau Phase 3. Mermaid render ở VS Code / GitHub / Gitea.
## 1. Full ERD
```mermaid
erDiagram
Users ||--o{ UserRoles : "has"
Roles ||--o{ UserRoles : "assigned to"
Users ||--o{ UserClaims : "has"
Users ||--o{ UserLogins : "has"
Users ||--o{ UserTokens : "has"
Roles ||--o{ RoleClaims : "has"
Roles ||--o{ Permissions : "grants"
MenuItems ||--o{ Permissions : "controlled by"
MenuItems ||--o{ MenuItems : "parent-of"
Suppliers ||--o{ Contracts : "party-B"
Projects ||--o{ Contracts : "belongs-to"
Departments ||--o{ Contracts : "drafted-in"
Users ||--o{ Contracts : "drafter"
ContractTemplates ||--o{ Contracts : "uses"
Contracts ||--o{ ContractApprovals : "history"
Contracts ||--o{ ContractComments : "thread"
Contracts ||--o{ ContractAttachments : "files"
Users ||--o{ ContractApprovals : "approved-by"
Users ||--o{ ContractComments : "author"
Users {
uniqueidentifier Id PK
nvarchar FullName "200"
nvarchar Email "UK"
nvarchar PasswordHash
nvarchar RefreshToken "512"
datetime2 RefreshTokenExpiresAt
bit IsActive
datetime2 CreatedAt
}
Roles {
uniqueidentifier Id PK
nvarchar Name "UK"
nvarchar Description "500"
datetime2 CreatedAt
}
MenuItems {
nvarchar Key PK "50"
nvarchar Label "200"
nvarchar ParentKey FK "NULL if root"
int Order
nvarchar Icon "50"
}
Permissions {
uniqueidentifier Id PK
uniqueidentifier RoleId FK
nvarchar MenuKey FK "50"
bit CanRead
bit CanCreate
bit CanUpdate
bit CanDelete
}
Suppliers {
uniqueidentifier Id PK
nvarchar Code "UK 50"
nvarchar Name "200"
int Type "NCC/NTP/TD/DVDV/CDT"
nvarchar TaxCode "20"
nvarchar Phone "30"
nvarchar Email "100"
nvarchar Address "500"
bit IsDeleted
}
Projects {
uniqueidentifier Id PK
nvarchar Code "UK 50"
nvarchar Name "200"
date StartDate
date EndDate
uniqueidentifier ManagerUserId FK
decimal BudgetTotal "18,2"
}
Departments {
uniqueidentifier Id PK
nvarchar Code "UK 50"
nvarchar Name "200"
uniqueidentifier ManagerUserId FK
}
ContractTemplates {
uniqueidentifier Id PK
nvarchar FormCode "UK 50"
nvarchar Name "200"
int ContractType "nullable"
nvarchar FileName "255"
nvarchar StoragePath "500"
nvarchar Format "docx/xlsx"
nvarchar FieldSpec "max JSON"
bit IsActive
}
ContractClauses {
uniqueidentifier Id PK
nvarchar Code "UK 50"
nvarchar Name "200"
nvarchar Content "max rich text"
int Version
bit IsActive
}
Contracts {
uniqueidentifier Id PK
nvarchar MaHopDong "UK filtered 100"
nvarchar TenHopDong "500"
int Type "ContractType enum"
int Phase "9 state"
uniqueidentifier SupplierId FK
uniqueidentifier ProjectId FK
uniqueidentifier DepartmentId FK
uniqueidentifier DrafterUserId FK
uniqueidentifier TemplateId FK
decimal GiaTri "18,2"
bit BypassProcurementAndCCM
datetime2 SlaDeadline
bit SlaWarningSent
nvarchar DraftData "max JSON"
bit IsDeleted
}
ContractApprovals {
uniqueidentifier Id PK
uniqueidentifier ContractId FK
int FromPhase
int ToPhase
uniqueidentifier ApproverUserId FK "NULL=system"
int Decision "Pending/Approve/Reject/AutoApprove"
nvarchar Comment "1000"
datetime2 ApprovedAt
}
ContractComments {
uniqueidentifier Id PK
uniqueidentifier ContractId FK
uniqueidentifier UserId FK
int Phase "phase luc comment"
nvarchar Content "2000"
datetime2 CreatedAt
}
ContractAttachments {
uniqueidentifier Id PK
uniqueidentifier ContractId FK
nvarchar FileName "255"
nvarchar StoragePath "500"
bigint FileSize
nvarchar ContentType "100"
int Purpose "DraftExport/ScannedSigned/SealedCopy"
nvarchar Note "500"
}
ContractCodeSequences {
nvarchar Prefix PK "200"
int LastSeq
datetime2 UpdatedAt
}
```
## 2. Luồng dữ liệu chính (data flow diagram)
```mermaid
flowchart TB
subgraph IDENTITY ["🔐 Identity domain (seed lần đầu)"]
U[Users] -->|N-M| R[Roles]
R --> P[Permissions]
P --> MI[MenuItems]
end
subgraph MASTER ["📋 Master data (admin CRUD)"]
S[Suppliers]
PR[Projects]
DE[Departments]
end
subgraph FORMS ["📄 Form templates (seed)"]
CT[ContractTemplates]
CC[ContractClauses]
end
subgraph CONTRACT ["📝 Contract workflow (Phase 3 core)"]
C[Contracts]
CA[ContractApprovals]
CCM[ContractComments]
CAT[ContractAttachments]
CCS[ContractCodeSequences]
end
U -.Drafter.-> C
S --> C
PR --> C
DE --> C
CT --> C
C --> CA
C --> CCM
C --> CAT
C -.gen when DangDongDau.-> CCS
```
## 3. Vòng đời 1 HĐ — data changes
```mermaid
flowchart LR
Create[POST /contracts]
Create --> C1["Contracts INSERT
Phase=2, SLA=+7d"]
Transition1[Transition 2→3]
Transition1 --> C2["UPDATE Phase=3
INSERT ContractApprovals"]
Comment[POST /comments]
Comment --> C3[INSERT ContractComments]
Transition2[Transition 3→4→5→6→7]
Transition2 --> C4["UPDATE Phase + SlaDeadline
INSERT ContractApprovals"]
Transition3[Transition 7→8 BOD ký]
Transition3 --> CG["ContractCodeGenerator
SERIALIZABLE tran
UPSERT ContractCodeSequences"]
CG --> C5["UPDATE Contract
SET MaHopDong='FLOCK 01/HĐGK/SOL&PVL/01',
Phase=8"]
Transition4[Transition 8→9 HRA phát hành]
Transition4 --> C6["UPDATE Phase=9, SlaDeadline=NULL
INSERT ContractApprovals"]
```
## 4. Index strategy
| Table | Index | Purpose |
|---|---|---|
| Contracts | `UX_Contracts_MaHopDong` filtered `WHERE [MaHopDong] IS NOT NULL` | Unique mã HĐ |
| Contracts | `IX_Contracts_Phase_IsDeleted` | Inbox query theo phase |
| Contracts | `IX_Contracts_SupplierId` | Filter HĐ theo NCC |
| Contracts | `IX_Contracts_ProjectId` | Filter HĐ theo dự án |
| Contracts | `IX_Contracts_SlaDeadline` | SLA expiry job query |
| ContractApprovals | `IX_ContractApprovals_ContractId_ApprovedAt` | Timeline query theo HĐ |
| ContractComments | `IX_ContractComments_ContractId_CreatedAt` | Thread load |
| ContractAttachments | `IX_ContractAttachments_ContractId` | Attachments load |
| Suppliers/Projects/Departments | `UX_{Table}_Code` | Unique business code |
| Permissions | `UX_Permissions_RoleId_MenuKey` | 1 row / role / menu |
| MenuItems | `IX_MenuItems_ParentKey` | Tree query |
Chi tiết + cheatsheet SQL: [`database-guide.md`](database-guide.md).
## 5. Relationship cardinality
| Parent → Child | Cardinality | OnDelete | Ghi chú |
|---|---|---|---|
| Contract → ContractApproval | 1 - N | Cascade | Xóa HĐ → xóa lịch sử (nhưng mức delete bị chặn sau DangInKy) |
| Contract → ContractComment | 1 - N | Cascade | — |
| Contract → ContractAttachment | 1 - N | Cascade | File vật lý vẫn còn trong `wwwroot/uploads/`, cleanup riêng |
| Supplier → Contract | 1 - N | Restrict | Không xóa Supplier nếu còn HĐ tham chiếu |
| Project → Contract | 1 - N | Restrict | Tương tự |
| Role → Permission | 1 - N | Cascade | Xóa role → clear permissions |
| MenuItem → Permission | 1 - N | Cascade | — |
| MenuItem → MenuItem (self-ref) | 1 - N | Restrict | Parent-child menu |
## 6. Soft delete behavior
Mọi entity extend `AuditableEntity`:
- Delete qua `db.X.Remove(entity)` → `AuditingInterceptor` chuyển `EntityState.Deleted` → `Modified`, set `IsDeleted=true, DeletedAt, DeletedBy`
- Query filter `HasQueryFilter(x => !x.IsDeleted)` tự động filter ra
- Để query bao gồm soft-deleted: `.IgnoreQueryFilters()`
Entity list áp dụng:
- Supplier, Project, Department
- Contract
- ContractTemplate, ContractClause
KHÔNG soft delete (cascade hoặc keep):
- ContractApproval, ContractComment, ContractAttachment — cascade khi Contract xóa
- Permission, MenuItem — cascade khi Role xóa
- ContractCodeSequence — không bao giờ xóa (giữ history seq)
- Identity tables (Users, Roles, ...) — Identity không support soft delete built-in
## 7. Truy vấn tiêu biểu
### Inbox HĐ chờ role của tôi
```sql
-- Tương đương GetMyInboxQuery
SELECT c.Id, c.MaHopDong, c.TenHopDong, c.Phase, c.SlaDeadline, s.Name AS SupplierName, p.Name AS ProjectName
FROM Contracts c
INNER JOIN Suppliers s ON c.SupplierId = s.Id
INNER JOIN Projects p ON c.ProjectId = p.Id
WHERE c.IsDeleted = 0
AND c.Phase IN (/* phase eligible cho role hiện tại */)
ORDER BY c.SlaDeadline ASC;
```
### Dashboard stats
```sql
-- Tổng + active + overdue
SELECT
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0) AS Total,
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99)) AS Active,
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99) AND SlaDeadline < GETUTCDATE()) AS Overdue;
-- By phase
SELECT Phase, COUNT(*) FROM Contracts WHERE IsDeleted = 0 GROUP BY Phase;
-- Top 5 NCC
SELECT TOP 5 c.SupplierId, s.Name, COUNT(*) AS Cnt, SUM(c.GiaTri) AS TotalValue
FROM Contracts c
INNER JOIN Suppliers s ON c.SupplierId = s.Id
WHERE c.IsDeleted = 0
GROUP BY c.SupplierId, s.Name
ORDER BY COUNT(*) DESC;
```
### Gen mã HĐ atomic
```sql
BEGIN TRAN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
MERGE ContractCodeSequences AS tgt
USING (SELECT @Prefix AS Prefix) AS src ON tgt.Prefix = src.Prefix
WHEN MATCHED THEN UPDATE SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE()
WHEN NOT MATCHED THEN INSERT (Prefix, LastSeq, UpdatedAt) VALUES (@Prefix, 1, GETUTCDATE());
SELECT LastSeq FROM ContractCodeSequences WHERE Prefix = @Prefix;
COMMIT;
```
(EF Core impl dùng `UPDATE + IF ROWCOUNT = 0 INSERT` thay MERGE — tương đương nhưng an toàn hơn với SERIALIZABLE).
## 8. Migration lịch sử
| # | Migration | Tables added |
|---|---|---|
| 1 | `Init` | 7 Identity tables |
| 2 | `AddMasterData` | Suppliers, Projects, Departments |
| 3 | `AddPermissions` | MenuItems, Permissions |
| 4 | `AddForms` | ContractTemplates, ContractClauses |
| 5 | `AddContractsWorkflow` | Contracts, ContractApprovals, ContractComments, ContractAttachments, ContractCodeSequences |
Tổng: **19 bảng** (+ `__EFMigrationsHistory` hệ thống).
## 9. Liên quan
- [`database-guide.md`](database-guide.md) — conventions + migration workflow + cheatsheet đầy đủ
- [`../architecture.md`](../architecture.md) — layered architecture + data flow
- [`../workflow-contract.md`](../workflow-contract.md) — state machine spec
- [`../flows/`](../flows/) — sequence diagrams