@asteby/storefront 0.5.0 → 0.7.0

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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a7 as Shop, W as Product, p as Collection, ae as Theme, R as Page, M as Menu, b as Article, C as Cart, A as Address, r as Customer, P as Order } from './types-D300wdPF.cjs';
1
+ import { aB as Shop, ah as Product, w as Collection, aI as Theme, $ as Page, U as Menu, b as Article, C as Cart, A as Address, y as Customer, Z as Order, a2 as PaymentAttemptResult, ao as PromotionAnnouncement } from './types-CSMuPSQA.cjs';
2
2
  import './geo.cjs';
3
3
 
4
4
  /**
@@ -37,6 +37,11 @@ type MockStorefrontOptions = {
37
37
  flatShipping?: number;
38
38
  /** Enforce token scopes (`pk_` cannot checkout or use customer routes). Default true. */
39
39
  enforceScopes?: boolean;
40
+ /**
41
+ * Mock `show_stock`: `low_stock_threshold:N` semantics. At or below this quantity a variant is `low_stock`
42
+ * and only then `availability.quantity` is published (default 5; `0` disables `low_stock`).
43
+ */
44
+ lowStockThreshold?: number;
40
45
  /** Artificial latency per request, in ms (default 0). */
41
46
  latencyMs?: number;
42
47
  /** Override seed data. */
@@ -49,6 +54,10 @@ type MockStorefrontOptions = {
49
54
  pages?: Page[];
50
55
  menus?: Menu[];
51
56
  articles?: Article[];
57
+ /** Entry pop-up announcements served by `GET /storefront/promotions` (default none). */
58
+ announcements?: PromotionAnnouncement[];
59
+ /** Payment attempts allowed per checkout before `429 too_many_attempts` (default 5). */
60
+ maxPaymentAttempts?: number;
52
61
  };
