Custom Box Builder¶
Our signature feature — customers pick a box size (matching the existing Woo box SKUs and prices exactly), then fill it with fruits within a credit budget. Margins are preserved regardless of which fruits they choose.
The model¶
Each box template has:
- A fixed anchor price (matches the Woo box SKU: £54.99 / £79.99 / £125)
- A credit budget (120 / 180 / 280)
- Sections with
min_items,max_items, andcredits_per_item - A list of eligible fruit slugs per section
Credits are anchored to internal wholesale cost, not retail. Example: if 1 credit = £0.10 wholesale, then:
- A Tropical-S box (£54.99, 120 credits) uses ~£12 of wholesale fruit, giving ~£43 for logistics + margin
- Whether the customer picks 1 premium fruit (20cr) or 10 everyday fruits (80cr), the wholesale cost is bounded by the budget → margin preserved
Template structure¶
Templates live in two synchronized places:
apps/web/lib/box-templates.ts— client-side (SSR page + client component)wp-plugin/fruitplug-api/includes/BoxTemplates.php— server-side source of truth for validation and pricing
If you edit one, edit the other in the same commit. Eventually the PHP file will be the single source and the TS side will fetch GET /wp-json/fruitplug/v1/box/templates.
{
slug: "tropical-s",
name: "Tropical Box — Small",
price_gbp: 54.99,
woo_product_id: 2900, // existing Woo box SKU
total_credits: 120,
sections: [
{ id: "premium", min_items: 0, max_items: 2, credits_per_item: 20, eligible_slugs: [...] },
{ id: "tropical", min_items: 0, max_items: 4, credits_per_item: 12, eligible_slugs: [...] },
{ id: "everyday", min_items: 0, max_items: 6, credits_per_item: 8, eligible_slugs: [...] },
],
}
Customer flow¶
/build-your-box— picks Tropical / Exotic / Plug-In/build-your-box/tropical-s— SSR-rendered builder with the 3 sections- Client component
<BoxBuilder>tracks selection, enforces section + budget rules client-side for responsiveness (can't exceedmax_items, can't exceedtotal_credits, unmet minimums shown in red) - Every selection change is debounced (250ms) and posted to
POST /api/box/price, which proxies to the plugin. The server re-validates from the authoritative PHP templates and returns the canonical price. That price is what gets shown on the CTA. - "Add to cart" posts the box SKU to the Woo cart. Composition is carried forward (phase 1.5: cart-item meta via Store API extension).
- "Save this box" POSTs to
/api/box/save(login-gated; returns 401 with a friendly toast until JWT auth ships). On success the response includes a shareable/b/{slug}.
Server-side enforcement¶
The fruitplug-api plugin centralises box logic in Fruitplug\BoxTemplates:
BoxTemplates::validate_composition($template_slug, $items)
// → ['ok' => true, 'price_gbp' => 54.99, 'credits_used' => 104,
// 'credits_budget' => 120, 'lines' => [...], 'woo_product_id' => 2900]
// or ['ok' => false, 'code' => 'over_budget', 'message' => '...']
Every endpoint routes through this validator, so the PWA, the cart, and /box/save all agree on what's legal and what it costs.
REST endpoints¶
All under namespace fruitplug/v1:
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/box/templates |
public | Full template list (shape matches the TS types) |
POST |
/box/price |
public | Validate + price a draft box — no persistence |
POST |
/box/save |
logged-in | Persist a named composition, returns slug |
GET |
/box/mine |
logged-in | Your saved boxes |
GET |
/box/{slug} |
public | Fetch a shared public box |
/box/price and /box/save expect { template_slug, items: [{slug, qty}] }. The server resolves each slug to its section, checks eligibility, runs the credit-budget math, and rejects over-budget or illegal compositions with HTTP 422.
woocommerce_before_calculate_totals (cart)¶
wp-plugin/fruitplug-api/includes/Cart/CustomBoxPricing.php runs on every cart recalc. If a cart item carries _fruitplug_box_template meta, it:
- Resolves the template (server-side lookup — client can't inject a fake template).
- Pins the line price to the template's anchor price. This means a tampered cart can never under-pay: even if a proxy rewrites the price to zero, this hook stamps it back to £54.99.
- Re-validates the composition and stashes the validation result on the item for dispatch to see.
Earlier design note (superseded). A previous version of this hook summed the component fruit prices and used that as the line price. That contradicted the anchor-price model (the same box is always £X regardless of composition) and is fixed as of 2026-04-24.
Admin UI¶
Fruit Costs (COGS)¶
wp-admin → WooCommerce → Fruit Costs — single page listing every published product with an editable cost field. Stored as post meta _fruitplug_cost_gbp. Replaces the removed Cost of Goods plugin from Phase 0 cleanup.
Margin per fruit = Woo sale price − _fruitplug_cost_gbp. Margin on a custom box = template anchor price − sum(cost × qty) across the composition. Used by future order-export reports.
Box Templates (planned)¶
Phase 1.5 deliverable: wp-admin → WooCommerce → Box Templates to let ops edit:
- Credit budget per template
- Section min/max/cost
- Eligible fruit lists
- Activation toggle
Until then, templates are edited in code and require a deploy.
Design rationale¶
See the strategic plan for the tradeoff analysis: retail-price-as-budget (loses margin), weight-budget (same problem), credits + sections (chosen). The user confirmed this model on 2026-04-24.