@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/msw.mjs CHANGED
@@ -2982,6 +2982,178 @@ function buildElements(deps) {
2982
2982
  return app;
2983
2983
  }
2984
2984
 
2985
+ // src/mock/routes/images.ts
2986
+ var SLUG_KEYWORDS = {
2987
+ // ─── Akua's Bakery (default seed) ───
2988
+ sourdough: "sourdough,bread",
2989
+ wheat: "whole-wheat,bread",
2990
+ baguette: "baguette",
2991
+ focaccia: "focaccia,bread",
2992
+ croissant: "croissant",
2993
+ pac: "pain-au-chocolat,croissant",
2994
+ danish: "danish,pastry",
2995
+ cinnamon: "cinnamon-roll",
2996
+ "choc-cake": "chocolate-cake,slice",
2997
+ "red-velvet": "red-velvet-cake",
2998
+ carrot: "carrot-cake",
2999
+ cheesecake: "cheesecake",
3000
+ coffee: "coffee,cup",
3001
+ tea: "tea,cup",
3002
+ sobolo: "hibiscus-drink",
3003
+ oj: "orange-juice,glass",
3004
+ banana: "banana-bread",
3005
+ brownie: "brownie",
3006
+ macaron: "macaron,box",
3007
+ birthday: "birthday-cake",
3008
+ "breakfast-box": "breakfast",
3009
+ "pastry-box": "pastry,assortment",
3010
+ // ─── Mama's Kitchen (restaurant seed) ───
3011
+ kelewele: "fried-plantain,spicy",
3012
+ "spring-rolls": "spring-rolls",
3013
+ "soup-of-the-day": "soup,bowl",
3014
+ "jollof-rice": "jollof-rice",
3015
+ "banku-tilapia": "tilapia,grilled-fish",
3016
+ "fufu-light-soup": "fufu,soup",
3017
+ waakye: "rice,beans,african-food",
3018
+ "grilled-chicken": "grilled-chicken",
3019
+ burger: "burger",
3020
+ "yam-fries": "yam-fries,chips",
3021
+ "fried-plantain": "fried-plantain",
3022
+ salad: "garden-salad",
3023
+ "ice-cream": "ice-cream,scoop",
3024
+ "cake-slice": "chocolate-cake,slice",
3025
+ "fruit-bowl": "fruit-bowl,tropical",
3026
+ "soft-drink": "cola,glass",
3027
+ "fresh-juice": "fresh-juice",
3028
+ water: "water-bottle",
3029
+ feast: "family-meal,buffet",
3030
+ byo: "buffet,plate",
3031
+ // ─── Currents Electronics ───
3032
+ "android-flagship": "smartphone,android",
3033
+ "android-mid": "smartphone",
3034
+ "phone-budget": "phone",
3035
+ "laptop-pro": "laptop,macbook",
3036
+ "laptop-air": "laptop,thin",
3037
+ "laptop-budget": "laptop",
3038
+ "headphones-anc": "headphones",
3039
+ "earbuds-pro": "earbuds,wireless",
3040
+ "speaker-bt": "speaker,bluetooth",
3041
+ "phone-case": "phone-case",
3042
+ "screen-protector": "screen,protector",
3043
+ "fast-charger": "charger,usb",
3044
+ "laptop-sleeve": "laptop-sleeve,case",
3045
+ "wireless-mouse": "wireless-mouse",
3046
+ "mech-keyboard": "mechanical-keyboard",
3047
+ "gaming-controller": "game-controller",
3048
+ "gaming-headset": "gaming-headset",
3049
+ "console-handheld": "handheld-console,gaming",
3050
+ wfh: "desk-setup,laptop",
3051
+ "game-pack": "gaming,console",
3052
+ // ─── Reesa apparel ───
3053
+ "ankara-shirt": "ankara,african-fashion",
3054
+ "agbada-set": "agbada,nigerian-fashion",
3055
+ kaftan: "kaftan,african-dress",
3056
+ "leather-tote": "leather-tote,bag",
3057
+ sandals: "sandals,leather",
3058
+ "beaded-bracelet": "beaded-bracelet",
3059
+ owambe: "african-fashion",
3060
+ // ─── FreshMart ───
3061
+ "tomatoes-1kg": "tomatoes",
3062
+ "onions-1kg": "onions",
3063
+ "ginger-200g": "ginger,root",
3064
+ "bananas-bunch": "bananas",
3065
+ pineapple: "pineapple",
3066
+ "spinach-bunch": "spinach,leaves",
3067
+ "milk-1l": "milk,carton",
3068
+ "yogurt-500g": "yogurt",
3069
+ "eggs-12": "eggs,carton",
3070
+ "cheese-200g": "cheese",
3071
+ "chicken-1kg": "raw-chicken",
3072
+ "beef-mince-500g": "beef,mince",
3073
+ "tilapia-each": "fish,tilapia",
3074
+ "rice-5kg": "rice,bag",
3075
+ "garri-2kg": "rice,grain",
3076
+ "palm-oil-1l": "oil,bottle",
3077
+ "vegetable-oil-1l": "oil,bottle",
3078
+ "salt-1kg": "salt,seasoning",
3079
+ "sugar-1kg": "sugar,white",
3080
+ "tomato-paste": "tomato-paste,can",
3081
+ "soap-bar": "soap,bar",
3082
+ "detergent-1kg": "detergent,laundry",
3083
+ "tissue-pack": "tissue,paper",
3084
+ "trash-bags": "trash-bag",
3085
+ essentials: "groceries,box",
3086
+ // ─── Serene Spa ───
3087
+ "swedish-massage-60": "massage,relaxation",
3088
+ "swedish-massage-90": "massage,relaxation",
3089
+ "deep-tissue-60": "massage,deep-tissue",
3090
+ "couples-massage": "couples-massage",
3091
+ "hot-stone-90": "hot-stone,massage",
3092
+ "facial-classic": "facial,spa",
3093
+ "facial-anti-aging": "facial,skincare",
3094
+ "facial-acne": "facial,skincare",
3095
+ manicure: "manicure,nails",
3096
+ pedicure: "pedicure,feet",
3097
+ "gel-manicure": "manicure,gel-nails",
3098
+ haircut: "haircut,salon",
3099
+ "hair-treatment": "hair-treatment,salon",
3100
+ color: "hair-color,salon",
3101
+ "yoga-session": "yoga,studio",
3102
+ meditation: "meditation,zen",
3103
+ "spa-day": "spa,wellness",
3104
+ "couples-retreat": "spa,couple",
3105
+ "custom-experience": "spa,wellness"
3106
+ };
3107
+ var NO_CACHE = {
3108
+ "Cache-Control": "no-cache, no-store, must-revalidate"
3109
+ };
3110
+ function lockFromSlug(slug) {
3111
+ let hash = 0;
3112
+ for (let i = 0; i < slug.length; i++) hash = hash * 31 + slug.charCodeAt(i) >>> 0;
3113
+ return hash % 9999 + 1;
3114
+ }
3115
+ function svgFallback(slug) {
3116
+ const label = slug.replace(/[-_]+/g, " ").trim() || "product";
3117
+ const display = label.length > 20 ? label.slice(0, 20) + "\u2026" : label;
3118
+ let hash = 0;
3119
+ for (let i = 0; i < slug.length; i++) hash = hash * 31 + slug.charCodeAt(i) >>> 0;
3120
+ const hue = hash % 360;
3121
+ const safe = display.replace(/[<>&]/g, "");
3122
+ const svg = `<?xml version="1.0" encoding="UTF-8"?>
3123
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
3124
+ <defs>
3125
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
3126
+ <stop offset="0%" stop-color="hsl(${hue}, 65%, 65%)"/>
3127
+ <stop offset="100%" stop-color="hsl(${(hue + 40) % 360}, 70%, 45%)"/>
3128
+ </linearGradient>
3129
+ </defs>
3130
+ <rect width="400" height="400" fill="url(#g)"/>
3131
+ <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>
3132
+ </svg>`;
3133
+ return new Response(svg, {
3134
+ headers: {
3135
+ "Content-Type": "image/svg+xml; charset=utf-8",
3136
+ ...NO_CACHE,
3137
+ "Access-Control-Allow-Origin": "*"
3138
+ }
3139
+ });
3140
+ }
3141
+ function handleImageRequest(c) {
3142
+ const path = c.req.path.replace(/^\/img\//, "").replace(/\.[a-z0-9]+$/i, "");
3143
+ const slug = path.split("/").pop() ?? "";
3144
+ for (const [k, v] of Object.entries(NO_CACHE)) c.header(k, v);
3145
+ if (!slug) return svgFallback("");
3146
+ const keyword = SLUG_KEYWORDS[slug] ?? slug.replace(/[-_]+/g, ",");
3147
+ const lock = lockFromSlug(slug);
3148
+ return c.redirect(
3149
+ `https://loremflickr.com/600/600/${encodeURIComponent(keyword)}?lock=${lock}`,
3150
+ 302
3151
+ );
3152
+ }
3153
+ function mountImageRoutes(app) {
3154
+ app.get("/img/*", handleImageRequest);
3155
+ }
3156
+
2985
3157
  // src/mock/core/clock.ts
2986
3158
  function createClock(opts) {
2987
3159
  let frozenAt = opts?.frozenAt ? opts.frozenAt.getTime() : null;
@@ -3319,51 +3491,257 @@ function percent(a, pct) {
3319
3491
  return toMajor(Math.round(toMinor(a) * pct / 100));
3320
3492
  }
3321
3493
 
3322
- // src/mock/domain/catalogue.ts
3323
- function createCatalogueService(opts) {
3324
- const { registry, clock, ids } = opts;
3325
- const productsForBusiness = (businessId) => registry.products.list((p) => p.business_id === businessId);
3326
- const defaultPriceInfo = (price, currency) => ({
3494
+ // src/mock/domain/catalogue/price-info.ts
3495
+ function defaultPriceInfo(price, currency) {
3496
+ return {
3327
3497
  base_price: price,
3328
3498
  final_price: price,
3499
+ pre_tax_price: price,
3329
3500
  markup_percentage: "0.00",
3330
3501
  markup_amount: "0.00",
3331
3502
  markup_discount_percentage: "0.00",
3332
3503
  markup_discount_amount: "0.00",
3333
- currency
3334
- });
3335
- const toApiProduct = (p) => ({
3504
+ currency,
3505
+ tax_info: {
3506
+ tax_rate: "0",
3507
+ tax_amount: "0.00",
3508
+ is_inclusive: true,
3509
+ pre_tax_price: price,
3510
+ components: []
3511
+ }
3512
+ };
3513
+ }
3514
+
3515
+ // src/mock/domain/catalogue/product-shape.ts
3516
+ function toCanonicalProductSummary(p) {
3517
+ return {
3518
+ id: p.id,
3519
+ business_id: p.business_id,
3520
+ category_id: p.category_ids[0],
3521
+ name: p.name,
3522
+ slug: p.slug,
3523
+ description: p.description,
3524
+ image_url: p.image,
3525
+ images: p.images,
3526
+ default_price: p.base_price,
3527
+ type: p.product_type,
3528
+ inventory_type: "none",
3529
+ variant_strategy: "fetch_all",
3530
+ is_active: p.is_available,
3531
+ inventory_status: { in_stock: p.is_available, stock_level: 50, low_stock: false },
3532
+ tags: p.tags ?? [],
3533
+ created_at: p.created_at,
3534
+ updated_at: p.updated_at
3535
+ };
3536
+ }
3537
+ function deriveRenderHint(p) {
3538
+ if (p.render_hint) return p.render_hint;
3539
+ if (p.product_type === "service") return "general";
3540
+ if (p.product_type === "digital") return "general";
3541
+ const foodish = (p.tags ?? []).some(
3542
+ (t) => /food|drink|cake|bread|pastry|dessert|coffee|tea|juice|spice/i.test(t)
3543
+ );
3544
+ return foodish ? "food" : "general";
3545
+ }
3546
+ function toApiProduct(p, ctx) {
3547
+ const { registry, priceInfo, groups, components, composite, bundle } = ctx;
3548
+ return {
3549
+ // ─── Canonical Product fields (SDK type: Product / ProductWithDetails) ───
3336
3550
  id: p.id,
3337
3551
  business_id: p.business_id,
3552
+ category_id: p.category_ids[0],
3338
3553
  name: p.name,
3339
3554
  slug: p.slug,
3340
3555
  description: p.description ?? "",
3341
- product_type: p.product_type,
3342
- base_price: p.base_price,
3556
+ image_url: p.image,
3557
+ images: p.images,
3343
3558
  default_price: p.base_price,
3344
- default_price_info: defaultPriceInfo(p.base_price, p.currency),
3345
- price_info: defaultPriceInfo(p.base_price, p.currency),
3559
+ type: p.product_type,
3560
+ render_hint: deriveRenderHint(p),
3561
+ inventory_type: "none",
3562
+ variant_strategy: p.variant_ids.length > 0 ? "use_axes" : "fetch_all",
3563
+ is_active: p.is_available,
3564
+ inventory_status: { in_stock: p.is_available, stock_level: 50, low_stock: false },
3565
+ tags: p.tags ?? [],
3566
+ calories: p.calories,
3567
+ allergies: p.allergies ?? [],
3568
+ display_mode: p.display_mode,
3569
+ digital_product_type: p.digital_product_type,
3570
+ quantity_pricing: p.quantity_pricing,
3571
+ duration_unit: p.duration_unit,
3572
+ duration_minutes: p.duration_minutes,
3573
+ billing_plans: p.billing_plans,
3574
+ created_at: p.created_at,
3575
+ updated_at: p.updated_at,
3576
+ metadata: {
3577
+ is_signature: p.is_signature ?? false,
3578
+ is_new: p.is_new ?? false,
3579
+ ingredients: p.ingredients ?? [],
3580
+ pairings: p.pairings ?? [],
3581
+ ...p.metadata ?? {}
3582
+ },
3583
+ // ─── Pricing breakdown (Price / ProductCustomizer) ───
3584
+ base_price: p.base_price,
3585
+ default_price_info: priceInfo,
3586
+ price_info: priceInfo,
3346
3587
  currency: p.currency,
3588
+ // ─── Compatibility aliases (older callers) ───
3347
3589
  image: p.image,
3348
- image_url: p.image,
3349
- images: p.images,
3590
+ product_type: p.product_type,
3350
3591
  is_available: p.is_available,
3351
- category_id: p.category_ids[0],
3352
3592
  category_ids: p.category_ids,
3353
3593
  collection_ids: p.collection_ids,
3354
3594
  add_on_ids: p.add_on_ids,
3355
- metadata: p.metadata ?? {},
3356
- created_at: p.created_at,
3357
- updated_at: p.updated_at,
3595
+ variant_ids: p.variant_ids,
3358
3596
  custom_attributes: [],
3359
3597
  properties: [],
3360
3598
  has_variants: p.variant_ids.length > 0,
3361
- type: p.product_type,
3362
- render_hint: "general"
3363
- });
3599
+ // ─── Embedded relations (ProductWithDetails) ───
3600
+ category: registry.categories.get(p.category_ids[0]),
3601
+ add_ons: p.add_on_ids.map((aid) => registry.addOns.get(aid)).filter(Boolean),
3602
+ variants: registry.variants.list((v) => v.product_id === p.id),
3603
+ variant_axes: registry.variantAxes.list((a) => a.product_id === p.id),
3604
+ // ─── Composite-specific (CompositeProductLayout reads product.groups) ───
3605
+ groups,
3606
+ composite_id: composite?.id,
3607
+ pricing_mode: composite?.pricing_mode,
3608
+ min_order_quantity: composite?.min_order_quantity,
3609
+ max_order_quantity: composite?.max_order_quantity,
3610
+ // ─── Bundle-specific (BundleProductLayout reads product.components) ───
3611
+ bundle_id: bundle?.id,
3612
+ bundle_pricing_type: bundle?.pricing_type,
3613
+ bundle_price: bundle?.bundle_price,
3614
+ bundle_discount_value: bundle?.discount_value,
3615
+ components
3616
+ };
3617
+ }
3618
+
3619
+ // src/mock/domain/catalogue/composite-shape.ts
3620
+ function expandCompositeGroups(registry, productId, isoNow) {
3621
+ const composite = registry.composites.list((c) => c.product_id === productId)[0];
3622
+ if (!composite) return void 0;
3623
+ return composite.groups.map((g, gIdx) => buildGroup(registry, composite, g, gIdx, isoNow));
3624
+ }
3625
+ function buildGroup(registry, composite, group, groupIdx, isoNow) {
3626
+ return {
3627
+ // ─── Canonical ComponentGroupWithComponents ───
3628
+ id: group.id,
3629
+ composite_id: composite.id,
3630
+ name: group.name,
3631
+ description: void 0,
3632
+ display_order: groupIdx,
3633
+ min_selections: group.min,
3634
+ max_selections: group.max,
3635
+ allow_quantity: true,
3636
+ max_quantity_per_component: group.max,
3637
+ pricing_behavior: "additive",
3638
+ created_at: isoNow,
3639
+ updated_at: isoNow,
3640
+ // ─── Aliases for older selectors ───
3641
+ min: group.min,
3642
+ max: group.max,
3643
+ 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))
3644
+ };
3645
+ }
3646
+ function buildComponent(composite, group, p, idx) {
3647
+ return {
3648
+ // ─── Canonical CompositeComponent ───
3649
+ id: `comp_${composite.product_id}_${p.id}`,
3650
+ composite_id: composite.id,
3651
+ group_id: group.id,
3652
+ source_type: "product",
3653
+ source_product_id: p.id,
3654
+ product_id: p.id,
3655
+ display_name: p.name,
3656
+ display_description: p.description,
3657
+ display_image_url: p.image,
3658
+ price: p.base_price,
3659
+ calories: p.calories,
3660
+ allergens: p.allergies ?? [],
3661
+ display_order: idx,
3662
+ is_popular: idx === 0,
3663
+ is_premium: false,
3664
+ is_available: p.is_available,
3665
+ is_archived: false,
3666
+ created_at: p.created_at,
3667
+ updated_at: p.updated_at,
3668
+ // ─── Compatibility aliases ───
3669
+ name: p.name,
3670
+ description: p.description,
3671
+ image_url: p.image,
3672
+ unit_price: p.base_price,
3673
+ default_price: p.base_price,
3674
+ base_price: p.base_price,
3675
+ currency: p.currency,
3676
+ product: toCanonicalProductSummary(p)
3677
+ };
3678
+ }
3679
+
3680
+ // src/mock/domain/catalogue/bundle-shape.ts
3681
+ function expandBundleComponents(registry, productId) {
3682
+ const bundle = registry.bundles.list((b) => b.product_id === productId)[0];
3683
+ if (!bundle) return void 0;
3684
+ return bundle.product_ids.map((cid) => registry.products.get(cid)).filter((p) => Boolean(p)).map((p) => buildBundleComponent(bundle.id, productId, p));
3685
+ }
3686
+ function buildBundleComponent(bundleId, productId, p) {
3687
+ return {
3688
+ id: `bp_${productId}_${p.id}`,
3689
+ bundle_id: bundleId,
3690
+ product_id: p.id,
3691
+ // ─── Fields BundleProductLayout reads ───
3692
+ product_name: p.name,
3693
+ product_image_url: p.image,
3694
+ product_description: p.description,
3695
+ product_slug: p.slug,
3696
+ effective_price: p.base_price,
3697
+ unit_price: p.base_price,
3698
+ default_price: p.base_price,
3699
+ quantity: 1,
3700
+ allow_variant_choice: false,
3701
+ available_variants: [],
3702
+ selected_variant_id: null,
3703
+ // ─── Compatibility aliases ───
3704
+ display_name: p.name,
3705
+ display_description: p.description,
3706
+ name: p.name,
3707
+ description: p.description,
3708
+ image_url: p.image,
3709
+ currency: p.currency,
3710
+ is_available: p.is_available,
3711
+ product: toCanonicalProductSummary(p)
3712
+ };
3713
+ }
3714
+
3715
+ // src/mock/domain/catalogue/index.ts
3716
+ function createCatalogueService(opts) {
3717
+ const { registry, clock, ids } = opts;
3718
+ const productsForBusiness = (businessId) => registry.products.list((p) => p.business_id === businessId);
3364
3719
  const variantsForProduct = (productId) => registry.variants.list((v) => v.product_id === productId);
3365
3720
  const axesForProduct = (productId) => registry.variantAxes.list((a) => a.product_id === productId);
3366
3721
  const addOnLinksForProduct = (productId) => registry.productAddOns.list((l) => l.product_id === productId);
3722
+ const apiProduct = (p) => {
3723
+ const composite = p.product_type === "composite" ? registry.composites.list((c) => c.product_id === p.id)[0] : void 0;
3724
+ const bundle = p.product_type === "bundle" ? registry.bundles.list((b) => b.product_id === p.id)[0] : void 0;
3725
+ return toApiProduct(p, {
3726
+ registry,
3727
+ priceInfo: defaultPriceInfo(p.base_price, p.currency),
3728
+ groups: expandCompositeGroups(registry, p.id, clock.iso()),
3729
+ components: expandBundleComponents(registry, p.id),
3730
+ composite: composite ? {
3731
+ id: composite.id,
3732
+ pricing_mode: composite.pricing_mode,
3733
+ min_order_quantity: composite.min_order_quantity,
3734
+ max_order_quantity: composite.max_order_quantity
3735
+ } : void 0,
3736
+ bundle: bundle ? {
3737
+ id: bundle.id,
3738
+ pricing_type: bundle.pricing_type,
3739
+ bundle_price: bundle.bundle_price,
3740
+ discount_value: bundle.discount_value
3741
+ } : void 0
3742
+ });
3743
+ };
3744
+ const resolveProduct = (idOrSlug) => registry.products.get(idOrSlug) ?? registry.products.list((p) => p.slug === idOrSlug)[0];
3367
3745
  return {
3368
3746
  snapshot(businessId) {
3369
3747
  const products = productsForBusiness(businessId);
@@ -3371,7 +3749,7 @@ function createCatalogueService(opts) {
3371
3749
  const addOns = registry.addOns.list((a) => a.business_id === businessId);
3372
3750
  return {
3373
3751
  categories,
3374
- products: products.map(toApiProduct),
3752
+ products: products.map(apiProduct),
3375
3753
  add_ons: addOns,
3376
3754
  is_complete: true,
3377
3755
  total_available: products.length
@@ -3386,7 +3764,7 @@ function createCatalogueService(opts) {
3386
3764
  const start = (page - 1) * limit;
3387
3765
  const slice = items.slice(start, start + limit);
3388
3766
  return {
3389
- items: slice.map(toApiProduct),
3767
+ items: slice.map(apiProduct),
3390
3768
  is_complete: start + limit >= items.length,
3391
3769
  total_available: items.length,
3392
3770
  pagination: {
@@ -3399,9 +3777,9 @@ function createCatalogueService(opts) {
3399
3777
  };
3400
3778
  },
3401
3779
  getProduct(productId) {
3402
- const p = registry.products.get(productId);
3780
+ const p = resolveProduct(productId);
3403
3781
  if (!p) throw notFound(`Product ${productId} not found`);
3404
- return toApiProduct(p);
3782
+ return apiProduct(p);
3405
3783
  },
3406
3784
  getProductVariants(productId) {
3407
3785
  return variantsForProduct(productId);
@@ -3411,7 +3789,9 @@ function createCatalogueService(opts) {
3411
3789
  },
3412
3790
  getProductVariantById(productId, variantId) {
3413
3791
  const v = registry.variants.get(variantId);
3414
- if (!v || v.product_id !== productId) throw notFound(`Variant ${variantId} not found on product ${productId}`);
3792
+ if (!v || v.product_id !== productId) {
3793
+ throw notFound(`Variant ${variantId} not found on product ${productId}`);
3794
+ }
3415
3795
  return v;
3416
3796
  },
3417
3797
  findProductVariant(productId, properties) {
@@ -3441,13 +3821,13 @@ function createCatalogueService(opts) {
3441
3821
  return registry.categories.list((c) => c.business_id === businessId);
3442
3822
  },
3443
3823
  getCategory(id) {
3444
- const c = registry.categories.get(id);
3824
+ const c = registry.categories.get(id) ?? registry.categories.list((x) => x.slug === id)[0];
3445
3825
  if (!c) throw notFound(`Category ${id} not found`);
3446
3826
  return c;
3447
3827
  },
3448
3828
  getCategoryProducts(id) {
3449
3829
  const c = this.getCategory(id);
3450
- return c.product_ids.map((pid) => registry.products.get(pid)).filter((p) => Boolean(p)).map(toApiProduct);
3830
+ return c.product_ids.map((pid) => registry.products.get(pid)).filter((p) => Boolean(p)).map(apiProduct);
3451
3831
  },
3452
3832
  getCategoryAttributes(id) {
3453
3833
  const c = registry.categories.get(id);
@@ -3460,13 +3840,13 @@ function createCatalogueService(opts) {
3460
3840
  return registry.collections.list((c) => c.business_id === businessId);
3461
3841
  },
3462
3842
  getCollection(id) {
3463
- const c = registry.collections.get(id);
3843
+ const c = registry.collections.get(id) ?? registry.collections.list((x) => x.slug === id)[0];
3464
3844
  if (!c) throw notFound(`Collection ${id} not found`);
3465
3845
  return c;
3466
3846
  },
3467
3847
  getCollectionProducts(id) {
3468
3848
  const c = this.getCollection(id);
3469
- return c.product_ids.map((pid) => registry.products.get(pid)).filter((p) => Boolean(p)).map(toApiProduct);
3849
+ return c.product_ids.map((pid) => registry.products.get(pid)).filter((p) => Boolean(p)).map(apiProduct);
3470
3850
  },
3471
3851
  getCollectionAttributes(id) {
3472
3852
  const c = registry.collections.get(id);
@@ -3479,7 +3859,7 @@ function createCatalogueService(opts) {
3479
3859
  return registry.bundles.list((b) => b.business_id === businessId);
3480
3860
  },
3481
3861
  getBundle(id) {
3482
- const b = registry.bundles.get(id);
3862
+ const b = registry.bundles.get(id) ?? registry.bundles.list((x) => x.product_id === id || x.slug === id)[0];
3483
3863
  if (!b) throw notFound(`Bundle ${id} not found`);
3484
3864
  return b;
3485
3865
  },
@@ -3487,12 +3867,16 @@ function createCatalogueService(opts) {
3487
3867
  return registry.composites.list((c) => c.business_id === businessId);
3488
3868
  },
3489
3869
  getComposite(id) {
3490
- const c = registry.composites.get(id);
3870
+ const c = registry.composites.get(id) ?? registry.composites.list((x) => x.product_id === id)[0];
3491
3871
  if (!c) throw notFound(`Composite ${id} not found`);
3492
3872
  return c;
3493
3873
  },
3494
3874
  getCompositeByProduct(productId) {
3495
- return registry.composites.list((c) => c.product_id === productId)[0] ?? null;
3875
+ const direct = registry.composites.list((c) => c.product_id === productId)[0];
3876
+ if (direct) return direct;
3877
+ const product = registry.products.list((p) => p.slug === productId)[0];
3878
+ if (!product) return null;
3879
+ return registry.composites.list((c) => c.product_id === product.id)[0] ?? null;
3496
3880
  },
3497
3881
  calculateCompositePrice(id, body) {
3498
3882
  const c = registry.composites.get(id);
@@ -3500,14 +3884,20 @@ function createCatalogueService(opts) {
3500
3884
  const sel = body.selections ?? [];
3501
3885
  let total = c.base_price;
3502
3886
  for (const s of sel) total = add(total, money(parseFloat(s.unit_price ?? "0") * (s.quantity ?? 1)));
3503
- return { final_price: total, base_price: c.base_price, components_total: money(parseFloat(total) - parseFloat(c.base_price)) };
3887
+ return {
3888
+ final_price: total,
3889
+ base_price: c.base_price,
3890
+ components_total: money(parseFloat(total) - parseFloat(c.base_price))
3891
+ };
3504
3892
  },
3505
3893
  listTaxonomies(businessId) {
3506
3894
  return registry.taxonomies.list((t) => t.business_id === businessId);
3507
3895
  },
3508
3896
  searchTaxonomies(businessId, q) {
3509
3897
  const term = q.toLowerCase();
3510
- return registry.taxonomies.list((t) => t.business_id === businessId && t.name.toLowerCase().includes(term));
3898
+ return registry.taxonomies.list(
3899
+ (t) => t.business_id === businessId && t.name.toLowerCase().includes(term)
3900
+ );
3511
3901
  },
3512
3902
  getTaxonomy(id) {
3513
3903
  const t = registry.taxonomies.get(id);
@@ -3541,7 +3931,11 @@ function createCatalogueService(opts) {
3541
3931
  listPropertyFacets(businessId) {
3542
3932
  const products = productsForBusiness(businessId);
3543
3933
  const byCategory = /* @__PURE__ */ new Map();
3544
- for (const p of products) for (const cid of p.category_ids) byCategory.set(cid, (byCategory.get(cid) ?? 0) + 1);
3934
+ for (const p of products) {
3935
+ for (const cid of p.category_ids) {
3936
+ byCategory.set(cid, (byCategory.get(cid) ?? 0) + 1);
3937
+ }
3938
+ }
3545
3939
  return [
3546
3940
  {
3547
3941
  name: "Category",
@@ -3560,12 +3954,6 @@ function createCatalogueService(opts) {
3560
3954
  listOnSale() {
3561
3955
  return [];
3562
3956
  },
3563
- validateDiscount(code) {
3564
- if (code === "WELCOME10") {
3565
- return { valid: true, discount_id: "disc_welcome10", discount_value: "10.00", discount_type: "percentage" };
3566
- }
3567
- return { valid: false, reason: "Code not recognized" };
3568
- },
3569
3957
  getProductDeals() {
3570
3958
  return [];
3571
3959
  },
@@ -3575,15 +3963,33 @@ function createCatalogueService(opts) {
3575
3963
  getCollectionDeals() {
3576
3964
  return [];
3577
3965
  },
3966
+ validateDiscount(code) {
3967
+ if (code === "WELCOME10") {
3968
+ return { valid: true, discount_id: "disc_welcome10", discount_value: "10.00", discount_type: "percentage" };
3969
+ }
3970
+ return { valid: false, reason: "Code not recognized" };
3971
+ },
3578
3972
  createQuote(businessId, body) {
3579
3973
  const b = body;
3580
3974
  const id = ids.quote();
3975
+ const productId = b.product_id ?? "";
3976
+ const product = resolveProduct(productId);
3977
+ const price = b.total_price ?? product?.base_price ?? "0.00";
3978
+ const currency = b.currency ?? product?.currency ?? "GHS";
3979
+ const priceInfo = defaultPriceInfo(price, currency);
3581
3980
  const quote = {
3582
3981
  id,
3982
+ quote_id: id,
3583
3983
  business_id: businessId,
3584
- product_id: b.product_id ?? "",
3984
+ product_id: product?.id ?? productId,
3585
3985
  status: "fresh",
3586
- total_price: b.total_price ?? "0.00",
3986
+ total_price: price,
3987
+ currency,
3988
+ unit_price_info: priceInfo,
3989
+ final_price_info: priceInfo,
3990
+ quoted_total_price_info: priceInfo,
3991
+ components: [],
3992
+ breakdown: { base_price: price, components_total: "0.00", final_price: price },
3587
3993
  expires_at: new Date(clock.nowMs() + 10 * 60 * 1e3).toISOString(),
3588
3994
  created_at: clock.iso()
3589
3995
  };
@@ -3599,7 +4005,11 @@ function createCatalogueService(opts) {
3599
4005
  refreshQuote(id) {
3600
4006
  const q = registry.quotes.get(id);
3601
4007
  if (!q) throw notFound(`Quote ${id} not found`);
3602
- const refreshed = { ...q, status: "fresh", expires_at: new Date(clock.nowMs() + 10 * 60 * 1e3).toISOString() };
4008
+ const refreshed = {
4009
+ ...q,
4010
+ status: "fresh",
4011
+ expires_at: new Date(clock.nowMs() + 10 * 60 * 1e3).toISOString()
4012
+ };
3603
4013
  registry.quotes.put(id, refreshed);
3604
4014
  return refreshed;
3605
4015
  }
@@ -4624,67 +5034,99 @@ function createLiteService(opts) {
4624
5034
 
4625
5035
  // src/mock/seeds/default.ts
4626
5036
  var ISO = "2026-01-01T00:00:00.000Z";
4627
- var PRODUCTS = [
4628
- { name: "Sourdough Loaf", slug: "sourdough-loaf", price: "25.00", category: "breads", description: "Artisanal 24-hour fermented sourdough.", image: "/img/sourdough.jpg" },
4629
- { name: "Whole Wheat Bread", slug: "whole-wheat", price: "20.00", category: "breads", description: "Hearty whole wheat with seeds.", image: "/img/wheat.jpg" },
4630
- { name: "Baguette", slug: "baguette", price: "15.00", category: "breads", description: "Crispy crust, soft inside.", image: "/img/baguette.jpg" },
4631
- { name: "Focaccia", slug: "focaccia", price: "30.00", category: "breads", description: "Olive oil and rosemary focaccia.", image: "/img/focaccia.jpg" },
4632
- { name: "Croissant", slug: "croissant", price: "12.00", category: "pastries", description: "Buttery French croissant.", image: "/img/croissant.jpg" },
4633
- { name: "Pain au Chocolat", slug: "pain-au-chocolat", price: "14.00", category: "pastries", description: "Croissant with dark chocolate.", image: "/img/pac.jpg" },
4634
- { name: "Almond Danish", slug: "almond-danish", price: "16.00", category: "pastries", description: "Flaky pastry with almond cream.", image: "/img/danish.jpg" },
4635
- { name: "Cinnamon Roll", slug: "cinnamon-roll", price: "13.00", category: "pastries", description: "Soft roll with cinnamon glaze.", image: "/img/cinnamon.jpg" },
4636
- { name: "Chocolate Cake (Slice)", slug: "chocolate-cake", price: "22.00", category: "cakes", description: "Rich dark chocolate cake.", image: "/img/choc-cake.jpg" },
4637
- { name: "Red Velvet (Slice)", slug: "red-velvet", price: "22.00", category: "cakes", description: "Classic red velvet with cream cheese.", image: "/img/red-velvet.jpg" },
4638
- { name: "Carrot Cake (Slice)", slug: "carrot-cake", price: "20.00", category: "cakes", description: "Spiced carrot cake with walnut.", image: "/img/carrot.jpg" },
4639
- { name: "Cheesecake (Slice)", slug: "cheesecake", price: "24.00", category: "cakes", description: "New York style cheesecake.", image: "/img/cheesecake.jpg" },
4640
- { name: "Coffee", slug: "coffee", price: "10.00", category: "drinks", description: "Locally roasted Arabica.", image: "/img/coffee.jpg" },
4641
- { name: "Tea", slug: "tea", price: "8.00", category: "drinks", description: "Black, green, or herbal.", image: "/img/tea.jpg" },
4642
- { name: "Hibiscus Tea (Sobolo)", slug: "sobolo", price: "12.00", category: "drinks", description: "Iced hibiscus drink.", image: "/img/sobolo.jpg" },
4643
- { name: "Fresh Orange Juice", slug: "orange-juice", price: "15.00", category: "drinks", description: "Cold-pressed orange juice.", image: "/img/oj.jpg" },
4644
- { name: "Banana Bread", slug: "banana-bread", price: "18.00", category: "breads", description: "Moist banana loaf.", image: "/img/banana.jpg" },
4645
- { name: "Brownie", slug: "brownie", price: "12.00", category: "pastries", description: "Fudgy chocolate brownie.", image: "/img/brownie.jpg" },
4646
- { name: "Macaron Box (6)", slug: "macaron-box", price: "60.00", category: "pastries", description: "Six assorted macarons.", image: "/img/macaron.jpg" },
4647
- { name: "Custom Birthday Cake", slug: "birthday-cake", price: "250.00", category: "cakes", description: "Made-to-order. Min 48h notice.", image: "/img/birthday.jpg" }
4648
- ];
4649
- var CATEGORIES = [
4650
- { slug: "breads", name: "Breads", description: "Sourdough, baguettes, focaccia." },
4651
- { slug: "pastries", name: "Pastries", description: "Croissants, danishes, brownies." },
4652
- { slug: "cakes", name: "Cakes", description: "Cake slices and custom orders." },
4653
- { slug: "drinks", name: "Drinks", description: "Coffee, tea, juices." }
4654
- ];
5037
+ var BUSINESS_ID = "bus_default_akua_bakery";
5038
+ var img = (id) => `https://images.unsplash.com/photo-${id}?w=800&h=800&fit=crop&auto=format&q=80`;
5039
+ function makeProduct(input, categoryIds) {
5040
+ const catId = categoryIds.get(input.category);
5041
+ return {
5042
+ id: input.id,
5043
+ business_id: BUSINESS_ID,
5044
+ name: input.name,
5045
+ slug: input.slug,
5046
+ description: input.description,
5047
+ product_type: input.productType ?? "product",
5048
+ base_price: input.price,
5049
+ currency: "GHS",
5050
+ image: input.imageUrl,
5051
+ images: input.images ?? [input.imageUrl],
5052
+ is_available: true,
5053
+ category_ids: [catId],
5054
+ collection_ids: [],
5055
+ add_on_ids: [],
5056
+ variant_ids: [],
5057
+ tags: input.tags ?? [],
5058
+ calories: input.calories,
5059
+ allergies: input.allergies ?? [],
5060
+ ingredients: input.ingredients ?? [],
5061
+ pairings: input.pairings ?? [],
5062
+ is_signature: input.isSignature ?? false,
5063
+ is_new: input.isNew ?? false,
5064
+ render_hint: input.renderHint,
5065
+ display_mode: input.displayMode,
5066
+ digital_product_type: input.digitalProductType,
5067
+ quantity_pricing: input.quantityPricing,
5068
+ duration_unit: input.durationUnit,
5069
+ duration_minutes: input.durationMinutes,
5070
+ billing_plans: input.billingPlans?.map((b) => ({
5071
+ id: b.id,
5072
+ name: b.name,
5073
+ interval: b.interval,
5074
+ interval_count: b.interval_count,
5075
+ price: b.price,
5076
+ currency: b.currency ?? "GHS",
5077
+ trial_period_days: b.trial_period_days
5078
+ })),
5079
+ created_at: ISO,
5080
+ updated_at: ISO
5081
+ };
5082
+ }
4655
5083
  function seedDefault(registry) {
4656
- const businessId = "bus_default_akua_bakery";
4657
5084
  const business = {
4658
- id: businessId,
5085
+ id: BUSINESS_ID,
4659
5086
  name: "Akua's Bakery",
4660
5087
  handle: "akua",
4661
5088
  business_type: "eatery",
4662
5089
  email: "hello@akuasbakery.test",
4663
5090
  default_currency: "GHS",
4664
- default_phone: "+233244000000",
5091
+ default_phone: "+233 244 000 000",
4665
5092
  default_address: "12 Independence Ave, Accra",
4666
5093
  default_offers_table_service: true,
4667
5094
  default_accepts_online_orders: true,
4668
- image: "/img/akua-logo.png",
5095
+ image: img("1568254183919-78a4f43a2877"),
4669
5096
  status: "active",
4670
5097
  created_at: ISO,
4671
5098
  updated_at: ISO,
4672
5099
  created_by: "seed",
4673
- preferences: { theme: "warm", checkout: { allow_guest: true } },
5100
+ preferences: {
5101
+ tagline: "Fresh from the oven, every day",
5102
+ description: "Family-run bakery in East Legon. Sourdough, croissants, layered cakes \u2014 handcrafted since 2018, delivered across Accra.",
5103
+ instagram: "https://instagram.com/akuasbakery",
5104
+ theme: "warm",
5105
+ checkout: { allow_guest: true }
5106
+ },
4674
5107
  is_online_only: false,
4675
5108
  enabled_payment_types: ["mobile_money", "card", "cash"],
4676
5109
  default_location_settings: {},
4677
5110
  country_code: "GH",
4678
5111
  timezone: "Africa/Accra"
4679
5112
  };
4680
- registry.businesses.put(businessId, business);
4681
- const categoryIdsBySlug = /* @__PURE__ */ new Map();
4682
- for (const c of CATEGORIES) {
5113
+ registry.businesses.put(BUSINESS_ID, business);
5114
+ const categoryDefs = [
5115
+ { slug: "breads", name: "Breads", description: "Naturally leavened sourdough, baguettes, focaccia." },
5116
+ { slug: "pastries", name: "Pastries", description: "Hand-laminated croissants, danishes, brownies." },
5117
+ { slug: "cakes", name: "Cakes", description: "Slices and made-to-order showstoppers." },
5118
+ { slug: "drinks", name: "Drinks", description: "Locally roasted coffee, brewed teas, fresh juice." },
5119
+ { slug: "boxes", name: "Boxes & Bundles", description: "Curated boxes for sharing." },
5120
+ { slug: "merch", name: "Merch & More", description: "Branded goods, wholesale flour, downloadable recipes, gift cards." },
5121
+ { slug: "classes", name: "Classes & Stays", description: "Hands-on baking classes, kitchen rentals, monthly bread subscriptions." }
5122
+ ];
5123
+ const catIds = /* @__PURE__ */ new Map();
5124
+ for (const c of categoryDefs) {
4683
5125
  const id = `cat_${c.slug}`;
4684
- categoryIdsBySlug.set(c.slug, id);
5126
+ catIds.set(c.slug, id);
4685
5127
  const cat = {
4686
5128
  id,
4687
- business_id: businessId,
5129
+ business_id: BUSINESS_ID,
4688
5130
  name: c.name,
4689
5131
  slug: c.slug,
4690
5132
  description: c.description,
@@ -4696,99 +5138,502 @@ function seedDefault(registry) {
4696
5138
  };
4697
5139
  registry.categories.put(id, cat);
4698
5140
  }
4699
- const featured = [];
4700
- PRODUCTS.forEach((p, i) => {
4701
- const id = `prod_${p.slug}`;
4702
- const catId = categoryIdsBySlug.get(p.category);
4703
- const product = {
4704
- id,
4705
- business_id: businessId,
4706
- name: p.name,
4707
- slug: p.slug,
4708
- description: p.description,
4709
- product_type: "product",
4710
- base_price: p.price,
4711
- currency: "GHS",
4712
- image: p.image,
4713
- images: [p.image],
4714
- is_available: true,
4715
- category_ids: [catId],
4716
- collection_ids: [],
4717
- add_on_ids: [],
4718
- variant_ids: [],
4719
- created_at: ISO,
4720
- updated_at: ISO
4721
- };
4722
- registry.products.put(id, product);
4723
- const cat = registry.categories.get(catId);
4724
- cat.product_ids.push(id);
4725
- registry.categories.put(catId, cat);
4726
- if (i < 5) featured.push(id);
4727
- });
4728
- const featuredCollection = {
5141
+ const productInputs = [
5142
+ // ── Breads ──
5143
+ {
5144
+ id: "prod_sourdough-loaf",
5145
+ name: "Country Sourdough",
5146
+ slug: "sourdough-loaf",
5147
+ price: "25.00",
5148
+ description: "24-hour fermented country loaf with our heritage starter \u2014 open crumb, blistered crust, naturally tangy. Baked fresh every morning.",
5149
+ category: "breads",
5150
+ imageUrl: img("1509440159596-0249088772ff"),
5151
+ images: [img("1509440159596-0249088772ff"), img("1486887396153-fa416526c108")],
5152
+ tags: ["bread", "vegan", "signature"],
5153
+ isSignature: true,
5154
+ ingredients: ["Wheat flour", "Sourdough starter", "Sea salt", "Filtered water"],
5155
+ pairings: ["Olive oil", "Aged cheese", "Tomato jam"],
5156
+ allergies: ["wheat", "gluten"],
5157
+ calories: 240
5158
+ },
5159
+ {
5160
+ id: "prod_whole-wheat",
5161
+ name: "Whole Wheat Seed Loaf",
5162
+ slug: "whole-wheat",
5163
+ price: "20.00",
5164
+ description: "Hearty 100% whole wheat with sunflower, sesame, and pumpkin seeds. Slow-fermented for a soft crumb and nutty depth.",
5165
+ category: "breads",
5166
+ imageUrl: img("1486887396153-fa416526c108"),
5167
+ tags: ["bread", "vegan", "high-fibre"],
5168
+ ingredients: ["Whole wheat flour", "Sunflower seeds", "Sesame", "Pumpkin seeds"],
5169
+ allergies: ["wheat", "gluten", "sesame"],
5170
+ calories: 220
5171
+ },
5172
+ {
5173
+ id: "prod_baguette",
5174
+ name: "Classic Baguette",
5175
+ slug: "baguette",
5176
+ price: "15.00",
5177
+ description: "Crackling crust, airy crumb. Best eaten warm, within an hour of baking.",
5178
+ category: "breads",
5179
+ imageUrl: img("1568471173242-461f0a730452"),
5180
+ tags: ["bread", "vegan"],
5181
+ ingredients: ["Wheat flour", "Yeast", "Sea salt", "Water"],
5182
+ allergies: ["wheat", "gluten"],
5183
+ calories: 180
5184
+ },
5185
+ {
5186
+ id: "prod_focaccia",
5187
+ name: "Rosemary Focaccia",
5188
+ slug: "focaccia",
5189
+ price: "30.00",
5190
+ description: "High-hydration focaccia drenched in olive oil, scattered with sea salt and fresh rosemary. Sold whole or by the slab.",
5191
+ category: "breads",
5192
+ imageUrl: img("1571167530149-c1105da4c2c7"),
5193
+ tags: ["bread", "vegetarian"],
5194
+ ingredients: ["00 flour", "Olive oil", "Rosemary", "Maldon salt"],
5195
+ pairings: ["Burrata", "Prosciutto", "Caponata"],
5196
+ allergies: ["wheat", "gluten"],
5197
+ calories: 310
5198
+ },
5199
+ {
5200
+ id: "prod_banana-bread",
5201
+ name: "Brown Butter Banana Bread",
5202
+ slug: "banana-bread",
5203
+ price: "18.00",
5204
+ description: "Caramelised brown butter, three over-ripe bananas, walnuts, finished with a salted maple glaze.",
5205
+ category: "breads",
5206
+ imageUrl: img("1632931057819-4eefffa8e007"),
5207
+ tags: ["sweet", "comfort"],
5208
+ ingredients: ["Bananas", "Brown butter", "Walnuts", "Maple syrup"],
5209
+ allergies: ["wheat", "gluten", "dairy", "nuts", "eggs"],
5210
+ calories: 360,
5211
+ isNew: true
5212
+ },
5213
+ // ── Pastries ──
5214
+ {
5215
+ id: "prod_croissant",
5216
+ name: "Butter Croissant",
5217
+ slug: "croissant",
5218
+ price: "12.00",
5219
+ description: "27 layers of laminated dough wrapped around French butter. Crisp, shattering crust and a tender honeycomb interior.",
5220
+ category: "pastries",
5221
+ imageUrl: img("1555507036-ab1f4038808a"),
5222
+ images: [img("1555507036-ab1f4038808a"), img("1530610476181-d83430b64dcd")],
5223
+ tags: ["pastry", "vegetarian", "signature"],
5224
+ isSignature: true,
5225
+ ingredients: ["French butter", "Wheat flour", "Milk", "Eggs"],
5226
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5227
+ calories: 280
5228
+ },
5229
+ {
5230
+ id: "prod_pain-au-chocolat",
5231
+ name: "Pain au Chocolat",
5232
+ slug: "pain-au-chocolat",
5233
+ price: "14.00",
5234
+ description: "Our croissant dough wrapped around two batons of 70% Valrhona dark chocolate.",
5235
+ category: "pastries",
5236
+ imageUrl: img("1530610476181-d83430b64dcd"),
5237
+ tags: ["pastry", "chocolate"],
5238
+ ingredients: ["Croissant dough", "Valrhona chocolate"],
5239
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5240
+ calories: 320
5241
+ },
5242
+ {
5243
+ id: "prod_almond-danish",
5244
+ name: "Almond Danish",
5245
+ slug: "almond-danish",
5246
+ price: "16.00",
5247
+ description: "Flaky pastry filled with almond cream, dusted with toasted slivers and powdered sugar.",
5248
+ category: "pastries",
5249
+ imageUrl: img("1517433367423-c7e5b0f35086"),
5250
+ tags: ["pastry", "nuts"],
5251
+ ingredients: ["Pastry dough", "Almond cream", "Toasted almonds"],
5252
+ allergies: ["wheat", "gluten", "dairy", "eggs", "nuts"],
5253
+ calories: 340
5254
+ },
5255
+ {
5256
+ id: "prod_cinnamon-roll",
5257
+ name: "Cinnamon Roll",
5258
+ slug: "cinnamon-roll",
5259
+ price: "13.00",
5260
+ description: "Soft enriched dough swirled with butter and Ceylon cinnamon, finished with cream-cheese glaze.",
5261
+ category: "pastries",
5262
+ imageUrl: img("1607920591413-4ec007e70023"),
5263
+ tags: ["pastry", "comfort"],
5264
+ ingredients: ["Enriched dough", "Cinnamon", "Brown sugar", "Cream cheese"],
5265
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5266
+ calories: 380
5267
+ },
5268
+ {
5269
+ id: "prod_brownie",
5270
+ name: "Fudge Brownie",
5271
+ slug: "brownie",
5272
+ price: "12.00",
5273
+ description: "Dark, dense, fudgy brownie with a glossy shell. Chocolate four ways. Best slightly warm.",
5274
+ category: "pastries",
5275
+ imageUrl: img("1606313564200-e75d5e30476c"),
5276
+ tags: ["chocolate", "vegetarian"],
5277
+ ingredients: ["Dark chocolate", "Cocoa", "Brown butter", "Eggs"],
5278
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5279
+ calories: 420
5280
+ },
5281
+ {
5282
+ id: "prod_macaron-box",
5283
+ name: "Macaron Box (6)",
5284
+ slug: "macaron-box",
5285
+ price: "60.00",
5286
+ description: "Six French macarons \u2014 pistachio, raspberry, salted caramel, espresso, vanilla bean, and dark chocolate ganache.",
5287
+ category: "pastries",
5288
+ imageUrl: img("1558326567-98ae2405596b"),
5289
+ tags: ["pastry", "gift", "premium"],
5290
+ ingredients: ["Almond flour", "Egg whites", "Sugar", "Various fillings"],
5291
+ allergies: ["nuts", "eggs", "dairy"],
5292
+ calories: 540
5293
+ },
5294
+ // ── Cakes ──
5295
+ {
5296
+ id: "prod_chocolate-cake",
5297
+ name: "Dark Chocolate Layer Cake",
5298
+ slug: "chocolate-cake",
5299
+ price: "22.00",
5300
+ description: "Three layers of dark chocolate sponge with chocolate ganache and cr\xE8me p\xE2tissi\xE8re, finished with a mirror glaze. (Slice)",
5301
+ category: "cakes",
5302
+ imageUrl: img("1565958011703-44f9829ba187"),
5303
+ tags: ["cake", "chocolate", "signature"],
5304
+ isSignature: true,
5305
+ ingredients: ["Dark chocolate", "Cr\xE8me p\xE2tissi\xE8re", "Cocoa", "Eggs"],
5306
+ pairings: ["Espresso", "Cabernet"],
5307
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5308
+ calories: 480
5309
+ },
5310
+ {
5311
+ id: "prod_red-velvet",
5312
+ name: "Red Velvet (Slice)",
5313
+ slug: "red-velvet",
5314
+ price: "22.00",
5315
+ description: "Classic red velvet with cream cheese frosting and white chocolate shavings.",
5316
+ category: "cakes",
5317
+ imageUrl: img("1586788680434-30d324b2d46f"),
5318
+ tags: ["cake"],
5319
+ ingredients: ["Buttermilk", "Cocoa", "Cream cheese", "Beetroot"],
5320
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5321
+ calories: 460
5322
+ },
5323
+ {
5324
+ id: "prod_carrot-cake",
5325
+ name: "Carrot Cake (Slice)",
5326
+ slug: "carrot-cake",
5327
+ price: "20.00",
5328
+ description: "Spiced carrot cake with toasted walnuts, golden raisins, and cream-cheese frosting.",
5329
+ category: "cakes",
5330
+ imageUrl: img("1568827999250-3f6afff96e66"),
5331
+ tags: ["cake", "spiced"],
5332
+ ingredients: ["Carrot", "Walnuts", "Cinnamon", "Cream cheese"],
5333
+ allergies: ["wheat", "gluten", "dairy", "eggs", "nuts"],
5334
+ calories: 420
5335
+ },
5336
+ {
5337
+ id: "prod_cheesecake",
5338
+ name: "New York Cheesecake (Slice)",
5339
+ slug: "cheesecake",
5340
+ price: "24.00",
5341
+ description: "Dense, creamy New York cheesecake on a butter graham crust. Topped with seasonal berry compote.",
5342
+ category: "cakes",
5343
+ imageUrl: img("1567306226416-28f0efdc88ce"),
5344
+ tags: ["cake", "premium"],
5345
+ ingredients: ["Cream cheese", "Sour cream", "Graham crust", "Vanilla"],
5346
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5347
+ calories: 520
5348
+ },
5349
+ {
5350
+ id: "prod_birthday-cake",
5351
+ name: "Custom Celebration Cake",
5352
+ slug: "birthday-cake",
5353
+ price: "250.00",
5354
+ description: "Made-to-order three-tier cake. Choose size, flavour, and frosting. 48-hour notice required for personalisation.",
5355
+ category: "cakes",
5356
+ imageUrl: img("1535254973040-607b474cb50d"),
5357
+ images: [img("1535254973040-607b474cb50d"), img("1565958011703-44f9829ba187")],
5358
+ tags: ["cake", "made-to-order", "premium"],
5359
+ ingredients: ["Sponge cake", "Buttercream", "Custom decoration"],
5360
+ allergies: ["wheat", "gluten", "dairy", "eggs"],
5361
+ productType: "product"
5362
+ },
5363
+ // ── Drinks ──
5364
+ {
5365
+ id: "prod_coffee",
5366
+ name: "Single-Origin Coffee",
5367
+ slug: "coffee",
5368
+ price: "10.00",
5369
+ description: "Locally roasted Arabica from Volta Region. Notes of cocoa, dark fruit, and brown sugar.",
5370
+ category: "drinks",
5371
+ imageUrl: img("1495474472287-4d71bcdd2085"),
5372
+ tags: ["drink", "coffee", "vegan"],
5373
+ ingredients: ["Single-origin Arabica beans"],
5374
+ pairings: ["Croissant", "Brownie"],
5375
+ calories: 5
5376
+ },
5377
+ {
5378
+ id: "prod_tea",
5379
+ name: "Loose-Leaf Tea",
5380
+ slug: "tea",
5381
+ price: "8.00",
5382
+ description: "Choice of English breakfast, Earl Grey, green sencha, or chamomile.",
5383
+ category: "drinks",
5384
+ imageUrl: img("1576092768241-dec231879fc3"),
5385
+ tags: ["drink", "tea", "vegan"],
5386
+ calories: 0
5387
+ },
5388
+ {
5389
+ id: "prod_sobolo",
5390
+ name: "Hibiscus (Sobolo)",
5391
+ slug: "sobolo",
5392
+ price: "12.00",
5393
+ description: "Iced hibiscus tea with ginger, pineapple peel, and a hint of clove. Tart, refreshing, lightly sweet.",
5394
+ category: "drinks",
5395
+ imageUrl: img("1556679343-c7306c1976bc"),
5396
+ tags: ["drink", "vegan", "ghana"],
5397
+ isSignature: true,
5398
+ ingredients: ["Hibiscus", "Ginger", "Pineapple", "Clove"],
5399
+ calories: 60
5400
+ },
5401
+ {
5402
+ id: "prod_orange-juice",
5403
+ name: "Cold-Pressed Orange Juice",
5404
+ slug: "orange-juice",
5405
+ price: "15.00",
5406
+ description: "Hand-pressed at sunrise from Ghanaian Valencia oranges. No sugar, no concentrate.",
5407
+ category: "drinks",
5408
+ imageUrl: img("1622597467836-f3285f2131b8"),
5409
+ tags: ["drink", "vegan"],
5410
+ calories: 110
5411
+ },
5412
+ // ── Boxes (anchor products for bundle/composite) ──
5413
+ {
5414
+ id: "prod_breakfast-box",
5415
+ name: "Breakfast Box",
5416
+ slug: "breakfast-box",
5417
+ price: "30.00",
5418
+ 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.",
5419
+ category: "boxes",
5420
+ imageUrl: img("1533089860892-a7c6f0a88666"),
5421
+ images: [img("1533089860892-a7c6f0a88666"), img("1495474472287-4d71bcdd2085")],
5422
+ tags: ["bundle", "breakfast"],
5423
+ productType: "bundle"
5424
+ },
5425
+ {
5426
+ id: "prod_pastry-box",
5427
+ name: "Build-Your-Own Pastry Box",
5428
+ slug: "pastry-box",
5429
+ price: "60.00",
5430
+ description: "Pick 6 to 12 of our pastries \u2014 mix and match. Freshly boxed for sharing.",
5431
+ category: "boxes",
5432
+ imageUrl: img("1486427944299-d1955d23e34d"),
5433
+ tags: ["composite", "build-your-own", "gift"],
5434
+ productType: "composite"
5435
+ },
5436
+ // ── Merch (Retail card — render_hint=physical) ──
5437
+ {
5438
+ id: "prod_apron",
5439
+ name: "Akua's Bakery Apron",
5440
+ slug: "apron",
5441
+ price: "85.00",
5442
+ description: "Heavy-weight cotton-canvas apron with leather neck strap, embroidered logo, and two front pockets. Made in Accra.",
5443
+ category: "merch",
5444
+ imageUrl: img("1556905055-8f358a7a47b2"),
5445
+ tags: ["merch", "physical"],
5446
+ renderHint: "physical"
5447
+ },
5448
+ {
5449
+ id: "prod_tote",
5450
+ name: "Bakery Tote Bag",
5451
+ slug: "tote-bag",
5452
+ price: "35.00",
5453
+ description: "12oz natural canvas tote, screen-printed with our wheat-stalk crest. Roomy enough for two loaves.",
5454
+ category: "merch",
5455
+ imageUrl: img("1591561954557-26941169b49e"),
5456
+ tags: ["merch", "physical", "gift"],
5457
+ renderHint: "physical",
5458
+ isNew: true
5459
+ },
5460
+ // ── Wholesale (Wholesale card — quantity_pricing tiers) ──
5461
+ {
5462
+ id: "prod_flour-wholesale",
5463
+ name: "Heritage Flour \u2014 25kg Sack",
5464
+ slug: "flour-25kg",
5465
+ price: "320.00",
5466
+ 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.",
5467
+ category: "merch",
5468
+ imageUrl: img("1568254183919-78a4f43a2877"),
5469
+ tags: ["wholesale", "physical", "trade"],
5470
+ renderHint: "physical",
5471
+ quantityPricing: [
5472
+ { min_quantity: 1, unit_price: "320.00" },
5473
+ { min_quantity: 5, unit_price: "295.00" },
5474
+ { min_quantity: 10, unit_price: "270.00" },
5475
+ { min_quantity: 25, unit_price: "245.00" }
5476
+ ]
5477
+ },
5478
+ // ── Digital products (Digital card — type=digital) ──
5479
+ {
5480
+ id: "prod_recipe-ebook",
5481
+ name: "Sourdough Starter eBook",
5482
+ slug: "sourdough-ebook",
5483
+ price: "45.00",
5484
+ description: "PDF + EPUB walkthrough of our sourdough programme \u2014 starter, levain, autolyse, bulk fermentation, shaping, scoring. Lifetime updates.",
5485
+ category: "merch",
5486
+ imageUrl: img("1543002588-bfa74002ed7e"),
5487
+ tags: ["digital", "download"],
5488
+ productType: "digital",
5489
+ digitalProductType: "download"
5490
+ },
5491
+ {
5492
+ id: "prod_gift-card",
5493
+ name: "Bakery Gift Card",
5494
+ slug: "gift-card",
5495
+ price: "100.00",
5496
+ description: "Send instantly. Redeemable in-store and online toward any product. Choose any amount from GH\u20B550 upward.",
5497
+ category: "merch",
5498
+ imageUrl: img("1556905055-8f358a7a47b2"),
5499
+ tags: ["digital", "gift"],
5500
+ productType: "digital",
5501
+ digitalProductType: "gift_code",
5502
+ isSignature: true
5503
+ },
5504
+ // ── Services (Standard service card — type=service + duration_minutes) ──
5505
+ {
5506
+ id: "prod_baking-class",
5507
+ name: "Sourdough Baking Class",
5508
+ slug: "baking-class",
5509
+ price: "320.00",
5510
+ 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.",
5511
+ category: "classes",
5512
+ imageUrl: img("1556909114-44e3e9399a2e"),
5513
+ tags: ["class", "service", "hands-on"],
5514
+ productType: "service",
5515
+ durationMinutes: 120
5516
+ },
5517
+ {
5518
+ id: "prod_pastry-workshop",
5519
+ name: "Lamination Workshop",
5520
+ slug: "pastry-workshop",
5521
+ price: "480.00",
5522
+ description: "Three-hour deep-dive on laminated dough. We cover croissants, danishes, kouign-amann. Aprons supplied; pastries to take home.",
5523
+ category: "classes",
5524
+ imageUrl: img("1517433367423-c7e5b0f35086"),
5525
+ tags: ["class", "service", "advanced"],
5526
+ productType: "service",
5527
+ durationMinutes: 180,
5528
+ isNew: true
5529
+ },
5530
+ // ── Rental service (Rental card — duration_unit=hours) ──
5531
+ {
5532
+ id: "prod_kitchen-rental",
5533
+ name: "Pro Kitchen Rental",
5534
+ slug: "kitchen-rental",
5535
+ price: "180.00",
5536
+ description: "Rent our commercial bakery after hours. Deck oven, retarder-proofer, mixers, scales \u2014 bring your team or shoot your content. Hourly.",
5537
+ category: "classes",
5538
+ imageUrl: img("1556909114-44e3e9399a2e"),
5539
+ tags: ["rental", "service", "commercial"],
5540
+ productType: "service",
5541
+ durationUnit: "hours"
5542
+ },
5543
+ // ── Accommodation (Accommodation card — duration_unit=nights) ──
5544
+ {
5545
+ id: "prod_loft-stay",
5546
+ name: "Bakery Loft \u2014 Weekend Stay",
5547
+ slug: "loft-stay",
5548
+ price: "650.00",
5549
+ description: "One-bedroom loft above the bakery. Wakes up to fresh croissants, ends with a private dinner. Ghana Tourism-licensed. Per night.",
5550
+ category: "classes",
5551
+ imageUrl: img("1564013799919-ab600027ffc6"),
5552
+ tags: ["stay", "service", "premium"],
5553
+ productType: "service",
5554
+ durationUnit: "nights"
5555
+ },
5556
+ // ── Subscription (Subscription card — billing_plans, no duration_minutes) ──
5557
+ {
5558
+ id: "prod_loaf-subscription",
5559
+ name: "Weekly Loaf Subscription",
5560
+ slug: "loaf-subscription",
5561
+ price: "0.00",
5562
+ description: "A fresh loaf delivered to your door every week. Pause or skip any time. Choose monthly or quarterly billing for a discount.",
5563
+ category: "classes",
5564
+ imageUrl: img("1509440159596-0249088772ff"),
5565
+ tags: ["subscription", "service", "delivery"],
5566
+ productType: "service",
5567
+ billingPlans: [
5568
+ {
5569
+ id: "plan_loaf_monthly",
5570
+ name: "Monthly",
5571
+ interval: "month",
5572
+ interval_count: 1,
5573
+ price: "90.00"
5574
+ },
5575
+ {
5576
+ id: "plan_loaf_quarterly",
5577
+ name: "Quarterly (save 10%)",
5578
+ interval: "month",
5579
+ interval_count: 3,
5580
+ price: "243.00"
5581
+ }
5582
+ ]
5583
+ }
5584
+ ];
5585
+ for (const input of productInputs) {
5586
+ const product = makeProduct(input, catIds);
5587
+ registry.products.put(product.id, product);
5588
+ const cat = registry.categories.get(catIds.get(input.category));
5589
+ if (cat) {
5590
+ cat.product_ids.push(product.id);
5591
+ registry.categories.put(cat.id, cat);
5592
+ }
5593
+ }
5594
+ const featuredIds = [
5595
+ "prod_sourdough-loaf",
5596
+ "prod_croissant",
5597
+ "prod_chocolate-cake",
5598
+ "prod_sobolo",
5599
+ "prod_pastry-box"
5600
+ ];
5601
+ const featured = {
4729
5602
  id: "col_featured",
4730
- business_id: businessId,
4731
- name: "Featured",
5603
+ business_id: BUSINESS_ID,
5604
+ name: "Signature & Featured",
4732
5605
  slug: "featured",
4733
- description: "Our most popular items.",
4734
- product_ids: featured,
5606
+ description: "Our most-loved bakes and a few seasonal stars.",
5607
+ product_ids: featuredIds,
4735
5608
  is_active: true,
4736
5609
  created_at: ISO,
4737
5610
  updated_at: ISO
4738
5611
  };
4739
- registry.collections.put(featuredCollection.id, featuredCollection);
4740
- for (const pid of featured) {
5612
+ registry.collections.put(featured.id, featured);
5613
+ for (const pid of featuredIds) {
4741
5614
  const p = registry.products.get(pid);
4742
5615
  if (p) {
4743
- p.collection_ids.push(featuredCollection.id);
5616
+ p.collection_ids.push(featured.id);
4744
5617
  registry.products.put(pid, p);
4745
5618
  }
4746
5619
  }
4747
- const tasting = {
4748
- id: "svc_tasting",
4749
- business_id: businessId,
4750
- name: "Bakery Tasting Session",
4751
- description: "Walk through our pastry menu with our head baker.",
4752
- duration_minutes: 60,
4753
- price: "150.00",
4754
- is_available: true
4755
- };
4756
- registry.services.put(tasting.id, tasting);
4757
- const cake = {
4758
- id: "svc_custom_cake",
4759
- business_id: businessId,
4760
- name: "Custom Cake Consultation",
4761
- description: "Design a custom cake for your event.",
4762
- duration_minutes: 30,
4763
- price: "0.00",
4764
- is_available: true
4765
- };
4766
- registry.services.put(cake.id, cake);
4767
- const sizeAxisId = "axis_size";
4768
- const flavourAxisId = "axis_flavour";
4769
- const sizeValues = [
4770
- { id: "axv_size_small", name: 'Small (8")' },
4771
- { id: "axv_size_medium", name: 'Medium (10")' },
4772
- { id: "axv_size_large", name: 'Large (12")' }
4773
- ];
4774
- const flavourValues = [
4775
- { id: "axv_flavour_chocolate", name: "Chocolate" },
4776
- { id: "axv_flavour_vanilla", name: "Vanilla" },
4777
- { id: "axv_flavour_red_velvet", name: "Red Velvet" }
4778
- ];
4779
5620
  const sizeAxis = {
4780
- id: sizeAxisId,
4781
- business_id: businessId,
5621
+ id: "axis_cake_size",
5622
+ business_id: BUSINESS_ID,
4782
5623
  product_id: "prod_birthday-cake",
4783
5624
  name: "Size",
4784
5625
  display_order: 0,
4785
5626
  affects_recipe: false,
4786
- values: sizeValues.map((v, i) => ({
5627
+ values: [
5628
+ { id: "axv_size_small", name: 'Small (8")', display: 0 },
5629
+ { id: "axv_size_medium", name: 'Medium (10")', display: 1 },
5630
+ { id: "axv_size_large", name: 'Large (12")', display: 2 }
5631
+ ].map((v) => ({
4787
5632
  id: v.id,
4788
- business_id: businessId,
4789
- axis_id: sizeAxisId,
5633
+ business_id: BUSINESS_ID,
5634
+ axis_id: "axis_cake_size",
4790
5635
  name: v.name,
4791
- display_order: i,
5636
+ display_order: v.display,
4792
5637
  created_at: ISO,
4793
5638
  updated_at: ISO
4794
5639
  })),
@@ -4796,18 +5641,22 @@ function seedDefault(registry) {
4796
5641
  updated_at: ISO
4797
5642
  };
4798
5643
  const flavourAxis = {
4799
- id: flavourAxisId,
4800
- business_id: businessId,
5644
+ id: "axis_cake_flavour",
5645
+ business_id: BUSINESS_ID,
4801
5646
  product_id: "prod_birthday-cake",
4802
5647
  name: "Flavour",
4803
5648
  display_order: 1,
4804
5649
  affects_recipe: true,
4805
- values: flavourValues.map((v, i) => ({
5650
+ values: [
5651
+ { id: "axv_flavour_chocolate", name: "Dark Chocolate", display: 0 },
5652
+ { id: "axv_flavour_vanilla", name: "Vanilla Bean", display: 1 },
5653
+ { id: "axv_flavour_red_velvet", name: "Red Velvet", display: 2 }
5654
+ ].map((v) => ({
4806
5655
  id: v.id,
4807
- business_id: businessId,
4808
- axis_id: flavourAxisId,
5656
+ business_id: BUSINESS_ID,
5657
+ axis_id: "axis_cake_flavour",
4809
5658
  name: v.name,
4810
- display_order: i,
5659
+ display_order: v.display,
4811
5660
  created_at: ISO,
4812
5661
  updated_at: ISO
4813
5662
  })),
@@ -4816,40 +5665,42 @@ function seedDefault(registry) {
4816
5665
  };
4817
5666
  registry.variantAxes.put(sizeAxis.id, sizeAxis);
4818
5667
  registry.variantAxes.put(flavourAxis.id, flavourAxis);
4819
- const variantPrices = ["250.00", "350.00", "450.00"];
4820
- const variantIds = [];
4821
- let isFirst = true;
4822
- for (let s = 0; s < sizeValues.length; s++) {
4823
- for (let f = 0; f < flavourValues.length; f++) {
4824
- const id = `var_${sizeValues[s].id.replace("axv_", "")}_${flavourValues[f].id.replace("axv_", "")}`;
5668
+ const sizeAdj = { small: "250.00", medium: "350.00", large: "450.00" };
5669
+ const cakeVariantIds = [];
5670
+ let firstVariant = true;
5671
+ for (const sz of sizeAxis.values) {
5672
+ for (const fl of flavourAxis.values) {
5673
+ const sizeKey = sz.id.replace("axv_size_", "");
5674
+ const flavourKey = fl.id.replace("axv_flavour_", "");
5675
+ const id = `var_size_${sizeKey}_flavour_${flavourKey}`;
4825
5676
  const variant = {
4826
5677
  id,
4827
5678
  product_id: "prod_birthday-cake",
4828
- business_id: businessId,
4829
- name: `${sizeValues[s].name} \u2022 ${flavourValues[f].name}`,
4830
- sku: `BIRTHDAY-${sizeValues[s].id.split("_").pop()}-${flavourValues[f].id.split("_").pop()}`,
4831
- price_adjustment: variantPrices[s],
5679
+ business_id: BUSINESS_ID,
5680
+ name: `${sz.name} \u2022 ${fl.name}`,
5681
+ sku: `BIRTHDAY-${sizeKey.toUpperCase()}-${flavourKey.toUpperCase()}`,
5682
+ price_adjustment: sizeAdj[sizeKey] ?? "0.00",
4832
5683
  component_multiplier: "1.00",
4833
- is_default: isFirst,
5684
+ is_default: firstVariant,
4834
5685
  is_active: true,
4835
- axis_value_ids: [sizeValues[s].id, flavourValues[f].id],
5686
+ axis_value_ids: [sz.id, fl.id],
4836
5687
  created_at: ISO,
4837
5688
  updated_at: ISO
4838
5689
  };
4839
5690
  registry.variants.put(id, variant);
4840
- variantIds.push(id);
4841
- isFirst = false;
5691
+ cakeVariantIds.push(id);
5692
+ firstVariant = false;
4842
5693
  }
4843
5694
  }
4844
- const birthday = registry.products.get("prod_birthday-cake");
4845
- if (birthday) {
4846
- birthday.variant_ids = variantIds;
4847
- registry.products.put(birthday.id, birthday);
5695
+ const birthdayProduct = registry.products.get("prod_birthday-cake");
5696
+ if (birthdayProduct) {
5697
+ birthdayProduct.variant_ids = cakeVariantIds;
5698
+ registry.products.put(birthdayProduct.id, birthdayProduct);
4848
5699
  }
4849
5700
  const toppingsAddOn = {
4850
5701
  id: "addon_toppings",
4851
- business_id: businessId,
4852
- name: "Toppings",
5702
+ business_id: BUSINESS_ID,
5703
+ name: "Cake toppings",
4853
5704
  is_multiple_allowed: true,
4854
5705
  is_required: false,
4855
5706
  is_mutually_exclusive: false,
@@ -4858,15 +5709,14 @@ function seedDefault(registry) {
4858
5709
  created_at: ISO,
4859
5710
  updated_at: ISO,
4860
5711
  options: [
4861
- { 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 },
4862
- { 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 },
4863
- { 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 }
5712
+ { 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 },
5713
+ { 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 },
5714
+ { 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 }
4864
5715
  ]
4865
5716
  };
4866
- registry.addOns.put(toppingsAddOn.id, toppingsAddOn);
4867
5717
  const milkAddOn = {
4868
5718
  id: "addon_milk",
4869
- business_id: businessId,
5719
+ business_id: BUSINESS_ID,
4870
5720
  name: "Milk",
4871
5721
  is_multiple_allowed: false,
4872
5722
  is_required: false,
@@ -4876,17 +5726,17 @@ function seedDefault(registry) {
4876
5726
  created_at: ISO,
4877
5727
  updated_at: ISO,
4878
5728
  options: [
4879
- { 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 },
4880
- { 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 },
4881
- { 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 }
5729
+ { 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 },
5730
+ { 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 },
5731
+ { 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 }
4882
5732
  ]
4883
5733
  };
5734
+ registry.addOns.put(toppingsAddOn.id, toppingsAddOn);
4884
5735
  registry.addOns.put(milkAddOn.id, milkAddOn);
4885
5736
  const cakeProductIds = ["prod_chocolate-cake", "prod_red-velvet", "prod_carrot-cake", "prod_cheesecake", "prod_birthday-cake"];
4886
5737
  const drinkProductIds = ["prod_coffee", "prod_tea"];
4887
5738
  for (const pid of cakeProductIds) {
4888
- const link = { product_id: pid, add_on_id: toppingsAddOn.id };
4889
- registry.productAddOns.put(`${pid}:${toppingsAddOn.id}`, link);
5739
+ registry.productAddOns.put(`${pid}:${toppingsAddOn.id}`, { product_id: pid, add_on_id: toppingsAddOn.id });
4890
5740
  const p = registry.products.get(pid);
4891
5741
  if (p) {
4892
5742
  p.add_on_ids.push(toppingsAddOn.id);
@@ -4894,8 +5744,7 @@ function seedDefault(registry) {
4894
5744
  }
4895
5745
  }
4896
5746
  for (const pid of drinkProductIds) {
4897
- const link = { product_id: pid, add_on_id: milkAddOn.id };
4898
- registry.productAddOns.put(`${pid}:${milkAddOn.id}`, link);
5747
+ registry.productAddOns.put(`${pid}:${milkAddOn.id}`, { product_id: pid, add_on_id: milkAddOn.id });
4899
5748
  const p = registry.products.get(pid);
4900
5749
  if (p) {
4901
5750
  p.add_on_ids.push(milkAddOn.id);
@@ -4904,7 +5753,7 @@ function seedDefault(registry) {
4904
5753
  }
4905
5754
  const breakfastBundle = {
4906
5755
  id: "bnd_breakfast_box",
4907
- business_id: businessId,
5756
+ business_id: BUSINESS_ID,
4908
5757
  product_id: "prod_breakfast-box",
4909
5758
  name: "Breakfast Box",
4910
5759
  slug: "breakfast-box",
@@ -4916,29 +5765,9 @@ function seedDefault(registry) {
4916
5765
  updated_at: ISO
4917
5766
  };
4918
5767
  registry.bundles.put(breakfastBundle.id, breakfastBundle);
4919
- const breakfastProduct = {
4920
- id: "prod_breakfast-box",
4921
- business_id: businessId,
4922
- name: "Breakfast Box",
4923
- slug: "breakfast-box",
4924
- description: "Bundled breakfast for one.",
4925
- product_type: "bundle",
4926
- base_price: "30.00",
4927
- currency: "GHS",
4928
- image: "/img/breakfast-box.jpg",
4929
- images: ["/img/breakfast-box.jpg"],
4930
- is_available: true,
4931
- category_ids: [categoryIdsBySlug.get("pastries")],
4932
- collection_ids: [],
4933
- add_on_ids: [],
4934
- variant_ids: [],
4935
- created_at: ISO,
4936
- updated_at: ISO
4937
- };
4938
- registry.products.put(breakfastProduct.id, breakfastProduct);
4939
5768
  const pastryComposite = {
4940
5769
  id: "comp_pastry_box",
4941
- business_id: businessId,
5770
+ business_id: BUSINESS_ID,
4942
5771
  product_id: "prod_pastry-box",
4943
5772
  base_price: "60.00",
4944
5773
  pricing_mode: "additive",
@@ -4950,43 +5779,51 @@ function seedDefault(registry) {
4950
5779
  name: "Choose your pastries",
4951
5780
  min: 6,
4952
5781
  max: 12,
4953
- component_ids: ["prod_croissant", "prod_pain-au-chocolat", "prod_almond-danish", "prod_cinnamon-roll", "prod_brownie"]
5782
+ component_ids: [
5783
+ "prod_croissant",
5784
+ "prod_pain-au-chocolat",
5785
+ "prod_almond-danish",
5786
+ "prod_cinnamon-roll",
5787
+ "prod_brownie",
5788
+ "prod_banana-bread",
5789
+ "prod_macaron-box"
5790
+ ]
4954
5791
  }
4955
5792
  ],
4956
5793
  created_at: ISO,
4957
5794
  updated_at: ISO
4958
5795
  };
4959
5796
  registry.composites.put(pastryComposite.id, pastryComposite);
4960
- const pastryBoxProduct = {
4961
- id: "prod_pastry-box",
4962
- business_id: businessId,
4963
- name: "Build-Your-Own Pastry Box",
4964
- slug: "pastry-box",
4965
- description: "Pick 6 to 12 pastries \u2014 mix and match.",
4966
- product_type: "composite",
4967
- base_price: "60.00",
4968
- currency: "GHS",
4969
- image: "/img/pastry-box.jpg",
4970
- images: ["/img/pastry-box.jpg"],
4971
- is_available: true,
4972
- category_ids: [categoryIdsBySlug.get("pastries")],
4973
- collection_ids: [],
4974
- add_on_ids: [],
4975
- variant_ids: [],
4976
- created_at: ISO,
4977
- updated_at: ISO
5797
+ const tasting = {
5798
+ id: "svc_tasting",
5799
+ business_id: BUSINESS_ID,
5800
+ name: "Bakery Tasting Session",
5801
+ description: "Sample our pastry menu with our head baker.",
5802
+ duration_minutes: 60,
5803
+ price: "150.00",
5804
+ is_available: true
4978
5805
  };
4979
- registry.products.put(pastryBoxProduct.id, pastryBoxProduct);
5806
+ const consult = {
5807
+ id: "svc_custom_cake",
5808
+ business_id: BUSINESS_ID,
5809
+ name: "Custom Cake Consultation",
5810
+ description: "Design a custom cake for your event.",
5811
+ duration_minutes: 30,
5812
+ price: "0.00",
5813
+ is_available: true
5814
+ };
5815
+ registry.services.put(tasting.id, tasting);
5816
+ registry.services.put(consult.id, consult);
4980
5817
  const tags = [
4981
- { id: "tag_signature", business_id: businessId, name: "Signature", slug: "signature", color: "#d97706", sort_order: 0, usage_count: 5, created_at: ISO, updated_at: ISO },
4982
- { id: "tag_vegan", business_id: businessId, name: "Vegan", slug: "vegan", color: "#16a34a", sort_order: 1, usage_count: 3, created_at: ISO, updated_at: ISO },
4983
- { 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 },
4984
- { id: "tag_seasonal", business_id: businessId, name: "Seasonal", slug: "seasonal", color: "#dc2626", sort_order: 3, usage_count: 4, created_at: ISO, updated_at: ISO }
5818
+ { 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 },
5819
+ { 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 },
5820
+ { 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 },
5821
+ { 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 }
4985
5822
  ];
4986
5823
  for (const t of tags) registry.tags.put(t.id, t);
4987
5824
  const baked = {
4988
5825
  id: "tax_baked_goods",
4989
- business_id: businessId,
5826
+ business_id: BUSINESS_ID,
4990
5827
  name: "Baked Goods",
4991
5828
  slug: "baked-goods",
4992
5829
  path: ["Food", "Baked Goods"],
@@ -4996,7 +5833,7 @@ function seedDefault(registry) {
4996
5833
  };
4997
5834
  const beverages = {
4998
5835
  id: "tax_beverages",
4999
- business_id: businessId,
5836
+ business_id: BUSINESS_ID,
5000
5837
  name: "Beverages",
5001
5838
  slug: "beverages",
5002
5839
  path: ["Food", "Beverages"],
@@ -5009,13 +5846,13 @@ function seedDefault(registry) {
5009
5846
  const attrs = [
5010
5847
  {
5011
5848
  id: "attr_allergens",
5012
- business_id: businessId,
5849
+ business_id: BUSINESS_ID,
5013
5850
  namespace: "core",
5014
5851
  name: "Allergens",
5015
5852
  slug: "allergens",
5016
5853
  description: "Common allergens present in this item.",
5017
5854
  attribute_type: "multi_select",
5018
- options: ["wheat", "eggs", "dairy", "nuts", "soy"],
5855
+ options: ["wheat", "gluten", "eggs", "dairy", "nuts", "soy", "sesame"],
5019
5856
  is_required: false,
5020
5857
  is_filterable: true,
5021
5858
  visibility: "public",
@@ -5026,7 +5863,7 @@ function seedDefault(registry) {
5026
5863
  },
5027
5864
  {
5028
5865
  id: "attr_shelf_life",
5029
- business_id: businessId,
5866
+ business_id: BUSINESS_ID,
5030
5867
  namespace: "core",
5031
5868
  name: "Shelf life",
5032
5869
  slug: "shelf-life",
@@ -5041,7 +5878,7 @@ function seedDefault(registry) {
5041
5878
  },
5042
5879
  {
5043
5880
  id: "attr_caffeine",
5044
- business_id: businessId,
5881
+ business_id: BUSINESS_ID,
5045
5882
  namespace: "core",
5046
5883
  name: "Caffeine",
5047
5884
  slug: "caffeine",
@@ -5056,7 +5893,7 @@ function seedDefault(registry) {
5056
5893
  },
5057
5894
  {
5058
5895
  id: "attr_seasonal",
5059
- business_id: businessId,
5896
+ business_id: BUSINESS_ID,
5060
5897
  namespace: "core",
5061
5898
  name: "Seasonal",
5062
5899
  slug: "seasonal",
@@ -5074,10 +5911,10 @@ function seedDefault(registry) {
5074
5911
  const articles = [
5075
5912
  {
5076
5913
  id: "kb_delivery",
5077
- business_id: businessId,
5078
- title: "Delivery & Pickup",
5914
+ business_id: BUSINESS_ID,
5915
+ title: "Delivery & pickup",
5079
5916
  slug: "delivery-pickup",
5080
- content: "Same-day delivery within Accra for orders placed before 2pm. Pickup available 8am\u20138pm.",
5917
+ content: "Same-day delivery within Greater Accra for orders placed before 2pm. Pickup available 8am\u20138pm at our East Legon location.",
5081
5918
  category: "logistics",
5082
5919
  tags: ["delivery", "pickup"],
5083
5920
  created_at: ISO,
@@ -5085,10 +5922,10 @@ function seedDefault(registry) {
5085
5922
  },
5086
5923
  {
5087
5924
  id: "kb_custom_cakes",
5088
- business_id: businessId,
5089
- title: "Ordering custom cakes",
5925
+ business_id: BUSINESS_ID,
5926
+ title: "Custom cakes & celebrations",
5090
5927
  slug: "custom-cakes",
5091
- content: "Custom cakes require 48 hours notice. Book a consultation through the bookings page.",
5928
+ content: "Custom cakes require 48 hours notice. Book a consultation through the bookings page to walk through flavours, sizes, and decoration.",
5092
5929
  category: "products",
5093
5930
  tags: ["cakes", "custom"],
5094
5931
  created_at: ISO,
@@ -5096,10 +5933,10 @@ function seedDefault(registry) {
5096
5933
  },
5097
5934
  {
5098
5935
  id: "kb_allergens",
5099
- business_id: businessId,
5100
- title: "Allergens",
5936
+ business_id: BUSINESS_ID,
5937
+ title: "Allergen information",
5101
5938
  slug: "allergens",
5102
- content: "Our kitchen handles wheat, dairy, eggs, and nuts. Cross-contamination is possible.",
5939
+ content: "Our kitchen handles wheat, dairy, eggs, and nuts. Cross-contamination is possible \u2014 let us know about severe allergies before ordering.",
5103
5940
  category: "products",
5104
5941
  tags: ["allergens", "safety"],
5105
5942
  created_at: ISO,
@@ -5107,7 +5944,7 @@ function seedDefault(registry) {
5107
5944
  }
5108
5945
  ];
5109
5946
  for (const a of articles) registry.knowledgeArticles.put(a.id, a);
5110
- return { businessId };
5947
+ return { businessId: BUSINESS_ID };
5111
5948
  }
5112
5949
 
5113
5950
  // src/mock/seeds/empty.ts
@@ -7210,195 +8047,47 @@ function createDeps(options = {}) {
7210
8047
  }
7211
8048
 
7212
8049
  // src/mock/server.ts
8050
+ var PACKAGE_NAME = "@cimplify/sdk/mock";
8051
+ function corsMiddleware(allow) {
8052
+ return async (c, next) => {
8053
+ const origin = c.req.header("origin") ?? "";
8054
+ const allowOrigin = allow === "*" ? "*" : allow.includes(origin) ? origin : allow[0] ?? "*";
8055
+ const headers = {
8056
+ "Access-Control-Allow-Origin": allowOrigin,
8057
+ "Access-Control-Allow-Credentials": "true",
8058
+ "Access-Control-Allow-Headers": c.req.header("access-control-request-headers") ?? "content-type,authorization,x-session-token,idempotency-key",
8059
+ "Access-Control-Allow-Methods": "GET,POST,PATCH,DELETE,OPTIONS",
8060
+ "Access-Control-Expose-Headers": "x-session-token",
8061
+ "Access-Control-Max-Age": "86400",
8062
+ Vary: "Origin"
8063
+ };
8064
+ if (c.req.method === "OPTIONS") {
8065
+ return new Response(null, { status: 204, headers });
8066
+ }
8067
+ for (const [k, v] of Object.entries(headers)) c.header(k, v);
8068
+ await next();
8069
+ };
8070
+ }
7213
8071
  function createMockApp(options = {}) {
7214
8072
  const deps = createDeps(options);
7215
8073
  const app = new Hono2();
7216
- const corsOrigins = options.cors === "*" ? "*" : options.cors;
7217
- if (corsOrigins) {
7218
- app.use("*", async (c, next) => {
7219
- const origin = c.req.header("origin") ?? "";
7220
- const allow = corsOrigins === "*" ? "*" : corsOrigins.includes(origin) ? origin : corsOrigins[0] ?? "*";
7221
- const headers = {
7222
- "Access-Control-Allow-Origin": allow,
7223
- "Access-Control-Allow-Credentials": "true",
7224
- "Access-Control-Allow-Headers": c.req.header("access-control-request-headers") ?? "content-type,authorization,x-session-token,idempotency-key",
7225
- "Access-Control-Allow-Methods": "GET,POST,PATCH,DELETE,OPTIONS",
7226
- "Access-Control-Expose-Headers": "x-session-token",
7227
- "Access-Control-Max-Age": "86400",
7228
- Vary: "Origin"
7229
- };
7230
- if (c.req.method === "OPTIONS") {
7231
- return new Response(null, { status: 204, headers });
7232
- }
7233
- for (const [k, v] of Object.entries(headers)) c.header(k, v);
7234
- await next();
7235
- });
7236
- }
8074
+ const cors = options.cors ?? "*";
8075
+ if (cors) app.use("*", corsMiddleware(cors));
7237
8076
  app.get(
7238
8077
  "/",
7239
8078
  (c) => c.json({
7240
- name: "@cimplify/sdk/mock",
7241
- version: "0.36.0",
8079
+ name: PACKAGE_NAME,
7242
8080
  docs: "https://github.com/cimplify/cimplify",
7243
8081
  api: "/api/v1",
7244
8082
  admin: "/_mock",
7245
8083
  elements: "/elements",
7246
- seed_business_id: deps.defaultBusinessId
8084
+ images: "/img/<slug>",
8085
+ seed_business_id: deps.defaultBusinessId,
8086
+ time: deps.clock.iso()
7247
8087
  })
7248
8088
  );
7249
8089
  app.get("/healthz", (c) => c.json({ ok: true, time: deps.clock.iso() }));
7250
- const UNSPLASH_PHOTOS = {
7251
- // Akua's Bakery (default seed)
7252
- sourdough: "1509440159596-0249088772ff",
7253
- wheat: "1486887396153-fa416526c108",
7254
- baguette: "1568471173242-461f0a730452",
7255
- focaccia: "1573472635068-fd9b1abdd8e5",
7256
- croissant: "1555507036-ab1f4038808a",
7257
- pac: "1530610476181-d83430b64dcd",
7258
- danish: "1517433367423-c7e5b0f35086",
7259
- cinnamon: "1583527976767-50068e72b7a3",
7260
- "choc-cake": "1565958011703-44f9829ba187",
7261
- "red-velvet": "1586788680434-30d324b2d46f",
7262
- carrot: "1568827999250-3f6afff96e66",
7263
- cheesecake: "1567306226416-28f0efdc88ce",
7264
- coffee: "1495474472287-4d71bcdd2085",
7265
- tea: "1576092768241-dec231879fc3",
7266
- sobolo: "1556679343-c7306c1976bc",
7267
- oj: "1600271886742-f049b969dc7a",
7268
- banana: "1632931057819-4eefffa8e007",
7269
- brownie: "1551024506-0bccd828d307",
7270
- macaron: "1558326567-98ae2405596b",
7271
- birthday: "1535254973040-607b474cb50d",
7272
- "breakfast-box": "1533089860892-a7c6f0a88666",
7273
- "pastry-box": "1486427944299-d1955d23e34d",
7274
- // Mama's Kitchen (restaurant seed)
7275
- kelewele: "1585536830049-aedf5c1b98a4",
7276
- "spring-rolls": "1606755962773-d324e0a13086",
7277
- "soup-of-the-day": "1547592180-85f173990554",
7278
- "jollof-rice": "1604329760661-e71dc83f8f26",
7279
- "banku-tilapia": "1559847844-5315695dadae",
7280
- "fufu-light-soup": "1547928576-b822bcced36e",
7281
- waakye: "1604329760661-e71dc83f8f26",
7282
- "grilled-chicken": "1532550907401-a500c9a57435",
7283
- burger: "1568901346375-23c9450c58cd",
7284
- "yam-fries": "1573080496219-bb080dd4f877",
7285
- "fried-plantain": "1604329760661-e71dc83f8f26",
7286
- salad: "1512621776951-a57141f2eefd",
7287
- "ice-cream": "1497034825429-c343d7c6a68f",
7288
- "cake-slice": "1565958011703-44f9829ba187",
7289
- "fruit-bowl": "1490474418585-ba9bad8fd0ea",
7290
- "soft-drink": "1497534446932-c925b458314e",
7291
- "fresh-juice": "1622597467836-f3285f2131b8",
7292
- water: "1548839140-29a749e1cf4d",
7293
- feast: "1546069901-ba9599a7e63c",
7294
- byo: "1546069901-ba9599a7e63c",
7295
- // Currents Electronics
7296
- "android-flagship": "1511707171634-5f897ff02aa9",
7297
- "android-mid": "1592899677977-9c10ca588bbd",
7298
- "phone-budget": "1574944985070-8f3ebc6b79d2",
7299
- "laptop-pro": "1496181133206-80ce9b88a853",
7300
- "laptop-air": "1517336714731-489689fd1ca8",
7301
- "laptop-budget": "1541807084-5c52b6b3adef",
7302
- "headphones-anc": "1583394838336-acd977736f90",
7303
- "earbuds-pro": "1606220945770-b5b6c2c55bf1",
7304
- "speaker-bt": "1608043152269-423dbba4e7e1",
7305
- "phone-case": "1601593346740-925612772716",
7306
- "screen-protector": "1592890288564-76628a30a657",
7307
- "fast-charger": "1583394838336-acd977736f90",
7308
- "laptop-sleeve": "1525547719571-a2d4ac8945e2",
7309
- "wireless-mouse": "1527864550417-7fd91fc51a46",
7310
- "mech-keyboard": "1587829741301-dc798b83add3",
7311
- "gaming-controller": "1592840496694-26d035b52b48",
7312
- "gaming-headset": "1591105327764-407a4a06e768",
7313
- "console-handheld": "1606144042614-b2417e99c4e3",
7314
- wfh: "1593642632559-0c6d3fc62b89",
7315
- "game-pack": "1606144042614-b2417e99c4e3",
7316
- // Reesa apparel
7317
- "ankara-shirt": "1591047139829-d91aecb6caea",
7318
- "agbada-set": "1605497788044-5a32c7078486",
7319
- kaftan: "1554412933-514a83d2f3c8",
7320
- "leather-tote": "1584917865442-de89df76afd3",
7321
- sandals: "1543163521-1bf539c55dd2",
7322
- "beaded-bracelet": "1611591437281-460bfbe1220a",
7323
- owambe: "1605497788044-5a32c7078486",
7324
- // FreshMart
7325
- "tomatoes-1kg": "1546470427-227f1c75a9ef",
7326
- "onions-1kg": "1620574387735-3624d75b2dbc",
7327
- "ginger-200g": "1606937330526-f5e6a2a83555",
7328
- "bananas-bunch": "1571771894821-ce9b6c11b08e",
7329
- pineapple: "1550258987-190a2d41a8ba",
7330
- "spinach-bunch": "1576045057995-568f588f82fb",
7331
- "milk-1l": "1550583724-b2692b85b150",
7332
- "yogurt-500g": "1571212515416-fef01fc43637",
7333
- "eggs-12": "1582722872445-44dc5f7e3c8f",
7334
- "cheese-200g": "1486297678162-eb2a19b0a32d",
7335
- "chicken-1kg": "1604503468506-a8da13d82791",
7336
- "beef-mince-500g": "1607623814075-e51df1bdc82f",
7337
- "tilapia-each": "1546069901-ba9599a7e63c",
7338
- "rice-5kg": "1586201375761-83865001e31c",
7339
- "garri-2kg": "1604329760661-e71dc83f8f26",
7340
- "palm-oil-1l": "1474979266404-7eaacbcd87c5",
7341
- "vegetable-oil-1l": "1474979266404-7eaacbcd87c5",
7342
- "salt-1kg": "1612531386530-97286d97c2d2",
7343
- "sugar-1kg": "1581281697996-3aef0c3b9b12",
7344
- "tomato-paste": "1546470427-227f1c75a9ef",
7345
- "soap-bar": "1583912267550-d6c2ac3196c0",
7346
- "detergent-1kg": "1583912267550-d6c2ac3196c0",
7347
- "tissue-pack": "1610557892470-55d9e80c0bce",
7348
- "trash-bags": "1583912267550-d6c2ac3196c0",
7349
- essentials: "1542838132-92c53300491e",
7350
- // Serene Spa services
7351
- "swedish-massage-60": "1544161515-4ab6ce6db874",
7352
- "swedish-massage-90": "1544161515-4ab6ce6db874",
7353
- "deep-tissue-60": "1600334129128-685c5582fd35",
7354
- "couples-massage": "1540555700478-4be289fbecef",
7355
- "hot-stone-90": "1600334129128-685c5582fd35",
7356
- "facial-classic": "1570172619644-dfd03ed5d881",
7357
- "facial-anti-aging": "1571019613454-1cb2f99b2d8b",
7358
- "facial-acne": "1571019613454-1cb2f99b2d8b",
7359
- manicure: "1604654894610-df63bc536371",
7360
- pedicure: "1610701596007-11502861dcfa",
7361
- "gel-manicure": "1604654894610-df63bc536371",
7362
- haircut: "1562322140-8baeececf3df",
7363
- "hair-treatment": "1522337360788-8b13dee7a37e",
7364
- color: "1562322140-8baeececf3df",
7365
- "yoga-session": "1545205597-3d9d02c29597",
7366
- meditation: "1545389336-cf090694435e",
7367
- "spa-day": "1540555700478-4be289fbecef",
7368
- "couples-retreat": "1540555700478-4be289fbecef",
7369
- "custom-experience": "1545389336-cf090694435e"
7370
- };
7371
- app.get("/img/*", (c) => {
7372
- const path = c.req.path.replace(/^\/img\//, "").replace(/\.[a-z0-9]+$/i, "");
7373
- const slug = path.split("/").pop() ?? "";
7374
- const photoId = UNSPLASH_PHOTOS[slug];
7375
- if (photoId) {
7376
- return c.redirect(`https://images.unsplash.com/photo-${photoId}?w=600&h=600&fit=crop&auto=format&q=80`, 302);
7377
- }
7378
- const label = slug.replace(/[-_]+/g, " ").trim() || "product";
7379
- let hash = 0;
7380
- for (let i = 0; i < slug.length; i++) hash = hash * 31 + slug.charCodeAt(i) >>> 0;
7381
- const hue = hash % 360;
7382
- const display = label.length > 20 ? label.slice(0, 20) + "\u2026" : label;
7383
- const svg = `<?xml version="1.0" encoding="UTF-8"?>
7384
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
7385
- <defs>
7386
- <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
7387
- <stop offset="0%" stop-color="hsl(${hue}, 65%, 65%)"/>
7388
- <stop offset="100%" stop-color="hsl(${(hue + 40) % 360}, 70%, 45%)"/>
7389
- </linearGradient>
7390
- </defs>
7391
- <rect width="400" height="400" fill="url(#g)"/>
7392
- <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>
7393
- </svg>`;
7394
- return new Response(svg, {
7395
- headers: {
7396
- "Content-Type": "image/svg+xml; charset=utf-8",
7397
- "Cache-Control": "public, max-age=3600",
7398
- "Access-Control-Allow-Origin": "*"
7399
- }
7400
- });
7401
- });
8090
+ mountImageRoutes(app);
7402
8091
  app.route("/api/v1", buildV1(deps));
7403
8092
  app.route("/_mock", buildAdmin(deps));
7404
8093
  app.route("/elements", buildElements(deps));
@@ -7406,7 +8095,11 @@ function createMockApp(options = {}) {
7406
8095
  (c) => c.json(
7407
8096
  {
7408
8097
  success: false,
7409
- error: { code: "ROUTE_NOT_REGISTERED", message: `Route ${c.req.method} ${c.req.path} not handled by mock`, retryable: false },
8098
+ error: {
8099
+ code: "ROUTE_NOT_REGISTERED",
8100
+ message: `Route ${c.req.method} ${c.req.path} not handled by mock`,
8101
+ retryable: false
8102
+ },
7410
8103
  hint: "See /api/v1 for the SDK surface and /_mock for the admin surface."
7411
8104
  },
7412
8105
  501