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:J4 — main 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/9fraction. 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_qtygain amultipliers: dictparam. 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}→ updatevalue(+ setupdated_byfrom 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_aton the row (who/when of the current value). - Later (optional):
po_multiplier_config_historyappend-on-change table for full lineage. Note only — not v1.
Acceptance
- Migration + seed applied →
po_multiplier_configshas the cotton rows = today's constants. cotton_calc.pyreads config; reference collection suggested qtys byte-identical to pre-change (parity/golden-master).- Settings page: change
precut1.8 → 2.0 → save → a fresh calc on a precut-bearing collection reflects 2.0;updated_by= the logged-in user. - Out-of-range value rejected by API validation.
Build order (once approved)
- 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.
~/ai-projects/mission-control/plans/po-multiplier-settings-spec.md