# Database Guide — SOLUTION_ERP

> **DB engine:** SQL Server 2022 (LocalDB dev, full instance prod)
> **ORM:** EF Core 10 Code-First migrations — **91 bảng, 42 migration** (Init → AddLeaveBalances)
> **DbContext:** [`ApplicationDbContext`](../../src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs) extends `IdentityDbContext<User, Role, Guid>`
> **Current schema + ERD:** xem [`schema-diagram.md`](schema-diagram.md) (luôn cập nhật)
>
> File này = conventions + migration workflow + cheatsheet. Section "Schema dự kiến" (§4) là Phase 0 historical — actual schema xem `schema-diagram.md`.

## 1. Connection strings

| Env | ConnectionString | DB name |
|---|---|---|
| Dev (default) | `Server=(localdb)\MSSQLLocalDB;Database=SolutionErp_Dev;Trusted_Connection=True` | `SolutionErp_Dev` |
| Design-time (EF CLI) | same server, `Database=SolutionErp_Design` | `SolutionErp_Design` |
| Prod | `Server=<prod>;Database=SolutionErp;User=<>;Password=<>;TrustServerCertificate=true` | `SolutionErp` |

Cả 3 DB dùng chung schema, khác tên → có thể drop `SolutionErp_Design` không mất dữ liệu.

## 2. Conventions

### Naming

| Item | Rule | Ví dụ |
|---|---|---|
| Schema | 1 schema duy nhất: `dbo` | — |
| Table | PascalCase tiếng Anh, số nhiều | `Contracts`, `Suppliers`, `ContractApprovals` |
| Column | PascalCase | `FullName`, `CreatedAt`, `SupplierId` |
| PK | `Id` | `Guid` (không auto-increment) |
| FK | `{EntityName}Id` | `SupplierId`, `ContractId` |
| Index | `IX_{Table}_{Col1}_{Col2}` | `IX_Contracts_Phase_SupplierId` |
| Unique | `UX_{Table}_{Col}` | `UX_Suppliers_Code` |
| Migration | `YYYYMMDDHHMMSS_{Name}` (auto) | `20260421034520_Init` |

### Data types

| C# type | SQL type | Notes |
|---|---|---|
| `Guid` | `uniqueidentifier` | PK + FK default |
| `string` | `nvarchar(max)` default | Luôn set `HasMaxLength(n)` trong config |
| `DateTime` | `datetime2` | UTC — luôn qua `IDateTime.UtcNow` |
| `decimal` (tiền) | `decimal(18,2)` | `HasPrecision(18, 2)` |
| `int` enum | `int` | `HasConversion<int>()` |
| `bool` | `bit` | — |
| JSON blob | `nvarchar(max)` | `HasColumnType("nvarchar(max)")` — dùng `HasConversion` serialize |

### Audit fields (mọi entity extend `BaseEntity`)

```csharp
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime CreatedAt { get; set; }      // Set by AuditingInterceptor
public DateTime? UpdatedAt { get; set; }     // Set by AuditingInterceptor
public Guid? CreatedBy { get; set; }         // From ICurrentUser.UserId
public Guid? UpdatedBy { get; set; }         // From ICurrentUser.UserId
```

### Soft delete (entity extend `AuditableEntity`)

```csharp
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public Guid? DeletedBy { get; set; }
```

[`AuditingInterceptor`](../../src/Backend/SolutionErp.Infrastructure/Persistence/Interceptors/AuditingInterceptor.cs) tự động:
- `Added` entity → set `CreatedAt`, `CreatedBy`
- `Modified` entity → set `UpdatedAt`, `UpdatedBy`
- `Deleted` entity kiểu `AuditableEntity` → convert sang `Modified` + set `IsDeleted=true`, `DeletedAt`, `DeletedBy` (soft delete)

**Query filter soft delete:** config `modelBuilder.Entity<T>().HasQueryFilter(e => !e.IsDeleted)` cho mọi `AuditableEntity`. Sẽ làm Phase 1 đợt 2.

## 3. Schema hiện tại (sau migration `Init`)

### Identity tables (đã có sau migration 1)

