Medusa inventory: model Ingram drop-ship as a single stock location
Why now
Today’s SFF availability-filter experiment proved we need real inventory tracking, not a discovery-time filter. The iPage saved-search filter was opaque (excluded Between Two Fires despite 410 on-hand) and removing 16 books from the storefront based on it lost real catalog. Reverted, but the right answer is: discover everything, track inventory accurately, let the storefront UI show availability honestly (in-stock / out / preorder / backorder).
Today we also unblocked the rescrape/enrich path (just enrich <isbn>+ per ipage-extract-by-ean_id — see today’s work) which means inventory can sync independently of full catalog ingestion.
The model
Single virtual location representing Ingram’s drop-ship. Not 4 locations for PA/IN/OR/TN — those are Ingram’s internal routing, not anything Carrie operates. From the store’s perspective there’s one fulfillment source.
- Name:
Ingram (Drop-Ship)orDistributor - Address:
One Ingram Blvd, La Vergne, TN 37086, US(Ingram’s actual primary US fulfillment center — real public address, not a fiction) - Linked to the existing
Online Storesales channel
Per-variant inventory:
manage_inventory: trueon the variant (currentlyfalse)inventory_level = sum(on_hand across all 4 DCs)at the Ingram locationon_order(per-DC sum) stays inmetadata.ingram_stockfor surfacing in admin / future “available to backorder” UI
What this gives
- Cart-time validation: Medusa rejects orders for books at 0 inventory unless backorder is allowed per-variant
- Honest in-stock labels computed from real data instead of just “is the product visible”
- Backorder UX: 0 on_hand + positive on_order → “Available to order, ships in 1-2 weeks”
- Preorder UX: future
pub_date→ “Preorder, releases [date]” - Low-stock event hook (Medusa fires when inventory drops below threshold) — useful later for “reorder these from Ingram” alerts
What this avoids
- Per-DC routing detail customers don’t care about
- 4 inventory_level rows per book
- Pretending we operate warehouses we don’t
Implementation
Two PRs, ~80 lines net in emporium + ~50 in catalog.
PR 1 (emporium): create location + wire inventory on import
- Migration: create the
Ingram (Drop-Ship)stock location at the real La Vergne TN address, link toOnline Storesales channel buildProductInputincatalog-import.ts:- Set
manage_inventory: trueon the variant - Set initial
inventory_levelagainst the Ingram location =sum(book.stock[*].onHand)
- Set
runCatalogImport’s “already exists by SKU” branch needs to updateinventory_leveltoo (currently it only flipssync_statusto imported and does nothing in Medusa). New code path.- Admin: optional “Sync inventory” button on the Catalog Import settings page that re-runs only the inventory-update path against all imported books.
PR 2 (catalog): lightweight just sync-stock command
- New entry point:
just sync-stock <target>(defaults to the SFF searchId — or maybe Indie Vault once we have it) - Downloads CSV via existing
download_csv.run()to get the ISBN list + price (no detail-page fetches — that’s what makes this “lightweight”) - Wait — CSV doesn’t include per-DC stock. Stock is on the detail page only. So sync-stock either:
- (a) Hits the detail page per ISBN (basically the existing enrich flow but only extracting stock), or
- (b) Hits iPage’s grid which has aggregate stock counts in the cell layout (need to verify)
- Goes with (a) for simplicity unless (b) is meaningfully faster: ~15s per 100 books at concurrency 8.
- Updates the queue DB
stockcolumn on each book - Emits a flag (per book?) when stock changes meaningfully so emporium’s sync can be selective
- Cron-ready: should run hourly or every few hours, independent of full catalog ingestion
The freshness question
iPage stock at sync time → Medusa inventory. Customer order at T+2 hours might not be fulfillable if Ingram sold through. Two paths:
- Tighten sync cadence — hourly stock sync via cron. Each run is ~15s for 100 books (we know this from today’s live runs). Cheap.
- Validate on order placement — at checkout time, hit iPage’s product page for that ISBN and confirm stock before charging. Real-time but adds Imperva exposure and a synchronous hop in the checkout path.
Start with (1). Revisit (2) only if we hit real “sold a book Ingram doesn’t have anymore” incidents.
Order of operations
- Land catalog stack (PRs 7 already merged; #8 rename refactor pending)
- Decide SFF filter strategy — probably drop the in-stock filter, use top-N unfiltered (see today’s journal for the experiment + reversal)
- PR 1 (emporium) — location + import-side wiring
- PR 2 (catalog) — sync-stock command + cron
- Backfill: run sync-stock once across all 100 imported books to populate inventory from current stock data
- Verify storefront in-stock/out-of-stock labels reflect reality
Out of scope for this plan
- cover-change-detection — separate plan, only matters when placeholder→final art rotation matters more than it does today
- Indie Vault as a discovery source — depends on Carrie pulling the URL (indie-vault)
- Order routing across multiple sources (Bookshop, Amazon, direct from publisher)
- Real-time stock validation at checkout time
- Reorder triggers / “low stock” alerts to Carrie