Files
solution-erp/docs/changelog/sessions/2026-07-23-1730-S147-pe-attach-fix-project-dedup.md
pqhuy1987 5f6c3becfb [CLAUDE] Docs: S147 closeout — PE attach-fix (Trả lại) + project-dedup ship-prod
① nút đính kèm 'Bảng so sánh' gate drafter+phase (cả 2 app) · ② gom 3 cặp dự án trùng dấu-cách trên PROD (5 phiếu+5 NS repoint, 3 ẩn mềm) + vá gốc chuẩn hóa Code +4 test. CI #407 PASS, bundle rotate ×2 verify 2-nguồn. Session log + merge script (docs/changelog/data-fixes) + STATUS/HANDOFF + garble tally ×3 + harvest investigator/reviewer/cicd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:37:59 +07:00

6.1 KiB
Raw Blame History

S147 — PE đính-kèm sau Trả-lại + gom dự án trùng (SHIP PROD)

2026-07-23 · product/UAT session (khác chuỗi governance S135S146) · lead Opus 4.8 · /tiep (WAL trống, S146 đóng sạch) → 2 việc UAT anh giao.

Bối cảnh vào phiên

/tiep: WAL chain rỗng nhưng chạy đủ Sàn-3 (5 tín-hiệu MẠNH). Tín-hiệu ⑤ fire (unpushed wal: chạm .claude/sessions/session-1/closed.md) NHƯNG ORPHAN-L cân sổ (pause=1/resume=1/closed=1) ⇒ L1(S146) đóng sạch, không có mạch dở. Rồi anh giao 2 việc + chỉ đạo "invest + review chạy → sau đó fix".

Việc ① — Bug đính kèm "Bảng so sánh" không sửa được khi phiếu bị Trả lại

Triệu chứng (UAT, @Kenny/C Trà): phiếu Duyệt NCC bị Trả lại (TraLai=98) → người soạn KHÔNG xóa file đính kèm sai + KHÔNG tải file mới được.

Gốc (investigator + reviewer xác nhận): KHÔNG phải phân quyền. Nút xóa/tải của GeneralAttachmentsSection (khối "Bảng so sánh giá", purpose=ComparisonTable) gate theo readOnly của MÀN HÌNH, không theo trạng-thái + người-soạn. Màn "Danh sách" mở phiếu readOnly=true cứng (PurchaseEvaluationsListPage.tsx) → ẩn nút ở MỌI phase, kể cả TraLai vốn ĐÃ nằm trong isEditablePhase = {DangSoanThao, TraLai}. Màn "Thao tác" (workspace) vốn chạy được (đó là cách gỡ tạm). BE không hề chặn theo phase — cố ý (S78 cần approver upload ApprovalAttachment lúc ChoDuyet).

Fix (surgical, cả 2 app): trong ThongTinChonThauSection (PeDetailTabs.tsx) tính attachEditable = (isAdmin || isDrafter) && isEditablePhase(ev.phase), truyền readOnly={!attachEditable} CHỈ vào GeneralAttachmentsSection (:2013). KHÔNG đụng readOnly cấp :421 (kẻo mở nhầm HoSoLinkRow + winner). Blast-radius = 1 hàm, 0 signature-change.

3 bẫy reviewer bắt (đã né): (1) :421 bleed → HoSoLink; (2) SupplierAttachmentsCell gate readOnly && !approverEditMode — áp công-thức naive sẽ vỡ Mig 28 F3 (approver sửa QuoteDocument lúc ChoDuyet) → descope (bug chỉ về comparison-doc); (3) BE guard naive vỡ cả S78 lẫn Mig 28 F3không thêm BE guard. Test: FE-only, không có .NET test home → verify bằng tsc -b + vite build ×2 sạch.

Việc ② — Gom dự án trùng (khác dấu-cách trong mã) + vá gốc

Yêu cầu (UAT): FLOCK 01FLOCK01, FLOCK 03FLOCK03, CAL 01CAL01 (cây "Duyệt NCC" hiện trùng do mã lệch 1 dấu cách).

