18 KiB
SUB-INVEST-DB (Fable deep-pass, lens DATABASE) — S182 KHKK AWF mirror NCC
Đề bài: 8 workflow "Duyệt KHKK — Nhóm 1..8" cần đủ chức năng như "Duyệt NCC" trên approval-workflows-v2. Câu hỏi trung tâm: schema hiện tại ĐỦ chưa hay cần mig mới? Kỳ vọng "máy rộng UI hẹp" ⇒ 0 mig — PHẢI CHỨNG. Trạng thái file: đổ ruột liên tục (chống-#53). Khu chưa đo ghi
CHƯA ĐO.
§1 Schema V2 — cột thật 3 bảng (từ ModelSnapshot + entity)
Nguồn: ApplicationDbContextModelSnapshot.cs:128-313 + src/Backend/SolutionErp.Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs (1 file chứa cả 3 entity).
ApprovalWorkflows (snapshot :128-187)
| Cột | Kiểu | Ghi chú |
|---|---|---|
| Id | uniqueidentifier PK | BaseEntity |
| Code | nvarchar(100) NOT NULL | "QT-DN-V2-001" |
| Version | int | monotonic per Code |
| ApplicableType | int | enum 1..10 — ContractSigningPlan=10 (ApprovalWorkflow.cs:70) |
| Name | nvarchar(200) NOT NULL | |
| Description | nvarchar(1000) NULL | |
| IsActive | bit | |
| ActivatedAt | datetime2 NULL | |
| IsUserSelectable | bit | Mig 25 — Ghim/Bỏ ghim |
| CeoApprovalThreshold | decimal(18,2) NULL | Mig 53 — ngưỡng gói CEO (PE semantic) |
| CreatedAt/CreatedBy/UpdatedAt/UpdatedBy | BaseEntity audit |
Index: (ApplicableType, IsActive) + UNIQUE (Code, Version) (:181-184). 🔴 KHÔNG có IsDeleted — ApprovalWorkflow : BaseEntity (entity :21), KHÔNG AuditableEntity ⇒ 0 soft-delete, 0 query-filter trên cả 3 bảng V2.
ApprovalWorkflowSteps (snapshot :274-313)
Id · ApprovalWorkflowId (FK) · Order int · Name nvarchar(200) NOT NULL · DepartmentId uniqueidentifier NULL (hint) · audit 4 cột. Index: DepartmentId + (ApprovalWorkflowId, Order).
ApprovalWorkflowLevels (snapshot :189-272)
Id · ApprovalWorkflowStepId (FK) · Order int (Cấp, OR-of-N rows cùng Order) · Name nvarchar(200) NULL · ApproverUserId NOT NULL (1 row = 1 NV slot) · audit 4 cột · 9 cờ bit Allow*. Index: ApproverUserId + (ApprovalWorkflowStepId, Order).
§1b Bảng ánh xạ 9 checkbox ảnh ↔ cột — 9/9 CÓ ĐỦ, 0 cột thiếu
Per-NV = per-row ApprovalWorkflowLevels (mỗi row = 1 ApproverUserId ⇒ cờ trên row chính là cờ per-NV).
| # | Checkbox (ảnh YC-013) | Cột thật | Default (snapshot) | Mig gốc |
|---|---|---|---|---|
| 1 | Trả về 1 Cấp trước | AllowReturnOneLevel |
false (:220-223) | 29 |
| 2 | Trả về 1 Bước trước | AllowReturnOneStep |
false (:225-228) | 29 |
| 3 | Trả về Người chỉ định | AllowReturnToAssignee |
false (:230-233) | 29 |
| 4 | Trả về Drafter (mặc định) | AllowReturnToDrafter |
true (:235-238) | 29 |
| 5 | Sửa Section 2 | AllowApproverEditDetails |
false (:205-208) | 29 |
| 6 | Sửa Section ngân sách | AllowApproverEditBudget |
false (:200-203) | 30 |
| 7 | Duyệt thẳng Cấp cuối | AllowApproverSkipToFinal |
false (:215-218) | 31 |
| 8 | Duyệt là KẾT THÚC | AllowApproverFinalize |
false (:210-213) | 58 |
| 9 | XÓA phiếu | AllowApproverDelete |
false (:195-198) | 68 |
Cột nằm trên bảng V2 DÙNG CHUNG mọi ApplicableType ⇒ workflow KHKK (ApplicableType=10) lưu được cả 9 cờ ngay hôm nay, 0 mig. ⚠️ Ranh giới lens: cột TỒN TẠI ≠ service KHKK ĐỌC cờ (semantic #3 đọc PeLevelOpinions, #5/#6 đọc section PE — entity comment ApprovalWorkflow.cs:119-173; KHKK service có honor không = lens BE).
§2 Versioning — đọc từ ApprovalWorkflowV2AdminFeatures.cs (KHÔNG đoán từ UI)
- Lưu version: cùng
Codenhiều row,Version intmonotonic; UNIQUE (Code, Version) (snapshot :183-184). "Archived" KHÔNG có cột riêng — chỉ làIsActive=false(nhãn UI suy từ bit). - Tạo phiên bản mới =
CreateAwDefinitionCommandHandler(:326-): INSERT row mớiVersion = max(Code)+1(:334-337),IsActive=true, IsUserSelectable=true(:352-353). 🔴 LANDMINE cho KHKK: deactivate cũ chạy theoApplicableTypechứ KHÔNG theo Code —Where(w.ApplicableType == typeEnum && w.IsActive)setIsActive=falseTẤT CẢ (:339-343). Với DuyetNcc (1 code/type) đúng; với KHKK 8 code chung type=10 ⇒ tạo version mới choKHKK-N1sẽ tắt IsActive của cả KHKK-N2..N8. Chính seeder đã né bẫy same-class ở check idempotent (DbInitializer.cs:540-542"check theo type sẽ chặn 7 nhóm còn lại") nhưng Create handler CHƯA vá — vá-tệp-A-quên-tệp-B. Fix = code BE (đổi scope deactivate theo Code), 0 mig. - Sửa tại chỗ =
UpdateAwDefinitionCommandHandler(:515-900): UPDATE in-place ĐÚNG row + diff Levels theoLevel.Id— GIỮ NGUYÊN Id (:561-634, "F-3 CẤM delete-then-insert") ⇒ không đẻ Id mới, không mồ-côi con trỏ (khác nỗi lo F17/S155 — máy Update nay ĐÃ TỒN TẠI, memory cũ "WF-V2 0 lệnh Update" STALE). Cấm in-place: đổi Code/ApplicableType/CeoThreshold (:539-550), thêm/bớt/đảo Bước (:553-574), đổi Order dòng (:593), toggle 2 cờ đổi-đường-đi F5/F2 (:598-603), bỏ người giữ F5/F2 (:627-632), làm rỗng Cấp (:639-647) — tất cả 409 + gợi ý tạo version mới. 7 cờ còn lại + đổi người + thêm/bỏ người trong Cấp = hợp lệ in-place (:789-797). - Guard chữ ký khi bỏ/đổi người: 2 tầng quét CẢ 8 bảng LevelOpinions gồm
ContractSigningPlanLevelOpinions— count-sống (:961-965) + retained (:1033-1038); transaction rào (:744-745, :895-899). ⇒ máy sửa-tại-chỗ ĐÃ KHKK-aware ở tầng guard. - ⚠️ Vết changelog khi sửa-tại-chỗ chỉ ghi cho PE (
ApplicableType ∈ {DuyetNcc, DuyetNccPhuongAn}:863-892); KHKK khai-giới-hạn defer F-6b (:857-862) — bảngContractSigningPlanChangelogsĐÃ TỒN TẠI (Mig 69) ⇒ wire = code BE thuần, 0 mig. - Ghim/Bỏ ghim =
SetAwUserSelectableCommandHandler(:1173-1185): flip 1 bit, generic mọi type — KHKK dùng ngay. - Xoá version =
DeleteAwDefinitionCommandHandler(:1193-1205): HARD delete (Remove+ BaseEntity không soft-delete), 0 guard usage — comment tự khai ":1187-1189 Sau UAT … cần check usage trước khi delete". Phiếu pin (PE/HĐ/KHKK Restrict) hoặc LevelOpinion tồn tại ⇒ SqlException 547 → 500. Version không ai đụng ⇒ xoá sạch (cascade Steps→Levels).
§3 KHKK wiring — Mig 70/71 + ContractSigningPlans pin/pointer/finalize-flag
FK map V2 (snapshot :6210-6323) — quyết định risk xoá
| Quan hệ | Behavior |
|---|---|
| Steps → ApprovalWorkflows | Cascade (:6229-6233) |
| Levels → Steps | Cascade (:6212-6216) |
| Levels.ApproverUserId → Users | Restrict (:6218-6222) |
| Steps.DepartmentId → Departments | Restrict (:6235-6238) |
ContractSigningPlans.ApprovalWorkflowId → ApprovalWorkflows |
Restrict (:6245-6248) |
Contracts.ApprovalWorkflowId → |
Restrict (:6327-6330) |
PurchaseEvaluations.ApprovalWorkflowId → |
Restrict (:6755-6758) |
ContractSigningPlanLevelOpinions.ApprovalWorkflowLevelId → Levels |
Restrict (:6297-6301) |
ContractSigningPlanApprovals.ApprovalWorkflowLevelId |
⚠️ LOOSE Guid — 0 FK (cột + index :416/:455 nhưng relationship section :6251-6260 chỉ có 1 HasOne về Plan Cascade) |
⇒ Hard-delete 1 workflow version bị DB CHẶN (SqlException 547) nếu: (a) bất kỳ phiếu PE/HĐ/KHKK pin nó, HOẶC (b) bất kỳ LevelOpinion (PE/HĐ/KHKK) trỏ Level của nó — cascade Steps→Levels đụng Restrict của LevelOpinions.
Mig 71 AddKhkkApprovalGroupCatalogAndFinalizeRuntime (20260731085624_*.cs)
- KHÔNG có bảng catalog-nhóm-duyệt riêng — "catalog" trong tên mig = gắn
ContractSigningPlanLines.CatalogEntryId(loose-Guid, KHÔNG FK, :70-74) +TenHangMucdenorm (:76-81) trỏ vềContractCatalogEntries(Mig 70, bảng danh mục 86 dòng). ContractSigningPlans.ApprovalGroup int NOT NULL default 0+ backfill vô-điều-kiệnSET = 1(:47-61) — nhóm duyệt 1..8 lưu TRÊN PHIẾU dạng int trần (0 bảng, 0 FK, 0 CHECK constraint 1..8).EndedByLevelFinalize bitruntime-flag (:63-68) — ghost-window khai trước, K3 tiêu thụ.- Index-swap: UNIQUE
(PlanId, SupplierId)→ UNIQUE(PlanId, SupplierId, CatalogEntryId)filtered[IsDeleted]=0(:43-45, :93-98). Down có bẫy F-8 (:132-141).
ContractSigningPlans pin + pointer (snapshot :315-408) — mirror ĐỦ khuôn PE
ApprovalWorkflowId Guid? (pin, Restrict) · CurrentWorkflowStepIndex int? (:336) · CurrentApprovalLevelOrder int? (:333) · EndedByLevelFinalize bit (:351) · ApprovalGroup int (:321) · Phase int · AuditableEntity (IsDeleted/DeletedAt/DeletedBy — phiếu SOFT-delete, ≠ workflow) · UNIQUE filtered MaKeHoach (:395-397) · index (ApprovalGroup, Phase) (:403).
8 workflow nhóm = 8 row ApprovalWorkflows phân loại thế nào?
- Seed
SeedKhkkGroupWorkflowsAsync(DbInitializer.cs:549-687): 8 rowCode="KHKK-N{n}"n=1..8,Version=1,ApplicableType=10,Name="Duyệt KHKK — Nhóm {n}",IsActive=true,IsUserSelectable=true(:609-625). 3 Bước PRO→CCM→BOD, Cấp OR-of-N, riêng Cấp 2 CCM (a Chương)AllowApproverFinalize=true(:575-579, :655). Idempotent PER-CODE (:610) — cố ý né bẫy check-theo-type (:540-542). Resolve người theo EMAIL, thiếu user → bỏ slot + log, workflow 0 bước vẫn TẠO (:544-548). - Liên kết nhóm↔workflow = QUY ƯỚC CODE, không phải cột/FK: phiếu chốt
ApprovalGrouplúc tạo (default 1, validator 1..8ContractSigningPlanFeatures.cs:415-418, :461); rào (v) regex^KHKK-N([1-8])$trên Code của workflow user pick — lệch nhóm = 409, Code NGOÀI họ (type-10 chung) cho qua (:475-486).ContractCatalogEntry.ApprovalGroup int1..8 (validatorContractCatalogFeatures.cs:120, KHÔNG CHECK constraint) quyết nhóm của hạng mục; line-gán guard khớp nhóm phiếu (:691, :802). - Con trỏ duyệt KHKK: service
ContractSigningPlanWorkflowService.csGHI đủ cặpCurrentWorkflowStepIndex(0-based index list sorted) +CurrentApprovalLevelOrder(1-based) (:201-202, :295, :309-310) +EndedByLevelFinalize=truenhánh finalize (:275-282 — ghost-window Mig 71 ĐÃ ĐÓNG, K3 live) + null-out khi terminal (:323-324). Đọc pointer từ CHÍNH workflow đã pin, KHÔNG đụngIsActive(:422-446, có bounds-check) ⇒ version Archived (IsActive=false) không làm phiếu treo kẹt. - Mig 72
AddMaHangMucToContractSigningPlanLines(20260806070242): +MaHangMuc nvarchar(20)denorm + backfill từ catalog Code, idempotent (:13-34) — mig CUỐI trên đĩa.
§4 Dữ liệu thật LocalDB Dev (sqlcmd -E, đo 2026-08-08)
| Phép đo | Kết quả |
|---|---|
ApprovalWorkflows theo type |
chỉ type {3,4,5,6,7,9} × 1 row (6 seeder mẫu). Type 1/2 (DuyetNcc): 0 row. Type 10 (KHKK): 0 row |
8 workflow KHKK-N* |
0/8 trên Dev local — seeder idempotent sẽ tạo ở lần boot API kế; QT-DN-V2-001 v01-v04 là dữ liệu PROD (admin tạo tay), local không có |
ContractSigningPlans |
0 phiếu · 0 LevelOpinions · 0 Approvals |
__EFMigrationsHistory Dev |
top = 20260731085624 (Mig 71) — Mig 72 trên đĩa CHƯA áp |
__EFMigrationsHistory Design |
top = Mig 71 — cùng drift |
⇒ Câu "8 row đủ chưa, IsUserSelectable bao nhiêu" trên PROD: CHƯA ĐO (SSH chết khi load SQL-client — S134/S148/S177). Local suy được từ seed-code: lần boot prod sau commit K3 đã tạo 8 row IsActive=1, IsUserSelectable=1; ảnh sidebar YC-013 hiện 8 mục = seed đã chạy prod. Query bàn giao implementer chạy trên prod:
SELECT Code, Version, IsActive, IsUserSelectable FROM ApprovalWorkflows WHERE ApplicableType=10 ORDER BY Code, Version;
SELECT ApprovalGroup, Phase, COUNT(*) n FROM ContractSigningPlans GROUP BY ApprovalGroup, Phase;
SELECT COUNT(*) FROM ContractSigningPlanLevelOpinions;
§5 Verdict migration + risk
VERDICT: 0 MIGRATION. Schema V2 dùng chung đã đủ 100% cho mirror
- 9/9 checkbox = 9 cột bit có sẵn trên
ApprovalWorkflowLevels(§1b), bảng dùng chung mọi ApplicableType. - Versioning v01→v04 + Archived: UNIQUE
(Code, Version)+IsActivebit — đã phục vụ đúng cơ chế này choQT-DN-V2-001v04;KHKK-N{n}là Code như mọi Code. - Ghim =
IsUserSelectablesẵn; Sửa-tại-chỗ = máy Update diff-by-Id ĐÃ KHKK-aware ở guard chữ ký (3/3 site :961/:1033/:1099); Xoá = máy Delete sẵn (thiếu guard là thiếu CODE, không thiếu cột). - Pin + con trỏ + finalize-flag phiếu KHKK: đủ từ Mig 69/71, service đã ghi/đọc sống.
- Hạng-mục/nhóm:
ApprovalGroup(phiếu + catalog) + regex Code — 0 nhu cầu cột mới.
Nhưng 3 QUẢ MÌN CODE-LEVEL lộ ra từ scan DB-semantics (fix = BE code, vẫn 0 mig)
| # | Mìn | Bằng chứng | Hệ quả nếu bỏ qua |
|---|---|---|---|
| M1 | Create-new-version deactivate theo ApplicableType — tạo v02 cho KHKK-N1 sẽ IsActive=false cả active-version của N2..N8 (8 Code chung type=10; giả định 1-Code-1-type vỡ) |
ApprovalWorkflowV2AdminFeatures.cs:339-343; đối chiếu seeder đã né same-class DbInitializer.cs:540-542 |
Overview 7 nhóm kia mất "Active" (thành Archived-oan); mọi consumer lọc IsActive theo type sai |
| M2 | Overview API Active = FirstOrDefault(IsActive) per-TYPE + KHÔNG có param Code — type 10 có 8 chuỗi version trộn chung 1 History, Active lấy 1 trong 8 |
:107-109 (query 2 param), :203-212 |
Trang admin 8 mục sidebar không tách được version-list per nhóm nếu FE không tự group by Code |
| M3 | Delete = hard-delete 0 guard usage — phiếu pin (Restrict) / opinion (Restrict) → SqlException 547 → 500 thô; version rác thì xoá êm | :1187-1204; FK snapshot :6245-6248, :6297-6301 |
UX 500 khó hiểu thay 409; data KHÔNG mất (DB chặn được) |
Risk integrity/concurrency chi tiết
- Sửa workflow đang có phiếu KHKK treo: an toàn cấu trúc — mọi mutation đổi-đường-đi bị 409; đổi người/cờ giữ nguyên
Level.Id⇒ pointer (StepIndex, LevelOrder) vẫn giải được; guard chữ ký quét cảContractSigningPlanLevelOpinions(sống + retained); transaction rào :744/:899. Lỗ nhỏ: vết sửa KHÔNG ghi vàoContractSigningPlanChangelogs(chỉ PE :863-892, defer F-6b) — phiếu KHKK đang chạy bị sửa quy trình mà lịch sử phiếu im lặng. - Xoá version Archived có phiếu treo/xong trỏ vào: bị Restrict chặn (pin
ApprovalWorkflowId) — kể cả phiếu soft-deleted (row còn, FK còn). Pointer phiếu KHÔNG chết vì không xoá được. RiêngContractSigningPlanApprovals.ApprovalWorkflowLevelIdLOOSE (0 FK) — nếu tương lai có đường hard-delete phiếu/level thì history giữ Guid treo (hiện không có đường đó). - Race 2 admin cùng "Tạo version mới" cùng Code: UNIQUE (Code,Version) đỡ — kẻ thua 2601/2627 → 500 (không silent-dup). Chấp nhận được cho màn quản trị.
- Race tạo 2 phiếu cùng (PE, nhóm): rào handler check-then-insert, CỐ Ý không filtered-unique (
ContractSigningPlanFeatures.cs:456-460, gate-K2 F-7) — giữ nguyên quyết định cũ, ngoài scope.
§6 Đề xuất wave DB
- W-DB-0 (prep local, bắt buộc trước test):
dotnet ef database update×2 DB (Dev + Design đang kẹt Mig 71, đĩa 72) + boot API 1 lần đểSeedKhkkGroupWorkflowsAsyncđẻ 8 row local. 0 mig mới. - W-DB-1 (BE, đi cùng wave mirror): vá M1 — scope deactivate trong
CreateAwDefinitionCommandHandlerđổiApplicableType→Code(hoặc Code∧Type); test: tạo v2KHKK-N1⇒ N2..N8IsActivebất biến + case DuyetNcc regression. - W-DB-2 (BE): overview/API thêm chiều Code (param
code=hoặc group per-Code trongAwTypeSummaryDto) để 8 trang nhóm đọc đúng chuỗi version của mình. - W-DB-3 (BE, khuyến nghị): guard lịch sự cho Delete — pre-count pin (PE/HĐ/KHKK/5 office) + LevelOpinions → 409 kể tên hồ sơ (tái dùng khuôn
CountLiveOpinionsAsync), thay 547→500. - W-DB-4 (BE, optional): wire changelog KHKK khi sửa-tại-chỗ (bảng
ContractSigningPlanChangelogssẵn — mirror khối PE :863-892). - Verify prod (bàn giao implementer): 3 query §4.
§7 Surprises ngoài đề bài
- Local DB drift: Mig 72 committed trên đĩa (2026-08-06) nhưng CẢ Dev lẫn Design local dừng Mig 71 — pattern committed-but-unapplied-local; test wave nào chạm Lines sẽ lệch schema nếu quên update.
- Dev local 0 row type-1/2 (DuyetNcc) — workflow NCC prod tạo tay qua UI, không seed ⇒ mọi test local về trang DuyetNcc cần tự tạo workflow trước.
- Memory cũ S155 "WF-V2 KHÔNG có lệnh Update" đã STALE —
UpdateAwDefinitionCommand(sửa-tại-chỗ diff-by-Id, giữ Id Level) đã land sau đó, guard rất dày. - Seeder KHKK resolve người theo EMAIL cứng (11 email prod
DbInitializer.cs:556-584) — DB thiếu user (local!) ⇒ workflow RỖNG 0 bước vẫn được tạo + log warning; trang designer local sẽ thấy 8 workflow trống người, KHÔNG phải bug. HaveSequentialOrders(Create :306-316) — validator jagged-levels per-step chỉ chạy path Create; Update path dùng bộ guard riêng (T1 set-equality) — 2 bộ luật song song cùng file, drift-risk nếu wave sau chỉnh 1 quên 1.
Đo: ModelSnapshot + entity + 2 handler-file + DbInitializer + Mig 69/70/71/72 + sqlcmd Dev/Design. Prod: CHƯA ĐO (SSH giới hạn). ~62K token.