Adding Bundle Feature to Map

PRD: WPC Product Bundles (WOOSB) — Edge Server & Thin Client Parity

Status: Draft — plugin source reviewed (WPC Product Bundles Premium 8.5.6 in .cursor/woo-product-bundle-premium-856/)
Grilled decisions: Q1 1 — till type bundle when woosb_ids meta present; native Woo bundle without WOOSB meta stays one-tap. Q2 1conditional picker: one-tap default composition when no variables and no optional rows; picker when has_variables or has_optional. Q3 1parent owns customer total on basket/Till Order line; bundle_children[] hold per-kit breakdown for kitchen/receipt (fixed mode child prices may be reference-only). Q4 1 — merge basket lines when parent catalog product id + bundle configuration fingerprint match (same pattern as composite configuration). Q5 1catalog.composite_configure gates Configure bundle catalog editing (same as WOOCO). Q6 1sell-first release: ship B1–B6 (ingest through kitchen/EOD) before B7–B8 (catalog editor + push); website-defined bundles sellable on till first. Q7 1layout text rows (h1/p/etc.) shown in picker as plain-text headings; excluded from bundle_children and merge fingerprint. Q8 1Bundle fulfillment picker uses same modal contract as Composite fulfillment picker (full-screen on #/, blocks checkout until confirm/cancel). Q9 1woosb_before_text / woosb_after_text shown in picker only as plain text (strip HTML); not kitchen/receipt/basket row in v1 (same rule as Composite operator copy). Q10 1same as composite downstream display: Kitchen board indents bundle_children under parent; Receipt lists children under parent when ESC/POS supports nested lines; EOD orders_json includes parent row plus nested child lines (name, qty, line total). QC-A 1 — Recipe bundle: each staging meal prep dish is a WOOSB bundle of existing catalog component products; no component creation inside the designer. QC-B 1 — Component products are existing catalog products (standalone simple or variable), pre-created via the normal New product wizard before bundle configuration. QC-C 2 — Configure bundle lives in Catalog Manage (WOOSB B7); the designer shows only a component status badge and a → Configure shortcut per dish card — no component editor inside the designer itself. Repository: WPPosTill
Canonical glossary: [CONTEXT.md](../../CONTEXT.md)
Plugin reference: WPC Product Bundles (WOOSB) · source: wpc-product-bundles.php
Contrast (implemented): WPCleaverPro.md (WOOCO / composite) · [edge-server-wooco-composites-table.md](./edge-server-wooco-composites-table.md) · [thin-client-composite-fulfillment-picker.md](./thin-client-composite-fulfillment-picker.md)
Scope: Edge Server SQLite + Woo bootstrap/push + Thin Client POS/catalog — WOOSB only (not native Woo bundle, not grouped, not WOOCO)

Problem Statement

The store runs WPC Product Bundles for WooCommerce (Premium) — plugin prefix woosb, Woo product type woosb, admin label Smart Bundle. This is a different product from WPC Composite Products (WOOCO) (composite + wooco_components), which WPPosTill already supports end-to-end (fulfillment picker, composite_children on Till Order lines, conversion screen, wooco_composites / composite_items materialization).

WOOSB bundles are fixed product lists (simple, variation, or variable parents) with per-item default quantities, optional/custom-qty rows, bundle-level discounts, and fixed vs summed pricing. On the website, add-to-cart expands a parent line into linked child lines with WOOSB cart metadata; checkout persists _woosb_ids / _woosb_parent_id on order items.

Today WPPosTill:

  • Downloads bundle parents at catalog bootstrap but stores no woosb_* meta columns on products (export schema has WOOCO meta only).
  • Treats type === 'bundle' (and likely woosb rows mapped to bundle) as one-tap add — parent flat line only, no component breakdown ([CONTEXT.md](../../CONTEXT.md) Non-composite kit types).
  • Has no bundle definition API, picker, pricing engine, order-line shape, catalog editor, or push mapper for WOOSB meta.

Kitchen cannot sell “Meal Deal” bundles with optional sides, variation picks, or correct discounted totals; EOD and kitchen cards cannot show what to make inside a bundle.

Goals

  1. Ingest & persist WOOSB parent meta from WooCommerce REST / export into Edge Server products (same authority model as WOOCO).
  2. Resolve bundle definitions for the till: explicit bundled product ids, variation constraints, optional/custom qty rules, stock flags.
  3. Sell at checkout via a Bundle fulfillment picker when the bundle requires configuration (has_variables, has_optional); otherwise fast-path add with default composition.
  4. Price bundle lines using WOOSB semantics: fixed parent price vs summed children with discount % or fixed discount amount.
  5. Persist Till Order line_items with structured bundle children (parallel to composite_children, distinct type discriminator).
  6. Edit bundle composition in catalog management (new screen or extension — not the WOOCO conversion screen).
  7. Push WOOSB meta back to Woo on staff edit / reconcile (parity with catalog push).

Non-goals (clarify plugin identity)

ProductPluginWoo typePrimary metaWPPosTill today
WPC CompositeWOOCOcompositewooco_componentsFull parity
WPC Product BundlesWOOSB (this PRD)woosbwoosb_ids + pricing metaOne-tap flat add only
Native WooCommerce Bundlecore / otherbundle_bundle_data etc.One-tap flat add; out of scope unless store uses it
Groupedcoregroupedchildren idsOne-tap; out of scope

Verify at bootstrap: Woo REST may return type: "woosb" or legacy bundle for the same plugin depending on API version. v1 maps woosb REST type → till bundle only when meta.woosb_ids is present; native bundles without WOOSB meta stay on the legacy one-tap path until a separate PRD.

Plugin data model summary

Storage is WordPress post meta only — no custom tables. Settings: woosb_settings, woosb_localization (site options; not per-product).

Product type

  • Class: WC_Product_Woosb (includes/class-product.php)
  • get_type()woosb
  • Registered via product_type_selector filter (“Smart Bundle” in admin)

Composition — woosb_ids

Primary meta key. v7+ stores a PHP/JSON associative array keyed by stable string keys (4-char alphanumeric, e.g. a3f9). Legacy string format still parsed:

{id}/{key}/{qty},{id2}/{qty2},...

Product item (typical):

{
  "k7m2": {
    "id": 1665,
    "sku": "BK331",
    "qty": 1,
    "optional": "1",
    "min": 0,
    "max": 3,
    "attrs": { "attribute_pa_size": "large" },
    "terms": { "attribute_pa_color": ["red", "blue"] }
  }
}
FieldMeaning
idBundled catalog product id (variation id when a specific variation is bundled)
skuDenormalized SKU; used when woosb_use_sku filter is true
qtyDefault quantity per one bundle parent qty
optionalWhen truthy, row is custom quantity — customer/staff may change qty within min/max; omitted/false = fixed qty (must match default at cart)
min / maxPer-item bounds when optional is set; if absent, inherited from parent meta woosb_limit_each_min, woosb_limit_each_max, or woosb_limit_each_min_default (= default qty as min)
attrsSelected variation attributes (serialized on add-to-cart)
termsPremium: restrict which variation term values are offered for a variable parent row

Text/heading items (layout only — not purchasable):

{
  "h1ab": { "type": "h1", "text": "Choose your drink" }
}

type: h1h6, p, span, none. No id. Skipped in cart validation except display.

Not used: category-sourced components (WOOCO product_cat + other[]). WOOSB is explicit product list only.

Parent-level meta keys

Meta keyValues / role
woosb_idsComposition (array or legacy string)
woosb_disable_auto_priceon = fixed bundle price (parent regular/sale price); off = dynamic (sum children − discount)
woosb_discountPercentage 0–100 off summed item prices (ignored when fixed price or woosb_discount_amount set)
woosb_discount_amountFixed currency amount subtracted from summed total
woosb_manage_stockon = bundle participates in stock calc from children
woosb_exclude_unpurchasableyes / no / unset / default — skip OOS/unpurchasable items in totals & stock
woosb_limit_whole_min / woosb_limit_whole_maxWhen bundle has optional items: min/max count of optional rows selected
woosb_total_limitson enables price total guardrails
woosb_total_limits_min / woosb_total_limits_maxMin/max currency total of selected items (dynamic pricing)
woosb_limit_each_min / woosb_limit_each_maxDefault per-item min/max when item row omits them
woosb_limit_each_min_defaulton → default min = item qty
woosb_shipping_feee.g. each — affects needs_shipping
woosb_layoutlist / grid (frontend)
woosb_custom_priceDisplay price HTML override
woosb_before_text / woosb_after_textOperator copy on product page

Pricing modes (sell-time)

  1. Dynamic (auto price)woosb_disable_auto_price !== 'on'
    • Child line unit prices = product price (sale or regular per bundled_price_from setting) × bundle discount %.
    • Parent cart line price = 0 or −discount_amount (adjustment line); display total in woosb_price meta on parent.
    • Parent tax status set none for summed bundles.
  2. Fixed bundle pricewoosb_disable_auto_price === 'on'
    • Parent carries Woo regular/sale price.
    • Bundled children often priced at 0 in cart (woosb_fixed_price flag) but may show reference prices in UI.

Discount precedence: woosb_discount_amount wins over woosb_discount percentage.

Quantity rules

RuleSourceBehavior
Fixed item qtyitem without optionalSubmitted qty must equal qty
Custom item qtyoptional + min/maxStaff/customer pick qty in range
Optional selection countwoosb_limit_whole_min/maxCount of optional rows with qty > 0
Total price boundswoosb_total_limits + min/maxSum of selected item display prices
Parent quantitycartChild cart qty = woosb_qty × parent_qty
Variable productshas_variables()Requires variation selection before add
Nested bundlesItems of type woosb / bundle / composite / grouped blocked from being bundled children

Stock / inventory

  • Bundle in stock when every required (non-optional) item has enough stock for qty × order_qty (optional items use min or 0 for worst-case check).
  • get_stock_quantity: when managing stock and no optional items, parent stock ≈ min(floor(component_stock / component_qty)) across required managed items; optional bundles often skip aggregation.
  • Component stock reduced on order (standard Woo child lines).
  • update_stock_status hook propagates child stock changes to containing bundles.

Cart / order line structure

Parent cart item (class-woosb.php):

  • woosb_ids — serialized selection string or array snapshot
  • woosb_keys[] — cart keys of child lines
  • woosb_fixed_price, woosb_discount, woosb_discount_amount
  • woosb_price — computed display total (dynamic mode)

Child cart item:

  • woosb_parent_id, woosb_parent_key, woosb_qty (per-kit qty)
  • Same discount/fixed flags copied from parent

Order item meta (hidden keys prefixed _):

  • _woosb_ids, _woosb_parent_id, _woosb_price

Admin manual orders duplicate parent + children via add_order_item_meta in class-backend.php.

WooCommerce hooks / APIs used (reference)

AreaHooks / APIs
Product typeproduct_type_selector, woocommerce_product_data_tabs, woocommerce_process_product_meta_woosb
Cartwoocommerce_add_to_cart_validation, woocommerce_add_cart_item_data, woocommerce_add_to_cart, woocommerce_before_calculate_totals, woocommerce_get_cart_item_from_session
Orderwoocommerce_checkout_create_order_line_item, woocommerce_order_item_visible
Stockwoocommerce_product_set_stock_status, woocommerce_variation_set_stock_status
REST / exportwoocommerce_product_export_meta_value, woocommerce_product_import_pre_insert_product_object for woosb_ids
Admin AJAXwp_ajax_woosb_get_search_results, woosb_add_text, woosb_import_export

Till integration should use WooCommerce REST GET/PUT /products/:id meta_data keys above — not admin-ajax.

Gap analysis vs current WPPosTill WOOCO work

CapabilityWOOCO (composite)WOOSB (gap)
Meta columns on productsmeta_wooco_* + CSV headersNone for woosb_*
Shared parser moduleshared/wooco/Missing shared/woosb/
Definition read APIGET /api/products/:id/compositeMissing …/bundle
Materialized reporting tableswooco_composites, composite_itemsNone (optional woosb_bundle_items follow-on)
Checkout UIComposite fulfillment pickerOne-tap parent only
Basket / order shapecomposite_childrenNo bundle_children
Pricingonly / include / exclude + component discountsNo WOOSB fixed/sum/discount
Catalog editorComposite conversion screenNo bundle editor
Push mapperproductMetaPushMapper WOOCO keysNo WOOSB keys
Type inferenceinferTillProductTypecomposite from metaNo woosb inference
Kitchen / receiptParent + indented children (Till)Bundle shows parent name only
Component sourceCategories + explicit idsExplicit products only
Picker entryAlways for compositeConditional — only when has_variables or has_optional

Reuse (do not conflate): Till session auth, catalog bootstrap pipeline, GET /api/products, basket merge patterns, EOD nested line display, catalog push queue, permission gates — extend with bundle-specific types.

Solution overview

flowchart TB
  woo[WooCommerce REST meta woosb_*]
  products[(products master)]
  shared[shared/woosb parser + pricing]
  api[GET /api/products/:id/bundle]
  picker[Bundle fulfillment picker]
  basket[bundle basket line + bundle_children]
  order[Till Order line_items]
  catalog[Bundle configuration screen]
  push[catalog push woosb meta]
  woo --> products
  products --> shared
  shared --> api
  api --> picker
  picker --> basket
  basket --> order
  catalog --> products
  products --> push
  push --> woo

User stories

Edge Server — ingest & schema

  1. As a developer, I want woosb_ids and parent pricing/stock meta stored on products at bootstrap, so bundle definitions survive offline.
  2. As a developer, I want Woo export CSV headers extended with Meta: woosb_* columns, so CSV re-bootstrap matches REST ingest.
  3. As a developer, I want till type bundle when REST type is woosb or legacy bundle and woosb_ids meta exists, so POS grid logic has a single discriminator.
  4. As a maintainer, I want invalid or empty woosb_ids flagged invalid_bundle_definition on read API, so broken kits are visible in catalog.

Edge Server — definition API

  1. As kitchen staff, I want GET /api/products/:id/bundle returning normalized items (product id, name, sku, default qty, optional, min/max, variable flag, resolved variation options), so the picker does not parse raw meta.
  2. As kitchen staff, I want the API to include pricingMode (fixed dynamic), discountPercent, discountAmount, and fixedPrice summary, so the picker shows correct totals.
  3. As kitchen staff, I want beforeText / afterText and non-product text rows returned as layoutRows, so operator copy matches the website.
  4. As kitchen staff, I want missing bundled product ids flagged unavailable, so incomplete sync is obvious.
  5. As a developer, I want bundle resolution implemented in shared/woosb/ and called from the Edge route, so rules are testable and not duplicated in Vue.

Thin Client — POS discovery & picker

  1. As kitchen staff, I want a Bundle badge on grid cards for type === 'bundle' with valid WOOSB meta, so I distinguish kits from simple items.
  2. As kitchen staff, when a bundle has no variables and no optional items, I want one-tap add using default woosb_ids composition, so fixed meal deals stay fast.
  3. As kitchen staff, when a bundle has_variables or has_optional, I want tapping the grid to open a Bundle fulfillment picker, so I choose variations and optional qtys.
  4. As kitchen staff, I want required bundled rows always included with fixed qty, so I cannot submit an invalid kit.
  5. As kitchen staff, I want optional rows with qty 0 omitted from the sale, so add-ons are truly optional.
  6. As kitchen staff, I want per-item qty clamped to min/max, and optional-item count clamped to limit_whole_min/max, so website rules match the till.
  7. As kitchen staff, I want a running estimated total using WOOSB dynamic/fixed pricing, so customers see the correct bundle price.
  8. As kitchen staff, I want Add to basket disabled until variable parents have a chosen variation and required stock is available.
  9. As kitchen staff, I want Edit bundle on a basket line to reopen the picker with prior selections.
  10. As kitchen staff, I want parent line quantity increments to scale child per-kit qtys (woosb_qty × parent_qty), matching Woo cart sync.

Checkout, kitchen, EOD

  1. As kitchen staff, I want Till Order line_items to store line_kind: 'bundle' with bundle_children[] (product id, name, sku, qty, unit price), so the kitchen knows what to make.
  2. As kitchen staff, I want the Kitchen board to indent bundle children under the parent (same UX as composite children).
  3. As a manager, I want EOD snapshots to include bundle parent + nested child lines, so reporting matches composite behavior.
  4. As kitchen staff, I want Receipt ESC/POS to list bundled items under the parent when configured, so counter tickets are readable.

Catalog management

  1. As kitchen staff, I want Configure bundle entry (separate from Configure composite), so I do not mix WOOCO and WOOSB editors.
  2. As kitchen staff, I want to search/add bundled products by name/SKU, reorder rows, set default qty, toggle custom quantity, and set min/max, mirroring wp-admin Smart Bundle UI.
  3. As kitchen staff, I want to set fixed vs dynamic pricing and discount fields, so LAN edits match WPClever.
  4. As kitchen staff, I want saves to PATCH the parent row and enqueue catalog push with woosb_* meta when not Local_Only.
  5. As kitchen staff without catalog.edit, I want bundle configuration blocked server-side (403), consistent with composite rules.

Push & authority

  1. As a store operator, I want staff bundle edits pushed to WooCommerce meta_data, so the website stays aligned with the Edge master.
  2. As a developer, I want mapProductRowToWooPayload to include woosb_ids JSON and parent meta keys, without sending invalid Woo product type if REST requires woosb.

Security & capabilities

  1. As a store operator, I want GET /api/products/:id/bundle behind Till session bearer (same as composite definition reads).
  2. As a store operator, I want bundle configuration behind catalog.edit and checkout picker behind normal till permissions (grid/cart automatic).

Architecture

Edge Server

ModuleResponsibility
productSchemaAdd meta_woosb_* columns + CSV header mapping
WooCommerceSync / bootstrapIngest woosb_* from REST meta_data
shared/woosbparseWoosbBundle, resolveBundleItems, validateBundleSelection, priceBundleSelection, serializeWoosbIds
bundleDefinitionApiGET /api/products/:id/bundle
productMetaPushMapperOutbound woosb_* keys
orderRepository / receiptOrderMapperPersist and render bundle_children
woosbBundleItems (optional v1.1)Materialized child rows for reporting — mirror composite_items pattern

Thin Client

ModuleResponsibility
bundleDefinitionClientFetch GET …/bundle; degraded parse fallback from csv meta (last resort)
BundleFulfillmentPicker.vueModal picker (reuse composite picker shell patterns; separate component)
useBasket / PosViewline_kind: 'bundle', merge fingerprint, edit flow
BundleConfigurationView.vueCatalog editor screen
ProductTypeControlDistinguish Single / Composite / Bundle where product type is shown

Till order line shape (proposed)

{
  "product_id": "1200",
  "name": "Lunch Deal",
  "sku": "LD01",
  "quantity": 2,
  "line_total": 19.98,
  "line_kind": "bundle",
  "bundle_selection": {
    "woosb_ids": "1665/k7m2/1,1700/x9p1/2",
    "pricing_mode": "dynamic",
    "discount_percent": 10
  },
  "bundle_children": [
    { "product_id": "1665", "name": "Wrap", "sku": "WR1", "quantity": 2, "unit_price": 4.50 },
    { "product_id": "1700", "name": "Chips", "sku": "CH1", "quantity": 4, "unit_price": 2.00 }
  ]
}

bundle_selection stores the WOOSB ids snapshot for edit/merge fingerprinting (parallel to composite configuration).

Pricing & quantity — implementation rules (must mirror plugin)

Implement priceBundleSelection against class-woosb.php before_calculate_totals and add_to_cart_validation:

  1. Dynamic: Sum child get_price × per-kit qty × parent qty; apply % discount per child; subtract fixed discount once on parent allocation; round per woosb_round_price (store currency decimals).
  2. Fixed: Line total = parent regular_price / sale_price × parent qty; children contribute 0 to line_total but remain on kitchen breakdown.
  3. Validation: Reject when required item missing, fixed qty mismatch, optional min/max violated, whole optional count out of range, total_limits exceeded, or nested bundle types in selection.
  4. exclude_unpurchasable: When yes, omit OOS lines from validation totals; when no, block add with explicit error.

Migration / bootstrap

  1. Schema migration adds nullable meta_woosb_* TEXT columns; no SQLite rewrite of existing rows required.
  2. Re-bootstrap or incremental product sync pulls meta_data keys from Woo REST; map into columns and csv['Meta: woosb_*'] mirrors.
  3. Type normalization: If type from Woo is woosb, persist till bundle + retain remote_type: woosb in snapshot if needed for push.
  4. No automatic picker enablement until shared/woosb tests pass against fixtures exported from the plugin’s woosb_import_export JSON shape.
  5. Existing flat basket lines for bundle parents (legacy one-tap) are not retrofitted; new sales use bundle line shape after ship.

Out of scope (v1)

  • Native WooCommerce core bundle type (_bundle_data) without WOOSB meta
  • Grouped products (grouped + children)
  • WOOCO changes (already shipped)
  • Category-sourced bundle components (WOOSB does not have them)
  • Global woosb_settings / woosb_localization admin on till
  • Shipping package splitting (woocommerce_cart_shipping_packages parity)
  • Coupon restriction (woocommerce_coupon_is_valid_for_product) replication
  • WPC Price by Quantity tier integration inside bundles
  • Web Order ingest mapping of Woo _woosb_* order meta (till-first; website orders stay parent-flat in v1)
  • Nested bundle inside bundle
  • woosb_bundle_items materialized table (optional reporting slice v1.1)
  • Android-specific picker window (modal on #/ only, same as composite v1)

Implementation slices (tracer bullets)

Release plan (grill Q6 1): Phase A = B1–B6 (sell at till). Phase B = B7–B8 (Configure bundle + push). Phase B ships immediately after Phase A before marking operator-facing v1 complete.

SliceDeliversExtend
B1 — Meta ingestmeta_woosb_* columns; bootstrap + CSV; tests with plugin export JSONproductSchema.ts, WooCommerceSync.ts
B2 — shared/woosb coreParse woosb_ids array + legacy string; BundleDefinition types; fixtures from .cursor/woo-product-bundle-premium-856/New shared/woosb/ (mirror shared/wooco/)
B3 — Definition APIGET /api/products/:id/bundle + Till session authproductApi.ts, pattern from compositeKitApi.ts
B4 — Fast-path addFixed bundles (no optional/variable) one-tap with default composition + pricingPosView.vue, basket composable
B5 — Bundle pickerModal UI + validation + priceBundleSelectionComposite fulfillment picker PRD patterns; new Vue module
B6 — Order linesPersist line_kind: 'bundle' + bundle_children[] on Till Order checkout; Bundle kitchen display (indented children, name + qty only); Receipt nested lines when ESC/POS route supports them; EOD record parent + nested child rows in orders_json (grill Q10 1 — mirror Composite kitchen display / EOD composite shape)orderRepository, receiptOrderMapper, kitchen card renderer, EOD snapshot mapper; ADR follow-on
B7 — Catalog editorConfigure bundle screen + PATCH + pushComposite conversion screen navigation shell only
B8 — Push parityproductMetaPushMapper WOOSB keys; inferTillProductType / parity checkswooProductTypePush.test.ts patterns

Recommended TDD order: B2 → B3 → B4 → B5 → B6 (red-green per slice; isolated unit tests in shared/woosb/*.test.ts, server/**/bundle*.test.ts, client/**/bundle*.test.ts).

Testing decisions

ModuleCoverage
parseWoosbBundleArray fixture; legacy string id/key/qty; text-only rows; optional min/max inheritance
validateBundleSelectionFixed qty reject; optional bounds; whole min/max; total_limits
priceBundleSelectionDynamic + % discount; dynamic + fixed amount; fixed parent price
bundleDefinitionApiMissing child product; variable expansion
order persistParent qty 2 scales children; EOD JSON nested lines

Further notes

  • Glossary entries in CONTEXT.md: WOOSB bundle, Bundle fulfillment picker, bundle basket line, Bundle kitchen display, Bundle children — distinct from WOOCO terms (Q10 1 aligns kitchen/receipt/EOD with composite patterns).
  • Consider ADR for bundle till order lines (sibling to ADR 0005) before B6.
  • Plugin source reference copy: .cursor/woo-product-bundle-premium-856/woo-product-bundle-premium/ (read-only; do not modify).
  • WPCleaverPro.md documents WOOCO only; do not merge WOOSB shapes into that file — cross-link from both.

Phase C — Meal Prep Bundle Designer Integration

Overview

When a meal prep staging product has bundle components configured via Configure bundle (WOOSB B7, Catalog Manage), Meal Prep Designer Save creates the resulting meal prep menu product as type: bundle with woosb_ids — instead of a type: simple product. The woosb_ids on the menu product references the same component product IDs as the staging product; no new component products are created by the designer. Staging dishes without components configured continue to produce type: simple menu products, preserving backwards compatibility with existing weekly menus and the full designer PRD (thin-client-meal-prep-designer-screen.md).

Configure bundle is not available inside the designer. Staff configure components on a staging product via Catalog Manage (WOOSB B7) before or after placing the dish in the designer. The designer reflects the current configuration state — it does not enforce that components are configured before Save.


C1 — Component status badge in designer (UI slice)

What ships:

Each dish card in MealPrepDesignerView gains a compact bundle component status badge below the product name and SKU row:

  • Green badge — “N components” when the staging product has componentCount > 0.
  • Amber badge — “⚠ Not configured” when componentCount === 0.
  • A tappable → Configure bundle link on each card opens the staging product in Catalog Manage:
    • Electron: calls openCatalogManageForProduct(stagingProductId) (same IPC pattern as Manage this product from POS grid) to focus the catalog management window with that product pre-selected.
    • Web / Android Capacitor: navigates to #/catalog?productId={id}.
  • No component editor exists inside the designer. Badge and link are display-only.
  • Cards in all three columns (Monday, Wednesday, pool) show the badge.
  • If componentCount is absent on the response (legacy Edge Server before C2 ships), badge is omitted; → Configure link still renders.

Server changes:

  • GET /api/meal-prep-designer/session — extend each product summary in poolProducts, mondayProducts, wednesdayProducts with componentCount: number (see C2 for full implementation detail).

Client changes:

  • MealPrepPoolProductSummary type (shared) gains componentCount?: number.
  • MealPrepDesignerSessionResponse product summary type gains componentCount?: number.
  • MealPrepDesignerView.vue dish card template updated with badge and → Configure link.
  • Electron: openCatalogManageForProduct(id) called from → Configure link handler.

Shared changes:

  • shared/mealPrepDesigner/sessionTypes.ts — add componentCount?: number to product summary.

Test pointers:

  • Unit: badge renders “3 components” (green class) from componentCount: 3; “⚠ Not configured” (amber class) from componentCount: 0.
  • Unit: badge is absent when componentCount is undefined (legacy response).
  • Unit: → Configure link invokes openCatalogManageForProduct with the correct staging product id.
  • Contract: pool cards in all three columns include badge data when session endpoint returns componentCount.

C2 — Session endpoint enhancement

What ships:

GET /api/meal-prep-designer/session returns componentCount: number per product in poolProducts, mondayProducts, and wednesdayProducts. This is the sole data path for designer bundle-awareness; the designer makes no separate call to GET /api/products/:id/bundle.

componentCount equals the number of non-layout bundled items in woosb_ids — items with a numeric id field, excluding rows with type h1h6, p, span, none. componentCount: 0 is always returned (never omitted) when meta_woosb_ids is null, empty, or unparseable.

Server changes:

  • Session query: for each product included in the response, JOIN or sub-select on products for meta_woosb_ids (added by WOOSB B1).
  • Count helper: use parseWoosbBundle from shared/woosb/ (B2) to count non-layout items when B2 is available. Lightweight fallback when B2 has not yet shipped: parse meta_woosb_ids TEXT as JSON/legacy string, count entries whose value has a numeric id field and no type property (or type not in layout set).
  • Return componentCount: 0 (not omitted) for null or empty meta_woosb_ids.

Shared changes:

  • shared/mealPrepDesigner/sessionTypes.tscomponentCount?: number on product summary (already noted in C1; no additional change).

Test pointers:

  • Integration: session route with a staging product having three WOOSB product items → componentCount: 3.
  • Integration: staging product with one layout heading and two product items → componentCount: 2 (layout row excluded).
  • Integration: staging product with null meta_woosb_idscomponentCount: 0.
  • Edge case: malformed meta_woosb_ids TEXT → componentCount: 0; no 500 error.

C3 — Save creates bundle product when components exist

What ships:

mealPrepMenuProductFactory.createMealPrepMenuProductFromStaging is extended: when the staging product has componentCount > 0 and a non-empty woosb_ids value, the created menu product is written with type: 'bundle' and meta_woosb_ids copied from the staging row. When componentCount === 0 or woosb_ids is absent, the menu product is type: 'simple' as today (backwards compatible).

Menu product woosb_ids references the same component product IDs as the staging product. The factory does not create, modify, or validate component products — that responsibility belongs to WOOSB B7 (Configure bundle) and the component product rows themselves.

The re-save freeze rule from designer PRD Q10 4 applies to woosb_ids: on re-save of an existing menu product, woosb_ids / type is not refreshed from staging — only categories and meal_prep_menu metadata may change. Staging bundle edits after first create apply to menu products via the future Publish workflow only.

meal_prep_menu table addition:

The meal_prep_menu table gains a nullable integer column bundle_component_count:

  • NULL for rows created before this migration (legacy).
  • 0 for placements where the staging product had no WOOSB components at save time → simple menu product.
  • N > 0 for placements where the staging product had N components at save time → bundle menu product.

Server changes:

  • CreateMealPrepMenuProductInput interface gains woosbIds?: string | null and componentCount?: number.
  • createMealPrepMenuProductFromStaging: when componentCount > 0 and woosbIds truthy, pass type: 'bundle' and meta_woosb_ids: woosbIds to createStaffProduct; otherwise omit both (defaults to type: 'simple').
  • Save service (meal prep designer save service): before the transaction, read meta_woosb_ids and derived componentCount from the staging product row; pass to factory.
  • meal_prep_menu SQLite migration: ALTER TABLE meal_prep_menu ADD COLUMN bundle_component_count INTEGER (nullable; no default required — existing rows get NULL).
  • meal_prep_menu repository upsert: include bundle_component_count in insert/update.

Shared changes:

  • None required. Save payload (shared/mealPrepDesigner/savePayload.ts) is unchanged — woosb_ids is resolved server-side from the staging row at save time, not sent from the client.

Test pointers:

  • Unit: factory with componentCount: 3, woosbIds: '1665/k7m2/1' → product row has type = 'bundle', meta_woosb_ids = '1665/k7m2/1'.
  • Unit: factory with componentCount: 0 → product row has type = 'simple'; meta_woosb_ids not set.
  • Unit: factory with componentCount: undefined (legacy call) → product row has type = 'simple' (no regression).
  • Integration: POST /api/meal-prep-designer/save with staging product that has meta_woosb_ids → created menu product row type = 'bundle'; meal_prep_menu row bundle_component_count > 0.
  • Integration: re-save of same week with bundle staging product → existing menu product meta_woosb_ids not overwritten (Q10 4 freeze).
  • Integration: staging product with no meta_woosb_ids → menu product type = 'simple'; meal_prep_menu.bundle_component_count = 0.
  • Regression: existing plain meal prep save (no WOOSB) continues to produce type: simple menu product.

C4 — Staging product type in catalog

What ships:

When a staging meal prep product has bundle components configured via WOOSB Configure bundle (B7), its type on the Edge Server SQLite master is bundle — driven by inferTillProductType returning bundle when meta_woosb_ids is present (WOOSB PRD Q1 1). No new logic is required in Phase C; this slice confirms the interaction between the existing WOOSB type-inference and meal prep staging products.

POS sell grid filter: Staging bundle products are Published draft / Local_Only. The existing sell-grid draft filter already excludes them. No new filter required for bundle staging rows.

Label printing: The type: bundle field on a staging or menu product does not affect template selection. Categories drive template selection via label-category-templates.json. A staging product in Meal Preps or a menu product in Meal Prep - Monday / Meal Prep - Wednesday prints via mealprep-label regardless of type (designer PRD Q11 1).

No new server, client, or shared changes required. This is a confirmatory slice.

Server changes: None.

Client changes: None.

Shared changes: None.

Test pointers:

  • Verify: inferTillProductType returns bundle for a staging product row with meta_woosb_ids populated (covered by WOOSB B8 tests; cite reference only).
  • Verify: staging product with type: bundle is absent from POS sell grid (existing draft-filter test extended with a bundle-typed staging row).
  • Verify: Print Label on a staging bundle product in Meal Preps → resolves mealprep-label (existing label-category template test extended).

C5 — POS sell path for bundle menu products

What ships:

When staff tap a published Meal Prep – Monday or Meal Prep – Wednesday menu product whose type is bundle, the standard WOOSB sell path applies automatically:

  • B4 fast path (one-tap add): when the bundle has no optional rows and no variable products (has_optional = false, has_variables = false) — default composition added immediately.
  • B5 Bundle fulfillment picker: when has_optional = true or has_variables = true — picker opens on grid tap, blocking checkout until confirm or cancel.

Kitchen board: bundle_children are indented under the parent line (Q10 1 — mirrors Composite kitchen display). Quantities reflect per-kit qty × parent quantity.

Receipt: bundle children are listed under the parent when the ESC/POS route supports nested lines (B6 rule — same as composite receipt).

No new code beyond general WOOSB B4–B5 sell path. Meal prep menu products with type: bundle participate in the standard WOOSB checkout flow without special-casing. This slice confirms that the sell path is complete for meal prep bundles once B4–B5 ship.

Server changes: None specific to meal prep — WOOSB B4–B6 order-line shape covers this.

Client changes: None specific to meal prep — WOOSB B4–B5 picker and basket composable cover this.

Shared changes: None.

Test pointers:

  • Integration: a Meal Prep - Monday product with type: bundle, fixed composition, no optional rows → one-tap add produces line_kind: 'bundle' with bundle_children matching component ids.
  • Integration: kitchen card for the above order → bundle children indented under parent (Q10 1).
  • Integration: Meal Prep - Wednesday product with has_optional: true → grid tap opens Bundle fulfillment picker.
  • Regression: Meal Prep - Monday product with type: simple → unchanged one-tap add (no bundle_children).

Dependency map

Phase C sliceDepends on
C1 (badge)WOOSB B1 (meta_woosb_ids column on products)
C2 (session count)WOOSB B1; B2 (shared/woosb/parseWoosbBundle) preferred but not hard-blocked
C3 (factory)WOOSB B1; B7 (Configure bundle) for staff to populate woosb_ids on staging; C2 (session count drives factory input)
C4 (type inference)WOOSB B1, B8 (inferTillProductType) — no new work; confirmatory
C5 (sell path)WOOSB B4–B6 (fast-path add, picker, order lines)

C1 + C2 can ship as read-only enhancements as soon as B1 lands — the badge is informational and does not gate Save.

C3 can ship after B1. C5 requires B4–B5 to be complete. B7 (Configure bundle) must exist for the badge to show meaningful counts, but C3 will gracefully produce type: simple menu products for staging rows without woosb_ids until B7 is used.


User stories

Component configuration

  1. As a chef, I want to configure bundle components on a staging meal prep product via Configure bundle in Catalog Manage, so that the designer can reflect which proteins, veg, carbs, and sauces make up each dish.
  2. As a manager, I want each dish card in the Meal Prep Designer to show a bundle component status badge — “N components” (green) or “⚠ Not configured” (amber) — so that I can see at a glance which dishes are fully defined before saving the weekly menu.
  3. As a manager, I want a → Configure bundle tappable link on each dish card to open the staging product in Catalog Manage without leaving the designer window, so that I can configure missing components without rebuilding my planning session.

Designer save — mixed simple/bundle weeks

  1. As a manager, when I save a week where some dishes have bundle components configured and others do not, I want bundle dishes to produce type: bundle menu products and unconfigured dishes to produce type: simple menu products, so that each dish sells correctly at the till without requiring a uniform component-first workflow.
  2. As a manager, I want the designer’s Save to be backwards compatible with existing weekly menus that have no WOOSB components, so that weeks saved before Phase C ships are unaffected.

Customer at the till

  1. As kitchen staff, when I tap a Meal Prep – Monday or Meal Prep – Wednesday bundle product on the POS grid, I want one-tap add for fixed-composition dishes and the Bundle fulfillment picker for dishes with optional or variable rows, so that meal prep bundles sell as correctly as any other Smart Bundle.

Kitchen and receipt

  1. As kitchen staff, I want the Kitchen board to indent bundle component lines under the parent meal prep dish line, so that prep staff know exactly which components to assemble for each order.
  2. As kitchen staff, I want the Receipt to list bundle components under the parent meal prep dish (when the ESC/POS route supports nested lines), so that counter tickets are readable for bundle meal preps.

Out of scope (v1)

  • Component products created or selected inside the designer — component configuration remains in WOOSB B7 Configure bundle only.
  • Bundle pricing overrides per weekly menu — menu products inherit staging WOOSB pricing meta as-is; pricing changes require editing the staging bundle directly in Catalog Manage.
  • Nutritional rollup across bundle children — existing meta__nutrition on the staging product is copied to the menu product on first create per designer PRD Q10 4; per-component nutrition is not aggregated.
  • Publish button implementation in the designer — already stubbed as a separate PRD; Phase C does not affect the Publish extension point.
  • Variation selection inside the designer — variation picks belong to the Bundle fulfillment picker at sell time (B5), not the planning stage.
  • Validating that all staging bundle dishes have componentCount > 0 before allowing Save — the badge is advisory only; managers may Save a week containing “⚠ Not configured” dishes.

Leave a Comment

Your email address will not be published. Required fields are marked *

Shopping Basket