32 KiB
sub-investigator-codebase-2 — LANE-2 CODEBASE DRY-RUN MAP (S160, /fable-real)
Vai: investigator-codebase · engine-đắt REAL · ghi-đĩa-trong-lúc-làm (chống #53). Trục: CODE THẬT trên repo. Lane-1 (song song) lo nghiệp vụ từ QT/FORM. Spec KHKK cũ (GĐ2 b.7→12, W1-W5, 7 bảng, Mig 69, type=10) = RATIFIED — chỉ BỔ SUNG GĐ3+GĐ4. Ranh giai đoạn (owner chốt @S159, spec §1.2): GĐ2=b.7→12 · GĐ3=b.13→18 (tận dụng Contract V2) · GĐ4=b.19→21 + ký cứng upload-only.
§0 — Nguồn đã đọc + tiền đề
run.mdS160 (lệnh anh verbatim + ràng buộc 5-anchor + authz S156 + gotcha #84 no-SQL-tay).- Spec S157 497 dòng ĐỌC TRỌN (①②③④⑤): 7 bảng
ContractSigningPlan*, Mig 69,ApplicableType=10, W1-W5, NOTE W1 @S160 (Khkk_* skeleton ĐÃ prod, dark-launch ĐẢO — menu public, ComingSoon→trang thật mới là "bật"). src-QT-TRINH-KY-HD.txt: bảng 2 = 9 bước ISO (Lựa chọn→Soạn thảo→Góp ý→Đàm phán→In HĐ→CCM→BOD→Đóng dấu→Phát hành/Lưu trữ) ↔ContractPhase1-9 khớp 1:1 (xác nhận S157 lens-fidelity).src-RG-001-v02-ma-so-hd.txt(2.907B): formatA/B/SOL&C/D+ 6 ví dụ HĐ + 2 dạng PO (X/Y/PO Ztheo nguyên tắc ·A/PO/SOL&C/Dkhông nguyên tắc).
(mục chi tiết append dần bên dưới — mỗi §=1 câu hỏi)
§1 — Q1: MAP TÁI DÙNG b.13→21 → Contract V2 (mọi dòng RE-VERIFY trên đĩa 07-29, KHÔNG chép mù S156 — line đã TRÔI vì arc-2 thêm ListDeleted)
Khung đọc: GĐ3 = b.13→18 (owner @S159) chạy trên state machine 5-trạng-thái ContractPhase.cs:14-28 (sống: DangSoanThao=2 · ChoDuyet=10 · DaPhatHanh=9 · TraLai=98 · TuChoi=99; 7 phase [LEGACY] = chính các trạm 13-21 đã gỡ CỐ Ý Mig 21). GĐ4 = b.19→21. Duyệt V2 = ContractWorkflowService.ApproveV2Async (ContractWorkflowService.cs:217-394) cookie-cutter PE: OR-of-N :259-260 · UPSERT ContractLevelOpinions :292-316 · skipToFinal :320-352 · terminal gen-mã+DaPhatHanh :366-385.
| b | Việc (theo QT/PDF) | Phase/feature hiện có | Trạng thái | Chứng (file:line, đo 07-29) |
|---|---|---|---|---|
| 13 | PMH soạn draft HĐ từ giá chốt KHKK | DangSoanThao; 2 đường tạo: bridge PE→HĐ + create thường (create thường ĐÃ nhận pin V2 ContractFeatures.cs:109; gen MaHopDong NGAY tại create :113-118 — RG-001 có mã từ nháp, KHÔNG đợi terminal) |
PARTIAL | Bridge pin V1-only CreateContractFromEvaluationFeatures.cs:68-71,108 + KHÔNG đọc giá KHKK (giaTri=SUM Quote.IsSelected :88-90) → chi tiết §2 |
| 14 | BCH bổ sung/hoàn thiện draft | UpdateContractDraftCommand guard duy nhất Phase==DangSoanThao (ContractFeatures.cs:156-157), KHÔNG check drafter ⇒ BCH SỬA được về cơ chế |
PARTIAL | Nhưng BCH KHÔNG XEM được: view-guard GetEligiblePhases (:334-351) map DangSoanThao chỉ cho Drafter/DeptManager (:342-343); role ProjectManager chỉ được DangGopY legacy. Command cũng KHÔNG có ApprovalWorkflowId (:136-145) ⇒ 0 đường re-pin V2 sau tạo |
| 15 | Đàm phán với TP (ngoài hệ thống) | Không phase riêng (DangDamPhan=LEGACY đã gỡ) — ghi vết = comment + attachment | SẴN (triết lý Mig 21: trạm offline = chứng từ đính kèm) | ContractPhase.cs:19 LEGACY; attachment upload KHÔNG phase-guard (ContractAttachmentFeatures.cs 0 hit guard Phase — chỉ changelog :84,:144) |
| 16 | TP ký nháy → BCH chuyển PMH (scan) | Upload attachment mọi phase | GAP nhỏ | AttachmentPurpose (ContractAttachment.cs:5-11) = DraftExport/ScannedSigned/SealedCopy/Other — KHÔNG có "ký nháy" (InitialSigned). Enum-extend int-column 0-migration (tiền lệ PE ComparisonTable=4) |
| 17 | PMH trình ký; CCM kiểm tra + ký nháy | Trình DangSoanThao→ChoDuyet (ContractWorkflowService.cs:70-88, init con-trỏ đôi :81-84); CCM = Step 1 của workflow V2 ApplicableType.Contract=3 — duyệt qua ApproveV2Async |
PARTIAL | 🔴 3 lỗ: (i) guard trình đòi role Drafter∨DeptManager (:73-78) — PMH mang Procurement bị 403 (lớp "vai đi qua không có"); (ii) approver KHÔNG XEM được HĐ ChoDuyet — GetEligiblePhases 0 entry ChoDuyet; (iii) KHÔNG vào inbox — PhaseActorRoles (:421-430) 100% legacy, 0 entry ChoDuyet |
| 18 | CEO ký nếu >5 tỷ | Step CEO trong workflow V2 (Level đích danh Director) — chạy được nếu CEO ký MỌI HĐ | GAP (phần ngưỡng) | CeoApprovalThreshold = cột chung ApprovalWorkflow.cs:43 nhưng grep ContractWorkflowService.cs = 0 hit (đọc trọn 443 dòng 07-29) — admin set vào workflow Contract-type bị NGÓ LƠ im lặng. AllowApproverFinalize cũng PE-only. Đường có sẵn 0-code: 2 workflow (có/không Step CEO) + user chọn lúc tạo |
| — | (kết GĐ3) | Duyệt cấp cuối → DaPhatHanh terminal + gen mã nếu chưa có (:366-385) |
SẴN | Terminal của V2 = "Đã phát hành" — nghĩa là b.19-21 GĐ4 xảy ra SAU terminal về mặt hệ thống |
| 19 | HR đóng dấu | 2 lựa chọn: (a) HR = Step cuối workflow V2 (SẴN, chỉ cấu hình — terminal dời về sau HR); (b) tracking riêng trên vật thể "bản cứng" GĐ4 | SẴN-config / GAP-nếu-tracking-riêng | DangDongDau=8 LEGACY (ContractPhase.cs:23) — KHÔNG hồi sinh. §3 đề xuất |
| 20 | CCM scan bản cứng, lưu trữ | AttachmentPurpose.SealedCopy=3 upload ở DaPhatHanh |
SẴN | ContractAttachment.cs:9; upload không phase-guard ⇒ up được sau terminal |
| 21 | Chuyển phát nhanh bản gốc về TP + lưu | Không field nào (mã vận đơn/số bản gốc/vị trí lưu) | GAP (nhỏ — comment đủ nếu owner không đòi field) | Contract.cs:8-70 đọc trọn: 0 field bản-cứng (không seal-date, không courier, không số-bản-gốc, không vị-trí-lưu-file-cứng) |
Kết Q1: cơ chế duyệt (17-18) + terminal + attachment (15,16,20) = TÁI DÙNG được ngay sau khi vá 3 lỗ "đường ống" (view/inbox/vai-trình) — đúng verdict LAI S156, hôm nay re-verify còn nguyên. GĐ4 (19-21) KHÔNG có vật mang trong Contract entity — cần quyết định §3 (field-on-Contract vs entity mới).
Đếm endpoint (đo 07-29): ContractsController.cs = 30 endpoint (8 đọc + 22 ghi), class [Authorize] trần :13; ngoại lệ DUY NHẤT có policy: GET /deleted [Authorize(Policy="Contracts.Read")] :28-29 (arc-2 S159) — khuôn per-action ĐÃ CÓ TIỀN LỆ ngay trong file này.
§2 — Q2: BRIDGE KHKK chen giữa PE và Contract
Hiện trạng bridge (đo 07-29): POST /api/purchase-evaluations/{id}/create-contract (PurchaseEvaluationsController.cs:339-346, body {ContractType, TenHopDong, Bypass} :379-382 — KHÔNG có ApprovalWorkflowId) → handler CreateContractFromEvaluationFeatures.cs: guard Phase==DaDuyet + winners.Count>0 + pe.ContractId==null (:53-60); per-winner giaTri = SUM Quote.IsSelected (:88-90); pin V1-only WorkflowDefinitionId=activeWfId (:68-71,:108); gen mã atomic self-committing codegen (:78-115); idempotency pe.ContractId = contracts[0].Id (:145). Picker: GET /purchase-evaluations/approved-pending-contract (:335-337) → ListApprovedPurchaseEvaluationsQuery filter Phase==DaDuyet && ContractId==null (CreateContractFromEvaluationFeatures.cs:173).
(a) KHKK kế thừa từ PE (đã RATIFIED spec S157 §2.3 — chỉ tóm + anchor sống): header PurchaseEvaluationId loose-Guid + ProjectId denorm + DepartmentId (C5) + snapshot HoSoLink; Lines auto-sinh từ winners pe.Suppliers.Where(IsWinner) với PeReferenceAmount = SUM Quote.IsSelected per-winner snapshot 1 lần — copy đúng phép tính bridge cũ :88-90 + guard :53-60. Khuôn nguyên con nằm sẵn trong file bridge.
(b) Contract tạo từ KHKK kế thừa gì (spec §2.5 RATIFIED, W5): (i) giaTri = line.ApprovedAmount khớp w.SupplierId thay SUM :88-90 khi PE có plan DaDuyet + ContextNote lệch "giá KH x vs SUM-PE y" vào changelog (khuôn :121-140); (ii) +param ApprovalWorkflowId pin V2 — thiếu nó HĐ từ phiếu rơi nhánh V1: nếu 0 active WorkflowDefinition cho type đó → KẸT CỨNG ConflictException ContractWorkflowService.cs:115-116 (re-verify 07-29: đúng dòng def == null || def.Steps.Count == 0); (iii) phần còn lại giữ nguyên từ PE (TenHopDong/PaymentTerms→DraftData/BudgetManual* :101-107); (iv) 🆕 ghi ngược Line.ContractId = contract.Id per-winner (cột C6 spec đã có chỗ) — vòng lặp :82-115 đã per-winner nên chỗ ghi tự nhiên.
(c) Sửa bridge cũ hay thêm bridge mới → SỬA BRIDGE CŨ (1 file), KHÔNG đẻ bridge thứ 2 — đây là hướng spec W5 đã ratify; lý do kỹ thuật đứng vững hôm nay: entry-point PE giữ idempotency pe.ContractId (:59-60,:145) + FE nút tạo HĐ đã tồn tại màn PE; bridge mới từ KHKK = endpoint thứ 2 cùng nghĩa (2 chỗ sửa về sau — lớp lỗi 2-site S155). Bổ sung DUY NHẤT cho dry-run: màn KHKK detail (DaDuyet) thêm nút deep-link gọi CÙNG endpoint cũ — 0 endpoint mới, 0 handler mới. (Phương án B ghi để review chốt: POST /contract-signing-plans/{id}/create-contracts riêng — chỉ đáng nếu owner muốn 1 KHKK chia nhiều đợt HĐ; hiện 0 yêu cầu.)
(d) Cardinality chuỗi PE↔KHKK↔Contract:
| Cặp | Ràng buộc | Cơ chế enforce |
|---|---|---|
| PE → KHKK | 1 PE — ≤1 plan SỐNG (1-N lịch sử, tạo lại được sau TuChoi) | app-level AnyAsync(p.PeId==x && p.Phase!=TuChoi) → 409 (spec §2.3; KHÔNG đặt được UNIQUE-filtered theo Phase động — đúng lựa chọn app-level) |
| PE → Contract | 1 PE — N HĐ (multi-winner Mig 58) | pe.ContractId = contracts[0].Id :145 + guard :59-60 "đã tạo HĐ rồi" |
| KHKK → Contract | 1 plan — N Lines (UNIQUE filtered PlanId+SupplierId) — mỗi Line ↔ ≤1 HĐ |
Line.ContractId Guid? (C6) ghi tại bridge; truy ngược HĐ→plan qua Line |
| ⚠️ đường tắt | PE → Contract KHÔNG QUA KHKK vẫn sống (Q11 mặc định = cảnh báo mềm) | dry-run phải chấp nhận 2 đường song song cho tới khi owner nâng cứng Q11 |
Pattern tái dùng cite được: (1) pull-model picker ListApprovedPurchaseEvaluationsQuery:173 — nhân bản đổi vị ngữ && chưa-có-plan-sống cho màn "PE chờ lập KH" (W2 spec cũ); picker tạo-HĐ đổi nguồn thành plan DaDuyet (hoặc giữ PE + banner có/không plan — Q11). (2) Multi-entity create + self-committing codegen atomic :78-82 comment + loop. (3) Auto-Lines từ winners :56-62,:82-91.
§3 — Q3: GĐ4 Hdc_* trang thật cần gì
🔴 SỬA GROUNDING đề bài (2 chỗ):
Hdc_*KHÔNG phải 6-leaf {WfView/List/Create/Pending/Approved/Deleted} — đó là cấu trúcKhkk_*(DbInitializer.cs:1779-1786).Hdc_*thật = 7 leaf theo ContractType, 1 leaf/loại, seed loopDbInitializer.cs:1862-1867(tree.Add(("Hdc_"+code, label+" - Bảng cứng", MenuKeys.HopDongCung, ...))), label "01. HĐ Thầu phụ - Bảng cứng"…"07." (:1980-1986), staticMap →/coming-soon?stage=4&t=01..07(fe-user Layout.tsx:63-69). Owner tự vẽ cấu trúc này @S159-đợt4 — KHÔNG mirror mù bộ 6-leaf.- Enum KHÔNG có "KyCung" — grep
KyCungtoànsrc= 0 hit. Phase 8 =DangDongDau[LEGACY](ContractPhase.cs:23), 9 =DaPhatHanhterminal SỐNG (:24). "Bản cứng" chưa từng là phase.
Đã có gì cho GĐ4 (đo): DaPhatHanh = terminal V2 gen-mã (ContractWorkflowService.cs:366-385) · AttachmentPurpose.ScannedSigned=2/SealedCopy=3 (ContractAttachment.cs:8-9) upload không phase-guard ⇒ up ĐƯỢC sau terminal · Contract.cs 0 field bản-cứng.
Đề xuất TỐI THIỂU cho dry-run (options — review chốt):
- (A) 0-migration DERIVE ⬅ đề xuất chính: mỗi leaf
Hdc_<Code>→ trang listGET /contracts?type=N&phase=9(paramphaseCÓ SẴNContractsController.cs:20, FE đã chứng minh dùng được: leafCt_*_Approved→/my-contracts?type=N&phase=9Layout.tsx:141); trạng thái bản cứng = DERIVEattachments.Any(Purpose==SealedCopy)→ badge "Đã lưu bản cứng / Chưa"; hành động = uploadScannedSigned/SealedCopy+ enum-extendInitialSigned=4ký-nháy (int column, 0-mig — tiền lệ PEComparisonTable=4). List DTO hiện KHÔNG trả attachments (ContractListItemDto12 fieldContractFeatures.cs:323-327) ⇒ cần +1 cột derivehasSealedCopytrong query (sửa DTO, 0-mig). - (B) field mỏng trên Contract:
HardCopyStatus int?(+HardCopyNote) — 1 mig AddColumn (Mig 70+, bảng giữ 89) khi owner muốn trạng thái TƯỜNG MINH (in/ký/đóng dấu/lưu/chuyển phát per bước b.19-21) thay vì derive từ attachment. - (C) entity
ContractHardCopyriêng — KHÔNG khuyến nghị cho dry-run: b.19-21 là 3 thao tác trên 1 HĐ đã phát hành, không có vòng đời duyệt riêng (giấy đã ký xong); bảng mới = màn admin + FK + configuration thừa.
Bind 7 leaf (phương án A): Hdc_<Code> → /hard-copies?type=<N> (page mới) hoặc rẻ nhất tái dùng /my-contracts?type=N&phase=9&hdc=1 biến thể hiển thị. Route mới = 4-place mirror ×2 app (App.tsx route fe-user/src/App.tsx:52-65 khuôn + Layout staticMap :63-69 đổi khỏi coming-soon + page + types) — khuôn MyContractsPage.tsx ĐÃ đọc đủ type/phase/deleted params (:29,:34-35) + tự rẽ /contracts/deleted (:41-46) ⇒ mirror được, page ~300-400 LOC/app theo khuôn.
Mirror khuôn 6-leaf Ct_ được không?* — Về CƠ CHẾ (regex resolvePath Layout.tsx:132-143 + query-param page) = ĐƯỢC và NÊN; về HÌNH DẠNG menu = KHÔNG (owner đã chốt 7-leaf-per-type cho Hdc). Nếu sau này owner muốn Pending/Approved trong Hdc thì nới regex Hdc_([^_]+)_(...) — hiện KHÔNG tự đẻ key mới (gotcha #84 seeder-only + đổi cấu trúc menu = hỏi owner).
Authz lớp menu: Hdc_* leaf + root HopDongCung đều NGOÀI MenuKeys.All (MenuKeys.cs:37-42 — CỐ Ý, comment ghi rõ) ⇒ 0 policy Hdc_*/HopDongCung.* tồn tại; per-action authz cho trang Hdc phải dùng policy root Contracts.{Action} (root ∈ All :168) — tiền lệ đúng khuôn: GET /contracts/deleted dùng Contracts.Read (ContractsController.cs:29).
§4 — Q4: "DRY-RUN" về mặt kỹ thuật (prod đang có 37 NCC / 17 user / 62 dự án seed / 7 HĐ V1 thật)
Đề xuất: phương án LAI "dự án test riêng", KHÔNG cờ IsDryRun.
| Phương án | Đánh giá | Chứng |
|---|---|---|
(i) Dự án test riêng ZZTEST + 1-2 NCC test ⬅ đề xuất cho GĐ3+GĐ4 |
Mã HĐ dry-run nằm CHUỖI SEQUENCE RIÊNG — prefix codegen = {ProjectCode∨Year}/{typeCode}/SOL&{SupplierCode} (ContractCodeGenerator.cs:33), seq per-prefix (:40-53) ⇒ KHÔNG ăn số của dự án thật. 2 loại framework scope=Year vẫn tách nhờ SupplierCode test. Dự án test NGOÀI danh sách seed ⇒ soft-delete xong KHÔNG resurrect (#75/#76 chỉ áp per-code trong seed SeedRealMasterDataAsync ungated). Tạo qua UI CRUD, 0 code |
ContractCodeGenerator.cs:33,:40-53 |
(ii) Cờ IsDryRun — KHÔNG khuyến nghị |
+1 mig + PHẢI sửa MỌI list/inbox/picker query (blast hàng chục handler — đúng lớp lỗi cardinality S87/S88 "sửa write-site sót read-site") + FE filter ×2 app. Đắt hơn giá trị 1 đợt dry-run | bài học feedback_cardinality_change_grep_consumers |
| (iii) Cứ dùng thật | ĐÚNG cho KHKK GĐ2 (7 bảng mới 0-data — mỗi phiếu đầu đều là "thử", xóa mềm sạch); SAI cho Contract GĐ3 (7 HĐ V1 prod thật chạy nhánh V1 ContractWorkflowService.cs:97 comment + 37 NCC thật trong picker + mã ăn sequence thật nếu chọn dự án thật) |
— |
Rollback path (soft-delete sẵn có + 2 hố):
- Cơ chế nền: soft-delete qua interceptor (Remove→IsDeleted, lớp #81-EXT indirect) + global
HasQueryFilter(!IsDeleted)— bằng chứng Contract có filter:ListDeletedphảiIgnoreQueryFilters().Where(IsDeleted)(ContractFeatures.cs:372-373+ LUẬT comment:356-360). Màn "Đã xóa" chỉ-xem ĐÃ CÓ cả Contract (arc-2) lẫn PE (S155). - 🔴 HỐ 1 — HĐ dry-run chạy trọn vòng KHÔNG XÓA ĐƯỢC qua API: guard
DeleteContractCommand=Phase >= DangInKy(ContractFeatures.cs:632-633) là so sánh SỐ trên enum ⇒ chặn cảDaPhatHanh=9,ChoDuyet=10,TraLai=98,TuChoi=99(chỉ xóa được phase 2/3/4). Đường vòng tồn tại nhưng xấu: Admin manual override settargetPhase=DangSoanThao(ContractWorkflowService.cs:191-198) rồi DELETE. ⇒ dry-run phải CHẤP NHẬN HĐ test DaPhatHanh nằm lại (lọc bằng dự án ZZTEST) hoặc dùng đường vòng admin — ghi vào checklist wave cuối, review chốt. - HỐ 2 — phiếu KHKK/PE
DaDuyetcũng không xóa (KHKK spec allow-listDangSoanThao|TuChoimirror PE:1404) — cùng cách xử lý. - Sequence KHÔNG rollback — gap số chấp nhận được, chính comment repo nói vậy (
ContractFeatures.cs:113-115"wasted... Acceptable"). - Notification dry-run bắn cho user THẬT (in-app
LogTransitionAsync→NotifyAsyncContractWorkflowService.cs:407-427) — không tắt được per-record; dặn trước team.
Rủi ro lẫn data thật (liệt kê để checklist): (1) record test hiện CHUNG list với data thật (IDOR theo role; admin thấy hết) → convention "mọi record dry-run thuộc ZZTEST" + tên phiếu tiền tố; (2) CẤM đặt Code dự án/NCC test TRÙNG danh sách seed (twin FLOCK-01 S148 — seed re-fill theo Code); (3) pe.ContractId idempotency — mỗi vòng thử cần PE test MỚI (:59-60); (4) file upload thật nằm wwwroot/uploads — delete attachment có cleanup, HĐ xóa mềm thì file ở lại (rác chấp nhận); (5) menu đã public 13/13 role + authz trần ⇒ trong lúc dry-run ai cũng bấm được nút ghi trên HĐ THẬT — sang §5.
Ràng buộc đo đạc: SSH prod chết khi mở SQL-client (S134/S148 re-confirm) ⇒ phép đo prod trong §7 ưu tiên curl API làm chân chính; sqlcmd chỉ chạy local/Dev hoặc đưa lệnh cho người có RDP.
§5 — Q5: AUTHZ khoanh vùng cho dry-run (options — REVIEW CHỐT, không tự quyết)
Hiện trạng lộ (menu public 13/13 role ❹ + [Authorize] trần): bất kỳ user đăng nhập nào gọi được 22 endpoint ghi của ContractsController (:13 class-trần; ngoại lệ duy nhất /deleted có policy :28-29). Phân nhóm theo độ nguy hiểm:
| Nhóm | Endpoint | Nguy hiểm THẬT (đã re-verify service) |
|---|---|---|
| 🔴 Workflow | POST {id}/transitions (:61-66) |
Reject-trước-guard ContractWorkflowService.cs:49-66: nhánh Reject KHÔNG check role, KHÔNG check phase-nguồn ⇒ user bất kỳ TraLai/TuChoi bất kỳ HĐ ở bất kỳ phase — kể cả HĐ THẬT DaPhatHanh (đặt TuChoi=99 lên HĐ đã phát hành). Nhánh Approve NGƯỢC LẠI có guard (OR-of-N 403 :259-265; trình đòi Drafter/DeptManager :73-78) |
| 🔴 CRUD | POST / · PUT {id} · DELETE {id} |
Create = tạo HĐ rác + ĂN SEQUENCE thật ngay lúc tạo (gen mã tại create ContractFeatures.cs:113-118) nếu chọn dự án thật; Update draft KHÔNG check drafter (:151-157) ⇒ sửa GiaTri nháp người khác; Delete không role-check (chỉ phase-guard :632-633) |
| 🟠 Attachment | POST/DELETE {id}/attachments |
Không phase-guard (§1) ⇒ thay/xóa file scan trên HĐ THẬT đã phát hành; delete cleanup file vật lý |
| 🟠 Details | 15 endpoint add/update/delete details (:123-226) |
Sửa ruột chi tiết 7 loại HĐ mọi lúc (handler có lock-edit 17 handler phase≠DangSoanThao? — lock S8/S9 áp Contract handlers cũ; details HĐ đã qua guard này, mức rủi ro còn lại là nháp người khác) |
| 🟡 Comment | POST {id}/comments |
Spam/ghi đè ngữ cảnh — nhẹ |
| 🟡 Form-export | FormsController (api/forms, cũng [Authorize] trần :11): render/export-pdf (:25,:32) = STATELESS đọc-render, rủi ro thấp; POST templates (:42) = ghi template dùng chung — cùng lỗ class-trần, NGOÀI ContractsController |
đo 07-29 |
Khuôn per-action CÓ SẴN trong repo (đối chiếu): (a) Master: SuppliersController.cs:67-99 4× [Authorize(Policy="Suppliers.Update")]; (b) chính ContractsController: /deleted dùng Contracts.Read :28-29; (c) S155 kết luận cho PE: root-policy dùng được NGAY vì root ∈ MenuKeys.All — áp y nguyên cho Contract (Contracts ∈ All MenuKeys.cs:168 ⇒ 4 policy Contracts.{Read,Create,Update,Delete} ĐÃ đăng ký runtime, 0 key mới, 0 mig). 🔴 Sửa grounding đề bài #3: "khuôn PE controller per-action 3 tầng" KHÔNG tồn tại — PurchaseEvaluationsController cũng class-trần (S155 đo :15); khuôn per-action thật nằm ở Master + /deleted.
OPTIONS cho review chốt (chi phí ↑ dần):
- O-A GIỮ NGUYÊN + chấp nhận có ý thức (đúng O-3 "hợp đồng cứ từ từ" + tinh thần dry-run mở): rủi ro tập trung 2 ô 🔴; giảm nhẹ bằng quy ước ZZTEST + backup DB trước đợt dry-run (
scripts/backup-sql.ps1). 0 file. - O-B PER-ACTION ROOT-POLICY 22 endpoint ghi (
Contracts.Create/Update/Deletetheo verb): 1 file controller, 0 key mới, tiền lệ/deleted. ⚠️ kèm bắt buộc: cấp CanCreate/CanUpdate/CanDelete cho role tham gia dry-run QUA SEEDER (gotcha #84, KHÔNG SQL tay) TRƯỚC deploy — không thì 403 im lặng (gotcha #44) làm chết chính dry-run. Chú ý: policy chặn theo MENU-permission, user được cấp vẫn đụng được HĐ THẬT ⇒ O-B thu hẹp AI bấm, không thu hẹp BẤM VÀO ĐÂU. - O-C = O-B + vá tối thiểu Reject-trước-guard: thêm 1 điều kiện
fromPhase==ChoDuyetcho nhánh Reject (ContractWorkflowService.cs:49) — đóng đúng ca "TuChoi HĐ DaPhatHanh", không đổi hành vi hợp lệ nào (Reject hợp lệ chỉ có nghĩa khi đang ChoDuyet). Chạm service HĐ ⇒ cần anh gật riêng (O-3). - O-D khoanh DATA: trước dry-run, đo phase 7 HĐ V1 thật (curl admin
GET /api/contracts— SSH-SQL không dùng được) — nếu cả 7 đã DaPhatHanh thì O-C đóng được ~hết mặt tấn công transitions lên chúng. UNCERTAIN hiện tại: phase thật của 7 HĐ V1 chưa đo được từ đây — khai đúng trạng thái, không đoán.
§6 — Q6: WAVE-STRUCTURE KỸ THUẬT GĐ3+GĐ4 (nối W1-W5 cũ; đánh số tiếp W6→W9)
Nguyên tắc giữ nguyên spec cũ: mỗi wave deploy độc lập, không để nửa tính năng chết trên prod. Đơn vị độ lớn = số file + có/không mig (KHÔNG ước giờ). Số bảng nền hôm nay: 89
sys.tables(88 app + history); mig cuối trên đĩa =20260727033522_AddPeAllowApproverDelete⇒ mig kế tiếp = 69 (W1 KHKK dùng), GĐ3/GĐ4 dưới đây 0 mig trừ W8 nếu owner chọn (B).
| Wave | Tên | Deploy riêng? | Người dùng THẤY gì | Mig? | Reuse gì | Độ lớn (file) |
|---|---|---|---|---|---|---|
| W6 | Đường ống HĐ V2 (điều kiện tiên quyết GĐ3) | ✅ có — 0 UI mới, HĐ V2 hết "vô hình" | Approver V2 xem được + thấy trong inbox HĐ đang chờ mình (nay: không) | ❌ 0 | mở rộng GetEligiblePhases (ContractFeatures.cs:334-351) + inbox PhaseActorRoles (:421-430) theo khuôn PE ResolveV2InboxIdsAsync; guard trình :73-78 nới cho Procurement/drafter-của-HĐ (lớp 403-PMH) |
2-3 file BE (ContractFeatures + service guard [+ DTO nếu trả currentApproval]) |
| W7 | Cầu KHKK→HĐ + pin V2 (= W5 spec cũ, đổi tên cho liền mạch) 🔴 CHẶN owner (chạm Contract, Q6/Q11) | ✅ có | Nút "Tạo HĐ" từ KHKK DaDuyet; HĐ sinh ra chạy trạm V2 thật (hết kẹt ConflictException :115-116) |
❌ 0 | CreateContractFromEvaluationFeatures.cs (+param ApprovalWorkflowId, giá từ ApprovedAmount, ContextNote lệch, ghi Line.ContractId) + body record PurchaseEvaluationsController.cs:379-382 + 1 nút FE ×2 app |
2 BE + 2 FE |
| W8 | GĐ4 bản cứng Hdc_* (7 leaf hết ComingSoon) |
✅ có | 7 trang "…- Bảng cứng" list HĐ phase=9 + badge trạng thái bản cứng + upload ScannedSigned/SealedCopy/InitialSigned |
❌ 0 nếu (A); ✅ 1 mig AddColumn nếu owner chọn (B) HardCopyStatus |
param phase (ContractsController.cs:20) + khuôn MyContractsPage.tsx:29-46 + enum-extend AttachmentPurpose (ContractAttachment.cs:5-11) + staticMap Layout.tsx:63-69 re-point |
~6-8 FE (page+types+App.tsx+Layout ×2 app) +1-2 BE (DTO thêm cờ derive) |
| W9 | Authz khoanh vùng (theo option §5 review chốt) | ✅ có | (không thấy gì nếu làm đúng) — hoặc 403 sạch thay vì cho phép âm thầm | ❌ 0 | khuôn /deleted :28-29 + SuppliersController.cs:67-99; + seeder cấp quyền (gotcha #84) |
1 BE controller + 1 DbInitializer (+1 service nếu O-C) |
Thứ tự & phụ thuộc: W6 phải TRƯỚC W7 (không thì HĐ pin V2 xong vẫn không ai duyệt được — đúng lỗi "cơ chế đúng, đường đi không có"). W8 độc lập hoàn toàn với W6/W7 (chỉ đọc HĐ DaPhatHanh) ⇒ chạy song song được — hữu ích cho dry-run vì owner thấy GĐ4 sớm. W9 nên đi trước hoặc cùng buổi mở rộng người bấm; nếu review chọn O-A thì W9 = 0 file (ghi quyết định vào plan cha).
Kết nối W1-W5 cũ: W1-W4 (KHKK) không đụng Contract ⇒ chạy trước/song song thoải mái. W5 cũ ≡ W7 ở đây (đổi nhãn để khỏi 2 tên cho 1 việc — plan cha nên chọn 1 cách gọi).
§7 — Q7: ACCEPTANCE ĐO ĐƯỢC per wave (khuôn H5: pin N ≥ x + TÊN test cụ thể; neo TÍNH-CHẤT tránh literal tự-lão-hoá)
⚠️ Neo số test: KHÔNG viết "562 + N" như spec cũ (mốc 562 tự lão hoá +
Nthả tự do = 0 bit). Viết:dotnet test SolutionErp.slnxPASS 0 fail, và số test SAU ≥ số test TRƯỚC + x, và x test dưới đây tồn tại ĐÚNG TÊN. Baseline đo NGAY TRƯỚC wave bằng chính lệnh đó. Phép prod theo khuôn 3-chân-kiềng (cicd): ① CI run PASS · ② artifact/bundle prod verify byte-level (curl-grep, #77) · ③ dữ liệu prod đúng (curl API — KHÔNG sqlcmd qua SSH, chết client S134/S148).
W6 — Đường ống HĐ V2 (≥4)
- Test-before, class
ContractV2PipelineTests(tests/SolutionErp.Infrastructure.Tests/Application/), ≥3 tên:Inbox_ApproverCapDangCho_ThayHopDongChoDuyet·Inbox_CapKhac_KhongThayHopDong(ca ÂM) ·View_ApproverV2_KhongPhaiDrafter_XemDuocHopDongChoDuyet(nay ForbiddenExceptionContractFeatures.cs:488-489). dotnet testPASS 0 fail; số test ≥ baseline + 3.- Phép prod ③: đăng nhập user approver-V2 (không Admin) →
curl GET /api/contracts/inboxtrả ≥1 HĐ đangChoDuyetmà user là Level hiện tại; cùng userGET /api/contracts/{id}→ 200 (trước W6: inbox 0 + detail 403). - Ca ÂM giữ nguyên đường cũ: user KHÔNG thuộc Level nào và không Drafter →
GET /{id}vẫn 403 (chứng không mở toang bằng cách bỏ guard). - Phép prod ①: CI run xanh trên commit W6.
W7 — Cầu KHKK→HĐ + pin V2 (≥4)
- Test-before
ContractSigningPlanBridgeTests:Bridge_CoPlanDaDuyet_GiaTriHopDong_BangApprovedAmountCuaDungNCC·Bridge_KhongCoPlan_GiuSumBaoGiaPE_VaCanhBaoMem·Bridge_PinApprovalWorkflowV2_HopDongTrinhDuoc_KhongConConflictException·Bridge_GhiNguoc_LineContractId_DungTungWinner. dotnet testPASS 0 fail; số test ≥ baseline + 4; test cũCreateContractFromEvaluationMultiWinnerTests(đã tồn tại trên đĩa) vẫn xanh — chống hồi quy multi-winner.- TÍNH-CHẤT đo được (thay literal): với 1 PE có W winner và plan
DaDuyet,POST /{peId}/create-contracttrả đúng W contractId, và mỗi HĐ cógiaTri == ApprovedAmountcủa Line cùngsupplierId; tổng W HĐ ==SUM(ApprovedAmount)toàn plan. - Phép prod ③ (curl, dự án ZZTEST): HĐ vừa tạo →
POST /{id}/transitions {targetPhase:10, decision:1}bởi Drafter → 204 (không 409ConflictException); đọc lạiGET /{id}thấyphase=10+currentApprovalLevelOrder=1. - Changelog có ContextNote so-lệch khi giá KH ≠ SUM-PE (đọc
GET /{id}/changelogs, chuỗi chứa cả 2 số).
W8 — GĐ4 bản cứng (≥4)
- Test
ContractHardCopyTests:Danhsach_BangCung_ChiTraHopDong_PhaseDaPhatHanh·CoBanCung_Derive_TuAttachment_SealedCopy(+ nếu chọn (B):HardCopyStatus_MacDinh_Null_KhongVoLuoc). dotnet testPASS 0 fail; số test ≥ baseline + 2 (hoặc +3 nếu (B)).- FE:
npm run buildPASS ×2 app; 7 leafHdc_*KHÔNG còn trỏ/coming-soon— grepHdc_trongfe-user/src/components/Layout.tsx+fe-admin/.../Layout.tsxtrả 0 hit chuỗicoming-soontrên các dòngHdc_. - Phép prod ②+③: sau deploy, curl-grep bundle prod chứa route mới (byte-level, #77 — cache≠deploy); rồi curl
GET /api/contracts?type=1&phase=9trả list (200) và mọi item cóphase==9. - Ca ÂM: leaf Hdc của loại KHÔNG có HĐ phát hành nào → trang rỗng có empty-state, KHÔNG lỗi 500.
W9 — Authz (≥4; nếu review chọn O-A thì wave này = ghi quyết định, các acceptance dưới KHÔNG áp)
AuthorizePolicyRegressionTests(ĐÃ TỒN TẠItests/SolutionErp.Infrastructure.Tests/Api/) vẫn xanh — nó bắt ca policy trỏ menu-key ngoàiMenuKeys.All(⇒ chứngContracts.*hợp lệ, không đẻ 500).- Test mới
ContractsControllerAuthzTests:MoiEndpointGhi_CoAuthorizePolicy(reflection đếm, neo TÍNH-CHẤT: mọi action[HttpPost|HttpPut|HttpDelete]củaContractsControllercó[Authorize(Policy=...)]— không neo con số 22) +User_ThieuQuyen_GoiPost_Tra403(ca ÂM, KHÔNG 500/200). dotnet testPASS 0 fail; số test ≥ baseline + 2.- Phép prod ③ sau deploy: user dry-run ĐÃ được seeder cấp quyền →
POST /api/contracts201; user chưa cấp → 403 (không 500). ⚠️ chạy ngay sau deploy: 403 nhầm cho người đang dry-run = tự chặn chính mình (gotcha #44). - Nếu chọn O-C:
Reject_KhiHopDongDaPhatHanh_BiChan(ca ÂM cho lỗContractWorkflowService.cs:49-66) + verify HĐ thật không đổi phase.
Gate 5-anchor (ràng buộc run.md) — anchor gợi ý cho mỗi wave: W6 = ContractFeatures.cs:334-351 · :421-430 · :488-489 · ContractWorkflowService.cs:73-78 · ContractsController.cs:38-40. W7 = CreateContractFromEvaluationFeatures.cs:53-60 · :68-71 · :88-90 · :145 · ContractWorkflowService.cs:115-116. W8 = ContractAttachment.cs:5-11 · DbInitializer.cs:1862-1867 · Layout.tsx:63-69 · MyContractsPage.tsx:29-46 · ContractsController.cs:16-24. W9 = ContractsController.cs:13 · :28-29 · SuppliersController.cs:67 · MenuKeys.cs:163-181 · ContractWorkflowService.cs:49-66.