diff --git a/.claude/WAL.md b/.claude/WAL.md index 492b02c..c5e8722 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -5,8 +5,10 @@ goal: 2 mạch song song — (A) Supplier Excel-import BUILD→close→deploy · chain A (Supplier import): [x] BE DONE build EXIT-0 ✅ + case-fix (OrdinalIgnoreCase) verified [x] test-specialist DONE (aac55f3d): SupplierExcelImportServiceTests 10 test → dotnet test 450 PASS (440→450), 0 prod-bug; case-collation test có RĂNG (SQLite BINARY → dedup ở service không dựa DB) -[!] FE fe-admin (BG) Import-Excel button→dialog upload→preview-table→confirm -[ ] /fable-clone review đóng-spec → adjust (⚠️ bake ExpectedHeaderTokens 30 real-token đã-extract; Case-9 auto-flip LayoutValid=true, 0-test-edit) → ĐÓNG SPEC + commit + cicd +[x] FE DONE (a513): SupplierImportDialog 2-app byte-identical + SuppliersPage nút Import; npm build ×2 PASS +[x] close-review wf_75d6149b-9f7 GO-WITH-ADJUSTMENTS (2/2): 6-axis BE PASS + FE wiring+enum-int+mirror PASS + Mig63-prod-safe + 4NCC-reimport-fill-nulls-safe +[!] APPLY 3 adjust (SAU khi PE-BE ab2db8c6 land, tránh concurrent-build): (1)bake 30 header token từ test RealFileHeaderTokens→service:37-66 (2)+.Normalize(FormC) NormalizeHeader:309 (3)FE guard nút Import Suppliers/Create→Admin,CatalogManager 2-app +[ ] build+test → reviewer → commit+push → cicd DEPLOY Supplier prod chain B (PE sign-off change): [x] investigate wf_fd8f2bfe-9f6 + anh Kiệt chốt 6 decision: (1)A1-bypass-subordinate-only+creator-ký-tay (2)no-hard-guard-single-person (3)EDGE-5-hard-lock-DaDuyet (4)1-nút (5)opinion-history (6)phiếu-mới-only diff --git a/.claude/workflows/runs/2026-07-12-S112-supplier-import-close-review/review-L1-BE-adjust.md b/.claude/workflows/runs/2026-07-12-S112-supplier-import-close-review/review-L1-BE-adjust.md new file mode 100644 index 0000000..ce705de --- /dev/null +++ b/.claude/workflows/runs/2026-07-12-S112-supplier-import-close-review/review-L1-BE-adjust.md @@ -0,0 +1,32 @@ +**HEADLINE:** GO-WITH-ADJUSTMENTS — Lăng-kính FE/E2E/DEPLOY sạch về bản chất (wiring endpoint đúng, hợp đồng enum-int khớp byte-exact, mirror 2 app thật, Mig 63 an toàn prod); cần 1 adjust bắt buộc đã biết (bake header) + 1 catch MAJOR độc lập (nút Import dùng guard rộng hơn quyền endpoint). + +LĂNG-KÍNH FE + E2E + DEPLOY (đọc source thật, working-tree chưa commit). + +(1) FE gọi đúng endpoint + map response — PASS. Preview: SupplierImportDialog.tsx:118-122 gửi multipart field 'file' khớp param controller IFormFile file (SuppliersController.cs:67). Confirm: :134 gửi body { rows } khớp SupplierImportConfirmCommand(Rows). Type FE SupplierImportPreview/Result khớp 1-1 DTO (rows/newCount/updateCount/skipCount/errorCount/warnings/layoutValid). + +(2) Hợp đồng enum-int render nhãn VN — PASS (verify sâu). BE SupplierType=1-6 / FE types/master.ts:11-18=1-6; SupplierTypeLabel:22-29 phủ đủ 1-6. SupplierStatus BE=FE=1-3. RowImportStatus BE 0-3 (SupplierImportDtos.cs:12-18)=FE const 0-3 (Dialog:19-24). CHỐT: KHÔNG có JsonStringEnumConverter (Program.cs:33-34 chỉ UtcDateTimeJsonConverter) → enum serialize thành INT, đúng kỳ vọng SupplierTypeLabel[r.type] (:305); feature Supplier list hiện hữu đã chứng minh int này ở prod. + +(3) Confirm disable/hide — PASS. Hide khi layoutValid=false: nút chỉ render trong preview && preview.layoutValid (:188). Disable khi errorCount>0: disabled={preview.errorCount>0 || confirmMut.isPending} (:191). + +(4) Round-trip rows verbatim — PASS. confirmMut.mutate(preview.rows) (:190) gửi nguyên rows. Field camelCase FE khớp PascalCase BE (authorizationNote/linkGuq/ownerPmh/supplierStatus/sourceUpdatedAt/existingSupplierId). Confirm TỰ re-derive New/Update từ DB + re-scan hard-error + NormalizeLengths (không tin status client) → an toàn. SourceUpdatedAt qua UtcDateTimeJsonConverter coi naive=UTC → KHÔNG lệch ngày cho VN (UTC+7). + +(5) Invalidate list — PASS. qc.invalidateQueries(['suppliers']) (:140) prefix-match key list ['suppliers',{page,search,...}] (SuppliersPage.tsx:81) → list refresh. + +(6) Mirror 2 app THẬT — PASS. SupplierImportDialog.tsx admin vs user byte-identical (13426 bytes, git diff exit 0); SuppliersPage.tsx byte-identical (exit 0); khối enum SupplierType/Status/Label trong types/master.ts identical giữa 2 app. + +(7) E2E upload file thật (30 cột, 4 NCC) — PASS SAU BAKE. Luồng preview→bảng New/Update+counts→"Xác nhận nhập"→confirm→toast+invalidate+đóng: hợp lý đầu-cuối. 🔴 ADJUST BẮT BUỘC (gating): SupplierExcelImportService.cs:35-67 ExpectedHeaderTokens hiện BEST-GUESS ≠ 30 token thật; so khớp Ordinal-exact sau NormalizeHeader (:223) → chưa bake = MỌI file thật bị từ chối LayoutValid=false. Bake = thay 30 phần tử bằng đúng RealFileHeaderTokens có sẵn trong test (SupplierExcelImportServiceTests.cs:419-451). VERIFY: Test Case-9 (:310) robust 2 chiều LayoutValid.Should().Be(shouldMatch) → sau bake shouldMatch=true → tự khẳng định LayoutValid=true, 10/10 test giữ xanh, KHÔNG cần sửa test. Claim "tự lật khi bake" ĐÚNG. + +(8) DEPLOY-RISK prod live — GO. Dup-risk 4 NCC: xác nhận TRUONGGIANG/TANPHU/TGN/DONGDUONG seed UNGATED mỗi startup tại DbInitializer.cs:2775-2781 (SeedRealMasterDataAsync, comment :2769 "Import Excel Database NCC anh Kiệt"), Code=col-4 file. Re-import → khớp CI theo Code → Update → FillNulls (:381-409 chỉ điền field NULL, không đè non-null) → KHÔNG nhân đôi, KHÔNG đè. AN TOÀN. Sắc thái: là fill-nulls (làm giàu cột mở rộng đang null), KHÔNG phải no-op tuyệt đối; fill-nulls-safe = re-import KHÔNG sửa được field đã có giá trị (vd Status). Mig 63 20260712070117_AddSupplierImportSourceFields: 2 AddColumn nullable (SourceUpdatedAt datetime2 null + SourceUpdatedBy nvarchar(200) null) → metadata-only SQL Server, an toàn apply live; 3-file rule OK (.cs+.Designer.cs+ModelSnapshot đều có 2 cột); SupplierConfiguration.cs:45 HasMaxLength(200) khớp. + +CATCH ĐỘC LẬP (không có trong brief): 🟠 MAJOR (FE guard/authz-scope mismatch). Nút "Import Excel NCC" bọc PermissionGuard menuKey=Suppliers action="Create" (SuppliersPage.tsx:214-220, mirror 2 app), NHƯNG 2 endpoint import yêu cầu [Authorize(Roles="Admin,CatalogManager")] (SuppliersController.cs:64,77). Theo S59, endpoint Create (POST /suppliers) MỞ cho mọi user đăng nhập → user có quyền-menu Suppliers.Create nhưng KHÔNG có role Admin/CatalogManager (điển hình user thường fe-user) sẽ THẤY nút Import nhưng bấm → 403 ở preview. BE AN TOÀN (không lỗ hổng), là khiếm khuyết UX: guard rộng hơn authz endpoint. Khuyến nghị gate nút Import theo role Admin/CatalogManager (hoặc ẩn ở fe-user) cho khớp. Không phải blocker deploy. + +POSITIVE (kháng soi mói): 24 hằng Max* trong service (:76-81) khớp BYTE-EXACT độ dài cột EF → không có bug 500-on-save; dedup CI in-memory (không dựa DB) chặn unique-violation 500 prod (test chứng qua SQLite binary collation); all-or-nothing + fill-nulls-safe; multipart header set tay khớp 4 upload đang chạy prod (ContractAttachments/PeDetailTabs/FormsPage) → không phải bug boundary; DI/config/snapshot đăng ký đủ. + +PHẠM VI: KHÔNG tự chạy lại dotnet build/test/npm build — xác minh bằng nhất quán tĩnh (DI+config+snapshot+DTO+controller+command khớp; 10 [Fact] đếm đúng; collection-expression C#12 hợp .NET 10). Nhận claim build EXIT-0 + 450 PASS ở mức tin-cậy-có-điều-kiện. + +## Open Questions +1. [OWNER — em main/anh Kiệt] Nút "Import Excel NCC" có nên hiện ở fe-user cho user thường không? Guard hiện = Suppliers.Create (rộng) trong khi endpoint = Admin/CatalogManager (hẹp) → user thường thấy nút nhưng 403 khi upload. Đề xuất đổi guard nút Import sang điều kiện role Admin/CatalogManager cho khớp endpoint, hoặc bỏ dialog khỏi fe-user (import hàng loạt là thao tác admin). Không phải blocker (BE chặn đúng) nhưng nên xử trước/ngay sau deploy. + +2. [VERIFY khi bake — rủi ro E2E không thể loại trừ tại đây] 30 token bake lấy từ brief/test, CHƯA đối chiếu file .xlsx thật trên máy anh Kiệt. Fingerprint exact-match + hardcode HeaderRow=4/DataStartRow=5 + map theo chỉ-số-cột-tuyệt-đối. Nếu file thật lệch (space/ký-tự-ẩn/hàng-header-khác/cột-thừa) → bị từ chối (fail-safe, có "Header nhận được" trong hộp đỏ FE để copy về sửa 1 vòng, KHÔNG hỏng dữ liệu). Khuyến nghị: upload file thật 1 lần (prod hoặc bản copy) xác nhận LayoutValid=true TRƯỚC khi tuyên bố xong. + +3. [Xác nhận nhỏ] fill-nulls-safe khiến re-import KHÔNG sửa được field đã có giá trị ở 4 NCC seed (vd Status seed=DangHoatDong sẽ không đổi dù Excel ghi Blacklist). Đúng ý decision-4 chứ? Nếu cần "Excel thắng" cho vài cột (Status/Note) thì phải chốt riêng. diff --git a/.claude/workflows/runs/2026-07-12-S112-supplier-import-close-review/review-L2-FE-deploy.md b/.claude/workflows/runs/2026-07-12-S112-supplier-import-close-review/review-L2-FE-deploy.md new file mode 100644 index 0000000..652c1d1 --- /dev/null +++ b/.claude/workflows/runs/2026-07-12-S112-supplier-import-close-review/review-L2-FE-deploy.md @@ -0,0 +1,27 @@ +**HEADLINE:** GO-WITH-ADJUSTMENTS — Logic BE đúng trên cả 6 trục kiểm; chỉ cần bake 30 token header thật (đã có ground-truth trong test, col→field 30/30 khớp) + 1 khuyến nghị Unicode FormC trước lần upload thật đầu tiên. + +Đã đọc độc lập toàn bộ 8 file BE (service, DTOs, controller, 2 CQRS command, interface, Mig 63 + Designer + snapshot, entity, EF config, tests). Kết luận: implementation vững, adjust đã biết là bắt buộc và được test bảo vệ, thêm 1 de-risk trước deploy. + +1) CASE-FIX (OrdinalIgnoreCase preview + confirm) — PASS. Preview: BuildCiIndex (SupplierExcelImportService.cs:444-454) dùng StringComparer.OrdinalIgnoreCase, lookup với codeKey=row.Code?.Trim() (:100,:110). Confirm: cả 3 collection đều OrdinalIgnoreCase — existingByCode qua BuildCiIndex (:156-157), batchByCode (:157), seen (:158). KHÔNG đường nào lọt Ordinal. Header-fingerprint dùng Ordinal (:223) là CỐ Ý vì so trên chuỗi đã ToUpperInvariant (NormalizeHeader:310) → hiệu quả case-insensitive. Store Code giữ hoa/thường gốc: NewSupplier Code=r.Code!.Trim() (:413), key index=s.Code?.Trim() (:449). Test Case 1 (:50) + 1b (:90) chứng minh dedup nằm ở SERVICE, không dựa DB (SQLite fixture BINARY collation vẫn xanh). + +2) FILL-NULLS SAFE — PASS. FillNulls (:381-409): mọi field theo mẫu if(e.X is null && r.X is not null) → chỉ điền khi DB null, KHÔNG đè non-null. Bỏ đúng Code/Name/Type (identity/required, xác nhận comment :380). Bao gồm Status + SourceUpdatedAt/By với null-guard. Test Case 2 (:124): Phone non-null "0900" không bị đè "9999", Address null → điền. + +3) ALL-OR-NOTHING — PASS. Pass 1 quét hard-error (thiếu Code/Name trên hàng không rỗng) TRƯỚC mọi thao tác ghi (:137-146); có lỗi → Committed=false + return, KHÔNG load/mutate/SaveChanges (:147-152). Upsert + SaveChanges 1 lần duy nhất cuối method (:188), atomic. Nếu SaveChanges ném → propagate GlobalExceptionMiddleware, rollback, Committed vẫn false. Test Case 3 (:158): 1 hàng lỗi → cả batch abort, hàng hợp lệ "A" cũng không ghi. Lưu ý: NormalizeLengths (:358-372) clamp đủ 26 field string → không có surprise maxlength ở đường confirm-trực-tiếp. + +4) PARSER — PASS. ReadRaw (:282-291) đọc ws.Cell(row,col) TUYỆT ĐỐI, KHÔNG CellsUsed → ô rỗng giữa không dồn cột (Test Case 7 :344: FAX col 9 rỗng không lệch col 10-12). Error-literal → null: check XLDataType.Error (:286) + set ErrorLiterals (:69-70) (:289). NAS backslash giữ RAW (:290, Test Case 6 :250 xác nhận \\NAS-SOL\...). Header-fingerprint reject (:216-230) + chặn file mở lỗi (:203-208) + không sheet (:213-214). Test Case 6b (:284). + +5) MIG 63 — PASS. Up thêm 2 cột (SourceUpdatedAt datetime2 nullable + SourceUpdatedBy nvarchar(200) nullable), Down drop cả 2 — reversible đối xứng (:14-38). 3-file rule đủ: .cs + .Designer.cs + snapshot cập nhật (ApplicationDbContextModelSnapshot.cs:3330-3335). Unique index Code filter [IsDeleted]=0 còn nguyên (:3355-3357). Là migration mới nhất theo timestamp (20260712070117). Entity Supplier.cs:39-40 đủ field all-nullable; SupplierConfiguration maxlength khớp 1:1 với 26 hằng số Max* trong service. + +6) ADJUST ExpectedHeaderTokens — XÁC NHẬN cần bake; col→field 30/30 ĐÚNG. ExpectedHeaderTokens hiện tại (:35-67) là BEST-GUESS, lệch token thật ~19/30 cột (vd col 5 "TÊN NHÀ CUNG CẤP" vs thật "TÊN CÔNG TY...", col 3 thiếu "(NTP/NCC/Cả hai)") → fingerprint reject file thật. Đã đối chiếu TỪNG token thật với field MapRow gán (:246-274): col 4 "TÊN VIẾT TẮT" → Code (upsert key), col 5 "TÊN CÔNG TY (đầy đủ pháp lý)" → Name (khớp semantics entity), col 10 composite → BankAccount, col 24 composite → MailRecipient, col 27 → Status, col 28 → Note, col 29/30 → SourceUpdatedAt/By. KHÔNG cột nào lệch/thiếu/thừa — 30 token ↔ 30 field đúng thứ tự. +ADJUST CHÍNH XÁC: thay 30 chuỗi tại SupplierExcelImportService.cs:37-66 bằng đúng 30 chuỗi đã có sẵn trong repo tại SupplierExcelImportServiceTests.cs:421-450 (hàm RealFileHeaderTokens) — tôi đã đối chiếu, KHỚP TUYỆT ĐỐI với 30 token trong brief. Sau bake, Case 9 (:310) tự lật shouldMatch=true → khẳng định LayoutValid=true (KHÔNG cần sửa test). + +RISK — MAJOR de-risk (nên gộp CÙNG deploy bake): Unicode NFC/NFD. NormalizeHeader (:306-311) không chuẩn-hóa dạng Unicode. Test Case 9 dựng workbook từ chính literal C# (NFC) nên chỉ chứng minh 2 mảng literal đồng nhất — KHÔNG chứng minh file Excel THẬT (soạn ngoài repo) sẽ khớp. Nếu file thật lưu dấu tiếng Việt dạng NFD thì Ordinal compare (:223) FAIL dù đã bake đúng token → vẫn reject "Sai layout" lần đầu. Fail-closed (không hư dữ liệu) + có recovery thủ công nhưng cần deploy lại. Acceptance criteria: thêm .Normalize(NormalizationForm.FormC) vào NormalizeHeader (:309-310) — 1 dòng, zero-risk, bền với dạng chuẩn-hóa. + +MINOR (không chặn): (a) result.Updated (:170) đếm cả hàng fill-null-no-op như "updated" dù 0 field đổi — cosmetic. (b) ConfirmAsync tin hoàn toàn rows client (round-trip, không re-parse) — chấp nhận vì endpoint khóa Admin/CatalogManager + confirm re-resolve existing theo Code (không tin ExistingSupplierId :167) + clamp lại (:164). (c) Comment map col 5 nên đổi "TÊN NHÀ CUNG CẤP → Name" thành "TÊN CÔNG TY → Name" cho khớp (cosmetic). + +WIRE + MIRROR (Cat 1/3.9) — PASS. 2 endpoint import/preview + import/confirm khóa [Authorize(Roles="Admin,CatalogManager")] (SuppliersController.cs:64-81), wire mediator→handler→service THẬT (không mock/alert/TODO). FE dialog gọi api.post('/suppliers/import/preview') + '/confirm' thật (SupplierImportDialog.tsx:120,134). 2 file FE byte-identical (sha256 khớp). DI đăng ký (DependencyInjection.cs:45). Không có deploy claim (close-review PRE-deploy) → không chạy live curl. + +## Open Questions +1) [Owner: anh/em-main] Có gộp .Normalize(FormC) vào NormalizeHeader cùng lần bake token không? Khuyến nghị CÓ — de-risk lần upload thật đầu tiên khỏi rủi ro NFC/NFD (không thể verify trước vì file "Database NCC" thật không nằm trong repo). +2) [Owner: anh Kiệt/em-main] Xác nhận 3 giả định hard-lock của parser với file thật: (a) header đúng row 4, (b) data bắt đầu đúng row 5 (không có hàng ví dụ/sub-header xen giữa), (c) đúng 30 cột (không cột ẩn chèn giữa). Lệch bất kỳ → reject. Nếu chưa cầm file, nên upload thử 1 lần trên DEV sau khi bake để chốt TRƯỚC khi lên prod. +3) [Owner: em-main] Quyết định do-fix của MINOR (a) result.Updated đếm no-op: giữ nguyên (chấp nhận báo số hơi lố) hay chỉ đếm khi FillNulls thực sự đổi field? Không chặn deploy.