@cimplify/sdk 0.38.2 → 0.38.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/advanced.js +20 -20
  2. package/dist/advanced.mjs +1 -1
  3. package/dist/{chunk-ZYACIPKO.js → chunk-5VHKZTFJ.js} +35 -35
  4. package/dist/{chunk-HKVRUGM6.js → chunk-BAWKQRIB.js} +6 -1
  5. package/dist/{chunk-TFHKVQLL.mjs → chunk-EZK2FXTM.mjs} +6 -1
  6. package/dist/{chunk-2X6P5WJZ.mjs → chunk-JTU5P6GM.mjs} +1 -1
  7. package/dist/dispatcher.mjs +1 -1
  8. package/dist/index.js +66 -66
  9. package/dist/index.mjs +2 -2
  10. package/dist/mock/cli.mjs +1129 -436
  11. package/dist/mock/library.d.mts +2 -2
  12. package/dist/mock/library.d.ts +2 -2
  13. package/dist/mock/library.js +1129 -436
  14. package/dist/mock/library.mjs +1129 -436
  15. package/dist/mock/msw.d.mts +1 -1
  16. package/dist/mock/msw.d.ts +1 -1
  17. package/dist/mock/msw.js +1129 -436
  18. package/dist/mock/msw.mjs +1129 -436
  19. package/dist/react.js +60 -60
  20. package/dist/react.mjs +3 -3
  21. package/dist/{server-B8TeE2OD.d.mts → server-CVEsjECc.d.mts} +45 -0
  22. package/dist/{server-B8TeE2OD.d.ts → server-CVEsjECc.d.ts} +45 -0
  23. package/dist/styles.css +1 -1
  24. package/package.json +1 -1
  25. package/templates/storefront/.env.example +11 -2
  26. package/templates/storefront/README.md +35 -18
  27. package/templates/storefront/app/cart/page.tsx +9 -0
  28. package/templates/storefront/app/categories/[slug]/page.tsx +80 -0
  29. package/templates/storefront/app/checkout/page.tsx +17 -0
  30. package/templates/storefront/app/collections/[slug]/page.tsx +80 -0
  31. package/templates/storefront/app/globals.css +47 -0
  32. package/templates/storefront/app/layout.tsx +45 -0
  33. package/templates/storefront/app/orders/[id]/page.tsx +21 -0
  34. package/templates/storefront/app/page.tsx +65 -0
  35. package/templates/storefront/app/shop/page.tsx +46 -0
  36. package/templates/storefront/components/category-grid.tsx +28 -0
  37. package/templates/storefront/components/collection-strip.tsx +45 -0
  38. package/templates/storefront/components/footer.tsx +19 -0
  39. package/templates/storefront/components/header.tsx +41 -0
  40. package/templates/storefront/components/hero.tsx +25 -0
  41. package/templates/storefront/components/product-modal.tsx +105 -0
  42. package/templates/storefront/components/providers.tsx +31 -0
  43. package/templates/storefront/components/store-product-card.tsx +99 -0
  44. package/templates/storefront/lib/cart.ts +27 -0
  45. package/templates/storefront/next.config.ts +36 -0
  46. package/templates/storefront/package.json +13 -14
  47. package/templates/storefront/postcss.config.mjs +7 -0
  48. package/templates/storefront/tsconfig.json +15 -11
  49. package/templates/storefront/index.html +0 -12
  50. package/templates/storefront/src/App.tsx +0 -118
  51. package/templates/storefront/src/main.tsx +0 -28
  52. package/templates/storefront/src/styles.css +0 -65
  53. package/templates/storefront/vite.config.ts +0 -8
package/dist/mock/cli.mjs CHANGED
@@ -3618,6 +3618,178 @@ function buildElements(deps) {
3618
3618
  return app;
3619
3619
  }
3620
3620
 
