▼ DOC 06

Batch Planner

The pitcher math + COGS calculator — where a recipe becomes a pour

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. 1

    Open the planner

    Sidebar → Batch Planner (URL: /batch-planner). Open on every tier; commit count is capped on Free.

  2. 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. 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; click reset next to it to revert.

    TIP
    Use 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. 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 warning
    If 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. 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. 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 pillsNone, 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. 1

    Click + Add container

    Below the registry list. Inline form: name, capacity (oz), cost (optional), and a Save to my registry checkbox.

  2. 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. 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.

HEADS UP — Stock check doesn't block the pour
The Make Batch button stays clickable when something is short. The footer just shows “Make Anyway” instead of “Make Batch.” This is intentional — your Frag Lab inventory is the source of truth, not the in-app counter, which can drift if you've poured outside the app or hand-counted differently.

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.

NOTE
The math lives in 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. 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. 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. 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_oz decremented by what the recipe needed.
    • Container owned_qty decremented by total quantity (skipped on refill rows).
    • Wick owned_qty decremented by quantity × wicks_per_container.
    • Lid owned_qty decremented by quantity × lids_per_container.
  4. 4

    Audit row + outbound sync

    A row lands in batch_log with 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. 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. 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.

HEADS UP
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. 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. 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. 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. 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. 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. 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.

TIP
Changed your mind during a resume? Click STASH again — the planner re-stashes the updated counts and replaces the source, so you don't pile up duplicate pending plans. Or click the × on a Pending Pours row to discard a stash entirely without committing.
NOTE
Pending plans don't fire any external side effects — no Square / Shopify inventory movement happens until the commit. If you stash four batches and never commit them, your store inventory is exactly where you left it.

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.