Files
solution-erp/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/spec-khkk-awf-mirror-ncc-08-08-2026.md
2026-08-08 01:32:32 +07:00

15 KiB
Raw Blame History

SPEC FINAL — YC-013: 8 panel "Duyệt KHKK — Nhóm 1..8" mirror trang Duyệt NCC (S182/L13, 2026-08-08)

Nguồn: spec DRAFT + merge 2 review Fable — sub-review-1-fable.md (PASS_WITH_FIXES 2C/5M, F1-F12) + sub-review-2-fable.md (PASS_WITH_FIXES 1C/5M+7m, §8). Mọi file:line đã được ÍT NHẤT 1 reviewer re-verify trên cây (HEAD nền 936d1aed). Owner-decisions: D3 = C (YC-014, anh chốt qua AskUser) · D4 = title menu-label + phụ đề nghiệp vụ · D5/D6 = lead chọn nhánh an toàn theo bậc-2 luật tự-trị, anh veto 1 câu là gỡ (khai ở MỤC 3). Executor: hmw fan-out, worker Opus 5 MAX (worker-tier-pin sẵn). Cầm ĐÚNG file này chạy.

MỤC 1 — Ràng buộc toàn cục (R-1..R-6, đọc trước mọi wave)

  • R-1 (C-1): W1+W2 = MỘT commit code duy nhất. Nếu buộc tách: BẮT BUỘC W1 → CI xanh → W2. CẤM mọi commit chứa W2 mà thiếu W1a — nút "Tạo phiên bản mới" trên panel nhóm khi BE còn deactivate-per-type = tắt IsActive cả 8 nhóm prod. (Bom đã armed sẵn hôm nay qua panel /ContractSigningPlan.)
  • R-2 (M-4/F10): trên PROD chỉ đo ĐỌC. Mọi acceptance có GHI chạy LOCAL. Không có endpoint re-activate — POST version mới là ratchet một chiều.
  • R-3 (M-3/F9): baseline test = số đo tươi tại W0 ghi vào run.md. KHÔNG chép 644/645 từ doc nào. (Tham chiếu: lead đo tươi @bootstrap S182 = 645 PASS (45D+600I) — vẫn phải đo lại tại W0.)
  • R-4: 0 migration. Wave nào phát hiện cần mig ⇒ DỪNG, báo lead (premise 3-lens vỡ).
  • R-5: commit [CLAUDE] Workflow: KHKK 8 panel nhóm mirror DuyetNcc + BE guards (0-mig). Artifact run-folder không trigger CI (paths-ignore verified).
  • R-6 (F4): 🔴 CÔNG THỨC W1a KHOÁ = typeEnum == ContractSigningPlan ⇒ Where(w => w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive). CẤM biến-thể regex ^KHKK-N[1-8]$ (công thức FE-invest D1 — code lạ type-10 như QT-KHKK-V2-001 sẽ rơi nhánh per-type → NUKE cả 8 nhóm).

MỤC 2 — WAVE

W0 — prep + đo nền (blocking)

# Task Đạt khi
0.1 dotnet ef database update ×2 DB (Dev + Design — kẹt Mig 71, đĩa Mig 72; đo lại drift tại chỗ) sqlcmd top MigrationId = 20260806070242 cả 2
0.2 Boot API 1 lần → seeder đẻ 8 row local SQL: 8 row KHKK-N1..8 type-10 IsActive=1. Roster local rỗng/đủ người đều KHÔNG phải bug (seeder resolve email; local có 14 user Solutions thật)
0.3 Đo baseline test TƯƠI → ghi run.md R-3
0.4 Prod-state check ĐỌC-only: ssh vietreport-vps + sqlcmd -E (integrated — KHÔNG cần password) chạy 3 query DB-invest §4 + query audit pin-lệch (F11): SELECT p.MaKeHoach, p.ApprovalGroup, w.Code FROM ContractSigningPlans p JOIN ApprovalWorkflows w ON w.Id=p.ApprovalWorkflowId WHERE w.Code LIKE 'KHKK-N%' AND w.Code <> CONCAT('KHKK-N', p.ApprovalGroup) Kỳ vọng: 8 row IsActive=1 · phiếu KHKK đều Phase=3 (DaDuyet=TERMINAL, đã verify enum) · audit = 0 row. LỆCH ⇒ báo lead TRƯỚC W1 (nhóm Archived-oan → recovery = tạo version mới từ panel sau W2; phiếu treo pin lệch → sẽ kẹt submit sau W1b by-design, phải biết số trước)

W1 — BE: 5 fix + 14 test, 2 file + 1 file MỚI, 0-mig

Files: src/Backend/SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs · src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs · MỚI src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanWorkflowGuards.cs · src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs (1 call-site).

