# sub-invest-kehoach-1 — "Kế hoạch ký kết Hợp đồng" (PDF b.7→12) — quy trình + wire + wave > **Run:** `2026-07-28-S157-ke-hoach-ky-ket-hd` · agent: investigator-codebase (/fable-real, effort max) > **Trạng thái file:** GHI TRONG LÚC LÀM — mục chưa điền ghi `[PENDING]`. Propose-only; lead ghi spec sau verify. > **Nguồn:** run.md S157 (§1 grounding lead) · src-QT-TRINH-KY-HD.txt · src-FO-002.01 · run.md S156 · sub-invest-bch-1.md · review-synthesis.md §G. > **Luật đo:** mọi claim load-bearing neo file:line TỰ ĐỌC. Doc/skill = giả thuyết. Đĩa thắng. > **🔴 OWNER CHỐT GIỮA CHỪNG (run.md §1.7, nhận qua lead lúc đang điều tra):** > **(O-1)** *"Đúng chính xác, cấu trúc trình ký giống, chỉ khác nội dụng thôi"* ⇒ phiếu KH TÁI DÙNG khung duyệt V2 > (`ApprovalWorkflow`>Step>Level OR-of-N + LevelOpinions UPSERT); DỰNG MỚI = NỘI DUNG phiếu. Trục cơ-chế Q2 ĐÓNG; > option-space §D(6) chỉ còn trục NỘI DUNG (phiếu độc lập ⟂ mở rộng PE ⟂ entity con Contract). > **(O-2)** *"Cái này để tham khảo thôi, ko vấn đề j, đa số là trễ"* ⇒ SLA = hiển thị tham khảo; KHÔNG SLA-engine, > KHÔNG auto-approve-on-timeout, KHÔNG wave SLA; giữ hiển thị deadline; `AddDays(7)` hardcode = 1 dòng ghi nhận. ## §A — QUY TRÌNH CHI TIẾT khúc còn thiếu (hoà giải 3 nguồn) ### A.0 Khung — khoảng trống nằm giữa ISO-1 và ISO-2 (xác nhận Phát hiện B của lead từ nguồn gốc) Em tự đối chiếu: ISO doc bảng 2 (`src-QT-TRINH-KY-HD.txt:60-61`) đi THẲNG "Lựa chọn NTP/NCC" → "Soạn thảo hợp đồng", **0 bước trung gian**. PDF sơ đồ (bản 14/2026, `runs/...S156/run.md` bảng 21 bước) chèn b.7→12 đúng chỗ đó. FO-002.01 là tờ cover của KHÚC HĐ (ISO bước 5-7), không nói gì khúc 7→12. ⇒ **PDF là nguồn DUY NHẤT có khúc này** — spec ra từ đây vừa là spec phần mềm vừa là đề xuất bổ sung ISO. Vật mang trong hệ thống = **1 PHIẾU "Kế hoạch ký kết Hợp đồng"** (owner đặt tên, đầu ra = CON SỐ có phê duyệt), vòng đời 5-trạng-thái mirror PE, duyệt bằng **khung V2 có sẵn** (O-1). ### A.1 Bảng bước b.7→12 → ngôn ngữ hệ thống Role hệ thống có thật (`AppRoles.cs:5-16`): Procurement (PMH/PRO) · CostControl (CCM) · Director+AuthorizedSigner (BOD/NĐUQ) · ProjectManager/Drafter (phía BCH) · HrAdmin · Admin. "BCH dự án X" **không có model** (Q1 S156 — `Department.ManagerUserId` 0-row, PositionLevel NULL) → phiếu KH né bằng: BCH = **người tạo phiếu** (CreatedBy), PMH/CCM/CEO = **đích danh user trong Level** của workflow V2 (đúng triết lý V2, 0 model mới). | b | Việc | Ai (role thật) | Đầu vào | Đầu ra | Điều kiện chuyển | Nhánh lỗi/trả lại | |---|---|---|---|---|---|---| | 7 | PMH mail chuyển thông tin BCH/NTP/NCC | Procurement | PE chạm `DaDuyet` (winners `IsWinner`, giá `SUM Quote.IsSelected`, HoSoLink NAS) | BCH biết việc → vào lập KH | Hệ thống = **pull-model**: màn "PE đã duyệt — chờ lập KH" lọc `Phase==DaDuyet && chưa có plan` (khuôn `CreateContractFromEvaluationFeatures.cs:173` filter `DaDuyet && ContractId==null`) + notification in-app. Email ra NGOÀI (NTP/NCC) = ngoài scope (SMTP TODO toàn hệ thống) | PE thiếu winner → không hiện trong màn (guard sẵn ở PE) | | 8 | Duyệt mẫu + shopdrawing với TVGS | BCH nhập; TVGS = người NGOÀI hệ thống, **BCH nhập hộ kết quả** (tên TVGS + ngày + scan) — hướng (a) Q7 S156, sơ đồ không đòi account TVGS | Mẫu vật liệu + shopdrawing từ TP/NCC (offline) | Danh mục CĂN CỨ trong phiếu KH: per-item {Kind: MauVatLieu\|Shopdrawing, NCC nào, Status, TvgsName, ngày, file scan} | Vòng đời PER-ITEM: `ChuaNop→DaNop→TvgsDuyet\|TvgsBac`; `TvgsBac→DaNop` (nộp lại) — **sơ đồ KHÔNG vẽ vòng lặp này**, hệ thống phải có vì thực tế TVGS bác | Item bị bác = đổi status + note, KHÔNG đụng state machine phiếu | | 9 | Tổng hợp hồ sơ + so sánh giá → ĐỀ XUẤT giá trị ký HĐ | BCH (= Drafter phiếu KH; role thường ProjectManager) | Căn cứ b.8 + báo giá PE (snapshot `PeReferenceAmount` per-winner lúc tạo phiếu — nguyên tắc snapshot Mig 67) | Phiếu KH điền đủ: dòng giá trị ĐỀ XUẤT per-winner + file so sánh giá → **TRÌNH** (`DangSoanThao→ChoDuyet`) | Guard trình: dòng giá đề xuất đủ per-winner (mirror completeness-guard S60 PE `PurchaseEvaluationWorkflowService.cs:180-212`); căn cứ b.8 chưa đủ TvgsDuyet → **soft-warn** (tiền lệ S62 vượt-NS-cảnh-báo-mềm) — cứng/mềm = owner quyết (O-Q1 dưới) | Ghi chú sơ đồ: "hồ sơ sai sót thiếu khối lượng, sai spec, thiếu phạm vi → gửi mail làm rõ" = comment + item TvgsBac | | 10 | PMH kiểm tra hồ sơ so sánh giá, xác nhận giá trị | Procurement (đích danh user trong Level) | Phiếu ChoDuyet Bước 1 | Duyệt → Bước 2; ý kiến vào `LevelOpinions` (UPSERT khi duyệt — khuôn `ContractWorkflowService.cs:292-316`) | OR-of-N trong Cấp (`ApprovalWorkflow.cs:81-94`) | Trả lại → `TraLai` (reset) hoặc 4 return-mode per-level (cờ CÓ SẴN `ApprovalWorkflowLevel.cs:116-125`, service mirror PE `:421-424`) | | 11 | CCM kiểm tra | CostControl (đích danh trong Level) | Bước 2 | Duyệt → Bước 3 | như trên | như trên | | 12 | **CEO ký HOẶC CCM đóng dấu approval** | Director; nhánh "CCM đóng dấu" = cơ chế kết thúc sớm | Bước 3 (hoặc kết thúc tại Bước 2) | **GIÁ TRỊ KÝ KẾT CHỐT per-winner** (`ApprovedAmount`) ghi tại finalize choke-point — mirror `ApplyApprovedPriceOnFinalize` PE `:973-1002` + RULE "mọi nhánh set DaDuyet phải gọi helper" `:1008` | Terminal `DaDuyet` | Từ chối → `TuChoi` (terminal); trả lại → `TraLai` | **Cơ chế rẽ b.12 "hoặc" — 4 đường ĐỀU CÓ SẴN trong khung V2 (O-1), 0 migration:** (i) `CeoApprovalThreshold` + CCM tích "duyệt done miễn CEO" — PE service `:892-929` (fail-closed: đòi threshold + role CostControl + giá < ngưỡng); (ii) `AllowApproverFinalize` per-slot opt-out — PE `:870-890`; (iii) `AllowApproverSkipToFinal` per-slot (CEO vẫn ký thật) — Contract `:320-352`; (iv) 2 workflow (có/không Step CEO) admin pin. Điều kiện rẽ THẬT (ngưỡng? loại gói?) = **cả 3 nguồn im lặng** → Q3 owner còn treo, nhưng chọn đường nào cũng không đổi schema. **Tiếp giáp ra (b.12→13):** phiếu KH `DaDuyet` → cầu `POST /purchase-evaluations/{id}/create-contract` hiện có (`CreateContractFromEvaluationFeatures.cs`) tạo HĐ draft `DangSoanThao` — sửa cầu đọc `ApprovedAmount` từ KH thay `SUM Quote.IsSelected` (`:88-90`) + audit lệch PE-vs-KH + gate "chưa có KH duyệt thì cảnh báo/chặn" (Q11 mềm/cứng). Khúc b.13→21 = verdict LAI S156, KHÔNG bàn lại. ### A.2 b.8-9 là GÌ: **bảng con trong phiếu KH** — không phải phiếu con, không phải module riêng Repo 0-hit `shopdrawing|TVGS|duyệt mẫu` (verify S156 §0, em không đo lại — không có gì để đo). Chọn **satellite table `DossierItems` + attachments trong phiếu KH** vì: (1) owner phát biểu b.8-9 = **CĂN CỨ** của con số, không phải đối tượng duyệt riêng; (2) PDF chỉ vẽ MỘT chuỗi duyệt (10-12) cho cả khúc — phiếu con riêng = đẻ chuỗi duyệt thứ 2 không có trên sơ đồ; (3) vòng đời mẫu/shopdrawing là per-item status (nộp/duyệt/bác/nộp lại), không phải state machine phiếu; (4) TVGS ngoài hệ thống → chỉ cần ghi vết, không cần workflow. Module riêng chỉ đáng khi mẫu/shopdrawing cần quản lý ĐỘC LẬP với gói thầu (chưa có yêu cầu). ### A.3 Mâu thuẫn / im lặng giữa 3 nguồn (bảng tra) | # | Điểm | ISO doc | PDF sơ đồ | FO-002.01 | Xử lý | |---|---|---|---|---|---| | 1 | Khúc b.7→12 | **KHÔNG CÓ** (1→2 thẳng) | CÓ (duy nhất) | không nói | Spec = đề xuất bổ sung ISO | | 2 | SLA khúc 7→12 | im lặng | im lặng (10-14d là b.1-4; 3d + 7-10d là b.13-18) | im lặng | **O-2: tham khảo** — không hoà giải, không engine | | 3 | SLA khúc HĐ (13→19) | 07/07/07/07/03/01 ngày | 3d + 7-10d | **"mỗi bộ phận 01 ngày, quá hạn XEM NHƯ ĐÃ THÔNG QUA"** (auto-pass!) | 3 nguồn 3 kiểu; auto-pass mâu thuẫn mô hình duyệt chặn V2 (V2 không có auto-approve; V1 từng có System AutoApprove) — O-2 → chỉ ghi nhận | | 4 | Điều kiện rẽ b.12 CEO vs CCM | không có bước | "hoặc" — KHÔNG điều kiện | không nói | Q3 owner; 4 cơ chế sẵn (A.1) | | 5 | Nhánh trả lại 10/11/12 | không vẽ | không vẽ | không vẽ | Hệ thống có sẵn TraLai + 4 return-mode per-level | | 6 | Bypass Chủ đầu tư → GĐ trực tiếp | không nói | không nói | CÓ ("chuyển trực tiếp Giám đốc") | = `Contract.BypassProcurementAndCCM` (đã có trong code) — KHÔNG áp phiếu KH (KH chỉ sinh từ PE = TP/NCC) | | 7 | Nội dung kiểm tra cấu trúc per-phòng (PRO: điều khoản/thanh toán/rủi ro pháp lý · CCM: giá vs NS/rủi ro) | mô tả lời | không | CÓ (checkbox từng mục) | Hệ thống chỉ có `LevelOpinions.Comment` tự do — checklist cấu trúc = GAP ghi nhận, không blocking (owner quyết có số hoá checkbox không — O-Q2) | | 8 | Tên phòng/vai | PRO/CCM/BOD/NĐUQ/HRA | Procurement/CCM/CEO | PB-DA/PRO/CCM/GĐ | Khớp `AppRoles`: Procurement/CostControl/Director+AuthorizedSigner/HrAdmin — nhất quán, 0 mâu thuẫn | | 9 | Chuỗi ký FO-002.01 (ĐỀ XUẤT→PRO→CCM→GĐ) | ≈ bước 5-7 | ≈ b.9→10→11→12 CÙNG HÌNH | CÓ | Đối chứng giấy cho O-1: khung Step(Phòng)×Level+Opinions gánh được cả 2 khúc | **Câu hỏi owner còn treo sau run này (rút từ 14 câu S156, trừ đã chốt O-1/O-2/Q7a/Q8-ngoài-scope):** O-Q1 gate căn cứ b.8 khi trình: mềm (khuyến nghị, tiền lệ S62) hay cứng? · O-Q2 checkbox FO-002.01 số hoá hay comment tự do? · Q3 điều kiện rẽ b.12 (chọn cơ chế i-iv) · Q6 xác nhận giá KH thay SUM-PE khi tạo HĐ (mặc định thiết kế: KH thắng + audit lệch) · Q11 gate tạo-HĐ khi chưa có KH duyệt: mềm/cứng. ## §B — CÁCH WIRE vào codebase (twin THẬT, file:line) > O-1 chốt: **tái dùng khung duyệt V2** (`ApprovalWorkflows`/Steps/Levels + cột dùng chung CeoApprovalThreshold > `ApprovalWorkflow.cs:43` + 9 cờ per-slot `ApprovalWorkflowLevel.cs:116-169`) — **0 bảng approval mới**. > Dựng mới = NỘI DUNG phiếu. Twin đo thật: **Proposal** (BE 849 LOC · FE 1.956 LOC · 4 bảng Mig 38 — em wc/grep > trực tiếp) = khuôn CRUD/menu; **Contract ApproveV2Async** (`ContractWorkflowService.cs:217-394`) = khuôn service; > **PE** = khuôn finalize/giá-chốt (BE ~6.5K · FE ~14.9K = cận trên, không cần tới). ### B.1 ⚠️ Bẫy twin đo được lượt này — CẤM copy Proposal-lite cho phần DUYỆT `ProposalFeatures.cs` ApproveProposalHandler đi **global-level-order**: flatten mọi Level xuyên Steps (`:427-429` `SelectMany`), match `ElementAtOrDefault(order-1)` **1 row** (`:433`) + so đúng **1 user** (`:439` `currentSlot.Level.ApproverUserId != currentUser.UserId`). ⇒ 2 row cùng Order (OR-of-N hợp lệ theo `ApprovalWorkflow.cs:81-94`) bị biến thành 2 nấc TUẦN TỰ (AND) — **phá ngữ nghĩa OR-of-N**, và không có con trỏ Step (entity Proposal `:27` chỉ có `CurrentApprovalLevelOrder`, KHÔNG có `CurrentWorkflowStepIndex`). Phiếu KH cần 3 Bước (PMH→CCM→CEO) ⇒ **service PHẢI copy `ContractWorkflowService.ApproveV2Async:217-394`** (con trỏ đôi StepIndex+LevelOrder, GroupBy Order `:246`, OR-of-N `:259-260`, LevelOpinions UPSERT `:292-316`, skipToFinal `:320-352`) — Proposal chỉ làm khuôn cho CRUD/DTO/menu/FE. ### B.2 Schema — Mig **69** `AddContractSigningPlans` (mig cuối hiện tại = 68 `20260727033522_AddPeAllowApproverDelete.cs`, em ls trực tiếp) **7 CreateTable, 0 ALTER bảng cũ ⇒ Down = DropTable ×7, reversible sạch.** Convention: PascalCase EN, loose-Guid không FK vật lý sang PE/Supplier (convention PE — `PurchaseEvaluation` ProjectId/SelectedSupplierId đều loose, ghi ở Mig 49 note), FK vật lý chỉ trong nội bộ module + sang bảng V2 dùng chung. | # | Bảng | Cột chính | Ràng buộc | |---|---|---|---| | 1 | `ContractSigningPlans` (header) | MaKeHoach?, PurchaseEvaluationId (loose+IX), ProjectId (denorm lọc cây), Phase int, ApprovalWorkflowId Guid?, CurrentWorkflowStepIndex int?, CurrentApprovalLevelOrder int?, DrafterUserId, SlaDeadline?, SlaWarningSent, HoSoLink nvarchar(1000)? (mirror PE Mig 52), GhiChu | AuditableEntity + `HasQueryFilter(!IsDeleted)` (khuôn `PurchaseEvaluationConfiguration.cs:84`) | | 2 | `ContractSigningPlanLines` (giá per-winner) | PlanId FK Cascade, SupplierId loose, PeReferenceAmount decimal(18,2) **snapshot lúc tạo** (nguyên tắc freeze Mig 67 — không để PE sửa sau làm trôi baseline), ProposedAmount decimal(18,2), ApprovedAmount decimal(18,2)? (chỉ ghi tại finalize), Note | UNIQUE filtered (PlanId, SupplierId) WHERE IsDeleted=0 (gotcha #57) | | 3 | `ContractSigningPlanDossierItems` (căn cứ b.8-9) | PlanId FK Cascade, Kind int (MauVatLieu=1/Shopdrawing=2/Khac=99), SupplierId? loose, Name, Status int (ChuaNop=0/DaNop=1/TvgsDuyet=2/TvgsBac=3), TvgsName nvarchar(200)?, TvgsResultAt?, Note | IX (PlanId) | | 4 | `ContractSigningPlanLevelOpinions` | PlanId + ApprovalWorkflowLevelId, Comment nvarchar(2000), SignedAt, SignedByUserId, SignedByFullName | UNIQUE (PlanId, LevelId); FK **Cascade Plan + Restrict Level** (mirror `ContractLevelOpinions` Mig 33 / `ProposalLevelOpinionConfiguration.cs`) | | 5 | `ContractSigningPlanAttachments` | PlanId FK Cascade, FileName/Path/Size/ContentType, Purpose int (MauVatLieu=1/Shopdrawing=2/SoSanhGia=3/Khac=99), DossierItemId? | khuôn `ProposalAttachment.cs` (20 dòng) | | 6 | `ContractSigningPlanChangelogs` | PlanId, Action, PhaseAtChange, UserId, Summary, ContextNote | tiền lệ PE: Changelogs = nguồn transition-marker/backfill (Mig 61) — có từ ngày 1 | | 7 | `ContractSigningPlanCodeSequences` | Prefix PK, LastSeq | khuôn `ProposalCodeSequence.cs` (13 dòng); format mã đề xuất `KHKK/{YYYY}/{Seq:D3}` — **owner chốt format** | Biến thể tối thiểu 5 bảng (bỏ #6 Changelogs + #7 CodeSequences) — KHÔNG khuyến nghị: mất audit-trail transition (tiền lệ Mig 60/61 cho thấy cần) và mất mã phiếu atomic. **Enum ngoài bảng:** `ApprovalWorkflowApplicableType` **+`ContractSigningPlan = 10`** — append-only vào `ApprovalWorkflow.cs:53-67` (slot 10 trống, em đọc trực tiếp; tiền lệ Mig 37 extend enum). `ContractSigningPlanPhase` enum MỚI giá trị sạch `DangSoanThao=1/ChoDuyet=2/DaDuyet=3/TraLai=98/TuChoi=99` — KHÔNG copy 7/98 của PE (sẹo lịch sử enum PE, phiếu mới không nợ data cũ; 98/99 giữ cho quen mắt FE badge). **KHÔNG thêm cột `SigningPlanId` lên PE** — tra ngược bằng query `Plans.Where(PurchaseEvaluationId==x)`; thêm cột = đẻ "cột có mà 0 ai đọc" (lớp lỗi 6-lần của repo). DbSets: `ApplicationDbContext` + `IApplicationDbContext` (khuôn mọi module). 3-file rule mig (Up/Down + Designer + Snapshot). ### B.3 CQRS + service + controller - **`Application/ContractSigningPlans/ContractSigningPlanFeatures.cs`** (mega-file khuôn `ProposalFeatures.cs` 556 dòng): - `CreateContractSigningPlanCommand(PeId, ApprovalWorkflowId, GhiChu?)` — validator: PE tồn tại + `Phase==DaDuyet` (mirror cầu `CreateContractFromEvaluationFeatures.cs:53-54`) + **chưa có plan sống** (`AnyAsync(p.PeId==x && p.Phase!=TuChoi)` → Conflict; cho tạo lại sau TuChoi) + workflow `ApplicableType==ContractSigningPlan` match (mirror Proposal `:258-265`). Handler **auto-sinh Lines từ winners**: `pe.Suppliers.Where(IsWinner)` + `PeReferenceAmount = SUM Quote.IsSelected` per winner (copy đúng phép tính cầu `:56-62` + `:88-90`) — snapshot 1 lần. - `UpdateContractSigningPlanDraftCommand` — guard `Phase ∈ {DangSoanThao, TraLai}` + **cho re-pin ApprovalWorkflowId** (mirror PE `PurchaseEvaluationFeatures.cs` re-pin chỉ Nháp/TraLai; tránh vết Contract UpdateDraft KHÔNG có đường re-pin — S156 §2-bis). - `Get`/`List`/`pendingMe` — detail Include Lines+DossierItems+Attachments+LevelOpinions + workflow-tree JOIN (khuôn Proposal `:161-228`); **inbox V2 precompute từ ngày 1** (mirror PE `ResolveV2InboxIdsAsync` — bẫy S156: Contract THIẾU cái này nên approver không có inbox `ContractFeatures.cs:363-372` legacy-only). - `UpsertDossierItemCommand` / `DeleteDossierItemCommand` — BCH nhập hộ TVGS; mở cả khi ChoDuyet? **KHÔNG** — chỉ DangSoanThao/TraLai (căn cứ là input của con số đang duyệt); attachment mở mọi phase (triết lý PE S147 BE không phase-guard attachment). - Attachment upload/download/delete — khuôn `ProposalAttachment` + PE attachment features. - **`Infrastructure/Services/ContractSigningPlanWorkflowService.cs`** — copy `ContractWorkflowService.ApproveV2Async:217-394` (B.1) + adapt 3 chỗ: (1) terminal → `Phase=DaDuyet` + **`ApplyApprovedValuesOnFinalize`**: mọi Line `ApprovedAmount = ApprovedAmount ?? ProposedAmount` (người duyệt cấp cuối được sửa trước khi duyệt nếu bật `AllowApproverEditBudget`-tương-tự; khuôn choke-point + RULE grep-4-site `PurchaseEvaluationWorkflowService.cs:1008`); (2) port 2 nhánh kết thúc sớm PE: `AllowApproverFinalize` `:870-890` + `finalizeByCcmDelegation`+`CeoApprovalThreshold` `:892-929` (tổng so ngưỡng = `SUM ProposedAmount` các Line); (3) Reject → 4 return-mode per-level (cờ sẵn `ApprovalWorkflowLevel.cs:116-125`, logic mirror PE `ApplyReturnModeAsync:368-547`) — tối thiểu TraLai toàn phần nếu muốn gọn wave đầu. **Guard trình:** `CreatedBy==actor ∨ DeptManager ∨ Admin` — CỐ Ý KHÁC khuôn role-Drafter (`PurchaseEvaluationWorkflowService.cs:172-178` + `ContractWorkflowService.cs:70-79`) để né lớp 403-PMH (S156 §G3: role không khớp = kẹt cứng); nếu owner muốn đồng nhất role-based thì BCH users phải mang role Drafter. **Notify:** mirror `LogTransitionAsync` notify Drafter (`ContractWorkflowService.cs:407-427`) + THÊM notify đích danh approvers của Cấp kế (fix lỗ notify-chỉ-Drafter ngay từ ngày 1, đừng chép nợ). - **`Api/Controllers/ContractSigningPlansController.cs`** — authz **2 tầng** (bài học #82/S118): class `[Authorize]` + per-action `[Authorize(Policy = "ContractSigningPlans.{Read|Create|Update|Delete}")]` — KHÔNG chép kiểu `ContractsController.cs:12-13` class-trần (lỗ A1 S156). Policy TỰ SINH khi key vào `MenuKeys.All` (`Program.cs:82-89` foreach All × Actions). Endpoints: GET list/inbox · GET {id} · POST · PUT {id} · POST {id}/transitions (body {targetPhase, decision, comment, returnMode?, skipToFinal?, applyLevelFinalize?, finalizeByCcmDelegation?, approvedLines?}) · dossier-items CRUD · attachments · DELETE {id} (allow-list DangSoanThao|TuChoi mirror PE `PurchaseEvaluationFeatures.cs:1404`). ### B.4 Menu + permission + FE 2 app (Pattern 16-bis, twin Off_DeXuat đo thật) - **BE:** `MenuKeys.cs` +4 const kiểu `Csp`/`Csp_List`/`Csp_Create`/`Csp_Inbox` (khuôn `OffDeXuat*` `:109-112`) **+ đưa cả 4 vào `All` `:156-174`** (khuôn `:168` — Off_DeXuat 4 key ĐỀU trong All ⇒ policy sinh đủ; ĐỪNG theo kiểu `Pe_*` factory-ngoài-All `:142-144` vì per-action policy sẽ chưa tồn tại — bẫy S155 "policy-chưa-tồn-tại"). ⇒ 2 row canonical `docs/STATUS.md` (Menu keys · Policies DERIVED |All|×4) PHẢI cập nhật cùng lúc. - **Seed:** `DbInitializer.cs` menu 4 dòng (khuôn Off_DeXuat `:1817-1820`: root parent + 3 leaf, label VN "Kế hoạch ký kết HĐ"/"Danh sách"/"Tạo mới"/"Inbox duyệt") + admin-permission list (`:2383` — grep đủ MỌI site seed-permission khi làm, S155 ghi nhận có 2 site cho vài nhóm); dark-launch được bằng `IsVisible=0` (cột Mig 27) rồi bật sau. - **FE ×2 app (duplicate CÓ CHỦ ĐÍCH):** mỗi app 4 chỗ: (1) routes `App.tsx` (khuôn fe-user `:77-79` /proposals ×3 route); (2) `components/Layout.tsx` staticMap (khuôn `:95-97` `Off_DeXuat_List: '/proposals'`…); (3) `lib/menuKeys.ts` (khuôn `:54-57`); (4) pages ×3 (khuôn bộ Proposal 978 LOC/app: List 252 + Create 245 + Detail 386 + types 95) — Detail thêm: bảng Lines (đề xuất/chốt per-NCC) + bảng DossierItems (status chip + TVGS + scan) + panel workflow (mirror PE Panel-3 Steps/Levels ✓/●/○) + banner "Đến lượt bạn" (khuôn PE `blockedByV2Level`). - **Designer admin nhận type 10:** fe-admin `ApprovalWorkflowsV2Page.tsx` map typeCode→int `:143` + các site BE `ApprovalWorkflowV2AdminFeatures.cs` đọc ApplicableType (grep "ApplicableType" toàn file khi làm — S155 F6 liệt ~5 dây BE + fe-user matrix-view; **path file matrix-view fe-user em grep lượt này KHÔNG thấy `fe-user/src/pages/WorkflowMatrixViewPage.tsx` — vị trí thật cần grep lại khi implement, đừng tin line S155**). - **Cầu tiếp giáp (sửa 1 file):** `CreateContractFromEvaluationFeatures.cs` — (a) đọc plan DaDuyet của PE: có → `giaTri = line.ApprovedAmount` khớp `w.SupplierId` thay SUM `:88-90`, ghi ContextNote lệch "giá KH x vs SUM-PE y" vào 2 changelog `:121-140` (khuôn audit-note D4 S134); không có plan → giữ SUM + **cảnh báo mềm** (Q11 owner nâng cứng sau); (b) +param `ApprovalWorkflowId?` pin V2 cho HĐ (hiện pin V1-only `:68-71,:108` — HĐ V2-less trình xong KẸT CỨNG `ConflictException` `ContractWorkflowService.cs:115-116`, em tự xác minh trên đĩa). - **1 dòng bắt buộc nhắc (ngoài scope, treo theo ý anh):** lỗ Reject-trước-guard `ContractWorkflowService.cs:48-66` + controller HĐ class-trần vẫn SỐNG trên prod — anh chốt 07-28 "từ từ". ## §C — CHIA WAVE THI CÔNG + CHECKLIST ĐO ĐƯỢC [PENDING] ## §D — HẤP THỤ 6 việc sửa review-synthesis §G [PENDING]