Files
solution-erp/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-review-2-fable.md
2026-08-08 01:18:20 +07:00

17 KiB

SUB-REVIEW-2 (Fable) — lens HOÀN-CHỈNH-VẬN-HÀNH + tổng hợp WAVE-PLAN FINAL

Run: 2026-08-08-S182-khkk-awf-mirror-ncc · Reviewer #2/2 (/fable-real reviewer, YC-013) Trạng thái: ĐANG ĐỔ RUỘT — skeleton viết lúc spawn, Edit liên tục.

VERDICT (điền cuối)

(pending)

1. Thứ tự wave + phụ thuộc

Topology W0→W5 về logic là ĐÚNG (prep → BE → FE → [W3 điều kiện] → test → ship), nhưng spec THIẾU câu ràng buộc ghép-deploy — đây là finding nặng nhất của review này:

  • [C-1] Spec không cấm ship W2 (FE) trước W1a (BE). Executor là hmw fan-out per-wave; nếu wave FE commit + CI chạy TRƯỚC wave BE, thì 8 nút "Tạo phiên bản mới" trên 8 panel mới = 8 cò súng nối thẳng vào bom deactivate-per-TYPE còn nguyên (ApprovalWorkflowV2AdminFeatures.cs:339-343 — đã verify tận mắt: Where(w.ApplicableType == typeEnum && w.IsActive) tắt CẢ 8). Chú ý thêm 2 sự thật làm C-1 nặng hơn spec tưởng:
    1. Bom đã armed TỪ HÔM NAY — panel /system/approval-workflows-v2/ContractSigningPlan (TYPE_CODE_TO_INT có key này, :171) đã cho bấm "Tạo quy trình mới" vào type-10 từ K3. W1a vì thế không chỉ là "điều kiện cho W2" mà là vá nợ prod đang hở — càng sớm càng tốt, độc lập với FE.
    2. Chiều ngược (W1 trước, W2 sau) an toàn tuyệt đối: 4 fix BE đều backward-compatible với FE cũ (deactivate hẹp hơn, guard thêm 409, changelog thêm row, DELETE 409 thay 500). Cách vá spec: thêm vào MỤC 2 dòng ràng buộc: "W1+W2 = MỘT commit code duy nhất (mặc định); nếu buộc tách thì thứ tự BẮT BUỘC W1→CI xanh→W2; CẤM tồn tại commit nào chứa W2 mà thiếu W1a." — điều-kiện-chặn đã đưa vào WAVE-PLAN FINAL §8.
  • W1 nội bộ: a/b/c/d độc lập nhau (2 file + 1 helper), làm chung 1 wave là đúng. W1b phụ thuộc helper extract từ rào (v) ContractSigningPlanFeatures.cs:479-486 (đã verify site) — Create + UpdateDraft (:603-607 chỉ có EnsureWorkflowTypeAsync, đã verify thiếu group-match) + Submit (ContractSigningPlanWorkflowService.cs sau :167 EnsureCanSubmitAsync, cạnh rào hạng-mục :169-187 — điểm chèn tự nhiên, đã verify).
  • W2 phụ thuộc W0 (local có 8 row để nhìn panel) — đúng như spec.
  • W4 (test-specialist đóng suite) phải chạy SAU khi D3 chốt (số test W3 thay đổi theo phương án) — spec đã ngầm đúng, wave-plan final ghi tường minh.
  • W5 phụ thuộc TẤT CẢ — đúng.

