---
name: project-s113-supplier-import-v2-review
description: S113 Supplier import v2 close-review (owner anh Kiệt) — GO-WITH-ADJ, 2 coupled must-fix = visibility consumers unwired + CreateSupplier IsPublic missing
metadata:
  type: project
---

# S113 Supplier import v2 — pre-commit review (2026-07-12, owner anh Kiệt)

**Verdict: GO-WITH-ADJ (2 must-fix).** Build 0/0, 19 new supplier tests PASS (477 total). Dedup/publish/authz/mig/null-fix/SHA-mirror all solid. Feature's storage+admin half done; **the "ẩn" (hide-draft) half of R4/R6 NOT delivered.**

## CAUGHT (MAJOR, coupled — the cardinality S87/S88 miss the spec itself flagged as recover-lane):
1. **Consumers don't pass `published=true`** — `published` filter wired ONLY in admin SuppliersPage (both apps). PE picker `PeDetailTabs.tsx:2222` + `ContractCreatePage.tsx:321` (×2 apps = 4 sites) fetch ALL suppliers → draft NCC (IsPublic=false, Code="") LEAK into pickers. Defeats R4/R6. Concrete harm: draft Code="" selectable in ContractCreate → malformed contract# `SOL&/01`. Publish-gate becomes pointless.
2. **CreateSupplierCommand:86 does NOT set IsPublic** — manual "Thêm NCC" (Code required NotEmpty) → IsPublic=false (DB default) → new coded NCC becomes DRAFT. Seed✓/backfill✓/import✓/publish✓ set IsPublic but runtime CREATE path missed. Coupled: if #1 fixed, manual+inline-create (PeDetailTabs:2244) vanish from picker until re-published. Fix = `entity.IsPublic = true` (Code always non-empty).

## Verified SOLID (resisted scrutiny):
- **Dedup MST-primary+Code-backstop**: no 500-unique-violation path found. T6 proves OQ4 (MST→A/Code→B): FillNulls skips Code + B untouched (no silent-clobber). Blank-MST no-merge (T3). Batch precedence existing→batch, MST→Code. Service-level OrdinalIgnoreCase (T5 SQLite-BINARY proves not DB-collation-dependent).
- **Publish D2**: gate Code-non-empty on publish=true (T10); NOT via UpdateSupplier (own command, #73-safe); UpdateSupplier preserves IsPublic (not in field-set). Test HONESTLY flags publish unique-check uses DB-collation≠service OrdinalIgnoreCase (prod SQL CI handles).
- **Authz D3**: preview/confirm Roles→Policy=Suppliers.Update; publish+template new same policy; auto-reg (Suppliers∈All, Update∈Actions); List = class-[Authorize] any-auth (pickers OK, no #44). FE Import Create→Update both apps.
- **Mig 64**: 3-file (cs+Designer:3272/3365+snapshot), reversible Down, backfill `SET IsPublic=1` idempotent, filter `[Code]<>''` allows N drafts, no new table.
- **null-Code fix**: `(r.Code??"").Trim()` :462 + `IsPublic=!IsNullOrWhiteSpace` :209 + guarded codeKey all sites; no unguarded `.Code!` left.
- **FE SHA-mirror**: SuppliersPage+ImportDialog byte-identical (sha256); isPublic in both types; badge full-class-literal.

## Nice-to-have: stale svc comment :31-34 (old "BEST-GUESS") contradicts new :35-37; Down restores filter w/o `[Code]<>''` → rollback-after-drafts CreateIndex fails if ≥2 empty drafts; no BuildTemplate→Preview round-trip test; Case-9 self-consistency ≠ real-file (S112 carry-over, artifact off-repo).

**Lesson: implementer added the query-param + admin-page filter but stopped at the ADMIN surface — the whole point (hide from CONSUMERS) needs grep-all-consumer wiring + the create write-path. "Filter added" ≠ "drafts hidden". Green tests covered plumbing not the end-to-end R4/R6 promise.**
