diff --git a/src/data/catalog.ts b/src/data/catalog.ts index 4eda6cc..9faf495 100644 --- a/src/data/catalog.ts +++ b/src/data/catalog.ts @@ -1,7 +1,7 @@ /** * iBamboo catalog helpers. - * Live assortment SoT is Flash Catalog (see useFlashCatalog). - * Static merge below is emergency fallback when flash is unreachable. + * Static house + BSR catalog is the bulk shelf. Flash layers on top when live + * (see useFlashCatalog merge) until Flash publish depth can own full assortment. */ export type { Category, Product, ProductSpec } from './types' export { products as curatedProducts } from './products' diff --git a/src/hooks/useFlashCatalog.ts b/src/hooks/useFlashCatalog.ts index 0e9e30c..4040a91 100644 --- a/src/hooks/useFlashCatalog.ts +++ b/src/hooks/useFlashCatalog.ts @@ -26,17 +26,22 @@ function finalizePool(list: Product[]): Product[] { return out } +/** + * Merge flash over static house catalog. + * Flash alone is still too thin (~12 SKUs) to own the full shop; until Flash + * publish depth is production-grade, keep static assortment and layer flash on top. + */ +function mergeFlashOverStatic(flash: Product[]): Product[] { + return finalizePool([...flash, ...shopProducts]) +} + export type FlashCatalogState = { - /** - * Active shop assortment. - * When source === 'flash', this is flash-only (control plane SoT). - * When source === 'static', emergency fallback (static BSR/curated). - */ + /** Full shop list: flash picks first, then full static catalog */ products: Product[] flashOnly: Product[] payload: FlashCatalogPayload | null loading: boolean - source: 'flash' | 'static' + source: 'flash' | 'static' | 'merged' error?: string generatedAt?: string weekOf?: string @@ -44,8 +49,8 @@ export type FlashCatalogState = { } /** - * Load site assortment from Flash Catalog (source of truth). - * Static catalog is emergency fallback only when flash is empty/unreachable. + * Load flash catalog and merge onto the static house/BSR assortment. + * Static is the bulk shelf; flash is live control-plane overlay (not a wipe). */ export function useFlashCatalog(siteId = 'ibamboo'): FlashCatalogState { const catalogBaseUrl = flashCatalogBaseUrl() @@ -89,14 +94,14 @@ export function useFlashCatalog(siteId = 'ibamboo'): FlashCatalogState { return useMemo(() => { const flashOnly = finalizePool(mapFlashProducts(payload)) + const staticPool = finalizePool(shopProducts) if (flashOnly.length > 0) { return { - // Assortment SoT: flash products only — do not merge static shelves - products: flashOnly, + products: mergeFlashOverStatic(flashOnly), flashOnly, payload, loading, - source: 'flash' as const, + source: 'merged' as const, error: undefined, generatedAt: payload?.generatedAt, weekOf: payload?.weekOf, @@ -104,12 +109,12 @@ export function useFlashCatalog(siteId = 'ibamboo'): FlashCatalogState { } } return { - products: finalizePool(shopProducts), + products: staticPool, flashOnly: [], payload: null, loading, source: 'static' as const, - error: error || (loading ? undefined : 'Using static emergency catalog'), + error: error || (loading ? undefined : undefined), weekOf: undefined, generatedAt: undefined, catalogBaseUrl, diff --git a/src/pages/Shop.tsx b/src/pages/Shop.tsx index 7ca00da..797347f 100644 --- a/src/pages/Shop.tsx +++ b/src/pages/Shop.tsx @@ -151,8 +151,8 @@ export function Shop() {

{limited - ? 'Assortment from the live flash catalog control plane — filters and schedule managed there; refresh anytime without a site redeploy.' - : 'Browse by room of the house, then buy on Amazon with secure checkout. Assortment is driven by the flash catalog.'} + ? 'This week’s limited options from the house edit and live flash refresh.' + : 'Browse by room of the house, then buy on Amazon with secure checkout.'}

@@ -161,17 +161,17 @@ export function Shop() {

{flash.loading ? 'Loading assortment…' - : flash.source === 'flash' - ? 'Live assortment' - : 'Emergency static catalog'} + : flash.source === 'merged' + ? 'Full collection · live flash layered' + : 'Full collection'}

{flash.loading ? '…' : `${pool.length} products`} + {!flash.loading && flash.flashOnly.length + ? ` · ${flash.flashOnly.length} live flash` + : ''} {!flash.loading && drop.weekOf ? ` · week of ${drop.weekOf}` : ''} {!flash.loading && until ? ` · rotates ${until}` : ''} - {!flash.loading && flash.error && flash.source === 'static' - ? ` · ${flash.error}` - : ''}

diff --git a/tests/flash-catalog.test.mjs b/tests/flash-catalog.test.mjs index 368512a..256f6b0 100644 --- a/tests/flash-catalog.test.mjs +++ b/tests/flash-catalog.test.mjs @@ -68,11 +68,24 @@ describe('flash catalog Phase A contract', () => { ) }) - it('flash-only SoT: empty flash means fallback, non-empty means no static merge needed', () => { - // Document the contract used by useFlashCatalog - const flashOnly = [{ id: 'flash-A' }, { id: 'flash-B' }] - const products = flashOnly.length > 0 ? flashOnly : ['static'] - assert.deepEqual(products, flashOnly) - assert.notEqual(products[0], 'static') + it('merges flash over static without wiping the house catalog', () => { + const flashOnly = [{ id: 'flash-A', asin: 'A' }, { id: 'flash-B', asin: 'B' }] + const staticPool = [ + { id: 'static-1', asin: 'A' }, // duplicate ASIN → flash wins + { id: 'static-2', asin: 'C' }, + { id: 'static-3', asin: 'D' }, + ] + const seen = new Set() + const products = [] + for (const p of [...flashOnly, ...staticPool]) { + if (seen.has(p.asin)) continue + seen.add(p.asin) + products.push(p) + } + assert.equal(products.length, 4) + assert.equal(products[0].id, 'flash-A') + assert.ok(products.some((p) => p.id === 'static-2')) + assert.ok(products.some((p) => p.id === 'static-3')) }) }) +