```mermaid
erDiagram
    Users ||--o{ UserRoles : has
    Roles ||--o{ UserRoles : has
    Users ||--o{ UserClaims : has
    Users ||--o{ UserLogins : has
    Users ||--o{ UserTokens : has
    Roles ||--o{ RoleClaims : has

    Users {
        uniqueidentifier Id PK
        nvarchar FullName "200"
        nvarchar Email
        nvarchar UserName
        nvarchar PasswordHash
        nvarchar RefreshToken "512"
        datetime2 RefreshTokenExpiresAt
        bit IsActive
        datetime2 CreatedAt
        datetime2 UpdatedAt
        bit EmailConfirmed
        bit LockoutEnabled
        datetime2 LockoutEnd
        int AccessFailedCount
    }
    Roles {
        uniqueidentifier Id PK
        nvarchar Name
        nvarchar NormalizedName
        nvarchar Description "500"
        datetime2 CreatedAt
    }
    UserRoles {
        uniqueidentifier UserId PK,FK
        uniqueidentifier RoleId PK,FK
    }
    UserClaims {
        int Id PK
        uniqueidentifier UserId FK
        nvarchar ClaimType
        nvarchar ClaimValue
    }
    UserLogins {
        nvarchar LoginProvider PK
        nvarchar ProviderKey PK
        uniqueidentifier UserId FK
    }
    UserTokens {
        uniqueidentifier UserId PK,FK
        nvarchar LoginProvider PK
        nvarchar Name PK
        nvarchar Value
    }
    RoleClaims {
        int Id PK
        uniqueidentifier RoleId FK
        nvarchar ClaimType
        nvarchar ClaimValue
    }
```

### Seed data hiện tại

[`DbInitializer`](../../src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs) auto-run khi API start:

**12 role:**

| Role | Ý nghĩa (theo workflow-contract.md) |
|---|---|
| `Admin` | Quản trị hệ thống |
| `Drafter` | Người soạn thảo (QS/NV.PB) |
| `DeptManager` | Trưởng Phòng ban (TPB) |
| `ProjectManager` | Giám đốc Dự án (PM) |
| `Procurement` | Phòng Cung ứng (PRO) |
| `CostControl` | Phòng Kiểm soát Chi phí (CCM) |
| `Finance` | Phòng Tài chính (FIN) |
| `Accounting` | Phòng Kế toán (ACT) |
| `Equipment` | Phòng Thiết bị (EQU) |
| `Director` | Ban Giám đốc (BOD) |
| `AuthorizedSigner` | Người được ủy quyền ký (NĐUQ) |
| `HrAdmin` | Nhân sự - Hành chính (HRA) |

**1 admin user:**

| Field | Value |
|---|---|
| Email | `admin@solutions.com.vn` |
| Password | `Admin@123456` |
| FullName | `Administrator` |
| Roles | `[Admin]` |

## 4. Schema dự kiến

### Phase 1 đợt 2 — Master data

```mermaid
erDiagram
    Suppliers {
        uniqueidentifier Id PK
        nvarchar Code "50 UNIQUE"
        nvarchar Name "200"
        nvarchar TaxCode "20"
        nvarchar Phone "20"
        nvarchar Email "100"
        nvarchar Address "500"
        int Type "NCC/NTP/TD/DVDV"
        datetime2 CreatedAt
        datetime2 UpdatedAt
        uniqueidentifier CreatedBy FK
        uniqueidentifier UpdatedBy FK
        bit IsDeleted
    }
    Projects {
        uniqueidentifier Id PK
        nvarchar Code "50 UNIQUE"
        nvarchar Name "200"
        date StartDate
        date EndDate
        uniqueidentifier ManagerUserId FK
    }
    Departments {
        uniqueidentifier Id PK
        nvarchar Code "50 UNIQUE"
        nvarchar Name "200"
        uniqueidentifier ManagerUserId FK
    }
    Users ||--o{ Projects : manages
    Users ||--o{ Departments : manages
```

### Phase 1 đợt 2 — Permission Matrix

```mermaid
erDiagram
    MenuItems ||--o{ Permissions : has
    Roles ||--o{ Permissions : has

    MenuItems {
        nvarchar Key PK "PascalCase, vd Contracts"
        nvarchar Label "200 tieng Viet"
        nvarchar ParentKey FK "NULL neu root"
        int Order
        nvarchar Icon "50 lucide icon name"
    }
    Permissions {
        uniqueidentifier Id PK
        uniqueidentifier RoleId FK
        nvarchar MenuKey FK
        bit CanRead
        bit CanCreate
        bit CanUpdate
        bit CanDelete
    }
```