2. Deploy-safety

  • 1 commit hay tách?1 commit BE+FE (khuyến nghị chốt trong spec, hiện spec để trống). Lý do: 0 migration, diff nhỏ (~2 file BE + 1 file FE + tests), revert nguyên tử, và triệt tiêu C-1. Nếu lead muốn tách theo wave để dễ bisect: BẮT BUỘC W1 trước (điều kiện chặn ở §8).
  • cicd path-filter (#41): đã verify .gitea/workflows/deploy.ymlpaths-ignore gồm docs/**, **/*.md, .claude/workflows/runs/**. Diff này chạm src/Backend + fe-admin + teststrigger đủ. Artifact run-folder (sub-*.md) không trigger CI — commit spec/artifact riêng = 0s, đúng thiết kế.
  • Bundle rotate: deploy.yml build CẢ 2 FE mỗi run (:111-125) + copy đè cả 2 dist (:163-169), nhưng Vite hash theo content ⇒ fe-user không đổi source thì hash giữ nguyên. Chỉ W2a-d ⇒ admin rotate, eoffice GIỮ (đúng spec); làm W2e (chạm fe-user/src/lib/menuKeys.ts) ⇒ eoffice rotate theo. Khớp spec W5.
  • Phiếu đang treo trên prod: đã đối chiếu enum ContractSigningPlanPhase (Domain/ContractSigningPlans/ContractSigningPlanPhase.cs:10-14): 3 phiếu prod "phase=3" (đo S168) = DaDuyet — TERMINAL, không phải phiếu treo. ⇒ W1b Submit-guard và W1c changelog không đụng phiếu sống nào tại thời điểm đo. Deploy giữa giờ = app-pool restart vài giây (mỗi deploy đều vậy) + seeder idempotent skip (§3). Rủi ro THẤP. Điều kiện: chạy lại query 2 (GROUP BY ApprovalGroup, Phase) TRƯỚC deploy để re-confirm 0 phiếu ChoDuyet/TraLai — nếu có phiếu treo pin lệch nhóm thì nó sẽ kẹt submit sau vá W1b (by design, message chỉ việc); phải biết trước con số.
  • [m-4] Pre-deploy prod-state check thiếu trong spec: chạy query 1 (Code, Version, IsActive) TRƯỚC deploy — nếu ai đó đã lỡ bấm tạo version trên panel ContractSigningPlan từ K3 tới nay thì có nhóm đang Archived-oan; đường chữa KHÔNG cần SQL tay: sau khi W1a live, tạo version mới cho đúng nhóm đó qua UI (POST tự re-activate per-Code).
  • Seeder chạy mỗi restart: verify §3 — skip per-Code, không re-activate, an toàn.

3. Seeder tương tác (wipe-durability #75/#76) — VERIFIED-BY-CODE

Mở DbInitializer.cs:549-687 đọc tận mắt (không tin invest):

  • Idempotency = AnyAsync(w => w.Code == code) (:610) — match BẤT KỲ row nào cùng Code, không phân biệt Version/IsActive. ⇒ admin tạo v02 cho N1 (v01 archived): restart → AnyAsync("KHKK-N1") = true → SKIP. KHÔNG re-seed, KHÔNG re-activate. An toàn lớp #75/#76.
  • Nhưng chiều ngược: admin XOÁ hết version của 1 Code (v01 lẫn v02, khi chưa pin) → boot kế AnyAsync = false → seeder TÁI SINH v01 IsActive=true, IsUserSelectable=true với roster snapshot email cứng (:554-585). Đây là hành vi lớp #75/#76 CÓ THẬT nhưng chiều "resurrection-as-safety-net" — nhóm 1..8 là cấu trúc (ApprovalGroup int trên phiếu + 8 menu leaf) nên tái sinh có lợi, SONG spec không khai ⇒ admin xoá nhóm rồi thấy nó sống lại sau restart = ticket "xoá không được". → finding F-3.
  • Version tái sinh = Version=1 — nếu v01 cũ đã xoá thì UNIQUE (Code,1) trống → insert sạch, không 2601.
  • Roster local: seeder resolve email qua userManager.FindByEmailAsync (:593) — local CÓ 14 user Solutions thật (CLAUDE.md "30 demo user"), nên local KHÔNG chắc rỗng người như W0 spec khẳng định; rỗng hay đủ đều không phải bug → finding F-9 (chỉnh chữ W0).

4. Acceptance prod — đủ đo chưa

  • [M-4] Acceptance #3 ("Tạo phiên bản mới nhóm N1 ⇒ v02") chạy Ở ĐÂU — spec không nói, và chạy trên PROD là mutation MỘT CHIỀU. Đã verify controller (ApprovalWorkflowsV2Controller.cs): đúng 5 action, KHÔNG có endpoint re-activate. Tạo v02 ⇒ v01 archived vĩnh viễn; xoá v02 KHÔNG tự re-activate v01 ⇒ nhóm rơi vào trạng thái 0-active (panel hiện empty-state "Chưa có quy trình..." :405-409; KhkkCreatePage vẫn pin được v01 vì lọc isUserSelectable không lọc isActivefe-admin/src/pages/khkk/KhkkCreatePage.tsx:104, đã verify — nhưng badge/slice sai). Vá: acceptance #3 chạy LOCAL; trên prod chỉ đo ĐỌC (UI render 8 panel + 3 query SQL). Nếu anh muốn demo #3 trên prod thật: chấp nhận v02 (bản sao y v01) tồn tại vĩnh viễn — khai rõ trước khi bấm.
  • Account đo: spec không nêu. Chốt: admin@solutions.com.vn (role Admin: bypass mọi policy + menu auto-grant SeedAdminPermissionsAsync). 8 leaf sidebar hiện cho role nào khác = tầng menu-grant per-role (display) — ĐỘC LẬP tầng API (Workflows.Create cho verb ghi, authenticated-only cho GET); acceptance thêm 1 dòng: "role thường không có grant menu ⇒ không thấy leaf — đúng thiết kế, không phải bug".
  • 3 query SQL — chạy bằng đường nào: spec DB §4 giao query nhưng không giao ĐƯỜNG. Khuôn đã chứng S177: ssh vietreport-vps + sqlcmd -E (integrated auth, KHÔNG cần credential; env password TRỐNG là bình thường). Ghi vào wave-plan để executor không đi tìm password.
  • Thiếu 1 phép đo UI-ghi trên local: acceptance #2 chỉ MỞ dialog. Thêm: sửa-tại-chỗ 1 nhóm (đổi tên 1 NV slot / tick 1 cờ Return) → 204 → panel refresh giữ v01 + không đổi Version — chứng PUT đường nhóm sống thật (máy PUT là generic by-Id nên gần chắc chắn PASS, nhưng "gần chắc" ≠ đo).
  • Acceptance #1/#2/#4 (8 slice + dialog khuôn + regression 4 route cũ): ĐỦ và đo được. Bổ sung điều kiện #4: "645 test" → số đo tươi tại W0 (xem M-3 §5).

5. Test coverage W1

Spec W1 liệt 4 test — đúng nhưng THIẾU 3 ca (tính chốt-cuối + nhánh phủ):

  • [M-5a] Submit chốt-cuối group-mismatch KHÔNG có test. W1b vá 2-site (UpdateDraft + Submit) mà test list chỉ có ca UpdateDraft. Ca thiếu: phiếu ĐÃ pin lệch nhóm từ trước vá (seed data trực tiếp qua DbContext, bypass Create-guard) → SubmitAsync ⇒ 409 message chỉ việc. Đây chính là lý do tồn tại của chốt-cuối — không test thì không biết chốt có đóng.
  • [M-5b] DELETE 409 nhánh ĐỦ 2 chân: (i) phiếu pin (đã có trong spec) + (ii) workflow hết pin nhưng còn ContractSigningPlanLevelOpinions trỏ Levels (FK Restrict :6297-6301 — cascade Steps→Levels sẽ vấp) ⇒ 409 kể tên. Chân (ii) chưa có trong test list.
  • [M-5c] 2 ca NEGATIVE giữ hành-vi-cũ: (i) changelog W1c: phiếu DaDuyet/TuChoi pin workflow bị PUT ⇒ 0 row mới (mirror filter PE :867-870); (ii) workflow type-10 code NGOÀI họ KHKK-N* (vd QT-KHKK-V2-001) ⇒ Create/UpdateDraft/Submit CHO QUA như cũ (rào (v) comment :477-478 khai chủ-đích) — helper extract xong mà siết nhầm cả code ngoài họ là regression lặng.
  • Concurrent create 2 admin cùng nhóm (câu hỏi lead): UNIQUE (Code,Version) đỡ được — kẻ thua 2601 → 500 thô. ĐỒNG Ý với DB-invest: chấp nhận cho màn quản trị, KHÔNG mở scope map DbUpdateException đợt này (đổi GlobalExceptionMiddleware = blast radius mọi module) — nhưng phải ghi thành accepted-risk trong spec final, đừng im lặng.
  • Bỏ-ghim cả 8 nhóm = CHẶN TẠO PHIẾU toàn cục (KhkkCreatePage lọc isUserSelectable — đã verify :104): hành vi CŨ có sẵn, không phải regression của wave này; acceptance đã nêu răng nút Ghim per-nhóm — thêm nửa câu "bỏ ghim cả 8 = đóng cửa tạo phiếu KHKK toàn hệ thống (không có rào BE chặn bỏ-ghim-cái-cuối)" để anh biết nút này sắc tới đâu.
  • [M-3] Baseline "645 test" trong spec ⟂ STATUS.md canonical "644 test PASS (45D+599I, CI #444)" — lệch 1 và spec đang CHÉP SỐ (vi phạm B1 + measured-label gate: con số mang nhãn đo mà không kèm phép đo làm nó trượt). Vá: spec đổi thành "baseline = dotnet test đo TƯƠI tại W0, ghi số vào run.md; mọi so sánh về sau so với số W0". (Nếu 645 là đo tươi thật thì STATUS stale — cũng chỉ có phép đo W0 phân xử được.)
  • Nơi đặt test: homes có sẵn — tests/SolutionErp.Infrastructure.Tests/Application/UpdateAwDefinitionTests.cs (PUT/create-deactivate/delete-guard) + ContractSigningPlanApprovalTests.cs (submit chốt-cuối) — đã verify 2 file tồn tại.

6. D3/D4 — chấm phương án

D3 (7 ô chưa-wire) — chấm từng phương án theo hệ quả VẬN HÀNH:

  • A (giữ 9 ô tick được): rẻ nhất hôm nay, đắt nhất về sau. 2 hệ quả spec CHƯA nêu đủ: (i) ticket-support loại "tôi tick Trả-về-1-Cấp mà phiếu vẫn về Drafter" — lỗi class #78 đúng nghĩa, khó debug vì DB nói một đằng máy chạy một nẻo; (ii) mìn-ngủ khi port W3 sau này: cờ đã tick từ nhiều tháng trước đột nhiên BẬT hành vi ngày port — surprise nguy hiểm hơn cả config-lie, và lúc đó phải audit toàn bộ tick tồn kho trước khi bật máy. A = tôi KHÔNG khuyến nghị.
  • B (port cả 7): đổi hành vi duyệt (4 return-mode chạm state-machine TraLai-toàn-phần + con-trỏ-đôi reset; F3/F4 mở đường ghi lúc duyệt; F6 thêm rào 3-lớp) — mỗi cờ 1 quyết định nghiệp vụ thuộc owner + test RED→GREEN riêng; scope ước gấp 3-5 lần toàn bộ W1+W2 cộng lại. Chỉ đáng khi anh THẬT SỰ cần semantics đó ngay. Ghi chú: kể cả B thì CeoApprovalThreshold vẫn không port (OG-3 NĐUQ human-gate) ⇒ B cũng không đạt "9/9 sống" tuyệt đối.
  • C (mờ ô chưa-wire + tooltip): ĐÚNG tiền lệ đã sống ngay trong trang này (SkipToFinal :1292-1302 — checked-ép-false + disabled + title, đã verify) và trung thực #78. Khuôn giữ nguyên (9 ô cùng vị trí — "không lệch" ở nghĩa cấu trúc; trạng thái disabled là sự thật per-type, prod ĐANG hiển thị đúng kiểu này cho SkipToFinal). → KHUYẾN NGHỊ C, port dần per-cờ theo lệnh owner sau (mỗi lần port = gỡ 1 ô mờ, đúng khuôn K3-F5: port + gỡ khoá CÙNG wave).

[M-2] BẪY THI CÔNG của C mà spec chưa ghi (đã verify cơ chế payload): pattern SkipToFinal ép cả checked={type10 ? false : state}. Ép-checked AN TOÀN với F2 chỉ vì DB type-10 F2 luôn false (BE ép-false từ W1 S161 trước khi 8 nhóm seed). Áp NGUYÊN pattern cho 6 ô còn lại là SAI 2 chỗ:

  1. AllowReturnToDrafter seed default TRUE trên mọi slot — ép checked=false ⇒ dialog hiển thị "không trả về Drafter" trong khi runtime LUÔN trả về Drafter khi Trả lại (ContractSigningPlanWorkflowService.cs ReturnOrRejectAsync không đọc cờ) — display nói dối theo chiều NGƯỢC.
  2. Ô #4 nói riêng phải hiển thị tick-thật + disabled + tooltip "KHKK hiện luôn trả về Drafter khi Trả lại — cờ chưa phân nhánh". Luật thi công C: 6 ô (4 Return + EditDetails + EditBudget) chỉ thêm disabled + title, GIỮ checked={entry.<flag>} state thật, KHÔNG ép false; payload levelFlagsPayload (:255-267) không đổi (echo state — đã verify PUT gửi state không gửi display). SkipToFinal giữ nguyên như đang có. + ô "Ngưỡng CEO" (:944-961): POST-mode disable khi type-10 + helper-text "KHKK không áp ngưỡng CEO (NĐUQ)" (PUT-mode đã khoá sẵn disabled={isEdit}). Điều kiện disable dùng applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN (không phải chỉ khkkGroup) ⇒ panel ContractSigningPlan cũ đồng bộ luôn — nhất quán, đúng.

D4 (title panel): ĐỒNG Ý phương án lead: title = menu-label "Duyệt KHKK — Nhóm {g}" (khớp chữ user vừa bấm ở sidebar — nguyên tắc least-surprise) + phụ đề nghiệp vụ khkkGroupMenuLabel(g) (helper 2-app identical có sẵn types/khkk.ts:159-168). 1 dây nhỏ executor hay quên: khuôn hiện chỉ render applicableTypeLabel (PageHeader :309-317) — phụ đề cần thêm ~1 prop/1 dòng ở panel; ghi vào wave-plan.

7. Rollback + monitoring

  • Revert code = sạch phần MÁY, không sạch phần DATA — và đó là CHẤP NHẬN ĐƯỢC, nhưng phải khai:
    1. ContractSigningPlanChangelogs rows do W1c ghi (nếu có PUT sau deploy) — bảng có sẵn từ Mig 69, row là audit-history hợp lệ, revert KHÔNG cần xoá. Vô hại.
    2. Version v02+ do admin tạo sau deploy — tồn tại vĩnh viễn (ratchet §4). Revert FE chỉ làm nó "tàng hình" khỏi panel nhóm (quay về landing), KHÔNG hỏng runtime (phiếu pin by-Id).
    3. 🔴 Revert W1a ⇒ bom deactivate-per-TYPE TÁI VŨ TRANG — lần POST type-10 kế tiếp tắt cả 8 nhóm. Rollback-note bắt buộc: nếu revert, coi panel ContractSigningPlan + 8 panel nhóm là NO-GO cho "Tạo phiên bản mới" tới khi re-apply.
  • cicd-monitor 3-chân — spec nói khuôn nhưng thiếu MARKER cụ thể [m-3]:
    1. Chân CI: Run #N success + test-gate xanh (đối chiếu số W0).
    2. Chân bundle admin (byte-level, khuôn #77): curl admin.solutions.com.vn lấy tên bundle mới ≠ bundle cũ (STATUS :479 canonical) + grep trong js mới chuỗi chỉ-có-sau-W2: KhkkN1 (regex parse) — control-âm: bundle cũ 0-hit. eoffice: KHÔNG đổi hash nếu bỏ W2e (đo để chứng, không suy).
    3. Chân BE DLL (khuôn S177 2-encoding): ssh + grep SolutionErp.Application.dll token mới W1b (tên helper, vd EnsureWorkflowGroupMatch) — UTF-8 lẫn UTF-16, control-dương bằng token cũ đã biết có.
    4. Chân data (post-deploy): chạy lại query 1 — kỳ vọng 8 row IsActive=1 y trước deploy (deploy KHÔNG tự đổi data; lệch = có người bấm gì đó giữa chừng).
  • Post-deploy smoke KHÔNG-GHI trên prod: login admin → 3/8 panel bất kỳ render đúng slice + mở dialog Sửa-tại-chỗ rồi Hủy. KHÔNG bấm "Tạo phiên bản mới" trên prod trong smoke (M-4).

8. WAVE-PLAN FINAL

(pending)