Files
solution-erp/.claude/skills/ef-core-migration/SKILL.md
pqhuy1987 fc1b8d9642 [CLAUDE] Docs: S117 closeout — PE lũy-kế display-fix + duyệt theo khoảng Min–Max
STATUS + HANDOFF mega-line: prepend S117 (Mig 66 · 89 bảng · 501 test · bundle
cFI7ih4a/MxgooVZw · prod 37 NCC/17 user), demote S116 → Prev. ef-core-migration
SKILL +row Mig 66 AddPeApprovedPriceRange + range 51-66. Session log
2026-07-14-S117-pe-luyke-display-minmax-range.md (2 feature + /fable-real process
+ #53-garble ×2 recover + lessons).

Docs-only → CI-skip (path-filter #41). Feature code đã ship `316a82f` Run #387.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:39:08 +07:00

32 KiB
Raw Blame History

name, description, when-to-use
name description when-to-use
ef-core-migration Tạo/sửa/revert EF Core 10 migration cho SOLUTION_ERP. Dùng khi thêm entity mới, thay đổi schema, rollback migration, debug DesignTimeDbContextFactory fail. Đã có nhiều migration (số hiện tại + mới nhất → docs/STATUS.md canonical + row cuối bảng history trong skill này — KHÔNG hardcode số ở đây, B1 anti-drift). Snapshot + Designer + Migration 3-file rule bắt buộc commit đủ.
thêm migration
EF Core migration
dotnet ef migrations add
revert migration
schema DB update
DbContext change
snapshot lỗi
DesignTimeDbContextFactory

EF Core Migration — SOLUTION_ERP

Context: .NET 10 + EF Core 10 + SQL Server. DbContext: ApplicationDbContextInfrastructure/Persistence/. Startup: SolutionErp.Api.

Migration history (số hiện tại → docs/STATUS.md canonical; mới nhất = row cuối bảng)

# Name Tables added / changed
1 Init 7 Identity tables (AspNetUsers/Roles/UserRoles/...)
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 (legacy admin override)
8 AddVersionedWorkflows WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers + Contracts.WorkflowDefinitionId FK
9 AddContractDetailsAndChangelog 7 per-type Details (ThauPhu/GiaoKhoan/NhaCungCap/DichVu/MuaBan/NguyenTacNcc/NguyenTacDv) + ContractChangelogs
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/Steps/StepApprovers
13 AddPurchaseEvaluationCodeSequences 1 bảng PurchaseEvaluationCodeSequences (Prefix PK, LastSeq) — atomic sequence cho MaPhieu format PE/{YYYY}/{A|B}/{Seq:D3}
14 AddBudgets ⚠️ DROPPED Mig 50 (S61) — module Ngân sách cũ đã XÓA, thay bằng PeWorkItemBudgets per-gói-thầu. Bản gốc: 4 bảng Budgets + BudgetDetails + BudgetApprovals + BudgetChangelogs + nullable FK index Contracts.BudgetId & PurchaseEvaluations.BudgetId (append-only history, giữ row đánh dấu superseded).
15 AddPurchaseEvaluationDepartmentOpinions 1 bảng PurchaseEvaluationDepartmentOpinions (UNIQUE PEId+Kind, max 1 row mỗi PeDepartmentKind per phiếu — Phê duyệt/Ccm/MuaHang/SmPm)
16 AddTwoStageDeptApprovalAndSmartReject 3 bảng Contract/PurchaseEvaluation/Budget DepartmentApprovals (UNIQUE TargetId+Phase+Dept+Stage cho 2-stage NV.Review → TPB.Confirm per phòng × phase) + 4 ALTER (Users.CanBypassReview bit cho NV bypass + 3 RejectedFromPhase int cho smart reject jump-back). Phase 9 — đóng bug "NV duyệt được hết phase" anh Kiệt (FDC) báo. Logic 2-stage trong PurchaseEvaluationWorkflowService chỉ áp PE; HĐ + Budget defer.
17 AddManualBudgetFieldsToPeAndContract 4 ALTER (PE + HĐ × BudgetManualName nvarchar(200) + BudgetManualAmount decimal(18,2)) — manual budget fallback khi user không link Budget entity approved. KHÔNG XOR với BudgetId, cả 2 cùng null OK. Carry-forward pe.BudgetManualName/Amount → contractCreateContractFromEvaluation. Phase 9 — Session 11 (2026-05-07).
18 AddPeWorkflowInnerStepsAndPositionLevel N-stage workflow PE — 1 CREATE TABLE PurchaseEvaluationWorkflowStepInnerSteps (Order, DepartmentId, PositionLevel, Name, SlaDays, IsRequired) + 2 ALTER (Users.PositionLevel int? 1=NV/2=PP/3=TP + PEDeptApproval.InnerStepId Guid?) + 3 IX + FK Cascade Step / Restrict Dept+InnerStep. Phase 9+ — Session 12 (2026-05-07).
19 AlterPeDeptApprovalsUniqueFilteredForInnerSteps Filtered unique split: drop UNIQUE cũ Mig 16 → 2 filtered: legacy WHERE InnerStepId IS NULL (Stage Review/Confirm) + N-stage WHERE InnerStepId IS NOT NULL (per inner step). Tránh conflict 2 inner step cùng dept Stage=Confirm. Session 12.
20 AddContractWorkflowInnerStepsAndAlterDeptApprovalUnique N-stage workflow Contract mirror PE Mig 18+19 — GỘP 1 migration: CREATE TABLE WorkflowStepInnerSteps + ALTER ContractDeptApproval.InnerStepId + DropIndex old + Recreate filtered legacy/N-stage + 3 IX + FK. Session 13 (2026-05-07).
21 RefactorWorkflowToFlatModel 🎯 DRASTIC REFACTOR (Session 16, 2026-05-08): bỏ phase enum legacy, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking. Workflow flat (Phòng × Cấp + Approvers). 4 ALTER (PE/Contract +CurrentWorkflowStepIndex/RejectedAtStepIndex) + 2 ALTER (WorkflowStep +DepartmentId/PositionLevel PE+Contract) + DROP TABLE × 2 (PE+Contract WorkflowStepInnerSteps Mig 18+20) + DROP COLUMN InnerStepId × 2 + DROP filtered indexes Mig 19/20 + RESTORE simple unique non-filtered × 2.
22 AddApprovalWorkflowsV2 🎯 V2 schema mới (Session 17, 2026-05-08) — riêng cho UAT trước khi drop legacy V1. 3 entity ApprovalWorkflow + Step + Level + enum ApplicableType (DuyetNcc=1 / DuyetNccPhuongAn=2 / Contract=3). 3 CREATE TABLE + UNIQUE (Code, Version) + FK Cascade Step→Workflow + Level→Step + FK Restrict Department + ApproverUserId. Cấu trúc: Quy trình > Bước (Phòng) > Cấp (1 NV cụ thể qua ApproverUserId, OR-of-N rows cùng Order = same Cấp). DbInitializer +menu V2 + leaf AwV2_DuyetNcc/DuyetNccPhuongAn.
23 AddApprovalWorkflowIdToPurchaseEvaluation Pin V2 vào PE — PE.ApprovalWorkflowId Guid? + EF FK Restrict. Workspace Select bắt buộc workflow lúc create + Validate ApplicableType match PE.Type. Session 17.
24 AddCurrentApprovalLevelOrderToPe Service V2 wire — PE.CurrentApprovalLevelOrder int? track Cấp đang chờ trong Step hiện tại khi pin ApprovalWorkflowId. Null khi V1 legacy hoặc terminal. Service ApproveV2Async group Levels by Order = Cấp (OR-of-N), match actor.Id ∈ ApproverUserId, advance levelOrder++ trong Step → idx++ + reset levelOrder=1 → DaDuyet. Synthetic Policy ForV2Schema() cho FE nextPhases. Session 17.
25 AddIsUserSelectableToApprovalWorkflows ALTER ApprovalWorkflows +IsUserSelectable bit NOT NULL DEFAULT 0 — admin pin/unpin workflow cho user pick lúc create phiếu (multi-select, độc lập IsActive — multiple version cùng selectable). Backfill WHERE IsActive=1 SET 1 giữ behavior cũ (active workflows vẫn pickable). Designer +badge "Cho user chọn" + button Ghim/Bỏ ghim + mutation toggle. Workspace dropdown filter isUserSelectable=true only. API PATCH /api/approval-workflows-v2/{id}/user-selectable admin-only. Session 18 (2026-05-08).
26 AddPeLevelOpinionsForV2 🎯 Section 5 dynamic theo Workflow V2 (Session 19, 2026-05-09) — bảng mới PurchaseEvaluationLevelOpinions UNIQUE composite (PEId, ApprovalWorkflowLevelId), FK Cascade Pe + Restrict Level. Comment nvarchar(2000) + SignedAt + SignedByUserId + SignedByFullName denorm. Service ApproveV2Async UPSERT auto khi NV duyệt (Q1=1B sync gắn với Duyệt, KHÔNG endpoint CRUD rời). Match level theo ApproverUserId; Admin override fallback first level (FE banner "Admin duyệt thay" khi SignedByUserId !== ApproverUserId). Comment empty → "(duyệt — không ý kiến)" placeholder Q4 bonus. Phiếu V1 legacy fallback Mig 15 4 box readOnly. Mig 15 deprecated cho V2 phiếu (drop sau UAT confirm Mig 27+).
27 AddVisibilityAndDisplayLabelToMenuItems 2 ALTER MenuItems (+IsVisible bit=1 +DisplayLabel nvarchar(200)?) — admin ẩn/hiện + đổi tên menu eOffice (fe-user). Không bảng mới. Session 20.
28 AddAdvancedOptionsToApprovalWorkflows ALTER ApprovalWorkflows — advanced options (skip/optional level config). Session 22.
29 RefactorAdvancedOptionsToPerLevelAndDrafterUser Refactor advanced options → per-ApprovalWorkflowLevel + DrafterUser scope. Session 22.
30 AddAllowApproverEditBudgetToLevels ALTER ApprovalWorkflowLevels +AllowApproverEditBudget bit. Session 22.
31 RefactorSkipToFinalToApproverLevel Per-Approver-slot skip-to-final (Level → Approver slot swap). Session 23.
32 AddApprovalWorkflowToContract 🎯 Contract V2 wire (Plan B S29) — 2 ALTER Contracts (+ApprovalWorkflowId Guid? +CurrentApprovalLevelOrder int?) cookie-cutter mirror PE Mig 23+24. Không bảng mới.
33 AddContractLevelOpinions 1 bảng ContractLevelOpinions (UNIQUE composite ContractId+LevelId, FK Cascade Contract + Restrict Level) — mirror PE Mig 26. Session 29.
34 AddEmployeeProfiles HRM core — EmployeeProfiles + N satellite tables cùng parent (Pattern 12-ter). Session 34.
35 AddHrmConfigs HRM config catalogs (LeaveTypes / Holidays UNIQUE composite / declarative KIND_CONFIG). Session 35.
36 AddMeetingRooms Office — MeetingRooms + booking. Session 36-37.
37 ExtendApplicableTypeForWorkflowApps Enum extend ApplicableType (+Proposal +WorkflowApps values). Không bảng mới. Session 37-38.
38 AddProposals Module Đề xuất (Proposal) — tables + workflow V2 mirror PE/Contract. Session 37.
39 AddWorkflowApps Workflow Apps skeleton — Leave/OT/Travel/Vehicle/Ticket request tables (Phase 10 Plan G-O4/O5). Session 38.
40 AddAttendances Chấm công (Attendance) tables + Dashboard NS skeleton. Session 38.
41 WireWorkflowAppsApprovalV2 🎯 Phase 11 P11-A (S42) — +4 {Leave,Ot,Travel,Vehicle}LevelOpinions (UNIQUE composite + Cascade/Restrict) + WorkflowAppCodeSequences (atomic MaDonTu) + 4 ALTER RejectedFromStatus + enum TravelRequest=9.
42 AddLeaveBalances 🎯 Phase 11 P11-B (S43) — 1 bảng LeaveBalances (User×LeaveType×Year + Entitled/Used/Adjustment, UNIQUE composite + FK LeaveTypes Restrict). Trừ phép tự động khi đơn nghỉ duyệt cuối.
43 FilterHolidayUniqueIndexByIsDeleted 🎯 S45 — DropIndex + CreateIndex IX_Holidays_Year_Date UNIQUE filtered WHERE [IsDeleted]=0 (was unfiltered). Fix reachable 500 khi recreate ngày lễ trên slot soft-deleted. Không bảng mới (table vẫn 91). gotcha #57 — soft-delete UNIQUE phải filter.
44 AddVehicleAndDriverCatalogs 🎯 Phase 11 P11-C (S51) — 2 catalog Vehicles + Drivers (AuditableEntity, filtered-unique Code day-1). Extend HrmConfigs declarative. Tables 91→92.
45 FilterHrmCatalogUniqueIndexesByIsDeleted 🎯 S51 — LeaveType + ShiftPattern + OtPolicy Code UNIQUE filtered WHERE [IsDeleted]=0 (3 HRM catalog, gotcha #57 EXT). Index-only.
46 AddSlaFieldsToItTicket 🎯 Phase 11 P11-D (S52) — ItTickets +SlaDueAt/SlaWarnedSent/SlaBreached (SLA timer). Column-only, no new table.
47 FilterMasterCatalogUniqueIndexesByIsDeleted 🎯 S53 — Department + Supplier + Project Code UNIQUE filtered WHERE [IsDeleted]=0 (gotcha #57 EXT Master, 6× cumulative). Index-only.
48 AddProjectMasterFields 🎯 S55 — Project +4 cột nullable (Year int · Investor 250 · Location 500 · Package 300). AddColumn-only, no new table. Kèm SeedRealMasterDataAsync ungated nạp 62 dự án + 71 hạng mục + 3 NCC real từ Excel.
49 AddWorkItemToPurchaseEvaluation 🎯 S57bis — PE.WorkItemId Guid? loose-Guid (KHÔNG FK vật lý — convention PE: ProjectId/SelectedSupplierId đều loose) + IX_PurchaseEvaluations_WorkItemId. AddColumn+CreateIndex-only, no new table. Guard = validator NotEmpty (create) + handler AnyAsync IsActive→Conflict.
50 ReplaceBudgetModuleWithPeWorkItemBudgets 🎯 S61 (2026-06-13) — bảng mới PeWorkItemBudgets (1 record/cặp Dự án × Hạng mục, UNIQUE filtered [IsDeleted]=0) + DROP module Budget cũ (Budgets/BudgetDetails/BudgetApprovals/BudgetChangelogs…) + PE/Contracts DROP BudgetId + backfill BudgetManualAmount→BudgetPeriodAmount TRƯỚC DropColumn (phiếu UAT giữ số) + DELETE menu/permission Bg_* IN-list children-first. ⚠️ database-agent advise: KHÔNG FK vật lý PE/Contracts→Budgets → no DropForeignKey · DropIndex TRƯỚC DropColumn (SQL 5074) · IN-list thay LIKE Bg_%. Ngân sách giờ per-gói-thầu nhập theo role PRO/CCM. gotcha #63 (EF scaffold RenameColumn SAI-semantics → Add+UPDATE+Drop) + #64 (dotnet ef database update áp Design-DB 0-rows ≠ Dev-DB → backfill chạy thật lần đầu trên prod).
51 AddDepartmentParentId 🎯 S65 — Department.ParentId Guid? loose-Guid (KHÔNG FK vật lý — org-tree phân cấp; GET /departments/tree ráp cây in-memory + rollup count theo User.DepartmentId + cycle-guard HashSet chặn tự-cha + vòng A→B→A). AddColumn-only, no new table.
52 AddHoSoLinkToPurchaseEvaluation 🎯 S65 — PE.HoSoLink nvarchar(1000)? hyperlink NAS (mục "e. Link hồ sơ", FE <a target=_blank rel=noopener> null-safe). AddColumn-only, no new table.
53 AddPeUrgentAndCeoApprovalThreshold 🎯 S69 — PE +IsUrgentByPro/IsUrgentByCcm (bit, cờ gấp PRO đỏ/CCM xanh) + ApprovalWorkflow +CeoApprovalThreshold decimal(18,2)? (ngưỡng gói CEO — CCM role CostControl duyệt-final khi winnerQuoteTotal < ngưỡng). 3 AddColumn, no new table. anh Kiệt FDC.
54 AddPeSuggestedAndApprovedPrice 🎯 S73 — PE +5 cột giá đề xuất (ProSuggestedMin/Max + CcmSuggested + ApprovedPriceAmount + ApprovedPriceSource) additive-nullable — CEO chọn giá chốt + CCM duyệt-done opt-in. anh Kiệt FDC go-live 22/06.
55 AddCcmNoteToPeWorkItemBudget 🎯 S74 — PeWorkItemBudget +CcmNote nvarchar(1000)? (ô "Ghi chú từ CCM" mirror ProNote). AddColumn-only, no new table.
56 AddProBudgetSplitToPeWorkItemBudget 🎯 S76 — PeWorkItemBudget +ProInitialAmount+ProAdjustmentAmount decimal(18,2)? (cột PRO "Ban hành lần đầu"+"V0/hiệu chỉnh", mirror CCM Initial/Adjustment). AddColumn additive + Sql() data-migrate ProInitial=ProEstimate WHERE ProEstimate NOT NULL (4 rows prod, gotcha #64). no new table. Form ngân sách → MA TRẬN 3 cột Dự án/PRO/CCM (bảng lưới <table>).
57 AddPeSuggestedPriceNotes 🎯 S77 — PurchaseEvaluations +ProSuggestedPriceNote+CcmSuggestedPriceNote nvarchar(1000)? (ghi chú kèm giá đề xuất PRO/CCM, mirror ProNote/CcmNote). 2 AddColumn additive-nullable, no new table.
58 AddPeMultiWinnerAndLevelFinalize 🎯 S87 — PE multi-winner + per-Level finalize: PurchaseEvaluationSuppliers +IsWinner bit (nhiều NCC trúng thầu, thay winner đơn lẻ SelectedSupplierId) + ApprovalWorkflowLevels +AllowApproverFinalize bit (per-Cấp kết-thúc-tại-đây, không lên CEO) + Sql() backfill IsWinner=1 WHERE SupplierId = pe.SelectedSupplierId (phiếu UAT giữ winner cũ, idempotent). 2 AddColumn, no new table.
59 AddCcmBudgetPeriodToPurchaseEvaluation 🎯 S87 — PurchaseEvaluations +CcmBudgetPeriodAmount decimal(18,2)? (ngân sách CCM cột Section-B kỳ này, per-PE, mirror BudgetPeriodAmount phía PRO). 1 AddColumn additive-nullable, no new table.
60 AddPeEndedByLevelFinalize 🎯 S97 — PE +EndedByLevelFinalize bit NOT NULL DEFAULT 0 — cờ RUNTIME "phiếu ĐÃ kết thúc sớm bằng level-finalize" (set-true CHỈ nhánh finalize trong service); display EndsBeforeCeo cho phiếu DaDuyet đọc RUNTIME thay suy-CONFIG (gotcha #78 — phiếu opt-out lên CEO hết báo sai "không qua CEO"). Kèm Sql() backfill marker ⚠️ soi NHẦM bảng Approvals → match 0 (xem Mig 61). 1 AddColumn, no new table.
61 BackfillEndedByLevelFinalizeFromChangelog 🎯 S97 — DATA-ONLY no-schema (đính chính Mig 60): backfill query ĐÚNG bảng PurchaseEvaluationChangelogs.ContextNote LIKE N'%Duyệt KẾT THÚC tại%' + PhaseAtChange=7. PE 2 bảng lịch-sử TÁCH VAI: Approvals = per-approver record vs Changelogs.ContextNote = transition+marker (LogTransitionAsync). Idempotent, SQL-Server-only, prod-verified A/005/A/008 flag 0→1 + A/010 control unchanged (gotcha #79 — xác định đúng bảng-writer + verify marker-count prod TRƯỚC khi tin backfill; backfill=0-rows = cờ đỏ).
62 ExpandSupplierFields 🎯 S104 — Supplier 9→27 field (Phase A của 30-col NCC Excel import, anh Kiệt FDC): 18 AddColumn nullable + 18 DropColumn Down + SupplierType +CaHai=6 append-only + enum SupplierStatus mới + guarded un-cram Sql() (clear legacy crammed Note 4 seeded rows exact-match, reversible). No new table (89 giữ — 18 AddColumn/0 CreateTable). Seed fill-nulls-if-exists 30-field TRUONGGIANG/TANPHU/TGN/DONGDUONG idempotent. Commit 778fc98 shipped UN-closed-out → S104 docs-reconcile; prod-verified Run #488 PASS; 440 test #65-no-bite dù đổi chữ ký Create/UpdateSupplierCommand. Phase B (Excel-auto-map) defer.
63 AddSupplierImportSourceFields 🎯 S112 — Supplier +SourceUpdatedAt datetime2? + SourceUpdatedBy nvarchar(200)? (import-provenance cột 29/30 file Excel "Database NCC" — Phase B Excel-import Phase B). 2 AddColumn nullable + 2 DropColumn Down (reversible). No new table (89 giữ). Prod-verified Run #496 PASS __EFMigrationsHistory top + 2/2-col present; commit e100ef0.
64 AddSupplierPublishState 🎯 S113 — Supplier +IsPublic bit NOT NULL default 0 (cổng công-bố "thiếu Mã NCC → nháp/ẩn" R4): AddColumn + Sql("UPDATE Suppliers SET IsPublic=1") backfill 37/37 prod→public + DropIndex→CreateIndex đổi Code filtered-unique WHERE [IsDeleted]=0 AND [Code]<>'' (cho phép nhiều nháp Code=''). No new table (89 giữ). Reversible Down (⚠️ edge: rollback sau khi có ≥2 nháp Code='' sẽ kẹt CreateIndex filter cũ). Prod-verified Run #497; commit 5fa11b5. Kèm gotcha #80 (null-Code NRE NewSupplier).
65 BackfillPeQuoteIsSelectedAndWinnerInvariant 🎯 S114 — DATA-ONLY no-schema (multi-NCC per hạng mục anh Kiệt): backfill 2-CHIỀU thiết lập bất-biến Supplier.IsWinner == Any(Quote.IsSelected). CHIỀU-1 IsWinner→IsSelected (phiếu cũ whole-supplier winner Mig58 → set IsSelected mọi quote NCC, preserve tổng winnerQuoteTotal) + CHIỀU-2 IsSelected→IsWinner (dormant/seed EXISTS). Idempotent (WHERE ...=0), SQL-Server-only (SQLite EnsureCreated bỏ qua migration), Down no-op (data backfill không reversible, mirror Mig 61). No new table (89 giữ) — snapshot UNCHANGED (0 CreateColumn). Prod-verified bundle rotate ×2 D51OYyGV/BVdssm5S; commit d436af2. Kèm gotcha #81 (delete-writer sót re-derive IsWinner → phantom HĐ giaTri=0 — verify-bắt).
66 AddPeApprovedPriceRange 🎯 S117 — PE +ApprovedPriceMaxAmount decimal(18,2)? (feature "duyệt theo khoảng MinMax" anh Kiệt FDC "chọn cả 2 min và max"): source "ProMinMax" = Amount(Min)+MaxAmount(Max), bất-biến MaxAmount != null ⟺ Source=="ProMinMax" + Min≤Max (guard service ApplyApprovedPriceOnFinalize + FluentValidation). 1 AddColumn nullable, no backfill (phiếu cũ Source-đơn Max=null nguyên vẹn), no new table (89 giữ), reversible Down (DropColumn). HĐ.GiaTri KHÔNG đọc giá chốt (=SUM báo giá NCC được chọn) → 0 tác động HĐ (range = record-of-decision hiển thị). Prod-verified Run #387 (ApprovedPriceMaxAmount col decimal(18,2) + __EFMigrationsHistory top); commit 316a82f.

Total: 89 bảng sys.tables (= 88 dbo-app + __EFMigrationsHistory; re-ground S97 cicd Run #482 LIVE — số canonical → docs/STATUS.md; last schema-changing Mig 50 net 93→88-app, Mig 51-66 đều AddColumn/index/data-only/schema-expand không đổi số bảng). Xem docs/database/schema-diagram.md migration table + §11-15 module ERD (§16+ Mig 27-64 chi tiết pending).

N-stage workflow pattern (Mig 18-20 — Session 12-13)

Architecture decision đáng ghi cho session sau:

Pattern: Mỗi WorkflowStep cha (= 1 phase) cấu hình động chuỗi InnerSteps con theo Department × PositionLevel với Order sequential. Approver cần khớp DeptId + PositionLevel + Order tiếp theo chưa duyệt. CanBypassReview cho user level cao hơn skip cấp dưới cùng dept.

Per-module migration (PE first, mirror sang Contract sau):

  • PE: 2 migration tách (Mig 18 schema + Mig 19 alter index) — vì index issue phát hiện sau khi schema đã commit
  • Contract: 1 migration GỘP (Mig 20) — gộp CREATE TABLE + ALTER + DropIndex + Recreate filtered. Cleaner cho mirror.

Filtered unique trick: legacy 2-stage rows (Mig 16, Stage=Review/Confirm) + N-stage rows (per InnerStep) cùng table *DepartmentApprovals → cần unique split:

  • UX_*_Phase_Dept_Stage WHERE InnerStepId IS NULL (legacy, prevent duplicate Review/Confirm cùng phase × dept)
  • UX_*_Phase_InnerStep WHERE InnerStepId IS NOT NULL (N-stage, prevent duplicate per inner step)

Backward compat 100%: workflow no InnerSteps configured → service fallback legacy 2-stage logic Mig 16. Data legacy InnerStepId=null vẫn enforce unique cũ qua filtered index.

Budget defer: Budget chưa có versioned WorkflowDefinition entity (hardcoded BudgetPolicy.Default). Để mirror N-stage Budget cần migration AddBudgetVersionedWorkflow trước (4 bảng + ALTER Budget.WorkflowDefinitionId?). Defer cho user quyết riêng.

Phase 7 pending:

  • AddPePaymentTermFields — tách PurchaseEvaluations.PaymentTerms JSON thành 6 column riêng (Session 11+++++ đã thay UI Workspace <Textarea> JSON → <Select> 8 preset, BE schema vẫn nvarchar(max). Migration tách field defer cho khi BE cần aggregate/filter từng terms riêng)
  • AddPeDepartmentOpinions — thêm 4 field Ý kiến phòng ban (Phê duyệt/CCM/MuaHàng/SM-PM) vào header hoặc table riêng (đã làm Mig 15 — bảng riêng UNIQUE PEId+Kind)

Phase 8 pending (Budget refinement):

  • AddBudgetCodeSequences — atomic sequence khi format MaNganSach chốt (mirror Contract/PE pattern, hiện Random.Shared)
  • AddBudgetVersionedWorkflow — nếu Solutions cần admin config UI (3 bảng BudgetWorkflowDefinitions/Steps/StepApprovers + Budgets.WorkflowDefinitionId?)

Phase 8 update (2026-04-29 Session 5):

  • Migration 15 AddPurchaseEvaluationDepartmentOpinions — 1 bảng riêng (UNIQUE PEId+Kind), max 4 row mỗi phiếu cho 4 phòng ban (Phê duyệt/Ccm/MuaHang/SmPm). UPDATE in-place khi user đổi ý, audit qua Changelog.
  • Tests: tests/SolutionErp.Domain.Tests/ (58 test policy state machine) + tests/SolutionErp.Infrastructure.Tests/ (96 test = codegen + 2-stage/N-stage + AuthorizePolicy + V2 actor scope + WorkflowApps ApproveV2 + LeaveBalance/guard). Total 154 test pass (current S43; S22-S25 baseline was 111). S22-S23 +20 cumulative regression: ReturnMode + DraftGuard + Reflection AuthorizePolicy + V2 actor scope reject + per-NV lookup discrimination (Plan N+O). Mig 21 drop 19 legacy (PE 2-stage S9 + PE N-stage S12 + Contract N-stage S13 + PE Reject S14) — flat workflow stabilized post-S22 V2 ground truth.
  • CI optimize 3 fix (29/04):
    • Manual checkout bypass github.com (gotcha #39) — fix TCP timeout 21s
    • Path filter docs-only skip (gotcha #41) — commit MD-only KHÔNG trigger CI
    • npm junction cache (gotcha #40) — rolled back, debug session sau
  • Migration verify pattern thêm: chạy dotnet test SolutionErp.slnx local trước mỗi migration commit để chống regression policy/codegen.

Phase 7 update (2026-04-28):

  • Migration 14 AddBudgets⚠️ DROPPED Mig 50 (S61) — module Budget cũ đã XÓA, thay bằng PeWorkItemBudgets. (Bản gốc: 4 bảng Budgets/Details/Approvals/Changelogs + 2 cột BudgetId? thêm vào Contracts & PurchaseEvaluations. Workflow hardcoded BudgetPolicy.Default chưa versioned.)
  • 14 demo user thật @solutions.com.vn qua SeedDemoUsersAsync reconcile (PRO 5 + CCM 7 + ISO 1 + CEO 1).

Phase 6 update (2026-04-24):

  • Enum PurchaseEvaluationAttachmentPurpose.ComparisonTable = 4 mới (file bảng so sánh tổng). Int column, không cần migration.
  • BackfillUserEmailDomainAsync trong DbInitializer — rename in-place 4 field Email/NormalizedEmail/UserName/NormalizedUserName. Idempotent.

Commands (chạy từ root repo)

Thêm migration mới

# Sau khi chỉnh Domain entity + EF config + DbContext DbSet
dotnet ef migrations add <MigrationName> `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api `
  --output-dir Persistence/Migrations

Naming convention: PascalCase, verb prefix:

  • Add<Thing> — thêm entity / bảng
  • Update<Thing> — thay đổi schema
  • Remove<Thing> — xóa column / table
  • Rename<Thing> — đổi tên

Apply migration to DB

# Dev: LocalDB SolutionErp_Dev
dotnet ef database update `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api

# Hoặc đơn giản: chạy API → DbInitializer tự Migrate
dotnet run --project src/Backend/SolutionErp.Api

Revert 1 migration (rollback)

# Rollback về migration trước đó (ví dụ về sau AddWorkflowTypeAssignments)
dotnet ef database update AddWorkflowTypeAssignments `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api

# Xóa file migration (3 file, xem dưới)
dotnet ef migrations remove `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api

Gen SQL script (review trước khi apply prod)

# Script từ 1 migration → latest
dotnet ef migrations script AddWorkflowTypeAssignments `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api `
  --output tmp/migration-7-to-8.sql

# Idempotent (check if exists trước mỗi operation)
dotnet ef migrations script --idempotent `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api

3-file rule (BẮT BUỘC commit đủ)

Mỗi migrations add sinh 3 file — phải commit đủ:

src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/
├── {Timestamp}_{Name}.cs              ← migration logic (Up/Down)
├── {Timestamp}_{Name}.Designer.cs     ← model snapshot tại thời điểm migration
└── ApplicationDbContextModelSnapshot.cs  ← current model state (được overwrite mỗi migration)

Gotcha #17: Thiếu ModelSnapshot.csmigrations add kế tiếp sẽ sinh duplicate columns. Thiếu Designer.cs → không revert được.

Pitfalls thường gặp

P1 — DesignTime DbContext resolve fail

Triệu chứng: dotnet ef migrations addUnable to resolve service for type 'DbContextOptions<ApplicationDbContext>'.

Nguyên nhân: EF tooling chạy đứng ngoài runtime DI, cần factory riêng.

Fix: Đã có src/Backend/SolutionErp.Infrastructure/Persistence/DesignTimeDbContextFactory.cs. Nếu connection string thay đổi → update factory, không chỉ appsettings.json.

P2 — Table rename / column rename gen ra DROP + CREATE

Triệu chứng: Migration sinh DropColumn + AddColumn thay vì RenameColumn → mất data.

Fix: Sửa migration thủ công sang migrationBuilder.RenameColumn(...). Hoặc dùng [Column("newname")] attribute để EF tự detect rename.

P3 — Query filter (soft delete) khi thêm FK

Triệu chứng: Warning The entity type 'X' has a global query filter but referencing entity 'Y' doesn't. This may lead to inconsistent results.

Fix: Entity reference cũng phải có query filter:

builder.HasQueryFilter(x => !x.IsDeleted);

Hoặc dùng .IgnoreQueryFilters() cho query cần bypass.

P4 — FK cascade/restrict khác với expectation

Triệu chứng: Migration apply OK nhưng DELETE parent → cascade sang child (hoặc ngược lại restrict block).

Fix: Explicit config trong IEntityTypeConfiguration<T>:

builder.HasOne(x => x.Contract)
    .WithMany(c => c.Approvals)
    .HasForeignKey(x => x.ContractId)
    .OnDelete(DeleteBehavior.Cascade);  // hoặc Restrict, SetNull

Case study quan trọng: Contracts.WorkflowDefinitionIdWorkflowDefinitions.Id PHẢI dùng Restrict để protect HĐ cũ khi admin archive version (xem workflow-contract.md invariants).

P5 — Nullable reference type gen column NOT NULL

Triệu chứng: Property public string? Description → migration gen NOT NULL.

Fix: Check builder.HasQueryFilter chỗ config, hoặc explicit builder.Property(x => x.Description).IsRequired(false).

P6 — AddVersionedWorkflows duplicate seed

Case study hiện tại: Nếu thêm migration mới sau AddVersionedWorkflows, cẩn thận KHÔNG trigger DbInitializer.SeedWorkflowDefinitionsAsync 2 lần. Check if (!db.WorkflowDefinitions.Any()) trong DbInitializer.

Workflow khi thêm entity mới (example: add AuditLog)

1. Domain/AuditLogs/AuditLog.cs — tạo entity (BaseEntity hoặc AuditableEntity)
2. Infrastructure/Persistence/Configurations/AuditLogConfiguration.cs
   - ToTable("AuditLogs")
   - Unique index trên (EntityType, EntityId) nếu cần
3. Infrastructure/Persistence/ApplicationDbContext.cs
   - public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
4. Application/Common/Interfaces/IApplicationDbContext.cs
   - DbSet<AuditLog> AuditLogs { get; }
5. dotnet ef migrations add AddAuditLogs
6. Review file .cs sinh ra → OK
7. dotnet ef database update → LocalDB apply
8. Test: dotnet run → check bảng được tạo
9. Commit [CLAUDE] Domain+Infra: add AuditLog entity + migration
10. ⚠️ UPDATE docs/database/schema-diagram.md (ERD + migration table)
11. UPDATE docs/STATUS.md nếu là work lớn

Apply prod (VPS)

# Trên VPS, sau khi deploy code:
# Option 1: auto migrate — DbInitializer.MigrateAsync() chạy khi API startup
# Option 2: manual script
dotnet ef migrations script <LastApplied> --idempotent --output migrate.sql
# Review migrate.sql → chạy qua sqlcmd:
sqlcmd -S .\SQLEXPRESS -d SolutionErp -U vrapp -P <pw> -i migrate.sql

Prod safety:

  • Backup trước: scripts/backup-sql.ps1
  • Dry-run idempotent script local trước
  • Test rollback plan: script from-N-to-M-1 sẵn

Code pointers

  • src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs — DbSet toàn bộ bảng app (số bảng/migration → docs/STATUS.md)
  • src/Backend/SolutionErp.Infrastructure/Persistence/DesignTimeDbContextFactory.cs — EF tooling factory
  • src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs — seed + warn + migrate runtime + backfill (idempotent reconcile pattern)
  • src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ — IEntityTypeConfiguration per entity
  • src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs — interface Application layer
  • docs/database/database-guide.md — conventions + migration workflow chi tiết
  • docs/database/schema-diagram.mdERD (số bảng → docs/STATUS.md) + §11 PE + §12 Budget (REMOVED Mig 50) + §13 PEDeptOpinions (Mig 15) + §14 ApprovalWorkflow V2 (Mig 22-25) + §15 PE Level Opinions V2 (Mig 26); §16+ Mig 27-61 detail pending (xem migration table)
  • docs/gotchas.md #7, #17, #38 — migration pitfalls + Identity 4-field rename