Files
solution-erp/.claude/workflows/runs/2026-07-28-S157-ke-hoach-ky-ket-hd/sub-review-schema-3.md
pqhuy1987 d7eaeceaa7 [CLAUDE] Docs: spec "Ke hoach ky ket Hop dong" S157 (khuc con thieu PE -> HD) + review 4 lang kinh
Anh giao: dieu tra quy trinh CON THIEU giua Duyet NCC va HD NTP/NCC, ra spec day
du + chia wave. Chay /fable-real invest (Fable deep-pass) roi /fable-clone
reviewer 4 lang kinh.

PHAT HIEN NEN (lead do, invest + reviewer xac nhan tu nguon goc):
- 7 phase [LEGACY] cua ContractPhase CHINH LA quy trinh ISO 9 buoc ma-hoa cung
  (khop 1:1 dung thu tu). Khung V2 sinh ra de bieu dien chinh quy trinh do.
- Khoang trong nam DUNG giua ISO-1 va ISO-2, va ISO doc CUNG KHONG CO no
  => thieu o CA HAI tang (doc + code) => spec vua la spec phan mem vua la de
  xuat bo sung ISO.

OWNER CHOT trong cua: (O-1) cau truc trinh ky GIONG, chi khac NOI DUNG => tai
dung khung duyet V2, 0 bang approval moi => truc tranh luan Q2 hom qua DONG.
(O-2) SLA chi tham khao => khong SLA-engine, khong wave SLA.
(O-3) "cho hop dong cu tu tu" => lo hong an ninh HD NGOAI SCOPE.

REVIEW 4/4 lane sach 0 chet (dong goi C2 an), 112 diem. Claim nen DUNG VUNG.
Bat 7 loi NANG, da va het trong ban nay:
1. Ten menu key `Csp*` != tien to policy `ContractSigningPlans.*` => Program.cs
   khong sinh policy => HTTP 500 luc chay. Repo DA CO test bat
   (AuthorizePolicyRegressionTests.cs:138-139). Day la loi TU-VAP-BAY-MINH-VUA-
   NHAN-DIEN: §2.4 bat dung bay Pe_* roi §2.3 van vap bang co che khac.
2. W2 day rieng tao duoc 0 phieu (wiring Designer type-10 roi khoi moi wave)
   => keo vao W1, tai pham dung §F.0 S155.
3. Thieu bang Approvals ma service duoc lenh copy CO ghi vao.
4. Changelogs thieu 4 cot => copy service khong bien dich.
5. Thieu 3 site ApprovalWorkflowV2AdminFeatures => FK Restrict no 500.
6. Status enum bat dau tu 0 => bay falsy JS.
7. 4 anchor lech, nang nhat ApprovalWorkflowLevel.cs KHONG TON TAI.

Kem: bo tieu chi dark-launch sai co che (IsVisible chi pass-through, filter la
CanRead) - thay "sqlcmd dem policy" (khong do duoc, policy in-memory) bang test
regression - OR-of-N nang tu 1 ve len 4 ve co ca am - cat CodeSequences (tai
dung WorkflowAppCodeSequence dung chung 4 module Office).

Su co #53: invest luot 1 skeleton-ruot-rong (202K tok, dia 1.661B khung rong)
=> ghi-dia-trong-luc-lam la CAN KHONG DU, phai verify RUOT. Luot 2 resume cat
con 2 muc => CLEAN 23.085B. Luot 3 tien-trinh CLI thoat, §C/§D chua ghi => lead
tu viet theo H21 (engine propose-only, spec do lead ghi).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 11:30:03 +07:00

16 KiB
Raw Blame History

LĂNG KÍNH 3/4 — SCHEMA 7 BẢNG (reviewer, S157)

Đích chấm: spec-ke-hoach-ky-ket-hd-28-07-2026.md §②.2 (dòng 113-136). Nguồn đối chiếu ĐÃ ĐỌC: src/Backend/SolutionErp.Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs · src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs (+ grep xác minh trên config/service/enum).

