All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m30s
Plan K Mig 31 F2 refactor sang per-Approver-slot DONE — 8 commits cumulative S23 t1 (`56868bf..<this>`). K8 wrap docs + dirty MEMORY.md commit: Docs updates: - docs/STATUS.md: Last updated S23 t1 entry với Plan K summary 8 chunk - docs/HANDOFF.md: TL;DR S23 t1 đầy đủ (top) — multi-agent ROI evidence - docs/database/schema-diagram.md §14: title Mig 22-31 (was 22-29) + add Mig 30 F4 + Mig 31 F2 blocks per slot Approver + DROP Users column note - NEW docs/changelog/sessions/2026-05-14-s23-turn1-plan-k-mig31-f2-refactor.md session log đầy đủ 8 chunk timeline + multi-agent spawn cost table + pattern reinforced 3× FE Admin Designer comment cleanup (Reviewer K2 follow-up): - ApprovalWorkflowsV2Page.tsx lines 73-75 + 502-504: 2 stale narratives "F2 AllowDrafterSkipToFinal xuống per User (User Management)" rewrite Mig 29+30+31 cumulative narrative "7 Allow* ALL xuống per Level slot, pattern proven 3×" 3 agent MEMORY.md drift commit (dirty từ session start S23 + S22 chốt): - Investigator: K0 pre-flight findings + 5 surprises catch - Reviewer: K2 PASS report + new pattern "transient sentinel zombie" anti-pattern - CICD Monitor: S22 chốt verify cumulative (Run #193 + S23 t1 pending K9 spawn) User-level memory updates (cross-project diary persisted ngoài repo): - feedback_per_nv_permission_scope.md: reinforcement S23 t1 — Pattern 3× cumulative (Mig 29 + Mig 30 + Mig 31). Pattern ALSO applies cho refactor existing scope, KHÔNG chỉ greenfield. Cross-ref discoveries Plan K (compile-break workaround, stale narrative drift, transient sentinel zombie anti-pattern caught Reviewer). - MEMORY.md index: cumulative reinforcement note 3× Mig 31 Verify: - dotnet build production projects clean - npm run build fe-admin pass 17.76s, 0 TS err - Test 104/104 PASS (S23 t1 K7 chunk maintained baseline) Plan K state final: 31 mig · 59 tables · ~145 endpoints · 104 test · 47 gotcha · 20 memory · 6 skills · 4 sub-agents active. CHƯA push remote — chờ bro confirm K9 spawn CICD Monitor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
916 lines
43 KiB
Markdown
916 lines
43 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** |
|
||
| **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)** |
|
||
| **16** | **`AddTwoStageDeptApprovalAndSmartReject`** | **3 bảng `Contract/PurchaseEvaluation/Budget DepartmentApprovals` (UNIQUE TargetId+Phase+Dept+Stage cho 2-stage NV/TPB approval per phòng × phase) + 4 ALTER (`Users.CanBypassReview` bit + 3 `RejectedFromPhase` int cho smart reject quay về Drafter + jump-back phase đã reject)** |
|
||
|
||
Tổng: **55 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. 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 `PurchaseEvaluationChangelog` mỗi lần Upsert/Delete
|
||
|
||
```sql
|
||
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. ApprovalWorkflow V2 schema (Migration 22-31, Session 17-23 — 3 bảng mới + 7 Allow* options per slot Approver)
|
||
|
||
Schema riêng song song WorkflowDefinition V1 (Mig 21) — pin per phiếu PE.
|
||
V1 vẫn giữ cho phiếu cũ; V2 mới là active cho phiếu tạo từ Session 17 trở đi.
|
||
|
||
**Pattern cumulative cho admin opt-in flag per slot — proven 3× (memory `feedback_per_nv_permission_scope.md`):**
|
||
- Mig 29 (S21 t5) — Refactor 6 Allow* options từ workflow-level (Mig 28 S21 t4) sang PER-NV: 5 flag F1+F3 xuống Level slot, 1 flag F2 xuống User. Backfill bulk SQL preserve admin config S21 t4.
|
||
- Mig 30 (S22+5) — F4 `AllowApproverEditBudget` per slot Approver — admin tick cho NV chỉnh Section "Điều chỉnh ngân sách" khi đang duyệt.
|
||
- Mig 31 (S23 t1) — F2 refactor sang Approver scope: ADD `AllowApproverSkipToFinal` per Level slot, DROP `Users.AllowDrafterSkipToFinal`. Semantic cũ Drafter-from-Nháp deprecated. **NO BACKFILL** (4 prod user lose flag value — admin re-config qua Designer).
|
||
|
||
### Core (3 bảng):
|
||
|
||
```
|
||
ApprovalWorkflows
|
||
├── Id (PK), Code, Version (UNIQUE Code+Version)
|
||
├── ApplicableType (1=DuyetNcc, 2=DuyetNccPhuongAn, 3=Contract)
|
||
├── Name, Description, IsActive, ActivatedAt
|
||
├── IsUserSelectable (Mig 25, S18) — admin pin/unpin cho user pick lúc create phiếu
|
||
│ (Mig 28 cũ 6 column Allow* đã DROP trong Mig 29 — refactor sang per-NV)
|
||
└── (audit) CreatedAt, UpdatedAt, CreatedBy, UpdatedBy
|
||
|
||
ApprovalWorkflowSteps (FK Cascade ApprovalWorkflowId, FK Restrict DepartmentId)
|
||
├── Id (PK), ApprovalWorkflowId, Order (1-based, sort key — KHÔNG phải position)
|
||
├── Name (vd "Phòng A"), DepartmentId? (hint)
|
||
└── INDEX (ApprovalWorkflowId, Order) + INDEX DepartmentId
|
||
|
||
ApprovalWorkflowLevels (FK Cascade ApprovalWorkflowStepId, FK Restrict ApproverUserId)
|
||
├── Id (PK), ApprovalWorkflowStepId, Order (1/2/3 trong Step)
|
||
├── Name? (vd "Cấp 1"), ApproverUserId (1 NV cụ thể)
|
||
│
|
||
├── Mig 29 (S21 t5) — 5 advanced options per slot Approver (F1+F3):
|
||
├── AllowReturnOneLevel bit DEFAULT 0 — F1 mode: Trả về 1 Cấp trước (peer review)
|
||
├── AllowReturnOneStep bit DEFAULT 0 — F1 mode: Trả về 1 Bước trước
|
||
├── AllowReturnToAssignee bit DEFAULT 0 — F1 mode: Trả về Người chỉ định
|
||
├── AllowReturnToDrafter bit DEFAULT 1 — F1 mode: Trả về Drafter (S17 backward compat)
|
||
├── AllowApproverEditDetails bit DEFAULT 0 — F3: NV này chỉnh Section 2 lúc đang duyệt
|
||
│
|
||
├── Mig 30 (S22+5) — F4 admin opt-in per slot:
|
||
├── AllowApproverEditBudget bit DEFAULT 0 — F4: NV này chỉnh Section "Điều chỉnh ngân sách"
|
||
│
|
||
├── Mig 31 (S23 t1) — F2 refactor admin opt-in per slot:
|
||
├── AllowApproverSkipToFinal bit DEFAULT 0 — F2: NV này duyệt thẳng Cấp cuối (skip trung gian)
|
||
│ (storage cũ Users.AllowDrafterSkipToFinal đã DROP)
|
||
│
|
||
└── INDEX (ApprovalWorkflowStepId, Order) + INDEX ApproverUserId
|
||
|
||
Users (AspNetUsers extension)
|
||
├── ... existing columns (FullName, DepartmentId, PositionLevel, CanBypassReview, etc)
|
||
└── (Mig 29 AllowDrafterSkipToFinal column đã DROP trong Mig 31 — F2 refactor sang Approver scope)
|
||
```
|
||
|
||
**Convention quan trọng:** nhiều `ApprovalWorkflowLevel` rows cùng `Order` trong cùng Step = **same Cấp với N approvers** (OR-of-N). Ví dụ Cấp 1 có 2 NV: 2 row Level cùng `Order=1` khác `ApproverUserId`.
|
||
|
||
### Pin V2 vào PE (Mig 23-24):
|
||
|
||
```
|
||
PurchaseEvaluations (column mới)
|
||
├── ApprovalWorkflowId Guid? (FK Restrict ApprovalWorkflows) — pin lúc create
|
||
└── CurrentApprovalLevelOrder int? — track Cấp đang chờ duyệt (1/2/3)
|
||
```
|
||
|
||
Combined với `CurrentWorkflowStepIndex int?` (Mig 21) để track full state V2:
|
||
- `CurrentWorkflowStepIndex` = 0-based index của Step (sau khi sort by `Order`)
|
||
- `CurrentApprovalLevelOrder` = `Order` của Cấp đang chờ trong Step đó
|
||
|
||
Service iterate: `steps[CurrentStepIndex].Levels.Where(l => l.Order == CurrentLevelOrder)` → list approvers, match `actor.Id ∈ ApproverUserId`.
|
||
|
||
### Match approver V2 (khác V1):
|
||
|
||
| Schema | Match logic | Approver type |
|
||
|---|---|---|
|
||
| V1 (Mig 21) | `actor.Dept == step.Dept && actor.PositionLevel >= step.PositionLevel` OR Approvers Role/User explicit | Group qua Dept+Level |
|
||
| V2 (Mig 22-25) | `actor.Id == any level.ApproverUserId where level.Order == currentLevelOrder` | NV cụ thể 1-1 |
|
||
|
||
### State transitions (V2 + V1 cùng):
|
||
|
||
```
|
||
DangSoanThao ──Drafter trình──► ChoDuyet (idx=0, levelOrder=1 nếu V2)
|
||
TraLai ──Drafter sửa+gửi lại──► ChoDuyet (idx=0, levelOrder=1) — chạy LẠI từ đầu
|
||
ChoDuyet ──Approver duyệt cấp──► ChoDuyet (advance levelOrder hoặc idx)
|
||
──Approver Trả lại──► TraLai (clear pointers)
|
||
──Approver Từ chối──► TuChoi (terminal khoá)
|
||
──last step+level done──► DaDuyet (terminal)
|
||
```
|
||
|
||
### Service branch theo schema pin:
|
||
|
||
```csharp
|
||
// PurchaseEvaluationWorkflowService.TransitionAsync (Mig 24 wire)
|
||
if (evaluation.ApprovalWorkflowId is Guid awId)
|
||
await ApproveV2Async(...) // iterate ApprovalWorkflowSteps + Levels match ApproverUserId
|
||
else
|
||
await ApproveV1LegacyAsync(...) // iterate WorkflowSteps match Dept+PositionLevel
|
||
```
|
||
|
||
### Designer constraints (FE Validator + BE Validator):
|
||
|
||
- Tối đa 3 Cấp/Bước (`Order ∈ {1,2,3}`)
|
||
- Sequential gating: `1` → `1+2` → `1+2+3` (không cho `2` nếu thiếu `1`, không `1+3` nếu thiếu `2`)
|
||
- No duplicate `(Order, ApproverUserId)` cùng Step (1 NV không thêm 2 lần cùng Cấp)
|
||
- Mỗi Cấp có N NV (OR-of-N)
|
||
- Đổi Phòng → clear toàn bộ approvers (NV cũ có thể không thuộc Phòng mới)
|
||
|
||
### IsUserSelectable filter logic (Mig 25, Session 18):
|
||
|
||
`IsUserSelectable` độc lập với `IsActive`:
|
||
- `IsActive` = "đang áp dụng (default mới)" — max 1 per ApplicableType
|
||
- `IsUserSelectable` = "cho user pick lúc create phiếu" — multiple version cùng selectable
|
||
|
||
Default behavior (sau Mig 25 backfill): active workflows tự động `IsUserSelectable=1` (giữ behavior cũ — Workspace dropdown vẫn hiện active workflow). Archived versions = `0` default, admin có thể toggle qua Designer khi muốn user pick lại version cũ.
|
||
|
||
`CreateAwDefinitionCommand` set `IsUserSelectable=true` cho version mới (mirror IsActive default). API `PATCH /api/approval-workflows-v2/{id}/user-selectable` — admin toggle stick/unstick (policy `Workflows.Create`).
|
||
|
||
Workspace `PeWorkspaceCreateView` query `.filter(w => w.isUserSelectable)` — chỉ hiện workflows admin đã ghim.
|
||
|
||
### Pending Session 20+:
|
||
|
||
- Contract V2 wire (Mig 27/28): mirror PE pattern — thêm `Contract.ApprovalWorkflowId` + `CurrentApprovalLevelOrder` + `ContractWorkflowService.ApproveV2Async` + `ContractLevelOpinions` (mirror Mig 26)
|
||
- Drop legacy V1: sau khi không còn phiếu pin `WorkflowDefinitionId` → drop `WorkflowDefinitions` + `WorkflowSteps` + `WorkflowStepApprovers` + drop deprecated columns `RejectedAtStepIndex` / `RejectedFromPhase` (Mig cleanup)
|
||
- Drop Mig 15 `PurchaseEvaluationDepartmentOpinions` cho V2 phiếu (sau khi tất cả phiếu V2 chỉ dùng Mig 26 LevelOpinions). Phiếu V1 legacy giữ Mig 15 backward compat.
|
||
|
||
## 15. PE Level Opinions V2 (Migration 26 — 1 bảng mới, Session 19)
|
||
|
||
**Mục đích:** Thay thế Section 5 "Ý kiến 4 phòng ban" CỨNG (Mig 15 — PheDuyet/CCM/MuaHàng/SmPm) → động theo `ApprovalWorkflowsV2` (Mig 22-25). Mỗi (PE × Level) = 1 row sign-off với ý kiến + chữ ký NV.
|
||
|
||
**Bối cảnh decision (đáng giữ nguyên văn cho session sau):** User UAT Section 5 cũ "có 4 box CỨNG" — không scale theo workflow V2 cấu hình động (N Bước × N Cấp × N NV). Q&A 5 câu chốt spec trước code (memory `feedback_drastic_refactor_scope` áp dụng):
|
||
|
||
- **Q1=1B (gắn — KHÔNG rời):** Comment khi NV nhấn Duyệt qua Workflow Panel → auto sync sang OpinionBox của NV đó. Section 5 read-only summary, KHÔNG có form input rời (anti-pattern Mig 15 cũ — user phải double action: duyệt + ký).
|
||
- **Q2=2A+Admin override:** NV chính chủ duyệt (match `ApproverUserId == actor.Id`). Admin override → fallback first level group, lưu `SignedByUserId = admin.Id`. FE banner amber "⚠ Admin <name> duyệt thay" khi `SignedByUserId !== ApproverUserId` → minh bạch ai thực sự duyệt.
|
||
- **Q3=V2 hết:** Phiếu V2 (pin `ApprovalWorkflowId`) → render dynamic. Phiếu V1 legacy (chỉ `WorkflowDefinitionId`) → fallback Mig 15 readOnly (giữ data history, KHÔNG drop). Mig 15 deprecated cho V2 phiếu, drop sau UAT confirm.
|
||
- **Q4=4C+placeholder:** Phase=DaDuyet/TuChoi → khoá hoàn toàn. Comment empty/whitespace → ghi `"(duyệt — không ý kiến)"` placeholder (vẫn tính là sign-off).
|
||
- **Q5=5A grid layout:** Group theo Step (header "Bước N — Phòng X" badge emerald + hint "(N người duyệt)") → grid-cols-2 cho N approvers. "Bước 1 phòng A có 2 NV → 2 box ngang hàng" — wrap row khi N>2.
|
||
|
||
**Bảng `PurchaseEvaluationLevelOpinions`:**
|
||
|
||
```
|
||
Id uniqueidentifier PK
|
||
PurchaseEvaluationId FK Cascade → PurchaseEvaluations
|
||
ApprovalWorkflowLevelId FK Restrict → ApprovalWorkflowLevels (Mig 22)
|
||
Comment nvarchar(2000) -- ý kiến hoặc placeholder
|
||
SignedAt datetime2 NOT NULL -- luôn có khi UPSERT
|
||
SignedByUserId uniqueidentifier NOT NULL -- NV chính chủ HOẶC Admin override
|
||
SignedByFullName nvarchar(200) NOT NULL -- denorm: tránh user xóa/đổi tên
|
||
+ AuditableEntity audit fields (CreatedAt/UpdatedAt/IsDeleted/...)
|
||
|
||
UNIQUE (PurchaseEvaluationId, ApprovalWorkflowLevelId)
|
||
INDEX (ApprovalWorkflowLevelId)
|
||
```
|
||
|
||
**Constraint design rationale (KHÔNG paraphrase):**
|
||
|
||
- **UNIQUE composite (PEId, LevelId):** 1 row per (phiếu × Cấp). Latest write wins khi Service ApproveV2Async UPSERT — phiếu loop qua TraLai → resubmit → cùng NV duyệt lại = OVERWRITE row cũ (không tạo duplicate). Multi-NV cùng Cấp (OR-of-N) = nhiều LevelId distinct → mỗi NV có row riêng nếu duyệt.
|
||
- **FK Cascade Pe:** Xoá phiếu (soft-delete actually) → opinions cascade.
|
||
- **FK Restrict Level:** Admin xoá Level (qua Designer V2) bị chặn nếu opinion tồn tại — protect history sign-off.
|
||
- **`SignedByUserId` KHÔNG nav (denorm `SignedByFullName`):** User có thể bị soft-delete / đổi tên → opinion vẫn render lịch sử đúng tên tại thời điểm ký. Tránh cascade phức tạp khi xoá user.
|
||
|
||
**Service hook pattern (ApproveV2Async):**
|
||
|
||
```csharp
|
||
// Sau khi log approval row → UPSERT opinion cho Cấp hiện tại
|
||
var matchingLevel = pendingLevelGroup
|
||
.FirstOrDefault(l => l.ApproverUserId == actorUserId) // NV chính chủ
|
||
?? pendingLevelGroup.First(); // Admin fallback first
|
||
|
||
var existing = await db.PurchaseEvaluationLevelOpinions
|
||
.FirstOrDefaultAsync(o => o.PEId == pe.Id && o.LevelId == matchingLevel.Id, ct);
|
||
|
||
var normalizedComment = string.IsNullOrWhiteSpace(comment)
|
||
? "(duyệt — không ý kiến)" // Q4 bonus placeholder
|
||
: comment.Trim();
|
||
|
||
// Upsert: existing → update; null → Add new
|
||
```
|
||
|
||
**Reject KHÔNG sync** (Trả lại / Từ chối) — giữ snapshot opinion cũ nếu có.
|
||
|
||
**Lưu ý anti-pattern (so với Mig 15 cũ):**
|
||
|
||
- ❌ Mig 15 dùng endpoint POST `/pe/{id}/opinions` cho user nhập rời + 4 box CỨNG → user phải nhấn 2 action (Duyệt workflow + Ký opinion box) → UX kém + risk inconsistency
|
||
- ✅ Mig 26 dùng Service hook UPSERT khi nhấn Duyệt → 1 action user, sync auto
|
||
|
||
→ Pattern **"derived state qua Service hook, KHÔNG endpoint CRUD riêng"** áp dụng được cho:
|
||
- Audit trail tự động khi save entity
|
||
- Notification interceptor SaveChangesInterceptor
|
||
- Code generation (gen mã HĐ khi DangDongDau) — đã có
|
||
|
||
### Pending Session 20+:
|
||
|
||
- Contract Section 5 V2 dynamic — mirror PE Mig 26 pattern. Tạo `ContractLevelOpinions` (Mig sau Contract V2 wire). Audit-reuse pattern memory `feedback_audit_reuse_before_clone` áp dụng.
|
||
- Drop Mig 15 cho V2 phiếu — cleanup sau UAT confirm.
|
||
- Test V2 Service hook — Domain test ApproveV2 + UPSERT opinion match logic + Admin override match firstLevel + comment empty placeholder.
|
||
|
||
## 16. 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
|