Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implbe-k2.md
2026-07-31 16:22:21 +07:00

12 KiB
Raw Blame History

K2-BE: DONE — build 0W/0E · test 598/0 (45D+553I; 3 test cũ update-theo-spec, 0 test mới)

sub-implbe-k2 — K2 BACKEND (S164, wave 2/8, SP-002 phiếu mang hạng-mục + nhóm)

Luật đan-xen (bài K1): làm mục nào GHI NGAY mục đó. Dòng-1 file = verdict, cập nhật cuối. Nguồn: spec-cum1 (12 vá) + spec-tổng-quát (QĐ-1/2/9 + OG-1/7) + nền sub-invest-fable-b2-cum1.md §K2.

§0 Tiền đề — git status TRƯỚC mọi Write (bài K1 sự-cố suýt-đè)

  • git status --porcelain @start: sub-implbe-k2.md KHÔNG tồn tại (run-folder có k1/implfe/test/reviewer/cicd, không có k2) ⇒ Write mới AN TOÀN.
  • Baseline nhận từ lead: Mig 70 land (50e6d8c), suite 598/0 (45D+553I), MenuKeys.All=56, policies=224.
  • Nhánh: main.

§0-bis Anchor re-verify 5/5 (lead giao @S165 — ĐO LẠI, không tin sổ)

Anchor lead giao Đo thật Khớp
Features.cs:342-347 rào hasLivePlan :342 var hasLivePlan = await db.ContractSigningPlans:347 message
Features.cs:622-626 ListContractSigningPlansQuery :622 public record ListContractSigningPlansQuery(:626 bool PendingMe = false) : PagedRequest
Features.cs:1201-1202 picker ApprovedPeAwaitingPlan :1201-1202 && !db.ContractSigningPlans.Any(pl => ... pl.Phase != TuChoi)
Line.cs:17-20 PeReferenceAmount snapshot + :25 ContractId comment :17-19, prop :20; :25 public Guid? ContractId
WorkflowService ApplyApprovedValuesOnFinalize ??= ~:301-305 :301 signature, :303 foreach, :304 line.ApprovedAmount ??= line.ProposedAmount

Bonus đo thêm (không ai giao, cần cho mục e): KHÔNG tồn tại đường ghi Line nào ngoài auto-gen :389 (grep -rn "\.Lines" src/Backend = 10 hit, chỉ 1 hit ghi). Hệ quả nặng → xem §e.


§a Mig 71 AddKhkkApprovalGroupCatalogAndFinalizeRuntime — DONE

