Files
solution-erp/docs/database/schema-diagram.md
pqhuy1987 fbca83264c
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m33s
[CLAUDE] Docs: chốt session Tier 3 feature-complete + versioned workflow
- Session log 2026-04-22-0300 (A→K): attachment, SignalR, form builder,
  PDF, dynamic + versioned workflow, nested menu, 3-panel permissions,
  seed master, brand identity, content polish, Gitea fix
- STATUS: Tier 3 feature-complete snapshot + cumulative stats (24 tables,
  ~50 endpoints, 8 migrations); next-up = UAT + Email SMTP (blocked) +
  rotate creds + SQL backup schedule
- HANDOFF: rewrite brief cho session mới — phase 5 prod done, Tier 3
  đóng gói, quick sanity-check 2 app, versioned workflow quick ref,
  file active hiện trạng, git state
- migration-todos: tick Tier 3 items (attachment/realtime/form builder/
  PDF/dynamic+versioned workflow/nested menu) + thêm iter-3 versioned
  workflow section + post-launch list
- schema-diagram: +5 table (Notifications, WorkflowTypeAssignments,
  WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers); indexes
  mới, cardinality FK restrict cho pinned policy, truy vấn tiêu biểu
- workflow-contract: +section 7bis resolution order, 7ter admin
  designer flow, updated data model + code pointers Tier 3
- PROJECT-MAP: module map post-Tier-3 (3 box mới Notification/
  Attachment/Branding + Infra/DevOps box), API namespace đầy đủ,
  architectural wins 5 điểm
- contract-workflow skill: versioned workflow section, policy
  resolution code snippet, admin designer flow, code pointers Tier 3,
  tier 4+ backlog
- gotchas +7 bẫy mới (#26-32): SignalR WebSocket headers, interceptor
  2-phase pattern, LibreOffice mirror 404, PS 5.1 UTF-16 GITHUB_PATH,
  PS 5.1 diacritics parse, Dialog size TS, NavLink end query-params

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 10:25:02 +07:00

18 KiB

Schema Diagram — Luồng DB SOLUTION_ERP

ERD đầy đủ + mối quan hệ 24 table sau Tier 3 (Notifications + Versioned workflows). 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)AuditingInterceptor chuyển EntityState.DeletedModified, 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)

-- 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

Tổng: 24 bảng (+ __EFMigrationsHistory hệ thống).

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)

10. Liên quan