Unique key: `(RoleId, MenuKey)`.

### Phase 3 — Contract + Workflow

```mermaid
erDiagram
    Contracts ||--o{ ContractApprovals : has
    Contracts ||--o{ ContractComments : has
    Contracts ||--o{ ContractAttachments : has
    Contracts }o--|| Suppliers : references
    Contracts }o--|| Projects : references
    Contracts }o--|| Departments : drafted_in
    Contracts }o--|| ContractTemplates : uses

    Contracts {
        uniqueidentifier Id PK
        nvarchar MaHopDong "100 nullable until Phase5"
        int Type "ContractType enum"
        int Phase "ContractPhase enum 9 state"
        uniqueidentifier SupplierId FK
        uniqueidentifier ProjectId FK
        uniqueidentifier DepartmentId FK
        uniqueidentifier DrafterUserId FK
        uniqueidentifier TemplateId FK
        decimal GiaTri "18 2"
        bit BypassProcurementAndCCM
        datetime2 SlaDeadline "khi nao phase hien tai het han"
        nvarchar DraftData "nvarchar max JSON field values"
    }
    ContractApprovals {
        uniqueidentifier Id PK
        uniqueidentifier ContractId FK
        int Phase
        uniqueidentifier ApproverUserId FK
        datetime2 ApprovedAt
        int Decision "Pending/Approve/Reject/AutoApprove"
        nvarchar Comment "1000"
    }
    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"
        nvarchar Purpose "50 Scan/Export/Template"
        datetime2 UploadedAt
    }
    ContractTemplates {
        uniqueidentifier Id PK
        nvarchar FormCode "20 UNIQUE vd FO-002.05"
        nvarchar Name "200"
        int ContractType
        nvarchar TemplatePath "500"
        nvarchar FieldSpec "nvarchar max JSON"
        bit IsActive
    }
    ContractClauses {
        uniqueidentifier Id PK
        nvarchar Code "20 UNIQUE vd FO-002.04"
        nvarchar Name "200"
        nvarchar Content "nvarchar max rich text"
        int Version
        bit IsActive
    }
```

### Phase 3 — Contract Code Generator

`ContractCodeSequence` bảng đơn giản để tránh race condition khi gen seq:

```sql
CREATE TABLE dbo.ContractCodeSequences (
    Prefix NVARCHAR(100) NOT NULL PRIMARY KEY,  -- "FLOCK 01/HĐTP/SOL&HD" hoặc "2026/NCC/SOL&AKATI"
    LastSeq INT NOT NULL,
    UpdatedAt DATETIME2 NOT NULL
);
```

Logic trong `ContractCodeGenerator`:
1. Begin transaction `SERIALIZABLE`
2. `SELECT LastSeq FROM ContractCodeSequences WITH (UPDLOCK, HOLDLOCK) WHERE Prefix = @prefix`
3. Nếu không có → INSERT với `LastSeq = 1`
4. Nếu có → `UPDATE ContractCodeSequences SET LastSeq = LastSeq + 1`
5. Return `{prefix}/{LastSeq:D2}`
6. Commit

## 5. Migration workflow

### Tạo migration mới

```powershell
cd D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP
dotnet ef migrations add <Name> `
  --project src\Backend\SolutionErp.Infrastructure `
  --startup-project src\Backend\SolutionErp.Api `
  --output-dir Persistence\Migrations
```

Ví dụ Phase 1 đợt 2:
```
dotnet ef migrations add AddMasterData
dotnet ef migrations add AddPermissions
dotnet ef migrations add AddContractDraft
```

Phase 3:
```
dotnet ef migrations add AddContractWorkflow
dotnet ef migrations add AddContractCodeSequence
```

### Apply migration

```powershell
dotnet ef database update `
  --project src\Backend\SolutionErp.Infrastructure `
  --startup-project src\Backend\SolutionErp.Api
```

Hoặc tự động khi API start (đã config trong `DbInitializer.InitializeAsync` → `db.Database.MigrateAsync()`).

### Revert migration

```powershell
# Rollback 1 bước
dotnet ef database update <PreviousMigrationName>