Gốc: Project Create/Update lưu Code thô + kiểm-trùng so-khớp-đúng-nguyên-văn (ProjectFeatures.cs x.Code == request.Code) → "FLOCK 01" ≠ "FLOCK01" đều lọt. Seed SeedRealMasterDataAsync chỉ sinh mã không-dấu-cách (idempotency key exact-Code, ungated) ⇒ nguồn dấu-cách = nhập tay. Reviewer sửa investigator: refs là loose-Guid (0 FK vật lý tới Projects) — trỏ lại an toàn thứ tự nhưng DB không có lưới an toàn → phải verify 0-ref trước ẩn.

Vá gốc (code, anh chốt "chỉ cắt đầu/cuối"): LƯU Code = request.Code.Trim() (giữ mã như gõ), nhưng KIỂM-TRÙNG so trên Code.Replace(" ","") (bỏ-dấu-cách) → chặn tạo/sửa thành bản trùng chỉ khác khoảng trắng. +4 test guard ProjectCodeNormalizationTests (EF dịch Replace trên SQLite OK).

Gom dữ liệu — Dev trước, prod sau:

  • Script docs/changelog/data-fixes/2026-07-23-merge-duplicate-projects.sql — idempotent, mỗi cặp 1 transaction, ABORT nếu >1 bản-giữ sống (reseed cycle), budget UNIQUE-collision → soft-delete, VERIFY 0 live-ref → else ROLLBACK. SET QUOTED_IDENTIFIER ON; GO baked-in (filtered-index DML bắt buộc, sqlcmd default OFF).
  • Dev: cặp FLOCK 01 (rỗng) ẩn mềm, idempotent re-run = SKIP.
  • PROD (qua ssh vietreport-vps, integrated auth -E → KHÔNG chạm password vrapp): đo trước (read-only) = cả 3 cặp live 2 bên, 0 HĐ, 1 bản-giữ/mã, 0 budget-collision → chạy gộp: 5 phiếu + 5 ngân sách trỏ lại, 0 số bỏ, 3 bản dấu-cách ẩn mềm. App-visible = 1 live row/mã. Đảo lại được (soft-delete). Verify em-main + kết-quả script 2-nguồn-khớp.
Mã giữ Trước Sau
CAL01 4 phiếu/2 NS 5 phiếu/3 NS
FLOCK01 2 phiếu/2 NS 4 phiếu/4 NS
FLOCK03 1 phiếu/1 NS 3 phiếu/3 NS

⚠️ Anh dặn: "sau này sẽ chạy tối ưu kỹ lại chỗ này" — đây là gom-sạch first-pass, chưa phải tối ưu cuối.

Deploy

2 commit tách việc: 7d39d8d (① FE 2-app) + 4510cea (② BE + test). Squash 5 wal: local trước push (reset origin/main, 0 wal: lọt origin). CI Run #407 SUCCESS 5m36s. Verify (cicd-monitor + em-main curl 2-nguồn-khớp): test gate 45D + 487I = 532 (was 483I → +4); bundle fe-admin DGPE6NkbB2j1LQ-v + fe-user DlTwAgJjC3wbJegk (CSS frozen = logic-only); smoke api/admin/eoffice + /health = 200; 0 migration (code-only, prod giữ Mig 67).

Lessons

  • #53 return-garble ×3 (investigator ①-attachment + reviewer ②-merge + cicd-monitor) — tất cả memory-curation-fragment, tất cả recover SendMessage-resume in-session 3/3 sạch. → feedback_agent_return_garble_recover (tally ×25→×28).
  • Prod access: S134 SSH flaky (reset trên reconnect nhanh) nhưng one-shot non-interactive OK + retry-loop; sqlcmd over SSH KHÔNG chết (S134 chỉ interactive); integrated auth -E (Administrator) né password; QUOTED_IDENTIFIER ON bắt buộc cho DML trên bảng có filtered-index (sqlcmd default OFF ≠ SSMS).
  • Reviewer-adversarial đáng giá: self-review sẽ ship 3 regression (bleed + Mig28 + S78). Reviewer ② sửa cả claim "FK" của investigator (thật là loose-Guid).

State

Mig 67 · 89 bảng · 532 test (45D+487I) · gotcha 82 · menu 54 · bundle admin B2j1LQ-v/user C3wbJegk · prod dự án hết trùng (5 phiếu repoint, 3 ẩn mềm).