@asteby/storefront 0.16.1 → 0.18.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 +24 -0
- package/dist/{types-DSHzYSHU.d.ts → account-settings-Cw3PamUA.d.ts} +241 -1
- package/dist/{types-C5nEQicj.d.cts → account-settings-DAzmXHIn.d.cts} +241 -1
- package/dist/{chunk-IPUAZ3LQ.js → chunk-5BJRZIS3.js} +196 -3
- package/dist/chunk-5BJRZIS3.js.map +1 -0
- package/dist/cli.js +62 -2
- package/dist/fiscal-document.d.cts +2 -2
- package/dist/fiscal-document.d.ts +2 -2
- package/dist/index.cjs +296 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +135 -70
- package/dist/index.d.ts +135 -70
- package/dist/index.js +87 -4
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +403 -3
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +33 -2
- package/dist/mock.d.ts +33 -2
- package/dist/mock.js +299 -5
- package/dist/mock.js.map +1 -1
- package/dist/mockup.d.cts +1 -1
- package/dist/mockup.d.ts +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IPUAZ3LQ.js.map +0 -1
package/dist/mock.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bM as SubjectKind, a6 as IdentityRulesV1, bg as RegionFiscalId, a4 as IdentityDocumentSpec, be as Region, bx as Shop, a$ as Product, y as Collection, bP as Theme, aG as Page, ap as Menu, c as Article, C as Cart, a as Address, af as InvoiceRequest, B as Customer, $ as FiscalProfile, bG as Subject, aw as Order, aJ as PaymentAttemptResult, aY as PendingPayment, ah as InvoiceStage, D as CustomerCapabilities, b6 as PromotionAnnouncement, bw as ShippingRate, aQ as PaymentMethod } from './account-settings-DAzmXHIn.cjs';
|
|
2
2
|
import './geo.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -16,6 +16,8 @@ declare const MOCK_POSTAL_CENTROIDS: Record<string, {
|
|
|
16
16
|
lng: number;
|
|
17
17
|
}>;
|
|
18
18
|
|
|
19
|
+
declare const MOCK_SUBJECT_KINDS: SubjectKind[];
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
22
|
* Region fixture of the mock: Mexico exactly as `GET /storefront/region` publishes it (see `region-mx.seed.ts`),
|
|
21
23
|
* identity document `MX:csf` included, so a storefront can be built and tested without a server. It lives here,
|
|
@@ -89,6 +91,11 @@ type MockStorefrontOptions = {
|
|
|
89
91
|
* carriers, ETAs and quote deadlines). `expires_at` missing: 24 h from the request. Free shipping still applies.
|
|
90
92
|
*/
|
|
91
93
|
shippingRates?: ShippingRate[];
|
|
94
|
+
/**
|
|
95
|
+
* `fixed_when_free`: past `policies.free_shipping_min` `delivery-options` serves ONLY the shop's free rate and flags
|
|
96
|
+
* `free` / `fixed` (what the `storefront_cart` addon does with the setting of the same name). Default: the usual list.
|
|
97
|
+
*/
|
|
98
|
+
shippingQuoteMode?: 'carriers' | 'fixed_when_free';
|
|
92
99
|
/** Methods served by `payment-methods` (default one builtin `mock`). */
|
|
93
100
|
paymentMethods?: PaymentMethod[];
|
|
94
101
|
/**
|
|
@@ -96,6 +103,11 @@ type MockStorefrontOptions = {
|
|
|
96
103
|
* organization has no region, the route answers `404` and invoice requests are not validated against a region.
|
|
97
104
|
*/
|
|
98
105
|
region?: Region | null;
|
|
106
|
+
/**
|
|
107
|
+
* The kinds of buyer item («My items») the store declares, served by `GET /subject-kinds` and enforced by
|
|
108
|
+
* `customer.subjects`. Default none: no such section, as in a store that declares nothing (`MOCK_SUBJECT_KINDS` has two).
|
|
109
|
+
*/
|
|
110
|
+
subjectKinds?: SubjectKind[];
|
|
99
111
|
};
|
|
100
112
|
type CartRecord = {
|
|
101
113
|
id: string;
|
|
@@ -128,6 +140,10 @@ type MockState = {
|
|
|
128
140
|
addresses: Map<string, Address[]>;
|
|
129
141
|
/** Saved fiscal profiles by customer id (contract 1.9). */
|
|
130
142
|
fiscalProfiles: Map<string, FiscalProfile[]>;
|
|
143
|
+
/** Saved items by customer id (contract 1.9); `private` holds the values the API only returns masked. */
|
|
144
|
+
subjects: Map<string, (Subject & {
|
|
145
|
+
private: Record<string, string>;
|
|
146
|
+
})[]>;
|
|
131
147
|
orders: (Order & {
|
|
132
148
|
customer_id: string | null;
|
|
133
149
|
})[];
|
|
@@ -143,6 +159,21 @@ type MockState = {
|
|
|
143
159
|
/** Consecutive failed password logins by email. */
|
|
144
160
|
loginFailures: Map<string, number>;
|
|
145
161
|
sessions: Map<string, string>;
|
|
162
|
+
/** What «Devices» shows for each session token (contract 1.9). */
|
|
163
|
+
sessionInfo: Map<string, {
|
|
164
|
+
id: string;
|
|
165
|
+
customer_id: string;
|
|
166
|
+
created_at: string;
|
|
167
|
+
last_used_at: string;
|
|
168
|
+
login_method: string;
|
|
169
|
+
reauth_at: number | null;
|
|
170
|
+
}>;
|
|
171
|
+
/** Wrong re-authentications per customer (5 and the mock answers 429). */
|
|
172
|
+
reauthFailures: Map<string, number>;
|
|
173
|
+
/** Data exports served per customer (3 a day). */
|
|
174
|
+
exportsServed: Map<string, number>;
|
|
175
|
+
/** Tokens of accounts that were deleted: deleting again answers 204, as the server does. */
|
|
176
|
+
deletedTokens: Set<string>;
|
|
146
177
|
idempotency: Map<string, {
|
|
147
178
|
status: number;
|
|
148
179
|
body: unknown;
|
|
@@ -176,4 +207,4 @@ declare function createMockStorefront(options?: MockStorefrontOptions): {
|
|
|
176
207
|
};
|
|
177
208
|
type MockStorefront = ReturnType<typeof createMockStorefront>;
|
|
178
209
|
|
|
179
|
-
export { MOCK_CSF_RULES, MOCK_CURRENCY, MOCK_CUSTOMER_EMAIL, MOCK_FISCAL_ID_MX, MOCK_IDENTITY_DOCUMENT_MX_CSF, MOCK_IMAGE_URL, MOCK_POSTAL_CENTROIDS, MOCK_REGION_MX, type MockImageUrl, type MockState, type MockStorefront, type MockStorefrontOptions, createMockStorefront };
|
|
210
|
+
export { MOCK_CSF_RULES, MOCK_CURRENCY, MOCK_CUSTOMER_EMAIL, MOCK_FISCAL_ID_MX, MOCK_IDENTITY_DOCUMENT_MX_CSF, MOCK_IMAGE_URL, MOCK_POSTAL_CENTROIDS, MOCK_REGION_MX, MOCK_SUBJECT_KINDS, type MockImageUrl, type MockState, type MockStorefront, type MockStorefrontOptions, createMockStorefront };
|
package/dist/mock.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bM as SubjectKind, a6 as IdentityRulesV1, bg as RegionFiscalId, a4 as IdentityDocumentSpec, be as Region, bx as Shop, a$ as Product, y as Collection, bP as Theme, aG as Page, ap as Menu, c as Article, C as Cart, a as Address, af as InvoiceRequest, B as Customer, $ as FiscalProfile, bG as Subject, aw as Order, aJ as PaymentAttemptResult, aY as PendingPayment, ah as InvoiceStage, D as CustomerCapabilities, b6 as PromotionAnnouncement, bw as ShippingRate, aQ as PaymentMethod } from './account-settings-Cw3PamUA.js';
|
|
2
2
|
import './geo.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -16,6 +16,8 @@ declare const MOCK_POSTAL_CENTROIDS: Record<string, {
|
|
|
16
16
|
lng: number;
|
|
17
17
|
}>;
|
|
18
18
|
|
|
19
|
+
declare const MOCK_SUBJECT_KINDS: SubjectKind[];
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
22
|
* Region fixture of the mock: Mexico exactly as `GET /storefront/region` publishes it (see `region-mx.seed.ts`),
|
|
21
23
|
* identity document `MX:csf` included, so a storefront can be built and tested without a server. It lives here,
|
|
@@ -89,6 +91,11 @@ type MockStorefrontOptions = {
|
|
|
89
91
|
* carriers, ETAs and quote deadlines). `expires_at` missing: 24 h from the request. Free shipping still applies.
|
|
90
92
|
*/
|
|
91
93
|
shippingRates?: ShippingRate[];
|
|
94
|
+
/**
|
|
95
|
+
* `fixed_when_free`: past `policies.free_shipping_min` `delivery-options` serves ONLY the shop's free rate and flags
|
|
96
|
+
* `free` / `fixed` (what the `storefront_cart` addon does with the setting of the same name). Default: the usual list.
|
|
97
|
+
*/
|
|
98
|
+
shippingQuoteMode?: 'carriers' | 'fixed_when_free';
|
|
92
99
|
/** Methods served by `payment-methods` (default one builtin `mock`). */
|
|
93
100
|
paymentMethods?: PaymentMethod[];
|
|
94
101
|
/**
|
|
@@ -96,6 +103,11 @@ type MockStorefrontOptions = {
|
|
|
96
103
|
* organization has no region, the route answers `404` and invoice requests are not validated against a region.
|
|
97
104
|
*/
|
|
98
105
|
region?: Region | null;
|
|
106
|
+
/**
|
|
107
|
+
* The kinds of buyer item («My items») the store declares, served by `GET /subject-kinds` and enforced by
|
|
108
|
+
* `customer.subjects`. Default none: no such section, as in a store that declares nothing (`MOCK_SUBJECT_KINDS` has two).
|
|
109
|
+
*/
|
|
110
|
+
subjectKinds?: SubjectKind[];
|
|
99
111
|
};
|
|
100
112
|
type CartRecord = {
|
|
101
113
|
id: string;
|
|
@@ -128,6 +140,10 @@ type MockState = {
|
|
|
128
140
|
addresses: Map<string, Address[]>;
|
|
129
141
|
/** Saved fiscal profiles by customer id (contract 1.9). */
|
|
130
142
|
fiscalProfiles: Map<string, FiscalProfile[]>;
|
|
143
|
+
/** Saved items by customer id (contract 1.9); `private` holds the values the API only returns masked. */
|
|
144
|
+
subjects: Map<string, (Subject & {
|
|
145
|
+
private: Record<string, string>;
|
|
146
|
+
})[]>;
|
|
131
147
|
orders: (Order & {
|
|
132
148
|
customer_id: string | null;
|
|
133
149
|
})[];
|
|
@@ -143,6 +159,21 @@ type MockState = {
|
|
|
143
159
|
/** Consecutive failed password logins by email. */
|
|
144
160
|
loginFailures: Map<string, number>;
|
|
145
161
|
sessions: Map<string, string>;
|
|
162
|
+
/** What «Devices» shows for each session token (contract 1.9). */
|
|
163
|
+
sessionInfo: Map<string, {
|
|
164
|
+
id: string;
|
|
165
|
+
customer_id: string;
|
|
166
|
+
created_at: string;
|
|
167
|
+
last_used_at: string;
|
|
168
|
+
login_method: string;
|
|
169
|
+
reauth_at: number | null;
|
|
170
|
+
}>;
|
|
171
|
+
/** Wrong re-authentications per customer (5 and the mock answers 429). */
|
|
172
|
+
reauthFailures: Map<string, number>;
|
|
173
|
+
/** Data exports served per customer (3 a day). */
|
|
174
|
+
exportsServed: Map<string, number>;
|
|
175
|
+
/** Tokens of accounts that were deleted: deleting again answers 204, as the server does. */
|
|
176
|
+
deletedTokens: Set<string>;
|
|
146
177
|
idempotency: Map<string, {
|
|
147
178
|
status: number;
|
|
148
179
|
body: unknown;
|
|
@@ -176,4 +207,4 @@ declare function createMockStorefront(options?: MockStorefrontOptions): {
|
|
|
176
207
|
};
|
|
177
208
|
type MockStorefront = ReturnType<typeof createMockStorefront>;
|
|
178
209
|
|
|
179
|
-
export { MOCK_CSF_RULES, MOCK_CURRENCY, MOCK_CUSTOMER_EMAIL, MOCK_FISCAL_ID_MX, MOCK_IDENTITY_DOCUMENT_MX_CSF, MOCK_IMAGE_URL, MOCK_POSTAL_CENTROIDS, MOCK_REGION_MX, type MockImageUrl, type MockState, type MockStorefront, type MockStorefrontOptions, createMockStorefront };
|
|
210
|
+
export { MOCK_CSF_RULES, MOCK_CURRENCY, MOCK_CUSTOMER_EMAIL, MOCK_FISCAL_ID_MX, MOCK_IDENTITY_DOCUMENT_MX_CSF, MOCK_IMAGE_URL, MOCK_POSTAL_CENTROIDS, MOCK_REGION_MX, MOCK_SUBJECT_KINDS, type MockImageUrl, type MockState, type MockStorefront, type MockStorefrontOptions, createMockStorefront };
|
package/dist/mock.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveAvailability, isTestShipment, sortTrackingEvents, orderProgress, nextAction, orderShipments, shipmentsStatus } from './chunk-
|
|
1
|
+
import { resolveAvailability, isMasked, isTestShipment, sortTrackingEvents, orderProgress, nextAction, orderShipments, shipmentsStatus, PRIVATE_MASK, resolveSubject } from './chunk-5BJRZIS3.js';
|
|
2
2
|
import { isValidPoint, rankByDistance } from './chunk-45R47HYO.js';
|
|
3
3
|
|
|
4
4
|
// src/mock/account-seed.ts
|
|
@@ -1511,6 +1511,55 @@ function buildCustomers() {
|
|
|
1511
1511
|
];
|
|
1512
1512
|
}
|
|
1513
1513
|
|
|
1514
|
+
// src/mock/subjects.ts
|
|
1515
|
+
var MOCK_SUBJECT_KINDS = [
|
|
1516
|
+
{
|
|
1517
|
+
key: "vehicle",
|
|
1518
|
+
label: "Veh\xEDculo",
|
|
1519
|
+
label_plural: "Mi garage",
|
|
1520
|
+
max_per_account: 3,
|
|
1521
|
+
attach_to_order: true,
|
|
1522
|
+
fit_key_template: "{make}/{model}/{year}/{engine}",
|
|
1523
|
+
fields: [
|
|
1524
|
+
{ key: "make", label: "Marca", type: "select", source: "catalog", required: true },
|
|
1525
|
+
{ key: "model", label: "Modelo", type: "select", source: "catalog", depends_on: "make", required: true },
|
|
1526
|
+
{ key: "year", label: "A\xF1o", type: "year", source: "catalog", depends_on: "model", required: true },
|
|
1527
|
+
{ key: "engine", label: "Versi\xF3n / motor", type: "select", source: "catalog", depends_on: "year" },
|
|
1528
|
+
{ key: "plate", label: "Placas (opcional)", type: "text", max: 12, private: true }
|
|
1529
|
+
],
|
|
1530
|
+
catalog: {
|
|
1531
|
+
version: "mock-1",
|
|
1532
|
+
tree: {
|
|
1533
|
+
nissan: {
|
|
1534
|
+
label: "Nissan",
|
|
1535
|
+
children: {
|
|
1536
|
+
np300: {
|
|
1537
|
+
label: "NP300 Frontier",
|
|
1538
|
+
from: 2008,
|
|
1539
|
+
to: 2022,
|
|
1540
|
+
children: { "2.5": { label: "2.5 L", ranges: [[2010, 2015]] }, "2.4": { label: "2.4 L" } }
|
|
1541
|
+
},
|
|
1542
|
+
versa: { label: "Versa", from: 2012, to: 2024, children: { "1.6": { label: "1.6 L" } } }
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
ford: { label: "Ford", children: { f150: { label: "F-150", from: 2e3, to: 2020, children: { "5.0": { label: "5.0 L" } } } } }
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
key: "size",
|
|
1551
|
+
label: "Talla",
|
|
1552
|
+
label_plural: "Mis tallas",
|
|
1553
|
+
max_per_account: 5,
|
|
1554
|
+
fit_key_template: "{garment}/{value}",
|
|
1555
|
+
fields: [
|
|
1556
|
+
{ key: "garment", label: "Prenda", type: "text", required: true },
|
|
1557
|
+
{ key: "value", label: "Talla", type: "text", required: true, max: 8 }
|
|
1558
|
+
],
|
|
1559
|
+
catalog: null
|
|
1560
|
+
}
|
|
1561
|
+
];
|
|
1562
|
+
|
|
1514
1563
|
// src/mock/index.ts
|
|
1515
1564
|
var HttpError = class extends Error {
|
|
1516
1565
|
constructor(status, code, message, field, details, requiredScopes) {
|
|
@@ -1585,6 +1634,10 @@ function createMockStorefront(options = {}) {
|
|
|
1585
1634
|
online_verify: false
|
|
1586
1635
|
},
|
|
1587
1636
|
fiscal_profiles: { max: 10 },
|
|
1637
|
+
notifications: { channels: ["email"] },
|
|
1638
|
+
data_export: true,
|
|
1639
|
+
account_delete: true,
|
|
1640
|
+
...(options.subjectKinds?.length ?? 0) > 0 ? { subjects: { kinds: options.subjectKinds.map((k) => k.key), share_with_workshop: false } } : {},
|
|
1588
1641
|
...options.customerCapabilities
|
|
1589
1642
|
};
|
|
1590
1643
|
const missingScopes = options.missingScopes ?? [];
|
|
@@ -1616,6 +1669,7 @@ function createMockStorefront(options = {}) {
|
|
|
1616
1669
|
customers: buildCustomers(),
|
|
1617
1670
|
addresses: /* @__PURE__ */ new Map(),
|
|
1618
1671
|
fiscalProfiles: /* @__PURE__ */ new Map(),
|
|
1672
|
+
subjects: /* @__PURE__ */ new Map(),
|
|
1619
1673
|
orders: [
|
|
1620
1674
|
{
|
|
1621
1675
|
id: "ord_seed_1",
|
|
@@ -1636,6 +1690,10 @@ function createMockStorefront(options = {}) {
|
|
|
1636
1690
|
pendingRegistrations: /* @__PURE__ */ new Map(),
|
|
1637
1691
|
loginFailures: /* @__PURE__ */ new Map(),
|
|
1638
1692
|
sessions: /* @__PURE__ */ new Map(),
|
|
1693
|
+
sessionInfo: /* @__PURE__ */ new Map(),
|
|
1694
|
+
reauthFailures: /* @__PURE__ */ new Map(),
|
|
1695
|
+
exportsServed: /* @__PURE__ */ new Map(),
|
|
1696
|
+
deletedTokens: /* @__PURE__ */ new Set(),
|
|
1639
1697
|
idempotency: /* @__PURE__ */ new Map(),
|
|
1640
1698
|
checkouts: /* @__PURE__ */ new Map(),
|
|
1641
1699
|
payments: /* @__PURE__ */ new Map(),
|
|
@@ -1717,6 +1775,9 @@ function createMockStorefront(options = {}) {
|
|
|
1717
1775
|
if (list.length > 100) throw new HttpError(400, "validation", "At most 100 handles", "handles");
|
|
1718
1776
|
items = items.filter((p) => list.includes(p.handle));
|
|
1719
1777
|
}
|
|
1778
|
+
const tagsAny = (search.get("tags_any") ?? "").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
1779
|
+
if (tagsAny.length > 5 || tagsAny.some((t) => t.length > 120)) throw new HttpError(400, "validation", "tags_any takes at most 5 tags of 120 characters", "tags_any");
|
|
1780
|
+
if (tagsAny.length > 0) items = items.filter((p) => p.tags.some((t) => tagsAny.includes(t.toLowerCase())));
|
|
1720
1781
|
const base = items;
|
|
1721
1782
|
if (search.get("available") === "true") items = items.filter((p) => p.available_for_sale);
|
|
1722
1783
|
const priceMin = search.get("price_min");
|
|
@@ -1844,6 +1905,7 @@ function createMockStorefront(options = {}) {
|
|
|
1844
1905
|
const expires = new Date(Date.now() + 24 * 36e5).toISOString();
|
|
1845
1906
|
return options.shippingRates.map((r) => ({ ...r, expires_at: r.expires_at ?? expires, cost: free ? fromCents(0, r.cost.currency_code) : r.cost }));
|
|
1846
1907
|
}
|
|
1908
|
+
if (free && options.shippingQuoteMode === "fixed_when_free") return [{ id: "flat", title: "Env\xEDo gratis", cost: fromCents(0, currency), eta: "2 a 5 d\xEDas h\xE1biles" }];
|
|
1847
1909
|
return [
|
|
1848
1910
|
{ id: "flat", title: free ? "Env\xEDo est\xE1ndar (gratis)" : "Env\xEDo est\xE1ndar", cost: fromCents(free ? 0 : flatShippingCents, currency), eta: "2 a 5 d\xEDas h\xE1biles" },
|
|
1849
1911
|
{ id: "express", title: "Env\xEDo expr\xE9s", cost: fromCents(flatShippingCents + 1e4, currency), eta: "1 a 2 d\xEDas h\xE1biles" }
|
|
@@ -1986,8 +2048,13 @@ function createMockStorefront(options = {}) {
|
|
|
1986
2048
|
}
|
|
1987
2049
|
const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
|
|
1988
2050
|
state.sessions.set(token, customer.id);
|
|
2051
|
+
trackSession(token, "password");
|
|
1989
2052
|
return { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer };
|
|
1990
2053
|
}
|
|
2054
|
+
function trackSession(token, loginMethod) {
|
|
2055
|
+
const at = now();
|
|
2056
|
+
state.sessionInfo.set(token, { id: nextId("ses"), customer_id: state.sessions.get(token) ?? "", created_at: at, last_used_at: at, login_method: loginMethod, reauth_at: null });
|
|
2057
|
+
}
|
|
1991
2058
|
function validateAddress(body, partial = false) {
|
|
1992
2059
|
const required = ["address1", "city", "zip", "country_code"];
|
|
1993
2060
|
if (!partial) {
|
|
@@ -2142,8 +2209,10 @@ function createMockStorefront(options = {}) {
|
|
|
2142
2209
|
route("GET", "/carts/:id/delivery-options", "cart:write", ({ params }) => {
|
|
2143
2210
|
const cart = renderCart(getCartRecord(params[0]));
|
|
2144
2211
|
const net = toCents(cart.cost.subtotal.amount) - toCents(cart.cost.discount.amount);
|
|
2212
|
+
const rates = shippingRates(net);
|
|
2213
|
+
const free = options.shippingQuoteMode === "fixed_when_free" && rates.length > 0 && rates.every((r) => toCents(r.cost.amount) === 0);
|
|
2145
2214
|
return {
|
|
2146
|
-
body: { rates:
|
|
2215
|
+
body: { rates, free, fixed: free, pickup: state.shop.branches.filter((b) => b.pickup?.enabled !== false).map((b) => ({ branch_id: b.id, name: b.name, address: b.address, city: b.city })), partial: false, unavailable: [] },
|
|
2147
2216
|
cache: "private"
|
|
2148
2217
|
};
|
|
2149
2218
|
});
|
|
@@ -2415,6 +2484,7 @@ function createMockStorefront(options = {}) {
|
|
|
2415
2484
|
}
|
|
2416
2485
|
const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
|
|
2417
2486
|
state.sessions.set(token, customer.id);
|
|
2487
|
+
trackSession(token, kind);
|
|
2418
2488
|
return {
|
|
2419
2489
|
status: 201,
|
|
2420
2490
|
body: { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer },
|
|
@@ -2489,7 +2559,8 @@ function createMockStorefront(options = {}) {
|
|
|
2489
2559
|
state.loginFailures.delete(email);
|
|
2490
2560
|
return { status: 204 };
|
|
2491
2561
|
});
|
|
2492
|
-
const publicCustomer = (
|
|
2562
|
+
const publicCustomer = (c0) => {
|
|
2563
|
+
const c = { ...c0, locale: c0.locale ?? null, preferences: c0.preferences ?? { notifications: {} } };
|
|
2493
2564
|
if (!c.fiscal?.document) return c;
|
|
2494
2565
|
const { sha256: _sha, ...document } = c.fiscal.document;
|
|
2495
2566
|
return { ...c, fiscal: { ...c.fiscal, document } };
|
|
@@ -2501,6 +2572,8 @@ function createMockStorefront(options = {}) {
|
|
|
2501
2572
|
if ("first_name" in body) c.first_name = text(body.first_name);
|
|
2502
2573
|
if ("last_name" in body) c.last_name = text(body.last_name);
|
|
2503
2574
|
if ("phone" in body) c.phone = text(body.phone);
|
|
2575
|
+
if ("locale" in body) c.locale = parseLocale(body.locale);
|
|
2576
|
+
if ("preferences" in body) c.preferences = parsePreferences(c.preferences, body.preferences);
|
|
2504
2577
|
if ("fiscal" in body) {
|
|
2505
2578
|
if (body.fiscal === null) c.fiscal = null;
|
|
2506
2579
|
else {
|
|
@@ -2520,6 +2593,144 @@ function createMockStorefront(options = {}) {
|
|
|
2520
2593
|
}
|
|
2521
2594
|
return { body: publicCustomer(c), cache: "private" };
|
|
2522
2595
|
});
|
|
2596
|
+
const OPTIONAL_EVENTS = /* @__PURE__ */ new Set(["order_updates", "shipping", "restock"]);
|
|
2597
|
+
const MANDATORY_EVENTS = /* @__PURE__ */ new Set(["order_confirmation", "payment", "invoice", "security"]);
|
|
2598
|
+
const CHANNELS = /* @__PURE__ */ new Set(["email", "whatsapp"]);
|
|
2599
|
+
const REAUTH_WINDOW_MS = 5 * 6e4;
|
|
2600
|
+
const supportedLocales = () => [state.shop.locale, ...state.shop.locales].filter((l, i, a) => l && a.indexOf(l) === i);
|
|
2601
|
+
function parseLocale(raw) {
|
|
2602
|
+
if (raw === null || typeof raw === "string" && !raw.trim()) return null;
|
|
2603
|
+
if (typeof raw !== "string") throw new HttpError(400, "validation", "locale must be a string", "locale");
|
|
2604
|
+
const tag = raw.trim().replace("_", "-");
|
|
2605
|
+
const [lang = "", ...rest] = tag.split("-");
|
|
2606
|
+
const canonical = [lang.toLowerCase(), ...rest.map((p) => p.toUpperCase())].join("-");
|
|
2607
|
+
const supported = supportedLocales();
|
|
2608
|
+
const hit = supported.find((l) => l.toLowerCase() === canonical.toLowerCase()) ?? (rest.length === 0 ? supported.find((l) => l.split("-")[0].toLowerCase() === canonical) : void 0);
|
|
2609
|
+
if (!hit) throw new HttpError(400, "validation", "this store is not available in that language", "locale");
|
|
2610
|
+
return hit;
|
|
2611
|
+
}
|
|
2612
|
+
function parsePreferences(current, raw) {
|
|
2613
|
+
if (raw === null) return { notifications: {} };
|
|
2614
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new HttpError(400, "validation", "preferences must be an object", "preferences");
|
|
2615
|
+
const merged = structuredClone(current?.notifications ?? {});
|
|
2616
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2617
|
+
if (key !== "notifications") throw new HttpError(400, "validation", "unknown preference", `preferences.${key}`);
|
|
2618
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new HttpError(400, "validation", "notifications must map each event to its channels", "preferences.notifications");
|
|
2619
|
+
for (const [event, channels] of Object.entries(value)) {
|
|
2620
|
+
const field = `preferences.notifications.${event}`;
|
|
2621
|
+
if (MANDATORY_EVENTS.has(event)) throw new HttpError(400, "validation", "this notification is required and cannot be turned off", field);
|
|
2622
|
+
if (!OPTIONAL_EVENTS.has(event)) throw new HttpError(400, "validation", "unknown notification", field);
|
|
2623
|
+
if (!channels || typeof channels !== "object" || Array.isArray(channels)) throw new HttpError(400, "validation", "each event maps its channels to true or false", field);
|
|
2624
|
+
for (const [channel, on] of Object.entries(channels)) {
|
|
2625
|
+
if (!CHANNELS.has(channel)) throw new HttpError(400, "validation", "unknown channel", `${field}.${channel}`);
|
|
2626
|
+
if (typeof on !== "boolean") throw new HttpError(400, "validation", "each channel must be true or false", `${field}.${channel}`);
|
|
2627
|
+
(merged[event] ??= {})[channel] = on;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return { notifications: merged };
|
|
2632
|
+
}
|
|
2633
|
+
function sessionOf(headers) {
|
|
2634
|
+
const c = requireCustomer(headers);
|
|
2635
|
+
const token = headers.get("x-customer-token");
|
|
2636
|
+
if (!state.sessionInfo.has(token)) trackSession(token, "email");
|
|
2637
|
+
return { c, token, info: state.sessionInfo.get(token) };
|
|
2638
|
+
}
|
|
2639
|
+
function requireRecentReauth(info) {
|
|
2640
|
+
if (info.reauth_at === null || Date.now() - info.reauth_at > REAUTH_WINDOW_MS) {
|
|
2641
|
+
throw new HttpError(403, "reauth_required", "confirm your password or a code first (customer.reauth)");
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
route("GET", "/customer/sessions", "customer:read", ({ headers }) => {
|
|
2645
|
+
const { c, token } = sessionOf(headers);
|
|
2646
|
+
const nodes = [...state.sessions.entries()].filter(([, id]) => id === c.id).map(([t]) => ({ t, info: state.sessionInfo.get(t) })).sort((a, b) => b.info.last_used_at.localeCompare(a.info.last_used_at)).map(({ t, info }) => ({
|
|
2647
|
+
id: info.id,
|
|
2648
|
+
current: t === token,
|
|
2649
|
+
device_label: "Mock browser \xB7 Test",
|
|
2650
|
+
login_method: info.login_method,
|
|
2651
|
+
last_used_at: info.last_used_at,
|
|
2652
|
+
created_at: info.created_at
|
|
2653
|
+
}));
|
|
2654
|
+
return { body: { nodes }, cache: "private" };
|
|
2655
|
+
});
|
|
2656
|
+
route("DELETE", "/customer/sessions/:id", "customer:write", ({ headers, params }) => {
|
|
2657
|
+
const { c, token } = sessionOf(headers);
|
|
2658
|
+
const mine = [...state.sessions.entries()].filter(([, id]) => id === c.id).map(([t]) => t);
|
|
2659
|
+
if (params[0] === "others") {
|
|
2660
|
+
for (const t of mine) if (t !== token) state.sessions.delete(t);
|
|
2661
|
+
return { status: 204 };
|
|
2662
|
+
}
|
|
2663
|
+
const entry = [...state.sessionInfo.entries()].find(([, i]) => i.id === params[0]);
|
|
2664
|
+
if (!entry || entry[1].customer_id !== c.id) throw new HttpError(404, "not_found", "Session not found", "id");
|
|
2665
|
+
state.sessions.delete(entry[0]);
|
|
2666
|
+
return { status: 204 };
|
|
2667
|
+
});
|
|
2668
|
+
route("POST", "/customer/reauth", "customer:write", ({ headers, body }) => {
|
|
2669
|
+
const { c, info } = sessionOf(headers);
|
|
2670
|
+
const hasPassword = typeof body.password === "string" && body.password !== "";
|
|
2671
|
+
const hasCode = typeof body.otp_code === "string" && body.otp_code !== "";
|
|
2672
|
+
if (hasPassword === hasCode) throw new HttpError(400, "validation", "send either password or otp_code", "password");
|
|
2673
|
+
if ((state.reauthFailures.get(c.id) ?? 0) >= 5) throw new HttpError(429, "rate_limited", "Too many attempts, try again later");
|
|
2674
|
+
const field = hasPassword ? "password" : "otp_code";
|
|
2675
|
+
const ok = hasPassword ? c.email !== null && state.passwords.get(c.email) === body.password : [c.email && `email:${c.email}`, c.phone && `phone:${c.phone}`].some((id) => !!id && state.otps.get(id) === body.otp_code);
|
|
2676
|
+
if (!ok) {
|
|
2677
|
+
state.reauthFailures.set(c.id, (state.reauthFailures.get(c.id) ?? 0) + 1);
|
|
2678
|
+
throw new HttpError(400, "validation", "could not verify; check the credential and try again", field);
|
|
2679
|
+
}
|
|
2680
|
+
state.reauthFailures.delete(c.id);
|
|
2681
|
+
info.reauth_at = Date.now();
|
|
2682
|
+
return { body: { reauth_at: new Date(info.reauth_at).toISOString(), valid_for_seconds: REAUTH_WINDOW_MS / 1e3 }, cache: "private" };
|
|
2683
|
+
});
|
|
2684
|
+
route("POST", "/customer/data-export", "customer:write", ({ headers }) => {
|
|
2685
|
+
const { c, info } = sessionOf(headers);
|
|
2686
|
+
requireRecentReauth(info);
|
|
2687
|
+
if ((state.exportsServed.get(c.id) ?? 0) >= 3) throw new HttpError(429, "rate_limited", "Three exports a day; try again tomorrow");
|
|
2688
|
+
state.exportsServed.set(c.id, (state.exportsServed.get(c.id) ?? 0) + 1);
|
|
2689
|
+
const doc = {
|
|
2690
|
+
format: "storefront-customer-data/1",
|
|
2691
|
+
exported_at: now(),
|
|
2692
|
+
account: { id: c.id, email: c.email, phone: c.phone, first_name: c.first_name, last_name: c.last_name, locale: c.locale ?? null, created_at: c.created_at },
|
|
2693
|
+
preferences: c.preferences ?? { notifications: {} },
|
|
2694
|
+
addresses: state.addresses.get(c.id) ?? [],
|
|
2695
|
+
fiscal_profiles: state.fiscalProfiles.get(c.id) ?? [],
|
|
2696
|
+
orders: state.orders.filter((o) => o.customer_id === c.id).map((o) => ({ id: o.id, number: o.number, created_at: o.created_at, status: o.status, total: o.total }))
|
|
2697
|
+
};
|
|
2698
|
+
return {
|
|
2699
|
+
body: {
|
|
2700
|
+
filename: `account-data-${now().slice(0, 10)}.json`,
|
|
2701
|
+
content_type: "application/json",
|
|
2702
|
+
content_base64: btoa(unescape(encodeURIComponent(JSON.stringify(doc, null, 2))))
|
|
2703
|
+
},
|
|
2704
|
+
cache: "private"
|
|
2705
|
+
};
|
|
2706
|
+
});
|
|
2707
|
+
route("DELETE", "/customer", "customer:write", ({ headers, body }) => {
|
|
2708
|
+
const token = headers.get("x-customer-token");
|
|
2709
|
+
if (token && state.deletedTokens.has(token)) return { status: 204 };
|
|
2710
|
+
const { c, info } = sessionOf(headers);
|
|
2711
|
+
requireRecentReauth(info);
|
|
2712
|
+
if (typeof body.confirm !== "string" || !body.confirm.trim() || body.confirm.length > 100) {
|
|
2713
|
+
throw new HttpError(400, "validation", "confirm is required: the text the buyer typed", "confirm");
|
|
2714
|
+
}
|
|
2715
|
+
const closed = /* @__PURE__ */ new Set(["fulfilled", "delivered", "cancelled"]);
|
|
2716
|
+
if (state.orders.some((o) => o.customer_id === c.id && !closed.has(o.status))) {
|
|
2717
|
+
throw new HttpError(409, "conflict", "you have an order in progress; you can delete the account once it is delivered", "orders");
|
|
2718
|
+
}
|
|
2719
|
+
if (c.id === "cus_1" && state.pendingPayments.some((p) => p.status === "pending_payment")) {
|
|
2720
|
+
throw new HttpError(409, "conflict", "you have a payment waiting to be completed", "pending_payments");
|
|
2721
|
+
}
|
|
2722
|
+
for (const [t, id] of [...state.sessions.entries()]) {
|
|
2723
|
+
if (id === c.id) {
|
|
2724
|
+
state.sessions.delete(t);
|
|
2725
|
+
state.deletedTokens.add(t);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
if (c.email) state.passwords.delete(c.email);
|
|
2729
|
+
state.addresses.delete(c.id);
|
|
2730
|
+
state.fiscalProfiles.delete(c.id);
|
|
2731
|
+
state.customers = state.customers.filter((x) => x.id !== c.id);
|
|
2732
|
+
return { status: 204 };
|
|
2733
|
+
});
|
|
2523
2734
|
const orderView = ({ customer_id: _c, ...o }, detail) => {
|
|
2524
2735
|
const order = { ...o, invoice: orderInvoice(o) };
|
|
2525
2736
|
if (order.shipments) order.shipments = order.shipments.map((s) => isTestShipment(s) ? { ...s, tracking_number: null, tracking_url: null } : { ...s, events: sortTrackingEvents(s.events) });
|
|
@@ -2676,6 +2887,89 @@ function createMockStorefront(options = {}) {
|
|
|
2676
2887
|
mirrorDefaultProfile(c);
|
|
2677
2888
|
return { status: 204 };
|
|
2678
2889
|
});
|
|
2890
|
+
const MAX_SUBJECTS = 50;
|
|
2891
|
+
const subjectsOf = (c) => state.subjects.get(c.id) ?? [];
|
|
2892
|
+
const kinds = () => options.subjectKinds ?? [];
|
|
2893
|
+
const publicSubject = (s) => {
|
|
2894
|
+
const { private: priv, ...rest } = s;
|
|
2895
|
+
return { ...rest, attributes: { ...s.attributes, ...Object.fromEntries(Object.keys(priv).map((k) => [k, PRIVATE_MASK + (priv[k].length > 3 ? priv[k].slice(-2) : "")])) } };
|
|
2896
|
+
};
|
|
2897
|
+
const ownSubject = (c, id) => {
|
|
2898
|
+
const s = subjectsOf(c).find((x) => x.id === id);
|
|
2899
|
+
if (!s) throw new HttpError(404, "not_found", "Item not found", "id");
|
|
2900
|
+
return s;
|
|
2901
|
+
};
|
|
2902
|
+
const setDefaultSubject = (c, id) => {
|
|
2903
|
+
const kind = ownSubject(c, id).kind;
|
|
2904
|
+
state.subjects.set(c.id, subjectsOf(c).map((s) => s.kind !== kind ? s : s.id === id ? { ...s, is_default: true, last_used_at: now() } : { ...s, is_default: false }));
|
|
2905
|
+
};
|
|
2906
|
+
const resolveOrThrow = (kind, values) => {
|
|
2907
|
+
const r = resolveSubject(kind, values);
|
|
2908
|
+
if (!r.ok) {
|
|
2909
|
+
const [field, code] = Object.entries(r.problems)[0];
|
|
2910
|
+
throw new HttpError(422, "validation", `${field}: ${code}`, field === "_" ? "attributes" : `attributes.${field}`);
|
|
2911
|
+
}
|
|
2912
|
+
return r.subject;
|
|
2913
|
+
};
|
|
2914
|
+
const cleanNickname = (v) => {
|
|
2915
|
+
if (v === void 0 || v === null || v === "") return null;
|
|
2916
|
+
if (typeof v !== "string" || [...v.trim()].length > 40) throw new HttpError(422, "validation", "nickname is too long (max 40)", "nickname");
|
|
2917
|
+
return v.trim().split(/\s+/).join(" ");
|
|
2918
|
+
};
|
|
2919
|
+
route("GET", "/subject-kinds", "content:read", () => ({ body: { nodes: kinds() }, cache: "public" }));
|
|
2920
|
+
route("GET", "/customer/subjects", "customer:read", ({ headers, search }) => {
|
|
2921
|
+
const c = requireCustomer(headers);
|
|
2922
|
+
const kind = search.get("kind");
|
|
2923
|
+
const nodes = subjectsOf(c).filter((s) => !kind || s.kind === kind).sort((a, b) => a.kind.localeCompare(b.kind) || Number(b.is_default) - Number(a.is_default) || (b.last_used_at ?? "").localeCompare(a.last_used_at ?? ""));
|
|
2924
|
+
return { body: { nodes: nodes.map(publicSubject) }, cache: "private" };
|
|
2925
|
+
});
|
|
2926
|
+
route("POST", "/customer/subjects", "customer:write", ({ headers, body }) => {
|
|
2927
|
+
const c = requireCustomer(headers);
|
|
2928
|
+
const kind = kinds().find((k) => k.key === body.kind);
|
|
2929
|
+
if (!kind) throw new HttpError(422, "validation", "This store has no such kind", "kind");
|
|
2930
|
+
const attrs = body.attributes;
|
|
2931
|
+
if (!attrs || typeof attrs !== "object" || Array.isArray(attrs)) throw new HttpError(422, "validation", "attributes must be an object", "attributes");
|
|
2932
|
+
const r = resolveOrThrow(kind, attrs);
|
|
2933
|
+
const list = subjectsOf(c);
|
|
2934
|
+
if (list.length >= MAX_SUBJECTS || list.filter((s) => s.kind === kind.key).length >= kind.max_per_account) throw new HttpError(409, "conflict", "Limit reached; delete one first", "kind");
|
|
2935
|
+
if (r.fit_key && list.some((s) => s.kind === kind.key && s.fit_key === r.fit_key)) throw new HttpError(409, "conflict", "You already saved this item", "attributes");
|
|
2936
|
+
const subject = { id: nextId("subj"), kind: kind.key, label: r.label, nickname: cleanNickname(body.nickname), fit_key: r.fit_key, attributes: r.attributes, private: r.private, is_default: false, last_used_at: null, created_at: now() };
|
|
2937
|
+
state.subjects.set(c.id, [...list, subject]);
|
|
2938
|
+
if (!list.some((s) => s.kind === kind.key) || body.is_default === true) setDefaultSubject(c, subject.id);
|
|
2939
|
+
return { status: 201, body: publicSubject(ownSubject(c, subject.id)), cache: "private" };
|
|
2940
|
+
});
|
|
2941
|
+
route("PATCH", "/customer/subjects/:id", "customer:write", ({ headers, params, body }) => {
|
|
2942
|
+
const c = requireCustomer(headers);
|
|
2943
|
+
const cur = ownSubject(c, params[0]);
|
|
2944
|
+
if (body.is_default === false && cur.is_default) throw new HttpError(422, "validation", "Make another item the default instead", "is_default");
|
|
2945
|
+
let next = { ...cur };
|
|
2946
|
+
if ("nickname" in body) next.nickname = cleanNickname(body.nickname);
|
|
2947
|
+
if ("attributes" in body) {
|
|
2948
|
+
const kind = kinds().find((k) => k.key === cur.kind);
|
|
2949
|
+
if (!kind) throw new HttpError(422, "validation", "This store no longer offers this kind", "kind");
|
|
2950
|
+
const sent = body.attributes ?? {};
|
|
2951
|
+
const merged = { ...cur.attributes, ...cur.private };
|
|
2952
|
+
for (const [k, v] of Object.entries(sent)) {
|
|
2953
|
+
if (k in cur.private && isMasked(v)) continue;
|
|
2954
|
+
if (v === null) delete merged[k];
|
|
2955
|
+
else merged[k] = v;
|
|
2956
|
+
}
|
|
2957
|
+
const r = resolveOrThrow(kind, merged);
|
|
2958
|
+
if (r.fit_key && subjectsOf(c).some((s) => s.id !== cur.id && s.kind === cur.kind && s.fit_key === r.fit_key)) throw new HttpError(409, "conflict", "You already saved this item", "attributes");
|
|
2959
|
+
next = { ...next, label: r.label, fit_key: r.fit_key, attributes: r.attributes, private: r.private };
|
|
2960
|
+
}
|
|
2961
|
+
state.subjects.set(c.id, subjectsOf(c).map((s) => s.id === cur.id ? next : s));
|
|
2962
|
+
if (body.is_default === true && !cur.is_default) setDefaultSubject(c, cur.id);
|
|
2963
|
+
return { body: publicSubject(ownSubject(c, cur.id)), cache: "private" };
|
|
2964
|
+
});
|
|
2965
|
+
route("DELETE", "/customer/subjects/:id", "customer:write", ({ headers, params }) => {
|
|
2966
|
+
const c = requireCustomer(headers);
|
|
2967
|
+
const cur = ownSubject(c, params[0]);
|
|
2968
|
+
state.subjects.set(c.id, subjectsOf(c).filter((s) => s.id !== cur.id));
|
|
2969
|
+
const heir = subjectsOf(c).filter((s) => s.kind === cur.kind).sort((a, b) => (b.last_used_at ?? "").localeCompare(a.last_used_at ?? ""))[0];
|
|
2970
|
+
if (cur.is_default && heir) setDefaultSubject(c, heir.id);
|
|
2971
|
+
return { status: 204 };
|
|
2972
|
+
});
|
|
2679
2973
|
route("GET", "/customer/addresses", "customer:read", ({ headers }) => {
|
|
2680
2974
|
const c = requireCustomer(headers);
|
|
2681
2975
|
return { body: { nodes: state.addresses.get(c.id) ?? [] }, cache: "private" };
|
|
@@ -2772,7 +3066,7 @@ function createMockStorefront(options = {}) {
|
|
|
2772
3066
|
return errorResponse(new HttpError(403, "insufficient_scope", `Token lacks scope ${r.scope}`, void 0, void 0, [r.scope]));
|
|
2773
3067
|
}
|
|
2774
3068
|
let body = {};
|
|
2775
|
-
if (method !== "GET"
|
|
3069
|
+
if (method !== "GET") {
|
|
2776
3070
|
const text = await request.text();
|
|
2777
3071
|
if (text) {
|
|
2778
3072
|
try {
|
|
@@ -2825,6 +3119,6 @@ function createMockStorefront(options = {}) {
|
|
|
2825
3119
|
};
|
|
2826
3120
|
}
|
|
2827
3121
|
|
|
2828
|
-
export { MOCK_CSF_RULES, MOCK_CURRENCY, MOCK_CUSTOMER_EMAIL, MOCK_FISCAL_ID_MX, MOCK_IDENTITY_DOCUMENT_MX_CSF, MOCK_IMAGE_URL, MOCK_POSTAL_CENTROIDS, MOCK_REGION_MX, createMockStorefront };
|
|
3122
|
+
export { MOCK_CSF_RULES, MOCK_CURRENCY, MOCK_CUSTOMER_EMAIL, MOCK_FISCAL_ID_MX, MOCK_IDENTITY_DOCUMENT_MX_CSF, MOCK_IMAGE_URL, MOCK_POSTAL_CENTROIDS, MOCK_REGION_MX, MOCK_SUBJECT_KINDS, createMockStorefront };
|
|
2829
3123
|
//# sourceMappingURL=mock.js.map
|
|
2830
3124
|
//# sourceMappingURL=mock.js.map
|