@asteby/storefront 0.5.0 → 0.6.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/README.md +40 -0
- package/dist/chunk-HBSB7XS2.js +220 -0
- package/dist/chunk-HBSB7XS2.js.map +1 -0
- package/dist/index.cjs +705 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +268 -3
- package/dist/index.d.ts +268 -3
- package/dist/index.js +469 -3
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +233 -12
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +18 -1
- package/dist/mock.d.ts +18 -1
- package/dist/mock.js +214 -12
- package/dist/mock.js.map +1 -1
- package/dist/mockup.d.cts +1 -1
- package/dist/mockup.d.ts +1 -1
- package/dist/{types-D300wdPF.d.cts → types-CSMuPSQA.d.cts} +321 -1
- package/dist/{types-BmC-RDo8.d.ts → types-iGGZ9EiC.d.ts} +321 -1
- package/package.json +1 -1
package/dist/mock.cjs
CHANGED
|
@@ -24,6 +24,26 @@ function rankByDistance(items, origin, { radiusKm, limit, getPoint = (i) => i }
|
|
|
24
24
|
return limit === void 0 ? out : out.slice(0, Math.max(0, limit));
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// src/availability.ts
|
|
28
|
+
function resolveAvailability(source, options = {}) {
|
|
29
|
+
const a = source.availability;
|
|
30
|
+
if (a && typeof a === "object" && typeof a.status === "string") return a;
|
|
31
|
+
const qty = typeof source.quantity_available === "number" ? source.quantity_available : null;
|
|
32
|
+
const sellable = source.available_for_sale && (qty === null || qty > 0);
|
|
33
|
+
const threshold = options.lowStockThreshold;
|
|
34
|
+
const low = sellable && qty !== null && typeof threshold === "number" && threshold > 0 && qty <= threshold;
|
|
35
|
+
const status = !sellable ? "out_of_stock" : low ? "low_stock" : "in_stock";
|
|
36
|
+
return {
|
|
37
|
+
status,
|
|
38
|
+
quantity: sellable ? qty : null,
|
|
39
|
+
max_per_order: sellable ? qty : null,
|
|
40
|
+
restock_eta: null,
|
|
41
|
+
preorder_release_at: null,
|
|
42
|
+
// Old servers have no subscription route: never offer a form that cannot work.
|
|
43
|
+
notify_available: false
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
// src/mock/seed.ts
|
|
28
48
|
var MOCK_IMAGE_URL = (seed, width = 800, height = 800) => `https://picsum.photos/seed/${encodeURIComponent(seed)}/${width}/${height}`;
|
|
29
49
|
var MOCK_CURRENCY = "MXN";
|
|
@@ -362,15 +382,17 @@ function buildCustomers() {
|
|
|
362
382
|
|
|
363
383
|
// src/mock/index.ts
|
|
364
384
|
var HttpError = class extends Error {
|
|
365
|
-
constructor(status, code, message, field) {
|
|
385
|
+
constructor(status, code, message, field, details) {
|
|
366
386
|
super(message);
|
|
367
387
|
this.status = status;
|
|
368
388
|
this.code = code;
|
|
369
389
|
this.field = field;
|
|
390
|
+
this.details = details;
|
|
370
391
|
}
|
|
371
392
|
status;
|
|
372
393
|
code;
|
|
373
394
|
field;
|
|
395
|
+
details;
|
|
374
396
|
};
|
|
375
397
|
var PUBLIC_CACHE = "public, max-age=60, stale-while-revalidate=600";
|
|
376
398
|
var PRIVATE_CACHE = "private, no-store";
|
|
@@ -451,10 +473,36 @@ function createMockStorefront(options = {}) {
|
|
|
451
473
|
sessions: /* @__PURE__ */ new Map(),
|
|
452
474
|
idempotency: /* @__PURE__ */ new Map(),
|
|
453
475
|
checkouts: /* @__PURE__ */ new Map(),
|
|
476
|
+
payments: /* @__PURE__ */ new Map(),
|
|
477
|
+
restock: /* @__PURE__ */ new Set(),
|
|
454
478
|
seq: 0
|
|
455
479
|
};
|
|
456
480
|
}
|
|
481
|
+
const lowStockThreshold = options.lowStockThreshold ?? 5;
|
|
482
|
+
function syncAvailability(products) {
|
|
483
|
+
for (const p of products) {
|
|
484
|
+
for (const v of p.variants) {
|
|
485
|
+
const qty = v.quantity_available;
|
|
486
|
+
const sellable = v.available_for_sale && (qty === null || qty > 0);
|
|
487
|
+
v.available_for_sale = sellable;
|
|
488
|
+
const low = sellable && qty !== null && lowStockThreshold > 0 && qty <= lowStockThreshold;
|
|
489
|
+
v.availability = {
|
|
490
|
+
status: !sellable ? "out_of_stock" : low ? "low_stock" : "in_stock",
|
|
491
|
+
quantity: low ? qty : null,
|
|
492
|
+
max_per_order: sellable ? qty === null ? null : Math.min(qty, 10) : null,
|
|
493
|
+
restock_eta: null,
|
|
494
|
+
preorder_release_at: null,
|
|
495
|
+
notify_available: !sellable
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
p.available_for_sale = p.variants.some((v) => v.available_for_sale);
|
|
499
|
+
const rank = ["in_stock", "low_stock", "backorder", "preorder", "out_of_stock"];
|
|
500
|
+
const best = p.variants.map((v) => v.availability).sort((a, b) => rank.indexOf(a.status) - rank.indexOf(b.status))[0];
|
|
501
|
+
p.availability = best ?? resolveAvailability(p);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
457
504
|
let state = seed();
|
|
505
|
+
syncAvailability(state.products);
|
|
458
506
|
const nextId = (prefix) => `${prefix}_${(++state.seq).toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
459
507
|
function findVariant(variantId) {
|
|
460
508
|
for (const product of state.products) {
|
|
@@ -573,7 +621,8 @@ function createMockStorefront(options = {}) {
|
|
|
573
621
|
}
|
|
574
622
|
function validateLines(lines, cart) {
|
|
575
623
|
if (!Array.isArray(lines)) throw new HttpError(400, "validation", "`lines` must be an array", "lines");
|
|
576
|
-
|
|
624
|
+
const toCheck = [];
|
|
625
|
+
const out = lines.map((raw, i) => {
|
|
577
626
|
const l = raw;
|
|
578
627
|
if (typeof l.variant_id !== "string") throw new HttpError(400, "validation", "variant_id is required", `lines[${i}].variant_id`);
|
|
579
628
|
const quantity = l.quantity === void 0 ? 1 : l.quantity;
|
|
@@ -582,15 +631,36 @@ function createMockStorefront(options = {}) {
|
|
|
582
631
|
}
|
|
583
632
|
const found = findVariant(l.variant_id);
|
|
584
633
|
if (!found) throw new HttpError(400, "validation", `Unknown variant ${l.variant_id}`, `lines[${i}].variant_id`);
|
|
585
|
-
const
|
|
586
|
-
|
|
634
|
+
const existing = cart?.lines.find((x) => x.variant_id === l.variant_id);
|
|
635
|
+
toCheck.push({ variant: found.variant, quantity: (existing?.quantity ?? 0) + quantity, lineId: existing?.id ?? null, field: `lines[${i}].quantity` });
|
|
587
636
|
return { variant_id: l.variant_id, quantity };
|
|
588
637
|
});
|
|
638
|
+
checkStock(toCheck);
|
|
639
|
+
return out;
|
|
589
640
|
}
|
|
590
|
-
function
|
|
591
|
-
|
|
592
|
-
|
|
641
|
+
function visibleMax(variant) {
|
|
642
|
+
const a = variant.availability;
|
|
643
|
+
if (!a || a.status === "out_of_stock") return 0;
|
|
644
|
+
return a.status === "low_stock" ? a.quantity : null;
|
|
645
|
+
}
|
|
646
|
+
function lineProblem(variant, quantity) {
|
|
647
|
+
if (!variant.available_for_sale) return { code: "out_of_stock", max_quantity: 0 };
|
|
648
|
+
if (variant.quantity_available !== null && quantity > variant.quantity_available) {
|
|
649
|
+
return { code: "insufficient_stock", max_quantity: visibleMax(variant) };
|
|
593
650
|
}
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
function checkStock(items) {
|
|
654
|
+
const bad = items.filter((i) => lineProblem(i.variant, i.quantity));
|
|
655
|
+
if (bad.length === 0) return;
|
|
656
|
+
const lines = bad.map((i) => ({
|
|
657
|
+
line_id: i.lineId,
|
|
658
|
+
variant_id: i.variant.id,
|
|
659
|
+
sku: i.variant.sku,
|
|
660
|
+
status: i.variant.availability?.status ?? "out_of_stock",
|
|
661
|
+
max_quantity: visibleMax(i.variant)
|
|
662
|
+
}));
|
|
663
|
+
throw new HttpError(409, "insufficient_stock", "Algunos productos ya no tienen existencias suficientes", bad[0].field, { lines });
|
|
594
664
|
}
|
|
595
665
|
function addLines(cart, lines) {
|
|
596
666
|
for (const l of lines) {
|
|
@@ -623,6 +693,7 @@ function createMockStorefront(options = {}) {
|
|
|
623
693
|
lines.push({
|
|
624
694
|
id: l.id,
|
|
625
695
|
quantity: l.quantity,
|
|
696
|
+
problem: lineProblem(variant, l.quantity),
|
|
626
697
|
merchandise: {
|
|
627
698
|
...variant,
|
|
628
699
|
product: { handle: product.handle, title: product.title, featured_image: product.featured_image }
|
|
@@ -661,6 +732,7 @@ function createMockStorefront(options = {}) {
|
|
|
661
732
|
total: fromCents(total, currency),
|
|
662
733
|
tax_included: true
|
|
663
734
|
},
|
|
735
|
+
note: cart.note,
|
|
664
736
|
updated_at: cart.updated_at
|
|
665
737
|
};
|
|
666
738
|
}
|
|
@@ -744,7 +816,7 @@ function createMockStorefront(options = {}) {
|
|
|
744
816
|
const nodes = skus.flatMap((sku) => {
|
|
745
817
|
for (const p of state.products) {
|
|
746
818
|
const v = p.variants.find((x) => x.sku === sku);
|
|
747
|
-
if (v) return [{ sku, quantity_available: v.quantity_available }];
|
|
819
|
+
if (v) return [{ sku, quantity_available: v.quantity_available, ...v.availability ? { availability: v.availability } : {} }];
|
|
748
820
|
}
|
|
749
821
|
return [];
|
|
750
822
|
});
|
|
@@ -758,6 +830,7 @@ function createMockStorefront(options = {}) {
|
|
|
758
830
|
buyer: { email: null, phone: null, customer_id: null },
|
|
759
831
|
shipping_address: null,
|
|
760
832
|
delivery: { method: null, rate_id: null, branch_id: null },
|
|
833
|
+
note: null,
|
|
761
834
|
updated_at: now()
|
|
762
835
|
};
|
|
763
836
|
if (body.lines !== void 0) addLines(cart, validateLines(body.lines));
|
|
@@ -781,8 +854,13 @@ function createMockStorefront(options = {}) {
|
|
|
781
854
|
if (typeof u.quantity !== "number" || !Number.isInteger(u.quantity) || u.quantity < 0) {
|
|
782
855
|
throw new HttpError(400, "validation", "quantity must be a non-negative integer", `lines[${i}].quantity`);
|
|
783
856
|
}
|
|
784
|
-
if (u.quantity > 0) checkStock(findVariant(line.variant_id).variant, u.quantity, `lines[${i}].quantity`);
|
|
785
857
|
});
|
|
858
|
+
checkStock(
|
|
859
|
+
updates.flatMap((u, i) => {
|
|
860
|
+
const line = cart.lines.find((l) => l.id === u.id);
|
|
861
|
+
return u.quantity > 0 ? [{ variant: findVariant(line.variant_id).variant, quantity: u.quantity, lineId: line.id, field: `lines[${i}].quantity` }] : [];
|
|
862
|
+
})
|
|
863
|
+
);
|
|
786
864
|
for (const u of updates) {
|
|
787
865
|
const line = cart.lines.find((l) => l.id === u.id);
|
|
788
866
|
line.quantity = u.quantity;
|
|
@@ -849,6 +927,40 @@ function createMockStorefront(options = {}) {
|
|
|
849
927
|
cart.updated_at = now();
|
|
850
928
|
return { body: renderCart(cart), cache: "private" };
|
|
851
929
|
});
|
|
930
|
+
route("PUT", "/carts/:id/note", "cart:write", ({ params, body }) => {
|
|
931
|
+
const cart = getCartRecord(params[0]);
|
|
932
|
+
const raw = body.note;
|
|
933
|
+
if (raw !== null && raw !== void 0 && typeof raw !== "string") throw new HttpError(400, "validation", "note must be a string or null", "note");
|
|
934
|
+
const text = typeof raw === "string" ? raw.trim() : "";
|
|
935
|
+
if (text.length > 1e3) throw new HttpError(400, "validation", "note must be at most 1000 characters", "note");
|
|
936
|
+
cart.note = text || null;
|
|
937
|
+
cart.updated_at = now();
|
|
938
|
+
return { body: renderCart(cart), cache: "private" };
|
|
939
|
+
});
|
|
940
|
+
route("GET", "/carts/:id/recommendations", "cart:write", ({ params, search }) => {
|
|
941
|
+
const cart = getCartRecord(params[0]);
|
|
942
|
+
const first = Math.min(8, Math.max(1, Number.parseInt(search.get("first") ?? "4", 10) || 4));
|
|
943
|
+
const inCart = new Set(cart.lines.map((l) => findVariant(l.variant_id)?.product.id));
|
|
944
|
+
const nodes = [];
|
|
945
|
+
for (const p of state.products) {
|
|
946
|
+
if (inCart.has(p.id)) continue;
|
|
947
|
+
const variants = p.variants.filter((v) => v.available_for_sale);
|
|
948
|
+
if (variants.length === 0) continue;
|
|
949
|
+
nodes.push({
|
|
950
|
+
product: { id: p.id, handle: p.handle, title: p.title, featured_image: p.featured_image, options: p.options, vendor: p.vendor },
|
|
951
|
+
variants,
|
|
952
|
+
default_variant_id: variants[0].id,
|
|
953
|
+
source: "similar",
|
|
954
|
+
unlocks: null
|
|
955
|
+
});
|
|
956
|
+
if (nodes.length >= first) break;
|
|
957
|
+
}
|
|
958
|
+
return { body: { nodes, promotions: null }, cache: "private" };
|
|
959
|
+
});
|
|
960
|
+
route("GET", "/promotions", "catalog:read", () => ({
|
|
961
|
+
body: { nodes: [], announcements: options.announcements ?? [] },
|
|
962
|
+
cache: "public"
|
|
963
|
+
}));
|
|
852
964
|
route("POST", "/carts/:id/checkout", "checkout:write", ({ params, body, headers }) => {
|
|
853
965
|
const key = headers.get("idempotency-key");
|
|
854
966
|
if (key) {
|
|
@@ -865,7 +977,7 @@ function createMockStorefront(options = {}) {
|
|
|
865
977
|
}
|
|
866
978
|
if (typeof body.payment_method !== "string" || !body.payment_method) throw new HttpError(400, "validation", "payment_method is required", "payment_method");
|
|
867
979
|
if (typeof body.return_url !== "string" || !body.return_url) throw new HttpError(400, "validation", "return_url is required", "return_url");
|
|
868
|
-
record.lines.
|
|
980
|
+
checkStock(record.lines.map((l, i) => ({ variant: findVariant(l.variant_id).variant, quantity: l.quantity, lineId: l.id, field: `lines[${i}].quantity` })));
|
|
869
981
|
for (const l of record.lines) {
|
|
870
982
|
const v = findVariant(l.variant_id).variant;
|
|
871
983
|
if (v.quantity_available !== null) {
|
|
@@ -873,7 +985,7 @@ function createMockStorefront(options = {}) {
|
|
|
873
985
|
v.available_for_sale = v.quantity_available > 0;
|
|
874
986
|
}
|
|
875
987
|
}
|
|
876
|
-
|
|
988
|
+
syncAvailability(state.products);
|
|
877
989
|
const orderId = nextId("ord");
|
|
878
990
|
const number = String(1001 + state.orders.length - 1);
|
|
879
991
|
const customerId = cart.buyer.customer_id ?? state.customers.find((c) => c.email && c.email === cart.buyer.email)?.id ?? null;
|
|
@@ -917,6 +1029,110 @@ function createMockStorefront(options = {}) {
|
|
|
917
1029
|
if (!found) throw new HttpError(404, "not_found", "Checkout not found");
|
|
918
1030
|
return { body: found, cache: "private" };
|
|
919
1031
|
});
|
|
1032
|
+
const DECLINES = {
|
|
1033
|
+
tok_OTHE: "declined",
|
|
1034
|
+
tok_CALL: "call_issuer",
|
|
1035
|
+
tok_FUND: "insufficient_funds",
|
|
1036
|
+
tok_SECU: "invalid_cvc",
|
|
1037
|
+
tok_EXPI: "invalid_expiry",
|
|
1038
|
+
tok_FORM: "invalid_card_data"
|
|
1039
|
+
};
|
|
1040
|
+
const maxPaymentAttempts = options.maxPaymentAttempts ?? 5;
|
|
1041
|
+
route("GET", "/checkouts/:id/payment-config", "checkout:write", ({ params }) => {
|
|
1042
|
+
const found = state.checkouts.get(params[0]);
|
|
1043
|
+
if (!found) throw new HttpError(404, "not_found", "Checkout not found");
|
|
1044
|
+
const st = state.payments.get(params[0]);
|
|
1045
|
+
const config = {
|
|
1046
|
+
provider: "mock",
|
|
1047
|
+
flow: "embedded",
|
|
1048
|
+
option: "card",
|
|
1049
|
+
payable: found.checkout.status === "pending" && (st?.attempts ?? 0) < maxPaymentAttempts,
|
|
1050
|
+
reason: null,
|
|
1051
|
+
amount: found.checkout.total,
|
|
1052
|
+
currency: found.checkout.total.currency_code,
|
|
1053
|
+
public_key: "TEST-mock-public-key",
|
|
1054
|
+
methods: ["card", "cash"],
|
|
1055
|
+
options: [{ key: "card", redirect: false, max_installments: 12 }, { key: "cash", redirect: false }],
|
|
1056
|
+
client: { kind: "mock", sdk_url: "https://mock.local/sdk.js", public_key: "TEST-mock-public-key" },
|
|
1057
|
+
checkout: found.checkout,
|
|
1058
|
+
action: st?.last?.payment.action ?? null,
|
|
1059
|
+
attempts_left: maxPaymentAttempts - (st?.attempts ?? 0)
|
|
1060
|
+
};
|
|
1061
|
+
return { body: config, cache: "private" };
|
|
1062
|
+
});
|
|
1063
|
+
route("POST", "/checkouts/:id/payment", "checkout:write", ({ params, body, headers }) => {
|
|
1064
|
+
const key = headers.get("idempotency-key");
|
|
1065
|
+
if (!key) throw new HttpError(400, "validation", "Idempotency-Key header is required", "Idempotency-Key");
|
|
1066
|
+
const id = params[0];
|
|
1067
|
+
const prev = state.idempotency.get(`pay:${id}:${key}`);
|
|
1068
|
+
if (prev) return { status: prev.status, body: prev.body, cache: "private" };
|
|
1069
|
+
const found = state.checkouts.get(id);
|
|
1070
|
+
if (!found) throw new HttpError(404, "not_found", "Checkout not found");
|
|
1071
|
+
const method = body.method;
|
|
1072
|
+
if (method !== "card" && method !== "cash" && method !== "bank_transfer") throw new HttpError(400, "validation", "method must be card, cash or bank_transfer", "method");
|
|
1073
|
+
const data = body.provider_data;
|
|
1074
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) throw new HttpError(400, "validation", "provider_data is required", "provider_data");
|
|
1075
|
+
const exp = body.expected_total;
|
|
1076
|
+
if (exp && exp.amount !== void 0 && toCents(exp.amount) !== toCents(found.checkout.total.amount)) {
|
|
1077
|
+
throw new HttpError(422, "price_mismatch", "The total changed", "expected_total", { cost: found.checkout.total });
|
|
1078
|
+
}
|
|
1079
|
+
const st = state.payments.get(id) ?? { attempts: 0, last: null };
|
|
1080
|
+
if (found.checkout.status === "paid" || st.last?.payment.status === "pending" || st.last?.payment.status === "requires_action") {
|
|
1081
|
+
throw new HttpError(409, "conflict", "A payment is already in progress", "payment_in_progress");
|
|
1082
|
+
}
|
|
1083
|
+
if (found.checkout.stock_hold_expires_at && Date.parse(found.checkout.stock_hold_expires_at) < Date.now()) {
|
|
1084
|
+
throw new HttpError(409, "expired", "The reservation expired");
|
|
1085
|
+
}
|
|
1086
|
+
if (st.attempts >= maxPaymentAttempts) throw new HttpError(429, "rate_limited", "too_many_attempts");
|
|
1087
|
+
st.attempts += 1;
|
|
1088
|
+
const token = typeof data.token === "string" ? data.token : "";
|
|
1089
|
+
const attemptsLeft = maxPaymentAttempts - st.attempts;
|
|
1090
|
+
let payment;
|
|
1091
|
+
if (method !== "card") {
|
|
1092
|
+
const pointId = String(data.payment_method_id ?? "oxxo");
|
|
1093
|
+
payment = {
|
|
1094
|
+
provider: "mock",
|
|
1095
|
+
status: "requires_action",
|
|
1096
|
+
action: {
|
|
1097
|
+
type: "voucher",
|
|
1098
|
+
voucher: {
|
|
1099
|
+
payment_point: pointId,
|
|
1100
|
+
reference: `${pointId.toUpperCase()}-${id.slice(-6).toUpperCase()}`,
|
|
1101
|
+
voucher_url: `https://mock.local/voucher/${id}`,
|
|
1102
|
+
amount: found.checkout.total.amount,
|
|
1103
|
+
currency: found.checkout.total.currency_code,
|
|
1104
|
+
expires_at: new Date(Date.now() + 48 * 36e5).toISOString()
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
} else if (token in DECLINES) {
|
|
1109
|
+
payment = { provider: "mock", status: "failed", decline_code: DECLINES[token], retryable: true };
|
|
1110
|
+
} else if (token === "tok_CONT") {
|
|
1111
|
+
payment = { provider: "mock", status: "pending", action: { type: "none" } };
|
|
1112
|
+
} else if (token === "tok_3DS") {
|
|
1113
|
+
payment = { provider: "mock", status: "requires_action", action: { type: "challenge", challenge: { url: "https://mock.local/3ds", creq: "creq" } } };
|
|
1114
|
+
} else {
|
|
1115
|
+
payment = { provider: "mock", status: "paid" };
|
|
1116
|
+
found.checkout.status = "paid";
|
|
1117
|
+
}
|
|
1118
|
+
payment.outcome = payment.status === "paid" ? "approved" : payment.status === "failed" ? "rejected" : payment.action?.type === "challenge" ? "challenge" : "pending";
|
|
1119
|
+
const result = { attempt: { n: st.attempts }, payment, checkout: found.checkout, attempts_left: attemptsLeft };
|
|
1120
|
+
st.last = result;
|
|
1121
|
+
state.payments.set(id, st);
|
|
1122
|
+
state.idempotency.set(`pay:${id}:${key}`, { status: 200, body: result });
|
|
1123
|
+
return { body: result, cache: "private" };
|
|
1124
|
+
});
|
|
1125
|
+
route("POST", "/restock-subscriptions", "customer:write", ({ body }) => {
|
|
1126
|
+
if (typeof body.sku !== "string" || !body.sku) throw new HttpError(400, "validation", "sku is required", "sku");
|
|
1127
|
+
if (typeof body.locale !== "string" || !body.locale) throw new HttpError(400, "validation", "locale is required", "locale");
|
|
1128
|
+
if (body.consent !== true) throw new HttpError(422, "invalid_state", "consent must be true", "consent");
|
|
1129
|
+
const contact = typeof body.email === "string" && body.email ? body.email.toLowerCase() : typeof body.phone === "string" ? body.phone : "";
|
|
1130
|
+
if (!contact) throw new HttpError(400, "validation", "email or phone is required", "email");
|
|
1131
|
+
const known = state.products.some((p) => p.variants.some((v) => v.sku === body.sku));
|
|
1132
|
+
if (!known) throw new HttpError(404, "not_found", `SKU ${body.sku} not found`, "sku");
|
|
1133
|
+
state.restock.add(`${contact}|${body.sku}`);
|
|
1134
|
+
return { status: 202, body: { status: "pending" }, cache: "private" };
|
|
1135
|
+
});
|
|
920
1136
|
route("POST", "/customer/otp", "customer:write", ({ body }) => {
|
|
921
1137
|
state.otps.set(identifierOf(body), otpCode);
|
|
922
1138
|
return { status: 204 };
|
|
@@ -1034,7 +1250,11 @@ function createMockStorefront(options = {}) {
|
|
|
1034
1250
|
});
|
|
1035
1251
|
}
|
|
1036
1252
|
function errorResponse(err) {
|
|
1037
|
-
return json(
|
|
1253
|
+
return json(
|
|
1254
|
+
err.status,
|
|
1255
|
+
{ error: { code: err.code, message: err.message, ...err.field ? { field: err.field } : {}, ...err.details !== void 0 ? { details: err.details } : {} } },
|
|
1256
|
+
{ "Cache-Control": PRIVATE_CACHE }
|
|
1257
|
+
);
|
|
1038
1258
|
}
|
|
1039
1259
|
async function handle(request) {
|
|
1040
1260
|
const url = new URL(request.url);
|
|
@@ -1098,6 +1318,7 @@ function createMockStorefront(options = {}) {
|
|
|
1098
1318
|
/** Restores the seed data and clears carts, sessions and orders. */
|
|
1099
1319
|
reset() {
|
|
1100
1320
|
state = seed();
|
|
1321
|
+
syncAvailability(state.products);
|
|
1101
1322
|
},
|
|
1102
1323
|
/** OTP code accepted by `/customer/session`. */
|
|
1103
1324
|
otpCode
|