# Fix Site Cách
a 🔴 Deactivate-per-Code type-10 AdminFeatures.cs:339-343 Công thức R-6. Type khác GIỮ per-type. Sửa comment :339 "only ONE active per type" CÙNG COMMIT (F12d). Ghi chú nhất quán: nextVersion :334-337 đã per-Code sẵn
b Group-match guard 3-site (F1) Create :479-486 · UpdateDraft :603-607 · SubmitAsync (Service, cạnh rào hạng-mục :169-187 trước LoadStepsAsync :189 — executor verify điểm chèn tại chỗ) 🔴 File MỚI ContractSigningPlanWorkflowGuards.cspublic static class (KHÔNG nhét vào Scope internal — Submit ở Infrastructure không gọi được; KHÔNG InternalsVisibleTo(Infrastructure); KHÔNG inline-duplicate). Method EnsureWorkflowGroupMatchAsync(IApplicationDbContext, Guid awId, int approvalGroup, CancellationToken) — move NGUYÊN logic rào (v), GIỮ semantics cho-qua-code-ngoài-họ (:477-478 chủ-đích). 3 site cùng gọi. Message 409: "Phiếu nhóm N{g} đang gắn quy trình N{k} — vào phiếu (Nháp/Trả lại) đổi lại quy trình đúng nhóm trước khi trình."
b2 Pin-check IsActive ∧ IsUserSelectable (F5 — D6) Create (sau rào iv) + UpdateDraft (CHỈ trong nhánh awId != plan.ApprovalWorkflowId :603) Mirror vá-3 CreateContractFromSigningPlanFeatures.cs:141-147 — 2 message tách "đã ngừng hoạt động"/"chưa ghim". Giữ-nguyên-pin KHÔNG check (phiếu cũ pin bản archived vẫn sửa GhiChu)
c Changelog PUT type-10 AdminFeatures.cs:863-893 Nhánh else if type-10: query ContractSigningPlans (ApprovalWorkflowId==def.Id && Phase != DaDuyet && Phase != TuChoi; query-filter tự loại phiếu xoá-mềm — ĐÚNG cho changelog) → add ContractSigningPlanChangelog { EntityType=Workflow /*=4 enum SẴN*/, Action=Update, EntityId=def.Id, PhaseAtChange, UserId/UserName, Summary="Sửa quy trình duyệt tại chỗ: {Code} v{NN} (giữ nguyên phiên bản)", ContextNote (kèm Reason) } mirror block PE. (Dialog PUT đang HỨA nguyên văn "ghi vào lịch sử phiếu đang chạy" Page.tsx:963-977 — fix này làm lời hứa thành thật)
d DELETE usage-check → 409/404 (F2+F3+M-1) AdminFeatures.cs:1193-1205 (i) đếm pin 8 module có cột ApprovalWorkflowId (grep Domain để liệt, đừng chép): PE/Contract/KHKK BẮT BUỘC IgnoreQueryFilters() (soft-deleted vẫn chặn FK) · Proposal/Leave/Ot/Vehicle/Travel đếm thẳng — 🔴 5 module này pin LOOSE 0-FK ⇒ guard này là RÀO DUY NHẤT chống dangling (rationale 2 vế: FK-modules = UX 547→409; office = DATA-INTEGRITY) · (ii) đếm opinions 8 bảng *LevelOpinion theo level-ids của workflow, IgnoreQueryFilters() → 409 "N phiếu đang gắn + M chữ ký" · (iii) KeyNotFoundExceptionNotFoundException (id lạ = 404, hết 500 thô — middleware không map BCL) · (iv) D5: code họ KHKK-N[1-8] && version CUỐI của Code ⇒ 409 "Nhóm N{n} phải còn ≥1 version" (chặn ca seeder-tái-sinh M-1 khỏi đường UI)

Test W1 — 14 ca (test-before a/b/b2 · test-after c/d). Homes verified: tests/SolutionErp.Infrastructure.Tests/Application/UpdateAwDefinitionTests.cs + ContractSigningPlanApprovalTests.cs: T1 create v2 KHKK-N1 ⇒ N2..N8 bất biến + N1 v01→false v02→true · T2 regression type-1 Code-mới-cùng-type vẫn tắt Code cũ · T3 POST type-10 code lạ QT-KHKK-V2-001 ⇒ 8 nhóm bất biến (F4) · T4 UpdateDraft re-pin lệch nhóm ⇒ 409 · T5 phiếu pin-lệch PRE-EXISTING (seed thẳng DbContext) Submit ⇒ 409 · T6 code ngoài họ: Create/UpdateDraft/Submit PASS như cũ (negative M-5c.ii) · T7 re-pin bản archived ⇒ 409 (D6) · T8 PUT workflow nhóm có phiếu ChoDuyet ⇒ 1 row changelog đúng shape · T9 phiếu DaDuyet/TuChoi ⇒ 0 row (negative) · T10 DELETE có pin KHKK ⇒ 409 · T11 phiếu KHKK ĐÃ XOÁ MỀM pin ⇒ vẫn 409 (F2) · T12 phiếu Leave (office, 0-FK) pin ⇒ 409 (F3 — hôm nay là 200-xoá-êm) · T13 id không tồn tại ⇒ 404 · T14 xoá version CUỐI nhóm ⇒ 409 (D5); version 0-usage không-phải-cuối ⇒ xoá OK. Đạt khi: build 0-err + FULL suite = baseline-W0 + 14, 0 fail.

