← back to index

U9 — Product Import (XLSX/CSV)LOCAL

Bulk-create products + variants via Narong's canonical XLSX template. Source: Notion test-run doc item #12 "Product Import"; spec is the drawio flow diagram + nix_pos_import_template.xlsx + the Figma 3-stage UI. Client-side parse via SheetJS keeps bytes off the Worker; per-row transactions so one bad row doesn't roll back the rest.

Summary

Status
11/11 local · typecheck clean · awaiting Gate 1 approval
Repos
nix-cafe + nix-outdoor-sales-backend (migration only)
Files
1 new migration · 1 new dep (xlsx) · 7 nix-cafe files (5 new + 2 modified) · ~1,400 LOC
Migration
Add cafe.product_template.track_inventory BOOLEAN NOT NULL DEFAULT false. Existing rows pick up DEFAULT false automatically. UI toggle for the flag deferred to a followup; for v1 the column just stores the import value.
Decisions
7 locked from earlier scoping (see project_u9_product_import_scoping): track_inventory column · template price = first row · skip existing combos silently · "+ Product" → 4-item "+ New ▾" dropdown · auto-map by header + dropdown override · XLSX + CSV via SheetJS · Figma confirmed.

3-stage flow

1 · UploadDrag-drop or browse → xlsx parses the file client-side. Auto-maps each column header to a NIX field. "Import Template for Products" download served from /public/import-templates/.
2 · MappingTest / Import / Load / Discard tabs. Left rail: file metadata + sheet picker + "Use first row as header". Right table: File Column · NIX Field dropdown · Comments. Per-column errors highlighted after Test.
3 · Result6 stat tiles (templates / variants added / variants skipped / categories / attributes / values). Red sub-cards for runtime + parse errors with first 10 rows highlighted.

What ships

Data layer

Normalize + actions

UI

11/11 local checks

Migration .ts: ADD COLUMN IF NOT EXISTS track_inventory BOOLEAN NOT NULL DEFAULT false + DROP COLUMN on down()
migrate.js registry: U9 entry slots after U8
schema.ts: cafeProductTemplate.trackInventory declared with expected shape
Postgres: cafe.product_template.track_inventory exists, default false, NOT NULL
Public template shipped: /public/import-templates/nix_pos_import_template.xlsx with 9 expected headers (verified via SheetJS read)
xlsx_normalize.ts: NIX_FIELDS exports 9 expected slugs
autoMapHeader + normalizeSheet: required-field rejection + Café Latte × 3 parse (sales_price normalized "1.25"/"1.75"/"2.00"; product_values parsed to {attribute, value} pairs)
DAO round-trip on local lumiere: feed Café Latte × 3 → 1 template + 3 variants + 1 category + 1 attribute + 3 values; re-run same import → variantsSkipped = 3
products-list-client: NewMenu replaces "+ Product"; 6 expected testids present; routes Import records to /products/import; "Coming soon" labels on Export + Spreadsheet
import-client: 3-stage state machine + 18 expected testids covering all interactive surfaces
actions: validateImportAction + runProductImportAction both exported via "use server" with requireImportSession gate

Typecheck: npx tsc --noEmit on nix-cafe → 0 errors. Local migration applied via knex.

Out of scope (followups)

Gate 2 plan

  1. Commit nix-outdoor-sales-backend (migration), then nix-cafe (xlsx dep + 7 files).
  2. node migrate.js against prod Supabase from local. Verify cafe.product_template.track_inventory column exists across get-coffee + demo + lumiere.
  3. Wait for CF auto-deploy on nix-cafe.
  4. test-u9-prod.mjs on lumiere: SSO → /cafe/products → click "+ New ▾" → Import records → upload nix_pos_import_template.xlsx → Test → Import → assert summary card values → DB verify 1 template + 3 variants + 1 category + 1 attribute + 3 values. Re-upload → skipped = 3.
  5. Regression sweep 51/51 (phase2-cafe-multishop solo).
  6. Publish prod gallery.