Pick recipe → pick ratio + wax + pitcher → pick containers + quantities. The page recalculates as you type — no Calculate button. When the manifest looks right, the sticky footer turns green and the Make Batch button decrements your inventory in one go and pushes finished counts to Square / Shopify (when connected).
The page at a glance
Three columns of inputs across the middle, two sticky bars top + bottom:
- Sticky page header — title + ticker + the collapsible Profit & Margin drawer. The drawer's margin % stays visible at the top as you scroll.
- Pending Pours panel (when present) — every batch you've stashed without committing. RESUME hydrates one back into the planner; × discards.
- Left column — Recipe / Ratio / Wax Used / Pitcher Size cards. The Recipe card also shows live on-hand inventory per size for the selected recipe so you don't have to bounce back to /recipes.
- Center column — Container Selector. Pick which jars/tins this batch is going into and how many.
- Right column — Batch Summary, Total Fragrance Needed, Per-Pitcher Breakdown, Stock Check. The live math.
- Sticky footer — BATCH STATUS line + STASH and Make Batch (or Make Anyway) buttons. Green stripe = ready, red stripe = stock short.
Set up the batch (left column)
- 1
Open the planner
Sidebar →
Batch Planner(URL:/batch-planner). Open on every tier; commit count is capped on Free. - 2
Pick a recipe
Dropdown of every saved recipe in your account. Picking one fills the wax label, the load %, and the oil list below. The Recipe card also surfaces on-hand inventory per size for the picked recipe — same per-size breakdown the BlendCard shows on /recipes, sourced from your Square store inventory. Switching mid-plan resets the per-batch load override and the “batch made” flag.
- 3
Adjust the load % (optional, this-batch-only)
The Recipe card shows an editable
Load: __%input. Default value resolves ratio → recipe → settings, in that order. Type a new number and the math recalculates immediately — but the override is local and won't write back to the recipe row. A small(this batch only)tag marks the override; clickresetnext to it to revert.TIPUse the per-batch override when you want to test a stronger pour for a one-off (say, 12% on a recipe normally poured at 8%) without forking a new recipe. - 4
Pick a ratio (if you have any)
The Ratio card only appears once you've saved at least one in Settings → My Formulas. A ratio is a (wax + load %) pair you reuse. Picking one sets both. The dropdown shows
★ Name · Wax · Load%; ★ marks the default.HEADS UP — Soft mismatch warningIf the picked ratio calls for a different wax than what's in Wax Used, the card surfaces a small warning. It's informational — substituting waxes is sometimes intentional and isn't blocked. - 5
Pick the wax you're pouring (Wax Used)
Dropdown of your saved
user_waxes. Each option shows★ Wax · Vendor · Size · $price; ★ marks your settings default. The wax you pick drives $/lb for COGS — make sure the “actual price” on the wax is the price you actually pay (Settings → My Waxes).Empty list? You'll see a link straight to Settings; COGS stays blank until then.
- 6
Set the pitcher size
How much wax fits in your melting pitcher (oz). Drives the pitchers-needed math: if the batch needs more wax than fits in one pitcher, BLNDR splits across N pitchers and flags any rounding error in the per-pitcher breakdown.
Pick containers + quantities (center column)
Each saved container shows up as a row. Each row has:
- Name + capacity — “Status Jar 8oz”.
- Quantity pills —
None, plus your saved batch-size presets (e.g.,4,12,24). - Custom qty input — for any number not on a preset.
Total wax needed = sum of (container.fill_weight × quantity) across every selected row.
Add a one-off container for this batch
- 1
Click + Add container
Below the registry list. Inline form: name, capacity (oz), cost (optional), and a
Save to my registrycheckbox. - 2
Default off — ephemeral
With the checkbox unchecked, the container gets a synthetic
local-…id and lives in this-batch-only state. It persists across page reloads (localStorage) but is never written to the DB. - 3
Check the box to save permanently
POSTs to
/api/containers; the row joins your permanent registry alongside the ones you set up in Settings.
Refill SKUs (V1.2)
Refill containers (jars a customer brought back) swap the qty pills for two different controls:
- Oil amount — total fragrance oz this refill needs (since the customer's jar is one specific size). The unit auto-switches between oz and gm based on your default measurement setting.
- Dedicated pitcher — checkbox. When ON, this refill gets its own pitcher in the math, separate from the rest of the batch. Useful when a customer wants a strong / weak pour off the same recipe.
Refill rows are always quantity 1 (per-customer-jar model) and skip Square / Shopify outbound — the customer brought their own vessel, so there's no SKU to decrement.
Live Math panel (right column)
Batch Summary card
Four big numbers in a 2×2 grid: Total Wax (oz), Total Fragrance (oz), Pitchers, Candles. Numbers animate as you change inputs.
Total Fragrance Needed
Per-oil breakdown: every oil in the recipe with the exact amount this batch needs (oz / g, both shown). Sums to the Total Fragrance number above.
Per-Pitcher Breakdown
If your batch needs multiple pitchers, you get a card per pitcher with the exact oz of each oil for that pitcher specifically. Each card has print + collapse buttons. The math splits proportionally — pitcher 1 of 3 gets 1/3 of every ingredient — except for refills with dedicated pitcher ON, which get their own row.
Stock Check card
One row per oil that's in the batch. Each shows a check or warn icon plus “you have X oz, need Y oz.” Wax + containers + wicks + lids are checked too if any are short.
Profit & Margin drawer (sticky header)
Lives in the sticky page header so the margin % stays visible as you scroll. Default-collapsed; click anywhere on the strip to expand.
Three columns when expanded:
- Cost Breakdown — itemized: Wax, Fragrance, Containers, Wicks, Lids, Wick Stickers, with a Total COGS row at the bottom.
- Per Container — one card per container with sell / cost / profit per unit and a margin% bar (green/amber/red zone).
- Total Batch Hero — big margin % number + zone label (Industry: 50–65%) + revenue / cost / profit summary.
If something's missing (no wax cost, no oil prices, no sell prices), the drawer flips into diagnostic mode with a punch list of what to fix and links straight to Settings.
Print button in the header prints the drawer + breakdown — useful for taking a paper manifest to the bench.
How COGS is calculated
totalCogs = waxCost (oz needed × cost per oz from user_waxes) + fragranceCost (sum over oils: oz × cost per oz from fragrance_oils) + containerCost (sum over containers: qty × cost — skipped when is_refill) + wickCost (sum over containers: qty × wicks_per_container × wick.cost) + lidCost (sum over containers: qty × lids_per_container × lid.cost) + wickStickerCost (separate line item, when configured)
Margin % = profit ÷ revenue × 100, where revenue = sum of (container.sell_price × qty) across the batch and profit = revenue − totalCogs.
lib/batch-calculator.ts. Pitcher math is pure functions in lib/batch/. Both are tested in __tests__/ — pure-function tests run in CI on every PR.Worked example
Concrete numbers so you can spot-check the math:
- Recipe: Library at Dusk, 8% load.
- Wax: Coconut Apricot CAW464, $4.20/lb.
- Containers: 12× Status Jar 8oz, $2.50 each, sells for $24.
- Wick: ECO-12 at $0.20/unit, 1 per jar. Lid: $0.40, 1 per jar.
Batch math:
Total wax = 12 × 8 oz × (1 - 0.08) ≈ 88.32 oz (~5.52 lb) Total fragrance = 12 × 8 oz × 0.08 ≈ 7.68 oz Pitchers needed (64 oz pitcher) = ceil(96 / 64) = 2 waxCost = 5.52 lb × $4.20 ≈ $23.18 fragranceCost = (per the recipe + library prices for 7.68 oz) containerCost = 12 × $2.50 = $30.00 wickCost = 12 × 1 × $0.20 = $2.40 lidCost = 12 × 1 × $0.40 = $4.80 totalCOGS = waxCost + fragranceCost + 30 + 2.40 + 4.80 revenue = 12 × $24.00 = $288.00 profit = revenue - totalCOGS margin % = profit / revenue × 100
Make Batch (commit the pour)
- 1
Read the BATCH STATUS line
Sticky footer. Three states:
- Stock check cleared. — green stripe + green Make Batch button. Everything you need is in stock.
- Stock may be out of date — make anyway if your Frag Lab says otherwise. — red stripe + Make Anyway button. Something's short by BLNDR's count, but the pour isn't blocked.
- Batch made · inventory updated. — green; the post-commit success state.
- 2
Click Make Batch
Opens the BatchConfirmModal — the full manifest of what's about to be decremented (wax, oils, containers, wicks, lids) and what's about to be pushed to Square / Shopify (if connected). Last chance to back out.
- 3
Confirm
The batch-decrement engine runs in a single transaction:
- Wax stock decremented in
user_waxes.owned_oz. - Each oil's
user_libraries.amount_ozdecremented by what the recipe needed. - Container
owned_qtydecremented by total quantity (skipped on refill rows). - Wick
owned_qtydecremented byquantity × wicks_per_container. - Lid
owned_qtydecremented byquantity × lids_per_container.
- Wax stock decremented in
- 4
Audit row + outbound sync
A row lands in
batch_logwith the full manifest. If Square / Shopify are connected and the container isn't a refill, finished counts push to those catalogs as inventory adjustments. - 5
Restock alerts (if any)
Any material now at or below its restock threshold fires a toast (and an email if email alerts are enabled in Settings). Alerts fire on transitions — if you were already below threshold, no new alert fires.
- 6
Make another
The footer flips to “Batch made · inventory updated” with a
Make another →button. Click to reset just the batch-made flag (recipe + containers stay) so you can pour the same thing again, or pick a new recipe to start fresh.
batch_logis append-only by RLS — users can SELECT and INSERT, but never UPDATE or DELETE. The audit trail of “what you actually poured” should never be rewritable.STASH — plan now, commit after the pour
The two-phase workflow for makers who pour several recipes at once and want to nail down actual unit counts only after the wax is in jars. STASH saves the plan without firing any side effects — no inventory decrement, no Square push, no batch_log entry, no cap burn. Resume from the Pending Pours panel when you've measured the actual numbers, then commit normally.
- 1
Plan as usual
Pick recipe, set container counts with your best estimates. Same flow as a normal batch — the Live Math panel updates, the Stock Check fires, all the same numbers appear.
- 2
Click STASH instead of Make Batch
The STASH button sits to the left of Make Batch in the sticky footer (ghost border,
◆ STASH). Click → toast confirms “Stashed. Find it in Pending Pours.” → the planner clears so you can plan the next recipe. - 3
Repeat for every recipe in your pour session
The Pending Pours panel at the top of /batch-planner fills up — each row shows recipe + total units + size breakdown + the time you stashed. Stash up to as many as you need; pending plans don't count against your tier's batch cap.
- 4
Go physically pour everything
While you're at the bench, the stashed plans wait. Close the tab if you want — they live in the DB, not in localStorage.
- 5
RESUME with actual counts
Back at the screen, click
RESUME →on a pending plan. The recipe + container counts + load override + pitcher size rehydrate into the planner. Edit the container quantities to match what you actually poured. - 6
Click Make Batch (commit)
Normal commit flow: BatchConfirmModal opens, you confirm, inventory decrements, Square / Shopify push, batch_log writes. The original pending plan is cleared as part of the commit (it carried a
replaces_pending_id) so it disappears from the Pending Pours panel.
× on a Pending Pours row to discard a stash entirely without committing.Troubleshooting
- COGS shows blank everywhere — Wax Used isn't set, OR the wax is set but has no
actual_price. Open Settings → My Waxes and fill in the price. - Profit drawer shows “Can't show margin yet” — at least one of: no wax cost, no oil price for one of the oils at your default buy size, no sell price on a container. Diagnostic body lists exactly what's missing.
- The Make Batch button is disabled — you're missing a recipe, or you're mid-submit. (It's NOT disabled by stock check — that's the “Make Anyway” case above.)
- Pitchers needed is 0 — total wax is 0, usually because all container quantities are 0. Pick at least one quantity.
- Square / Shopify push didn't happen — either the integration isn't connected, the container has no Square / Shopify mapping configured, or the row was a refill. Check Settings → integrations.