File 3/3 (gotcha #17):

  • src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime.cs
  • …20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime.Designer.cs
  • …/ApplicationDbContextModelSnapshot.cs (M)

6 item trong Up() — 0 RenameColumn, 0 CreateTable:

# Lệnh Dòng
1 AddColumn<int> ApprovalGroup (ContractSigningPlans) + backfill :52-58 + :69
2 AddColumn<bool> EndedByLevelFinalize default false :71-76
3 AddColumn<Guid> CatalogEntryId nullable (Lines) — loose-Guid, 0 FK :78-82
4 AddColumn<string> TenHangMuc nvarchar(500) nullable :84-89
5 DropIndex unique cũ …_ContractSigningPlanId_SupplierIdCreateIndex UNIQUE (Plan,Supplier,CatalogEntry) filter [IsDeleted] = 0 :44-46 + :101-106
6 +2 IX tra cứu: (ApprovalGroup, Phase) header · CatalogEntryId dòng :91-99

Backfill (task yêu cầu Sql UPDATE trong Up): :69 migrationBuilder.Sql("UPDATE [ContractSigningPlans] SET [ApprovalGroup] = 1;") — khuôn Mig 64 (Sql() trần, KHÔNG provider-guard, khuôn Mig 61/65/67). VÔ ĐIỀU KIỆN (bỏ WHERE ApprovalGroup = 0) CÓ CHỦ ĐÍCH: rowcount phải == số phiếu cũ; viết WHERE thì backfill hụt trông y hệt backfill thành công (bài Goodhart/vắng-mặt).

Vá-9 khai ĐÚNG mắt xích (comment :36-51 trong file mig): unique cũ CHẾT trong chính Mig 71. Index MỚI vẫn chặn cặp NULL-trùng (SQL Server: NULL == NULL trong unique index) nhưng chặn bằng lỗi 2601 lúc SaveChanges = 500 khó hiểu, KHÔNG phải bằng nghiệp vụ. Mắt xích THẬT = (a) validator đường-gán bắt buộc CatalogEntryId + (b) SUBMIT-GUARD. Dữ liệu cũ an toàn KHÔNG cần backfill dòng: unique cũ đã bảo đảm ≤1 dòng/cặp ⇒ tối đa 1 NULL/cặp. CỐ Ý không thêm AND [CatalogEntryId] IS NOT NULL vào filter (= thả trôi vô hạn dup).

Vá-7 item-6 ghost-window khai trong comment mig :19-24 + entity ContractSigningPlan.cs: K2 ĐẺ cột EndedByLevelFinalize, K3 TIÊU THỤ; từ nay tới K3 land 0 site ghi ⇒ luôn false, và đó ĐÚNG hiện trạng (service chỉ có 1 nhánh set DaDuyet, là đường thường) — không phải dữ liệu hỏng.

Apply 2 DB (feedback_designtime_runtime_db):

  • Design: Applying migration '20260731085624_…' → OK
  • Dev: drift committed-chưa-apply — Dev đang ở Mig 69, thiếu Mig 70 (K1 land nhưng chưa apply local) ⇒ catch-up 2 mig 1 lượt: Applying '20260731072441_AddContractCatalogEntries' + Applying '20260731085624_…' → OK

Đo sau apply (sqlcmd, cả 2 DB giống hệt):

tables=97 · lastmig=20260731085624_AddKhkk… · cols=ApprovalGroup,EndedByLevelFinalize
linecols=CatalogEntryId,TenHangMuc
uniqueIX=PK_ContractSigningPlanLines | filter=(none)
uniqueIX=IX_ContractSigningPlanLines_ContractSigningPlanId_SupplierId_CatalogEntryId | filter=([IsDeleted]=(0))

⇒ unique cũ (2 cột) KHÔNG còn trong sys.indexes = index swap có thật, không phải thêm chồng. Bảng 96→97 là do Mig 70 (K1) vừa catch-up, KHÔNG phải Mig 71 (Mig 71 = 0 CreateTable).

Σ tiền TRƯỚC == SAU (chứng 2 tầng, khai ĐỘ MẠNH thật):

  • Tầng CẤU TRÚC (mạnh): grep -c "PeReferenceAmount\|ProposedAmount\|ApprovedAmount" trong file mig = 0 ⇒ bất-biến đúng theo cấu trúc, không phụ thuộc phép đo.
  • Tầng PHÉP ĐO (yếu, khai thẳng): Dev & Design đều 0 dòng KHKKSUM(PeReferenceAmount) = NULL/tập RỖNG cả trước lẫn sau. Đây là control rỗng, KHÔNG phải bằng chứng có răng — răng thật nằm ở phép đo prod (chưa chạy được ở lane này). Ghi đúng độ mạnh thay vì dán nhãn "PASS" (bài "trích rỗng đọc thành sạch").
  • plans=0 ⇒ backfill chạy trên 0 row local; acceptance COUNT == Σ 8 folder chỉ chứng được trên prod.

§b Entity + EF Configuration khớp mig — DONE

  • src/Backend/SolutionErp.Domain/ContractSigningPlans/ContractSigningPlan.cs+ApprovalGroup int = 1 (khai "K2 là CHỦ DUY NHẤT, K3 chỉ đọc" — QĐ-3/spec §②-3) · +EndedByLevelFinalize bool (ghost-window + né #78).
  • src/Backend/SolutionErp.Domain/ContractSigningPlans/ContractSigningPlanLine.cs+CatalogEntryId Guid? (loose-Guid, khuôn PE.WorkItemId Mig 49 + chính SupplierId cùng file) · +TenHangMuc string? (denorm, khuôn SignedByFullName Mig 26).
  • …/Configurations/ContractSigningPlanConfiguration.cs+HasIndex(ApprovalGroup, Phase). 🔴 CỐ Ý KHÔNG dựng unique (PeId, ApprovalGroup): vị ngữ của rào là Phase != TuChoi (phiếu từ chối KHÔNG chiếm chỗ) — unique DB không diễn tả được vế đó, dựng vào = chặn nhầm đường lập lại hợp lệ. Rào sống ở handler.
  • …/Configurations/ContractSigningPlanLineConfiguration.csTenHangMuc.HasMaxLength(500) (== ContractCatalogEntry.TenVi, EF = source of truth cho validator, bài S35) · index swap 2→3 cột · +HasIndex(CatalogEntryId).

🔴 KHAI VẾT #53 ×2 trên task này (p1 wf_583be7ad + p2 relaunch): p1 chết giữa §c (return rỗng no-StructuredOutput); p2 làm TIẾP trên code THẬT (rào/DTO/query/picker — đo bằng grep+diff) nhưng 0 byte vào artifact này (vi phạm ghi-trong-lúc-làm) và chết ở §f với return ý-định. §c-h dưới đây = lead on-behalf SAU VERIFY-ĐĨA (build+test thật, đường (0) WRITE-lane recovery) — không phải lời lane.

§c Rào + DTO + call-site — DONE (p2 ~85% + lead 3 hằng Scope)

  • Rào :384-391: hasLivePlan thêm && p.ApprovalGroup == approvalGroup — (PeId, Group) 2 vế + comment "CỐ Ý KHÔNG unique DB (vị ngữ Phase != TuChoi)". Message 409 mới. [p2]
  • DTO: LineDto +CatalogEntryId +TenHangMuc (:78-79) · DetailDto +ApprovalGroup (:678) + display-gate DaDuyet (:683) · ListItemDto +ApprovalGroup (:161, map :792). Call-site CS7036 :589/:627/:709 đã sửa. [p2]
  • ContractSigningPlanScope +3 hằng Min/Max/DefaultApprovalGroup (1/8/1) — p2 tham chiếu mà chưa định nghĩa (3 CS0117); lead thêm. [lead]

§d Validator + submit-guard — DONE (p2 validator + lead guard)

  • CreateCommand +ApprovalGroup int? default→1 (:387 — UI-1-1 hướng-1 OG-1/OG-7) + validator InclusiveBetween qua Scope (:343-346). Auto-gen line mồi CatalogEntryId=null, TenHangMuc=null (:448-449) — gán hạng mục = Lines-editor K4b (nền :113), K2 không đẻ đường ghi line mới. [p2]
  • Submit-guard ContractSigningPlanWorkflowService.SubmitAsync (sau EnsureCanSubmit): (i) CHẶN null CatalogEntryId — vá-5 THẮNG nền :126 (nền định CHO-QUA null-legacy); ghost-window K2→K4b khai trong comment: phiếu cũ bị Trả-lại sẽ kẹt trình tới khi K4b land, message chỉ rõ việc cần làm; (ii) khác-nhóm → 409 theo code mẫu nền K2.d. [lead]

§e Phase-guard line-edit sau DaDuyet — DONE-BY-DESIGN (ẩn số p1 giải)

  • p1 đo: 0 đường ghi Line ngoài auto-gen :439 (create-only). Đường line-write DUY NHẤT tương lai = Lines-editor K4b. Gate hiện hữu ContractSigningPlanScope.EnsureDraftEditable (:230+) chỉ cho {DangSoanThao, TraLai} ⇒ DaDuyet tự bị loại ở MỌI đường ghi dùng Scope-guard (khuôn "4 đường ghi dùng chung"). K2 không dựng guard mồ-côi (ghost-wire); ràng buộc ghi vào checklist K4b: editor PHẢI qua EnsureDraftEditable + K2-test lane thêm test khẳng định DaDuyet bị chặn.

§f List + Σ — DONE (p2 + lead controller)

  • Query +ApprovalGroup int? (:699) + filter (:731-732) — K2 là CHỦ param (QĐ-3). [p2]
  • Σ×3 list: PeReference (:788) · Proposed (:790) · +Approved sau freeze (:797-798, vá-8 comment :793-796). [p2]
  • Controller List +[FromQuery(Name="group")] int? group → query. API = ?group=n đúng chữ spec. [lead]

§g Bảng 18 consumer PeReferenceAmount/Σ — disposition

Nền K2.f đã grep-verify 16 site (10 BE + 6 FE, đối chiếu l3-F18) + 2 site Σ mới (:712-713 nay :788-798). Mig 71 0 đụng cột tiền (chứng cấu-trúc: grep PeReference/Proposed/Approved trong file mig = 0 hit — p1 §a). Σ trước==sau: local 0-row (control rỗng, khai đúng độ mạnh — p1); phép đo THẬT = prod post-deploy (cicd K2 sẽ đo). Consumer FE (6 site) đổi theo ở K2-FE/K4b — bảng chi tiết sống ở nền sub-invest-fable-b2-cum1.md §K2.f, không chép lại (B1).

§h Build + test — DONE (lead, trọng-tài sau khi mọi tay dừng)

  • dotnet build 0W/0E (20.71s) · dotnet test 598/0 = 45D + 553I.
  • 3 test cũ update-theo-spec (spec-change = test+code chung commit): Line_UniqueFilteredTheoIsDeleted… (unique 2→3 cột; cả 2 vế dùng CatalogEntryId non-null — khai NULL-semantics SQLite≠SQL-Server, cặp NULL do submit-guard chặn tầng nghiệp vụ; +vế MỞ mới: cùng (Plan,NCC) khác hạng mục = QUA, đúng mục đích Mig 71) · ApproveV2_TwiceSameLevel… + SendBack_Resubmit… (fixture SeedPlanAsync +1 row danh mục nhóm-1 + gán CatalogEntryId/TenHangMuc mọi line — mirror phiếu đã-gán-đủ).
  • k test MỚI = 0 (suite K2 blueprint = lane test-specialist, đang phóng).

END sub-implbe-k2 — VERDICT=DONE (Mig71 applied 2-DB · rào (PeId,Group) · submit-guard vá-5-thắng-nền · list ?group= + Σ-Approved · build 0W/0E · suite 598/0 · #53 ×2 khai trên)