**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.
