# 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