@asteby/storefront 0.4.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 +71 -1
- package/dist/chunk-HBSB7XS2.js +220 -0
- package/dist/chunk-HBSB7XS2.js.map +1 -0
- package/dist/index.cjs +732 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +290 -3
- package/dist/index.d.ts +290 -3
- package/dist/index.js +494 -4
- 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.cjs +218 -0
- package/dist/mockup.cjs.map +1 -0
- package/dist/mockup.d.cts +142 -0
- package/dist/mockup.d.ts +142 -0
- package/dist/mockup.js +205 -0
- package/dist/mockup.js.map +1 -0
- 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 +11 -1
package/dist/mock.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
562
|
-
|
|
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
|
|
567
|
-
if (!variant.available_for_sale
|
|
568
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|