Session 5 (29/04) — 6 commit feature + 1 chốt MD này. ==== Stats sau session 5 ==== - 52 DB tables (+1 PEDeptOpinions) - 15 migrations (+`AddPurchaseEvaluationDepartmentOpinions`) - ~128 API endpoints (+4) - ~31 FE pages (+5 Budget + 1 PeWorkflowsPage) - 71 unit test pass (54 Domain + 17 Infra) — CI gate live, fail → no deploy - ~13050 BE LOC (+1300) - 30 demo user, 38 gotchas, 6 skill (no change) ==== MD touched ==== - STATUS.md: header Phase 8 + 6 row Recently Done session 5 + cumulative cột S5 + In Progress S6 (Hard blockers + Optional polish + Tests Phase 3-5 + Ops) - HANDOFF.md: TL;DR 6 milestone S5 + Cảnh báo S6 (CI test gate workflow mới) + Priority 0 S6 (UAT + Ops focus) + Phase status table cập nhật - migration-todos.md: Phase 8 done với A/B/C/D/E (FE Budget / PE-HD integration / PE WF Designer / Ý kiến 4 PB / Tests Phase 1-2) + Phase 9 active (UAT + Ops + carry over) - architecture.md: §11 Testing strategy mới (test pyramid bottom-heavy + stack + CI gate + phased priority + quy tắc bổ sung mỗi feature) - database/schema-diagram.md: Migration 15 row + total 52 tables + §13 PE Department Opinion (1 bảng UNIQUE PEId+Kind + Upsert behavior + SQL DDL) - ef-core-migration SKILL: migration 15 entry + 52 tables total + Phase 8 update note - CLAUDE.md (root): modules table + Tests row + scope `Tests` + Tests section mới + count update 15/52 - docs/CLAUDE.md: 7 module bullet + ERD 52 bảng + Roadmap Phase 8 done + Phase 9 active S6 - memory project_solution_erp.md: Phase 8 summary + Session 6 priority + workflow user mới (dotnet test → commit → push) - session log 2026-04-29-chot-session-5-budget-fe-pe-tests.md (NEW — 10+ section detail) ==== Verify ==== - dotnet test SolutionErp.slnx → 71 pass / 2s - git status clean sau commit này Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
30 KiB
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
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)
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)
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.
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)→AuditingInterceptorchuyểnEntityState.Deleted→Modified, setIsDeleted=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ỉ
IsReadflag (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)
-- 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
SELECT TOP 1 Id
FROM WorkflowDefinitions
WHERE ContractType = @Type AND IsActive = 1
ORDER BY [Version] DESC;
Tạo version mới (atomic)
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)
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)
SELECT COUNT(*) FROM Notifications
WHERE RecipientUserId = @Me AND IsRead = 0;
Gen mã HĐ atomic
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 |
| 15 | AddPurchaseEvaluationDepartmentOpinions |
1 bảng PurchaseEvaluationDepartmentOpinions (UNIQUE PEId+Kind, max 1 row mỗi loại phòng ban per phiếu — UPDATE in-place, audit qua Changelog) |
Tổng: 52 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,ContractIdFK CascadeEntityTypeint enum: 1=Contract, 2=Detail, 3=Workflow, 4=Comment, 5=AttachmentEntityIdGuid? (PK của child entity, null nếu là Header)Actionint enum: 1=Insert, 2=Update, 3=Delete, 4=TransitionPhaseAtChangeint? (snapshot Phase tại thời điểm change)UserIdGuid? +UserNamenvarchar(200) (denormalize cho readable)Summarynvarchar(500),FieldChangesJsonnvarchar(max),ContextNotenvarchar(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:
ShortNamenvarchar(50) — Mã viết tắt VN (QTV/BOD/CCM/PRO/FIN/...)Descriptionđã có — full Vietnamese label
User:
DepartmentIdGuid? FK Departments OnDelete Restrict (không xóa dept nếu còn user reference)Positionnvarchar(200) — chức vụ free textIX_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:
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:
// 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. PE Department Opinion (Migration 15 — 1 bảng mới)
Section "Ý kiến 4 phòng ban" trên PHIẾU TRÌNH KÝ CHỌN TP/NCC — sign-off block 4 box (Phê duyệt / P.CCM / P.MuaHàng / SM-PM). Lưu thành table riêng (1:N với PE) thay vì bloat 12 column nullable trong PE header.
Core (1 bảng):
| Bảng | Mục đích |
|---|---|
PurchaseEvaluationDepartmentOpinions |
PEId FK Cascade + Kind enum PeDepartmentKind (1=PheDuyet, 2=Ccm, 3=MuaHang, 4=SmPm) + Opinion text(2000) + SignedAt? + UserId? + UserName denorm. UNIQUE(PEId, Kind) — max 1 row mỗi phòng ban per phiếu. AuditableEntity. |
Upsert behavior:
- Lần đầu (Add): entity mới với Kind = X, Opinion = text, SignedAt =
Sign ? UtcNow : null - Lần sau (Update): UPDATE in-place (gốc giữ Id), Opinion = new text. Sign=true → cập nhật SignedAt+UserId; Sign=false → giữ chữ ký cũ (chỉ update text)
- Audit qua
PurchaseEvaluationChangelogmỗi lần Upsert/Delete
CREATE TABLE PurchaseEvaluationDepartmentOpinions (
Id UNIQUEIDENTIFIER PRIMARY KEY,
PurchaseEvaluationId UNIQUEIDENTIFIER NOT NULL,
Kind INT NOT NULL,
Opinion NVARCHAR(2000) NULL,
SignedAt DATETIME2 NULL,
UserId UNIQUEIDENTIFIER NULL,
UserName NVARCHAR(200) NULL,
-- audit
CreatedAt, CreatedBy, UpdatedAt, UpdatedBy, IsDeleted, DeletedAt, DeletedBy,
CONSTRAINT FK_... FOREIGN KEY (PurchaseEvaluationId) REFERENCES PurchaseEvaluations(Id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IX_PEDeptOpinions_PEId_Kind ON PurchaseEvaluationDepartmentOpinions (PurchaseEvaluationId, Kind);
14. Liên quan
database-guide.md— conventions + migration workflow + cheatsheet đầy đủ../architecture.md— layered architecture + data flow../workflow-contract.md— state machine spec + versioned../flows/— sequence diagrams