All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m53s
- STATUS.md: header Phase 7 + 3 row Recently Done (Budget BE / 14 users / Docs cleanup) + cumulative cột mới (51 tables / 14 mig / ~124 endpoints) - HANDOFF.md: TL;DR session 4 (2 milestone Budget BE + 14 users) + Cảnh báo session 5 + Priority 0 (FE Budget + PE/HD integration + PE feature gap) + Credentials 30 user - migration-todos.md: Phase 7 thêm section D Budget done + Phase 8 mới (FE Budget pages + integration) + pending migrations Budget - architecture.md: §10 Budget module mới (ERD + state machine + auto-recompute + integration roadmap) - database/schema-diagram.md: migration history rows 13+14 + §12 Budget ERD chi tiết - ef-core-migration SKILL: migration 14 entry + Phase 8 pending Budget refinement - CLAUDE.md root + docs: modules table thêm Budget row + scope Budget + count 51 bảng / 14 mig - Session log 2026-04-28-chot-session-4-budget.md (10+ section detailed) Stats: 51 tables (+4 Budget), 14 migrations (+AddBudgets), ~124 endpoints (+11 Budget), 30 demo user (16 sample + 14 Solutions thật), 38 gotchas, ~340 LOC Budget CQRS. FE Budget pages chưa làm — Priority 0 session 5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
690 lines
28 KiB
Markdown
690 lines
28 KiB
Markdown
# Schema Diagram — Luồng DB SOLUTION_ERP
|
||
|
||
> ERD đầy đủ + mối quan hệ **36 table** sau Migration 11 (Tier 3 + 4-bảng overhaul + 4 master catalogs + Role/User VN). 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"
|
||
WorkflowDefinitions ||--o{ Contracts : "pinned-policy"
|
||
|
||
Contracts ||--o{ ContractApprovals : "history"
|
||
Contracts ||--o{ ContractComments : "thread"
|
||
Contracts ||--o{ ContractAttachments : "files"
|
||
Users ||--o{ ContractApprovals : "approved-by"
|
||
Users ||--o{ ContractComments : "author"
|
||
|
||
WorkflowDefinitions ||--o{ WorkflowSteps : "has"
|
||
WorkflowSteps ||--o{ WorkflowStepApprovers : "allowed-by"
|
||
|
||
Users ||--o{ Notifications : "recipient"
|
||
|
||
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
|
||
uniqueidentifier WorkflowDefinitionId FK "pinned policy, nullable"
|
||
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
|
||
}
|
||
|
||
Notifications {
|
||
uniqueidentifier Id PK
|
||
uniqueidentifier RecipientUserId FK
|
||
int Type "ContractTransition/CommentAdded/SlaExpired/..."
|
||
nvarchar Title "200"
|
||
nvarchar Body "2000"
|
||
nvarchar Link "500"
|
||
bit IsRead
|
||
datetime2 ReadAt
|
||
datetime2 CreatedAt
|
||
}
|
||
|
||
WorkflowTypeAssignments {
|
||
uniqueidentifier Id PK
|
||
int ContractType "UK"
|
||
nvarchar PolicyName "50 Standard/SkipCcm"
|
||
datetime2 UpdatedAt
|
||
uniqueidentifier UpdatedBy FK
|
||
}
|
||
|
||
WorkflowDefinitions {
|
||
uniqueidentifier Id PK
|
||
nvarchar Code "100 QT-MB / QT-TP / ..."
|
||
int Version "auto-increment per Code"
|
||
bit IsActive "chi 1 active per ContractType"
|
||
int ContractType
|
||
nvarchar Name "200"
|
||
nvarchar Description "500"
|
||
datetime2 CreatedAt
|
||
uniqueidentifier CreatedBy FK
|
||
}
|
||
|
||
WorkflowSteps {
|
||
uniqueidentifier Id PK
|
||
uniqueidentifier WorkflowDefinitionId FK
|
||
int Order
|
||
int Phase "target ContractPhase int"
|
||
nvarchar Name "200"
|
||
int SlaDays
|
||
}
|
||
|
||
WorkflowStepApprovers {
|
||
uniqueidentifier Id PK
|
||
uniqueidentifier WorkflowStepId FK
|
||
int Kind "1=Role, 2=User"
|
||
nvarchar AssignmentValue "200 RoleName or UserId Guid string"
|
||
}
|
||
```
|
||
|
||
## 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 + seed demo)"]
|
||
S[Suppliers]
|
||
PR[Projects]
|
||
DE[Departments]
|
||
end
|
||
|
||
subgraph FORMS ["📄 Form templates (admin upload)"]
|
||
CT[ContractTemplates]
|
||
CC[ContractClauses]
|
||
end
|
||
|
||
subgraph WORKFLOW ["⚙️ Versioned workflow (admin designer)"]
|
||
WD[WorkflowDefinitions]
|
||
WS[WorkflowSteps]
|
||
WSA[WorkflowStepApprovers]
|
||
WTA[WorkflowTypeAssignments legacy override]
|
||
WD --> WS
|
||
WS --> WSA
|
||
end
|
||
|
||
subgraph CONTRACT ["📝 Contract workflow"]
|
||
C[Contracts]
|
||
CA[ContractApprovals]
|
||
CCM[ContractComments]
|
||
CAT[ContractAttachments]
|
||
CCS[ContractCodeSequences]
|
||
end
|
||
|
||
subgraph NOTIFY ["🔔 Notification module"]
|
||
N[Notifications]
|
||
end
|
||
|
||
U -.Drafter.-> C
|
||
S --> C
|
||
PR --> C
|
||
DE --> C
|
||
CT --> C
|
||
WD -.pinned at create.-> C
|
||
C --> CA
|
||
C --> CCM
|
||
C --> CAT
|
||
C -.gen when DangDongDau.-> CCS
|
||
C -.transition event.-> N
|
||
CCM -.comment added.-> N
|
||
U -.recipient.-> N
|
||
```
|
||
|
||
## 3. Vòng đời 1 HĐ — data changes (với versioned workflow)
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Create[POST /contracts type=5]
|
||
Create --> PickWD["SELECT TOP 1 WorkflowDefinition<br/>WHERE ContractType=5 AND IsActive=1<br/>→ Id=wf-v02"]
|
||
PickWD --> C1["Contracts INSERT<br/>Phase=2, SLA=+7d, WorkflowDefinitionId=wf-v02"]
|
||
|
||
Transition1[Transition 2→3]
|
||
Transition1 --> LoadPolicy["Load wf-v02.Steps.Approvers<br/>WorkflowPolicyRegistry.FromDefinition(def)"]
|
||
LoadPolicy --> Guard["Check allowed roles for<br/>(from=2, to=3)"]
|
||
Guard --> C2["UPDATE Phase=3<br/>INSERT ContractApprovals<br/>INSERT Notifications bulk"]
|
||
|
||
Comment[POST /comments]
|
||
Comment --> C3["INSERT ContractComments<br/>INSERT Notifications"]
|
||
|
||
NewVersion[Admin creates QT-MB-v03]
|
||
NewVersion --> NV1["INSERT WorkflowDefinition v03 IsActive=1<br/>UPDATE v02 SET IsActive=0 (atomic)"]
|
||
NV1 -.->|HĐ cũ không ảnh hưởng| C1
|
||
|
||
Transition3[Transition 7→8 BOD ký]
|
||
Transition3 --> CG["ContractCodeGenerator SERIALIZABLE<br/>UPSERT ContractCodeSequences"]
|
||
CG --> C5["UPDATE Contract<br/>SET MaHopDong, Phase=8<br/>INSERT Notifications"]
|
||
```
|
||
|
||
## 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 |
|
||
| Contracts | `IX_Contracts_WorkflowDefinitionId` | Pinned policy lookup |
|
||
| 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 |
|
||
| Notifications | `IX_Notifications_RecipientUserId_IsRead_CreatedAt` | Bell badge unread count + list |
|
||
| WorkflowDefinitions | `UX_WorkflowDefinitions_Code_Version` | Unique version per code |
|
||
| WorkflowDefinitions | `IX_WorkflowDefinitions_ContractType_IsActive` | Active policy lookup |
|
||
| WorkflowSteps | `IX_WorkflowSteps_WorkflowDefinitionId_Order` | Load steps ordered |
|
||
| WorkflowStepApprovers | `IX_WorkflowStepApprovers_WorkflowStepId` | Approver load |
|
||
| WorkflowTypeAssignments | `UX_WorkflowTypeAssignments_ContractType` | 1 override per type (legacy) |
|
||
|
||
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ự |
|
||
| **WorkflowDefinition → Contract** | 1 - N | **Restrict** | **KHÔNG cascade** → HĐ cũ pin version cũ không bị xóa khi admin archive |
|
||
| 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 |
|
||
| **WorkflowDefinition → WorkflowStep** | 1 - N | Cascade | Delete def → remove steps (chỉ khi no Contract tham chiếu) |
|
||
| **WorkflowStep → WorkflowStepApprover** | 1 - N | Cascade | — |
|
||
| **User → Notification (RecipientUserId)** | 1 - N | Cascade | — |
|
||
|
||
## 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
|
||
- WorkflowDefinition (admin archive = `IsActive=false`, xóa logic chỉ khi muốn scrub)
|
||
|
||
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)
|
||
- Notifications — không soft delete, chỉ `IsRead` flag (giữ history ngắn hạn, cleanup job sau)
|
||
- WorkflowStep / WorkflowStepApprover — cascade khi WorkflowDefinition xóa
|
||
- 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 (với versioned workflow)
|
||
|
||
```sql
|
||
-- Server filter (API): HĐ chờ role eligible phase theo pinned policy
|
||
-- Thực tế ContractsController resolve policy runtime:
|
||
-- def = db.WorkflowDefinitions.Include(Steps.Approvers).Where(Id == contract.WorkflowDefinitionId).FirstOrDefault()
|
||
-- policy = def != null ? Registry.FromDefinition(def) : Registry.For(contract.Type)
|
||
-- phase eligible = policy.Transitions.Where(t => t.From == contract.Phase && t.AllowedRoles.Intersect(myRoles).Any())
|
||
-- SQL tương đương:
|
||
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 computed theo pinned workflow */)
|
||
ORDER BY c.SlaDeadline ASC;
|
||
```
|
||
|
||
### Pick active workflow tại create-time
|
||
|
||
```sql
|
||
SELECT TOP 1 Id
|
||
FROM WorkflowDefinitions
|
||
WHERE ContractType = @Type AND IsActive = 1
|
||
ORDER BY [Version] DESC;
|
||
```
|
||
|
||
### Tạo version mới (atomic)
|
||
|
||
```sql
|
||
BEGIN TRAN;
|
||
|
||
-- Step 1: deactivate current active
|
||
UPDATE WorkflowDefinitions
|
||
SET IsActive = 0
|
||
WHERE ContractType = @Type AND IsActive = 1;
|
||
|
||
-- Step 2: compute next version
|
||
DECLARE @NextVersion INT = (SELECT ISNULL(MAX([Version]), 0) + 1 FROM WorkflowDefinitions WHERE Code = @Code);
|
||
|
||
-- Step 3: insert new active version
|
||
INSERT WorkflowDefinitions (Id, Code, [Version], IsActive, ContractType, Name, Description, CreatedAt, CreatedBy)
|
||
VALUES (NEWID(), @Code, @NextVersion, 1, @Type, @Name, @Description, GETUTCDATE(), @UserId);
|
||
|
||
-- Step 4: insert steps + approvers (batch)
|
||
-- ...
|
||
|
||
COMMIT;
|
||
```
|
||
|
||
### Dashboard stats (MyDashboard user-specific)
|
||
|
||
```sql
|
||
SELECT
|
||
(SELECT COUNT(*) FROM Contracts
|
||
WHERE DrafterUserId = @Me AND IsDeleted = 0 AND Phase NOT IN (9, 99)) AS DraftsInProgress,
|
||
(SELECT COUNT(*) FROM Contracts
|
||
WHERE Phase IN (/* eligible phases cho role tôi */) AND IsDeleted = 0) AS PendingMyApproval,
|
||
(SELECT COUNT(*) FROM Contracts
|
||
WHERE IsDeleted = 0 AND SlaDeadline BETWEEN GETUTCDATE() AND DATEADD(DAY, 2, GETUTCDATE())) AS DueSoon,
|
||
(SELECT COUNT(*) FROM Contracts
|
||
WHERE IsDeleted = 0 AND SlaDeadline < GETUTCDATE() AND Phase NOT IN (9, 99)) AS Overdue,
|
||
(SELECT ISNULL(SUM(GiaTri), 0) FROM Contracts
|
||
WHERE DrafterUserId = @Me AND Phase = 2) AS DraftsTotalValue;
|
||
```
|
||
|
||
### Notifications unread count (bell badge)
|
||
|
||
```sql
|
||
SELECT COUNT(*) FROM Notifications
|
||
WHERE RecipientUserId = @Me AND IsRead = 0;
|
||
```
|
||
|
||
### Gen mã HĐ atomic
|
||
|
||
```sql
|
||
BEGIN TRAN;
|
||
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||
|
||
UPDATE ContractCodeSequences
|
||
SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE()
|
||
WHERE Prefix = @Prefix;
|
||
|
||
IF @@ROWCOUNT = 0
|
||
INSERT ContractCodeSequences (Prefix, LastSeq, UpdatedAt)
|
||
VALUES (@Prefix, 1, GETUTCDATE());
|
||
|
||
SELECT LastSeq FROM ContractCodeSequences WHERE Prefix = @Prefix;
|
||
COMMIT;
|
||
```
|
||
|
||
## 8. Migration lịch sử
|
||
|
||
| # | Migration | Tables added / changed |
|
||
|---|---|---|
|
||
| 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 |
|
||
| 6 | `AddNotifications` | Notifications |
|
||
| 7 | `AddWorkflowTypeAssignments` | WorkflowTypeAssignments (admin override legacy) |
|
||
| 8 | `AddVersionedWorkflows` | WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers + Contracts.WorkflowDefinitionId FK |
|
||
| **9** | **`AddContractDetailsAndChangelog`** | **7 ContractType-specific Details + ContractChangelogs (unified audit log)** |
|
||
| **10** | **`AddMasterCatalogs`** | **UnitsOfMeasure, MaterialItems, ServiceItems, WorkItems** |
|
||
| **11** | **`AddRoleShortNameAndUserDepartment`** | **+Role.ShortName + User.DepartmentId/Position (cột thêm, không bảng mới)** |
|
||
| **12** | **`AddPurchaseEvaluations`** | **10 bảng module Duyệt NCC: PurchaseEvaluations + Suppliers + Details + Quotes + Approvals + Changelogs + Attachments + WorkflowDefinitions + WorkflowSteps + WorkflowStepApprovers** |
|
||
| **13** | **`AddPurchaseEvaluationCodeSequences`** | **PurchaseEvaluationCodeSequences (Prefix PK, atomic seq mirror ContractCodeSequences). Format MaPhieu PE/{YYYY}/{A\|B}/{Seq:D3}** |
|
||
| **14** | **`AddBudgets`** | **4 bảng module Ngân sách: Budgets + BudgetDetails + BudgetApprovals + BudgetChangelogs. + nullable FK index Contract.BudgetId & PurchaseEvaluation.BudgetId** |
|
||
|
||
Tổng: **51 bảng** (+ `__EFMigrationsHistory` hệ thống).
|
||
|
||
## 8bis. Bảng mới sau Migration 9-11
|
||
|
||
### Per-type Details (7 bảng — Migration 9)
|
||
|
||
Mỗi loại HĐ có bảng riêng. Common base: `Id`, `ContractId` FK Cascade,
|
||
`Order` int, `ThanhTien` decimal(18,2), `GhiChu` nvarchar(1000) +
|
||
`AuditableEntity` (CreatedAt/By/UpdatedAt/By).
|
||
|
||
| Bảng | Loại HĐ (Type) | Field đặc trưng |
|
||
|---|---|---|
|
||
| `ThauPhuDetails` | 1 | HangMuc, DonViTinh, KhoiLuong, DonGia, ThoiGianHoanThanh |
|
||
| `GiaoKhoanDetails` | 2 | MaCongViec, TenCongViec, KhoiLuong, DonGia, YeuCauKyThuat |
|
||
| `NhaCungCapDetails` | 3 | MaSP, TenSP, ThongSoKyThuat, SoLuong, DonGia, ThoiGianGiao, XuatXu |
|
||
| `DichVuDetails` | 4 | MaDichVu, TenDichVu, ThoiGian, DonGia, TuNgay/DenNgay |
|
||
| `MuaBanDetails` | 5 | MaSP, TenSP, SoLuong, DonGia, **ThueVAT** decimal(5,2), XuatXu |
|
||
| `NguyenTacNccDetails` | 6 | NhomSP, TenSP, **DonGiaToiThieu/ToiDa** (range), DieuKien |
|
||
| `NguyenTacDvDetails` | 7 | LoaiDichVu, TenDichVu, DonGiaToiThieu/ToiDa, PhamViDichVu, SLA |
|
||
|
||
### ContractChangelogs (Migration 9)
|
||
|
||
Unified audit log cho mọi entity HĐ:
|
||
- `Id`, `ContractId` FK Cascade
|
||
- `EntityType` int enum: 1=Contract, 2=Detail, 3=Workflow, 4=Comment, 5=Attachment
|
||
- `EntityId` Guid? (PK của child entity, null nếu là Header)
|
||
- `Action` int enum: 1=Insert, 2=Update, 3=Delete, 4=Transition
|
||
- `PhaseAtChange` int? (snapshot Phase tại thời điểm change)
|
||
- `UserId` Guid? + `UserName` nvarchar(200) (denormalize cho readable)
|
||
- `Summary` nvarchar(500), `FieldChangesJson` nvarchar(max), `ContextNote` nvarchar(2000)
|
||
- Indexes: (ContractId, CreatedAt), (ContractId, EntityType)
|
||
|
||
### 4 Master Catalogs (Migration 10)
|
||
|
||
Phục vụ autocomplete trong Details add form (FE HTML5 datalist).
|
||
|
||
| Bảng | Field | Dùng cho HĐ Detail |
|
||
|---|---|---|
|
||
| `UnitsOfMeasure` | Code, Name, Description | Tất cả 7 type (donViTinh) |
|
||
| `MaterialItems` | Code, Name, Category, DefaultUnit, Specification, OriginCountry, IsActive | NCC + Mua bán + Nguyên tắc NCC |
|
||
| `ServiceItems` | Code, Name, Category, DefaultUnit, Description, IsActive | Dịch vụ + Nguyên tắc DV |
|
||
| `WorkItems` | Code, Name, Category, DefaultUnit, Description, IsActive | Thầu phụ + Giao khoán |
|
||
|
||
Common: `AuditableEntity`, `IX_<Table>_Code` UNIQUE filtered `IsDeleted=0`,
|
||
`IX_<Table>_Category`, `HasQueryFilter !IsDeleted`.
|
||
|
||
### Identity columns thêm (Migration 11)
|
||
|
||
**Role:**
|
||
- `ShortName` nvarchar(50) — Mã viết tắt VN (QTV/BOD/CCM/PRO/FIN/...)
|
||
- `Description` đã có — full Vietnamese label
|
||
|
||
**User:**
|
||
- `DepartmentId` Guid? FK Departments **OnDelete Restrict** (không xóa
|
||
dept nếu còn user reference)
|
||
- `Position` nvarchar(200) — chức vụ free text
|
||
- `IX_Users_DepartmentId`
|
||
|
||
## 9. Versioned workflow invariants
|
||
|
||
```
|
||
1. UNIQUE (WorkflowDefinitions.Code, Version)
|
||
→ không 2 row cùng Code + Version (enforce qua IX unique)
|
||
|
||
2. Chỉ 1 WorkflowDefinition.IsActive = true per ContractType tại 1 thời điểm
|
||
→ enforce qua CreateWorkflowDefinitionCommand: UPDATE deactivate trước INSERT, cùng transaction
|
||
|
||
3. Contract.WorkflowDefinitionId pinned at create → không update sau đó
|
||
→ CreateContractCommandHandler pick active version 1 lần, save
|
||
|
||
4. ON DELETE Restrict FK Contract.WorkflowDefinitionId → WorkflowDefinitions.Id
|
||
→ admin không thể DELETE WorkflowDefinition nếu còn Contract pin
|
||
→ admin archive = set IsActive=false thôi, row vẫn tồn tại
|
||
|
||
5. Runtime policy resolution order (ContractWorkflowService):
|
||
a. If contract.WorkflowDefinitionId NOT NULL → load def → FromDefinition
|
||
b. Else if admin override ở WorkflowTypeAssignments for contract.Type → Registry.ByName
|
||
c. Else → Registry.For(contract.Type) (hardcoded Standard/SkipCcm)
|
||
|
||
6. WorkflowStepApprover.Kind
|
||
- 1=Role: AssignmentValue là RoleName (Domain/Identity/AppRoles constants)
|
||
- 2=User: AssignmentValue là UserId Guid string
|
||
- Runtime guard hiện tại chỉ dùng Role-kind (User-kind data model ready, enable iter sau)
|
||
```
|
||
|
||
## 11. PurchaseEvaluation module (Migration 12 — 10 bảng mới)
|
||
|
||
Module tiền-HĐ: phiếu trình duyệt so sánh giá N NCC × M hạng mục.
|
||
Sau khi DaDuyet → user click "Tạo HĐ từ phiếu" → gen Contract draft
|
||
kế thừa Supplier/Project/GiaTri, link qua `PurchaseEvaluation.ContractId`.
|
||
|
||
### Core (7 bảng):
|
||
|
||
| Bảng | Mục đích |
|
||
|---|---|
|
||
| `PurchaseEvaluations` | Header. MaPhieu auto PE-YYYYMM-XXXX, Type enum (1=DuyetNcc A, 2=DuyetNccPhuongAn B), Phase (7 state + TuChoi), WorkflowDefinitionId pinned at create, SelectedSupplierId (winner), PaymentTerms JSON (D section form), ContractId? FK kế thừa. AuditableEntity. |
|
||
| `PurchaseEvaluationSuppliers` | N:M Phiếu × Supplier. DisplayName ("TGN-30 ngày"), ContactName/Email/Phone (E section), PaymentTermText per NCC, Note (chip "ĐÃ CHỐT SO SÁNH LẦN 1/2"), Order. UK(EvaluationId, SupplierId). |
|
||
| `PurchaseEvaluationDetails` | Hạng mục so sánh. GroupCode (A.I/II/III/IV), GroupName (Bê tông/Phụ gia...), ItemCode, NoiDung, ĐơnViTinh, KhoiLuongNganSach/ThiCong, DonGiaNganSach, ThanhTienNganSach. |
|
||
| `PurchaseEvaluationQuotes` | Báo giá per NCC per Detail (matrix cell). FK Detail + Supplier-row, BgVat/ChuaVat/ThanhTien, IsSelected flag, Note. UK(DetailId, SupplierId). |
|
||
| `PurchaseEvaluationApprovals` | Workflow history (giống ContractApprovals). FromPhase/ToPhase/Decision/Comment/ApprovedAt. |
|
||
| `PurchaseEvaluationChangelogs` | Audit log unified (EntityType: Header/Supplier/Detail/Quote/Workflow/Attachment + Action: Insert/Update/Delete/Transition). |
|
||
| `PurchaseEvaluationAttachments` | File upload — báo giá NCC gửi, bản vẽ, phiếu export. Purpose enum. |
|
||
|
||
### Workflow config (3 bảng — tái dùng pattern HĐ versioned):
|
||
|
||
| Bảng | Mục đích |
|
||
|---|---|
|
||
| `PurchaseEvaluationWorkflowDefinitions` | Versioned definition per Type. Code+Version UNIQUE, IsActive (1 per Type). Seed v01: QT-DN-A (3-step) + QT-DN-B (5-step). |
|
||
| `PurchaseEvaluationWorkflowSteps` | Ordered steps: Order+Phase+Name+SlaDays. |
|
||
| `PurchaseEvaluationWorkflowStepApprovers` | Role/User kind + AssignmentValue (reuse WorkflowApproverKind enum từ HĐ). |
|
||
|
||
### State machine PE Phase:
|
||
|
||
```
|
||
A (NccOnly 3-step):
|
||
DangSoanThao → ChoPurchasing → ChoCCM → ChoCEODuyetNCC → DaDuyet
|
||
bất kỳ phase duyệt reject → DangSoanThao
|
||
DangSoanThao → TuChoi (cancel)
|
||
|
||
B (NccWithPlan 5-step):
|
||
DangSoanThao → ChoPurchasing → ChoDuAn → ChoCCM → ChoCEODuyetPA → ChoCEODuyetNCC → DaDuyet
|
||
Role mapping: Drafter/DeptManager → Procurement (PRO) → ProjectManager (PM) → CostControl (CCM) → Director (BOD)
|
||
```
|
||
|
||
### Kế thừa HĐ flow:
|
||
|
||
```
|
||
PE.Phase=DaDuyet && SelectedSupplierId && !ContractId
|
||
→ user FE click "Tạo HĐ từ phiếu"
|
||
→ pick ContractType (1-7) + TenHopDong + bypassCCM flag
|
||
→ POST /api/purchase-evaluations/{id}/create-contract
|
||
→ CreateContractFromEvaluationCommand:
|
||
1. Verify PE state
|
||
2. new Contract { SupplierId=PE.Selected, ProjectId=PE.Project, ... }
|
||
3. GiaTri = sum(PE.Details.ThanhTienNganSach)
|
||
4. DraftData = PE.PaymentTerms (carry)
|
||
5. WorkflowDefinitionId = active ContractWorkflowDefinition[Type]
|
||
6. Gen MaHopDong (ContractCodeGenerator SERIALIZABLE)
|
||
7. PE.ContractId = contract.Id (link 2 chiều)
|
||
8. Changelog cả 2 bảng
|
||
→ navigate /contracts/{newId}
|
||
```
|
||
|
||
## 12. Budget module (Migration 14 — 4 bảng mới)
|
||
|
||
Module quản lý ngân sách dự án. Liên kết nullable Contract & PurchaseEvaluation cho đối chiếu chi phí. Workflow simple 3-step (chưa versioned, hardcoded `BudgetPolicy.Default`).
|
||
|
||
### Core (4 bảng):
|
||
|
||
| Bảng | Mục đích |
|
||
|---|---|
|
||
| `Budgets` | Header. **MaNganSach** `NS-{YYYYMM}-{Random:4d}` UK filtered, **TenNganSach** required, Description, **NamNganSach** int (year), **ProjectId** FK Restrict, **DepartmentId?** FK Restrict, **DrafterUserId** FK Restrict, **Phase** enum (5-state), **TongNganSach** decimal(18,2) auto-recompute từ Sum(BudgetDetails.ThanhTien), **SlaDeadline** DateTime?, **SlaWarningSent** bool. AuditableEntity (soft delete). |
|
||
| `BudgetDetails` | Flat row pattern (giống PE Details, KHÔNG nested). **BudgetId** FK Cascade, **GroupCode** (50 char) + **GroupName** (200) — phân nhóm hạng mục, **ItemCode** (100), **NoiDung** (500) required, **DonViTinh** (50), **KhoiLuong** decimal(18,4), **DonGia** decimal(18,2), **ThanhTien** decimal(18,2), **Order** int, **GhiChu** (1000). Index (BudgetId, Order). |
|
||
| `BudgetApprovals` | Workflow history. FromPhase/ToPhase/Decision (reuse `ApprovalDecision` enum), ApproverUserId, Comment, ApprovedAt. Index (BudgetId, ApprovedAt). |
|
||
| `BudgetChangelogs` | Audit log unified. **EntityType** enum `BudgetEntityType` (1=Header, 2=Detail, 3=Workflow), EntityId Guid?, **Action** enum `ChangelogAction` (Insert/Update/Delete/Transition), PhaseAtChange enum?, UserId Guid? + UserName denorm, Summary (500), FieldChangesJson nvarchar(max), ContextNote (2000). Indexes (BudgetId, CreatedAt) + (BudgetId, EntityType). |
|
||
|
||
### Link nullable từ Contract & PE:
|
||
|
||
```sql
|
||
ALTER TABLE Contracts ADD BudgetId UNIQUEIDENTIFIER NULL;
|
||
ALTER TABLE PurchaseEvaluations ADD BudgetId UNIQUEIDENTIFIER NULL;
|
||
CREATE INDEX IX_Contracts_BudgetId ON Contracts (BudgetId);
|
||
CREATE INDEX IX_PurchaseEvaluations_BudgetId ON PurchaseEvaluations (BudgetId);
|
||
```
|
||
|
||
Không có FK constraint cứng → Budget có thể bị soft-delete mà không ảnh hưởng HĐ/PE đã link. App layer guard validate khi chọn Budget (filter `Phase=DaDuyet && !IsDeleted`).
|
||
|
||
### State machine BudgetPhase (5-state):
|
||
|
||
```
|
||
DangSoanThao(1) ──Trình──► ChoCCM(2) ──Duyệt──► ChoCEO(3) ──Duyệt──► DaDuyet(4)
|
||
▲ │ │
|
||
└──Trả về───────────────┘ │
|
||
└──Trả về─────────────────────────────────────┘
|
||
└──Hủy──► TuChoi(99)
|
||
|
||
Role mapping (BudgetPolicy.Default):
|
||
- Drafter / DeptManager: DangSoanThao → ChoCCM (Trình) hoặc TuChoi (Hủy)
|
||
- CostControl (CCM): ChoCCM → ChoCEO (Duyệt) hoặc DangSoanThao (Trả về)
|
||
- Director / AuthSigner: ChoCEO → DaDuyet (Duyệt) hoặc DangSoanThao (Trả về)
|
||
```
|
||
|
||
### Auto-recompute TongNganSach:
|
||
|
||
```csharp
|
||
// AddBudgetDetailHandler / UpdateBudgetDetailHandler / DeleteBudgetDetailHandler
|
||
budget.TongNganSach = budget.Details.Where(d => !d.IsDeleted).Sum(d => d.ThanhTien);
|
||
db.Budgets.Update(budget);
|
||
```
|
||
|
||
Không trigger DB → app layer tính lại sau mỗi mutation Detail. Đơn giản, debug dễ.
|
||
|
||
### Pending refinement (Phase 8):
|
||
|
||
- **`AddBudgetCodeSequences`** — atomic SERIALIZABLE sequence khi format chốt chính thức (mirror Contract/PE pattern).
|
||
- **`AddBudgetVersionedWorkflow`** — nếu Solutions cần admin config UI thay vì hardcoded policy. Pattern: `BudgetWorkflowDefinitions` + `Steps` + `StepApprovers` + `Budget.WorkflowDefinitionId?` pinned at create.
|
||
|
||
## 13. 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 + versioned
|
||
- [`../flows/`](../flows/) — sequence diagrams
|