@cimplify/sdk 0.44.11 → 0.44.14

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 (50) hide show
  1. package/dist/mock/cli.mjs +171 -23
  2. package/dist/mock/library.d.mts +2 -2
  3. package/dist/mock/library.d.ts +2 -2
  4. package/dist/mock/library.js +171 -23
  5. package/dist/mock/library.mjs +171 -23
  6. package/dist/mock/msw.d.mts +1 -1
  7. package/dist/mock/msw.d.ts +1 -1
  8. package/dist/mock/msw.js +171 -23
  9. package/dist/mock/msw.mjs +171 -23
  10. package/dist/{server-CUbuQxOV.d.mts → server-CnECAQDQ.d.mts} +28 -6
  11. package/dist/{server-CUbuQxOV.d.ts → server-CnECAQDQ.d.ts} +28 -6
  12. package/package.json +1 -1
  13. package/registry/account.json +15 -0
  14. package/registry/billing-plan-selector.json +16 -0
  15. package/registry/cart-drawer.json +17 -0
  16. package/registry/currency-selector.json +15 -0
  17. package/registry/customer-input-fields.json +15 -0
  18. package/registry/delivery-estimate.json +16 -0
  19. package/registry/index.json +105 -0
  20. package/registry/location-picker.json +15 -0
  21. package/registry/recently-viewed.json +16 -0
  22. package/registry/recommendation-carousel.json +16 -0
  23. package/registry/resource-picker.json +15 -0
  24. package/registry/session-message-banner.json +15 -0
  25. package/templates/storefront-bakery/AGENTS.md +12 -0
  26. package/templates/storefront-bakery/app/layout.tsx +2 -0
  27. package/templates/storefront-bakery/components/cart-drawer.tsx +14 -0
  28. package/templates/storefront-bakery/components/cart-pill.tsx +11 -7
  29. package/templates/storefront-bakery/components/providers.tsx +6 -2
  30. package/templates/storefront-fashion/AGENTS.md +21 -1
  31. package/templates/storefront-grocery/AGENTS.md +12 -0
  32. package/templates/storefront-grocery/app/layout.tsx +2 -0
  33. package/templates/storefront-grocery/components/cart-drawer.tsx +14 -0
  34. package/templates/storefront-grocery/components/cart-pill.tsx +11 -7
  35. package/templates/storefront-grocery/components/providers.tsx +6 -2
  36. package/templates/storefront-restaurant/AGENTS.md +12 -0
  37. package/templates/storefront-restaurant/app/layout.tsx +2 -0
  38. package/templates/storefront-restaurant/components/cart-drawer.tsx +14 -0
  39. package/templates/storefront-restaurant/components/cart-pill.tsx +11 -7
  40. package/templates/storefront-restaurant/components/providers.tsx +6 -2
  41. package/templates/storefront-retail/AGENTS.md +12 -0
  42. package/templates/storefront-retail/app/layout.tsx +2 -0
  43. package/templates/storefront-retail/components/cart-drawer.tsx +14 -0
  44. package/templates/storefront-retail/components/cart-pill.tsx +11 -7
  45. package/templates/storefront-retail/components/providers.tsx +6 -2
  46. package/templates/storefront-services/AGENTS.md +12 -0
  47. package/templates/storefront-services/app/layout.tsx +2 -0
  48. package/templates/storefront-services/components/cart-drawer.tsx +14 -0
  49. package/templates/storefront-services/components/cart-pill.tsx +11 -7
  50. package/templates/storefront-services/components/providers.tsx +6 -2
package/dist/mock/msw.js CHANGED
@@ -2137,6 +2137,44 @@ function authMiddleware(deps) {
2137
2137
  await next();
2138
2138
  };
2139
2139
  }
