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_itemsNS 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_gaphandling)- Frontend:
CottonPOListPage.tsx+CottonPODetailPage.tsx, nav entry inLayout.tsx, inline API calls viasrc/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 (
.envns_consumer_key/ns_token_keyblank). 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_templatememory): - 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. Seereference_batik_po_templatememory. -
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_warehouseis a shared 74-table DB — batik owns onlybatik_*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:
- Exact pricing on every collection, including mixed-rate — no estimate compromise.
- Unlocks per-fabric precut yardage breakout (needed for the full yardage picture, not just mill-PO piece pricing).
- 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), mirroringcotton_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.pyas-is (or thinbatik_pricingwrapper 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_rulesfrom 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
./venv/bin/python tests/test_batik_calc.pyandtests/test_batik_import.py→ all pass../venv/bin/python tests/parity/batik_parity.py→ tool == sheet within tolerance on every fixture.- 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_ordersin 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: builtinround()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_templatememory - 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
~/ai-projects/mission-control/plans/batik-po-web-app-port.md