Verdict

GO-WITH-FIXES — hình dạng 7 bảng ĐÚNG HƯỚNG (loose-Guid, decimal(18,2), query-filter, FK Cascade/Restrict đều khớp twin thật), nhưng 5 điểm HIGH phải đóng TRƯỚC Wave 1 vì chúng làm migration/CQRS/FE sai IM LẶNG (build vẫn xanh): thiếu bảng lịch-sử-transition mà chính service được lệnh copy có ghi vào, thiếu đăng ký ở 3 site cross-module ApprovalWorkflowV2AdminFeatures, thiếu 4 cột Changelog, không khai base-class cho bảng #2-#7, và bộ số enum phase đụng khuôn FE đang được copy.

Phân bố: 5 HIGH · 7 MAJOR · 3 MEDIUM · 7 CỦNG CỐ = 22 điểm.

Bảng THỪA

T1 [MAJOR] — #7 ContractSigningPlanCodeSequences gộp được, KHÔNG mất gì. Repo đã có 4 bảng giống hệt nhau về cấu trúc (Prefix string PK · LastSeq int · UpdatedAt): Contracts/ContractCodeSequence.cs:6-11 · Office/ProposalCodeSequence.cs:8-13 · Office/WorkflowAppCodeSequence.cs:14-19 · (+ PurchaseEvaluationCodeSequence, mirror khai ở ProposalCodeSequence.cs:4). Trong đó WorkflowAppCodeSequence đã là bảng DÙNG CHUNG cho 4 module Office, phân biệt nhau bằng Prefix có không-gian-tên: "DT/LR/2026" (nghỉ phép) · "DT/OT/2026" (OT) · "DT/CT/2026" (công tác) · "DX/XE/2026" (đặt xe) — xem WorkflowAppCodeSequence.cs:8-11. Prefix "KHKK/{YYYY}" nhét vào bảng đó là 0 va chạm (khoá là chuỗi, đã namespaced sẵn). ⇒ Cắt #7 ⇒ còn 6 bảng, giảm 1 CreateTable + 1 configuration + 1 DbSet, không mất tính atomic.

T2 [MAJOR] — lý do bác biến-thể-5-bảng đứng NỬA, đổ NỬA.

  • Nửa ĐỨNG: "giữ #6 Changelogs" có bằng chứng thật — Mig 60/61 phải backfill từ changelog (CLAUDE.md ghi Mig 60 soi nhầm bảng Approvals → Mig 61 mới đúng Changelogs). Thứ tự ưu tiên spec đưa ("cắt #7 trước, giữ #6") là ĐÚNG.
  • Nửa ĐỔ: mệnh đề "mất mã phiếu atomic" không suy ra được. Tính atomic đến từ transaction SERIALIZABLE trong CodeGen service, không đến từ việc bảng đó thuộc riêng module — nói rõ trong chính comment của cả 3 bảng: ContractCodeSequence.cs:5 · ProposalCodeSequence.cs:7 · WorkflowAppCodeSequence.cs:12-13. Vậy "cắt #7" ⇏ "mã tay"; nó chỉ là "dùng chung bảng sequence sẵn có".
  • Các bảng còn lại KHÔNG gộp được (đã xét từng cặp): #2 Lines (multi-winner, per-NCC) không nhét vào header; #3 DossierItems không nhét vào #5 Attachments vì trạng thái ChuaNop là hàng không có file — gộp là phải đẻ attachment giả; #4 LevelOpinions bị máy-móc admin V2 truy vấn theo (phiếu × Level) nên phải là bảng riêng có UNIQUE.

Bảng THIẾU

