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