← Back to all projects
In Progress Created 2026-08-12 0/25 tasks

Batik PO — Web App Port

Priority: P2 Category: PREP


Executive Summary

Port the Batik PO generator (currently the rigid Template (NEW) Google Sheet) into the RBD Reorder Tool web app as a new Batik POs module that mirrors the existing Cotton POs module 1:1. This is a rebuild, not a port of the sheet's positional 24-column precut matrix — precuts/kits become dynamic data rows. Spec is already written and confirmed: ~/ai-projects/mission-control/plans/batik-po-web-app-logic.md.


Research Phase

Current State

  • Cotton module is the template. It has a clean layered structure the batik module copies:
  • routers/cotton_pos.py (16 endpoints, prefix=/cotton-pos)
  • services/cotton_calc.py (per-SKU math + populate_collection_items NS ingestion)
  • services/cotton_import.py (pure module → 10-col NetSuite import; unit-tested rule-for-rule)
  • services/cotton_pricing.py (vendor×prefix rate lookup — generic, reuse as-is)
  • Tables: cotton_collections, cotton_po_items, cotton_precut_items, cotton_stacking_orders, cotton_pricing_rules
  • tests/parity/cotton_parity.py + JSON fixtures (tool-vs-sheet, $0.01 tol, known_gap handling)
  • Frontend: CottonPOListPage.tsx + CottonPODetailPage.tsx, nav entry in Layout.tsx, inline API calls via src/lib/api.ts
  • Batik multipliers already seeded (migration 015): ('batik','main',1.6), ('batik','precut',1.8), ('batik','cp',+2). No multiplier schema work needed.
  • NetSuite creds are EMPTY locally (.env ns_consumer_key/ns_token_key blank). Live data ingestion (saved search 3314 for batik) only runs server-side, and Stage A read-creds may not be loaded yet.

  • Batik calc is fully specified & sheet-validated (see logic doc + reference_batik_po_template memory):

  • Class-based SKU tiers (Data col D: Batik - Collection = main, Precut Batik = precut).
  • Precut yardage: piecesPerBundle × totalUnits ÷ divisor (FQ 4, RP 14, 5" 56, 10" 12, ASY 4, 25" 238).
  • Blended price-per-piece: round(Σ(pieces×rate)/Σpieces/divisor, 2), piece-weighted, with a uniform-rate fallback (rate÷divisor when recipe empty) + mixed-rate-only ESTIMATE flag — just shipped in the sheet.

  • 8-yd bolts (cotton is 15/10); flat per-yd rate by mill; precut surcharge by mill (Data BB).

  • Kits = data gap (KTBT- not in Data / search 3314) → deferred, same as the sheet.
  • No dropship (cotton-only concept).

Historical Context

  • The sheet has been hardened repeatedly (breakout overflow #REF!, price-per-piece SKU-lookup redesign, adjuster breakout-resize, fallback pricing). Those are sheet fixes; the web app sidesteps all of them by modeling precuts as lists. See reference_batik_po_template memory.

  • Cotton port already solved the hard infra questions (NS client, multiplier configs, parity harness pattern, export/import, email send). Batik inherits all of it.

Constraints

  • No live NS data locally → Phase 1 must be fixture-driven (like cotton's parity harness).
  • Prod DB rb_warehouse is a shared 74-table DB — batik owns only batik_* tables; touch nothing else.
  • Deploy is manual (no deploy.sh); all edits local → PR → merge → deploy. WarehouseAPI-style approval rules apply.
  • Never commit .claude/.

Options Considered — the one real fork: precut stacking recipe source

The precut price-per-piece and per-fabric yardage both depend on the "stacking recipe" (how many pieces of each fabric go in each precut bundle). In the sheet this is a manual grid. In the web app, where does it come from?

Option A: Fallback-only (no recipe UI)

Approach: Always price precuts as rate ÷ divisor (uniform-rate fallback). No recipe input. Pros: Zero UI lift; exact for uniform-rate collections (the overwhelming majority). Cons: Mixed-rate collections get an approximate price with no way to refine; per-fabric precut yardage can't be computed (only mill-PO piece pricing, which is what the import needs anyway). Effort: low

Option B: Full recipe editor

Approach: Rebuild the stacking grid as a web UI; buyer enters pieces-per-fabric per precut. Pros: Exact always; unlocks per-fabric precut yardage. Cons: Large UI build; replicates the sheet's most tedious manual step; most collections don't need it. Effort: high

Option C: Hybrid — fallback default + optional recipe refine (mirrors the sheet we just shipped)

Approach: Default to the uniform-rate fallback (works instantly, no input). Surface an "⚠ estimate — mixed rates" badge only when a collection has mixed fabric rates AND no recipe, exactly like the sheet flag. Allow an optional recipe entry later to refine those cases. Pros: Matches the exact behavior we just built & Cole approved in the sheet; low initial lift (recipe entry is a later, optional increment); correct-by-default for uniform collections. Cons: Mixed-rate refine deferred to a later phase. Effort: low initial, medium if/when recipe entry added


Chosen Approach

Decision: Option B (Full recipe editor now) — Cole's call 2026-08-12. The batik module includes a stacking-recipe editor (pieces-per-fabric-per-precut) so pricing is exact always and per-fabric precut yardage is computed. The uniform-rate fallback is retained as a graceful default (an empty recipe still prices via rate÷divisor + shows the mixed-rate ESTIMATE badge) so a fresh collection is never blank — but the recipe is the primary path.

Rationale:

  1. Exact pricing on every collection, including mixed-rate — no estimate compromise.
  2. Unlocks per-fabric precut yardage breakout (needed for the full yardage picture, not just mill-PO piece pricing).
  3. Recipe editor is the sheet's core value; buyers already think in stacking recipes, so the UI matches their mental model.

Trade-offs Accepted: larger frontend build (a recipe-grid component + batik_stacking_orders table); the recipe is manual input per collection, same tedium as the sheet — but that's the tool's job.


Implementation Plan

Phase 1 — Calc engine + parity harness (LOCAL, no NS dependency) ← start here

  • services/batik_calc.py — port class-based tiering, precut divisors, recipe-driven blended price-per-piece + per-fabric precut yardage (piecesPerBundle × totalUnits ÷ divisor), with uniform-rate fallback + mixed-rate flag when the recipe is empty. Pure functions, no DB/IO.

  • services/batik_import.py — pure module → 10-col NetSuite import (main yardage / precuts / casepacks / kits-deferred), mirroring cotton_import.py. Vendor-initials map for real batik mills (Chakra/Haryan/Pria Tampan/Batik Agung) — flag unknowns rather than guess.

  • tests/test_batik_calc.py + tests/test_batik_import.py — rule-for-rule unit tests.

  • tests/parity/batik_parity.py + 2–3 JSON fixtures exported from live sheet tabs (e.g. ARBORGRACE uniform; one mixed-rate collection if one exists) — assert tool == sheet within $0.01.

  • Reuse cotton_pricing.py as-is (or thin batik_pricing wrapper if surcharge-by-mill differs).

Phase 2 — Schema + data layer

  • Migration 016_create_batik_tables.sql: batik_collections, batik_po_items, batik_precut_items, batik_pricing_rules, batik_stacking_orders (recipe grid — in scope per Option B).

  • Seed batik_pricing_rules from the sheet's Simple Pricing tab (per-mill flat per-yd + precut surcharge).

  • Extend populate_collection_items-equivalent to ingest batik saved search 3314 (server-side; needs Stage A creds — verify A7 first).

Phase 3 — Router

  • routers/batik_pos.py (prefix=/batik-pos) — mirror cotton endpoints: list/create/get/patch/ delete, items patch, repopulate, apply-rates, export (internal + NetSuite), build-import, send.

  • Register in main.py.

Phase 4 — Frontend

  • BatikPOListPage.tsx + BatikPODetailPage.tsx (copy cotton pages, adjust columns for batik shape: 8-yd bolts, precut price-per-piece, mixed-rate badge, no dropship).

  • Stacking-recipe editor component (grid: fabrics × precuts → pieces per cell) wired to batik_stacking_orders; recomputes price-per-piece + per-fabric yardage on edit.

  • Nav entry in Layout.tsx (/purchasing/batik-pos, label "Batik POs").

  • Route wiring in App.tsx.

Phase 5 — Verify + ship

  • Parity harness green on all fixtures; unit tests green.
  • code-reviewer + qa-refactorer agents on the diff (per PR workflow).
  • PR from local (never edit prod); Cole reviews/merges; manual deploy.

Acceptance Criteria

Must Have

  • Batik parity harness reproduces the sheet's PO totals & line items within $0.01 on ≥2 fixtures.
  • Class-based tiering, precut divisors, and blended price-per-piece match the sheet.
  • Recipe-driven exact blended price-per-piece + per-fabric precut yardage; empty recipe falls back to rate÷divisor (uniform exact) with mixed-rate ESTIMATE badge.

  • 10-col NetSuite import matches cotton's format & the sheet's import.

  • Batik module touches only batik_* tables.

Should Have

  • Frontend Batik POs pages functional (list, detail, edit, export).
  • Live NS ingestion via search 3314 (server-side).

Nice to Have

  • Kits (blocked on the KTBT- data-source decision — deferred).

Verification Steps

  1. ./venv/bin/python tests/test_batik_calc.py and tests/test_batik_import.py → all pass.
  2. ./venv/bin/python tests/parity/batik_parity.py → tool == sheet within tolerance on every fixture.
  3. Manual: create a batik collection in the UI, confirm totals tie to the sheet for the same collection.

Execution Log

2026-08-12

  • Recon complete: cotton module mapped (backend + frontend); batik maps 1:1.
  • Confirmed batik multiplier seed (migration 015), empty local NS creds → Phase 1 is fixture-driven.
  • Plan drafted. Chosen precut-recipe approach = Hybrid (Option C), matching the sheet fallback+flag just shipped.
  • Cole chose Option B (Full recipe editor) — plan updated: recipe editor + batik_stacking_orders in scope; fallback retained as graceful default.
  • Grounded the precut model in LIVE sheet formulas (rows 42-48, Template (NEW)): confirmed C=units, M=labor, N=blended price/piece, O=pieces-per-bundle from SKU suffix (not recipe), P=O·N+M, Q=P·C, S=divisor.

  • Phase 1 (calc engine) DONE: services/batik_calc.py — precut type/divisor, pieces-per-precut, recipe-driven blended price-per-piece (+ uniform-rate fallback + mixed-rate estimate flag), per-fabric yardage, calc_precut_line. Fixed a rounding-parity trap: builtin round() banker's-rounds 0.575→0.57; added Decimal _round2 (half-up) → 0.58 to match sheet.

  • tests/test_batik_calc.py — 46 checks, all green; encode validated ARBORGRACE numbers (FQ $0.58/$10.20/$1285.20, per-fabric 31.5yd) + rounding + mixed/piece-weighted/fallback cases.

  • NEXT: services/batik_import.py (10-col NetSuite import) + tests/parity/batik_parity.py + a real exported fixture.


References

  • Spec: ~/ai-projects/mission-control/plans/batik-po-web-app-logic.md
  • Sheet behavior/gotchas: reference_batik_po_template memory
  • Cotton precedent: routers/cotton_pos.py, services/cotton_*.py, tests/parity/cotton_parity.py
  • NS integration: ~/ai-projects-local/rbd-reorder-tool/notes/netsuite-integration-setup.md
  • Cotton web port precedent plan: cotton-po-import-web-app-port.md