Overall framing for the room: most of what Rouge asked is either already solid or a known, near-term roadmap item where we own the underlying data — very little is a research problem. Their forecasting pain (spike dilution, promo pollution) is real and is our best design input. Rouge is not blocked for this year's BF (Vivien's plan is the fallback), so this is a "which engine to lean on" conversation, not a rescue.
"Yes — they aggregate into one demand record. We key all sales and demand history off your internal SKU, not the Shopify product ID. Each Shopify listing is just a mapping that points at the same internal product, so both the bundle listing and the panel listing resolve to one SKU and their two years of orders sum into a single demand signal. That's the same master-SKU model Inventory Planner, Linnworks, Cin7 and NetSuite use — and because our link is an explicit mapping set at import rather than a fuzzy SKU-string match, it won't silently split them the way heuristic matching can."
Order line → Shopify variant_id → ProductListing → internal product_id. The forecast aggregates strictly by internal product_id (GROUP BY product_id); the channel listing plays no role in grouping. Two Shopify products = two ProductListing rows both pointing at the same internal Product.
Both Shopify listings must actually be mapped to the same internal Product in this tenant. An unmapped listing's lines get product_id = null and simply drop out of the forecast (they don't split or double-count) — worth confirming before go-live.
Inventory Planner uses a "Master SKU" (auto-match by SKU/barcode/ASIN, else manual); Linnworks/Cin7/NetSuite all forecast off the internal item. Ours is a deterministic FK set at import, so it doesn't depend on the two listings sharing an identical SKU string — removing competitors' #1 failure mode (fuzzy-match misses).
"You're right on the part that matters, and I'll be straight about the part where we have a gap. Our forecast runs purely on units, never dollars — so the $0 price doesn't do anything magic, and critically that phantom door-mount line does not add demand to your panel SKU, because we attribute it to the door-mount's own SKU, not the panel. Where it is a problem is the door-mount SKU itself: old orders carry that line and new ones don't, so that SKU's own history is inflated for the legacy period. Today we don't have a 'this part is only ever sold inside a bundle — don't forecast it standalone' flag, which an ERP would call a phantom component. The clean fix now is to stop reordering the door-mount independently and treat it as driven off the panel — and giving you that flag natively is fair to put on our roadmap."
Forecast demand = SUM(quantity) → daily average. Revenue is a display-only column the reorder math never reads — so a $0 line contributes its full unit count. It's the inverse of "only revenue counts": the units count, the revenue evaporates. Units attribute to that line's own product_id (the door-mount), never the panel.
Every sales-order-line SKU becomes independent demand. There's no phantom/"bundle-component-only" flag forecasting honors, and filters are order-level (can't strip one $0 line). So no setting reconciles the mixed old/new eras — the rows genuinely differ.
If the panel is modeled as a kit whose recipe already lists the door-mount AND a standalone $0 door-mount line also exists on old orders, the door-mount can be double-counted on those orders. Worth a quick data check.
Don't set reorder points on the door-mount (exclude from replenishment) so its polluted history never drives a PO; or trim its forecast window to the post-transition period.
Units-not-dollars and component-level attribution = standard (SAP, NetSuite, Inventory Planner). The missing piece — an item-level phantom flag (Oracle/Infor "MRP sees through it") and history-cleansing for structural breaks — is where we're behind.
"Here's the safe order of operations: promote your panels whenever you like — the SKU is unchanged, so they auto-link to the same internal product. But leave the old ghost-bundle listings live on Shopify until we've run and verified the full two-year import, because our order matching keys on the Shopify variant ID, so those old variants need to be present at import time for your history to back-link and bundle-expand correctly. Once that's confirmed, deleting them is completely safe — the line-to-product link is already locked in. The one thing that would permanently orphan your history is deleting the bundles first — because today we don't fall back to matching by SKU to recover it. That SKU-based re-match is on our roadmap."
Order lines match to products by shopify_variant_id, with no SKU fallback. At import, each line snapshots its resolved product_id and survives later listing deletion. But if you delete a ghost-bundle listing before importing, its historical lines have no catalog variant to join to → permanently unmapped (no product, no COGS, no inventory movement, no bundle expansion). "SKU stays the same" can't rescue it, because SKU isn't the join key.
Generic ERP advice ("restructure master data before import") assumes the target re-matches by SKU — Linnworks/Cin7 do. We don't (yet), which inverts the advice for this specific migration. Roadmap: a SKU-based re-link job + a delete-guard warning on non-FBA listings with order history.
"I'll be straight: this is the area where we're behind, and your instinct is exactly right. Today we use a trailing simple average (default 30 days) as the baseline — not a same-year event-vs-baseline comparison and not year-over-year — and we don't auto-detect seasonality at all. A trailing average dilutes a sharp spike: a 30× day inside a 30-day window barely moves the average. The only way to express your 7–9× panel behaviour today is a manual multiplier you type in, and our current design can even average a one-month spike down when the planning horizon spans quieter months. That's the exact gap we're closing next: auto-deriving a week-level, year-over-year seasonal index that suggests the multiplier and keeps it concentrated on the spike dates instead of smearing it. Your 30×/7–9× data is the perfect design input."
The "Seasonal" modifier is a manual, uncapped % (a user could type +2900% for 30×) — the system applies it, it just won't derive it.
Multiplicative seasonal index via ratio-to-moving-average, computed at the resolution the spike lives at (weekly / day-of-week, not monthly) and year-over-year across ≥2 years — with recurring seasonality separated from one-off promo/event lift (baseline + event overlay). Netstock, Inventory Planner (Sage), GAINS, Holt-Winters all auto-derive it. We hand-enter it.
"Honest answer: today promo handling is manual, not automatic — there's no campaign-tag and no auto spike-detection, and nothing has been set up for Rouge because the feature doesn't exist yet. To keep a promo like July 4th out of a clean baseline, you point the forecast at a clean date window that avoids it — or filter those orders out. That's the same manual approach Inventory Planner and Cogsy take; only the enterprise suites like ToolsGroup and Blue Ridge auto-separate baseline demand from promo lift. That exact gap — automatic spike-flagging plus a one-click 'exclude this campaign' control so a July-4th window can't silently pollute your baseline — is what we want to build next, and your case is the perfect design input."
The baseline is one contiguous date window; no promo/campaign concept, no outlier/anomaly detection anywhere in forecasting. A promo spike inside the window inflates the mean proportionally — exactly your July-4th problem.
The advanced sales filter has a generic order tag filter and an order_date filter with is_not / OR-groups — so you can exclude tagged orders or carve out promo dates by hand. It's manual and uses generic order primitives, not a forecast-native campaign concept.
Ladder: (a) auto outlier detection/smoothing with review → (b) manual campaign tagging → (c) full baseline-vs-promo-lift decomposition. ToolsGroup/Blue Ridge do (c) automatically; Netstock does (a); Inventory Planner/Cogsy/NetSuite are manual like us. Near-term win for us: named promo-window exclusion + auto spike-flag (would've caught your July-4th window automatically).
"Yes — you can forecast and set seasonality per market inside your single Shopify store today, because our forecasting segments on the shipping destination, not just the sales platform. Each market gets its own demand config and its own seasonal adjustment. Being straight with you: right now that's a config-per-market setup (CA, US, AU, UK, and EU as a country list), and the seasonal curve is one you set rather than one we auto-learn. 'Channel' in our sense is platform-level (Shopify vs Amazon vs wholesale); market is expressed as a shipping-country filter. Auto-detected per-market seasonality and a single dashboard that breaks demand out by market are on our roadmap — that's where RELEX and Blue Yonder sit."
Forecasting has no fixed "channel" dimension — it segments demand via a saved sales-order filter tree. That includes geo filters (shipping_country_code, shipping_province) alongside platform filters (integration = Shopify). So per-market = one ForecastConfiguration per market. Seasonal modifiers scope only by product, not geo — so per-market seasonality means attaching modifiers to each per-market config.
True per-destination-market seasonality within one storefront is uncommon even among competitors — most (Netstock, Inventory Planner) segment by warehouse/location, not shipping country. Our filter model is genuinely flexible here; the gaps are auto-detection and a per-market breakdown view. Short-term: ship a "market" preset that scaffolds the per-country configs so Rouge isn't hand-building filter trees (EU = an is_one_of country list).
"For any single recommendation, Rouge can open the calculation and see the whole chain — the exact baseline window, the daily sales rate before and after any multiplier, lead time, MOQ, min-stock, and the literal formula, each with a badge showing where the number came from. That's genuinely line-item defensible to your CFO in the app today. Two honest caveats: our current spreadsheet export shows the final rate and baseline day-count but drops the multiplier and source tags — we're adding those so the download is a full audit trail — and we don't yet put a numeric confidence score on a line. A brand-new SKU with little history still gets a sensible number from your min-stock and backorder rules, but it won't be flagged 'low confidence' yet. That flag is on our roadmap; today that judgment stays with your planner, and all the transparency to make it is on screen."
SMB tools (Inventory Planner, Cogsy) match our UI transparency and also lack a numeric confidence score. Enterprise/AI (Netstock, Lokad) add probabilistic confidence intervals + explicit new-product (cold-start) handling — link-to-similar-SKU is the natural next step for us. Buyers literally screen for "how do you handle new products with no history, and is it a black box?"
"Today our demand-planning forecast resolves one lead time per product/supplier — it doesn't change by destination market (the warehouse only changes on-hand numbers). So the clean way to reflect your 2-month vs 2.5-month markets right now is a per-region supplier setup keyed to each warehouse. Per-destination lead time is on our roadmap as a first-class field — and importantly we already model per-lane transit times in our manufacturing/transfer engine, so it's a matter of surfacing that in the forecasting workflow, not inventing it from scratch."
Forecast lead time resolves supplier-product → supplier → global default. The supplier_products table has no warehouse/market column. There's no "Market" entity — a market maps to a Warehouse (which has country_code).
The separate MRP engine already supports per-(product, warehouse) lead time (ItemPlanningPolicy) and per source→destination lane transfer lead times (DistributionNetwork.transfer_lead_time_days) — exactly your sea-freight-to-UK case. They're just not wired into the forecasting screens yet. Fix = let destination_warehouse_id feed lead-time resolution.
Enterprise APS (GAINS, Netstock, Blue Yonder) model supplier + lane transit lead time per supplier-item-destination. In the ecommerce mid-market a clean "per-market lead time" field is uncommon — Inventory Planner/Cogsy also handle it via per-vendor settings or separate supplier records, like our workaround.
"Today our forecast is built on gross units sold, so for a high-return SKU you'd currently account for returns separately — there's an exclude-returns filter, but it's a coarse whole-order toggle, not a per-SKU net. Where we're actually strong is that we already compute return rate by SKU in our product scorecard and analytics — the gap is purely that it isn't wired into forecasting yet. Honestly, most ecommerce tools in our tier — Inventory Planner, Cogsy, Prediko — don't do true per-SKU return-rate netting either; only the enterprise retail suites do. So accounting for returns separately today is normal for the category, but because we already own the return-rate data, netting it into demand is a near-term, high-confidence roadmap item, not a research project."
Demand = gross SUM(quantity), excluding only draft/cancelled orders. No return term in the reorder formula. The forecast doesn't even subtract the per-line returned_quantity the rest of the app uses for net-sold — so a high-return SKU is systematically over-forecast. The one lever (exclude_returns) is opt-in and drops whole refunded orders by payment status.
ProductScorecardManager::returnRate() = returned ÷ sold per SKU; return_rate_pct in product analytics. Not consumed by forecasting. Wiring = a quick win (subtract returned_quantity) + an opt-in "expected return rate" demand modifier netting velocity by (1 − rate).
Forecast on gross, model returns as a separate per-SKU stream, then net replenishment (or treat returns as inbound supply). DTC returns run 20–40% vs 8–10% retail, so gross-only over-buys high-return SKUs. Enterprise (Blue Yonder/RELEX/o9) do ML return-probability; SMB tools mostly don't.
"Most planning tools will give your ops tool the forecast number and your PO and lead-time data over an API — we do that too — but very few will hand you the calculation itself. Our forecast API returns the full per-line trace: the baseline daily demand, the exact multiplier we applied and why, the lead-time and coverage math, the live inventory position, and the literal formula — each with a source tag — so Rouge can reconstruct our recommendation and blend it with your own context. The one thing we don't yet emit is a single statistical confidence interval; today you'd infer confidence from our velocity-trend and warning signals, and a first-class confidence field is on our roadmap."
auth:sanctum)POST /api/inventory-forecasting/build returns per line: base_daily_average → demand_modifier_description → daily_average → lead-time/target coverage → inventory snapshot → final_quantity, plus the literal formula and a *_source on every input.avg_actual_lead_time, reliability, variance) via the inventory-planning report.Route::resource('purchase-orders') + lines/receipts/movements), plus forecast-driven PO creation.No single "confidence" field (infer from velocity-trend + base-vs-adjusted + warnings). Forecasts are computed on-demand via POST (no GET-by-id stored breakdown yet). Measured lead-time actuals live on a companion report call, not in the build payload. External token needs purchase-orders/suppliers/reports scopes.
Inventory Planner/Netstock expose forecast + PO + lead-time data but keep the "why" in a UI drill-down. Exposing a machine-readable per-line calculation trace is uncommon — a genuine differentiator. Big ERPs (D365 BC, NetSuite) lead only on returning a confidence interval.
Short version: you're right on both counts. Subscription editions genuinely are effective-dated point-in-time versioning — but only of a single scalar product pointer (offering → which SKU ships in a date window), and they've drifted. What began (Dec 2025) as a clean effective_date/cutoff_date resolver became (Mar 2026) a priority-ranked, quantity-rationed, order-splitting allocation engine — and the "no overlapping date ranges" validation was gutted to a no-op because priority now does the work. Separately, bundle/kit composition isn't effective-dated at all: the recipe table has no temporal columns. These are two different mechanisms, not one framework — and neither is a general-purpose time-versioned association system.
product_id. Real as-of-date semantics (findEditionForDate). This core idea is sound and industry-standard — it's exactly what Recharge ("monthly SKU swap", "order-sequence swap") and Skio do for changing box contents. Resolve at order time, then freeze onto the sales-order line. Keep this.product_components) = live snapshot, no version, no dates. Bundles fake history by snapshotting the component qty onto the order line at write time (immutable — good). Kits retain nothing: the forecast re-explodes every historical kit sale against the current recipe, so editing a kit recipe silently rewrites history. You can't ask "what was product X's recipe on date Y."The edition row now fuses three orthogonal concerns: (a) temporal versioning, (b) priority tie-break, (c) inventory quota/allocation/splitting. Only (a) is temporal. Jamming (b)+(c) in is what forced disabling the non-overlap invariant.
Time-versioned composition belongs in an effective-dated / revision-controlled Bill of Materials, not in subscriptions. Each recipe revision carries valid_from/valid_to; the recipe resolves as-of the order/build date; and the system enforces non-overlap (NetSuite auto-computes obsolete dates so revisions never gap or overlap — the exact invariant you had to disable). Subscription platforms deliberately keep "what ships next month" as a workflow concern, separate from composition versioning.
BomRevision (valid_from/valid_to, non-overlap enforced) owning the recipe rows, resolved as-of date — so bundles and kits finally share one as-of-date model, and kit history stops silently rewriting.Meeting-safe line if Otis asks: "You're right that it drifted — the core effective-dated idea is sound and is what Recharge and Skio do, but we bolted quota and splitting onto it. The clean pattern for versioning a recipe over time is an effective-dated Bill of Materials — NetSuite's revision-controlled BOM — and separating those two is the fix."