# Xóa file migration chưa apply
dotnet ef migrations remove
```

### Revert DB về trạng thái clean

```sql
-- Chạy trong SSMS hoặc sqlcmd
DROP DATABASE SolutionErp_Dev;
-- Hoặc giữ DB, chỉ xóa data:
EXEC sp_MSforeachtable 'DELETE FROM ?';
```

Rồi chạy lại API → tự migrate + seed.

## 6. Index strategy (Phase 3-4 optimize)

### Hot queries cần index

| Query | Index đề xuất |
|---|---|
| Inbox — HĐ theo phase + role của user | `IX_Contracts_Phase_IsDeleted` |
| List HĐ theo NCC | `IX_Contracts_SupplierId_IsDeleted` |
| List HĐ theo dự án | `IX_Contracts_ProjectId_IsDeleted` |
| Approval của 1 HĐ | `IX_ContractApprovals_ContractId_Phase` |
| Comment thread | `IX_ContractComments_ContractId_CreatedAt` |
| Audit log | `IX_AuditLogs_EntityId_CreatedAt` |

### Index unique

| Unique | Cột |
|---|---|
| `UX_Suppliers_Code` | `Code` |
| `UX_Projects_Code` | `Code` |
| `UX_Departments_Code` | `Code` |
| `UX_Contracts_MaHopDong` | `MaHopDong` (WHERE `MaHopDong IS NOT NULL` — filtered) |
| `UX_Users_Email` | đã có qua Identity |
| `UX_MenuItems_Key` | PK |
| `UX_Permissions_Role_Menu` | `(RoleId, MenuKey)` |

## 7. Backup & restore (prod)

### Backup daily (SQL Agent job)

```sql
BACKUP DATABASE SolutionErp
TO DISK = 'D:\Backups\SolutionErp_full.bak'
WITH INIT, COMPRESSION, CHECKSUM;
```

### Backup log mỗi 15 phút (nếu full recovery model)

```sql
BACKUP LOG SolutionErp
TO DISK = 'D:\Backups\SolutionErp_log.trn'
WITH INIT;
```

### Restore

```sql
RESTORE DATABASE SolutionErp
FROM DISK = 'D:\Backups\SolutionErp_full.bak'
WITH REPLACE, RECOVERY;
```

Chi tiết runbook ở Phase 5 `docs/guides/cicd.md`.

## 8. Common pitfalls

- **Quên `HasMaxLength`** → `nvarchar(max)`, gây bloat + index lỗi. Luôn set.
- **Lưu DateTime local thay vì UTC** → sai timezone khi deploy server khác múi giờ. Dùng `IDateTime.UtcNow` (tiêm qua DI), KHÔNG `DateTime.Now`.
- **Soft delete quên filter** → user thấy data đã xóa. Luôn `HasQueryFilter(e => !e.IsDeleted)` ở config.
- **FK không có index** → slow join. EF Core tự tạo index cho FK, nhưng vẫn phải verify sau migration.
- **Migration bị commit thiếu `.Designer.cs`** → team khác không apply được. Commit cả 3 file (`{name}.cs`, `{name}.Designer.cs`, `ApplicationDbContextModelSnapshot.cs`).
- **Chạy `dotnet ef` khi API đang chạy** → lock conflict. Dừng API trước khi migrate ở dev.
- **Gen mã HĐ ngoài transaction** → race condition. Luôn dùng `ContractCodeGenerator` với lock.

## 9. Quick SQL cheatsheet

```sql
-- Xem 10 HĐ mới nhất
SELECT TOP 10 Id, MaHopDong, Phase, CreatedAt FROM Contracts ORDER BY CreatedAt DESC;

-- Đếm HĐ theo phase
SELECT Phase, COUNT(*) FROM Contracts WHERE IsDeleted = 0 GROUP BY Phase;

-- List user + roles
SELECT u.Email, u.FullName, r.Name AS Role
FROM Users u
LEFT JOIN UserRoles ur ON u.Id = ur.UserId
LEFT JOIN Roles r ON ur.RoleId = r.Id
ORDER BY u.Email;

-- Reset admin password (emergency)
-- Cách an toàn: không edit PasswordHash trực tiếp, dùng UserManager.ResetPasswordAsync qua API

-- Clear toàn bộ dev data (giữ migrations)
EXEC sp_MSforeachtable 'DELETE FROM ?';
```