W2 — FE: fe-admin src/pages/system/ApprovalWorkflowsV2Page.tsx (~70-100 dòng) + KhkkCreatePage ×2 app

# Task Site Cách
a Parse kind :164-172 typeCode?.match(/^KhkkN([1-8])$/)khkkGroup; selectedTypeInt = khkkGroup ? TYPE_INT_CONTRACT_SIGNING_PLAN : TYPE_CODE_TO_INT[typeCode]; query overview giữ applicableType=10 (queryKey dùng chung — invalidate prefix tự tươi cả panel nhóm lẫn panel type-10, đã verify :292/:348)
b Synthetic slice per-Code :299-301 versions = type10.history.filter(d => d.code === khkkGroupWorkflowCode(g)) (🔴 History = FULL list kể cả active — verified BE :206-211; panel tự filter !isActive khi render) · active = versions.find(v=>v.isActive) ?? null (KHÔNG đọc ô Active per-type — 1-trong-8) · giữ shape TypeSummaryDto
c lockedCode xuyên suốt TypePanel:357Designer:620/:650-651/:798/:920-933 prop lockedCode = khkkGroupWorkflowCode(g); ô Mã disabled={isEdit || !!lockedCode} + hint "Mã khoá theo nhóm — version tự tăng" (F12a); POST gửi lockedCode ?? code
d D3=C thi hành — danh-sách CHỐT (F6+M-2 merge) :1235-1328 Gate applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN (áp cả panel ContractSigningPlan cũ — nhất quán). 6 ô (#1 allowReturnOneLevel · #2 allowReturnOneStep · #3 allowReturnToAssignee · #5 EditDetails :1268-1276 · #6 EditBudget :1277-1285 · #9 Delete :1320-1328): thêm disabled + title tooltip "Chưa áp dụng cho KHKK" — 🔴 GIỮ checked={entry.<flag>} state THẬT, CẤM ép false (trung thực với DB; payload levelFlagsPayload :255-267 echo state, không đổi). Ô #4 allowReturnToDrafter: disabled + hiển thị state thật (thường TRUE) + tooltip RIÊNG "KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh" (KHÔNG mờ-kiểu-chưa-có — hành-vi thật là LUÔN-CHO). #7 SkipToFinal GIỮ NGUYÊN như đang có (:1292-1302) · #8 Finalize SỐNG
e (optional — kéo eoffice rotate) mirror menuKeys fe-{admin,user}/src/lib/menuKeys.ts:31-32 +8 const AwV2_KhkkN1..8 — 0 hệ quả runtime
f Ô Ngưỡng CEO — NGOÀI D3, disable type-10 VÔ ĐIỀU KIỆN (F7) :944-961 disabled={isEdit || applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN} + helper-text "KHKK không dùng ngưỡng máy — NĐUQ quyết" + POST type-10 gửi null. Tiền lệ :1292. (CeoApprovalThreshold KHÔNG port VĨNH VIỄN — OG-3)
g Auto-pin lọc isActive ×2 app (F5/D6) fe-user KhkkCreatePage.tsx:114 + fe-admin :113-116 find(w => w.isActive && code-match) — giữ filter isUserSelectable :104. Mirror LOGIC 2 app (2 file vốn khác nhau — không cần byte-equal)
h Comment lệch + D4 phụ đề :641-644 + PageHeader :305-318 sửa comment "2 ô duyệt sớm sống cho MỌI loại" cho khớp thực tế (m-2) · thêm ~1 prop subtitle = khkkGroupMenuLabel(g) (types/khkk.ts:159-168 — 2-app identical sẵn)

Acceptance LOCAL (R-2): A1 8 leaf → 8 slice khác nhau, hết landing · A2 dialog đúng khuôn ảnh theo D3=C (title KHKK-N{n} v01 (giữ nguyên phiên bản), ô #4 tick-disabled, 6 ô disabled-state-thật, rule-box) · A2b sửa-tại-chỗ THẬT 1 nhóm ⇒ 204 + panel giữ v01 (M-5/m-5) · A3 tạo v02 N1 ⇒ nằm ĐÚNG panel N1 + badge 7 nhóm KHÔNG mất · A4 regression 4 route cũ + fe-user auto-pin. Đạt khi: npm run build ×2 app + tsc 0-err. 🔸 Răng nút Ghim (sửa theo F5): sau D6, bỏ-ghim bản active = chặn tạo phiếu nhóm; bỏ ghim cả 8 = đóng cửa tạo phiếu KHKK toàn cục (không có rào chặn bỏ-ghim-cái-cuối — khai, không rào đợt này).

W3 — N/A (D3=C đã chốt; port sau theo lệnh owner = gỡ mờ per-cờ CÙNG wave, khuôn K3-F5)

W4 — test-specialist đóng suite: FULL dotnet test SolutionErp.slnx = baseline-W0 + 14, 0 fail. Số canonical đổi ⇒ update DUY NHẤT docs/STATUS.md row Tests (B1).

W5 — ship + verify

  1. Commit MỘT phát BE+FE+tests (R-1, R-5) → push.
  2. cicd-monitor 4-chân (m-3): (i) CI Run success + test-gate = số W4 · (ii) bundle admin rotate + grep js mới marker KhkkN1 (control-âm bundle cũ 0-hit); eoffice: đổi nếu làm W2e/W2g-fe-user, GIỮ nếu không — ĐO đừng suy · (iii) BE DLL: ssh grep SolutionErp.Application.dll token EnsureWorkflowGroupMatch 2-encoding UTF-8/UTF-16 (khuôn S177) + control-dương token cũ · (iv) query-1 lại: 8 row IsActive=1 bất biến qua deploy.
  3. Acceptance PROD đọc-only: login admin@solutions.com.vn → ≥3/8 panel render đúng slice + mở dialog rồi Hủy · 3 query SQL + audit pin-lệch (F11) · KHÔNG bấm "Tạo phiên bản mới" trên prod · note: role thường chưa grant menu-row ⇒ không thấy leaf = đúng thiết kế 2-tầng (#82).
  4. Đóng sổ: STATUS/HANDOFF/WAL + tick YC-013 + khai accepted-risk: race 2-admin cùng Code → 500 thô (UNIQUE đỡ data — không mở scope middleware).

Rollback

0-mig ⇒ revert commit sạch phần MÁY. Data ở lại hợp lệ (changelog rows = audit-history; v02+ = ratchet vô hại, pin by-Id). 🔴 Revert W1a = bom per-type TÁI VŨ TRANG — sau revert, CẤM "Tạo phiên bản mới" mọi panel type-10 tới khi re-apply.

MỤC 3 — Quyết định + khai

# Quyết Ai Nội dung
D3 anh (YC-014) C — mờ ô chưa nghe, thi hành theo danh-sách-chốt W2d (6 ô disabled-state-thật + #4 forced-true-disabled + #7 giữ + #8 sống)
D4 lead (reviewer đồng ý) title = menu-label + phụ đề nghiệp vụ
D5 🟡 lead chọn CÓ (bậc-2 an toàn — reviewer-2 M-1 khuyến nghị CÓ) rào delete-last-version nhóm ⇒ 409 (chặn seeder-tái-sinh khỏi đường UI). Anh veto 1 câu là gỡ
D6 🟡 lead chọn CÓ (bậc-2 an toàn — reviewer-1 F5 khuyến nghị; tiền lệ vá-3 bridge CÙNG LỖ cho type-3) pin-check IsActive∧IsUserSelectable + FE lọc isActive ×2 app. Anh veto 1 câu là gỡ

Khai mâu-thuẫn lens (F8): (a) công thức W1a: BE-invest thắng, FE-invest regex BỊ CẤM (R-6) · (b) D3: FE-invest nghiêng A, BE-invest nghiêng C — anh đã chốt C · (c) DB-invest W-DB-2 (param code= phía BE) = KHÔNG LÀM — chọn client-side slice (payload nhỏ, 0-đổi-BE); disposition khai tại đây.

Nợ NGOÀI scope (khai, không lặng): GET Overview 0-policy lộ roster+flags cho mọi user authenticated (owner ngó sau) · roster 8 nhóm = snapshot email lúc seed, không tự sync theo QT-DN-V2-001 · Khkk_WfView panel aggregate giấu 7/8 nhóm (re-point = việc riêng) · sửa-workflow không notify approver phiếu treo (parity PE) · DEFAULT_CODE_BY_TYPE[10] panel cũ vẫn gợi mã ngoài-họ (sau W1a vô hại — per-Code) · comment "7 bảng" :1120 · race 2-admin 2601→500 (accepted).