H1 [HIGH] — không có ContractSigningPlanApprovals (lịch sử transition), trong khi service được lệnh copy CÓ ghi vào bảng đó. ContractWorkflowService.cs:151 ghi thẳng: db.ContractApprovals.Add(new ContractApproval { ContractId = …, FromPhase = fromPhase, ToPhase = fromPhase, … }). Twin PE cũng có nhánh này: PurchaseEvaluation.cs:137 (List<PurchaseEvaluationApproval> Approvals) + index chuyên dụng (PurchaseEvaluationId, ApprovedAt)PurchaseEvaluationConfiguration.cs:168; PurchaseEvaluationApproval.cs:6-7 ghi rõ "Lịch sử phê duyệt — giống ContractApproval pattern". Spec §2.1 bắt copy ApproveV2Async:217-394 ⇒ đoạn copy sẽ trỏ vào một bảng KHÔNG TỒN TẠI trong danh sách 7 bảng. ⇒ Chọn 1 trong 2 và ghi vào spec: (i) thêm bảng #8 …Approvals, hoặc (ii) tuyên bố "Changelog gánh vai Approvals" kèm bù cột FromPhase/ToPhase/Decision/Comment/ApprovalWorkflowLevelId (hiện #6 chỉ có MỘT trường phase là PhaseAtChange, không có cặp from→to, không có Decision).

H2 [HIGH] — module thứ 8 phải đăng ký ở 3 site liệt-kê-thủ-công trong ApprovalWorkflowV2AdminFeatures.cs; spec chỉ nói chung chung. File này liệt kê từng bảng *LevelOpinions một ở 3 chỗ: :911 + :929 (Merge "chữ ký còn sống" — db.ProposalLevelOpinions… / db.TravelRequestLevelOpinions…) · :971 + :992 (rows.AddRange dựng RetainedOpinionRef) · :1044 + :1056 (dọn chữ ký MỒ CÔI). Chuỗi guard S155 đứng trên dữ liệu này: :651 "F-5/F-5a — chặn LỊCH SỰ trước khi FK nổ":733 "Dọn rác chữ ký MỒ CÔI trước khi xoá dòng". ⇒ Nếu ContractSigningPlanLevelOpinions không được thêm vào cả 3 site, admin sửa quy trình type = 10 sẽ không nhìn thấy chữ ký của phiếu KHKK, xoá Level đang có chữ ký → FK Restrict nổ 500 — đúng lớp lỗi S155 vừa vá xong. Spec §2.4 chỉ viết "các site BE ApprovalWorkflowV2AdminFeatures.cs" (không nêu 3 site này, chỉ nêu map typeCode) ⇒ chưa đủ để người làm không sót.

Cột sai

C1 [HIGH] — #6 Changelogs thiếu 4 cột so với twin. PurchaseEvaluationChangelog.cs:14-22 có: EntityType · EntityId · Action · PhaseAtChange · UserId · UserName · Summary · FieldChangesJson · ContextNote. Spec #6 chỉ liệt Action · PhaseAtChange · UserId · Summary · ContextNotethiếu EntityType, EntityId, UserName, FieldChangesJson. Hệ quả đo được: index twin (PurchaseEvaluationId, EntityType) (PurchaseEvaluationConfiguration.cs:188) mất chỗ dựa; LogTransitionAsync (ContractWorkflowService.cs:405) gọi changelog.LogWorkflowTransitionAsync(...) — copy service changelog sang module mới sẽ không biên dịch nếu entity thiếu trường; và mất khả năng ghi diff trường (FieldChangesJson, twin khai nvarchar(max)PurchaseEvaluationConfiguration.cs:185).

C2 [MAJOR] — Status int bắt đầu từ 0 (ChuaNop=0) là lệch quy ước + bẫy JavaScript. Mọi enum trong repo bắt đầu ở 1: Office/Enums.cs:9 (Confirmed=1) · :19 (Nhap=1) · :31 (Nhap=1) · :41 (Hardware=1) · :50 (Low=1) · :58 (Open=1); ApprovalWorkflow.cs:55 (DuyetNcc=1). Số 0 là giá trị falsy trong JS ⇒ status ? A : B, if (filter.status), ?status=0 trên query-string đều rơi im lặng ở FE. Đề nghị ChuaNop = 1 (giữ Khac = 99 của Kind — cái đó ĐÚNG khuôn ItTicketCategory.Other = 99, Office/Enums.cs:45).