3621
+ // src/mock/routes/images.ts
3622
+ var SLUG_KEYWORDS = {
3623
+ // ─── Akua's Bakery (default seed) ───
3624
+ sourdough: "sourdough,bread",
3625
+ wheat: "whole-wheat,bread",
3626
+ baguette: "baguette",
3627
+ focaccia: "focaccia,bread",
3628
+ croissant: "croissant",
3629
+ pac: "pain-au-chocolat,croissant",
3630
+ danish: "danish,pastry",
3631
+ cinnamon: "cinnamon-roll",
3632
+ "choc-cake": "chocolate-cake,slice",
3633
+ "red-velvet": "red-velvet-cake",
3634
+ carrot: "carrot-cake",
3635
+ cheesecake: "cheesecake",
3636
+ coffee: "coffee,cup",
3637
+ tea: "tea,cup",
3638
+ sobolo: "hibiscus-drink",
3639
+ oj: "orange-juice,glass",
3640
+ banana: "banana-bread",
3641
+ brownie: "brownie",
3642
+ macaron: "macaron,box",
3643
+ birthday: "birthday-cake",
3644
+ "breakfast-box": "breakfast",
3645
+ "pastry-box": "pastry,assortment",
3646
+ // ─── Mama's Kitchen (restaurant seed) ───
3647
+ kelewele: "fried-plantain,spicy",
3648
+ "spring-rolls": "spring-rolls",
3649
+ "soup-of-the-day": "soup,bowl",
3650
+ "jollof-rice": "jollof-rice",
3651
+ "banku-tilapia": "tilapia,grilled-fish",
3652
+ "fufu-light-soup": "fufu,soup",
3653
+ waakye: "rice,beans,african-food",
3654
+ "grilled-chicken": "grilled-chicken",
3655
+ burger: "burger",
3656
+ "yam-fries": "yam-fries,chips",
3657
+ "fried-plantain": "fried-plantain",
3658
+ salad: "garden-salad",
3659
+ "ice-cream": "ice-cream,scoop",
3660
+ "cake-slice": "chocolate-cake,slice",
3661
+ "fruit-bowl": "fruit-bowl,tropical",
3662
+ "soft-drink": "cola,glass",
3663
+ "fresh-juice": "fresh-juice",
3664
+ water: "water-bottle",
3665
+ feast: "family-meal,buffet",
3666
+ byo: "buffet,plate",
3667
+ // ─── Currents Electronics ───
3668
+ "android-flagship": "smartphone,android",
3669
+ "android-mid": "smartphone",
3670
+ "phone-budget": "phone",
3671
+ "laptop-pro": "laptop,macbook",
3672
+ "laptop-air": "laptop,thin",
3673
+ "laptop-budget": "laptop",
3674
+ "headphones-anc": "headphones",
3675
+ "earbuds-pro": "earbuds,wireless",
3676
+ "speaker-bt": "speaker,bluetooth",
3677
+ "phone-case": "phone-case",
3678
+ "screen-protector": "screen,protector",
3679
+ "fast-charger": "charger,usb",
3680
+ "laptop-sleeve": "laptop-sleeve,case",
3681
+ "wireless-mouse": "wireless-mouse",
3682
+ "mech-keyboard": "mechanical-keyboard",
3683
+ "gaming-controller": "game-controller",
3684
+ "gaming-headset": "gaming-headset",
3685
+ "console-handheld": "handheld-console,gaming",
3686
+ wfh: "desk-setup,laptop",
3687
+ "game-pack": "gaming,console",
3688
+ // ─── Reesa apparel ───
3689
+ "ankara-shirt": "ankara,african-fashion",
3690
+ "agbada-set": "agbada,nigerian-fashion",
3691
+ kaftan: "kaftan,african-dress",
3692
+ "leather-tote": "leather-tote,bag",
3693
+ sandals: "sandals,leather",
3694
+ "beaded-bracelet": "beaded-bracelet",
3695
+ owambe: "african-fashion",
3696
+ // ─── FreshMart ───
3697
+ "tomatoes-1kg": "tomatoes",
3698
+ "onions-1kg": "onions",
3699
+ "ginger-200g": "ginger,root",
3700
+ "bananas-bunch": "bananas",
3701
+ pineapple: "pineapple",
3702
+ "spinach-bunch": "spinach,leaves",
3703
+ "milk-1l": "milk,carton",
3704
+ "yogurt-500g": "yogurt",
3705
+ "eggs-12": "eggs,carton",
3706
+ "cheese-200g": "cheese",
3707
+ "chicken-1kg": "raw-chicken",
3708
+ "beef-mince-500g": "beef,mince",
3709
+ "tilapia-each": "fish,tilapia",
3710
+ "rice-5kg": "rice,bag",
3711
+ "garri-2kg": "rice,grain",
3712
+ "palm-oil-1l": "oil,bottle",
3713
+ "vegetable-oil-1l": "oil,bottle",
3714
+ "salt-1kg": "salt,seasoning",
3715
+ "sugar-1kg": "sugar,white",
3716
+ "tomato-paste": "tomato-paste,can",
3717
+ "soap-bar": "soap,bar",
3718
+ "detergent-1kg": "detergent,laundry",
3719
+ "tissue-pack": "tissue,paper",
3720
+ "trash-bags": "trash-bag",
3721
+ essentials: "groceries,box",
3722
+ // ─── Serene Spa ───
3723
+ "swedish-massage-60": "massage,relaxation",
3724
+ "swedish-massage-90": "massage,relaxation",
3725
+ "deep-tissue-60": "massage,deep-tissue",
3726
+ "couples-massage": "couples-massage",
3727
+ "hot-stone-90": "hot-stone,massage",
3728
+ "facial-classic": "facial,spa",
3729
+ "facial-anti-aging": "facial,skincare",
3730
+ "facial-acne": "facial,skincare",
3731
+ manicure: "manicure,nails",
3732
+ pedicure: "pedicure,feet",
3733
+ "gel-manicure": "manicure,gel-nails",
3734
+ haircut: "haircut,salon",
3735
+ "hair-treatment": "hair-treatment,salon",
3736
+ color: "hair-color,salon",
3737
+ "yoga-session": "yoga,studio",
3738
+ meditation: "meditation,zen",
3739
+ "spa-day": "spa,wellness",
3740
+ "couples-retreat": "spa,couple",
3741
+ "custom-experience": "spa,wellness"
3742
+ };
3743
+ var NO_CACHE = {
3744
+ "Cache-Control": "no-cache, no-store, must-revalidate"
3745
+ };
3746
+ function lockFromSlug(slug) {
3747
+ let hash = 0;
3748
+ for (let i = 0; i < slug.length; i++) hash = hash * 31 + slug.charCodeAt(i) >>> 0;
3749
+ return hash % 9999 + 1;
3750
+ }
3751
+ function svgFallback(slug) {
3752
+ const label = slug.replace(/[-_]+/g, " ").trim() || "product";
3753
+ const display = label.length > 20 ? label.slice(0, 20) + "\u2026" : label;
3754
+ let hash = 0;
3755
+ for (let i = 0; i < slug.length; i++) hash = hash * 31 + slug.charCodeAt(i) >>> 0;
3756
+ const hue = hash % 360;
3757
+ const safe = display.replace(/[<>&]/g, "");
3758
+ const svg = `<?xml version="1.0" encoding="UTF-8"?>
3759
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
3760
+ <defs>
3761
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
3762
+ <stop offset="0%" stop-color="hsl(${hue}, 65%, 65%)"/>
3763
+ <stop offset="100%" stop-color="hsl(${(hue + 40) % 360}, 70%, 45%)"/>
3764
+ </linearGradient>
3765
+ </defs>
3766
+ <rect width="400" height="400" fill="url(#g)"/>
3767
+ <text x="200" y="210" font-family="-apple-system, BlinkMacSystemFont, Inter, sans-serif" font-size="22" font-weight="600" fill="white" text-anchor="middle" dominant-baseline="middle" style="text-transform: capitalize">${safe}</text>
3768
+ </svg>`;
3769
+ return new Response(svg, {
3770
+ headers: {
3771
+ "Content-Type": "image/svg+xml; charset=utf-8",
3772
+ ...NO_CACHE,
3773
+ "Access-Control-Allow-Origin": "*"
3774
+ }
3775
+ });
3776
+ }
3777
+ function handleImageRequest(c) {
3778
+ const path = c.req.path.replace(/^\/img\//, "").replace(/\.[a-z0-9]+$/i, "");
3779
+ const slug = path.split("/").pop() ?? "";
3780
+ for (const [k, v] of Object.entries(NO_CACHE)) c.header(k, v);
3781
+ if (!slug) return svgFallback("");
3782
+ const keyword = SLUG_KEYWORDS[slug] ?? slug.replace(/[-_]+/g, ",");
3783
+ const lock = lockFromSlug(slug);
3784
+ return c.redirect(
3785
+ `https://loremflickr.com/600/600/${encodeURIComponent(keyword)}?lock=${lock}`,
3786
+ 302
3787
+ );
3788
+ }
3789
+ function mountImageRoutes(app) {
3790
+ app.get("/img/*", handleImageRequest);
3791
+ }
3792
+
3621
3793
  // src/mock/core/clock.ts
3622
3794
  function createClock(opts) {
3623
3795
  let frozenAt = opts?.frozenAt ? opts.frozenAt.getTime() : null;
@@ -3955,51 +4127,257 @@ function percent(a, pct) {
3955
4127
  return toMajor(Math.round(toMinor(a) * pct / 100));
3956
4128
  }
3957
4129
 
3958
- // src/mock/domain/catalogue.ts
3959
- function createCatalogueService(opts) {
3960
- const { registry, clock, ids } = opts;
3961
- const productsForBusiness = (businessId) => registry.products.list((p) => p.business_id === businessId);
3962
- const defaultPriceInfo = (price, currency) => ({
4130
+ // src/mock/domain/catalogue/price-info.ts
4131
+ function defaultPriceInfo(price, currency) {
4132
+ return {
3963
4133
  base_price: price,
3964
4134
  final_price: price,
4135
+ pre_tax_price: price,
3965
4136
  markup_percentage: "0.00",
3966
4137
  markup_amount: "0.00",
3967
4138
  markup_discount_percentage: "0.00",
3968
4139
  markup_discount_amount: "0.00",
3969
- currency
3970
- });
3971
- const toApiProduct = (p) => ({
4140
+ currency,
4141
+ tax_info: {
4142
+ tax_rate: "0",
4143
+ tax_amount: "0.00",
4144
+ is_inclusive: true,
4145
+ pre_tax_price: price,
4146
+ components: []
4147
+ }
4148
+ };
4149
+ }
4150
+
4151
+ // src/mock/domain/catalogue/product-shape.ts
4152
+ function toCanonicalProductSummary(p) {
4153
+ return {
4154
+ id: p.id,
4155
+ business_id: p.business_id,
4156
+ category_id: p.category_ids[0],
4157
+ name: p.name,
4158
+ slug: p.slug,
4159
+ description: p.description,
4160
+ image_url: p.image,
4161
+ images: p.images,
4162
+ default_price: p.base_price,
4163
+ type: p.product_type,
4164
+ inventory_type: "none",
4165
+ variant_strategy: "fetch_all",
4166
+ is_active: p.is_available,
4167
+ inventory_status: { in_stock: p.is_available, stock_level: 50, low_stock: false },
4168
+ tags: p.tags ?? [],
4169
+ created_at: p.created_at,
4170
+ updated_at: p.updated_at
4171
+ };
4172
+ }
4173
+ function deriveRenderHint(p) {
4174
+ if (p.render_hint) return p.render_hint;
4175
+ if (p.product_type === "service") return "general";
4176
+ if (p.product_type === "digital") return "general";
4177
+ const foodish = (p.tags ?? []).some(
4178
+ (t) => /food|drink|cake|bread|pastry|dessert|coffee|tea|juice|spice/i.test(t)
4179
+ );
4180
+ return foodish ? "food" : "general";
4181
+ }
4182
+ function toApiProduct(p, ctx) {
4183
+ const { registry, priceInfo, groups, components, composite, bundle } = ctx;
4184
+ return {
4185
+ // ─── Canonical Product fields (SDK type: Product / ProductWithDetails) ───
3972
4186
  id: p.id,
3973
4187
  business_id: p.business_id,
4188
+ category_id: p.category_ids[0],
3974
4189
  name: p.name,
3975
4190
  slug: p.slug,
3976
4191
  description: p.description ?? "",
3977
- product_type: p.product_type,
3978
- base_price: p.base_price,
4192
+ image_url: p.image,
4193
+ images: p.images,
3979
4194
  default_price: p.base_price,
3980
- default_price_info: defaultPriceInfo(p.base_price, p.currency),
3981
- price_info: defaultPriceInfo(p.base_price, p.currency),
4195
+ type: p.product_type,
4196
+ render_hint: deriveRenderHint(p),
4197
+ inventory_type: "none",
4198
+ variant_strategy: p.variant_ids.length > 0 ? "use_axes" : "fetch_all",
4199
+ is_active: p.is_available,
4200
+ inventory_status: { in_stock: p.is_available, stock_level: 50, low_stock: false },
4201
+ tags: p.tags ?? [],
4202
+ calories: p.calories,
4203
+ allergies: p.allergies ?? [],
4204
+ display_mode: p.display_mode,
4205
+ digital_product_type: p.digital_product_type,
4206
+ quantity_pricing: p.quantity_pricing,
4207
+ duration_unit: p.duration_unit,
4208
+ duration_minutes: p.duration_minutes,
4209
+ billing_plans: p.billing_plans,
4210
+ created_at: p.created_at,
4211
+ updated_at: p.updated_at,
4212
+ metadata: {
4213
+ is_signature: p.is_signature ?? false,
4214
+ is_new: p.is_new ?? false,
4215
+ ingredients: p.ingredients ?? [],
4216
+ pairings: p.pairings ?? [],
4217
+ ...p.metadata ?? {}
4218
+ },
4219
+ // ─── Pricing breakdown (Price / ProductCustomizer) ───
4220
+ base_price: p.base_price,
4221
+ default_price_info: priceInfo,
4222
+ price_info: priceInfo,
3982
4223
  currency: p.currency,
4224
+ // ─── Compatibility aliases (older callers) ───
3983
4225
  image: p.image,
3984
- image_url: p.image,
3985
- images: p.images,
4226
+ product_type: p.product_type,
3986
4227
  is_available: p.is_available,
3987
- category_id: p.category_ids[0],
3988
4228
  category_ids: p.category_ids,
3989
4229
  collection_ids: p.collection_ids,
3990
4230
  add_on_ids: p.add_on_ids,
3991
- metadata: p.metadata ?? {},
3992
- created_at: p.created_at,
3993
- updated_at: p.updated_at,
4231
+ variant_ids: p.variant_ids,
3994
4232
  custom_attributes: [],
3995
4233
  properties: [],
3996
4234
  has_variants: p.variant_ids.length > 0,
3997
- type: p.product_type,
3998
- render_hint: "general"
3999
- });
4235
+ // ─── Embedded relations (ProductWithDetails) ───
4236
+ category: registry.categories.get(p.category_ids[0]),
4237
+ add_ons: p.add_on_ids.map((aid) => registry.addOns.get(aid)).filter(Boolean),
4238
+ variants: registry.variants.list((v) => v.product_id === p.id),
4239
+ variant_axes: registry.variantAxes.list((a) => a.product_id === p.id),
4240
+ // ─── Composite-specific (CompositeProductLayout reads product.groups) ───
4241
+ groups,
4242
+ composite_id: composite?.id,
4243
+ pricing_mode: composite?.pricing_mode,
4244
+ min_order_quantity: composite?.min_order_quantity,
4245
+ max_order_quantity: composite?.max_order_quantity,
4246
+ // ─── Bundle-specific (BundleProductLayout reads product.components) ───
4247
+ bundle_id: bundle?.id,
4248
+ bundle_pricing_type: bundle?.pricing_type,
4249
+ bundle_price: bundle?.bundle_price,
4250
+ bundle_discount_value: bundle?.discount_value,
4251
+ components
4252
+ };
4253
+ }
4254
+
4255
+ // src/mock/domain/catalogue/composite-shape.ts
4256
+ function expandCompositeGroups(registry, productId, isoNow) {
4257
+ const composite = registry.composites.list((c) => c.product_id === productId)[0];
4258
+ if (!composite) return void 0;
4259
+ return composite.groups.map((g, gIdx) => buildGroup(registry, composite, g, gIdx, isoNow));
4260
+ }
4261
+ function buildGroup(registry, composite, group, groupIdx, isoNow) {
4262
+ return {
4263
+ // ─── Canonical ComponentGroupWithComponents ───
4264
+ id: group.id,
4265
+ composite_id: composite.id,
4266
+ name: group.name,
4267
+ description: void 0,
4268
+ display_order: groupIdx,
4269
+ min_selections: group.min,
4270
+ max_selections: group.max,
4271
+ allow_quantity: true,
4272
+ max_quantity_per_component: group.max,
4273
+ pricing_behavior: "additive",
4274
+ created_at: isoNow,
4275
+ updated_at: isoNow,
4276
+ // ─── Aliases for older selectors ───
4277
+ min: group.min,
4278
+ max: group.max,
4279
+ components: group.component_ids.map((cid, idx) => ({ p: registry.products.get(cid), idx })).filter((x) => Boolean(x.p)).map(({ p, idx }) => buildComponent(composite, group, p, idx))
4280
+ };
4281
+ }
4282
+ function buildComponent(composite, group, p, idx) {
4283
+ return {
4284
+ // ─── Canonical CompositeComponent ───
4285
+ id: `comp_${composite.product_id}_${p.id}`,
4286
+ composite_id: composite.id,
4287
+ group_id: group.id,
4288
+ source_type: "product",
4289
+ source_product_id: p.id,
4290
+ product_id: p.id,
4291
+ display_name: p.name,
4292
+ display_description: p.description,
4293
+ display_image_url: p.image,
4294
+ price: p.base_price,
4295
+ calories: p.calories,
4296
+ allergens: p.allergies ?? [],
4297
+ display_order: idx,
4298
+ is_popular: idx === 0,
4299
+ is_premium: false,
4300
+ is_available: p.is_available,
4301
+ is_archived: false,
4302
+ created_at: p.created_at,
4303
+ updated_at: p.updated_at,
4304
+ // ─── Compatibility aliases ───
4305
+ name: p.name,
4306
+ description: p.description,
4307
+ image_url: p.image,
4308
+ unit_price: p.base_price,
4309
+ default_price: p.base_price,
4310
+ base_price: p.base_price,
4311
+ currency: p.currency,
4312
+ product: toCanonicalProductSummary(p)
4313
+ };
4314
+ }
4315
+
4316
+ // src/mock/domain/catalogue/bundle-shape.ts
4317
+ function expandBundleComponents(registry, productId) {
4318
+ const bundle = registry.bundles.list((b) => b.product_id === productId)[0];
4319
+ if (!bundle) return void 0;
4320
+ return bundle.product_ids.map((cid) => registry.products.get(cid)).filter((p) => Boolean(p)).map((p) => buildBundleComponent(bundle.id, productId, p));
4321
+ }
4322
+ function buildBundleComponent(bundleId, productId, p) {
4323
+ return {
4324
+ id: `bp_${productId}_${p.id}`,
4325
+ bundle_id: bundleId,
4326
+ product_id: p.id,
4327
+ // ─── Fields BundleProductLayout reads ───
4328
+ product_name: p.name,
4329
+ product_image_url: p.image,
4330
+ product_description: p.description,
4331
+ product_slug: p.slug,
4332
+ effective_price: p.base_price,
4333
+ unit_price: p.base_price,
4334
+ default_price: p.base_price,
4335
+ quantity: 1,
4336
+ allow_variant_choice: false,
4337
+ available_variants: [],
4338
+ selected_variant_id: null,
4339
+ // ─── Compatibility aliases ───
4340
+ display_name: p.name,
4341
+ display_description: p.description,
4342
+ name: p.name,
4343
+ description: p.description,
4344
+ image_url: p.image,
4345
+ currency: p.currency,
4346
+ is_available: p.is_available,
4347
+ product: toCanonicalProductSummary(p)
4348
+ };
4349
+ }
4350
+
4351
+ // src/mock/domain/catalogue/index.ts
4352
+ function createCatalogueService(opts) {
4353
+ const { registry, clock, ids } = opts;
4354
+ const productsForBusiness = (businessId) => registry.products.list((p) => p.business_id === businessId);
4000
4355
  const variantsForProduct = (productId) => registry.variants.list((v) => v.product_id === productId);
4001
4356
  const axesForProduct = (productId) => registry.variantAxes.list((a) => a.product_id === productId);
4002
4357
  const addOnLinksForProduct = (productId) => registry.productAddOns.list((l) => l.product_id === productId);
4358
+ const apiProduct = (p) => {
4359
+ const composite = p.product_type === "composite" ? registry.composites.list((c) => c.product_id === p.id)[0] : void 0;
4360
+ const bundle = p.product_type === "bundle" ? registry.bundles.list((b) => b.product_id === p.id)[0] : void 0;
4361
+ return toApiProduct(p, {
4362
+ registry,
4363
+ priceInfo: defaultPriceInfo(p.base_price, p.currency),
4364
+ groups: expandCompositeGroups(registry, p.id, clock.iso()),
4365
+ components: expandBundleComponents(registry, p.id),
4366
+ composite: composite ? {
4367
+ id: composite.id,
4368
+ pricing_mode: composite.pricing_mode,
4369
+ min_order_quantity: composite.min_order_quantity,
4370
+ max_order_quantity: composite.max_order_quantity
4371
+ } : void 0,
4372
+ bundle: bundle ? {
4373
+ id: bundle.id,
4374
+ pricing_type: bundle.pricing_type,
4375
+ bundle_price: bundle.bundle_price,
4376
+ discount_value: bundle.discount_value
4377
+ } : void 0
4378
+ });
4379
+ };
4380
+ const resolveProduct = (idOrSlug) => registry.products.get(idOrSlug) ?? registry.products.list((p) => p.slug === idOrSlug)[0];
4003
4381
  return {
4004
4382
  snapshot(businessId) {
4005
4383
  const products = productsForBusiness(businessId);
@@ -4007,7 +4385,7 @@ function createCatalogueService(opts) {
4007
4385
  const addOns = registry.addOns.list((a) => a.business_id === businessId);
4008
4386
  return {
4009
4387
  categories,
4010
- products: products.map(toApiProduct),
4388
+ products: products.map(apiProduct),
4011
4389
  add_ons: addOns,
4012
4390
  is_complete: true,
4013
4391
  total_available: products.length
@@ -4022,7 +4400,7 @@ function createCatalogueService(opts) {
4022
4400
  const start = (page - 1) * limit;
4023
4401
  const slice = items.slice(start, start + limit);
4024
4402
  return {
4025
- items: slice.map(toApiProduct),
4403
+ items: slice.map(apiProduct),
4026
4404
  is_complete: start + limit >= items.length,
4027
4405
  total_available: items.length,
4028
4406
  pagination: {
@@ -4035,9 +4413,9 @@ function createCatalogueService(opts) {
4035
4413
  };
4036
4414
  },
4037
4415
  getProduct(productId) {
4038
- const p = registry.products.get(productId);
4416
+ const p = resolveProduct(productId);
4039
4417
  if (!p) throw notFound(`Product ${productId} not found`);
4040
- return toApiProduct(p);
4418
+ return apiProduct(p);
4041
4419
  },
4042
4420
  getProductVariants(productId) {
4043
4421
  return variantsForProduct(productId);
@@ -4047,7 +4425,9 @@ function createCatalogueService(opts) {
4047
4425
  },
4048
4426
  getProductVariantById(productId, variantId) {
4049
4427
  const v = registry.variants.get(variantId);
4050
- if (!v || v.product_id !== productId) throw notFound(`Variant ${variantId} not found on product ${productId}`);
4428
+ if (!v || v.product_id !== productId) {
4429
+ throw notFound(`Variant ${variantId} not found on product ${productId}`);
4430
+ }
4051
4431
  return v;
4052
4432
  },
4053
4433
  findProductVariant(productId, properties) {
@@ -4077,13 +4457,13 @@ function createCatalogueService(opts) {
4077
4457
  return registry.categories.list((c) => c.business_id === businessId);
4078
4458
  },
4079
4459
  getCategory(id) {
4080
- const c = registry.categories.get(id);
4460
+ const c = registry.categories.get(id) ?? registry.categories.list((x) => x.slug === id)[0];
4081
4461
  if (!c) throw notFound(`Category ${id} not found`);
4082
4462
  return c;
4083
4463
  },
4084
4464
  getCategoryProducts(id) {
4085
4465
  const c = this.getCategory(id);
4086
- return c.product_ids.map((pid) => registry.products.get(pid)).filter((p) => Boolean(p)).map(toApiProduct);
4466
+ return c.product_ids.map((pid) => registry.products.get(pid)).filter((p) => Boolean(p)).map(apiProduct);
4087
4467
  },
4088
4468
  getCategoryAttributes(id) {
4089
4469
  const c = registry.categories.get(id);
@@ -4096,13 +4476,13 @@ function createCatalogueService(opts) {
4096
4476
  return registry.collections.list((c) => c.business_id === businessId);
4097
4477
  },
4098
4478
  getCollection(id) {
4099
- const c = registry.collections.get(id);
4479
+ const c = registry.collections.get(id) ?? registry.collections.list((x) => x.slug === id)[0];
4100
4480
  if (!c) throw notFound(`Collection ${id} not found`);
4101
4481
  return c;
4102
4482
  },
4103
4483
  getCollectionProducts(id) {
4104
4484
  const c = this.getCollection(id);
4105
- return c.product_ids.map((pid) => registry.products.get(pid)).filter((p) => Boolean(p)).map(toApiProduct);
4485
+ return c.product_ids.map((pid) => registry.products.get(pid)).filter((p) => Boolean(p)).map(apiProduct);
4106
4486
  },
4107
4487
  getCollectionAttributes(id) {
4108
4488
  const c = registry.collections.get(id);
@@ -4115,7 +4495,7 @@ function createCatalogueService(opts) {
4115
4495
  return registry.bundles.list((b) => b.business_id === businessId);
4116
4496
  },
4117
4497
  getBundle(id) {
4118
- const b = registry.bundles.get(id);
4498
+ const b = registry.bundles.get(id) ?? registry.bundles.list((x) => x.product_id === id || x.slug === id)[0];
4119
4499
  if (!b) throw notFound(`Bundle ${id} not found`);
4120
4500
  return b;
4121
4501
  },
@@ -4123,12 +4503,16 @@ function createCatalogueService(opts) {
4123
4503
  return registry.composites.list((c) => c.business_id === businessId);
4124
4504
  },
4125
4505
  getComposite(id) {
4126
- const c = registry.composites.get(id);
4506
+ const c = registry.composites.get(id) ?? registry.composites.list((x) => x.product_id === id)[0];
4127
4507
  if (!c) throw notFound(`Composite ${id} not found`);
4128
4508
  return c;
4129
4509
  },
4130
4510
  getCompositeByProduct(productId) {
4131
- return registry.composites.list((c) => c.product_id === productId)[0] ?? null;
4511
+ const direct = registry.composites.list((c) => c.product_id === productId)[0];
4512
+ if (direct) return direct;
4513
+ const product = registry.products.list((p) => p.slug === productId)[0];
4514
+ if (!product) return null;
4515
+ return registry.composites.list((c) => c.product_id === product.id)[0] ?? null;
4132
4516
  },
4133
4517
  calculateCompositePrice(id, body) {
4134
4518
  const c = registry.composites.get(id);
@@ -4136,14 +4520,20 @@ function createCatalogueService(opts) {
4136
4520
  const sel = body.selections ?? [];
4137
4521
  let total = c.base_price;
4138
4522
  for (const s of sel) total = add(total, money(parseFloat(s.unit_price ?? "0") * (s.quantity ?? 1)));
4139
- return { final_price: total, base_price: c.base_price, components_total: money(parseFloat(total) - parseFloat(c.base_price)) };
4523
+ return {
4524
+ final_price: total,
4525
+ base_price: c.base_price,
4526
+ components_total: money(parseFloat(total) - parseFloat(c.base_price))
4527
+ };
4140
4528
  },
4141
4529
  listTaxonomies(businessId) {
4142
4530
  return registry.taxonomies.list((t) => t.business_id === businessId);
4143
4531
  },
4144
4532
  searchTaxonomies(businessId, q) {
4145
4533
  const term = q.toLowerCase();
4146
- return registry.taxonomies.list((t) => t.business_id === businessId && t.name.toLowerCase().includes(term));
4534
+ return registry.taxonomies.list(
4535
+ (t) => t.business_id === businessId && t.name.toLowerCase().includes(term)
4536
+ );
4147
4537
  },
4148
4538
  getTaxonomy(id) {
4149
4539
  const t = registry.taxonomies.get(id);
@@ -4177,7 +4567,11 @@ function createCatalogueService(opts) {
4177
4567
  listPropertyFacets(businessId) {
4178
4568
  const products = productsForBusiness(businessId);
4179
4569
  const byCategory = /* @__PURE__ */ new Map();
4180
- for (const p of products) for (const cid of p.category_ids) byCategory.set(cid, (byCategory.get(cid) ?? 0) + 1);
4570
+ for (const p of products) {
4571
+ for (const cid of p.category_ids) {
4572
+ byCategory.set(cid, (byCategory.get(cid) ?? 0) + 1);
4573
+ }
4574
+ }
4181
4575
  return [
4182
4576
  {
4183
4577
  name: "Category",
@@ -4196,12 +4590,6 @@ function createCatalogueService(opts) {
4196
4590
  listOnSale() {
4197
4591
  return [];
4198
4592
  },
4199
- validateDiscount(code) {
4200
- if (code === "WELCOME10") {
4201
- return { valid: true, discount_id: "disc_welcome10", discount_value: "10.00", discount_type: "percentage" };
4202
- }
4203
- return { valid: false, reason: "Code not recognized" };
4204
- },
4205
4593
  getProductDeals() {
4206
4594
  return [];
4207
4595
  },
@@ -4211,15 +4599,33 @@ function createCatalogueService(opts) {
4211
4599
  getCollectionDeals() {
4212
4600
  return [];
4213
4601
  },
4602
+ validateDiscount(code) {
4603
+ if (code === "WELCOME10") {
4604
+ return { valid: true, discount_id: "disc_welcome10", discount_value: "10.00", discount_type: "percentage" };
4605
+ }
4606
+ return { valid: false, reason: "Code not recognized" };
4607
+ },
4214
4608
  createQuote(businessId, body) {
4215
4609
  const b = body;
4216
4610
  const id = ids.quote();
4611
+ const productId = b.product_id ?? "";
4612
+ const product = resolveProduct(productId);
4613
+ const price = b.total_price ?? product?.base_price ?? "0.00";
4614
+ const currency = b.currency ?? product?.currency ?? "GHS";
4615
+ const priceInfo = defaultPriceInfo(price, currency);
4217
4616
  const quote = {
4218
4617
  id,
4618
+ quote_id: id,
4219
4619
  business_id: businessId,
4220
- product_id: b.product_id ?? "",
4620
+ product_id: product?.id ?? productId,
4221
4621
  status: "fresh",
4222
- total_price: b.total_price ?? "0.00",
4622
+ total_price: price,
4623
+ currency,
4624
+ unit_price_info: priceInfo,
4625
+ final_price_info: priceInfo,
4626
+ quoted_total_price_info: priceInfo,
4627
+ components: [],
4628
+ breakdown: { base_price: price, components_total: "0.00", final_price: price },
4223
4629
  expires_at: new Date(clock.nowMs() + 10 * 60 * 1e3).toISOString(),
4224
4630
  created_at: clock.iso()
4225
4631
  };
@@ -4235,7 +4641,11 @@ function createCatalogueService(opts) {
4235
4641
  refreshQuote(id) {
4236
4642
  const q = registry.quotes.get(id);
4237
4643
  if (!q) throw notFound(`Quote ${id} not found`);
4238
- const refreshed = { ...q, status: "fresh", expires_at: new Date(clock.nowMs() + 10 * 60 * 1e3).toISOString() };
4644
+ const refreshed = {
4645
+ ...q,
4646
+ status: "fresh",
4647
+ expires_at: new Date(clock.nowMs() + 10 * 60 * 1e3).toISOString()
4648
+ };
4239
4649
  registry.quotes.put(id, refreshed);
4240
4650
  return refreshed;
4241
4651
  }
@@ -5260,67 +5670,99 @@ function createLiteService(opts) {
5260
5670
 
5261
5671
  // src/mock/seeds/default.ts
5262
5672
  var ISO = "2026-01-01T00:00:00.000Z";
5263
- var PRODUCTS = [
5264
- { name: "Sourdough Loaf", slug: "sourdough-loaf", price: "25.00", category: "breads", description: "Artisanal 24-hour fermented sourdough.", image: "/img/sourdough.jpg" },
5265
- { name: "Whole Wheat Bread", slug: "whole-wheat", price: "20.00", category: "breads", description: "Hearty whole wheat with seeds.", image: "/img/wheat.jpg" },
5266
- { name: "Baguette", slug: "baguette", price: "15.00", category: "breads", description: "Crispy crust, soft inside.", image: "/img/baguette.jpg" },
5267
- { name: "Focaccia", slug: "focaccia", price: "30.00", category: "breads", description: "Olive oil and rosemary focaccia.", image: "/img/focaccia.jpg" },
5268
- { name: "Croissant", slug: "croissant", price: "12.00", category: "pastries", description: "Buttery French croissant.", image: "/img/croissant.jpg" },
5269
- { name: "Pain au Chocolat", slug: "pain-au-chocolat", price: "14.00", category: "pastries", description: "Croissant with dark chocolate.", image: "/img/pac.jpg" },
5270
- { name: "Almond Danish", slug: "almond-danish", price: "16.00", category: "pastries", description: "Flaky pastry with almond cream.", image: "/img/danish.jpg" },
5271
- { name: "Cinnamon Roll", slug: "cinnamon-roll", price: "13.00", category: "pastries", description: "Soft roll with cinnamon glaze.", image: "/img/cinnamon.jpg" },
5272
- { name: "Chocolate Cake (Slice)", slug: "chocolate-cake", price: "22.00", category: "cakes", description: "Rich dark chocolate cake.", image: "/img/choc-cake.jpg" },
5273
- { name: "Red Velvet (Slice)", slug: "red-velvet", price: "22.00", category: "cakes", description: "Classic red velvet with cream cheese.", image: "/img/red-velvet.jpg" },
5274
- { name: "Carrot Cake (Slice)", slug: "carrot-cake", price: "20.00", category: "cakes", description: "Spiced carrot cake with walnut.", image: "/img/carrot.jpg" },
5275
- { name: "Cheesecake (Slice)", slug: "cheesecake", price: "24.00", category: "cakes", description: "New York style cheesecake.", image: "/img/cheesecake.jpg" },
5276
- { name: "Coffee", slug: "coffee", price: "10.00", category: "drinks", description: "Locally roasted Arabica.", image: "/img/coffee.jpg" },
5277
- { name: "Tea", slug: "tea", price: "8.00", category: "drinks", description: "Black, green, or herbal.", image: "/img/tea.jpg" },
5278
- { name: "Hibiscus Tea (Sobolo)", slug: "sobolo", price: "12.00", category: "drinks", description: "Iced hibiscus drink.", image: "/img/sobolo.jpg" },
5279
- { name: "Fresh Orange Juice", slug: "orange-juice", price: "15.00", category: "drinks", description: "Cold-pressed orange juice.", image: "/img/oj.jpg" },
5280
- { name: "Banana Bread", slug: "banana-bread", price: "18.00", category: "breads", description: "Moist banana loaf.", image: "/img/banana.jpg" },
5281
- { name: "Brownie", slug: "brownie", price: "12.00", category: "pastries", description: "Fudgy chocolate brownie.", image: "/img/brownie.jpg" },
5282
- { name: "Macaron Box (6)", slug: "macaron-box", price: "60.00", category: "pastries", description: "Six assorted macarons.", image: "/img/macaron.jpg" },
5283
- { name: "Custom Birthday Cake", slug: "birthday-cake", price: "250.00", category: "cakes", description: "Made-to-order. Min 48h notice.", image: "/img/birthday.jpg" }
5284
- ];
5285
- var CATEGORIES = [
5286
- { slug: "breads", name: "Breads", description: "Sourdough, baguettes, focaccia." },
5287
- { slug: "pastries", name: "Pastries", description: "Croissants, danishes, brownies." },
5288
- { slug: "cakes", name: "Cakes", description: "Cake slices and custom orders." },
5289
- { slug: "drinks", name: "Drinks", description: "Coffee, tea, juices." }
5290
- ];
5673
+ var BUSINESS_ID = "bus_default_akua_bakery";
5674
+ var img = (id) => `https://images.unsplash.com/photo-${id}?w=800&h=800&fit=crop&auto=format&q=80`;
5675
+ function makeProduct(input, categoryIds) {
5676
+ const catId = categoryIds.get(input.category);
5677
+ return {
5678
+ id: input.id,
5679
+ business_id: BUSINESS_ID,
5680
+ name: input.name,
5681
+ slug: input.slug,
5682
+ description: input.description,
5683
+ product_type: input.productType ?? "product",
5684
+ base_price: input.price,
5685
+ currency: "GHS",
5686
+ image: input.imageUrl,
5687
+ images: input.images ?? [input.imageUrl],
5688
+ is_available: true,
5689
+ category_ids: [catId],
5690
+ collection_ids: [],
5691
+ add_on_ids: [],
5692
+ variant_ids: [],
5693
+ tags: input.tags ?? [],
5694
+ calories: input.calories,
5695
+ allergies: input.allergies ?? [],
5696
+ ingredients: input.ingredients ?? [],
5697
+ pairings: input.pairings ?? [],
5698
+ is_signature: input.isSignature ?? false,
5699
+ is_new: input.isNew ?? false,
5700
+ render_hint: input.renderHint,
5701
+ display_mode: input.displayMode,
5702
+ digital_product_type: input.digitalProductType,
5703
+ quantity_pricing: input.quantityPricing,
5704
+ duration_unit: input.durationUnit,
5705
+ duration_minutes: input.durationMinutes,
5706
+ billing_plans: input.billingPlans?.map((b) => ({
5707
+ id: b.id,
5708
+ name: b.name,
5709
+ interval: b.interval,
5710
+ interval_count: b.interval_count,
5711
+ price: b.price,
5712
+ currency: b.currency ?? "GHS",
5713
+ trial_period_days: b.trial_period_days
5714
+ })),
5715
+ created_at: ISO,
5716
+ updated_at: ISO
5717
+ };
5718
+ }
5291
5719
  function seedDefault(registry) {
5292
- const businessId = "bus_default_akua_bakery";
5293
5720
  const business = {
5294
- id: businessId,
5721
+ id: BUSINESS_ID,
5295
5722
  name: "Akua's Bakery",
5296
5723
  handle: "akua",
5297
5724
  business_type: "eatery",
5298
5725
  email: "hello@akuasbakery.test",
5299
5726
  default_currency: "GHS",
5300
- default_phone: "+233244000000",
5727
+ default_phone: "+233 244 000 000",
5301
5728
  default_address: "12 Independence Ave, Accra",
5302
5729
  default_offers_table_service: true,
5303
5730
  default_accepts_online_orders: true,
5304
- image: "/img/akua-logo.png",
5731
+ image: img("1568254183919-78a4f43a2877"),
5305
5732
  status: "active",
5306
5733
  created_at: ISO,
5307
5734
  updated_at: ISO,
5308
5735
  created_by: "seed",
5309
- preferences: { theme: "warm", checkout: { allow_guest: true } },
5736
+ preferences: {
5737
+ tagline: "Fresh from the oven, every day",
5738
+ description: "Family-run bakery in East Legon. Sourdough, croissants, layered cakes \u2014 handcrafted since 2018, delivered across Accra.",
5739
+ instagram: "https://instagram.com/akuasbakery",
5740
+ theme: "warm",
5741
+ checkout: { allow_guest: true }
5742
+ },
5310
5743
  is_online_only: false,
5311
5744
  enabled_payment_types: ["mobile_money", "card", "cash"],
5312
5745
  default_location_settings: {},
5313
5746
  country_code: "GH",
5314
5747
  timezone: "Africa/Accra"
5315
5748
  };
5316
- registry.businesses.put(businessId, business);
5317
- const categoryIdsBySlug = /* @__PURE__ */ new Map();
5318
- for (const c of CATEGORIES) {
5749
+ registry.businesses.put(BUSINESS_ID, business);
5750
+ const categoryDefs = [
5751
+ { slug: "breads", name: "Breads", description: "Naturally leavened sourdough, baguettes, focaccia." },
5752
+ { slug: "pastries", name: "Pastries", description: "Hand-laminated croissants, danishes, brownies." },
5753
+ { slug: "cakes", name: "Cakes", description: "Slices and made-to-order showstoppers." },
5754
+ { slug: "drinks", name: "Drinks", description: "Locally roasted coffee, brewed teas, fresh juice." },
5755
+ { slug: "boxes", name: "Boxes & Bundles", description: "Curated boxes for sharing." },
5756
+ { slug: "merch", name: "Merch & More", description: "Branded goods, wholesale flour, downloadable recipes, gift cards." },
5757
+ { slug: "classes", name: "Classes & Stays", description: "Hands-on baking classes, kitchen rentals, monthly bread subscriptions." }
5758
+ ];
5759
+ const catIds = /* @__PURE__ */ new Map();
5760
+ for (const c of categoryDefs) {
5319
5761
  const id = `cat_${c.slug}`;
5320
- categoryIdsBySlug.set(c.slug, id);
5762
+ catIds.set(c.slug, id);
5321
5763
  const cat = {
5322
5764
  id,
5323
- business_id: businessId,
5765
+ business_id: BUSINESS_ID,
5324
5766
  name: c.name,
5325
5767
  slug: c.slug,
5326
5768
  description: c.description,
@@ -5332,99 +5774,502 @@ function seedDefault(registry) {
5332
5774
  };
5333
5775
  registry.categories.put(id, cat);
5334
5776
  }
5335
- const featured = [];
5336
- PRODUCTS.forEach((p, i) => {
5337
- const id = `prod_${p.slug}`;
5338
- const catId = categoryIdsBySlug.get(p.category);
5339
- const product = {
5340
- id,
5341
- business_id: businessId,
5342
- name: p.name,
5343
- slug: p.slug,
5344
- description: p.description,
5345
- product_type: "product",
5346
- base_price: p.price,
5347
- currency: "GHS",
5348
- image: p.image,
5349
- images: [p.image],
5350
- is_available: true,
5351
- category_ids: [catId],
5352
- collection_ids: [],
5353
- add_on_ids: [],
5354
- variant_ids: [],
5355
- created_at: ISO,
5356
- updated_at: ISO
5357
- };
5358
- registry.products.put(id, product);
5359
- const cat = registry.categories.get(catId);
5360
- cat.product_ids.push(id);
5361
- registry.categories.put(catId, cat);
5362
- if (i < 5) featured.push(id);
5363
- });
5364
- const featuredCollection = {
5777
+ const productInputs = [
5778
+ // ── Breads ──
5779
+ {
5780
+ id: "prod_sourdough-loaf",
5781
+ name: "Country Sourdough",
5782
+ slug: "sourdough-loaf",
5783
+ price: "25.00",
5784
+ description: "24-hour fermented country loaf with our heritage starter \u2014 open crumb, blistered crust, naturally tangy. Baked fresh every morning.",
5785
+ category: "breads",
5786
+ imageUrl: img("1509440159596-0249088772ff"),
5787
+ images: [img("1509440159596-0249088772ff"), img("1486887396153-fa416526c108")],
5788
+ tags: ["bread", "vegan", "signature"],
5789
+ isSignature: true,
5790
+ ingredients: ["Wheat flour", "Sourdough starter", "Sea salt", "Filtered water"],
5791
+ pairings: ["Olive oil", "Aged cheese", "Tomato jam"],
5792
+ allergies: ["wheat", "gluten"],
5793
+ calories: 240
5794
+ },
5795
+ {
5796
+ id: "prod_whole-wheat",
5797
+ name: "Whole Wheat Seed Loaf",
5798
+ slug: "whole-wheat",
5799
+ price: "20.00",
5800
+ description: "Hearty 100% whole wheat with sunflower, sesame, and pumpkin seeds. Slow-fermented for a soft crumb and nutty depth.",
5801
+ category: "breads",
5802
+ imageUrl: img("1486887396153-fa416526c108"),
5803
+ tags: ["bread", "vegan", "high-fibre"],
5804
+ ingredients: ["Whole wheat flour", "Sunflower seeds", "Sesame", "Pumpkin seeds"],
5805
+ allergies: ["wheat", "gluten", "sesame"],
5806
+ calories: 220
5807
+ },
5808
+ {
5809
+ id: "prod_baguette",
5810
+ name: "Classic Baguette",
5811
+ slug: "baguette",
5812
+ price: "15.00",
5813
+ description: "Crackling crust, airy crumb. Best eaten warm, within an hour of baking.",
5814
+ category: "breads",
5815
+ imageUrl: img("1568471173242-461f0a730452"),
5816
+ tags: ["bread", "vegan"],
5817
+ ingredients: ["Wheat flour", "Yeast", "Sea salt", "Water"],
5818
+ allergies: ["wheat", "gluten"],
5819
+ calories: 180
5820
+ },
5821
+ {
5822
+ id: "prod_focaccia",
5823
+ name: "Rosemary Focaccia",
5824
+ slug: "focaccia",
5825
+ price: "30.00",
5826
+ description: "High-hydration focaccia drenched in olive oil, scattered with sea salt and fresh rosemary. Sold whole or by the slab.",
5827
+ category: "breads",
5828
+ imageUrl: img("1571167530149-c1105da4c2c7"),
5829
+ tags: ["bread", "vegetarian"],
5830
+ ingredients: ["00 flour", "Olive oil", "Rosemary", "Maldon salt"],
5831
+ pairings: ["Burrata", "Prosciutto", "Caponata"],
5832
+ allergies: ["wheat", "gluten"],
5833
+ calories: 310
5834
+ },
5835
+ {
5836
+ id: "prod_banana-bread",
5837
+ name: "Brown Butter Banana Bread",
5838
+ slug: "banana-bread",
5839
+ price: "18.00",
5840
+ description: "Caramelised brown butter, three over-ripe bananas, walnuts, finished with a salted maple glaze.",
5841
+ category: "breads",
5842
+ imageUrl: img("1632931057819-4eefffa8e007"),
5843
+ tags: ["sweet", "comfort"],
5844
+ ingredients: ["Bananas", "Brown butter", "Walnuts", "Maple syrup"],
5845
+ allergies: ["wheat", "gluten", "dairy", "nuts", "eggs"],
5846
+ calories: 360,
5847
+ isNew: true
5848
+ },
5849
+ // ── Pastries ──
5850
+ {
5851
+ id: "prod_croissant",
5852
+ name: "Butter Croissant",
5853
+ slug: "croissant",
5854
+ price: "12.00",
5855
+ description: "27 layers of laminated dough wrapped around French butter. Crisp, shattering crust and a tender honeycomb interior.",
5856
+ category: "pastries",
5857
+ imageUrl: img("1555507036-ab1f4038808a"),
5858
+ images: [img("1555507036-ab1f4038808a"), img("1530610476181-d83430b64dcd")],
5859
+ tags: ["pastry", "vegetarian", "signature"],
5860
+ isSignature: true,
5861
+ ingredients: ["French butter", "Wheat flour", "Milk", "Eggs"],
5862
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5863
+ calories: 280
5864
+ },
5865
+ {
5866
+ id: "prod_pain-au-chocolat",
5867
+ name: "Pain au Chocolat",
5868
+ slug: "pain-au-chocolat",
5869
+ price: "14.00",
5870
+ description: "Our croissant dough wrapped around two batons of 70% Valrhona dark chocolate.",
5871
+ category: "pastries",
5872
+ imageUrl: img("1530610476181-d83430b64dcd"),
5873
+ tags: ["pastry", "chocolate"],
5874
+ ingredients: ["Croissant dough", "Valrhona chocolate"],
5875
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5876
+ calories: 320
5877
+ },
5878
+ {
5879
+ id: "prod_almond-danish",
5880
+ name: "Almond Danish",
5881
+ slug: "almond-danish",
5882
+ price: "16.00",
5883
+ description: "Flaky pastry filled with almond cream, dusted with toasted slivers and powdered sugar.",
5884
+ category: "pastries",
5885
+ imageUrl: img("1517433367423-c7e5b0f35086"),
5886
+ tags: ["pastry", "nuts"],
5887
+ ingredients: ["Pastry dough", "Almond cream", "Toasted almonds"],
5888
+ allergies: ["wheat", "gluten", "dairy", "eggs", "nuts"],
5889
+ calories: 340
5890
+ },
5891
+ {
5892
+ id: "prod_cinnamon-roll",
5893
+ name: "Cinnamon Roll",
5894
+ slug: "cinnamon-roll",
5895
+ price: "13.00",
5896
+ description: "Soft enriched dough swirled with butter and Ceylon cinnamon, finished with cream-cheese glaze.",
5897
+ category: "pastries",
5898
+ imageUrl: img("1607920591413-4ec007e70023"),
5899
+ tags: ["pastry", "comfort"],
5900
+ ingredients: ["Enriched dough", "Cinnamon", "Brown sugar", "Cream cheese"],
5901
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5902
+ calories: 380
5903
+ },
5904
+ {
5905
+ id: "prod_brownie",
5906
+ name: "Fudge Brownie",
5907
+ slug: "brownie",
5908
+ price: "12.00",
5909
+ description: "Dark, dense, fudgy brownie with a glossy shell. Chocolate four ways. Best slightly warm.",
5910
+ category: "pastries",
5911
+ imageUrl: img("1606313564200-e75d5e30476c"),
5912
+ tags: ["chocolate", "vegetarian"],
5913
+ ingredients: ["Dark chocolate", "Cocoa", "Brown butter", "Eggs"],
5914
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5915
+ calories: 420
5916
+ },
5917
+ {
5918
+ id: "prod_macaron-box",
5919
+ name: "Macaron Box (6)",
5920
+ slug: "macaron-box",
5921
+ price: "60.00",
5922
+ description: "Six French macarons \u2014 pistachio, raspberry, salted caramel, espresso, vanilla bean, and dark chocolate ganache.",
5923
+ category: "pastries",
5924
+ imageUrl: img("1558326567-98ae2405596b"),
5925
+ tags: ["pastry", "gift", "premium"],
5926
+ ingredients: ["Almond flour", "Egg whites", "Sugar", "Various fillings"],
5927
+ allergies: ["nuts", "eggs", "dairy"],
5928
+ calories: 540
5929
+ },
5930
+ // ── Cakes ──
5931
+ {
5932
+ id: "prod_chocolate-cake",
5933
+ name: "Dark Chocolate Layer Cake",
5934
+ slug: "chocolate-cake",
5935
+ price: "22.00",
5936
+ description: "Three layers of dark chocolate sponge with chocolate ganache and cr\xE8me p\xE2tissi\xE8re, finished with a mirror glaze. (Slice)",
5937
+ category: "cakes",
5938
+ imageUrl: img("1565958011703-44f9829ba187"),
5939
+ tags: ["cake", "chocolate", "signature"],
5940
+ isSignature: true,
5941
+ ingredients: ["Dark chocolate", "Cr\xE8me p\xE2tissi\xE8re", "Cocoa", "Eggs"],
5942
+ pairings: ["Espresso", "Cabernet"],
5943
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5944
+ calories: 480
5945
+ },
5946
+ {
5947
+ id: "prod_red-velvet",
5948
+ name: "Red Velvet (Slice)",
5949
+ slug: "red-velvet",
5950
+ price: "22.00",
5951
+ description: "Classic red velvet with cream cheese frosting and white chocolate shavings.",
5952
+ category: "cakes",
5953
+ imageUrl: img("1586788680434-30d324b2d46f"),
5954
+ tags: ["cake"],
5955
+ ingredients: ["Buttermilk", "Cocoa", "Cream cheese", "Beetroot"],
5956
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5957
+ calories: 460
5958
+ },
5959
+ {
5960
+ id: "prod_carrot-cake",
5961
+ name: "Carrot Cake (Slice)",
5962
+ slug: "carrot-cake",
5963
+ price: "20.00",
5964
+ description: "Spiced carrot cake with toasted walnuts, golden raisins, and cream-cheese frosting.",
5965
+ category: "cakes",
5966
+ imageUrl: img("1568827999250-3f6afff96e66"),
5967
+ tags: ["cake", "spiced"],
5968
+ ingredients: ["Carrot", "Walnuts", "Cinnamon", "Cream cheese"],
5969
+ allergies: ["wheat", "gluten", "dairy", "eggs", "nuts"],
5970
+ calories: 420
5971
+ },
5972
+ {
5973
+ id: "prod_cheesecake",
5974
+ name: "New York Cheesecake (Slice)",
5975
+ slug: "cheesecake",
5976
+ price: "24.00",
5977
+ description: "Dense, creamy New York cheesecake on a butter graham crust. Topped with seasonal berry compote.",
5978
+ category: "cakes",
5979
+ imageUrl: img("1567306226416-28f0efdc88ce"),
5980
+ tags: ["cake", "premium"],
5981
+ ingredients: ["Cream cheese", "Sour cream", "Graham crust", "Vanilla"],
5982
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5983
+ calories: 520
5984
+ },
5985
+ {
5986
+ id: "prod_birthday-cake",
5987
+ name: "Custom Celebration Cake",
5988
+ slug: "birthday-cake",
5989
+ price: "250.00",
5990
+ description: "Made-to-order three-tier cake. Choose size, flavour, and frosting. 48-hour notice required for personalisation.",
5991
+ category: "cakes",
5992
+ imageUrl: img("1535254973040-607b474cb50d"),
5993
+ images: [img("1535254973040-607b474cb50d"), img("1565958011703-44f9829ba187")],
5994
+ tags: ["cake", "made-to-order", "premium"],
5995
+ ingredients: ["Sponge cake", "Buttercream", "Custom decoration"],
5996
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5997
+ productType: "product"
5998
+ },
5999
+ // ── Drinks ──
6000
+ {
6001
+ id: "prod_coffee",
6002
+ name: "Single-Origin Coffee",
6003
+ slug: "coffee",
6004
+ price: "10.00",
6005
+ description: "Locally roasted Arabica from Volta Region. Notes of cocoa, dark fruit, and brown sugar.",
6006
+ category: "drinks",
6007
+ imageUrl: img("1495474472287-4d71bcdd2085"),
6008
+ tags: ["drink", "coffee", "vegan"],
6009
+ ingredients: ["Single-origin Arabica beans"],
6010
+ pairings: ["Croissant", "Brownie"],
6011
+ calories: 5
6012
+ },
6013
+ {
6014
+ id: "prod_tea",
6015
+ name: "Loose-Leaf Tea",
6016
+ slug: "tea",
6017
+ price: "8.00",
6018
+ description: "Choice of English breakfast, Earl Grey, green sencha, or chamomile.",
6019
+ category: "drinks",
6020
+ imageUrl: img("1576092768241-dec231879fc3"),
6021
+ tags: ["drink", "tea", "vegan"],
6022
+ calories: 0
6023
+ },
6024
+ {
6025
+ id: "prod_sobolo",
6026
+ name: "Hibiscus (Sobolo)",
6027
+ slug: "sobolo",
6028
+ price: "12.00",
6029
+ description: "Iced hibiscus tea with ginger, pineapple peel, and a hint of clove. Tart, refreshing, lightly sweet.",
6030
+ category: "drinks",
6031
+ imageUrl: img("1556679343-c7306c1976bc"),
6032
+ tags: ["drink", "vegan", "ghana"],
6033
+ isSignature: true,
6034
+ ingredients: ["Hibiscus", "Ginger", "Pineapple", "Clove"],
6035
+ calories: 60
6036
+ },
6037
+ {
6038
+ id: "prod_orange-juice",
6039
+ name: "Cold-Pressed Orange Juice",
6040
+ slug: "orange-juice",
6041
+ price: "15.00",
6042
+ description: "Hand-pressed at sunrise from Ghanaian Valencia oranges. No sugar, no concentrate.",
6043
+ category: "drinks",
6044
+ imageUrl: img("1622597467836-f3285f2131b8"),
6045
+ tags: ["drink", "vegan"],
6046
+ calories: 110
6047
+ },
6048
+ // ── Boxes (anchor products for bundle/composite) ──
6049
+ {
6050
+ id: "prod_breakfast-box",
6051
+ name: "Breakfast Box",
6052
+ slug: "breakfast-box",
6053
+ price: "30.00",
6054
+ description: "A buttery croissant, a single-origin coffee, and a fresh-pressed orange juice. Everything you need to start the day. Save GH\u20B57 vs buying separately.",
6055
+ category: "boxes",
6056
+ imageUrl: img("1533089860892-a7c6f0a88666"),
6057
+ images: [img("1533089860892-a7c6f0a88666"), img("1495474472287-4d71bcdd2085")],
6058
+ tags: ["bundle", "breakfast"],
6059
+ productType: "bundle"
6060
+ },
6061
+ {
6062
+ id: "prod_pastry-box",
6063
+ name: "Build-Your-Own Pastry Box",
6064
+ slug: "pastry-box",
6065
+ price: "60.00",
6066
+ description: "Pick 6 to 12 of our pastries \u2014 mix and match. Freshly boxed for sharing.",
6067
+ category: "boxes",
6068
+ imageUrl: img("1486427944299-d1955d23e34d"),
6069
+ tags: ["composite", "build-your-own", "gift"],
6070
+ productType: "composite"
6071
+ },
6072
+ // ── Merch (Retail card — render_hint=physical) ──
6073
+ {
6074
+ id: "prod_apron",
6075
+ name: "Akua's Bakery Apron",
6076
+ slug: "apron",
6077
+ price: "85.00",
6078
+ description: "Heavy-weight cotton-canvas apron with leather neck strap, embroidered logo, and two front pockets. Made in Accra.",
6079
+ category: "merch",
6080
+ imageUrl: img("1556905055-8f358a7a47b2"),
6081
+ tags: ["merch", "physical"],
6082
+ renderHint: "physical"
6083
+ },
6084
+ {
6085
+ id: "prod_tote",
6086
+ name: "Bakery Tote Bag",
6087
+ slug: "tote-bag",
6088
+ price: "35.00",
6089
+ description: "12oz natural canvas tote, screen-printed with our wheat-stalk crest. Roomy enough for two loaves.",
6090
+ category: "merch",
6091
+ imageUrl: img("1591561954557-26941169b49e"),
6092
+ tags: ["merch", "physical", "gift"],
6093
+ renderHint: "physical",
6094
+ isNew: true
6095
+ },
6096
+ // ── Wholesale (Wholesale card — quantity_pricing tiers) ──
6097
+ {
6098
+ id: "prod_flour-wholesale",
6099
+ name: "Heritage Flour \u2014 25kg Sack",
6100
+ slug: "flour-25kg",
6101
+ price: "320.00",
6102
+ description: "Stone-milled heritage wheat flour, the same we use in our country sourdough. Tiered pricing for caf\xE9s, restaurants, and home-baking co-ops.",
6103
+ category: "merch",
6104
+ imageUrl: img("1568254183919-78a4f43a2877"),
6105
+ tags: ["wholesale", "physical", "trade"],
6106
+ renderHint: "physical",
6107
+ quantityPricing: [
6108
+ { min_quantity: 1, unit_price: "320.00" },
6109
+ { min_quantity: 5, unit_price: "295.00" },
6110
+ { min_quantity: 10, unit_price: "270.00" },
6111
+ { min_quantity: 25, unit_price: "245.00" }
6112
+ ]
6113
+ },
6114
+ // ── Digital products (Digital card — type=digital) ──
6115
+ {
6116
+ id: "prod_recipe-ebook",
6117
+ name: "Sourdough Starter eBook",
6118
+ slug: "sourdough-ebook",
6119
+ price: "45.00",
6120
+ description: "PDF + EPUB walkthrough of our sourdough programme \u2014 starter, levain, autolyse, bulk fermentation, shaping, scoring. Lifetime updates.",
6121
+ category: "merch",
6122
+ imageUrl: img("1543002588-bfa74002ed7e"),
6123
+ tags: ["digital", "download"],
6124
+ productType: "digital",
6125
+ digitalProductType: "download"
6126
+ },
6127
+ {
6128
+ id: "prod_gift-card",
6129
+ name: "Bakery Gift Card",
6130
+ slug: "gift-card",
6131
+ price: "100.00",
6132
+ description: "Send instantly. Redeemable in-store and online toward any product. Choose any amount from GH\u20B550 upward.",
6133
+ category: "merch",
6134
+ imageUrl: img("1556905055-8f358a7a47b2"),
6135
+ tags: ["digital", "gift"],
6136
+ productType: "digital",
6137
+ digitalProductType: "gift_code",
6138
+ isSignature: true
6139
+ },
6140
+ // ── Services (Standard service card — type=service + duration_minutes) ──
6141
+ {
6142
+ id: "prod_baking-class",
6143
+ name: "Sourdough Baking Class",
6144
+ slug: "baking-class",
6145
+ price: "320.00",
6146
+ description: "Two-hour hands-on class: shape your own boule, score it, and take home a baked loaf and a starter jar. Held Saturdays, 10am\u201312pm.",
6147
+ category: "classes",
6148
+ imageUrl: img("1556909114-44e3e9399a2e"),
6149
+ tags: ["class", "service", "hands-on"],
6150
+ productType: "service",
6151
+ durationMinutes: 120
6152
+ },
6153
+ {
6154
+ id: "prod_pastry-workshop",
6155
+ name: "Lamination Workshop",
6156
+ slug: "pastry-workshop",
6157
+ price: "480.00",
6158
+ description: "Three-hour deep-dive on laminated dough. We cover croissants, danishes, kouign-amann. Aprons supplied; pastries to take home.",
6159
+ category: "classes",
6160
+ imageUrl: img("1517433367423-c7e5b0f35086"),
6161
+ tags: ["class", "service", "advanced"],
6162
+ productType: "service",
6163
+ durationMinutes: 180,
6164
+ isNew: true
6165
+ },
6166
+ // ── Rental service (Rental card — duration_unit=hours) ──
6167
+ {
6168
+ id: "prod_kitchen-rental",
6169
+ name: "Pro Kitchen Rental",
6170
+ slug: "kitchen-rental",
6171
+ price: "180.00",
6172
+ description: "Rent our commercial bakery after hours. Deck oven, retarder-proofer, mixers, scales \u2014 bring your team or shoot your content. Hourly.",
6173
+ category: "classes",
6174
+ imageUrl: img("1556909114-44e3e9399a2e"),
6175
+ tags: ["rental", "service", "commercial"],
6176
+ productType: "service",
6177
+ durationUnit: "hours"
6178
+ },
6179
+ // ── Accommodation (Accommodation card — duration_unit=nights) ──
6180
+ {
6181
+ id: "prod_loft-stay",
6182
+ name: "Bakery Loft \u2014 Weekend Stay",
6183
+ slug: "loft-stay",
6184
+ price: "650.00",
6185
+ description: "One-bedroom loft above the bakery. Wakes up to fresh croissants, ends with a private dinner. Ghana Tourism-licensed. Per night.",
6186
+ category: "classes",
6187
+ imageUrl: img("1564013799919-ab600027ffc6"),
6188
+ tags: ["stay", "service", "premium"],
6189
+ productType: "service",
6190
+ durationUnit: "nights"
6191
+ },
6192
+ // ── Subscription (Subscription card — billing_plans, no duration_minutes) ──
6193
+ {
6194
+ id: "prod_loaf-subscription",
6195
+ name: "Weekly Loaf Subscription",
6196
+ slug: "loaf-subscription",
6197
+ price: "0.00",
6198
+ description: "A fresh loaf delivered to your door every week. Pause or skip any time. Choose monthly or quarterly billing for a discount.",
6199
+ category: "classes",
6200
+ imageUrl: img("1509440159596-0249088772ff"),
6201
+ tags: ["subscription", "service", "delivery"],
6202
+ productType: "service",
6203
+ billingPlans: [
6204
+ {
6205
+ id: "plan_loaf_monthly",
6206
+ name: "Monthly",
6207
+ interval: "month",
6208
+ interval_count: 1,
6209
+ price: "90.00"
6210
+ },
6211
+ {
6212
+ id: "plan_loaf_quarterly",
6213
+ name: "Quarterly (save 10%)",
6214
+ interval: "month",
6215
+ interval_count: 3,
6216
+ price: "243.00"
6217
+ }
6218
+ ]
6219
+ }
6220
+ ];
6221
+ for (const input of productInputs) {
6222
+ const product = makeProduct(input, catIds);
6223
+ registry.products.put(product.id, product);
6224
+ const cat = registry.categories.get(catIds.get(input.category));
6225
+ if (cat) {
6226
+ cat.product_ids.push(product.id);
6227
+ registry.categories.put(cat.id, cat);
6228
+ }
6229
+ }
6230
+ const featuredIds = [
6231
+ "prod_sourdough-loaf",
6232
+ "prod_croissant",
6233
+ "prod_chocolate-cake",
6234
+ "prod_sobolo",
6235
+ "prod_pastry-box"
6236
+ ];
6237
+ const featured = {
5365
6238
  id: "col_featured",
5366
- business_id: businessId,
5367
- name: "Featured",
6239
+ business_id: BUSINESS_ID,
6240
+ name: "Signature & Featured",
5368
6241
  slug: "featured",
5369
- description: "Our most popular items.",
5370
- product_ids: featured,
6242
+ description: "Our most-loved bakes and a few seasonal stars.",
6243
+ product_ids: featuredIds,
5371
6244
  is_active: true,
5372
6245
  created_at: ISO,
5373
6246
  updated_at: ISO
5374
6247
  };
5375
- registry.collections.put(featuredCollection.id, featuredCollection);
5376
- for (const pid of featured) {
6248
+ registry.collections.put(featured.id, featured);
6249
+ for (const pid of featuredIds) {
5377
6250
  const p = registry.products.get(pid);
5378
6251
  if (p) {
5379
- p.collection_ids.push(featuredCollection.id);
6252
+ p.collection_ids.push(featured.id);
5380
6253
  registry.products.put(pid, p);
5381
6254
  }
5382
6255
  }
5383
- const tasting = {
5384
- id: "svc_tasting",
5385
- business_id: businessId,
5386
- name: "Bakery Tasting Session",
5387
- description: "Walk through our pastry menu with our head baker.",
5388
- duration_minutes: 60,
5389
- price: "150.00",
5390
- is_available: true
5391
- };
5392
- registry.services.put(tasting.id, tasting);
5393
- const cake = {
5394
- id: "svc_custom_cake",
5395
- business_id: businessId,
5396
- name: "Custom Cake Consultation",
5397
- description: "Design a custom cake for your event.",
5398
- duration_minutes: 30,
5399
- price: "0.00",
5400
- is_available: true
5401
- };
5402
- registry.services.put(cake.id, cake);
5403
- const sizeAxisId = "axis_size";
5404
- const flavourAxisId = "axis_flavour";
5405
- const sizeValues = [
5406
- { id: "axv_size_small", name: 'Small (8")' },
5407
- { id: "axv_size_medium", name: 'Medium (10")' },
5408
- { id: "axv_size_large", name: 'Large (12")' }
5409
- ];
5410
- const flavourValues = [
5411
- { id: "axv_flavour_chocolate", name: "Chocolate" },
5412
- { id: "axv_flavour_vanilla", name: "Vanilla" },
5413
- { id: "axv_flavour_red_velvet", name: "Red Velvet" }
5414
- ];
5415
6256
  const sizeAxis = {
5416
- id: sizeAxisId,
5417
- business_id: businessId,
6257
+ id: "axis_cake_size",
6258
+ business_id: BUSINESS_ID,
5418
6259
  product_id: "prod_birthday-cake",
5419
6260
  name: "Size",
5420
6261
  display_order: 0,
5421
6262
  affects_recipe: false,
5422
- values: sizeValues.map((v, i) => ({
6263
+ values: [
6264
+ { id: "axv_size_small", name: 'Small (8")', display: 0 },
6265
+ { id: "axv_size_medium", name: 'Medium (10")', display: 1 },
6266
+ { id: "axv_size_large", name: 'Large (12")', display: 2 }
6267
+ ].map((v) => ({
5423
6268
  id: v.id,
5424
- business_id: businessId,
5425
- axis_id: sizeAxisId,
6269
+ business_id: BUSINESS_ID,
6270
+ axis_id: "axis_cake_size",
5426
6271
  name: v.name,
5427
- display_order: i,
6272
+ display_order: v.display,
5428
6273
  created_at: ISO,
5429
6274
  updated_at: ISO
5430
6275
  })),
@@ -5432,18 +6277,22 @@ function seedDefault(registry) {
5432
6277
  updated_at: ISO
5433
6278
  };
5434
6279
  const flavourAxis = {
5435
- id: flavourAxisId,
5436
- business_id: businessId,
6280
+ id: "axis_cake_flavour",
6281
+ business_id: BUSINESS_ID,
5437
6282
  product_id: "prod_birthday-cake",
5438
6283
  name: "Flavour",
5439
6284
  display_order: 1,
5440
6285
  affects_recipe: true,
5441
- values: flavourValues.map((v, i) => ({
6286
+ values: [
6287
+ { id: "axv_flavour_chocolate", name: "Dark Chocolate", display: 0 },
6288
+ { id: "axv_flavour_vanilla", name: "Vanilla Bean", display: 1 },
6289
+ { id: "axv_flavour_red_velvet", name: "Red Velvet", display: 2 }
6290
+ ].map((v) => ({
5442
6291
  id: v.id,
5443
- business_id: businessId,
5444
- axis_id: flavourAxisId,
6292
+ business_id: BUSINESS_ID,
6293
+ axis_id: "axis_cake_flavour",
5445
6294
  name: v.name,
5446
- display_order: i,
6295
+ display_order: v.display,
5447
6296
  created_at: ISO,
5448
6297
  updated_at: ISO
5449
6298
  })),
@@ -5452,40 +6301,42 @@ function seedDefault(registry) {
5452
6301
  };
5453
6302
  registry.variantAxes.put(sizeAxis.id, sizeAxis);
5454
6303
  registry.variantAxes.put(flavourAxis.id, flavourAxis);
5455
- const variantPrices = ["250.00", "350.00", "450.00"];
5456
- const variantIds = [];
5457
- let isFirst = true;
5458
- for (let s = 0; s < sizeValues.length; s++) {
5459
- for (let f = 0; f < flavourValues.length; f++) {
5460
- const id = `var_${sizeValues[s].id.replace("axv_", "")}_${flavourValues[f].id.replace("axv_", "")}`;
6304
+ const sizeAdj = { small: "250.00", medium: "350.00", large: "450.00" };
6305
+ const cakeVariantIds = [];
6306
+ let firstVariant = true;
6307
+ for (const sz of sizeAxis.values) {
6308
+ for (const fl of flavourAxis.values) {
6309
+ const sizeKey = sz.id.replace("axv_size_", "");
6310
+ const flavourKey = fl.id.replace("axv_flavour_", "");
6311
+ const id = `var_size_${sizeKey}_flavour_${flavourKey}`;
5461
6312
  const variant = {
5462
6313
  id,
5463
6314
  product_id: "prod_birthday-cake",
5464
- business_id: businessId,
5465
- name: `${sizeValues[s].name} \u2022 ${flavourValues[f].name}`,
5466
- sku: `BIRTHDAY-${sizeValues[s].id.split("_").pop()}-${flavourValues[f].id.split("_").pop()}`,
5467
- price_adjustment: variantPrices[s],
6315
+ business_id: BUSINESS_ID,
6316
+ name: `${sz.name} \u2022 ${fl.name}`,
6317
+ sku: `BIRTHDAY-${sizeKey.toUpperCase()}-${flavourKey.toUpperCase()}`,
6318
+ price_adjustment: sizeAdj[sizeKey] ?? "0.00",
5468
6319
  component_multiplier: "1.00",
5469
- is_default: isFirst,
6320
+ is_default: firstVariant,
5470
6321
  is_active: true,
5471
- axis_value_ids: [sizeValues[s].id, flavourValues[f].id],
6322
+ axis_value_ids: [sz.id, fl.id],
5472
6323
  created_at: ISO,
5473
6324
  updated_at: ISO
5474
6325
  };
5475
6326
  registry.variants.put(id, variant);
5476
- variantIds.push(id);
5477
- isFirst = false;
6327
+ cakeVariantIds.push(id);
6328
+ firstVariant = false;
5478
6329
  }
5479
6330
  }
5480
- const birthday = registry.products.get("prod_birthday-cake");
5481
- if (birthday) {
5482
- birthday.variant_ids = variantIds;
5483
- registry.products.put(birthday.id, birthday);
6331
+ const birthdayProduct = registry.products.get("prod_birthday-cake");
6332
+ if (birthdayProduct) {
6333
+ birthdayProduct.variant_ids = cakeVariantIds;
6334
+ registry.products.put(birthdayProduct.id, birthdayProduct);
5484
6335
  }
5485
6336
  const toppingsAddOn = {
5486
6337
  id: "addon_toppings",
5487
- business_id: businessId,
5488
- name: "Toppings",
6338
+ business_id: BUSINESS_ID,
6339
+ name: "Cake toppings",
5489
6340
  is_multiple_allowed: true,
5490
6341
  is_required: false,
5491
6342
  is_mutually_exclusive: false,
@@ -5494,15 +6345,14 @@ function seedDefault(registry) {
5494
6345
  created_at: ISO,
5495
6346
  updated_at: ISO,
5496
6347
  options: [
5497
- { id: "addopt_strawberry", add_on_id: "addon_toppings", business_id: businessId, name: "Fresh strawberries", default_price: "8.00", is_required: false, is_mutually_exclusive: false, created_at: ISO, updated_at: ISO },
5498
- { id: "addopt_choc_chips", add_on_id: "addon_toppings", business_id: businessId, name: "Chocolate chips", default_price: "5.00", is_required: false, is_mutually_exclusive: false, created_at: ISO, updated_at: ISO },
5499
- { id: "addopt_walnuts", add_on_id: "addon_toppings", business_id: businessId, name: "Walnuts", default_price: "6.00", is_required: false, is_mutually_exclusive: false, created_at: ISO, updated_at: ISO }
6348
+ { id: "addopt_strawberry", add_on_id: "addon_toppings", business_id: BUSINESS_ID, name: "Fresh strawberries", default_price: "8.00", is_required: false, is_mutually_exclusive: false, created_at: ISO, updated_at: ISO },
6349
+ { id: "addopt_choc_chips", add_on_id: "addon_toppings", business_id: BUSINESS_ID, name: "Dark chocolate shards", default_price: "5.00", is_required: false, is_mutually_exclusive: false, created_at: ISO, updated_at: ISO },
6350
+ { id: "addopt_walnuts", add_on_id: "addon_toppings", business_id: BUSINESS_ID, name: "Toasted walnuts", default_price: "6.00", is_required: false, is_mutually_exclusive: false, created_at: ISO, updated_at: ISO }
5500
6351
  ]
5501
6352
  };
5502
- registry.addOns.put(toppingsAddOn.id, toppingsAddOn);
5503
6353
  const milkAddOn = {
5504
6354
  id: "addon_milk",
5505
- business_id: businessId,
6355
+ business_id: BUSINESS_ID,
5506
6356
  name: "Milk",
5507
6357
  is_multiple_allowed: false,
5508
6358
  is_required: false,
@@ -5512,17 +6362,17 @@ function seedDefault(registry) {
5512
6362
  created_at: ISO,
5513
6363
  updated_at: ISO,
5514
6364
  options: [
5515
- { id: "addopt_whole", add_on_id: "addon_milk", business_id: businessId, name: "Whole milk", default_price: "0.00", is_required: false, is_mutually_exclusive: true, created_at: ISO, updated_at: ISO },
5516
- { id: "addopt_oat", add_on_id: "addon_milk", business_id: businessId, name: "Oat milk", default_price: "3.00", is_required: false, is_mutually_exclusive: true, created_at: ISO, updated_at: ISO },
5517
- { id: "addopt_almond", add_on_id: "addon_milk", business_id: businessId, name: "Almond milk", default_price: "3.00", is_required: false, is_mutually_exclusive: true, created_at: ISO, updated_at: ISO }
6365
+ { id: "addopt_whole", add_on_id: "addon_milk", business_id: BUSINESS_ID, name: "Whole milk", default_price: "0.00", is_required: false, is_mutually_exclusive: true, created_at: ISO, updated_at: ISO },
6366
+ { id: "addopt_oat", add_on_id: "addon_milk", business_id: BUSINESS_ID, name: "Oat milk", default_price: "3.00", is_required: false, is_mutually_exclusive: true, created_at: ISO, updated_at: ISO },
6367
+ { id: "addopt_almond", add_on_id: "addon_milk", business_id: BUSINESS_ID, name: "Almond milk", default_price: "3.00", is_required: false, is_mutually_exclusive: true, created_at: ISO, updated_at: ISO }
5518
6368
  ]
5519
6369
  };
6370
+ registry.addOns.put(toppingsAddOn.id, toppingsAddOn);
5520
6371
  registry.addOns.put(milkAddOn.id, milkAddOn);
5521
6372
  const cakeProductIds = ["prod_chocolate-cake", "prod_red-velvet", "prod_carrot-cake", "prod_cheesecake", "prod_birthday-cake"];
5522
6373
  const drinkProductIds = ["prod_coffee", "prod_tea"];
5523
6374
  for (const pid of cakeProductIds) {
5524
- const link = { product_id: pid, add_on_id: toppingsAddOn.id };
5525
- registry.productAddOns.put(`${pid}:${toppingsAddOn.id}`, link);
6375
+ registry.productAddOns.put(`${pid}:${toppingsAddOn.id}`, { product_id: pid, add_on_id: toppingsAddOn.id });
5526
6376
  const p = registry.products.get(pid);
5527
6377
  if (p) {
5528
6378
  p.add_on_ids.push(toppingsAddOn.id);
@@ -5530,8 +6380,7 @@ function seedDefault(registry) {
5530
6380
  }
5531
6381
  }
5532
6382
  for (const pid of drinkProductIds) {
5533
- const link = { product_id: pid, add_on_id: milkAddOn.id };
5534
- registry.productAddOns.put(`${pid}:${milkAddOn.id}`, link);
6383
+ registry.productAddOns.put(`${pid}:${milkAddOn.id}`, { product_id: pid, add_on_id: milkAddOn.id });
5535
6384
  const p = registry.products.get(pid);
5536
6385
  if (p) {
5537
6386
  p.add_on_ids.push(milkAddOn.id);
@@ -5540,7 +6389,7 @@ function seedDefault(registry) {
5540
6389
  }
5541
6390
  const breakfastBundle = {
5542
6391
  id: "bnd_breakfast_box",
5543
- business_id: businessId,
6392
+ business_id: BUSINESS_ID,
5544
6393
  product_id: "prod_breakfast-box",
5545
6394
  name: "Breakfast Box",
5546
6395
  slug: "breakfast-box",
@@ -5552,29 +6401,9 @@ function seedDefault(registry) {
5552
6401
  updated_at: ISO
5553
6402
  };
5554
6403
  registry.bundles.put(breakfastBundle.id, breakfastBundle);
5555
- const breakfastProduct = {
5556
- id: "prod_breakfast-box",
5557
- business_id: businessId,
5558
- name: "Breakfast Box",
5559
- slug: "breakfast-box",
5560
- description: "Bundled breakfast for one.",
5561
- product_type: "bundle",
5562
- base_price: "30.00",
5563
- currency: "GHS",
5564
- image: "/img/breakfast-box.jpg",
5565
- images: ["/img/breakfast-box.jpg"],
5566
- is_available: true,
5567
- category_ids: [categoryIdsBySlug.get("pastries")],
5568
- collection_ids: [],
5569
- add_on_ids: [],
5570
- variant_ids: [],
5571
- created_at: ISO,
5572
- updated_at: ISO
5573
- };
5574
- registry.products.put(breakfastProduct.id, breakfastProduct);
5575
6404
  const pastryComposite = {
5576
6405
  id: "comp_pastry_box",
5577
- business_id: businessId,
6406
+ business_id: BUSINESS_ID,
5578
6407
  product_id: "prod_pastry-box",
5579
6408
  base_price: "60.00",
5580
6409
  pricing_mode: "additive",
@@ -5586,43 +6415,51 @@ function seedDefault(registry) {
5586
6415
  name: "Choose your pastries",
5587
6416
  min: 6,
5588
6417
  max: 12,
5589
- component_ids: ["prod_croissant", "prod_pain-au-chocolat", "prod_almond-danish", "prod_cinnamon-roll", "prod_brownie"]
6418
+ component_ids: [
6419
+ "prod_croissant",
6420
+ "prod_pain-au-chocolat",
6421
+ "prod_almond-danish",
6422
+ "prod_cinnamon-roll",
6423
+ "prod_brownie",
6424
+ "prod_banana-bread",
6425
+ "prod_macaron-box"
6426
+ ]
5590
6427
  }
5591
6428
  ],
5592
6429
  created_at: ISO,
5593
6430
  updated_at: ISO
5594
6431
  };
5595
6432
  registry.composites.put(pastryComposite.id, pastryComposite);
5596
- const pastryBoxProduct = {
5597
- id: "prod_pastry-box",
5598
- business_id: businessId,
5599
- name: "Build-Your-Own Pastry Box",
5600
- slug: "pastry-box",
5601
- description: "Pick 6 to 12 pastries \u2014 mix and match.",
5602
- product_type: "composite",
5603
- base_price: "60.00",
5604
- currency: "GHS",
5605
- image: "/img/pastry-box.jpg",
5606
- images: ["/img/pastry-box.jpg"],
5607
- is_available: true,
5608
- category_ids: [categoryIdsBySlug.get("pastries")],
5609
- collection_ids: [],
5610
- add_on_ids: [],
5611
- variant_ids: [],
5612
- created_at: ISO,
5613
- updated_at: ISO
6433
+ const tasting = {
6434
+ id: "svc_tasting",
6435
+ business_id: BUSINESS_ID,
6436
+ name: "Bakery Tasting Session",
6437
+ description: "Sample our pastry menu with our head baker.",
6438
+ duration_minutes: 60,
6439
+ price: "150.00",
6440
+ is_available: true
5614
6441
  };
5615
- registry.products.put(pastryBoxProduct.id, pastryBoxProduct);
6442
+ const consult = {
6443
+ id: "svc_custom_cake",
6444
+ business_id: BUSINESS_ID,
6445
+ name: "Custom Cake Consultation",
6446
+ description: "Design a custom cake for your event.",
6447
+ duration_minutes: 30,
6448
+ price: "0.00",
6449
+ is_available: true
6450
+ };
6451
+ registry.services.put(tasting.id, tasting);
6452
+ registry.services.put(consult.id, consult);
5616
6453
  const tags = [
5617
- { id: "tag_signature", business_id: businessId, name: "Signature", slug: "signature", color: "#d97706", sort_order: 0, usage_count: 5, created_at: ISO, updated_at: ISO },
5618
- { id: "tag_vegan", business_id: businessId, name: "Vegan", slug: "vegan", color: "#16a34a", sort_order: 1, usage_count: 3, created_at: ISO, updated_at: ISO },
5619
- { id: "tag_gluten_free", business_id: businessId, name: "Gluten-Free", slug: "gluten-free", color: "#2563eb", sort_order: 2, usage_count: 2, created_at: ISO, updated_at: ISO },
5620
- { id: "tag_seasonal", business_id: businessId, name: "Seasonal", slug: "seasonal", color: "#dc2626", sort_order: 3, usage_count: 4, created_at: ISO, updated_at: ISO }
6454
+ { id: "tag_signature", business_id: BUSINESS_ID, name: "Signature", slug: "signature", color: "#d97706", sort_order: 0, usage_count: 5, created_at: ISO, updated_at: ISO },
6455
+ { id: "tag_vegan", business_id: BUSINESS_ID, name: "Vegan", slug: "vegan", color: "#16a34a", sort_order: 1, usage_count: 4, created_at: ISO, updated_at: ISO },
6456
+ { id: "tag_gluten_free", business_id: BUSINESS_ID, name: "Gluten-Free", slug: "gluten-free", color: "#2563eb", sort_order: 2, usage_count: 0, created_at: ISO, updated_at: ISO },
6457
+ { id: "tag_seasonal", business_id: BUSINESS_ID, name: "Seasonal", slug: "seasonal", color: "#dc2626", sort_order: 3, usage_count: 1, created_at: ISO, updated_at: ISO }
5621
6458
  ];
5622
6459
  for (const t of tags) registry.tags.put(t.id, t);
5623
6460
  const baked = {
5624
6461
  id: "tax_baked_goods",
5625
- business_id: businessId,
6462
+ business_id: BUSINESS_ID,
5626
6463
  name: "Baked Goods",
5627
6464
  slug: "baked-goods",
5628
6465
  path: ["Food", "Baked Goods"],
@@ -5632,7 +6469,7 @@ function seedDefault(registry) {
5632
6469
  };
5633
6470
  const beverages = {
5634
6471
  id: "tax_beverages",
5635
- business_id: businessId,
6472
+ business_id: BUSINESS_ID,
5636
6473
  name: "Beverages",
5637
6474
  slug: "beverages",
5638
6475
  path: ["Food", "Beverages"],
@@ -5645,13 +6482,13 @@ function seedDefault(registry) {
5645
6482
  const attrs = [
5646
6483
  {
5647
6484
  id: "attr_allergens",
5648
- business_id: businessId,
6485
+ business_id: BUSINESS_ID,
5649
6486
  namespace: "core",
5650
6487
  name: "Allergens",
5651
6488
  slug: "allergens",
5652
6489
  description: "Common allergens present in this item.",
5653
6490
  attribute_type: "multi_select",
5654
- options: ["wheat", "eggs", "dairy", "nuts", "soy"],
6491
+ options: ["wheat", "gluten", "eggs", "dairy", "nuts", "soy", "sesame"],
5655
6492
  is_required: false,
5656
6493
  is_filterable: true,
5657
6494
  visibility: "public",
@@ -5662,7 +6499,7 @@ function seedDefault(registry) {
5662
6499
  },
5663
6500
  {
5664
6501
  id: "attr_shelf_life",
5665
- business_id: businessId,
6502
+ business_id: BUSINESS_ID,
5666
6503
  namespace: "core",
5667
6504
  name: "Shelf life",
5668
6505
  slug: "shelf-life",
@@ -5677,7 +6514,7 @@ function seedDefault(registry) {
5677
6514
  },
5678
6515
  {
5679
6516
  id: "attr_caffeine",
5680
- business_id: businessId,
6517
+ business_id: BUSINESS_ID,
5681
6518
  namespace: "core",
5682
6519
  name: "Caffeine",
5683
6520
  slug: "caffeine",
@@ -5692,7 +6529,7 @@ function seedDefault(registry) {
5692
6529
  },
5693
6530
  {
5694
6531
  id: "attr_seasonal",
5695
- business_id: businessId,
6532
+ business_id: BUSINESS_ID,
5696
6533
  namespace: "core",
5697
6534
  name: "Seasonal",
5698
6535
  slug: "seasonal",
@@ -5710,10 +6547,10 @@ function seedDefault(registry) {
5710
6547
  const articles = [
5711
6548
  {
5712
6549
  id: "kb_delivery",
5713
- business_id: businessId,
5714
- title: "Delivery & Pickup",
6550
+ business_id: BUSINESS_ID,
6551
+ title: "Delivery & pickup",
5715
6552
  slug: "delivery-pickup",
5716
- content: "Same-day delivery within Accra for orders placed before 2pm. Pickup available 8am\u20138pm.",
6553
+ content: "Same-day delivery within Greater Accra for orders placed before 2pm. Pickup available 8am\u20138pm at our East Legon location.",
5717
6554
  category: "logistics",
5718
6555
  tags: ["delivery", "pickup"],
5719
6556
  created_at: ISO,
@@ -5721,10 +6558,10 @@ function seedDefault(registry) {
5721
6558
  },
5722
6559
  {
5723
6560
  id: "kb_custom_cakes",
5724
- business_id: businessId,
5725
- title: "Ordering custom cakes",
6561
+ business_id: BUSINESS_ID,
6562
+ title: "Custom cakes & celebrations",
5726
6563
  slug: "custom-cakes",
5727
- content: "Custom cakes require 48 hours notice. Book a consultation through the bookings page.",
6564
+ content: "Custom cakes require 48 hours notice. Book a consultation through the bookings page to walk through flavours, sizes, and decoration.",
5728
6565
  category: "products",
5729
6566
  tags: ["cakes", "custom"],
5730
6567
  created_at: ISO,
@@ -5732,10 +6569,10 @@ function seedDefault(registry) {
5732
6569
  },
5733
6570
  {
5734
6571
  id: "kb_allergens",
5735
- business_id: businessId,
5736
- title: "Allergens",
6572
+ business_id: BUSINESS_ID,
6573
+ title: "Allergen information",
5737
6574
  slug: "allergens",
5738
- content: "Our kitchen handles wheat, dairy, eggs, and nuts. Cross-contamination is possible.",
6575
+ content: "Our kitchen handles wheat, dairy, eggs, and nuts. Cross-contamination is possible \u2014 let us know about severe allergies before ordering.",
5739
6576
  category: "products",
5740
6577
  tags: ["allergens", "safety"],
5741
6578
  created_at: ISO,
@@ -5743,7 +6580,7 @@ function seedDefault(registry) {
5743
6580
  }
5744
6581
  ];
5745
6582
  for (const a of articles) registry.knowledgeArticles.put(a.id, a);
5746
- return { businessId };
6583
+ return { businessId: BUSINESS_ID };
5747
6584
  }
5748
6585
 
5749
6586
  // src/mock/seeds/empty.ts
@@ -7846,195 +8683,47 @@ function createDeps(options = {}) {
7846
8683
  }
7847
8684
 
7848
8685
  // src/mock/server.ts
8686
+ var PACKAGE_NAME = "@cimplify/sdk/mock";
8687
+ function corsMiddleware(allow) {
8688
+ return async (c, next) => {
8689
+ const origin = c.req.header("origin") ?? "";
8690
+ const allowOrigin = allow === "*" ? "*" : allow.includes(origin) ? origin : allow[0] ?? "*";
8691
+ const headers = {
8692
+ "Access-Control-Allow-Origin": allowOrigin,
8693
+ "Access-Control-Allow-Credentials": "true",
8694
+ "Access-Control-Allow-Headers": c.req.header("access-control-request-headers") ?? "content-type,authorization,x-session-token,idempotency-key",
8695
+ "Access-Control-Allow-Methods": "GET,POST,PATCH,DELETE,OPTIONS",
8696
+ "Access-Control-Expose-Headers": "x-session-token",
8697
+ "Access-Control-Max-Age": "86400",
8698
+ Vary: "Origin"
8699
+ };
8700
+ if (c.req.method === "OPTIONS") {
8701
+ return new Response(null, { status: 204, headers });
8702
+ }
8703
+ for (const [k, v] of Object.entries(headers)) c.header(k, v);
8704
+ await next();
8705
+ };
8706
+ }
7849
8707
  function createMockApp(options = {}) {
7850
8708
  const deps = createDeps(options);
7851
8709
  const app = new Hono2();
7852
- const corsOrigins = options.cors === "*" ? "*" : options.cors;
7853
- if (corsOrigins) {
7854
- app.use("*", async (c, next) => {
7855
- const origin = c.req.header("origin") ?? "";
7856
- const allow = corsOrigins === "*" ? "*" : corsOrigins.includes(origin) ? origin : corsOrigins[0] ?? "*";
7857
- const headers = {
7858
- "Access-Control-Allow-Origin": allow,
7859
- "Access-Control-Allow-Credentials": "true",
7860
- "Access-Control-Allow-Headers": c.req.header("access-control-request-headers") ?? "content-type,authorization,x-session-token,idempotency-key",
7861
- "Access-Control-Allow-Methods": "GET,POST,PATCH,DELETE,OPTIONS",
7862
- "Access-Control-Expose-Headers": "x-session-token",
7863
- "Access-Control-Max-Age": "86400",
7864
- Vary: "Origin"
7865
- };
7866
- if (c.req.method === "OPTIONS") {
7867
- return new Response(null, { status: 204, headers });
7868
- }
7869
- for (const [k, v] of Object.entries(headers)) c.header(k, v);
7870
- await next();
7871
- });
7872
- }
8710
+ const cors = options.cors ?? "*";
8711
+ if (cors) app.use("*", corsMiddleware(cors));
7873
8712
  app.get(
7874
8713
  "/",
7875
8714
  (c) => c.json({
7876
- name: "@cimplify/sdk/mock",
7877
- version: "0.36.0",
8715
+ name: PACKAGE_NAME,
7878
8716
  docs: "https://github.com/cimplify/cimplify",
7879
8717
  api: "/api/v1",
7880
8718
  admin: "/_mock",
7881
8719
  elements: "/elements",
7882
- seed_business_id: deps.defaultBusinessId
8720
+ images: "/img/<slug>",
8721
+ seed_business_id: deps.defaultBusinessId,
8722
+ time: deps.clock.iso()
7883
8723
  })
7884
8724
  );
7885
8725
  app.get("/healthz", (c) => c.json({ ok: true, time: deps.clock.iso() }));
7886
- const UNSPLASH_PHOTOS = {
7887
- // Akua's Bakery (default seed)
7888
- sourdough: "1509440159596-0249088772ff",
7889
- wheat: "1486887396153-fa416526c108",
7890
- baguette: "1568471173242-461f0a730452",
7891
- focaccia: "1573472635068-fd9b1abdd8e5",
7892
- croissant: "1555507036-ab1f4038808a",
7893
- pac: "1530610476181-d83430b64dcd",
7894
- danish: "1517433367423-c7e5b0f35086",
7895
- cinnamon: "1583527976767-50068e72b7a3",
7896
- "choc-cake": "1565958011703-44f9829ba187",
7897
- "red-velvet": "1586788680434-30d324b2d46f",
7898
- carrot: "1568827999250-3f6afff96e66",
7899
- cheesecake: "1567306226416-28f0efdc88ce",
7900
- coffee: "1495474472287-4d71bcdd2085",
7901
- tea: "1576092768241-dec231879fc3",
7902
- sobolo: "1556679343-c7306c1976bc",
7903
- oj: "1600271886742-f049b969dc7a",
7904
- banana: "1632931057819-4eefffa8e007",
7905
- brownie: "1551024506-0bccd828d307",
7906
- macaron: "1558326567-98ae2405596b",
7907
- birthday: "1535254973040-607b474cb50d",
7908
- "breakfast-box": "1533089860892-a7c6f0a88666",
7909
- "pastry-box": "1486427944299-d1955d23e34d",
7910
- // Mama's Kitchen (restaurant seed)
7911
- kelewele: "1585536830049-aedf5c1b98a4",
7912
- "spring-rolls": "1606755962773-d324e0a13086",
7913
- "soup-of-the-day": "1547592180-85f173990554",
7914
- "jollof-rice": "1604329760661-e71dc83f8f26",
7915
- "banku-tilapia": "1559847844-5315695dadae",
7916
- "fufu-light-soup": "1547928576-b822bcced36e",
7917
- waakye: "1604329760661-e71dc83f8f26",
7918
- "grilled-chicken": "1532550907401-a500c9a57435",
7919
- burger: "1568901346375-23c9450c58cd",
7920
- "yam-fries": "1573080496219-bb080dd4f877",
7921
- "fried-plantain": "1604329760661-e71dc83f8f26",
7922
- salad: "1512621776951-a57141f2eefd",
7923
- "ice-cream": "1497034825429-c343d7c6a68f",
7924
- "cake-slice": "1565958011703-44f9829ba187",
7925
- "fruit-bowl": "1490474418585-ba9bad8fd0ea",
7926
- "soft-drink": "1497534446932-c925b458314e",
7927
- "fresh-juice": "1622597467836-f3285f2131b8",
7928
- water: "1548839140-29a749e1cf4d",
7929
- feast: "1546069901-ba9599a7e63c",
7930
- byo: "1546069901-ba9599a7e63c",
7931
- // Currents Electronics
7932
- "android-flagship": "1511707171634-5f897ff02aa9",
7933
- "android-mid": "1592899677977-9c10ca588bbd",
7934
- "phone-budget": "1574944985070-8f3ebc6b79d2",
7935
- "laptop-pro": "1496181133206-80ce9b88a853",
7936
- "laptop-air": "1517336714731-489689fd1ca8",
7937
- "laptop-budget": "1541807084-5c52b6b3adef",
7938
- "headphones-anc": "1583394838336-acd977736f90",
7939
- "earbuds-pro": "1606220945770-b5b6c2c55bf1",
7940
- "speaker-bt": "1608043152269-423dbba4e7e1",
7941
- "phone-case": "1601593346740-925612772716",
7942
- "screen-protector": "1592890288564-76628a30a657",
7943
- "fast-charger": "1583394838336-acd977736f90",
7944
- "laptop-sleeve": "1525547719571-a2d4ac8945e2",
7945
- "wireless-mouse": "1527864550417-7fd91fc51a46",
7946
- "mech-keyboard": "1587829741301-dc798b83add3",
7947
- "gaming-controller": "1592840496694-26d035b52b48",
7948
- "gaming-headset": "1591105327764-407a4a06e768",
7949
- "console-handheld": "1606144042614-b2417e99c4e3",
7950
- wfh: "1593642632559-0c6d3fc62b89",
7951
- "game-pack": "1606144042614-b2417e99c4e3",
7952
- // Reesa apparel
7953
- "ankara-shirt": "1591047139829-d91aecb6caea",
7954
- "agbada-set": "1605497788044-5a32c7078486",
7955
- kaftan: "1554412933-514a83d2f3c8",
7956
- "leather-tote": "1584917865442-de89df76afd3",
7957
- sandals: "1543163521-1bf539c55dd2",
7958
- "beaded-bracelet": "1611591437281-460bfbe1220a",
7959
- owambe: "1605497788044-5a32c7078486",
7960
- // FreshMart
7961
- "tomatoes-1kg": "1546470427-227f1c75a9ef",
7962
- "onions-1kg": "1620574387735-3624d75b2dbc",
7963
- "ginger-200g": "1606937330526-f5e6a2a83555",
7964
- "bananas-bunch": "1571771894821-ce9b6c11b08e",
7965
- pineapple: "1550258987-190a2d41a8ba",
7966
- "spinach-bunch": "1576045057995-568f588f82fb",
7967
- "milk-1l": "1550583724-b2692b85b150",
7968
- "yogurt-500g": "1571212515416-fef01fc43637",
7969
- "eggs-12": "1582722872445-44dc5f7e3c8f",
7970
- "cheese-200g": "1486297678162-eb2a19b0a32d",
7971
- "chicken-1kg": "1604503468506-a8da13d82791",
7972
- "beef-mince-500g": "1607623814075-e51df1bdc82f",
7973
- "tilapia-each": "1546069901-ba9599a7e63c",
7974
- "rice-5kg": "1586201375761-83865001e31c",
7975
- "garri-2kg": "1604329760661-e71dc83f8f26",
7976
- "palm-oil-1l": "1474979266404-7eaacbcd87c5",
7977
- "vegetable-oil-1l": "1474979266404-7eaacbcd87c5",
7978
- "salt-1kg": "1612531386530-97286d97c2d2",
7979
- "sugar-1kg": "1581281697996-3aef0c3b9b12",
7980
- "tomato-paste": "1546470427-227f1c75a9ef",
7981
- "soap-bar": "1583912267550-d6c2ac3196c0",
7982
- "detergent-1kg": "1583912267550-d6c2ac3196c0",
7983
- "tissue-pack": "1610557892470-55d9e80c0bce",
7984
- "trash-bags": "1583912267550-d6c2ac3196c0",
7985
- essentials: "1542838132-92c53300491e",
7986
- // Serene Spa services
7987
- "swedish-massage-60": "1544161515-4ab6ce6db874",
7988
- "swedish-massage-90": "1544161515-4ab6ce6db874",
7989
- "deep-tissue-60": "1600334129128-685c5582fd35",
7990
- "couples-massage": "1540555700478-4be289fbecef",
7991
- "hot-stone-90": "1600334129128-685c5582fd35",
7992
- "facial-classic": "1570172619644-dfd03ed5d881",
7993
- "facial-anti-aging": "1571019613454-1cb2f99b2d8b",
7994
- "facial-acne": "1571019613454-1cb2f99b2d8b",
7995
- manicure: "1604654894610-df63bc536371",
7996
- pedicure: "1610701596007-11502861dcfa",
7997
- "gel-manicure": "1604654894610-df63bc536371",
7998
- haircut: "1562322140-8baeececf3df",
7999
- "hair-treatment": "1522337360788-8b13dee7a37e",
8000
- color: "1562322140-8baeececf3df",
8001
- "yoga-session": "1545205597-3d9d02c29597",
8002
- meditation: "1545389336-cf090694435e",
8003
- "spa-day": "1540555700478-4be289fbecef",
8004
- "couples-retreat": "1540555700478-4be289fbecef",
8005
- "custom-experience": "1545389336-cf090694435e"
8006
- };
8007
- app.get("/img/*", (c) => {
8008
- const path = c.req.path.replace(/^\/img\//, "").replace(/\.[a-z0-9]+$/i, "");
8009
- const slug = path.split("/").pop() ?? "";
8010
- const photoId = UNSPLASH_PHOTOS[slug];
8011
- if (photoId) {
8012
- return c.redirect(`https://images.unsplash.com/photo-${photoId}?w=600&h=600&fit=crop&auto=format&q=80`, 302);
8013
- }
8014
- const label = slug.replace(/[-_]+/g, " ").trim() || "product";
8015
- let hash = 0;
8016
- for (let i = 0; i < slug.length; i++) hash = hash * 31 + slug.charCodeAt(i) >>> 0;
8017
- const hue = hash % 360;
8018
- const display = label.length > 20 ? label.slice(0, 20) + "\u2026" : label;
8019
- const svg = `<?xml version="1.0" encoding="UTF-8"?>
8020
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
8021
- <defs>
8022
- <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
8023
- <stop offset="0%" stop-color="hsl(${hue}, 65%, 65%)"/>
8024
- <stop offset="100%" stop-color="hsl(${(hue + 40) % 360}, 70%, 45%)"/>
8025
- </linearGradient>
8026
- </defs>
8027
- <rect width="400" height="400" fill="url(#g)"/>
8028
- <text x="200" y="210" font-family="-apple-system, BlinkMacSystemFont, Inter, sans-serif" font-size="22" font-weight="600" fill="white" text-anchor="middle" dominant-baseline="middle" style="text-transform: capitalize">${display.replace(/[<>&]/g, "")}</text>
8029
- </svg>`;
8030
- return new Response(svg, {
8031
- headers: {
8032
- "Content-Type": "image/svg+xml; charset=utf-8",
8033
- "Cache-Control": "public, max-age=3600",
8034
- "Access-Control-Allow-Origin": "*"
8035
- }
8036
- });
8037
- });
8726
+ mountImageRoutes(app);
8038
8727
  app.route("/api/v1", buildV1(deps));
8039
8728
  app.route("/_mock", buildAdmin(deps));
8040
8729
  app.route("/elements", buildElements(deps));
@@ -8042,7 +8731,11 @@ function createMockApp(options = {}) {
8042
8731
  (c) => c.json(
8043
8732
  {
8044
8733
  success: false,
8045
- error: { code: "ROUTE_NOT_REGISTERED", message: `Route ${c.req.method} ${c.req.path} not handled by mock`, retryable: false },
8734
+ error: {
8735
+ code: "ROUTE_NOT_REGISTERED",
8736
+ message: `Route ${c.req.method} ${c.req.path} not handled by mock`,
8737
+ retryable: false
8738
+ },
8046
8739
  hint: "See /api/v1 for the SDK surface and /_mock for the admin surface."
8047
8740
  },
8048
8741
  501