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