← Back to all projects
Ready Created 2026-07-13

Spec: Configurable PO Multipliers (Settings Page)

Parent plan: po-templates-digital-migration.md (Decision #7) Repo: ~/ai-projects-local/rbd-reorder-tool/ (all local; nothing touches production) Goal: Lift the hardcoded PO multipliers out of cotton_calc.py into an editable, audited settings surface — company-wide defaults Sabrina can change without a code deploy.


Problem

The multipliers/additives that drive suggested quantities are hardcoded in cotton_calc.py:

Rule Current constant Location
Designer mains 5/9 (1.8x) calc_preorder_pct:99-100
Precuts (FQ-/5-/10-/RP-) 5/9 (1.8x) :97-98
1yd 5/8 (1.6x) :95-96
CP15 +1 calc_suggested_qty:152-153
CP10 +4 :154-155
ASY- +2 :156-157
SD +2 :158-159
Catch-all curve _PRESALE_X/_PRESALE_Y table :27-28
Catch-all clamp floor 0.30 (3.33x) / cap 5/8 (1.6x) :104-106

These have fluctuated historically and belong to buying policy, not code. Batik already externalizes them to a sheet cell block (How to Notes!I1:J4: Yardage 1.6 / Precut 1.8 / CP +2). The web app should do the same, properly.

Scope

In (v1): Company-wide, per-PO-type editable multipliers for the fixed rules above + catch-all clamp bounds. Seeded to exactly today's values so output is unchanged (golden-master safe). Audit who/when.

Out (v1, documented for later):

  • Per-collection multiplier override (per-item qty override already covers collection-level adjustment today).
  • Editing the full piecewise catch-all curve — keep the point table in code for v1; expose only its clamp bounds as config. (Revisit if Sabrina needs to reshape the curve.)

Schema — new table po_multiplier_configs

Follows the existing *_configs convention (natural key + typed cols + timestamps). One shared table with a po_type discriminator — this is config data, not module logic, so a shared table doesn't break module encapsulation.

CREATE TABLE po_multiplier_configs (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  po_type     VARCHAR(32)  NOT NULL,          -- 'cotton' | 'batik'
  rule_key    VARCHAR(64)  NOT NULL,          -- 'designer_main','precut','1yd','cp15','cp10','asy','sd','catchall_min_mult','catchall_max_mult'
  mode        VARCHAR(16)  NOT NULL,          -- 'multiplier' | 'additive'
  value       DECIMAL(8,4) NOT NULL,          -- 1.8000 for multiplier; 2.0000 for additive (+2)
  label       VARCHAR(128),                   -- human label for the settings UI ('Designer mains')
  updated_by  VARCHAR(255),                   -- who last changed it (from auth session)
  notes       TEXT,
  created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at  DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uq_type_rule (po_type, rule_key)
);

Migration file: 015_po_multiplier_configs.sql (014 stays reserved for the planned cotton NS-import field migration — see cotton-po-import-web-app-port.md).

Seed (cotton, = today's constants exactly): designer_main mult 1.8 · precut mult 1.8 · 1yd mult 1.6 · cp15 additive 1 · cp10 additive 4 · asy additive 2 · sd additive 2 · catchall_min_mult 1.6 · catchall_max_mult 3.3333. Seed (batik): from How to Notes!I1:J4main 1.6 · precut 1.8 · cp additive 2 (confirm against live sheet when building the batik module).

Store multipliers as the multiplier the UI shows (1.8), not the internal 5/9 fraction. The engine converts: pct = 1/multiplier. This is also what the UI already displays per Cole's badge convention — one representation end to end.

Engine change — cotton_calc.py

  • Add a loader load_multipliers(db, po_type) -> dict[str, MultiplierRule] (one query per run, cached).
  • calc_preorder_pct / calc_suggested_qty gain a multipliers: dict param. Each hardcoded constant is replaced by a lookup with the current value as the fallback default if a row is missing.
  • Parity guarantee: seeded config == current constants → identical output. The golden-master check (reference collection vs live sheet) must pass unchanged after this refactor. That's the acceptance gate.

API — extend admin.py (existing settings surface)

  • GET /admin/po-multipliers?po_type=cotton → list rows for the settings page.
  • PUT /admin/po-multipliers/{id} → update value (+ set updated_by from session). Validate: multiplier ∈ [1.0, 5.0], additive ∈ [0, 50] (guardrails against a fat-finger that balloons a PO).

UI — new Settings sub-page

  • Under Settings: "PO Multipliers", tab per PO type (Cotton / Boutique).
  • Table: Label · Mode · Value (editable) · Last changed by · When.
  • Save → toast + optimistic update. Show the effective badge (e.g. "1.8x", "+2") next to each so it reads like the sheet.
  • Copy line: "Changes apply to future calculations. Existing collections recalculate on their next refresh." (No silent retro-change to approved POs.)

Audit

  • v1: updated_by + updated_at on the row (who/when of the current value).
  • Later (optional): po_multiplier_config_history append-on-change table for full lineage. Note only — not v1.

Acceptance

  1. Migration + seed applied → po_multiplier_configs has the cotton rows = today's constants.
  2. cotton_calc.py reads config; reference collection suggested qtys byte-identical to pre-change (parity/golden-master).
  3. Settings page: change precut 1.8 → 2.0 → save → a fresh calc on a precut-bearing collection reflects 2.0; updated_by = the logged-in user.
  4. Out-of-range value rejected by API validation.

Build order (once approved)

  1. Migration 014 + seed. 2. Engine loader + param wiring (parity test). 3. Admin endpoints + validation. 4. Settings UI. 5. Verify acceptance end-to-end in the local clone.