2140
+ var IDEMPOTENT_PATH_RE = /\/(cart\/items|cart\/coupons|checkout|payments\/authorization|auth\/(request-otp|verify-otp|profile|logout))(\/|$)/;
2141
+ var IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1e3;
2142
+ function idempotencyMiddleware(deps) {
2143
+ return async (c, next) => {
2144
+ if (c.req.method !== "POST" || !IDEMPOTENT_PATH_RE.test(c.req.path)) {
2145
+ return next();
2146
+ }
2147
+ const key = c.req.header("idempotency-key") ?? c.req.header("Idempotency-Key");
2148
+ if (!key || key.length > 200) return next();
2149
+ const session = c.var.session;
2150
+ const recordId = `${session.id}:${key}`;
2151
+ const existing = deps.registry.idempotency.get(recordId);
2152
+ if (existing) {
2153
+ const expiresAt = Date.parse(existing.expires_at);
2154
+ if (Number.isFinite(expiresAt) && expiresAt > Date.now()) {
2155
+ return c.json(existing.response, existing.status);
2156
+ }
2157
+ deps.registry.idempotency.delete(recordId);
2158
+ }
2159
+ await next();
2160
+ if (c.res.status >= 200 && c.res.status < 300) {
2161
+ try {
2162
+ const cloned = c.res.clone();
2163
+ const response = await cloned.json();
2164
+ const now = Date.now();
2165
+ deps.registry.idempotency.put(recordId, {
2166
+ key: recordId,
2167
+ request_hash: "",
2168
+ response,
2169
+ status: c.res.status,
2170
+ created_at: new Date(now).toISOString(),
2171
+ expires_at: new Date(now + IDEMPOTENCY_TTL_MS).toISOString()
2172
+ });
2173
+ } catch {
2174
+ }
2175
+ }
2176
+ };
2177
+ }
2140
2178
  function chaosMiddleware(deps) {
2141
2179
  return async (c, next) => {
2142
2180
  const path = c.req.path;
@@ -2187,6 +2225,7 @@ function buildV1(deps) {
2187
2225
  const app = new Hono2();
2188
2226
  app.use("*", chaosMiddleware(deps));
2189
2227
  app.use("*", authMiddleware(deps));
2228
+ app.use("*", idempotencyMiddleware(deps));
2190
2229
  app.get("/bootstrap", (c) => {
2191
2230
  const session = c.var.session;
2192
2231
  return tryRoute(c, () => ({
@@ -2263,9 +2302,27 @@ function buildV1(deps) {
2263
2302
  (c) => tryRoute(c, () => ({ results: deps.catalogue.searchKnowledgeBase(deps.defaultBusinessId, c.req.query("q") ?? ""), query: c.req.query("q") ?? "" }))
2264
2303
  );
2265
2304
  app.get("/catalogue/products/:product_id/attributes", (c) => tryRoute(c, () => deps.catalogue.getProductAttributes(c.req.param("product_id"))));
2266
- app.get("/catalogue/products/:product_id/available-now", (c) => tryRoute(c, () => ({ product_id: c.req.param("product_id"), is_available: true })));
2267
- app.get("/catalogue/products/:product_id/billing-plans/eligible", (c) => tryRoute(c, () => ({ plans: [] })));
2268
- app.get("/catalogue/products/:product_id/schedules", (c) => tryRoute(c, () => ({ schedules: [] })));
2305
+ app.get(
2306
+ "/catalogue/products/:product_id/available-now",
2307
+ (c) => tryRoute(c, () => {
2308
+ const p = deps.registry.products.get(c.req.param("product_id"));
2309
+ return { product_id: c.req.param("product_id"), is_available: p?.is_available ?? false };
2310
+ })
2311
+ );
2312
+ app.get(
2313
+ "/catalogue/products/:product_id/billing-plans/eligible",
2314
+ (c) => tryRoute(c, () => {
2315
+ const p = deps.registry.products.get(c.req.param("product_id"));
2316
+ return { plans: p?.billing_plans ?? [] };
2317
+ })
2318
+ );
2319
+ app.get(
2320
+ "/catalogue/products/:product_id/schedules",
2321
+ (c) => tryRoute(c, () => {
2322
+ const p = deps.registry.products.get(c.req.param("product_id"));
2323
+ return { schedules: p?.metadata?.["time_profiles"] ?? [] };
2324
+ })
2325
+ );
2269
2326
  app.get("/catalogue/deals", (c) => tryRoute(c, () => deps.catalogue.listDeals(deps.defaultBusinessId)));
2270
2327
  app.get("/catalogue/deals/on-sale", (c) => tryRoute(c, () => deps.catalogue.listOnSale(deps.defaultBusinessId)));
2271
2328
  app.post("/catalogue/deals/validate", async (c) => {
@@ -2348,8 +2405,14 @@ function buildV1(deps) {
2348
2405
  });
2349
2406
  app.get("/inventory/products/:product_id/stock", (c) => tryRoute(c, () => deps.inventory.productStock(c.req.param("product_id"))));
2350
2407
  app.get("/inventory/variants/:variant_id/stock", (c) => tryRoute(c, () => deps.inventory.variantStock(c.req.param("variant_id"))));
2351
- app.get("/inventory/products/:product_id/availability", (c) => tryRoute(c, () => deps.inventory.productAvailability(c.req.param("product_id"))));
2352
- app.get("/inventory/variants/:variant_id/availability", (c) => tryRoute(c, () => deps.inventory.variantAvailability(c.req.param("variant_id"))));
2408
+ app.get(
2409
+ "/inventory/products/:product_id/availability",
2410
+ (c) => tryRoute(c, () => deps.inventory.productAvailability(c.req.param("product_id"), getQueryNum(c, "quantity") ?? 1))
2411
+ );
2412
+ app.get(
2413
+ "/inventory/variants/:variant_id/availability",
2414
+ (c) => tryRoute(c, () => deps.inventory.variantAvailability(c.req.param("variant_id"), getQueryNum(c, "quantity") ?? 1))
2415
+ );
2353
2416
  app.get(
2354
2417
  "/delivery/fee",
2355
2418
  (c) => tryRoute(
@@ -3373,7 +3436,14 @@ function createAuthService(opts) {
3373
3436
  },
3374
3437
  status(token) {
3375
3438
  const s = this.resolveSession(token);
3376
- return s ? { is_authenticated: s.is_authenticated, session: s } : { is_authenticated: false };
3439
+ if (!s) return { is_authenticated: false, user_type: "guest" };
3440
+ return {
3441
+ is_authenticated: s.is_authenticated,
3442
+ account_id: s.customer_id,
3443
+ user_type: s.is_authenticated ? "customer" : "guest",
3444
+ session_expires_at: s.expires_at ? Math.floor(new Date(s.expires_at).getTime() / 1e3) : void 0,
3445
+ session: s
3446
+ };
3377
3447
  }
3378
3448
  };
3379
3449
  }
@@ -4177,13 +4247,22 @@ function computeBundlePrice(bundle, componentSum) {
4177
4247
  }
4178
4248
  return componentSum;
4179
4249
  }
4180
- function computeCompositePrice(composite, componentsTotal, highestUnit) {
4250
+ function computeCompositePrice(composite, componentsTotal, highestUnit, perGroupHighest, selectionCount) {
4181
4251
  switch (composite.pricing_mode) {
4182
4252
  case "highest_overall":
4183
4253
  return add(composite.base_price, highestUnit ?? "0.00");
4254
+ case "highest_per_group": {
4255
+ let sum = "0.00";
4256
+ for (const v of Object.values(perGroupHighest ?? {})) sum = add(sum, v);
4257
+ return add(composite.base_price, sum);
4258
+ }
4259
+ case "tiered": {
4260
+ const tiers = composite.tiers ?? [];
4261
+ const count = selectionCount ?? 0;
4262
+ const matched = tiers.filter((t) => count >= t.min_count).sort((a, b) => b.min_count - a.min_count)[0];
4263
+ return matched ? add(composite.base_price, matched.price) : add(composite.base_price, componentsTotal);
4264
+ }
4184
4265
  case "additive":
4185
- case "highest_per_group":
4186
- case "tiered":
4187
4266
  default:
4188
4267
  return add(composite.base_price, componentsTotal);
4189
4268
  }
@@ -4317,7 +4396,11 @@ function toUICart(registry, cart) {
4317
4396
  // Billing / discounts / metadata
4318
4397
  billing_plan_id: i.billing_plan_id,
4319
4398
  applied_discount_ids: i.applied_discount_ids,
4320
- discount_details: void 0,
4399
+ discount_details: i.applied_discount_ids.length > 0 ? {
4400
+ applied_count: i.applied_discount_ids.length,
4401
+ total_amount: i.item_discount_amount,
4402
+ discount_ids: i.applied_discount_ids
4403
+ } : void 0,
4321
4404
  metadata: i.metadata,
4322
4405
  created_at: i.created_at,
4323
4406
  updated_at: i.updated_at
@@ -4352,7 +4435,7 @@ function toUICart(registry, cart) {
4352
4435
  var CART_TTL_MS = 24 * 60 * 60 * 1e3;
4353
4436
  var ZERO2 = "0.00";
4354
4437
  function createCartService(opts) {
4355
- const { registry, clock, ids, bus } = opts;
4438
+ const { registry, clock, ids, bus, inventory } = opts;
4356
4439
  const findOrCreate = (session) => {
4357
4440
  const existing = registry.carts.list(
4358
4441
  (c) => c.session_id === session.id && c.status === "active"
@@ -4434,7 +4517,12 @@ function createCartService(opts) {
4434
4517
  }
4435
4518
  const { stored, componentsTotal } = resolveCompositeSelections(registry, selections, composite.groups);
4436
4519
  const highestUnit = stored.reduce((max, s) => parseFloat(s.unit_price) > parseFloat(max) ? s.unit_price : max, ZERO2);
4437
- const finalPrice = computeCompositePrice(composite, componentsTotal, highestUnit);
4520
+ const perGroupHighest = {};
4521
+ for (const s of stored) {
4522
+ const cur = perGroupHighest[s.group_id];
4523
+ if (!cur || parseFloat(s.unit_price) > parseFloat(cur)) perGroupHighest[s.group_id] = s.unit_price;
4524
+ }
4525
+ const finalPrice = computeCompositePrice(composite, componentsTotal, highestUnit, perGroupHighest, totalQty);
4438
4526
  return {
4439
4527
  unitPrice: finalPrice,
4440
4528
  configuration: {
@@ -4512,6 +4600,7 @@ function createCartService(opts) {
4512
4600
  bundleSelections: body.bundle_selections,
4513
4601
  compositeSelections: body.composite_selections
4514
4602
  });
4603
+ inventory?.reserve(body.item_id, body.variant_id, body.quantity);
4515
4604
  const existing = cart.items.find((i) => i.line_key === lineKey);
4516
4605
  if (existing) {
4517
4606
  existing.quantity += body.quantity;
@@ -4551,9 +4640,13 @@ function createCartService(opts) {
4551
4640
  const cart = findOrCreate(session);
4552
4641
  const item = cart.items.find((i) => i.id === cartItemId);
4553
4642
  if (!item) throw notFound(`Cart item ${cartItemId} not found`);
4643
+ const delta = quantity - item.quantity;
4554
4644
  if (quantity <= 0) {
4645
+ inventory?.release(item.item_id, item.variant_id, item.quantity);
4555
4646
  cart.items = cart.items.filter((i) => i.id !== cartItemId);
4556
4647
  } else {
4648
+ if (delta > 0) inventory?.reserve(item.item_id, item.variant_id, delta);
4649
+ else if (delta < 0) inventory?.release(item.item_id, item.variant_id, -delta);
4557
4650
  item.quantity = quantity;
4558
4651
  item.updated_at = clock.iso();
4559
4652
  }
@@ -4562,12 +4655,15 @@ function createCartService(opts) {
4562
4655
  },
4563
4656
  removeItem(session, cartItemId) {
4564
4657
  const cart = findOrCreate(session);
4658
+ const item = cart.items.find((i) => i.id === cartItemId);
4659
+ if (item) inventory?.release(item.item_id, item.variant_id, item.quantity);
4565
4660
  cart.items = cart.items.filter((i) => i.id !== cartItemId);
4566
4661
  const updated = recalc(cart);
4567
4662
  return { success: true, cart: toUICart(registry, updated) };
4568
4663
  },
4569
4664
  clear(session) {
4570
4665
  const cart = findOrCreate(session);
4666
+ for (const item of cart.items) inventory?.release(item.item_id, item.variant_id, item.quantity);
4571
4667
  cart.items = [];
4572
4668
  cart.applied_discount_codes = [];
4573
4669
  cart.applied_discount_ids = [];
@@ -4643,18 +4739,23 @@ function createCheckoutService(opts) {
4643
4739
  bus.emit("order.payment.captured", { order_id: orderId, reference }, c.business_id);
4644
4740
  bus.emit("order.confirmed", { order_id: orderId }, c.business_id);
4645
4741
  }
4742
+ const billToken = `bt_${reference.slice(-12)}_${Math.random().toString(36).slice(2, 10)}`;
4646
4743
  const result = {
4647
4744
  order_id: orderId,
4648
4745
  order_number: orderNumber,
4649
4746
  payment_status: requiresAuth ? "pending" : "succeeded",
4650
4747
  payment_reference: reference,
4651
4748
  requires_authorization: requiresAuth,
4749
+ authorization_type: requiresAuth ? "otp" : void 0,
4652
4750
  next_action: requiresAuth ? { type: "authorization", authorization_type: "otp", display_text: "Approve on your mobile money app" } : { type: "none" },
4653
- provider: body.payment_method
4751
+ provider: body.payment_method,
4752
+ bill_token: billToken
4654
4753
  };
4655
4754
  if (isCard) {
4656
4755
  result.next_action = { type: "redirect", authorization_url: `https://mock.cimplify.io/pay/${reference}` };
4657
4756
  result.authorization_url = `https://mock.cimplify.io/pay/${reference}`;
4757
+ result.client_secret = `pi_${orderId.slice(-12)}_secret_${Math.random().toString(36).slice(2, 18)}`;
4758
+ result.public_key = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? "pk_test_mock_publishable_key";
4658
4759
  }
4659
4760
  return result;
4660
4761
  },
@@ -4860,19 +4961,66 @@ function createSchedulingService(opts) {
4860
4961
  }
4861
4962
 
4862
4963
  // src/mock/domain/inventory.ts
4863
- function createInventoryService(_opts) {
4964
+ var DEFAULT_INITIAL_STOCK = 50;
4965
+ function createInventoryService(opts) {
4966
+ const { registry } = opts;
4967
+ const stockMap = /* @__PURE__ */ new Map();
4968
+ const stockKey = (productId, variantId) => variantId ? `${productId}:${variantId}` : productId;
4969
+ const initialStock = (productId) => {
4970
+ const product = registry.products.get(productId);
4971
+ if (!product) return 0;
4972
+ if (product.product_type === "service" || product.product_type === "digital") {
4973
+ return Number.MAX_SAFE_INTEGER;
4974
+ }
4975
+ const seeded = product.metadata?.["stock_level"];
4976
+ if (typeof seeded === "number") return seeded;
4977
+ return DEFAULT_INITIAL_STOCK;
4978
+ };
4979
+ const ensure = (productId, variantId) => {
4980
+ const key = stockKey(productId, variantId);
4981
+ if (!stockMap.has(key)) stockMap.set(key, initialStock(productId));
4982
+ return key;
4983
+ };
4864
4984
  return {
4865
4985
  productStock(productId) {
4866
- return { product_id: productId, quantity: 100, available: true };
4986
+ const key = ensure(productId);
4987
+ const stock_level = stockMap.get(key) ?? 0;
4988
+ return { product_id: productId, stock_level, in_stock: stock_level > 0 };
4867
4989
  },
4868
4990
  variantStock(variantId) {
4869
- return { variant_id: variantId, quantity: 100, available: true };
4870
- },
4871
- productAvailability(productId) {
4872
- return { product_id: productId, is_available: true };
4991
+ const variant = registry.variants.get(variantId);
4992
+ const productId = variant?.product_id ?? "";
4993
+ const key = ensure(productId, variantId);
4994
+ const stock_level = stockMap.get(key) ?? 0;
4995
+ return { variant_id: variantId, stock_level, in_stock: stock_level > 0 };
4996
+ },
4997
+ productAvailability(productId, quantity = 1) {
4998
+ const key = ensure(productId);
4999
+ const have = stockMap.get(key) ?? 0;
5000
+ return { product_id: productId, quantity, is_available: have >= quantity };
5001
+ },
5002
+ variantAvailability(variantId, quantity = 1) {
5003
+ const variant = registry.variants.get(variantId);
5004
+ const productId = variant?.product_id ?? "";
5005
+ const key = ensure(productId, variantId);
5006
+ const have = stockMap.get(key) ?? 0;
5007
+ return { variant_id: variantId, quantity, is_available: have >= quantity };
5008
+ },
5009
+ reserve(productId, variantId, quantity) {
5010
+ const product = registry.products.get(productId);
5011
+ if (!product) return;
5012
+ if (product.product_type === "service" || product.product_type === "digital") return;
5013
+ const key = ensure(productId, variantId);
5014
+ const have = stockMap.get(key) ?? 0;
5015
+ stockMap.set(key, Math.max(0, have - quantity));
4873
5016
  },
4874
- variantAvailability(variantId) {
4875
- return { variant_id: variantId, is_available: true };
5017
+ release(productId, variantId, quantity) {
5018
+ const product = registry.products.get(productId);
5019
+ if (!product) return;
5020
+ if (product.product_type === "service" || product.product_type === "digital") return;
5021
+ const key = stockKey(productId, variantId);
5022
+ const have = stockMap.get(key) ?? 0;
5023
+ stockMap.set(key, have + quantity);
4876
5024
  }
4877
5025
  };
4878
5026
  }
@@ -8624,12 +8772,12 @@ function createDeps(options = {}) {
8624
8772
  });
8625
8773
  const business = createBusinessService({ registry, defaultBusinessId });
8626
8774
  const catalogue = createCatalogueService({ registry, clock, ids });
8627
- const cart = createCartService({ registry, clock, ids, bus });
8775
+ const inventory = createInventoryService({ registry });
8776
+ const cart = createCartService({ registry, clock, ids, bus, inventory });
8628
8777
  const orders = createOrderService({ registry, clock, bus });
8629
8778
  const checkout = createCheckoutService({ registry, clock, ids, bus});
8630
8779
  const subscriptions = createSubscriptionService({ registry, clock, bus });
8631
8780
  const scheduling = createSchedulingService({ registry, clock, bus });
8632
- const inventory = createInventoryService();
8633
8781
  const fx = createFxService({ registry, clock, ids });
8634
8782
  const places = createPlacesService();
8635
8783
  const delivery = createDeliveryService();
package/dist/mock/msw.mjs CHANGED
@@ -2135,6 +2135,44 @@ function authMiddleware(deps) {
2135
2135
  await next();
2136
2136
  };
2137
2137
  }
2138
+ var IDEMPOTENT_PATH_RE = /\/(cart\/items|cart\/coupons|checkout|payments\/authorization|auth\/(request-otp|verify-otp|profile|logout))(\/|$)/;
2139
+ var IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1e3;
2140
+ function idempotencyMiddleware(deps) {
2141
+ return async (c, next) => {
2142
+ if (c.req.method !== "POST" || !IDEMPOTENT_PATH_RE.test(c.req.path)) {
2143
+ return next();
2144
+ }
2145
+ const key = c.req.header("idempotency-key") ?? c.req.header("Idempotency-Key");
2146
+ if (!key || key.length > 200) return next();
2147
+ const session = c.var.session;
2148
+ const recordId = `${session.id}:${key}`;
2149
+ const existing = deps.registry.idempotency.get(recordId);
2150
+ if (existing) {
2151
+ const expiresAt = Date.parse(existing.expires_at);
2152
+ if (Number.isFinite(expiresAt) && expiresAt > Date.now()) {
2153
+ return c.json(existing.response, existing.status);
2154
+ }
2155
+ deps.registry.idempotency.delete(recordId);
2156
+ }
2157
+ await next();
2158
+ if (c.res.status >= 200 && c.res.status < 300) {
2159
+ try {
2160
+ const cloned = c.res.clone();
2161
+ const response = await cloned.json();
2162
+ const now = Date.now();
2163
+ deps.registry.idempotency.put(recordId, {
2164
+ key: recordId,
2165
+ request_hash: "",
2166
+ response,
2167
+ status: c.res.status,
2168
+ created_at: new Date(now).toISOString(),
2169
+ expires_at: new Date(now + IDEMPOTENCY_TTL_MS).toISOString()
2170
+ });
2171
+ } catch {
2172
+ }
2173
+ }
2174
+ };
2175
+ }
2138
2176
  function chaosMiddleware(deps) {
2139
2177
  return async (c, next) => {
2140
2178
  const path = c.req.path;
@@ -2185,6 +2223,7 @@ function buildV1(deps) {
2185
2223
  const app = new Hono2();
2186
2224
  app.use("*", chaosMiddleware(deps));
2187
2225
  app.use("*", authMiddleware(deps));
2226
+ app.use("*", idempotencyMiddleware(deps));
2188
2227
  app.get("/bootstrap", (c) => {
2189
2228
  const session = c.var.session;
2190
2229
  return tryRoute(c, () => ({
@@ -2261,9 +2300,27 @@ function buildV1(deps) {
2261
2300
  (c) => tryRoute(c, () => ({ results: deps.catalogue.searchKnowledgeBase(deps.defaultBusinessId, c.req.query("q") ?? ""), query: c.req.query("q") ?? "" }))
2262
2301
  );
2263
2302
  app.get("/catalogue/products/:product_id/attributes", (c) => tryRoute(c, () => deps.catalogue.getProductAttributes(c.req.param("product_id"))));
2264
- app.get("/catalogue/products/:product_id/available-now", (c) => tryRoute(c, () => ({ product_id: c.req.param("product_id"), is_available: true })));
2265
- app.get("/catalogue/products/:product_id/billing-plans/eligible", (c) => tryRoute(c, () => ({ plans: [] })));
2266
- app.get("/catalogue/products/:product_id/schedules", (c) => tryRoute(c, () => ({ schedules: [] })));
2303
+ app.get(
2304
+ "/catalogue/products/:product_id/available-now",
2305
+ (c) => tryRoute(c, () => {
2306
+ const p = deps.registry.products.get(c.req.param("product_id"));
2307
+ return { product_id: c.req.param("product_id"), is_available: p?.is_available ?? false };
2308
+ })
2309
+ );
2310
+ app.get(
2311
+ "/catalogue/products/:product_id/billing-plans/eligible",
2312
+ (c) => tryRoute(c, () => {
2313
+ const p = deps.registry.products.get(c.req.param("product_id"));
2314
+ return { plans: p?.billing_plans ?? [] };
2315
+ })
2316
+ );
2317
+ app.get(
2318
+ "/catalogue/products/:product_id/schedules",
2319
+ (c) => tryRoute(c, () => {
2320
+ const p = deps.registry.products.get(c.req.param("product_id"));
2321
+ return { schedules: p?.metadata?.["time_profiles"] ?? [] };
2322
+ })
2323
+ );
2267
2324
  app.get("/catalogue/deals", (c) => tryRoute(c, () => deps.catalogue.listDeals(deps.defaultBusinessId)));
2268
2325
  app.get("/catalogue/deals/on-sale", (c) => tryRoute(c, () => deps.catalogue.listOnSale(deps.defaultBusinessId)));
2269
2326
  app.post("/catalogue/deals/validate", async (c) => {
@@ -2346,8 +2403,14 @@ function buildV1(deps) {
2346
2403
  });
2347
2404
  app.get("/inventory/products/:product_id/stock", (c) => tryRoute(c, () => deps.inventory.productStock(c.req.param("product_id"))));
2348
2405
  app.get("/inventory/variants/:variant_id/stock", (c) => tryRoute(c, () => deps.inventory.variantStock(c.req.param("variant_id"))));
2349
- app.get("/inventory/products/:product_id/availability", (c) => tryRoute(c, () => deps.inventory.productAvailability(c.req.param("product_id"))));
2350
- app.get("/inventory/variants/:variant_id/availability", (c) => tryRoute(c, () => deps.inventory.variantAvailability(c.req.param("variant_id"))));
2406
+ app.get(
2407
+ "/inventory/products/:product_id/availability",
2408
+ (c) => tryRoute(c, () => deps.inventory.productAvailability(c.req.param("product_id"), getQueryNum(c, "quantity") ?? 1))
2409
+ );
2410
+ app.get(
2411
+ "/inventory/variants/:variant_id/availability",
2412
+ (c) => tryRoute(c, () => deps.inventory.variantAvailability(c.req.param("variant_id"), getQueryNum(c, "quantity") ?? 1))
2413
+ );
2351
2414
  app.get(
2352
2415
  "/delivery/fee",
2353
2416
  (c) => tryRoute(
@@ -3371,7 +3434,14 @@ function createAuthService(opts) {
3371
3434
  },
3372
3435
  status(token) {
3373
3436
  const s = this.resolveSession(token);
3374
- return s ? { is_authenticated: s.is_authenticated, session: s } : { is_authenticated: false };
3437
+ if (!s) return { is_authenticated: false, user_type: "guest" };
3438
+ return {
3439
+ is_authenticated: s.is_authenticated,
3440
+ account_id: s.customer_id,
3441
+ user_type: s.is_authenticated ? "customer" : "guest",
3442
+ session_expires_at: s.expires_at ? Math.floor(new Date(s.expires_at).getTime() / 1e3) : void 0,
3443
+ session: s
3444
+ };
3375
3445
  }
3376
3446
  };
3377
3447
  }
@@ -4175,13 +4245,22 @@ function computeBundlePrice(bundle, componentSum) {
4175
4245
  }
4176
4246
  return componentSum;
4177
4247
  }
4178
- function computeCompositePrice(composite, componentsTotal, highestUnit) {
4248
+ function computeCompositePrice(composite, componentsTotal, highestUnit, perGroupHighest, selectionCount) {
4179
4249
  switch (composite.pricing_mode) {
4180
4250
  case "highest_overall":
4181
4251
  return add(composite.base_price, highestUnit ?? "0.00");
4252
+ case "highest_per_group": {
4253
+ let sum = "0.00";
4254
+ for (const v of Object.values(perGroupHighest ?? {})) sum = add(sum, v);
4255
+ return add(composite.base_price, sum);
4256
+ }
4257
+ case "tiered": {
4258
+ const tiers = composite.tiers ?? [];
4259
+ const count = selectionCount ?? 0;
4260
+ const matched = tiers.filter((t) => count >= t.min_count).sort((a, b) => b.min_count - a.min_count)[0];
4261
+ return matched ? add(composite.base_price, matched.price) : add(composite.base_price, componentsTotal);
4262
+ }
4182
4263
  case "additive":
4183
- case "highest_per_group":
4184
- case "tiered":
4185
4264
  default:
4186
4265
  return add(composite.base_price, componentsTotal);
4187
4266
  }
@@ -4315,7 +4394,11 @@ function toUICart(registry, cart) {
4315
4394
  // Billing / discounts / metadata
4316
4395
  billing_plan_id: i.billing_plan_id,
4317
4396
  applied_discount_ids: i.applied_discount_ids,
4318
- discount_details: void 0,
4397
+ discount_details: i.applied_discount_ids.length > 0 ? {
4398
+ applied_count: i.applied_discount_ids.length,
4399
+ total_amount: i.item_discount_amount,
4400
+ discount_ids: i.applied_discount_ids
4401
+ } : void 0,
4319
4402
  metadata: i.metadata,
4320
4403
  created_at: i.created_at,
4321
4404
  updated_at: i.updated_at
@@ -4350,7 +4433,7 @@ function toUICart(registry, cart) {
4350
4433
  var CART_TTL_MS = 24 * 60 * 60 * 1e3;
4351
4434
  var ZERO2 = "0.00";
4352
4435
  function createCartService(opts) {
4353
- const { registry, clock, ids, bus } = opts;
4436
+ const { registry, clock, ids, bus, inventory } = opts;
4354
4437
  const findOrCreate = (session) => {
4355
4438
  const existing = registry.carts.list(
4356
4439
  (c) => c.session_id === session.id && c.status === "active"
@@ -4432,7 +4515,12 @@ function createCartService(opts) {
4432
4515
  }
4433
4516
  const { stored, componentsTotal } = resolveCompositeSelections(registry, selections, composite.groups);
4434
4517
  const highestUnit = stored.reduce((max, s) => parseFloat(s.unit_price) > parseFloat(max) ? s.unit_price : max, ZERO2);
4435
- const finalPrice = computeCompositePrice(composite, componentsTotal, highestUnit);
4518
+ const perGroupHighest = {};
4519
+ for (const s of stored) {
4520
+ const cur = perGroupHighest[s.group_id];
4521
+ if (!cur || parseFloat(s.unit_price) > parseFloat(cur)) perGroupHighest[s.group_id] = s.unit_price;
4522
+ }
4523
+ const finalPrice = computeCompositePrice(composite, componentsTotal, highestUnit, perGroupHighest, totalQty);
4436
4524
  return {
4437
4525
  unitPrice: finalPrice,
4438
4526
  configuration: {
@@ -4510,6 +4598,7 @@ function createCartService(opts) {
4510
4598
  bundleSelections: body.bundle_selections,
4511
4599
  compositeSelections: body.composite_selections
4512
4600
  });
4601
+ inventory?.reserve(body.item_id, body.variant_id, body.quantity);
4513
4602
  const existing = cart.items.find((i) => i.line_key === lineKey);
4514
4603
  if (existing) {
4515
4604
  existing.quantity += body.quantity;
@@ -4549,9 +4638,13 @@ function createCartService(opts) {
4549
4638
  const cart = findOrCreate(session);
4550
4639
  const item = cart.items.find((i) => i.id === cartItemId);
4551
4640
  if (!item) throw notFound(`Cart item ${cartItemId} not found`);
4641
+ const delta = quantity - item.quantity;
4552
4642
  if (quantity <= 0) {
4643
+ inventory?.release(item.item_id, item.variant_id, item.quantity);
4553
4644
  cart.items = cart.items.filter((i) => i.id !== cartItemId);
4554
4645
  } else {
4646
+ if (delta > 0) inventory?.reserve(item.item_id, item.variant_id, delta);
4647
+ else if (delta < 0) inventory?.release(item.item_id, item.variant_id, -delta);
4555
4648
  item.quantity = quantity;
4556
4649
  item.updated_at = clock.iso();
4557
4650
  }
@@ -4560,12 +4653,15 @@ function createCartService(opts) {
4560
4653
  },
4561
4654
  removeItem(session, cartItemId) {
4562
4655
  const cart = findOrCreate(session);
4656
+ const item = cart.items.find((i) => i.id === cartItemId);
4657
+ if (item) inventory?.release(item.item_id, item.variant_id, item.quantity);
4563
4658
  cart.items = cart.items.filter((i) => i.id !== cartItemId);
4564
4659
  const updated = recalc(cart);
4565
4660
  return { success: true, cart: toUICart(registry, updated) };
4566
4661
  },
4567
4662
  clear(session) {
4568
4663
  const cart = findOrCreate(session);
4664
+ for (const item of cart.items) inventory?.release(item.item_id, item.variant_id, item.quantity);
4569
4665
  cart.items = [];
4570
4666
  cart.applied_discount_codes = [];
4571
4667
  cart.applied_discount_ids = [];
@@ -4641,18 +4737,23 @@ function createCheckoutService(opts) {
4641
4737
  bus.emit("order.payment.captured", { order_id: orderId, reference }, c.business_id);
4642
4738
  bus.emit("order.confirmed", { order_id: orderId }, c.business_id);
4643
4739
  }
4740
+ const billToken = `bt_${reference.slice(-12)}_${Math.random().toString(36).slice(2, 10)}`;
4644
4741
  const result = {
4645
4742
  order_id: orderId,
4646
4743
  order_number: orderNumber,
4647
4744
  payment_status: requiresAuth ? "pending" : "succeeded",
4648
4745
  payment_reference: reference,
4649
4746
  requires_authorization: requiresAuth,
4747
+ authorization_type: requiresAuth ? "otp" : void 0,
4650
4748
  next_action: requiresAuth ? { type: "authorization", authorization_type: "otp", display_text: "Approve on your mobile money app" } : { type: "none" },
4651
- provider: body.payment_method
4749
+ provider: body.payment_method,
4750
+ bill_token: billToken
4652
4751
  };
4653
4752
  if (isCard) {
4654
4753
  result.next_action = { type: "redirect", authorization_url: `https://mock.cimplify.io/pay/${reference}` };
4655
4754
  result.authorization_url = `https://mock.cimplify.io/pay/${reference}`;
4755
+ result.client_secret = `pi_${orderId.slice(-12)}_secret_${Math.random().toString(36).slice(2, 18)}`;
4756
+ result.public_key = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? "pk_test_mock_publishable_key";
4656
4757
  }
4657
4758
  return result;
4658
4759
  },
@@ -4858,19 +4959,66 @@ function createSchedulingService(opts) {
4858
4959
  }
4859
4960
 
4860
4961
  // src/mock/domain/inventory.ts
4861
- function createInventoryService(_opts) {
4962
+ var DEFAULT_INITIAL_STOCK = 50;
4963
+ function createInventoryService(opts) {
4964
+ const { registry } = opts;
4965
+ const stockMap = /* @__PURE__ */ new Map();
4966
+ const stockKey = (productId, variantId) => variantId ? `${productId}:${variantId}` : productId;
4967
+ const initialStock = (productId) => {
4968
+ const product = registry.products.get(productId);
4969
+ if (!product) return 0;
4970
+ if (product.product_type === "service" || product.product_type === "digital") {
4971
+ return Number.MAX_SAFE_INTEGER;
4972
+ }
4973
+ const seeded = product.metadata?.["stock_level"];
4974
+ if (typeof seeded === "number") return seeded;
4975
+ return DEFAULT_INITIAL_STOCK;
4976
+ };
4977
+ const ensure = (productId, variantId) => {
4978
+ const key = stockKey(productId, variantId);
4979
+ if (!stockMap.has(key)) stockMap.set(key, initialStock(productId));
4980
+ return key;
4981
+ };
4862
4982
  return {
4863
4983
  productStock(productId) {
4864
- return { product_id: productId, quantity: 100, available: true };
4984
+ const key = ensure(productId);
4985
+ const stock_level = stockMap.get(key) ?? 0;
4986
+ return { product_id: productId, stock_level, in_stock: stock_level > 0 };
4865
4987
  },
4866
4988
  variantStock(variantId) {
4867
- return { variant_id: variantId, quantity: 100, available: true };
4868
- },
4869
- productAvailability(productId) {
4870
- return { product_id: productId, is_available: true };
4989
+ const variant = registry.variants.get(variantId);
4990
+ const productId = variant?.product_id ?? "";
4991
+ const key = ensure(productId, variantId);
4992
+ const stock_level = stockMap.get(key) ?? 0;
4993
+ return { variant_id: variantId, stock_level, in_stock: stock_level > 0 };
4994
+ },
4995
+ productAvailability(productId, quantity = 1) {
4996
+ const key = ensure(productId);
4997
+ const have = stockMap.get(key) ?? 0;
4998
+ return { product_id: productId, quantity, is_available: have >= quantity };
4999
+ },
5000
+ variantAvailability(variantId, quantity = 1) {
5001
+ const variant = registry.variants.get(variantId);
5002
+ const productId = variant?.product_id ?? "";
5003
+ const key = ensure(productId, variantId);
5004
+ const have = stockMap.get(key) ?? 0;
5005
+ return { variant_id: variantId, quantity, is_available: have >= quantity };
5006
+ },
5007
+ reserve(productId, variantId, quantity) {
5008
+ const product = registry.products.get(productId);
5009
+ if (!product) return;
5010
+ if (product.product_type === "service" || product.product_type === "digital") return;
5011
+ const key = ensure(productId, variantId);
5012
+ const have = stockMap.get(key) ?? 0;
5013
+ stockMap.set(key, Math.max(0, have - quantity));
4871
5014
  },
4872
- variantAvailability(variantId) {
4873
- return { variant_id: variantId, is_available: true };
5015
+ release(productId, variantId, quantity) {
5016
+ const product = registry.products.get(productId);
5017
+ if (!product) return;
5018
+ if (product.product_type === "service" || product.product_type === "digital") return;
5019
+ const key = stockKey(productId, variantId);
5020
+ const have = stockMap.get(key) ?? 0;
5021
+ stockMap.set(key, have + quantity);
4874
5022
  }
4875
5023
  };
4876
5024
  }
@@ -8622,12 +8770,12 @@ function createDeps(options = {}) {
8622
8770
  });
8623
8771
  const business = createBusinessService({ registry, defaultBusinessId });
8624
8772
  const catalogue = createCatalogueService({ registry, clock, ids });
8625
- const cart = createCartService({ registry, clock, ids, bus });
8773
+ const inventory = createInventoryService({ registry });
8774
+ const cart = createCartService({ registry, clock, ids, bus, inventory });
8626
8775
  const orders = createOrderService({ registry, clock, bus });
8627
8776
  const checkout = createCheckoutService({ registry, clock, ids, bus});
8628
8777
  const subscriptions = createSubscriptionService({ registry, clock, bus });
8629
8778
  const scheduling = createSchedulingService({ registry, clock, bus });
8630
- const inventory = createInventoryService();
8631
8779
  const fx = createFxService({ registry, clock, ids });
8632
8780
  const places = createPlacesService();
8633
8781
  const delivery = createDeliveryService();