@cimplify/sdk 0.44.12 → 0.44.16

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 (99) hide show
  1. package/dist/advanced.d.mts +1 -1
  2. package/dist/advanced.d.ts +1 -1
  3. package/dist/advanced.js +21 -21
  4. package/dist/advanced.mjs +2 -2
  5. package/dist/{chunk-6TVQQYQ5.mjs → chunk-6AZZ6AOF.mjs} +21 -1
  6. package/dist/{chunk-EZK2FXTM.mjs → chunk-7NML226B.mjs} +1 -1
  7. package/dist/{chunk-BAWKQRIB.js → chunk-CFD4J32N.js} +21 -21
  8. package/dist/{chunk-OMAP327U.js → chunk-MBGQP6FZ.js} +62 -61
  9. package/dist/{chunk-V2O5AO7I.js → chunk-MNIKXG7W.js} +26 -0
  10. package/dist/{chunk-3WD6U5RD.mjs → chunk-UXLQ7WXI.mjs} +4 -3
  11. package/dist/chunk-XL4CX75J.js +9370 -0
  12. package/dist/chunk-ZLEFMQC6.mjs +9318 -0
  13. package/dist/cli/commands/deploy.mjs +9 -1
  14. package/dist/{client-lvLvB1_K.d.ts → client-D6ZisKco.d.ts} +7 -0
  15. package/dist/{client-CkGlXZRd.d.mts → client-Dh-bHyhU.d.mts} +7 -0
  16. package/dist/index.d.mts +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +106 -106
  19. package/dist/index.mjs +3 -3
  20. package/dist/mock/cli.mjs +172 -23
  21. package/dist/mock/library.d.mts +2 -2
  22. package/dist/mock/library.d.ts +2 -2
  23. package/dist/mock/library.js +172 -23
  24. package/dist/mock/library.mjs +172 -23
  25. package/dist/mock/msw.d.mts +1 -1
  26. package/dist/mock/msw.d.ts +1 -1
  27. package/dist/mock/msw.js +172 -23
  28. package/dist/mock/msw.mjs +172 -23
  29. package/dist/react.d.mts +1 -1
  30. package/dist/react.d.ts +1 -1
  31. package/dist/react.js +80 -80
  32. package/dist/react.mjs +3 -3
  33. package/dist/server-CWpUlLOQ.d.mts +1003 -0
  34. package/dist/server-CWpUlLOQ.d.ts +1003 -0
  35. package/dist/{server-CUbuQxOV.d.ts → server-CnECAQDQ.d.mts} +28 -6
  36. package/dist/{server-CUbuQxOV.d.mts → server-CnECAQDQ.d.ts} +28 -6
  37. package/dist/server.d.mts +2 -2
  38. package/dist/server.d.ts +2 -2
  39. package/dist/server.js +5 -5
  40. package/dist/server.mjs +3 -3
  41. package/dist/testing/suite.d.mts +60 -0
  42. package/dist/testing/suite.d.ts +60 -0
  43. package/dist/testing/suite.js +169 -0
  44. package/dist/testing/suite.mjs +165 -0
  45. package/dist/testing.d.mts +1359 -0
  46. package/dist/testing.d.ts +1359 -0
  47. package/dist/testing.js +213 -0
  48. package/dist/testing.mjs +4 -0
  49. package/dist/utils.js +28 -28
  50. package/dist/utils.mjs +1 -1
  51. package/package.json +18 -3
  52. package/registry/account.json +15 -0
  53. package/registry/billing-plan-selector.json +16 -0
  54. package/registry/cart-drawer.json +17 -0
  55. package/registry/currency-selector.json +15 -0
  56. package/registry/customer-input-fields.json +15 -0
  57. package/registry/delivery-estimate.json +16 -0
  58. package/registry/index.json +105 -0
  59. package/registry/location-picker.json +15 -0
  60. package/registry/recently-viewed.json +16 -0
  61. package/registry/recommendation-carousel.json +16 -0
  62. package/registry/resource-picker.json +15 -0
  63. package/registry/session-message-banner.json +15 -0
  64. package/templates/storefront-bakery/AGENTS.md +12 -0
  65. package/templates/storefront-bakery/__tests__/brand.test.ts +4 -0
  66. package/templates/storefront-bakery/__tests__/cart-flow.test.ts +4 -0
  67. package/templates/storefront-bakery/__tests__/contract.test.ts +4 -0
  68. package/templates/storefront-bakery/package.json +11 -3
  69. package/templates/storefront-bakery/vitest.config.ts +9 -0
  70. package/templates/storefront-fashion/AGENTS.md +21 -1
  71. package/templates/storefront-fashion/__tests__/brand.test.ts +4 -0
  72. package/templates/storefront-fashion/__tests__/cart-flow.test.ts +4 -0
  73. package/templates/storefront-fashion/__tests__/contract.test.ts +4 -0
  74. package/templates/storefront-fashion/package.json +11 -3
  75. package/templates/storefront-fashion/vitest.config.ts +9 -0
  76. package/templates/storefront-grocery/AGENTS.md +12 -0
  77. package/templates/storefront-grocery/__tests__/brand.test.ts +4 -0
  78. package/templates/storefront-grocery/__tests__/cart-flow.test.ts +4 -0
  79. package/templates/storefront-grocery/__tests__/contract.test.ts +4 -0
  80. package/templates/storefront-grocery/package.json +11 -3
  81. package/templates/storefront-grocery/vitest.config.ts +9 -0
  82. package/templates/storefront-restaurant/AGENTS.md +12 -0
  83. package/templates/storefront-restaurant/__tests__/brand.test.ts +4 -0
  84. package/templates/storefront-restaurant/__tests__/cart-flow.test.ts +4 -0
  85. package/templates/storefront-restaurant/__tests__/contract.test.ts +4 -0
  86. package/templates/storefront-restaurant/package.json +11 -3
  87. package/templates/storefront-restaurant/vitest.config.ts +9 -0
  88. package/templates/storefront-retail/AGENTS.md +12 -0
  89. package/templates/storefront-retail/__tests__/brand.test.ts +4 -0
  90. package/templates/storefront-retail/__tests__/cart-flow.test.ts +4 -0
  91. package/templates/storefront-retail/__tests__/contract.test.ts +4 -0
  92. package/templates/storefront-retail/package.json +11 -3
  93. package/templates/storefront-retail/vitest.config.ts +9 -0
  94. package/templates/storefront-services/AGENTS.md +12 -0
  95. package/templates/storefront-services/__tests__/brand.test.ts +4 -0
  96. package/templates/storefront-services/__tests__/cart-flow.test.ts +4 -0
  97. package/templates/storefront-services/__tests__/contract.test.ts +4 -0
  98. package/templates/storefront-services/package.json +11 -3
  99. package/templates/storefront-services/vitest.config.ts +9 -0
