Files
solution-erp/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-db-fable.md
2026-08-08 01:01:12 +07:00

18 KiB
Raw Blame History

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ó IsDeletedApprovalWorkflow : 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 Code nhiều row, Version int monotonic; 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ới Version = max(Code)+1 (:334-337), IsActive=true, IsUserSelectable=true (:352-353). 🔴 LANDMINE cho KHKK: deactivate cũ chạy theo ApplicableType chứ KHÔNG theo Code — Where(w.ApplicableType == typeEnum && w.IsActive) set IsActive=false TẤT CẢ (:339-343). Với DuyetNcc (1 code/type) đúng; với KHKK 8 code chung type=10 ⇒ tạo version mới cho KHKK-N1 sẽ 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 theo Level.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ảng ContractSigningPlanChangelogs ĐÃ 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) + TenHangMuc denorm (: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ện SET = 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 bit runtime-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 row Code="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 ApprovalGroup lúc tạo (default 1, validator 1..8 ContractSigningPlanFeatures.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 int 1..8 (validator ContractCatalogFeatures.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.cs GHI đủ cặp CurrentWorkflowStepIndex (0-based index list sorted) + CurrentApprovalLevelOrder (1-based) (:201-202, :295, :309-310) + EndedByLevelFinalize=true nhá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 đụng IsActive (: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

  1. 9/9 checkbox = 9 cột bit có sẵn trên ApprovalWorkflowLevels (§1b), bảng dùng chung mọi ApplicableType.
  2. Versioning v01→v04 + Archived: UNIQUE (Code, Version) + IsActive bit — đã phục vụ đúng cơ chế này cho QT-DN-V2-001 v04; KHKK-N{n} là Code như mọi Code.
  3. Ghim = IsUserSelectable sẵ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).
  4. Pin + con trỏ + finalize-flag phiếu KHKK: đủ từ Mig 69/71, service đã ghi/đọc sống.
  5. 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ào ContractSigningPlanChangelogs (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êng ContractSigningPlanApprovals.ApprovalWorkflowLevelId LOOSE (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 đổi ApplicableTypeCode (hoặc Code∧Type); test: tạo v2 KHKK-N1 ⇒ N2..N8 IsActive bất biến + case DuyetNcc regression.
  • W-DB-2 (BE): overview/API thêm chiều Code (param code= hoặc group per-Code trong AwTypeSummaryDto) để 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 ContractSigningPlanChangelogs sẵn — mirror khối PE :863-892).
  • Verify prod (bàn giao implementer): 3 query §4.

§7 Surprises ngoài đề bài

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.