53
62
  type CartRecord = {
54
63
  id: string;
@@ -61,6 +70,7 @@ type CartRecord = {
61
70
  buyer: Cart['buyer'];
62
71
  shipping_address: Address | null;
63
72
  delivery: Cart['delivery'];
73
+ note: string | null;
64
74
  updated_at: string;
65
75
  };
66
76
  type MockState = {
@@ -85,6 +95,13 @@ type MockState = {
85
95
  body: unknown;
86
96
  }>;
87
97
  checkouts: Map<string, unknown>;
98
+ /** Payment attempts per checkout (`n`, last result). */
99
+ payments: Map<string, {
100
+ attempts: number;
101
+ last: PaymentAttemptResult | null;
102
+ }>;
103
+ /** `contact|sku` of accepted "notify me" subscriptions. */
104
+ restock: Set<string>;
88
105
  seq: number;
89
106
  };
90
107
  declare function createMockStorefront(options?: MockStorefrontOptions): {
package/dist/mock.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a7 as Shop, W as Product, p as Collection, ae as Theme, R as Page, M as Menu, b as Article, C as Cart, A as Address, r as Customer, P as Order } from './types-BmC-RDo8.js';
1
+ import { aB as Shop, ah as Product, w as Collection, aI as Theme, $ as Page, U as Menu, b as Article, C as Cart, A as Address, y as Customer, Z as Order, a2 as PaymentAttemptResult, ao as PromotionAnnouncement } from './types-iGGZ9EiC.js';
2
2
  import './geo.js';
3
3
 
4
4
  /**
@@ -37,6 +37,11 @@ type MockStorefrontOptions = {
37
37
  flatShipping?: number;
38
38
  /** Enforce token scopes (`pk_` cannot checkout or use customer routes). Default true. */
39
39
  enforceScopes?: boolean;
40
+ /**
41
+ * Mock `show_stock`: `low_stock_threshold:N` semantics. At or below this quantity a variant is `low_stock`
42
+ * and only then `availability.quantity` is published (default 5; `0` disables `low_stock`).
43
+ */
44
+ lowStockThreshold?: number;
40
45
  /** Artificial latency per request, in ms (default 0). */
41
46
  latencyMs?: number;
42
47
  /** Override seed data. */
@@ -49,6 +54,10 @@ type MockStorefrontOptions = {
49
54
  pages?: Page[];
50
55
  menus?: Menu[];
51
56
  articles?: Article[];
57
+ /** Entry pop-up announcements served by `GET /storefront/promotions` (default none). */
58
+ announcements?: PromotionAnnouncement[];
59
+ /** Payment attempts allowed per checkout before `429 too_many_attempts` (default 5). */
60
+ maxPaymentAttempts?: number;
52
61
  };
53
62
  type CartRecord = {
54
63
  id: string;
@@ -61,6 +70,7 @@ type CartRecord = {
61
70
  buyer: Cart['buyer'];
62
71
  shipping_address: Address | null;
63
72
  delivery: Cart['delivery'];
73
+ note: string | null;
64
74
  updated_at: string;
65
75
  };
66
76
  type MockState = {
@@ -85,6 +95,13 @@ type MockState = {
85
95
  body: unknown;
86
96
  }>;
87
97
  checkouts: Map<string, unknown>;
98
+ /** Payment attempts per checkout (`n`, last result). */
99
+ payments: Map<string, {
100
+ attempts: number;
101
+ last: PaymentAttemptResult | null;
102
+ }>;
103
+ /** `contact|sku` of accepted "notify me" subscriptions. */
104
+ restock: Set<string>;
88
105
  seq: number;
89
106
  };
90
107
  declare function createMockStorefront(options?: MockStorefrontOptions): {
package/dist/mock.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolveAvailability } from './chunk-HBSB7XS2.js';
1
2
  import { isValidPoint, rankByDistance } from './chunk-45R47HYO.js';
2
3
 
3
4
  // src/mock/seed.ts
@@ -338,15 +339,17 @@ function buildCustomers() {
338
339
 
339
340
  // src/mock/index.ts
340
341
  var HttpError = class extends Error {
341
- constructor(status, code, message, field) {
342
+ constructor(status, code, message, field, details) {
342
343
  super(message);
343
344
  this.status = status;
344
345
  this.code = code;
345
346
  this.field = field;
347
+ this.details = details;
346
348
  }
347
349
  status;
348
350
  code;
349
351
  field;
352
+ details;
350
353
  };
351
354
  var PUBLIC_CACHE = "public, max-age=60, stale-while-revalidate=600";
352
355
  var PRIVATE_CACHE = "private, no-store";
@@ -427,10 +430,36 @@ function createMockStorefront(options = {}) {
427
430
  sessions: /* @__PURE__ */ new Map(),
428
431
  idempotency: /* @__PURE__ */ new Map(),
429
432
  checkouts: /* @__PURE__ */ new Map(),
433
+ payments: /* @__PURE__ */ new Map(),
434
+ restock: /* @__PURE__ */ new Set(),
430
435
  seq: 0
431
436
  };
432
437
  }
438
+ const lowStockThreshold = options.lowStockThreshold ?? 5;
439
+ function syncAvailability(products) {
440
+ for (const p of products) {
441
+ for (const v of p.variants) {
442
+ const qty = v.quantity_available;
443
+ const sellable = v.available_for_sale && (qty === null || qty > 0);
444
+ v.available_for_sale = sellable;
445
+ const low = sellable && qty !== null && lowStockThreshold > 0 && qty <= lowStockThreshold;
446
+ v.availability = {
447
+ status: !sellable ? "out_of_stock" : low ? "low_stock" : "in_stock",
448
+ quantity: low ? qty : null,
449
+ max_per_order: sellable ? qty === null ? null : Math.min(qty, 10) : null,
450
+ restock_eta: null,
451
+ preorder_release_at: null,
452
+ notify_available: !sellable
453
+ };
454
+ }
455
+ p.available_for_sale = p.variants.some((v) => v.available_for_sale);
456
+ const rank = ["in_stock", "low_stock", "backorder", "preorder", "out_of_stock"];
457
+ const best = p.variants.map((v) => v.availability).sort((a, b) => rank.indexOf(a.status) - rank.indexOf(b.status))[0];
458
+ p.availability = best ?? resolveAvailability(p);
459
+ }
460
+ }
433
461
  let state = seed();
462
+ syncAvailability(state.products);
434
463
  const nextId = (prefix) => `${prefix}_${(++state.seq).toString(36)}${Math.random().toString(36).slice(2, 8)}`;
435
464
  function findVariant(variantId) {
436
465
  for (const product of state.products) {
@@ -549,7 +578,8 @@ function createMockStorefront(options = {}) {
549
578
  }
550
579
  function validateLines(lines, cart) {
551
580
  if (!Array.isArray(lines)) throw new HttpError(400, "validation", "`lines` must be an array", "lines");
552
- return lines.map((raw, i) => {
581
+ const toCheck = [];
582
+ const out = lines.map((raw, i) => {
553
583
  const l = raw;
554
584
  if (typeof l.variant_id !== "string") throw new HttpError(400, "validation", "variant_id is required", `lines[${i}].variant_id`);
555
585
  const quantity = l.quantity === void 0 ? 1 : l.quantity;
@@ -558,15 +588,36 @@ function createMockStorefront(options = {}) {
558
588
  }
559
589
  const found = findVariant(l.variant_id);
560
590
  if (!found) throw new HttpError(400, "validation", `Unknown variant ${l.variant_id}`, `lines[${i}].variant_id`);
561
- const already = cart?.lines.find((x) => x.variant_id === l.variant_id)?.quantity ?? 0;
562
- checkStock(found.variant, already + quantity, `lines[${i}].quantity`);
591
+ const existing = cart?.lines.find((x) => x.variant_id === l.variant_id);
592
+ toCheck.push({ variant: found.variant, quantity: (existing?.quantity ?? 0) + quantity, lineId: existing?.id ?? null, field: `lines[${i}].quantity` });
563
593
  return { variant_id: l.variant_id, quantity };
564
594
  });
595
+ checkStock(toCheck);
596
+ return out;
597
+ }
598
+ function visibleMax(variant) {
599
+ const a = variant.availability;
600
+ if (!a || a.status === "out_of_stock") return 0;
601
+ return a.status === "low_stock" ? a.quantity : null;
565
602
  }
566
- function checkStock(variant, quantity, field) {
567
- if (!variant.available_for_sale || variant.quantity_available !== null && quantity > variant.quantity_available) {
568
- throw new HttpError(409, "insufficient_stock", `Only ${variant.quantity_available ?? 0} available for ${variant.sku}`, field);
603
+ function lineProblem(variant, quantity) {
604
+ if (!variant.available_for_sale) return { code: "out_of_stock", max_quantity: 0 };
605
+ if (variant.quantity_available !== null && quantity > variant.quantity_available) {
606
+ return { code: "insufficient_stock", max_quantity: visibleMax(variant) };
569
607
  }
608
+ return null;
609
+ }
610
+ function checkStock(items) {
611
+ const bad = items.filter((i) => lineProblem(i.variant, i.quantity));
612
+ if (bad.length === 0) return;
613
+ const lines = bad.map((i) => ({
614
+ line_id: i.lineId,
615
+ variant_id: i.variant.id,
616
+ sku: i.variant.sku,
617
+ status: i.variant.availability?.status ?? "out_of_stock",
618
+ max_quantity: visibleMax(i.variant)
619
+ }));
620
+ throw new HttpError(409, "insufficient_stock", "Algunos productos ya no tienen existencias suficientes", bad[0].field, { lines });
570
621
  }
571
622
  function addLines(cart, lines) {
572
623
  for (const l of lines) {
@@ -599,6 +650,7 @@ function createMockStorefront(options = {}) {
599
650
  lines.push({
600
651
  id: l.id,
601
652
  quantity: l.quantity,
653
+ problem: lineProblem(variant, l.quantity),
602
654
  merchandise: {
603
655
  ...variant,
604
656
  product: { handle: product.handle, title: product.title, featured_image: product.featured_image }
@@ -637,6 +689,7 @@ function createMockStorefront(options = {}) {
637
689
  total: fromCents(total, currency),
638
690
  tax_included: true
639
691
  },
692
+ note: cart.note,
640
693
  updated_at: cart.updated_at
641
694
  };
642
695
  }
@@ -720,7 +773,7 @@ function createMockStorefront(options = {}) {
720
773
  const nodes = skus.flatMap((sku) => {
721
774
  for (const p of state.products) {
722
775
  const v = p.variants.find((x) => x.sku === sku);
723
- if (v) return [{ sku, quantity_available: v.quantity_available }];
776
+ if (v) return [{ sku, quantity_available: v.quantity_available, ...v.availability ? { availability: v.availability } : {} }];
724
777
  }
725
778
  return [];
726
779
  });
@@ -734,6 +787,7 @@ function createMockStorefront(options = {}) {
734
787
  buyer: { email: null, phone: null, customer_id: null },
735
788
  shipping_address: null,
736
789
  delivery: { method: null, rate_id: null, branch_id: null },
790
+ note: null,
737
791
  updated_at: now()
738
792
  };
739
793
  if (body.lines !== void 0) addLines(cart, validateLines(body.lines));
@@ -757,8 +811,13 @@ function createMockStorefront(options = {}) {
757
811
  if (typeof u.quantity !== "number" || !Number.isInteger(u.quantity) || u.quantity < 0) {
758
812
  throw new HttpError(400, "validation", "quantity must be a non-negative integer", `lines[${i}].quantity`);
759
813
  }
760
- if (u.quantity > 0) checkStock(findVariant(line.variant_id).variant, u.quantity, `lines[${i}].quantity`);
761
814
  });
815
+ checkStock(
816
+ updates.flatMap((u, i) => {
817
+ const line = cart.lines.find((l) => l.id === u.id);
818
+ return u.quantity > 0 ? [{ variant: findVariant(line.variant_id).variant, quantity: u.quantity, lineId: line.id, field: `lines[${i}].quantity` }] : [];
819
+ })
820
+ );
762
821
  for (const u of updates) {
763
822
  const line = cart.lines.find((l) => l.id === u.id);
764
823
  line.quantity = u.quantity;
@@ -825,6 +884,40 @@ function createMockStorefront(options = {}) {
825
884
  cart.updated_at = now();
826
885
  return { body: renderCart(cart), cache: "private" };
827
886
  });
887
+ route("PUT", "/carts/:id/note", "cart:write", ({ params, body }) => {
888
+ const cart = getCartRecord(params[0]);
889
+ const raw = body.note;
890
+ if (raw !== null && raw !== void 0 && typeof raw !== "string") throw new HttpError(400, "validation", "note must be a string or null", "note");
891
+ const text = typeof raw === "string" ? raw.trim() : "";
892
+ if (text.length > 1e3) throw new HttpError(400, "validation", "note must be at most 1000 characters", "note");
893
+ cart.note = text || null;
894
+ cart.updated_at = now();
895
+ return { body: renderCart(cart), cache: "private" };
896
+ });
897
+ route("GET", "/carts/:id/recommendations", "cart:write", ({ params, search }) => {
898
+ const cart = getCartRecord(params[0]);
899
+ const first = Math.min(8, Math.max(1, Number.parseInt(search.get("first") ?? "4", 10) || 4));
900
+ const inCart = new Set(cart.lines.map((l) => findVariant(l.variant_id)?.product.id));
901
+ const nodes = [];
902
+ for (const p of state.products) {
903
+ if (inCart.has(p.id)) continue;
904
+ const variants = p.variants.filter((v) => v.available_for_sale);
905
+ if (variants.length === 0) continue;
906
+ nodes.push({
907
+ product: { id: p.id, handle: p.handle, title: p.title, featured_image: p.featured_image, options: p.options, vendor: p.vendor },
908
+ variants,
909
+ default_variant_id: variants[0].id,
910
+ source: "similar",
911
+ unlocks: null
912
+ });
913
+ if (nodes.length >= first) break;
914
+ }
915
+ return { body: { nodes, promotions: null }, cache: "private" };
916
+ });
917
+ route("GET", "/promotions", "catalog:read", () => ({
918
+ body: { nodes: [], announcements: options.announcements ?? [] },
919
+ cache: "public"
920
+ }));
828
921
  route("POST", "/carts/:id/checkout", "checkout:write", ({ params, body, headers }) => {
829
922
  const key = headers.get("idempotency-key");
830
923
  if (key) {
@@ -841,7 +934,7 @@ function createMockStorefront(options = {}) {
841
934
  }
842
935
  if (typeof body.payment_method !== "string" || !body.payment_method) throw new HttpError(400, "validation", "payment_method is required", "payment_method");
843
936
  if (typeof body.return_url !== "string" || !body.return_url) throw new HttpError(400, "validation", "return_url is required", "return_url");
844
- record.lines.forEach((l, i) => checkStock(findVariant(l.variant_id).variant, l.quantity, `lines[${i}].quantity`));
937
+ checkStock(record.lines.map((l, i) => ({ variant: findVariant(l.variant_id).variant, quantity: l.quantity, lineId: l.id, field: `lines[${i}].quantity` })));
845
938
  for (const l of record.lines) {
846
939
  const v = findVariant(l.variant_id).variant;
847
940
  if (v.quantity_available !== null) {
@@ -849,7 +942,7 @@ function createMockStorefront(options = {}) {
849
942
  v.available_for_sale = v.quantity_available > 0;
850
943
  }
851
944
  }
852
- for (const p of state.products) p.available_for_sale = p.variants.some((v) => v.available_for_sale);
945
+ syncAvailability(state.products);
853
946
  const orderId = nextId("ord");
854
947
  const number = String(1001 + state.orders.length - 1);
855
948
  const customerId = cart.buyer.customer_id ?? state.customers.find((c) => c.email && c.email === cart.buyer.email)?.id ?? null;
@@ -893,6 +986,110 @@ function createMockStorefront(options = {}) {
893
986
  if (!found) throw new HttpError(404, "not_found", "Checkout not found");
894
987
  return { body: found, cache: "private" };
895
988
  });
989
+ const DECLINES = {
990
+ tok_OTHE: "declined",
991
+ tok_CALL: "call_issuer",
992
+ tok_FUND: "insufficient_funds",
993
+ tok_SECU: "invalid_cvc",
994
+ tok_EXPI: "invalid_expiry",
995
+ tok_FORM: "invalid_card_data"
996
+ };
997
+ const maxPaymentAttempts = options.maxPaymentAttempts ?? 5;
998
+ route("GET", "/checkouts/:id/payment-config", "checkout:write", ({ params }) => {
999
+ const found = state.checkouts.get(params[0]);
1000
+ if (!found) throw new HttpError(404, "not_found", "Checkout not found");
1001
+ const st = state.payments.get(params[0]);
1002
+ const config = {
1003
+ provider: "mock",
1004
+ flow: "embedded",
1005
+ option: "card",
1006
+ payable: found.checkout.status === "pending" && (st?.attempts ?? 0) < maxPaymentAttempts,
1007
+ reason: null,
1008
+ amount: found.checkout.total,
1009
+ currency: found.checkout.total.currency_code,
1010
+ public_key: "TEST-mock-public-key",
1011
+ methods: ["card", "cash"],
1012
+ options: [{ key: "card", redirect: false, max_installments: 12 }, { key: "cash", redirect: false }],
1013
+ client: { kind: "mock", sdk_url: "https://mock.local/sdk.js", public_key: "TEST-mock-public-key" },
1014
+ checkout: found.checkout,
1015
+ action: st?.last?.payment.action ?? null,
1016
+ attempts_left: maxPaymentAttempts - (st?.attempts ?? 0)
1017
+ };
1018
+ return { body: config, cache: "private" };
1019
+ });
1020
+ route("POST", "/checkouts/:id/payment", "checkout:write", ({ params, body, headers }) => {
1021
+ const key = headers.get("idempotency-key");
1022
+ if (!key) throw new HttpError(400, "validation", "Idempotency-Key header is required", "Idempotency-Key");
1023
+ const id = params[0];
1024
+ const prev = state.idempotency.get(`pay:${id}:${key}`);
1025
+ if (prev) return { status: prev.status, body: prev.body, cache: "private" };
1026
+ const found = state.checkouts.get(id);
1027
+ if (!found) throw new HttpError(404, "not_found", "Checkout not found");
1028
+ const method = body.method;
1029
+ if (method !== "card" && method !== "cash" && method !== "bank_transfer") throw new HttpError(400, "validation", "method must be card, cash or bank_transfer", "method");
1030
+ const data = body.provider_data;
1031
+ if (typeof data !== "object" || data === null || Array.isArray(data)) throw new HttpError(400, "validation", "provider_data is required", "provider_data");
1032
+ const exp = body.expected_total;
1033
+ if (exp && exp.amount !== void 0 && toCents(exp.amount) !== toCents(found.checkout.total.amount)) {
1034
+ throw new HttpError(422, "price_mismatch", "The total changed", "expected_total", { cost: found.checkout.total });
1035
+ }
1036
+ const st = state.payments.get(id) ?? { attempts: 0, last: null };
1037
+ if (found.checkout.status === "paid" || st.last?.payment.status === "pending" || st.last?.payment.status === "requires_action") {
1038
+ throw new HttpError(409, "conflict", "A payment is already in progress", "payment_in_progress");
1039
+ }
1040
+ if (found.checkout.stock_hold_expires_at && Date.parse(found.checkout.stock_hold_expires_at) < Date.now()) {
1041
+ throw new HttpError(409, "expired", "The reservation expired");
1042
+ }
1043
+ if (st.attempts >= maxPaymentAttempts) throw new HttpError(429, "rate_limited", "too_many_attempts");
1044
+ st.attempts += 1;
1045
+ const token = typeof data.token === "string" ? data.token : "";
1046
+ const attemptsLeft = maxPaymentAttempts - st.attempts;
1047
+ let payment;
1048
+ if (method !== "card") {
1049
+ const pointId = String(data.payment_method_id ?? "oxxo");
1050
+ payment = {
1051
+ provider: "mock",
1052
+ status: "requires_action",
1053
+ action: {
1054
+ type: "voucher",
1055
+ voucher: {
1056
+ payment_point: pointId,
1057
+ reference: `${pointId.toUpperCase()}-${id.slice(-6).toUpperCase()}`,
1058
+ voucher_url: `https://mock.local/voucher/${id}`,
1059
+ amount: found.checkout.total.amount,
1060
+ currency: found.checkout.total.currency_code,
1061
+ expires_at: new Date(Date.now() + 48 * 36e5).toISOString()
1062
+ }
1063
+ }
1064
+ };
1065
+ } else if (token in DECLINES) {
1066
+ payment = { provider: "mock", status: "failed", decline_code: DECLINES[token], retryable: true };
1067
+ } else if (token === "tok_CONT") {
1068
+ payment = { provider: "mock", status: "pending", action: { type: "none" } };
1069
+ } else if (token === "tok_3DS") {
1070
+ payment = { provider: "mock", status: "requires_action", action: { type: "challenge", challenge: { url: "https://mock.local/3ds", creq: "creq" } } };
1071
+ } else {
1072
+ payment = { provider: "mock", status: "paid" };
1073
+ found.checkout.status = "paid";
1074
+ }
1075
+ payment.outcome = payment.status === "paid" ? "approved" : payment.status === "failed" ? "rejected" : payment.action?.type === "challenge" ? "challenge" : "pending";
1076
+ const result = { attempt: { n: st.attempts }, payment, checkout: found.checkout, attempts_left: attemptsLeft };
1077
+ st.last = result;
1078
+ state.payments.set(id, st);
1079
+ state.idempotency.set(`pay:${id}:${key}`, { status: 200, body: result });
1080
+ return { body: result, cache: "private" };
1081
+ });
1082
+ route("POST", "/restock-subscriptions", "customer:write", ({ body }) => {
1083
+ if (typeof body.sku !== "string" || !body.sku) throw new HttpError(400, "validation", "sku is required", "sku");
1084
+ if (typeof body.locale !== "string" || !body.locale) throw new HttpError(400, "validation", "locale is required", "locale");
1085
+ if (body.consent !== true) throw new HttpError(422, "invalid_state", "consent must be true", "consent");
1086
+ const contact = typeof body.email === "string" && body.email ? body.email.toLowerCase() : typeof body.phone === "string" ? body.phone : "";
1087
+ if (!contact) throw new HttpError(400, "validation", "email or phone is required", "email");
1088
+ const known = state.products.some((p) => p.variants.some((v) => v.sku === body.sku));
1089
+ if (!known) throw new HttpError(404, "not_found", `SKU ${body.sku} not found`, "sku");
1090
+ state.restock.add(`${contact}|${body.sku}`);
1091
+ return { status: 202, body: { status: "pending" }, cache: "private" };
1092
+ });
896
1093
  route("POST", "/customer/otp", "customer:write", ({ body }) => {
897
1094
  state.otps.set(identifierOf(body), otpCode);
898
1095
  return { status: 204 };
@@ -1010,7 +1207,11 @@ function createMockStorefront(options = {}) {
1010
1207
  });
1011
1208
  }
1012
1209
  function errorResponse(err) {
1013
- return json(err.status, { error: { code: err.code, message: err.message, ...err.field ? { field: err.field } : {} } }, { "Cache-Control": PRIVATE_CACHE });
1210
+ return json(
1211
+ err.status,
1212
+ { error: { code: err.code, message: err.message, ...err.field ? { field: err.field } : {}, ...err.details !== void 0 ? { details: err.details } : {} } },
1213
+ { "Cache-Control": PRIVATE_CACHE }
1214
+ );
1014
1215
  }
1015
1216
  async function handle(request) {
1016
1217
  const url = new URL(request.url);
@@ -1074,6 +1275,7 @@ function createMockStorefront(options = {}) {
1074
1275
  /** Restores the seed data and clears carts, sessions and orders. */
1075
1276
  reset() {
1076
1277
  state = seed();
1278
+ syncAvailability(state.products);
1077
1279
  },
1078
1280
  /** OTP code accepted by `/customer/session`. */
1079
1281
  otpCode