Files
solution-erp/.claude/workflows/runs/2026-07-17-S134-pe-luyke-tam-tinh/sub-investigator-codebase-0.md
2026-07-17 14:27:19 +07:00

20 KiB
Raw Blame History

SUB investigator-codebase-0 — S134 PE lũy-kế TẠM TÍNH (ground-truth + propose spec)

Run: 2026-07-17-S134-pe-luyke-tam-tinh · engine fable-real single-deep-pass · ghi-đĩa-trong-lúc-làm (chống #53) Status: IN-PROGRESS — append từng phần

PHẦN 1 — Predicate lũy kế hiện tại (Claim 1)

Kết luận Claim 1: PARTIAL — quan trọng: "chỉ bắt DaDuyet" KHÔNG đúng cho cả 4 số

Helper PeBudgetAccumulator.cs (extracted S133, behavior-preserving):

  • Peers = cùng (ProjectId, WorkItemId), Id != this, CreatedAt < this (PeBudgetAccumulator.cs:38-40) + HasQueryFilter !IsDeleted tự áp.
  • PrevSubmitted (Total/Count) = peers Phase ∈ {ChoDuyet, DaDuyet} → Count + SUM(BudgetPeriodAmount ?? 0) — :42-48. ⟵ ĐÃ bao gồm ChoDuyet (phiếu đang trình chưa duyệt)!
  • PrevSelected (Total/Count) = peers Phase == DaDuyet AND Suppliers.Any(IsWinner) (existence) — :50-52; total = SUM ThanhTien quote IsSelected của các peer đó — :53-60. ⟵ Đây mới là chỗ DaDuyet-only ("duyệt rồi mới bắc" Kenny S117).
  • CurrentProposalTotal = own quotes IsSelected — :62-65.

Số bản predicate phải khớp — thực tế HIỆN TẠI là 2 bản sống + 1 bản one-time:

  • (a) Helper PeBudgetAccumulator.ComputeAsync — bản RUNTIME duy nhất.
  • (b) Live-path PurchaseEvaluationFeatures.cs:962-963GỌI helper, KHÔNG duplicate predicate (comment :959-961: "Row 1 = đã trình (ChoDuyet + DaDuyet); Row 2 = đã chọn thầu (DaDuyet + IsWinner). TraLai/DangSoanThao KHÔNG tính").
  • (c) Snapshot-path PurchaseEvaluationWorkflowService.cs:998 (ApplyBudgetSnapshotOnFinalizeAsync) — :1018 GỌI helper, KHÔNG duplicate. Helper được gọi từ 5 site: :311 (submit-bypass?), :865/:904/:937 (finalize branches), :1112 (admin-override) — cần verify chi tiết từng site.
  • (d) Backfill SQL Mig 67 20260717032812_AddPeApprovedBudgetSnapshot.cs:99-150 — bản DỊCH SQL one-time (đã chạy, không chạy lại — idempotent WHERE SnapshotAt IS NULL). Xác nhận enum số: Phase IN (10, 7) = ChoDuyet=10, DaDuyet=7 (:128), selected = Phase = 7 + EXISTS IsWinner (:135-137).

TẠM TÍNH có đụng các bản này không? → KHÔNG bắt buộc đụng semantics cũ. Cách sạch nhất: THÊM computation mới (nhánh/method mới trong helper hoặc method riêng) tính "phần góp từ phiếu chưa-terminal". Snapshot-path (c) + backfill (d) KHÔNG đụng — tạm-tính KHÔNG snapshot (đề bài chốt). Live-path (b) chỉ THÊM field vào DTO.

Frozen gate S133 (Claim 3 partial — evidence)

  • Gate: PurchaseEvaluationFeatures.cs:929frozen = Phase == DaDuyet && ApprovedBudgetSnapshotAt != null.
  • Frozen branch :930-955: serve 11 cột ApprovedBudget* từ PE row; row 4 "Giá trị kỳ này" VẪN live từ own quotes (:932-936 — SPEC S133); BudgetFrozen: true (:955); canEditPro/Ccm = false-false (:950).
  • Live branch :957-984: pairRec PeWorkItemBudgets + accumulator; BudgetFrozen: false (:984).
  • DTO: PurchaseEvaluationDtos.cs:353bool BudgetFrozen = false (cần đọc full record để map field position).
  • Entity: PurchaseEvaluation.cs:116-132 — 11 cột ApprovedBudget* + SnapshotAt.
  • Seeder demo cũng set snapshot: DbInitializer.cs:1327-1340.

PHẦN 2 — Cụm 4 số lũy kế + FE matrix (Claim 2)

BE DTO PeBudgetSummaryDto (PurchaseEvaluationDtos.cs:331-353, 18 field):

  • PreviousSubmittedTotal/Count (:342-343) = SUM BudgetPeriodAmount peers Phase ∈ {ChoDuyet, DaDuyet} — "Ngân sách trình duyệt trước"
  • PreviousSelectedTotal/Count (:344-345) = SUM quote ThanhTien IsSelected của peers DaDuyet+IsWinner — "Kỳ trước đã chọn thầu"
  • CurrentProposalTotal (:346) = own quotes IsSelected
  • BudgetFrozen (:353) — append CUỐI default false (pattern append-an-toàn construction site)

FE type PeBudgetSummary (fe-user/src/types/purchaseEvaluation.ts:314-334; block lũy kế :327-333). ⚠️ types 2-app KHÔNG SHA-mirror (fe-user 173529b vs fe-admin 5631542 — S117 confirmed lại): sửa type = edit CẢ 2 file riêng.

FE matrix PeDetailTabs.tsx (2-app SHA IDENTICAL 47c97df1):

  • Công thức: row1 = bs.previousSubmittedTotal (:1315) · row2 = bs.previousSelectedTotal (:1316) · row4 = bs.currentProposalTotal (:1317) · row6 = row2 + row4 (:1318 — "6. Lũy kế thực hiện = 2 + 4", render :1579-1586) · r5 = row1 + r3 per-cột trong colCalc (:1323-1335, :1325 — "5. Lũy kế ngân sách đã sử dụng = 1 + 3", render :1569-1578). r7 = colFull r5 (:1326), r9 = row4 + r8 (:1328), cmp56 = r5 row6 (:1332).
  • Row 1/2 CHUNG 3 cột (BudgetSharedNumCell colSpan=3, :1498/:1503) — hiển thị null khi Count==0.
  • Row 3 PER-cột: PRO = ev.budgetPeriodAmount ?? bs.proInitialAmount (:1516) / CCM = ev.ccmBudgetPeriodAmount ?? bs.initialAmount (:1531 — Mig 59).
  • Frozen badge "🔒 Ngân sách chốt tại thời điểm duyệt" (:1352-1356). WorkItemId null → bs null → banner nhắc gắn hạng mục (:1297-1304).
  • ⚠️ Nuance sẵn có: row1 (accumulator) SUM BudgetPeriodAmount ?? 0 — peer chưa nhập row3 góp 0, trong khi FE row3 phiếu đó hiển thị fallback proInitialAmount (drift-vector S133 đã ghi nhận, TẠM TÍNH kế thừa nguyên trạng — không sửa trong scope này).

PHẦN 3 — Phase taxonomy (Claim 4) + snapshot helper (Claim 3 evidence)

Enum PurchaseEvaluationPhase.cs:15-27:

  • DangSoanThao = 1 (Nháp) · ChoPurchasing=2/ChoDuAn=3/ChoCCM=4/ChoCEODuyetPA=5/ChoCEODuyetNCC=6 [LEGACY deprecated] · DaDuyet = 7 (terminal thành công) · ChoDuyet = 10 (đã gửi duyệt) · TraLai = 98 (trả lại — Drafter sửa gửi lại) · TuChoi = 99 (terminal khoá).

Snapshot helper ApplyBudgetSnapshotOnFinalizeAsync (PurchaseEvaluationWorkflowService.cs:998-1041):

  • WorkItemId có → load pair PeWorkItemBudgets 6 cột (:1004-1011) + accumulator 4 cột lũy kế (:1018-1023); WorkItemId null → 10 cột null (:1027-1037); LUÔN set ApprovedBudgetSnapshotAt (:1040).
  • 5 call-site: :311 + :865/:904/:937 + :1112 (RULE comment :997 "MỌI nhánh set Phase=DaDuyet PHẢI gọi helper NGAY TRƯỚC assignment").
  • Display-gate frozen: PurchaseEvaluationFeatures.cs:929 (Phase==DaDuyet && SnapshotAt != null); frozen branch :930-955 serve snapshot + BudgetFrozen:true; row 4 "giá trị kỳ này" VẪN live (:932-936 spec S133).

PHẦN 4 — Màn duyệt (Claim 5) — PeWorkflowPanel (2-app SHA IDENTICAL 9d88e671)

  • Component: fe-user/src/components/pe/PeWorkflowPanel.tsx (+ fe-admin mirror same SHA). Panel 3 = timeline + transition buttons + history.
  • Final-approver detect ĐÃ CÓ SẴN: currentIsFinalApprover = lastFlowLevel?.status === 'Current' (:112-115) · CCM-done eligible ccmDelegationEligible (:109-111) · per-level finalize approverFinalizeEligible (:119) · shouldPickPrice = isApproveAction && (currentIsFinalApprover || finalizeByCcm || (approverFinalizeEligible && applyLevelFinalize)) (:510) — ĐÂY chính là predicate "đang duyệt CUỐI" tái dụng được cho cảnh báo.
  • V2 banner "Đến lượt bạn" :418-437 · action buttons :439-487 · Dialog transition :494-530 (footer Xác nhận :522, priceMissing disable :514) · mutation POST /purchase-evaluations/{id}/transitions :161-216.
  • Chỗ chèn cảnh báo "phiếu trước chưa duyệt" khả thi: (i) TRONG Dialog duyệt khi isApproveAction (cạnh :509-514, trước bộ chọn giá) — approver PHẢI nhìn thấy trước Xác nhận; (ii) banner Panel cạnh V2 banner :437; (iii) badge/note trong matrix PeDetailTabs rows 1/5/6.
  • Data: bundle DTO (evaluation) hiện KHÔNG có danh sách phiếu-trước-chưa-duyệt (chỉ có count gộp ChoDuyet+DaDuyet trong PreviousSubmittedCount — không tách). ⇒ cần field DTO MỚI; endpoint MỚI: KHÔNG cần — bơm vào GetPurchaseEvaluationQuery (đã query peers sẵn).

PHẦN 5 — 5 call-site snapshot định danh + DB ground + tests

5 site ApplyBudgetSnapshotOnFinalizeAsync (đọc context thật):

  • :311 — Admin manual override (site 5, reviewer-catch S133; assignment qua biến targetPhase)
  • :865 — level-finalize (AllowApproverFinalize + applyLevelFinalize opt-out S96; set EndedByLevelFinalize=true)
  • :904 — CCM duyệt-done miễn CEO (threshold guard winnerQuoteTotal < ceoThreshold)
  • :937 — all-steps-done terminal (CEO/cấp cuối V2)
  • :1112 — V1 legacy final (KHÔNG ApplyApprovedPriceOnFinalize)

Entity fields: PurchaseEvaluation.cs:10 MaPhieu · :16 WorkItemId loose-Guid · :40 BudgetPeriodAmount · :49 CcmBudgetPeriodAmount. Bundle root DTO BudgetSummary field PurchaseEvaluationDtos.cs:260; construction site duy nhất PurchaseEvaluationFeatures.cs:1253. FE bundle type budgetSummary: PeBudgetSummary | null (fe-user/src/types/purchaseEvaluation.ts:470) ⇒ PeWorkflowPanel đọc được qua evaluation.budgetSummary — KHÔNG cần prop/endpoint mới.

DB ground: Dev LocalDB SolutionErp_Dev: Phase distribution = {1:1, 3:1, 6:1, 7:1} — phase LEGACY 3/6 CÒN ROW SỐNG (seed cũ); multi-pair (cùng ProjectId+WorkItemId ≥2 phiếu) = 0 trong Dev. Prod: UNVERIFIED run này (SSH password extraction fail ×2 — không chặn spec; predicate viết dạng loại-trừ tự cover).

Tests PeBudgetFreezeTests.cs: peer-seed helper :311-332 — CreatedAt override THỦ CÔNG (peer.CreatedAt = createdAt :332; TestApplicationDbContext KHÔNG wire audit interceptor); test lũy kế :435-451 seed 2 peer CreatedAt sớm hơn baseT, phiếu xét pe.CreatedAt = baseT :451. Own-quote seed helper :277-306 (IsWinner :287 + IsSelected :306).

PHẦN 6 — Phase-set TẠM TÍNH trade-offs (Claim 4)

Phase Vào tạm-tính? Trade-off
ChoDuyet=10 CORE "cung đặc thù" — đã trình, sẽ duyệt. Với row2-delta là gap chính; row1 ĐÃ gồm sẵn
TraLai=98 khuyến nghị đang sửa để gửi lại — sẽ quay lại luồng duyệt; badge trạng thái để người duyệt tự cân
DangSoanThao=1 ⚠️ FLAG ANH "tất cả lun" (lời em chat) NHƯNG: (i) nháp = RIÊNG TƯ S89 (PurchaseEvaluationFeatures.cs:893-898 chặn approver mở nháp) — cộng tiền nháp vào matrix phiếu khác = lộ thông tin nháp; (ii) số nháp chưa chốt → nhiễu. Khuyến nghị: list-only (hiện MÃ + badge "nháp" trong danh sách cảnh báo, KHÔNG cộng delta tiền)
TuChoi=99 terminal — không bao giờ thành "chính xác"; nghịch mục đích "số sẽ thành chính xác khi phiếu trước duyệt"
DaDuyet=7 đã nằm trong CHÍNH XÁC
Legacy 2-6 gộp Dev DB CÓ row (3, 6) — predicate loại-trừ Phase NOT IN (DaDuyet, TuChoi) tự cover, an toàn hơn liệt kê dương

Khuyến nghị tập mặc định: pendingPriorPes list = peers Phase NOT IN (7, 99) (mọi non-terminal, kể cả nháp — có badge); delta-TIỀN row2 = non-terminal TRỪ nháp (NOT IN (1, 7, 99)) + IsWinner-existence + SUM IsSelected; delta-TIỀN row1 = phases chưa nằm trong row1 hiện tại TRỪ nháp (= TraLai + legacy 2-6). Lead quyết + flag anh điểm nháp.

PHẦN 7 — Edge cases (Claim 7) — từng edge tạm-tính xử ra sao

  1. WorkItemId null: budgetSummary null toàn cục (gate Features.cs:912) → tạm-tính + warning KHÔNG TỒN TẠI (đúng — không định nghĩa được "cùng cặp"); banner "gắn hạng mục" cũ giữ (PeDetailTabs.tsx:1297-1304).
  2. Multi-winner D4/IsSelected (S114): delta dùng ĐÚNG predicate hiện hành (IsWinner existence + SUM IsSelected — mirror accumulator :50-60) → nhất quán cardinality, 0 quy ước mới.
  3. Giá âm (S116): SUM signed nguyên trạng → delta có thể ÂM; FE render dấu như mọi số tiền; KHÔNG guard thêm.
  4. MinMax (S117): ApprovedPrice* KHÔNG feed lũy kế (lũy kế = BudgetPeriodAmount + quote-sums; HĐ GiaTri cũng không đọc giá chốt) → orthogonal, 0 tác động.
  5. PRO-split Mig 56: proInitialAmount chỉ là FALLBACK DISPLAY row3 (PeDetailTabs:1516), accumulator dùng BudgetPeriodAmount ?? 0 — tạm-tính giữ nguyên quy ước (không sửa drift-vector sẵn có trong scope này).
  6. CcmBudgetPeriodAmount Mig 59: row1 CHỈ SUM BudgetPeriodAmount (PRO-side); CCM-kỳ-này per-phiếu KHÔNG vào lũy kế hiện tại → delta CŨNG KHÔNG (thêm = đổi semantics CHÍNH XÁC, ngoài scope).
  7. Mốc CreatedAt: tạm-tính GIỮ NGUYÊN peer-window CreatedAt < this.CreatedAt (khác window = 2 tập peer khác nhau — số chính xác và tạm tính hết so sánh được). Phiếu bị đè CreatedAt (seed/import) nằm ngoài/trong window y hệt semantics hiện tại. Test seed mirror PeBudgetFreezeTests:311-332/:435-451.
  8. Own-phase TuChoi/TraLai: live branch vẫn compute; FE ẩn NUDGE màn duyệt khi phiếu terminal (duyệt không còn); matrix vẫn hiển thị (thông tin).
  9. Audit gap (phiếu sau duyệt trước phiếu trước): snapshot CHỈ chốt 4 số CHÍNH XÁC; tạm-tính không snapshot → sau này không tái dựng "lúc duyệt có cảnh báo N phiếu". Optional-recommended: nối ContextNote LogTransitionAsync tại finalize "Duyệt khi N phiếu trước chưa duyệt: [mã…]" — 0 cột mới, Changelogs pattern Mig 61 đã có.

PHẦN 8 — PROPOSED SPEC 3-mục

① Tính năng / đề bài

UAT anh Kiệt FDC 15/07 (chốt 16:10): vì "cung đặc thù" (phiếu sau trình khi phiếu trước cùng gói thầu CHƯA duyệt), lũy kế PE phải: (a) "bắt lên và tính toán" cả phần góp từ phiếu chưa duyệt = Lũy kế TẠM TÍNH, phần góp NHẬN DIỆN ĐƯỢC (note màu); (b) Lũy kế CHÍNH XÁC = semantics hiện tại GIỮ NGUYÊN 100%; (c) người duyệt cuối (CEO/duyệt thay) NHÌN THẤY danh sách "phiếu trước chưa duyệt [mã — trạng thái]" lúc duyệt — informational nudge, KHÔNG hard-block. Ground-truth thu hẹp đề: row1 "NS trình duyệt trước" ĐÃ gồm ChoDuyet sẵn (accumulator :43-44). Gap thật = row2 "Kỳ trước đã chọn thầu" (DaDuyet-only) + row6 dẫn xuất, và (tuỳ tập chốt) row1 thiếu TraLai/nháp/legacy.

② Cách implement

BE (0 migration, 0 endpoint mới, 0 đụng snapshot-path):

  1. PeBudgetAccumulator.cs — THÊM record PePendingAccumulation(PendingPrevSubmittedTotal, PendingPrevSubmittedCount, PendingPrevSelectedTotal, PendingPrevSelectedCount, PendingPriorPes) + method MỚI ComputePendingAsync(db, projectId, workItemId, peId, peCreatedAt, ct). Predicate: peers cùng cặp + CreatedAt < + Phase NOT IN (DaDuyet, TuChoi); delta-selected mirror :50-60 (IsWinner-existence + SUM IsSelected); delta-submitted = phases ngoài {ChoDuyet, DaDuyet} theo tập chốt. PePendingPriorPeDto(Id, MaPhieu, Phase) cho danh sách. KHÔNG sửa ComputeAsync (snapshot-path + Mig 67 bất động).
  2. PeBudgetSummaryDto (PurchaseEvaluationDtos.cs:331-353) — append CUỐI sau BudgetFrozen: 4 số pending (default 0) + IReadOnlyList<PePendingPriorPeDto>? PendingPriorPes = null (pattern append-an-toàn như BudgetFrozen S133).
  3. GetPurchaseEvaluationQuery — nhánh LIVE (Features.cs:957-984): gọi ComputePendingAsync, fill fields. Nhánh FROZEN (:930-955): pending = 0/null/empty — R1-S133 giữ nguyên: phiếu DaDuyet là bảng snapshot bất biến, KHÔNG thêm số động (tạm-tính trên phiếu đã duyệt vô nghĩa + vi phạm freeze).
  4. (Optional-recommended, đóng audit-gap): tại 4 finalize-site + admin-site, nếu pending priors tồn tại → nối vào comment/ContextNote của LogTransitionAsync "Duyệt khi N phiếu trước cùng gói chưa duyệt: [mã…]" — 0 cột, Changelogs pattern sẵn.

FE (2 app): 5. types/purchaseEvaluation.ts ×2 app (KHÔNG SHA-mirror — edit block PeBudgetSummary TỪNG app): +5 field mới. 6. PeDetailTabs.tsx (SHA-mirror 2 app 47c97df1 — sửa 1 copy 2): matrix gate !bs.budgetFrozen:

  • Row 2 + Row 6: sub-dòng amber khi pendingPrevSelectedCount > 0: "Tạm tính: {row2+delta2} (+{delta2} từ {n} phiếu chưa duyệt)"; row6_tạm = row6 + delta2.
  • Row 1 + Row 5 (nếu delta1 bật): tương tự với delta1; r5_tạm per-cột = r5 + delta1.
  • Danh sách mã pending (MaPhieu + PhaseLabel badge màu per-phase) — render nhỏ dưới matrix hoặc tooltip.
  1. PeWorkflowPanel.tsx (SHA-mirror 9d88e671): (a) banner amber cạnh V2-banner (:437) khi evaluation.budgetSummary?.pendingPriorPes?.length > 0 && !readOnly: "⚠ N phiếu trước cùng gói thầu CHƯA duyệt: A/012 (Đã gửi duyệt), … — cần duyệt phiếu trước trước phiếu này"; (b) NHẮC LẠI block đó TRONG Dialog duyệt khi isApproveAction (vùng :509-514, trên bộ chọn giá) — người duyệt cuối (shouldPickPrice :510 = predicate CEO/duyệt-thay sẵn có) chắc chắn thấy trước "Xác nhận". KHÔNG disable nút.

③ Checklist per-vai + acceptance ĐO ĐƯỢC

implementer-backend:

  • ComputePendingAsync mới; acceptance: git diff vùng ComputeAsync = 0 dòng; build xanh.
  • DTO append-cuối; acceptance: 2 construction site (:946/:975) set explicit; không site nào khác gãy.
  • Frozen → pending 0/empty; acceptance: GET phiếu DaDuyet+SnapshotAt → pendingPriorPes=[], 4 số 0.
  • Live → pending đúng; acceptance: curl GET phiếu B (peer A ChoDuyet+winner) → pendingPrevSelectedTotal = SUM quote IsSelected của A, list chứa {maPhieu A, phase 10}.
  • KHÔNG migration: snapshot ApplicationDbContextModelSnapshot.cs diff = 0.
  • (opt) Changelog note finalize; acceptance: transition finalize khi có pending → Changelogs row ContextNote chứa mã A. implementer-frontend (×2 app):
  • types 2 app; acceptance: grep pendingPriorPes fe-{user,admin}/src/types/purchaseEvaluation.ts = 2 hit.
  • PeDetailTabs sub-rows + list; acceptance: SHA 2 app IDENTICAL sau sửa; phiếu frozen KHÔNG render tạm-tính.
  • PeWorkflowPanel banner + dialog-block; acceptance: SHA 2 app IDENTICAL; nút Xác nhận KHÔNG bị disable bởi pending.
  • UAT flow đo được: A ChoDuyet → mở B: matrix row2 hiện tạm-tính + banner liệt kê A; duyệt A → DaDuyet → refetch B: tạm-tính biến mất, row2 chính-xác tăng ĐÚNG delta cũ. test-specialist (pattern PeBudgetFreezeTests seed :277-306/:311-332):
  • ComputePendingAsync: peer ChoDuyet+IsWinner+IsSelected → delta2 = tổng; peer TuChoi → 0; peer DaDuyet → 0 (bên chính xác); TraLai/nháp theo tập chốt.
  • Peer-window: peer CreatedAt > this → loại (mirror :435-451).
  • Quote âm trong delta (signed).
  • Frozen phiếu → pending rỗng.
  • (opt) finalize ghi Changelog note.

Phân rã hmw staged (kiểu S133): INV done (file này) → IMPL-A backend (items 1-4) → IMPL-B frontend 2-app (5-7, sau A merge DTO shape) → TEST-C (sau A; FE acceptance UAT tay). A/B song song được nếu chốt DTO-shape trước; C sau A.

DECISION-POINTS (lead quyết / flag anh)

  1. DangSoanThao (nháp) vào delta-TIỀN? A=include-all (đúng verbatim "tất cả lun") / B=list-only mã+badge, KHÔNG cộng tiền (S89 nháp-riêng-tư + số chưa chốt) — tao khuyến nghị B, flag anh Kiệt.
  2. Delta row1 (TraLai+legacy BudgetPeriodAmount) làm luôn hay chỉ row2? Khuyến nghị: LÀM (nhất quán "tất cả") — chi phí ~0 (cùng query).
  3. Nudge mức nào: plain-info (khuyến nghị) / +confirm-checkbox FE "tôi biết N phiếu trước chưa duyệt" (rẻ, không BE) / hard-block BE (KHÔNG khuyến nghị — nghịch "cung đặc thù"; nếu anh muốn thì guard ở 4 finalize-site + admin bypass).
  4. Changelog audit note lúc finalize (optional-recommended — đóng gap "về sau không chứng minh được lúc duyệt đã cảnh báo").
  5. TuChoi exclude — khuyến nghị chắc, nêu để anh confirm.

STATUS: COMPLETE — return digest gửi kèm.