@cimplify/sdk 0.44.12 → 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.
- package/dist/mock/cli.mjs +171 -23
- package/dist/mock/library.d.mts +2 -2
- package/dist/mock/library.d.ts +2 -2
- package/dist/mock/library.js +171 -23
- package/dist/mock/library.mjs +171 -23
- package/dist/mock/msw.d.mts +1 -1
- package/dist/mock/msw.d.ts +1 -1
- package/dist/mock/msw.js +171 -23
- package/dist/mock/msw.mjs +171 -23
- package/dist/{server-CUbuQxOV.d.mts → server-CnECAQDQ.d.mts} +28 -6
- package/dist/{server-CUbuQxOV.d.ts → server-CnECAQDQ.d.ts} +28 -6
- package/package.json +1 -1
- package/registry/account.json +15 -0
- package/registry/billing-plan-selector.json +16 -0
- package/registry/cart-drawer.json +17 -0
- package/registry/currency-selector.json +15 -0
- package/registry/customer-input-fields.json +15 -0
- package/registry/delivery-estimate.json +16 -0
- package/registry/index.json +105 -0
- package/registry/location-picker.json +15 -0
- package/registry/recently-viewed.json +16 -0
- package/registry/recommendation-carousel.json +16 -0
- package/registry/resource-picker.json +15 -0
- package/registry/session-message-banner.json +15 -0
- package/templates/storefront-bakery/AGENTS.md +12 -0
- package/templates/storefront-fashion/AGENTS.md +21 -1
- package/templates/storefront-grocery/AGENTS.md +12 -0
- package/templates/storefront-restaurant/AGENTS.md +12 -0
- package/templates/storefront-retail/AGENTS.md +12 -0
- package/templates/storefront-services/AGENTS.md +12 -0
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(
|
|
2267
|
-
|
|
2268
|
-
|
|
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(
|
|
2352
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
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
|
-
|
|
4875
|
-
|
|
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
|
|
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(
|
|
2265
|
-
|
|
2266
|
-
|
|
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(
|
|
2350
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
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
|
-
|
|
4873
|
-
|
|
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
|
|
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();
|