C3 [MAJOR] — ~13 cột chuỗi không khai độ dài ⇒ EF sinh nvarchar(max). Spec chỉ khai 2 độ dài (HoSoLink 1000, Comment 2000). Không khai: MaKeHoach, GhiChu, Name (#3), Note (#2/#3), TvgsName, Summary, ContextNote, SignedByFullName, FileName/Path/ContentType (#5), Prefix (#7). Twin khai đủ: PurchaseEvaluationConfiguration.cs:22 (HoSoLink 1000) · :184 (ContextNote 2000) · ContractLevelOpinionConfiguration.cs:21-22 (Comment 2000, SignedByFullName 200 IsRequired). nvarchar(max) không index được và không so sánh được trong filtered-unique.

C4 [MAJOR] — SlaDeadline? chưa có người đọc ⇒ đúng lớp "cột có mà 0 ai đọc" mà chính spec cấm (dòng 136). Thiếu SlaWarningSent (twin PurchaseEvaluation.cs:26) và thiếu index SlaDeadline (twin PurchaseEvaluationConfiguration.cs:62). Job SLA hiện chỉ đọc/ghi cờ này trên nhánh Contract: SlaExpiryJob.cs:139 (.Where(c => !c.SlaWarningSent …)) và :169 (c.SlaWarningSent = true;). ⇒ Hoặc bỏ SlaDeadline, hoặc thêm cờ + index + khai rõ wave nào đăng ký module vào SlaExpiryJob.

C5 [MAJOR] — header thiếu DepartmentId, làm vế "DeptManager" của guard trình mất chỗ neo. Twin có: PurchaseEvaluation.cs:17 (Guid? DepartmentId). Guard spec §2.3 (dòng 164) = CreatedBy == actor DeptManager Admin. Không có phòng trên phiếu thì "DeptManager" không so được với gì ⇒ bất kỳ ai mang vai DeptManager trình được phiếu của phòng khác. ApprovalWorkflowStep.DepartmentId (ApprovalWorkflow.cs:75) chỉ là hint của quy trình, không thay được phòng của phiếu.

C6 [MAJOR] — không có đường quay ngược Kế-hoạch → Hợp-đồng sau b.13. Twin có PurchaseEvaluation.ContractId (:31) + index (PurchaseEvaluationConfiguration.cs:65). Ở đây HĐ sinh theo từng NCC thắng, mà #2 Lines cũng theo từng NCC ⇒ chỗ tự nhiên là ContractSigningPlanLines.ContractId? (+ IX). Thiếu nó thì: không trả lời được "dòng này đã ra HĐ chưa", không chặn được tạo HĐ hai lần cho cùng một NCC, và §2.5(a) không có chỗ ghi kết quả bắc cầu. (Lưu ý: đây KHÁC với "cột SigningPlanId trên PE" mà spec bác — bác cột đó là đúng, xem G6.)

C7 [MEDIUM] — PeReferenceAmount: GIỮ snapshot (đúng), nhưng thiếu mốc thời gian + luật ghi lại. Xem G5 để biết vì sao snapshot thắng "đọc live". Hai chỗ hở: (i) không có PeReferenceAmountAt — twin dùng ApprovedBudgetSnapshotAt làm cờ "đã chốt" (PurchaseEvaluation.cs:132, ghi rõ ở :120); (ii) spec nói "snapshot 1 lần" lúc tạo nhưng không nói điều gì xảy ra khi Drafter re-pin workflow hay sửa nháp (§2.3 cho phép re-pin) — có sinh lại Lines không, có tính lại số không.

Ràng buộc sai

R1 [HIGH] — không khai lớp cơ sở (BaseEntity vs AuditableEntity) cho bảng #2-#7, trong khi #2 lại đặt UNIQUE lọc WHERE IsDeleted=0. Bộ lọc đó đòi cột IsDeleted, tức #2 buộc phải là AuditableEntity. Nhưng twin gần nhất của #2 là PurchaseEvaluationSupplier.cs:9: BaseEntity (không có xoá mềm) — và UNIQUE của nó không lọc: PurchaseEvaluationConfiguration.cs:105 HasIndex(x => new { x.PurchaseEvaluationId, x.SupplierId }).IsUnique(). ⇒ Nếu người làm mirror twin (BaseEntity) thì migration sinh filter trỏ vào cột không tồn tại; nếu mirror spec (AuditableEntity) thì lệch khuôn con-của-phiếu. Spec phải khai base class cho từng bảng.

R2 [MEDIUM] — bất nhất nội bộ #2 (lọc) vs #4 (không lọc), và #4 kế thừa một vi phạm #57 tiềm ẩn. #4 không lọc là khớp cả 6 twin: ContractLevelOpinionConfiguration.cs:34 · ProposalLevelOpinionConfiguration.cs:28 · TravelRequestLevelOpinionConfiguration.cs:28 · OtRequestLevelOpinionConfiguration.cs:28 (+ Leave, PE). Nhưng các entity đó AuditableEntity: PurchaseEvaluationLevelOpinion.cs:24, TravelRequestLevelOpinion.cs:16 ⇒ cả họ đang ở thế "xoá mềm + UNIQUE không lọc" = gotcha #57 dạng ngủ (chỉ chưa cắn vì đường ghi là UPSERT, không soft-delete). Kế thừa thì kế thừa có ý thức: ghi 1 dòng lý do trong spec, nếu không lần sau ai đó thêm đường soft-delete chữ ký là kẹt không ký lại được.

R3 [MEDIUM] — loose-Guid đúng convention nhưng thiếu index đi kèm. Spec chỉ khai IX cho PurchaseEvaluationId. Twin đánh index cho mọi loose-Guid dùng để lọc: ProjectId (:58) · WorkItemId (:61) · ApprovalWorkflowId (:64) · ContractId (:65) · SlaDeadline (:62); và ở bảng con: SupplierId (:106), PurchaseEvaluationSupplierId (:206). ⇒ Bổ sung IX: ProjectId, ApprovalWorkflowId, DrafterUserId (header) + SupplierId (#2) + PlanId (#4/#6/#7 theo cặp).

Điểm CỦNG CỐ

G1 — trích dẫn PurchaseEvaluationConfiguration.cs:84 CHÍNH XÁC ĐẾN DÒNG: b.HasQueryFilter(x => !x.IsDeleted); nằm đúng dòng 84. (Đã kiểm bằng cách đọc file, không tin lời spec.)

G2 — decimal(18,2) đúng quy ước tiền tệ của repo: PurchaseEvaluationConfiguration.cs:23-30 (HasColumnType("decimal(18,2)") cho các cột ngân sách/giá).

G3 — "loose-Guid không FK vật lý sang PE/Supplier/Project" là quy ước THẬT, không phải suy diễn: PurchaseEvaluation.cs:16 (comment WorkItemId"scalar loose-Guid (KHÔNG navigation, KHÔNG FK vật lý — convention PE giống ProjectId/SelectedSupplierId)") + PurchaseEvaluationConfiguration.cs:59-61 ("index lọc query, KHÔNG HasOne/FK vật lý (convention PE: chỉ ApprovalWorkflowId có FK)") + :72-73 (FK duy nhất ra ngoài module là ApprovalWorkflowId, DeleteBehavior.Restrict).

G4 — bảng #4 mirror ĐÚNG 100% khuôn ContractLevelOpinions: FK Cascade phiếu (ContractLevelOpinionConfiguration.cs:25-27) + FK Restrict Level (:30-32) + UNIQUE (phiếu, Level) (:34)

  • Comment 2000 (:21). Đây là phần chắc tay nhất của §②.2. (Kèm nghĩa vụ H2 — chọn Restrict thì phải vào 3 site admin.)

G5 — PeReferenceAmount snapshot: BÁC phương án "đọc live", giữ snapshot. Ba căn cứ: (i) tiền lệ cùng repo — ApprovedPriceAmount (PurchaseEvaluation.cs:97) và 11 cột ApprovedBudget* (:122-132, Mig 67) đều là ảnh chụp tại thời điểm chốt; (ii) nguồn live biến động được dưới chân phiếu KHKK: cờ F3 AllowApproverEditDetails cho người duyệt sửa Hạng mục/NCC/Báo giá lúc đang duyệt (ApprovalWorkflowLevel.cs:127-129), cờ F6 AllowApproverDelete cho xoá mềm phiếu (:156-169) — mà phiếu PE xoá mềm thì HasQueryFilter làm số live biến mất, snapshot thì còn; (iii) spec đã tự cài máy dò lệch ở §2.5(a) (ghi ContextNote "giá KH x vs SUM-PE y") ⇒ snapshot không biến thành số mồ côi không ai đối chiếu.

G6 — "KHÔNG thêm cột SigningPlanId lên PE" là quyết định ĐÚNG (tra ngược bằng Plans.Where(PeId == x)); đây đúng là lớp lỗi cột-phái-sinh-kẹt-stale (gotcha #81, docs/gotchas.md:1343).

G7 (câu e, vế 2) — ApplicableType = 10 TRỐNG THẬT, xác nhận bằng đọc mã chứ không bằng lời spec: ApprovalWorkflow.cs:53-67 — enum dừng ở TravelRequest = 9, không có giá trị 10. Quét cả src/ với mẫu = 10, chỉ ra một kết quả duy nhất và nó thuộc enum KHÁC: PurchaseEvaluationPhase.cs:24 ChoDuyet = 10. ⇒ Claim của spec ĐÚNG, append-only an toàn.

Enum — điểm HIGH riêng

E1 [HIGH] — ContractSigningPlanPhase = 1/2/3/98/99 là bộ số THỨ BA, và nó đụng đúng khuôn FE mà §2.4 bắt copy. Repo đang sống hai quy ước:

  • Họ PE/Contract: PurchaseEvaluationPhase.cs:24 ChoDuyet = 10 (+ TraLai 98 / TuChoi 99).
  • Họ Office/Proposal — chính là khuôn FE spec chọn (§2.4: "khuôn bộ Proposal ~978 LOC/app"): Office/Enums.cs:17-24 ProposalStatus { Nhap=1, DaGuiDuyet=2, TraLai=3, TuChoi=4, DaDuyet=5 }:29-36 WorkflowAppStatus y hệt, dùng chung cho 5 module (LeaveRequest + OtRequest + TravelRequest + VehicleBooking + ItTicket — :27).

Va chạm cụ thể, đo được: giá trị 3 = DaDuyet ở enum mới, nhưng = TraLai ở khuôn FE Proposal đang được copy. Bảng map badge/nhãn trạng thái copy nguyên si sẽ render sai mà không có lỗi biên dịch — đúng lớp "xanh-giả". Lý do spec đưa ("giữ 98/99 cho quen mắt FE badge") chỉ đúng với FE PE/Contract, không đúng với FE khuôn đã chọn. ⇒ Hai lối ra, chọn 1 và ghi vào spec: (a) dùng đúng bộ WorkflowAppStatus 1-5 (hết va chạm, thêm cơ hội tái dùng chip/badge có sẵn), hoặc (b) giữ 1/2/3/98/99 nhưng khai nghĩa vụ viết lại toàn bộ map trạng thái khi copy trang Proposal, kèm 1 dòng kiểm ở checklist wave.