@@ -2139,6 +2139,44 @@ function authMiddleware(deps) {
2139
2139
  await next();
2140
2140
  };
2141
2141
  }
2142
+ var IDEMPOTENT_PATH_RE = /\/(cart\/items|cart\/coupons|checkout|payments\/authorization|auth\/(request-otp|verify-otp|profile|logout))(\/|$)/;
2143
+ var IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1e3;
2144
+ function idempotencyMiddleware(deps) {
2145
+ return async (c, next) => {
2146
+ if (c.req.method !== "POST" || !IDEMPOTENT_PATH_RE.test(c.req.path)) {
2147
+ return next();
2148
+ }
2149
+ const key = c.req.header("idempotency-key") ?? c.req.header("Idempotency-Key");
2150
+ if (!key || key.length > 200) return next();
2151
+ const session = c.var.session;
2152
+ const recordId = `${session.id}:${key}`;
2153
+ const existing = deps.registry.idempotency.get(recordId);
2154
+ if (existing) {
2155
+ const expiresAt = Date.parse(existing.expires_at);
2156
+ if (Number.isFinite(expiresAt) && expiresAt > Date.now()) {
2157
+ return c.json(existing.response, existing.status);
2158
+ }
2159
+ deps.registry.idempotency.delete(recordId);
2160
+ }
2161
+ await next();
2162
+ if (c.res.status >= 200 && c.res.status < 300) {
2163
+ try {
2164
+ const cloned = c.res.clone();
2165
+ const response = await cloned.json();
2166
+ const now = Date.now();
2167
+ deps.registry.idempotency.put(recordId, {
2168
+ key: recordId,
2169
+ request_hash: "",
2170
+ response,
2171
+ status: c.res.status,
2172
+ created_at: new Date(now).toISOString(),
2173
+ expires_at: new Date(now + IDEMPOTENCY_TTL_MS).toISOString()
2174
+ });
2175
+ } catch {
2176
+ }
2177
+ }
2178
+ };
2179
+ }
2142
2180
  function chaosMiddleware(deps) {
2143
2181
  return async (c, next) => {
2144
2182
  const path = c.req.path;
@@ -2189,6 +2227,7 @@ function buildV1(deps) {
2189
2227
  const app = new Hono2();
2190
2228
  app.use("*", chaosMiddleware(deps));
2191
2229
  app.use("*", authMiddleware(deps));
2230
+ app.use("*", idempotencyMiddleware(deps));
2192
2231
  app.get("/bootstrap", (c) => {
2193
2232
  const session = c.var.session;
2194
2233
  return tryRoute(c, () => ({
@@ -2265,9 +2304,27 @@ function buildV1(deps) {
2265
2304
  (c) => tryRoute(c, () => ({ results: deps.catalogue.searchKnowledgeBase(deps.defaultBusinessId, c.req.query("q") ?? ""), query: c.req.query("q") ?? "" }))
2266
2305
  );
2267
2306
  app.get("/catalogue/products/:product_id/attributes", (c) => tryRoute(c, () => deps.catalogue.getProductAttributes(c.req.param("product_id"))));
2268
- app.get("/catalogue/products/:product_id/available-now", (c) => tryRoute(c, () => ({ product_id: c.req.param("product_id"), is_available: true })));
2269
- app.get("/catalogue/products/:product_id/billing-plans/eligible", (c) => tryRoute(c, () => ({ plans: [] })));
2270
- app.get("/catalogue/products/:product_id/schedules", (c) => tryRoute(c, () => ({ schedules: [] })));
2307
+ app.get(
2308
+ "/catalogue/products/:product_id/available-now",
2309
+ (c) => tryRoute(c, () => {
2310
+ const p = deps.registry.products.get(c.req.param("product_id"));
2311
+ return { product_id: c.req.param("product_id"), is_available: p?.is_available ?? false };
2312
+ })
2313
+ );
2314
+ app.get(
2315
+ "/catalogue/products/:product_id/billing-plans/eligible",
2316
+ (c) => tryRoute(c, () => {
2317
+ const p = deps.registry.products.get(c.req.param("product_id"));
2318
+ return { plans: p?.billing_plans ?? [] };
2319
+ })
2320
+ );
2321
+ app.get(
2322
+ "/catalogue/products/:product_id/schedules",
2323
+ (c) => tryRoute(c, () => {
2324
+ const p = deps.registry.products.get(c.req.param("product_id"));
2325
+ return { schedules: p?.metadata?.["time_profiles"] ?? [] };
2326
+ })
2327
+ );
2271
2328
  app.get("/catalogue/deals", (c) => tryRoute(c, () => deps.catalogue.listDeals(deps.defaultBusinessId)));
2272
2329
  app.get("/catalogue/deals/on-sale", (c) => tryRoute(c, () => deps.catalogue.listOnSale(deps.defaultBusinessId)));
2273
2330
  app.post("/catalogue/deals/validate", async (c) => {
@@ -2350,8 +2407,14 @@ function buildV1(deps) {
2350
2407
  });
2351
2408
  app.get("/inventory/products/:product_id/stock", (c) => tryRoute(c, () => deps.inventory.productStock(c.req.param("product_id"))));
2352
2409
  app.get("/inventory/variants/:variant_id/stock", (c) => tryRoute(c, () => deps.inventory.variantStock(c.req.param("variant_id"))));
2353
- app.get("/inventory/products/:product_id/availability", (c) => tryRoute(c, () => deps.inventory.productAvailability(c.req.param("product_id"))));
2354
- app.get("/inventory/variants/:variant_id/availability", (c) => tryRoute(c, () => deps.inventory.variantAvailability(c.req.param("variant_id"))));
2410
+ app.get(
2411
+ "/inventory/products/:product_id/availability",
2412
+ (c) => tryRoute(c, () => deps.inventory.productAvailability(c.req.param("product_id"), getQueryNum(c, "quantity") ?? 1))
2413
+ );
2414
+ app.get(
2415
+ "/inventory/variants/:variant_id/availability",
2416
+ (c) => tryRoute(c, () => deps.inventory.variantAvailability(c.req.param("variant_id"), getQueryNum(c, "quantity") ?? 1))
2417
+ );
2355
2418
  app.get(
2356
2419
  "/delivery/fee",
2357
2420
  (c) => tryRoute(
@@ -3375,7 +3438,14 @@ function createAuthService(opts) {
3375
3438
  },
3376
3439
  status(token) {
3377
3440
  const s = this.resolveSession(token);
3378
- return s ? { is_authenticated: s.is_authenticated, session: s } : { is_authenticated: false };
3441
+ if (!s) return { is_authenticated: false, user_type: "guest" };
3442
+ return {
3443
+ is_authenticated: s.is_authenticated,
3444
+ account_id: s.customer_id,
3445
+ user_type: s.is_authenticated ? "customer" : "guest",
3446
+ session_expires_at: s.expires_at ? Math.floor(new Date(s.expires_at).getTime() / 1e3) : void 0,
3447
+ session: s
3448
+ };
3379
3449
  }
3380
3450
  };
3381
3451
  }
@@ -4179,13 +4249,22 @@ function computeBundlePrice(bundle, componentSum) {
4179
4249
  }
4180
4250
  return componentSum;
4181
4251
  }
4182
- function computeCompositePrice(composite, componentsTotal, highestUnit) {
4252
+ function computeCompositePrice(composite, componentsTotal, highestUnit, perGroupHighest, selectionCount) {
4183
4253
  switch (composite.pricing_mode) {
4184
4254
  case "highest_overall":
4185
4255
  return add(composite.base_price, highestUnit ?? "0.00");
4256
+ case "highest_per_group": {
4257
+ let sum = "0.00";
4258
+ for (const v of Object.values(perGroupHighest ?? {})) sum = add(sum, v);
4259
+ return add(composite.base_price, sum);
4260
+ }
4261
+ case "tiered": {
4262
+ const tiers = composite.tiers ?? [];
4263
+ const count = selectionCount ?? 0;
4264
+ const matched = tiers.filter((t) => count >= t.min_count).sort((a, b) => b.min_count - a.min_count)[0];
4265
+ return matched ? add(composite.base_price, matched.price) : add(composite.base_price, componentsTotal);
4266
+ }
4186
4267
  case "additive":
4187
- case "highest_per_group":
4188
- case "tiered":
4189
4268
  default:
4190
4269
  return add(composite.base_price, componentsTotal);
4191
4270
  }
@@ -4319,7 +4398,11 @@ function toUICart(registry, cart) {
4319
4398
  // Billing / discounts / metadata
4320
4399
  billing_plan_id: i.billing_plan_id,
4321
4400
  applied_discount_ids: i.applied_discount_ids,
4322
- discount_details: void 0,
4401
+ discount_details: i.applied_discount_ids.length > 0 ? {
4402
+ applied_count: i.applied_discount_ids.length,
4403
+ total_amount: i.item_discount_amount,
4404
+ discount_ids: i.applied_discount_ids
4405
+ } : void 0,
4323
4406
  metadata: i.metadata,
4324
4407
  created_at: i.created_at,
4325
4408
  updated_at: i.updated_at
@@ -4354,7 +4437,7 @@ function toUICart(registry, cart) {
4354
4437
  var CART_TTL_MS = 24 * 60 * 60 * 1e3;
4355
4438
  var ZERO2 = "0.00";
4356
4439
  function createCartService(opts) {
4357
- const { registry, clock, ids, bus } = opts;
4440
+ const { registry, clock, ids, bus, inventory } = opts;
4358
4441
  const findOrCreate = (session) => {
4359
4442
  const existing = registry.carts.list(
4360
4443
  (c) => c.session_id === session.id && c.status === "active"
@@ -4436,7 +4519,12 @@ function createCartService(opts) {
4436
4519
  }
4437
4520
  const { stored, componentsTotal } = resolveCompositeSelections(registry, selections, composite.groups);
4438
4521
  const highestUnit = stored.reduce((max, s) => parseFloat(s.unit_price) > parseFloat(max) ? s.unit_price : max, ZERO2);
4439
- const finalPrice = computeCompositePrice(composite, componentsTotal, highestUnit);
4522
+ const perGroupHighest = {};
4523
+ for (const s of stored) {
4524
+ const cur = perGroupHighest[s.group_id];
4525
+ if (!cur || parseFloat(s.unit_price) > parseFloat(cur)) perGroupHighest[s.group_id] = s.unit_price;
4526
+ }
4527
+ const finalPrice = computeCompositePrice(composite, componentsTotal, highestUnit, perGroupHighest, totalQty);
4440
4528
  return {
4441
4529
  unitPrice: finalPrice,
4442
4530
  configuration: {
@@ -4514,6 +4602,7 @@ function createCartService(opts) {
4514
4602
  bundleSelections: body.bundle_selections,
4515
4603
  compositeSelections: body.composite_selections
4516
4604
  });
4605
+ inventory?.reserve(body.item_id, body.variant_id, body.quantity);
4517
4606
  const existing = cart.items.find((i) => i.line_key === lineKey);
4518
4607
  if (existing) {
4519
4608
  existing.quantity += body.quantity;
@@ -4553,9 +4642,13 @@ function createCartService(opts) {
4553
4642
  const cart = findOrCreate(session);
4554
4643
  const item = cart.items.find((i) => i.id === cartItemId);
4555
4644
  if (!item) throw notFound(`Cart item ${cartItemId} not found`);
4645
+ const delta = quantity - item.quantity;
4556
4646
  if (quantity <= 0) {
4647
+ inventory?.release(item.item_id, item.variant_id, item.quantity);
4557
4648
  cart.items = cart.items.filter((i) => i.id !== cartItemId);
4558
4649
  } else {
4650
+ if (delta > 0) inventory?.reserve(item.item_id, item.variant_id, delta);
4651
+ else if (delta < 0) inventory?.release(item.item_id, item.variant_id, -delta);
4559
4652
  item.quantity = quantity;
4560
4653
  item.updated_at = clock.iso();
4561
4654
  }
@@ -4564,12 +4657,15 @@ function createCartService(opts) {
4564
4657
  },
4565
4658
  removeItem(session, cartItemId) {
4566
4659
  const cart = findOrCreate(session);
4660
+ const item = cart.items.find((i) => i.id === cartItemId);
4661
+ if (item) inventory?.release(item.item_id, item.variant_id, item.quantity);
4567
4662
  cart.items = cart.items.filter((i) => i.id !== cartItemId);
4568
4663
  const updated = recalc(cart);
4569
4664
  return { success: true, cart: toUICart(registry, updated) };
4570
4665
  },
4571
4666
  clear(session) {
4572
4667
  const cart = findOrCreate(session);
4668
+ for (const item of cart.items) inventory?.release(item.item_id, item.variant_id, item.quantity);
4573
4669
  cart.items = [];
4574
4670
  cart.applied_discount_codes = [];
4575
4671
  cart.applied_discount_ids = [];
@@ -4645,18 +4741,24 @@ function createCheckoutService(opts) {
4645
4741
  bus.emit("order.payment.captured", { order_id: orderId, reference }, c.business_id);
4646
4742
  bus.emit("order.confirmed", { order_id: orderId }, c.business_id);
4647
4743
  }
4744
+ const billToken = `bt_${reference.slice(-12)}_${Math.random().toString(36).slice(2, 10)}`;
4648
4745
  const result = {
4649
4746
  order_id: orderId,
4650
4747
  order_number: orderNumber,
4651
4748
  payment_status: requiresAuth ? "pending" : "succeeded",
4652
4749
  payment_reference: reference,
4653
4750
  requires_authorization: requiresAuth,
4751
+ authorization_type: requiresAuth ? "otp" : void 0,
4654
4752
  next_action: requiresAuth ? { type: "authorization", authorization_type: "otp", display_text: "Approve on your mobile money app" } : { type: "none" },
4655
- provider: body.payment_method
4753
+ provider: body.payment_method,
4754
+ bill_token: billToken
4656
4755
  };
4657
4756
  if (isCard) {
4658
4757
  result.next_action = { type: "redirect", authorization_url: `https://mock.cimplify.io/pay/${reference}` };
4659
4758
  result.authorization_url = `https://mock.cimplify.io/pay/${reference}`;
4759
+ result.client_secret = `pi_${orderId.slice(-12)}_secret_${Math.random().toString(36).slice(2, 18)}`;
4760
+ const proc = globalThis.process;
4761
+ result.public_key = proc?.env?.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? "pk_test_mock_publishable_key";
4660
4762
  }
4661
4763
  return result;
4662
4764
  },
@@ -4862,19 +4964,66 @@ function createSchedulingService(opts) {
4862
4964
  }
4863
4965
 
4864
4966
  // src/mock/domain/inventory.ts
4865
- function createInventoryService(_opts) {
4967
+ var DEFAULT_INITIAL_STOCK = 50;
4968
+ function createInventoryService(opts) {
4969
+ const { registry } = opts;
4970
+ const stockMap = /* @__PURE__ */ new Map();
4971
+ const stockKey = (productId, variantId) => variantId ? `${productId}:${variantId}` : productId;
4972
+ const initialStock = (productId) => {
4973
+ const product = registry.products.get(productId);
4974
+ if (!product) return 0;
4975
+ if (product.product_type === "service" || product.product_type === "digital") {
4976
+ return Number.MAX_SAFE_INTEGER;
4977
+ }
4978
+ const seeded = product.metadata?.["stock_level"];
4979
+ if (typeof seeded === "number") return seeded;
4980
+ return DEFAULT_INITIAL_STOCK;
4981
+ };
4982
+ const ensure = (productId, variantId) => {
4983
+ const key = stockKey(productId, variantId);
4984
+ if (!stockMap.has(key)) stockMap.set(key, initialStock(productId));
4985
+ return key;
4986
+ };
4866
4987
  return {
4867
4988
  productStock(productId) {
4868
- return { product_id: productId, quantity: 100, available: true };
4989
+ const key = ensure(productId);
4990
+ const stock_level = stockMap.get(key) ?? 0;
4991
+ return { product_id: productId, stock_level, in_stock: stock_level > 0 };
4869
4992
  },
4870
4993
  variantStock(variantId) {
4871
- return { variant_id: variantId, quantity: 100, available: true };
4872
- },
4873
- productAvailability(productId) {
4874
- return { product_id: productId, is_available: true };
4994
+ const variant = registry.variants.get(variantId);
4995
+ const productId = variant?.product_id ?? "";
4996
+ const key = ensure(productId, variantId);
4997
+ const stock_level = stockMap.get(key) ?? 0;
4998
+ return { variant_id: variantId, stock_level, in_stock: stock_level > 0 };
4999
+ },
5000
+ productAvailability(productId, quantity = 1) {
5001
+ const key = ensure(productId);
5002
+ const have = stockMap.get(key) ?? 0;
5003
+ return { product_id: productId, quantity, is_available: have >= quantity };
5004
+ },
5005
+ variantAvailability(variantId, quantity = 1) {
5006
+ const variant = registry.variants.get(variantId);
5007
+ const productId = variant?.product_id ?? "";
5008
+ const key = ensure(productId, variantId);
5009
+ const have = stockMap.get(key) ?? 0;
5010
+ return { variant_id: variantId, quantity, is_available: have >= quantity };
5011
+ },
5012
+ reserve(productId, variantId, quantity) {
5013
+ const product = registry.products.get(productId);
5014
+ if (!product) return;
5015
+ if (product.product_type === "service" || product.product_type === "digital") return;
5016
+ const key = ensure(productId, variantId);
5017
+ const have = stockMap.get(key) ?? 0;
5018
+ stockMap.set(key, Math.max(0, have - quantity));
4875
5019
  },
4876
- variantAvailability(variantId) {
4877
- return { variant_id: variantId, is_available: true };
5020
+ release(productId, variantId, quantity) {
5021
+ const product = registry.products.get(productId);
5022
+ if (!product) return;
5023
+ if (product.product_type === "service" || product.product_type === "digital") return;
5024
+ const key = stockKey(productId, variantId);
5025
+ const have = stockMap.get(key) ?? 0;
5026
+ stockMap.set(key, have + quantity);
4878
5027
  }
4879
5028
  };
4880
5029
  }
@@ -8626,12 +8775,12 @@ function createDeps(options = {}) {
8626
8775
  });
8627
8776
  const business = createBusinessService({ registry, defaultBusinessId });
8628
8777
  const catalogue = createCatalogueService({ registry, clock, ids });
8629
- const cart = createCartService({ registry, clock, ids, bus });
8778
+ const inventory = createInventoryService({ registry });
8779
+ const cart = createCartService({ registry, clock, ids, bus, inventory });
8630
8780
  const orders = createOrderService({ registry, clock, bus });
8631
8781
  const checkout = createCheckoutService({ registry, clock, ids, bus});
8632
8782
  const subscriptions = createSubscriptionService({ registry, clock, bus });
8633
8783
  const scheduling = createSchedulingService({ registry, clock, bus });
8634
- const inventory = createInventoryService();
8635
8784
  const fx = createFxService({ registry, clock, ids });
8636
8785
  const places = createPlacesService();
8637
8786
  const delivery = createDeliveryService();
@@ -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,24 @@ 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
+ const proc = globalThis.process;
4759
+ result.public_key = proc?.env?.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? "pk_test_mock_publishable_key";
4658
4760
  }
4659
4761
  return result;
4660
4762
  },
@@ -4860,19 +4962,66 @@ function createSchedulingService(opts) {
4860
4962
  }
4861
4963
 
4862
4964
  // src/mock/domain/inventory.ts
4863
- function createInventoryService(_opts) {
4965
+ var DEFAULT_INITIAL_STOCK = 50;
4966
+ function createInventoryService(opts) {
4967
+ const { registry } = opts;
4968
+ const stockMap = /* @__PURE__ */ new Map();
4969
+ const stockKey = (productId, variantId) => variantId ? `${productId}:${variantId}` : productId;
4970
+ const initialStock = (productId) => {
4971
+ const product = registry.products.get(productId);
4972
+ if (!product) return 0;
4973
+ if (product.product_type === "service" || product.product_type === "digital") {
4974
+ return Number.MAX_SAFE_INTEGER;
4975
+ }
4976
+ const seeded = product.metadata?.["stock_level"];
4977
+ if (typeof seeded === "number") return seeded;
4978
+ return DEFAULT_INITIAL_STOCK;
4979
+ };
4980
+ const ensure = (productId, variantId) => {
4981
+ const key = stockKey(productId, variantId);
4982
+ if (!stockMap.has(key)) stockMap.set(key, initialStock(productId));
4983
+ return key;
4984
+ };
4864
4985
  return {
4865
4986
  productStock(productId) {
4866
- return { product_id: productId, quantity: 100, available: true };
4987
+ const key = ensure(productId);
4988
+ const stock_level = stockMap.get(key) ?? 0;
4989
+ return { product_id: productId, stock_level, in_stock: stock_level > 0 };
4867
4990
  },
4868
4991
  variantStock(variantId) {
4869
- return { variant_id: variantId, quantity: 100, available: true };
4870
- },
4871
- productAvailability(productId) {
4872
- return { product_id: productId, is_available: true };
4992
+ const variant = registry.variants.get(variantId);
4993
+ const productId = variant?.product_id ?? "";
4994
+ const key = ensure(productId, variantId);
4995
+ const stock_level = stockMap.get(key) ?? 0;
4996
+ return { variant_id: variantId, stock_level, in_stock: stock_level > 0 };
4997
+ },
4998
+ productAvailability(productId, quantity = 1) {
4999
+ const key = ensure(productId);
5000
+ const have = stockMap.get(key) ?? 0;
5001
+ return { product_id: productId, quantity, is_available: have >= quantity };
5002
+ },
5003
+ variantAvailability(variantId, quantity = 1) {
5004
+ const variant = registry.variants.get(variantId);
5005
+ const productId = variant?.product_id ?? "";
5006
+ const key = ensure(productId, variantId);
5007
+ const have = stockMap.get(key) ?? 0;
5008
+ return { variant_id: variantId, quantity, is_available: have >= quantity };
5009
+ },
5010
+ reserve(productId, variantId, quantity) {
5011
+ const product = registry.products.get(productId);
5012
+ if (!product) return;
5013
+ if (product.product_type === "service" || product.product_type === "digital") return;
5014
+ const key = ensure(productId, variantId);
5015
+ const have = stockMap.get(key) ?? 0;
5016
+ stockMap.set(key, Math.max(0, have - quantity));
4873
5017
  },
4874
- variantAvailability(variantId) {
4875
- return { variant_id: variantId, is_available: true };
5018
+ release(productId, variantId, quantity) {
5019
+ const product = registry.products.get(productId);
5020
+ if (!product) return;
5021
+ if (product.product_type === "service" || product.product_type === "digital") return;
5022
+ const key = stockKey(productId, variantId);
5023
+ const have = stockMap.get(key) ?? 0;
5024
+ stockMap.set(key, have + quantity);
4876
5025
  }
4877
5026
  };
4878
5027
  }
@@ -8624,12 +8773,12 @@ function createDeps(options = {}) {
8624
8773
  });
8625
8774
  const business = createBusinessService({ registry, defaultBusinessId });
8626
8775
  const catalogue = createCatalogueService({ registry, clock, ids });
8627
- const cart = createCartService({ registry, clock, ids, bus });
8776
+ const inventory = createInventoryService({ registry });
8777
+ const cart = createCartService({ registry, clock, ids, bus, inventory });
8628
8778
  const orders = createOrderService({ registry, clock, bus });
8629
8779
  const checkout = createCheckoutService({ registry, clock, ids, bus});
8630
8780
  const subscriptions = createSubscriptionService({ registry, clock, bus });
8631
8781
  const scheduling = createSchedulingService({ registry, clock, bus });
8632
- const inventory = createInventoryService();
8633
8782
  const fx = createFxService({ registry, clock, ids });
8634
8783
  const places = createPlacesService();
8635
8784
  const delivery = createDeliveryService();
@@ -1,4 +1,4 @@
1
- import { A as AppHandle, C as CreateAppOptions } from '../server-CUbuQxOV.mjs';
1
+ import { A as AppHandle, C as CreateAppOptions } from '../server-CnECAQDQ.mjs';
2
2
  import 'hono';
3
3
 
4
4
  interface MswHandlerSetup {
@@ -1,4 +1,4 @@
1
- import { A as AppHandle, C as CreateAppOptions } from '../server-CUbuQxOV.js';
1
+ import { A as AppHandle, C as CreateAppOptions } from '../server-CnECAQDQ.js';
2
2
  import 'hono';
3
3
 
4
4
  interface MswHandlerSetup {