---
name: project-s112-supplier-import-review
description: S112 Supplier Excel-import (Phase B, Approach A) FE+E2E+DEPLOY close-review — GO-WITH-ADJUSTMENTS; caught FE guard/authz-scope mismatch + header-vs-realfile residual risk
metadata:
  type: project
---

# S112 (2026-07-12) Supplier Excel-import close-review — GO-WITH-ADJUSTMENTS (FE/E2E/DEPLOY lane)

Uncommitted working-tree. Feature: upload Excel "Database NCC" → preview → confirm upsert (fill-nulls-safe, all-or-nothing). Verdict **GO-WITH-ADJ**: 1 mandatory known adjust + 1 MAJOR independent catch + fail-safe residual.

**Mandatory adjust (planned, gating):** `SupplierExcelImportService.cs:35-67` ExpectedHeaderTokens = BEST-GUESS ≠ 30 real tokens → EVERY real upload rejected until baked. Test Case-9 (`SupplierExcelImportServiceTests.cs:310`) is robust 2-way self-adjusting (`preview.LayoutValid.Should().Be(shouldMatch)`) — bake flips it to assert true, all 10 tests stay green. RealFileHeaderTokens() at :419 already holds the 30 targets.

**MAJOR independent catch (I found, not in brief):** FE Import button guarded `PermissionGuard menuKey=Suppliers action=Create` (SuppliersPage.tsx:214) but import endpoints require `[Authorize(Roles="Admin,CatalogManager")]` (SuppliersController.cs:64,77). Per S59 Create-endpoint is open to every auth user → users with Suppliers.Create-menu but no Admin/CatalogManager role (notably regular fe-user) see a non-functional button → 403 on upload. BE SECURE (no hole), UX defect. Guard broader than endpoint authz. **Lesson: when reusing an existing menu-permission guard for an Admin-scoped NEW action, the guard scope silently under-restricts — re-derive guard from the ENDPOINT authz, not the sibling button.**

**Residual (MINOR, fail-safe):** header fingerprint + HeaderRow=4/DataStartRow=5 + absolute-col map validated only vs brief tokens, NOT the actual file on anh Kiệt's machine. Mismatch → LayoutValid=false + "Header nhận được" diagnostic in FE red box → 1-iteration re-bake, no data corruption.

**Deploy dup-risk (point 8) CONFIRMED safe:** 4 real NCC (TRUONGGIANG/TANPHU/TGN/DONGDUONG) seeded ungated `DbInitializer.cs:2775-2781` (SeedRealMasterDataAsync), Codes = Excel col-4. Re-import → CI-match → Update → fill-nulls-safe → no dup, no overwrite. Nuance: it's fill-nulls (enriches DB-null extended cols), NOT strict no-op; fill-nulls-safe means re-import canNOT correct an already-populated field. Mig 63 = 2 nullable AddColumn (datetime2+nvarchar200), metadata-only, safe on live prod; 3-file rule OK.

**Resisted scrutiny (positive):** (1) FE↔BE int-enum EXACT (Type 1-6, Status 1-3, RowImportStatus 0-3); no JsonStringEnumConverter registered (only UtcDateTimeJsonConverter) → ints confirmed. (2) 24 service Max* clamp constants match EF column lengths BYTE-EXACT → no 500-on-save gap. (3) multipart `Content-Type:multipart/form-data` manual header matches 4+ existing working uploads (ContractAttachments/PeDetailTabs/FormsPage) — not a boundary bug. (4) confirm re-derives New/Update from DB (doesn't trust client status) — secure. (5) CI dedup in-memory not DB-reliant → prevents prod SQL Server unique-violation 500 (test proves via SQLite binary collation). (6) SourceUpdatedAt UTC-converter treats naive as UTC → no VN day-shift. (7) dialog + SuppliersPage byte-identical ×2 app (mirror exit 0); SupplierType enum block identical. (8) invalidate `['suppliers']` prefix-matches list key `['suppliers',{...}]`. Round-trip camelCase fields all present.
