@zappie/verify 0.1.3 → 0.2.2
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/CHANGELOG.md +45 -0
- package/README.md +18 -1
- package/dist/{category-page-UBHOB5IN.js → category-page-I3HDIAJA.js} +7 -3
- package/dist/category-page-I3HDIAJA.js.map +1 -0
- package/dist/chunk-47RIF6JW.js +2807 -0
- package/dist/chunk-47RIF6JW.js.map +1 -0
- package/dist/{debug-C3UDGUED.js → debug-LOXPIQRS.js} +1 -1
- package/dist/debug-LOXPIQRS.js.map +1 -0
- package/dist/index.d.ts +55 -5
- package/dist/index.js +1133 -2386
- package/dist/index.js.map +1 -1
- package/dist/presentation-TKOXOILG.js +68 -0
- package/dist/presentation-TKOXOILG.js.map +1 -0
- package/dist/quote-body-S5LELT7L.js +24 -0
- package/dist/quote-body-S5LELT7L.js.map +1 -0
- package/dist/{quote-client-6XJJHITL.js → quote-client-4RUY3TKQ.js} +84 -6
- package/dist/quote-client-4RUY3TKQ.js.map +1 -0
- package/dist/{search-YIHT6CXN.js → search-OF62VKI3.js} +7 -2
- package/dist/search-OF62VKI3.js.map +1 -0
- package/dist/{selected-menu-3GCAICBQ.js → selected-menu-AQHWDELR.js} +9 -4
- package/dist/selected-menu-AQHWDELR.js.map +1 -0
- package/dist/{store-KIO5RCZO.js → store-OPM3OFLC.js} +20 -9
- package/dist/store-OPM3OFLC.js.map +1 -0
- package/package.json +2 -2
- package/schema/verify-report.v1.json +19 -1
- package/dist/category-page-UBHOB5IN.js.map +0 -1
- package/dist/chunk-WUWUDCYO.js +0 -485
- package/dist/chunk-WUWUDCYO.js.map +0 -1
- package/dist/debug-C3UDGUED.js.map +0 -1
- package/dist/quote-client-6XJJHITL.js.map +0 -1
- package/dist/search-YIHT6CXN.js.map +0 -1
- package/dist/selected-menu-3GCAICBQ.js.map +0 -1
- package/dist/store-KIO5RCZO.js.map +0 -1
|
@@ -0,0 +1,2807 @@
|
|
|
1
|
+
// ../sdk/src/errors.ts
|
|
2
|
+
var codes = [
|
|
3
|
+
"network_error",
|
|
4
|
+
"timeout",
|
|
5
|
+
"rate_limited",
|
|
6
|
+
"unauthorized",
|
|
7
|
+
"forbidden_origin",
|
|
8
|
+
"not_found",
|
|
9
|
+
"store_not_found",
|
|
10
|
+
"branch_not_found",
|
|
11
|
+
"item_not_found",
|
|
12
|
+
"cart_invalid",
|
|
13
|
+
"invalid_request",
|
|
14
|
+
"version_unsupported",
|
|
15
|
+
"server_error"
|
|
16
|
+
];
|
|
17
|
+
var ZappieError = class extends Error {
|
|
18
|
+
constructor(code, message, details = {}) {
|
|
19
|
+
super(message, { cause: details.cause });
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.name = "ZappieError";
|
|
22
|
+
if (details.status !== void 0) {
|
|
23
|
+
this.status = details.status;
|
|
24
|
+
}
|
|
25
|
+
if (details.param !== void 0) {
|
|
26
|
+
this.param = details.param;
|
|
27
|
+
}
|
|
28
|
+
if (details.requestId !== void 0) {
|
|
29
|
+
this.requestId = details.requestId;
|
|
30
|
+
}
|
|
31
|
+
this.docUrl = details.docUrl ?? `https://docs.zappie.ai/errors#${code}`;
|
|
32
|
+
this.retryable = details.retryable ?? ["network_error", "timeout", "rate_limited", "server_error"].includes(
|
|
33
|
+
code
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function isZappieError(error) {
|
|
38
|
+
return error instanceof ZappieError;
|
|
39
|
+
}
|
|
40
|
+
function asError(error) {
|
|
41
|
+
return isZappieError(error) ? error : new ZappieError("network_error", "The storefront request failed", {
|
|
42
|
+
cause: error
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function wireError(value, status, requestId) {
|
|
46
|
+
const envelope = value && typeof value === "object" ? value : {};
|
|
47
|
+
const e = envelope.error && typeof envelope.error === "object" ? envelope.error : {};
|
|
48
|
+
const fallback = status === 401 ? "unauthorized" : status === 403 ? "forbidden_origin" : status === 404 ? "not_found" : status === 429 ? "rate_limited" : status >= 500 ? "server_error" : "invalid_request";
|
|
49
|
+
const known = typeof e.code === "string" && codes.includes(e.code);
|
|
50
|
+
const code = known ? e.code : e.code ? "server_error" : fallback;
|
|
51
|
+
const message = typeof e.message === "string" ? e.message : "The storefront request failed";
|
|
52
|
+
return new ZappieError(
|
|
53
|
+
code,
|
|
54
|
+
!known && e.code ? `${String(e.code)}: ${message}` : message,
|
|
55
|
+
{
|
|
56
|
+
status,
|
|
57
|
+
...requestId ? { requestId } : {},
|
|
58
|
+
...typeof e.param === "string" ? { param: e.param } : {},
|
|
59
|
+
...typeof e.doc_url === "string" ? { docUrl: e.doc_url } : {}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ../sdk/src/money.ts
|
|
65
|
+
var ZERO_DIGIT_CURRENCIES = " AFN ALL BIF CLP DJF GNF IQD IRR ISK JPY KMF KPW KRW LAK LBP MGA MMK PYG RSD RWF SLL SOS SYP UGX UYI VND VUV XAF XOF XPF YER ";
|
|
66
|
+
var THREE_DIGIT_CURRENCIES = " BHD JOD KWD LYD OMR TND ";
|
|
67
|
+
var FOUR_DIGIT_CURRENCIES = " CLF UYW ";
|
|
68
|
+
var API_ONLY_CURRENCIES = " BOV CHE CHW CLF COU MXV USN UYI UYW VED ";
|
|
69
|
+
var contains = (codes2, currency) => codes2.includes(` ${currency} `);
|
|
70
|
+
var intlCurrencies;
|
|
71
|
+
function isCurrency(code) {
|
|
72
|
+
if (!/^[A-Z]{3}$/.test(code)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (typeof Intl.supportedValuesOf !== "function") {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
intlCurrencies ??= new Set(Intl.supportedValuesOf("currency"));
|
|
79
|
+
return contains(API_ONLY_CURRENCIES, code) || intlCurrencies.has(code);
|
|
80
|
+
}
|
|
81
|
+
function currencyDigits(currency) {
|
|
82
|
+
if (contains(ZERO_DIGIT_CURRENCIES, currency)) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
if (contains(THREE_DIGIT_CURRENCIES, currency)) {
|
|
86
|
+
return 3;
|
|
87
|
+
}
|
|
88
|
+
return contains(FOUR_DIGIT_CURRENCIES, currency) ? 4 : 2;
|
|
89
|
+
}
|
|
90
|
+
function money(amountMinor, currency) {
|
|
91
|
+
const normalized = typeof currency === "string" ? currency.trim().toUpperCase() : "";
|
|
92
|
+
if (!Number.isSafeInteger(amountMinor) || !isCurrency(normalized)) {
|
|
93
|
+
throw new ZappieError(
|
|
94
|
+
"invalid_request",
|
|
95
|
+
"Money requires safe integer minor units and a valid ISO 4217 currency"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return Object.freeze({ amountMinor, currency: normalized });
|
|
99
|
+
}
|
|
100
|
+
var format = {
|
|
101
|
+
money(value, opts = {}) {
|
|
102
|
+
const normalized = money(value.amountMinor, value.currency);
|
|
103
|
+
const locale = normalized.currency === "NGN" ? "en-NG" : "en";
|
|
104
|
+
const digits = currencyDigits(normalized.currency);
|
|
105
|
+
return new Intl.NumberFormat(locale, {
|
|
106
|
+
style: "currency",
|
|
107
|
+
currency: normalized.currency,
|
|
108
|
+
currencyDisplay: "narrowSymbol",
|
|
109
|
+
minimumFractionDigits: opts.showZeroDecimals || normalized.amountMinor % 10 ** digits !== 0 ? digits : 0,
|
|
110
|
+
maximumFractionDigits: digits,
|
|
111
|
+
notation: opts.compact ? "compact" : "standard",
|
|
112
|
+
signDisplay: opts.sign ?? "auto"
|
|
113
|
+
}).format(normalized.amountMinor / 10 ** digits);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ../sdk/src/internal/money-math.ts
|
|
118
|
+
function sumMoney(values, zero) {
|
|
119
|
+
let amount = zero.amountMinor;
|
|
120
|
+
for (const value of values) {
|
|
121
|
+
if (value.currency !== zero.currency) {
|
|
122
|
+
throw new ZappieError(
|
|
123
|
+
"invalid_request",
|
|
124
|
+
"Cannot combine different currencies"
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
amount = money(amount + value.amountMinor, zero.currency).amountMinor;
|
|
128
|
+
}
|
|
129
|
+
return money(amount, zero.currency);
|
|
130
|
+
}
|
|
131
|
+
function multiplyMoney(value, quantity) {
|
|
132
|
+
if (!Number.isSafeInteger(quantity)) {
|
|
133
|
+
throw new ZappieError(
|
|
134
|
+
"invalid_request",
|
|
135
|
+
"Money requires an integer quantity"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return money(value.amountMinor * quantity, value.currency);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ../sdk/src/cart/errors.ts
|
|
142
|
+
var ZappieCartError = class extends ZappieError {
|
|
143
|
+
constructor(issues) {
|
|
144
|
+
super("cart_invalid", issues[0]?.message ?? "The cart needs attention");
|
|
145
|
+
this.issues = issues;
|
|
146
|
+
this.name = "ZappieCartError";
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// ../sdk/src/cart/price.ts
|
|
151
|
+
function estimateUnitPrice(item, line) {
|
|
152
|
+
const variant = line.variantId ? item.variants.find((v) => v.id === line.variantId) : void 0;
|
|
153
|
+
if (line.variantId && !variant) {
|
|
154
|
+
throw new ZappieError("invalid_request", "The selected variant is missing");
|
|
155
|
+
}
|
|
156
|
+
const extras = line.choices.flatMap((selected) => {
|
|
157
|
+
const group = item.optionGroups.find((g) => g.id === selected.groupId);
|
|
158
|
+
const choice = group?.choices.find((c) => c.id === selected.choiceId);
|
|
159
|
+
if (!group || !choice) {
|
|
160
|
+
throw new ZappieError(
|
|
161
|
+
"invalid_request",
|
|
162
|
+
"The selected option is missing"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return group.isAttribute ? [] : [multiplyMoney(choice.price, Math.max(selected.quantity, 1))];
|
|
166
|
+
});
|
|
167
|
+
return sumMoney(extras, variant?.price ?? item.price);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ../sdk/src/cart/source-menu.ts
|
|
171
|
+
function resolveSourceMenu(menu, itemId, menuId) {
|
|
172
|
+
const categories = menu.categories.filter((c) => c.itemIds.includes(itemId));
|
|
173
|
+
const sources = menu.menus ?? [
|
|
174
|
+
{ id: menu.id, categoryIds: menu.categories.map((c) => c.id) }
|
|
175
|
+
];
|
|
176
|
+
const placements = menu.items[itemId]?.placements;
|
|
177
|
+
const candidates = sources.filter(
|
|
178
|
+
(source) => (!placements || placements.some(
|
|
179
|
+
(p) => p.menuId === source.id && source.categoryIds.includes(p.categoryId)
|
|
180
|
+
)) && source.categoryIds.some((id) => categories.some((c) => c.id === id))
|
|
181
|
+
);
|
|
182
|
+
if (menuId !== void 0) {
|
|
183
|
+
return typeof menuId === "string" && menuId.trim().length > 0 && menuId.length <= 256 && candidates.some((source) => source.id === menuId) ? menuId : void 0;
|
|
184
|
+
}
|
|
185
|
+
return candidates.length === 1 ? candidates[0].id : void 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ../sdk/src/cart/validate.ts
|
|
189
|
+
function issue(code, message, fixes = [], groupId) {
|
|
190
|
+
return {
|
|
191
|
+
code,
|
|
192
|
+
severity: "blocking",
|
|
193
|
+
message,
|
|
194
|
+
fixes,
|
|
195
|
+
source: "local",
|
|
196
|
+
...groupId ? { groupId } : {}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function effectiveGroups(item, input) {
|
|
200
|
+
const selected = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const choice of input.choices ?? []) {
|
|
202
|
+
const ids = selected.get(choice.groupId) ?? /* @__PURE__ */ new Set();
|
|
203
|
+
ids.add(choice.choiceId);
|
|
204
|
+
selected.set(choice.groupId, ids);
|
|
205
|
+
}
|
|
206
|
+
return item.optionGroups.map((group) => {
|
|
207
|
+
let required = group.required;
|
|
208
|
+
let { min, max } = group.selection;
|
|
209
|
+
for (const rule of group.conditions ?? []) {
|
|
210
|
+
const chosen = selected.get(rule.groupId) ?? /* @__PURE__ */ new Set();
|
|
211
|
+
const matches = rule.count ? rule.count.op === "eq" ? chosen.size === rule.count.value : rule.count.op === "gte" ? chosen.size >= rule.count.value : chosen.size <= rule.count.value : chosen.size > 0 && rule.choiceIds.length > 0 && (rule.match === "all" ? rule.choiceIds.every((id) => chosen.has(id)) : rule.choiceIds.some((id) => chosen.has(id)));
|
|
212
|
+
if (!matches) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
required = rule.setRequired ?? required;
|
|
216
|
+
min = rule.setMin ?? min;
|
|
217
|
+
if (rule.setMax != null) {
|
|
218
|
+
max = rule.setMax > 0 ? rule.setMax : void 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
...group,
|
|
223
|
+
required,
|
|
224
|
+
selection: { min, ...max !== void 0 ? { max } : {} }
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function normalizeInput(item, input) {
|
|
229
|
+
const inactive = new Set(
|
|
230
|
+
item.optionGroups.filter((g) => g.isAvailable === false).map((g) => g.id)
|
|
231
|
+
);
|
|
232
|
+
return {
|
|
233
|
+
...input,
|
|
234
|
+
quantity: input.quantity ?? item.quantity.min,
|
|
235
|
+
choices: (input.choices ?? []).filter((c) => !inactive.has(c.groupId)).map((c) => ({ ...c, quantity: c.quantity ?? 1 })),
|
|
236
|
+
inputs: Object.fromEntries(
|
|
237
|
+
Object.entries(input.inputs ?? {}).filter(([id]) => !inactive.has(id))
|
|
238
|
+
)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function resolveVariant(item, input) {
|
|
242
|
+
const attrs = item.optionGroups.filter((g) => g.isAttribute);
|
|
243
|
+
const matches = item.variants.filter(
|
|
244
|
+
(variant) => (!input.variantId || variant.id === input.variantId) && attrs.every((group) => {
|
|
245
|
+
const selected = input.choices?.filter((c) => c.groupId === group.id) ?? [];
|
|
246
|
+
return selected.length === 1 && variant.attributeChoices[group.id] === selected[0].choiceId;
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
250
|
+
}
|
|
251
|
+
function validInput(kind, value) {
|
|
252
|
+
switch (kind) {
|
|
253
|
+
case "number":
|
|
254
|
+
return Number.isFinite(Number(value));
|
|
255
|
+
case "date":
|
|
256
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
|
|
257
|
+
case "time":
|
|
258
|
+
return /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/.test(value);
|
|
259
|
+
case "toggle":
|
|
260
|
+
return ["true", "false"].includes(value.toLowerCase());
|
|
261
|
+
default:
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function validateLine(item, raw, ctx) {
|
|
266
|
+
if (!item || item.id !== raw.itemId || !Object.hasOwn(ctx.menu.items, raw.itemId)) {
|
|
267
|
+
return [
|
|
268
|
+
issue("item_unavailable", "This item is no longer on the menu", [
|
|
269
|
+
{ action: "remove_line" }
|
|
270
|
+
])
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
const issues = [];
|
|
274
|
+
if (!resolveSourceMenu(ctx.menu, raw.itemId, raw.menuId)) {
|
|
275
|
+
issues.push(
|
|
276
|
+
issue(
|
|
277
|
+
"item_unavailable",
|
|
278
|
+
raw.menuId === void 0 ? `${item.name} is on more than one menu. Remove it and add it again from the menu you want.` : `${item.name} is no longer on the menu it was added from.`,
|
|
279
|
+
[{ action: "remove_line" }]
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (!item.isAvailable) {
|
|
284
|
+
issues.push(
|
|
285
|
+
issue("item_unavailable", `${item.name} is unavailable`, [
|
|
286
|
+
{ action: "remove_line" }
|
|
287
|
+
])
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const input = normalizeInput(item, raw);
|
|
291
|
+
const quantity = input.quantity;
|
|
292
|
+
const { min, step } = item.quantity;
|
|
293
|
+
const max = Math.min(item.quantity.max ?? 99, 99);
|
|
294
|
+
if (!Number.isInteger(quantity) || quantity < min || quantity > max || quantity % step !== 0) {
|
|
295
|
+
const lower = Math.ceil(min / step) * step;
|
|
296
|
+
const upper = Math.floor(max / step) * step;
|
|
297
|
+
const nearest = Math.max(
|
|
298
|
+
lower,
|
|
299
|
+
Math.min(
|
|
300
|
+
upper,
|
|
301
|
+
Math.round((Number.isFinite(quantity) ? quantity : min) / step) * step
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
issues.push(
|
|
305
|
+
issue(
|
|
306
|
+
"quantity_limit",
|
|
307
|
+
`Choose a valid quantity of ${item.name} (${min}\u2013${max}, step ${step})`,
|
|
308
|
+
lower <= upper ? [{ action: "set_quantity", quantity: nearest }] : []
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if ((input.note?.length ?? 0) > 280) {
|
|
313
|
+
issues.push(issue("option_limit", "Notes must be at most 280 characters"));
|
|
314
|
+
}
|
|
315
|
+
for (const choice of input.choices ?? []) {
|
|
316
|
+
if (!item.optionGroups.some((g) => g.id === choice.groupId)) {
|
|
317
|
+
issues.push(
|
|
318
|
+
issue(
|
|
319
|
+
"option_unavailable",
|
|
320
|
+
"This option group no longer exists",
|
|
321
|
+
[{ action: "remove_line" }],
|
|
322
|
+
choice.groupId
|
|
323
|
+
)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const id of Object.keys(input.inputs ?? {})) {
|
|
328
|
+
if (!item.optionGroups.some((g) => g.id === id)) {
|
|
329
|
+
issues.push(
|
|
330
|
+
issue(
|
|
331
|
+
"option_unavailable",
|
|
332
|
+
"This input group no longer exists",
|
|
333
|
+
[{ action: "remove_line" }],
|
|
334
|
+
id
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const groups = effectiveGroups(item, input);
|
|
340
|
+
const missing = groups.filter(
|
|
341
|
+
(g) => g.isAvailable !== false && g.required && !(input.choices ?? []).some((c) => c.groupId === g.id) && !(input.inputs?.[g.id] ?? "").trim()
|
|
342
|
+
);
|
|
343
|
+
if (missing.length) {
|
|
344
|
+
return [
|
|
345
|
+
...issues,
|
|
346
|
+
...missing.map(
|
|
347
|
+
(g) => issue(
|
|
348
|
+
"option_required",
|
|
349
|
+
`Choose ${g.name}`,
|
|
350
|
+
[{ action: "choose_option", groupId: g.id }],
|
|
351
|
+
g.id
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
for (const group of groups) {
|
|
357
|
+
if (group.isAvailable === false) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const choices = input.choices?.filter((c) => c.groupId === group.id) ?? [];
|
|
361
|
+
const text2 = input.inputs?.[group.id] ?? "";
|
|
362
|
+
const value = text2.trim();
|
|
363
|
+
const hybrid = group.isInformational && group.kind === "select";
|
|
364
|
+
const isInput = group.kind !== "select" || hybrid && !group.choices.length;
|
|
365
|
+
const fix = [{ action: "choose_option", groupId: group.id }];
|
|
366
|
+
if (group.required && !choices.length && !value) {
|
|
367
|
+
issues.push(
|
|
368
|
+
issue("option_required", `Choose ${group.name}`, fix, group.id)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (value && (!isInput && !hybrid || choices.length > 0)) {
|
|
372
|
+
issues.push(
|
|
373
|
+
issue(
|
|
374
|
+
"option_limit",
|
|
375
|
+
`Choose either an option or a response for ${group.name}`,
|
|
376
|
+
fix,
|
|
377
|
+
group.id
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if ((isInput || hybrid) && (text2.length > (group.input?.maxLength ?? 500) || value && !validInput(group.kind, value))) {
|
|
382
|
+
issues.push(
|
|
383
|
+
issue(
|
|
384
|
+
"option_limit",
|
|
385
|
+
`Enter a valid response for ${group.name}`,
|
|
386
|
+
fix,
|
|
387
|
+
group.id
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const count = choices.reduce((sum, c) => sum + (c.quantity ?? 1), 0);
|
|
392
|
+
if (choices.length && (isInput || count < group.selection.min || count > (group.selection.max ?? Infinity) || new Set(choices.map((c) => c.choiceId)).size !== choices.length)) {
|
|
393
|
+
issues.push(
|
|
394
|
+
issue(
|
|
395
|
+
"option_limit",
|
|
396
|
+
`Check the number of options for ${group.name}`,
|
|
397
|
+
fix,
|
|
398
|
+
group.id
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
for (const selected of choices) {
|
|
403
|
+
const choice = group.choices.find((c) => c.id === selected.choiceId);
|
|
404
|
+
if (!choice?.isAvailable) {
|
|
405
|
+
issues.push(
|
|
406
|
+
issue(
|
|
407
|
+
"option_unavailable",
|
|
408
|
+
`An option in ${group.name} is unavailable`,
|
|
409
|
+
[
|
|
410
|
+
{ action: "choose_other_option", groupId: group.id },
|
|
411
|
+
{ action: "remove_line" }
|
|
412
|
+
],
|
|
413
|
+
group.id
|
|
414
|
+
)
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const n = selected.quantity ?? 1;
|
|
418
|
+
if (!Number.isInteger(n) || n < (choice?.quantity?.min ?? 1) || n > (choice?.quantity?.max ?? 99)) {
|
|
419
|
+
issues.push(
|
|
420
|
+
issue(
|
|
421
|
+
"option_limit",
|
|
422
|
+
`Check the option quantity for ${group.name}`,
|
|
423
|
+
fix,
|
|
424
|
+
group.id
|
|
425
|
+
)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const variant = resolveVariant(item, input);
|
|
431
|
+
if (!variant?.isAvailable) {
|
|
432
|
+
const groupId = item.optionGroups.find((g) => g.isAttribute)?.id;
|
|
433
|
+
issues.push(
|
|
434
|
+
issue(
|
|
435
|
+
"option_limit",
|
|
436
|
+
"Choose an available variant",
|
|
437
|
+
groupId ? [{ action: "choose_option", groupId }] : [{ action: "remove_line" }],
|
|
438
|
+
groupId
|
|
439
|
+
)
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
return issues;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ../sdk/src/cart/reconcile.ts
|
|
446
|
+
function lineInput(line) {
|
|
447
|
+
return {
|
|
448
|
+
itemId: line.itemId,
|
|
449
|
+
...line.menuId !== void 0 ? { menuId: line.menuId } : {},
|
|
450
|
+
quantity: line.quantity,
|
|
451
|
+
choices: line.choices.map((c) => ({ ...c })),
|
|
452
|
+
inputs: { ...line.inputs },
|
|
453
|
+
...line.variantId ? { variantId: line.variantId } : {},
|
|
454
|
+
...line.note !== void 0 ? { note: line.note } : {}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function freezeLine(line) {
|
|
458
|
+
return Object.freeze({
|
|
459
|
+
...line,
|
|
460
|
+
choices: Object.freeze(line.choices.map((c) => Object.freeze({ ...c }))),
|
|
461
|
+
inputs: Object.freeze({ ...line.inputs })
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function reconcile(lines, menu, branch, currency, previous = []) {
|
|
465
|
+
const issues = [];
|
|
466
|
+
let priced = true;
|
|
467
|
+
const result = lines.map((old) => {
|
|
468
|
+
const item = Object.hasOwn(menu.items, old.itemId) ? menu.items[old.itemId] : void 0;
|
|
469
|
+
const raw = {
|
|
470
|
+
...old,
|
|
471
|
+
choices: old.choices?.map((c) => ({ ...c })),
|
|
472
|
+
inputs: { ...old.inputs }
|
|
473
|
+
};
|
|
474
|
+
const input = item ? normalizeInput(item, raw) : raw;
|
|
475
|
+
issues.push(
|
|
476
|
+
...validateLine(item, input, { menu, branch }).map((i) => ({
|
|
477
|
+
...i,
|
|
478
|
+
lineId: old.id
|
|
479
|
+
}))
|
|
480
|
+
);
|
|
481
|
+
const variantId = input.variantId ?? (item ? resolveVariant(item, input)?.id : void 0);
|
|
482
|
+
let unitPrice = money(0, currency);
|
|
483
|
+
let linePriced = true;
|
|
484
|
+
try {
|
|
485
|
+
if (!item) {
|
|
486
|
+
throw new Error("Missing item");
|
|
487
|
+
}
|
|
488
|
+
unitPrice = estimateUnitPrice(item, {
|
|
489
|
+
choices: (input.choices ?? []).map((c) => ({
|
|
490
|
+
...c,
|
|
491
|
+
quantity: c.quantity ?? 1
|
|
492
|
+
})),
|
|
493
|
+
...variantId ? { variantId } : {}
|
|
494
|
+
});
|
|
495
|
+
} catch {
|
|
496
|
+
priced = false;
|
|
497
|
+
linePriced = false;
|
|
498
|
+
}
|
|
499
|
+
if (old.unitPrice && old.unitPrice.amountMinor !== unitPrice.amountMinor && item && linePriced) {
|
|
500
|
+
issues.push({
|
|
501
|
+
...issue(
|
|
502
|
+
"price_changed",
|
|
503
|
+
`${item.name} is now ${format.money(unitPrice)}`,
|
|
504
|
+
[{ action: "accept_price" }, { action: "remove_line" }]
|
|
505
|
+
),
|
|
506
|
+
severity: "warning",
|
|
507
|
+
lineId: old.id,
|
|
508
|
+
data: { previous: old.unitPrice, current: unitPrice }
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return freezeLine({
|
|
512
|
+
id: old.id,
|
|
513
|
+
itemId: old.itemId,
|
|
514
|
+
...input.menuId !== void 0 ? { menuId: input.menuId } : {},
|
|
515
|
+
name: item?.name ?? old.name,
|
|
516
|
+
quantity: input.quantity ?? 1,
|
|
517
|
+
choices: (input.choices ?? []).map((c) => ({
|
|
518
|
+
...c,
|
|
519
|
+
quantity: c.quantity ?? 1
|
|
520
|
+
})),
|
|
521
|
+
inputs: input.inputs ?? {},
|
|
522
|
+
...variantId ? { variantId } : {},
|
|
523
|
+
...input.note !== void 0 ? { note: input.note } : {},
|
|
524
|
+
unitPrice,
|
|
525
|
+
lineSubtotal: multiplyMoney(unitPrice, input.quantity ?? 1),
|
|
526
|
+
addedAt: old.addedAt
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
for (const pending of previous) {
|
|
530
|
+
if (pending.code === "price_changed" && result.some((l) => l.id === pending.lineId) && !issues.some(
|
|
531
|
+
(i) => i.code === pending.code && i.lineId === pending.lineId
|
|
532
|
+
)) {
|
|
533
|
+
issues.push(pending);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return { lines: Object.freeze(result), issues, priced };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ../sdk/src/cart/storage.ts
|
|
540
|
+
function storedMoney(value) {
|
|
541
|
+
try {
|
|
542
|
+
return value && money(value.amountMinor, value.currency);
|
|
543
|
+
} catch {
|
|
544
|
+
return void 0;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function parseCart(raw) {
|
|
548
|
+
const v = JSON.parse(raw);
|
|
549
|
+
if (!v || v.v !== 1 || !Array.isArray(v.lines) || v.lines.length > 100 || !Number.isFinite(v.updatedAt) || typeof v.menuVersion !== "string" || v.promoCode !== null && typeof v.promoCode !== "string") {
|
|
550
|
+
throw new Error("Invalid saved cart");
|
|
551
|
+
}
|
|
552
|
+
const ids = /* @__PURE__ */ new Set();
|
|
553
|
+
for (const line of v.lines) {
|
|
554
|
+
if (!line || typeof line.id !== "string" || ids.has(line.id) || typeof line.itemId !== "string" || typeof line.name !== "string" || !Number.isFinite(Date.parse(line.addedAt)) || !Number.isInteger(line.quantity) || line.quantity < 1 || line.quantity > 99 || line.note !== void 0 && (typeof line.note !== "string" || line.note.length > 280) || line.menuId !== void 0 && (typeof line.menuId !== "string" || !line.menuId.trim() || line.menuId.length > 256) || line.variantId !== void 0 && typeof line.variantId !== "string") {
|
|
555
|
+
throw new Error("Invalid saved cart line");
|
|
556
|
+
}
|
|
557
|
+
ids.add(line.id);
|
|
558
|
+
if (!Array.isArray(line.choices) || line.choices.length > 1e3 || line.choices.some(
|
|
559
|
+
(c) => !c || typeof c.groupId !== "string" || typeof c.choiceId !== "string" || !Number.isInteger(c.quantity) || c.quantity < 1 || c.quantity > 99
|
|
560
|
+
)) {
|
|
561
|
+
throw new Error("Invalid saved choices");
|
|
562
|
+
}
|
|
563
|
+
if (!line.inputs || typeof line.inputs !== "object" || Array.isArray(line.inputs) || Object.values(line.inputs).some((t) => typeof t !== "string")) {
|
|
564
|
+
throw new Error("Invalid saved inputs");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (v.fulfilment !== null && !validFulfilment(v.fulfilment)) {
|
|
568
|
+
throw new Error("Invalid saved fulfilment");
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
v: 1,
|
|
572
|
+
lines: v.lines.map((l) => {
|
|
573
|
+
const unitPrice = storedMoney(l.unitPrice);
|
|
574
|
+
return {
|
|
575
|
+
id: l.id,
|
|
576
|
+
itemId: l.itemId,
|
|
577
|
+
...l.menuId !== void 0 ? { menuId: l.menuId } : {},
|
|
578
|
+
name: l.name,
|
|
579
|
+
addedAt: l.addedAt,
|
|
580
|
+
...unitPrice ? { unitPrice } : {},
|
|
581
|
+
quantity: l.quantity,
|
|
582
|
+
choices: l.choices,
|
|
583
|
+
inputs: l.inputs,
|
|
584
|
+
...l.variantId ? { variantId: l.variantId } : {},
|
|
585
|
+
...l.note !== void 0 ? { note: l.note } : {}
|
|
586
|
+
};
|
|
587
|
+
}),
|
|
588
|
+
fulfilment: v.fulfilment,
|
|
589
|
+
promoCode: v.promoCode,
|
|
590
|
+
menuVersion: v.menuVersion,
|
|
591
|
+
updatedAt: v.updatedAt
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function validFulfilment(f) {
|
|
595
|
+
if (!f || !["pickup", "delivery", "dine_in"].includes(f.type) || typeof f.branchId !== "string") {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
if ("scheduledFor" in f && f.scheduledFor !== void 0 && !Number.isFinite(Date.parse(f.scheduledFor))) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
if (f.type === "delivery") {
|
|
602
|
+
const a = f.address;
|
|
603
|
+
if (!a || typeof a.line1 !== "string" || !a.line1.trim()) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
if ([a.line2, a.city, a.placeId, a.instructions, f.deliveryRegionId].some(
|
|
607
|
+
(s) => s !== void 0 && typeof s !== "string"
|
|
608
|
+
)) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
if (a.coordinates && (!Number.isFinite(a.coordinates.lat) || !Number.isFinite(a.coordinates.lng) || Math.abs(a.coordinates.lat) > 90 || Math.abs(a.coordinates.lng) > 180)) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return f.type !== "dine_in" || f.table === void 0 || typeof f.table === "string";
|
|
616
|
+
}
|
|
617
|
+
function createCartStorage(provided, warn) {
|
|
618
|
+
let storage;
|
|
619
|
+
let initialized = false;
|
|
620
|
+
let failed = false;
|
|
621
|
+
let warned = false;
|
|
622
|
+
const memory = /* @__PURE__ */ new Map();
|
|
623
|
+
const warning = (message) => {
|
|
624
|
+
if (!warned) {
|
|
625
|
+
warned = true;
|
|
626
|
+
warn(message);
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
const access = (fn, fallback) => {
|
|
630
|
+
try {
|
|
631
|
+
if (!initialized) {
|
|
632
|
+
initialized = true;
|
|
633
|
+
storage = provided ?? (typeof window === "undefined" ? void 0 : window.localStorage);
|
|
634
|
+
}
|
|
635
|
+
if (storage && !failed) {
|
|
636
|
+
return fn(storage);
|
|
637
|
+
}
|
|
638
|
+
} catch {
|
|
639
|
+
failed = true;
|
|
640
|
+
warning("Cart storage unavailable; using memory for this client");
|
|
641
|
+
}
|
|
642
|
+
return fallback();
|
|
643
|
+
};
|
|
644
|
+
return {
|
|
645
|
+
warning,
|
|
646
|
+
read(key) {
|
|
647
|
+
return access(
|
|
648
|
+
(s) => {
|
|
649
|
+
const value = s.getItem(key);
|
|
650
|
+
if (value !== null) {
|
|
651
|
+
memory.set(key, value);
|
|
652
|
+
} else {
|
|
653
|
+
memory.delete(key);
|
|
654
|
+
}
|
|
655
|
+
return value;
|
|
656
|
+
},
|
|
657
|
+
() => memory.get(key) ?? null
|
|
658
|
+
);
|
|
659
|
+
},
|
|
660
|
+
write(key, value) {
|
|
661
|
+
memory.set(key, value);
|
|
662
|
+
access(
|
|
663
|
+
(s) => s.setItem(key, value),
|
|
664
|
+
() => {
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
},
|
|
668
|
+
remove(key) {
|
|
669
|
+
memory.delete(key);
|
|
670
|
+
access(
|
|
671
|
+
(s) => s.removeItem(key),
|
|
672
|
+
() => {
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
},
|
|
676
|
+
accepts(area) {
|
|
677
|
+
return !failed && (!area || area === storage);
|
|
678
|
+
},
|
|
679
|
+
sweep(prefix, current, ttl) {
|
|
680
|
+
access(
|
|
681
|
+
(s) => {
|
|
682
|
+
const keys = Array.from(
|
|
683
|
+
{ length: s.length ?? 0 },
|
|
684
|
+
(_, index) => s.key?.(index)
|
|
685
|
+
);
|
|
686
|
+
for (const key of keys) {
|
|
687
|
+
if (!key || key === current || !key.startsWith(prefix)) {
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const raw = s.getItem(key);
|
|
692
|
+
if (raw && Date.now() - parseCart(raw).updatedAt > ttl) {
|
|
693
|
+
s.removeItem(key);
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
s.removeItem(key);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
() => {
|
|
701
|
+
}
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ../sdk/src/cart/wire.ts
|
|
708
|
+
function wireLine(line) {
|
|
709
|
+
return {
|
|
710
|
+
line_id: line.id,
|
|
711
|
+
item_id: line.itemId,
|
|
712
|
+
...line.menuId !== void 0 ? { menu_id: line.menuId } : {},
|
|
713
|
+
quantity: line.quantity,
|
|
714
|
+
...line.variantId ? { variant_id: line.variantId } : {},
|
|
715
|
+
options: line.choices.map((c) => ({
|
|
716
|
+
group_id: c.groupId,
|
|
717
|
+
choice_id: c.choiceId,
|
|
718
|
+
quantity: c.quantity
|
|
719
|
+
})),
|
|
720
|
+
inputs: { ...line.inputs },
|
|
721
|
+
...line.note !== void 0 ? { note: line.note } : {}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function wireFulfilment(f) {
|
|
725
|
+
return {
|
|
726
|
+
type: f.type,
|
|
727
|
+
..."scheduledFor" in f && f.scheduledFor ? { scheduled_for: f.scheduledFor } : {},
|
|
728
|
+
...f.type === "dine_in" && f.table ? { table: f.table } : {},
|
|
729
|
+
...f.type === "delivery" ? {
|
|
730
|
+
...f.deliveryRegionId !== void 0 ? { delivery_region_id: f.deliveryRegionId } : {},
|
|
731
|
+
address: {
|
|
732
|
+
line1: f.address.line1,
|
|
733
|
+
...f.address.line2 ? { line2: f.address.line2 } : {},
|
|
734
|
+
...f.address.city ? { city: f.address.city } : {},
|
|
735
|
+
...f.address.placeId ? { place_id: f.address.placeId } : {},
|
|
736
|
+
...f.address.instructions ? { instructions: f.address.instructions } : {},
|
|
737
|
+
...f.address.coordinates ? {
|
|
738
|
+
lat: f.address.coordinates.lat,
|
|
739
|
+
lng: f.address.coordinates.lng
|
|
740
|
+
} : {}
|
|
741
|
+
}
|
|
742
|
+
} : {}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ../sdk/src/cart/index.ts
|
|
747
|
+
var EMPTY_CART = Object.freeze({
|
|
748
|
+
lines: Object.freeze([]),
|
|
749
|
+
count: 0,
|
|
750
|
+
subtotal: null,
|
|
751
|
+
fulfilment: null,
|
|
752
|
+
promoCode: null,
|
|
753
|
+
state: { status: "ready" },
|
|
754
|
+
issues: Object.freeze([]),
|
|
755
|
+
isHydrated: false
|
|
756
|
+
});
|
|
757
|
+
function deepFreeze(value) {
|
|
758
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
759
|
+
Object.freeze(value);
|
|
760
|
+
for (const child of Object.values(value)) {
|
|
761
|
+
deepFreeze(child);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
function identity(input) {
|
|
767
|
+
return JSON.stringify([
|
|
768
|
+
input.itemId,
|
|
769
|
+
input.menuId,
|
|
770
|
+
input.variantId,
|
|
771
|
+
input.note ?? "",
|
|
772
|
+
[...input.choices ?? []].sort(
|
|
773
|
+
(a, b) => a.groupId.localeCompare(b.groupId) || a.choiceId.localeCompare(b.choiceId)
|
|
774
|
+
),
|
|
775
|
+
Object.entries(input.inputs ?? {}).sort(([a], [b]) => a.localeCompare(b))
|
|
776
|
+
]);
|
|
777
|
+
}
|
|
778
|
+
function createCart(deps) {
|
|
779
|
+
const { config, menu: menus, store, events } = deps;
|
|
780
|
+
const ttl = config.cart?.ttl ?? 7 * 24 * 60 * 60 * 1e3;
|
|
781
|
+
if (!Number.isFinite(ttl) || ttl <= 0) {
|
|
782
|
+
throw new ZappieError(
|
|
783
|
+
"invalid_request",
|
|
784
|
+
"Cart ttl must be positive milliseconds"
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
let snapshot = EMPTY_CART;
|
|
788
|
+
let menu;
|
|
789
|
+
let branch;
|
|
790
|
+
let selected = config.branch;
|
|
791
|
+
let updatedAt = 0;
|
|
792
|
+
let timer;
|
|
793
|
+
let revision = 0;
|
|
794
|
+
let quoteRevision = 0;
|
|
795
|
+
let moveSequence = 0;
|
|
796
|
+
let refreshSequence = 0;
|
|
797
|
+
let mounted = 0;
|
|
798
|
+
let detach;
|
|
799
|
+
let hydration;
|
|
800
|
+
let lifecycle = 0;
|
|
801
|
+
let legacy;
|
|
802
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
803
|
+
const prefix = `zappie:cart:v1:${config.store}:`;
|
|
804
|
+
const scope = config.cart?.storageScope;
|
|
805
|
+
const keyFor = (id) => `${prefix}${id}${scope ? `:${scope}` : ""}`;
|
|
806
|
+
const key = () => keyFor(branch.id);
|
|
807
|
+
const storage = createCartStorage(
|
|
808
|
+
config.storage,
|
|
809
|
+
(message) => events.emit("warning", { message })
|
|
810
|
+
);
|
|
811
|
+
const readSaved = (id) => {
|
|
812
|
+
let raw = storage.read(keyFor(id));
|
|
813
|
+
if (raw === null && scope) {
|
|
814
|
+
raw = storage.read(prefix + id);
|
|
815
|
+
if (raw !== null) {
|
|
816
|
+
storage.remove(prefix + id);
|
|
817
|
+
storage.write(keyFor(id), raw);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return raw;
|
|
821
|
+
};
|
|
822
|
+
let quotes;
|
|
823
|
+
let loadingQuotes;
|
|
824
|
+
const quoteClient = () => loadingQuotes ??= import("./quote-client-4RUY3TKQ.js").then(
|
|
825
|
+
({ createQuoteClient }) => quotes = createQuoteClient(deps.transport, deps.context, config),
|
|
826
|
+
(error) => {
|
|
827
|
+
loadingQuotes = void 0;
|
|
828
|
+
throw error;
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
const publish = (patch) => {
|
|
832
|
+
snapshot = deepFreeze({
|
|
833
|
+
...snapshot,
|
|
834
|
+
...patch,
|
|
835
|
+
branchId: branch?.id
|
|
836
|
+
});
|
|
837
|
+
for (const fn of [...listeners]) {
|
|
838
|
+
try {
|
|
839
|
+
fn();
|
|
840
|
+
} catch (error) {
|
|
841
|
+
events.emit("error", { error: asError(error) });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
const attention = (issues, quote) => issues.some((i) => i.severity !== "info") ? { status: "needs_attention", issues, ...quote ? { quote } : {} } : quote ? { status: "quoted", quote } : { status: "ready" };
|
|
846
|
+
const cancel = () => {
|
|
847
|
+
revision++;
|
|
848
|
+
quoteRevision++;
|
|
849
|
+
clearTimeout(timer);
|
|
850
|
+
quotes?.cancel();
|
|
851
|
+
};
|
|
852
|
+
const persist = () => {
|
|
853
|
+
const value = {
|
|
854
|
+
v: 1,
|
|
855
|
+
lines: snapshot.lines.map((line) => ({
|
|
856
|
+
...lineInput(line),
|
|
857
|
+
id: line.id,
|
|
858
|
+
name: line.name,
|
|
859
|
+
addedAt: line.addedAt,
|
|
860
|
+
unitPrice: line.unitPrice
|
|
861
|
+
})),
|
|
862
|
+
fulfilment: snapshot.fulfilment,
|
|
863
|
+
promoCode: snapshot.promoCode,
|
|
864
|
+
menuVersion: menu.version,
|
|
865
|
+
updatedAt
|
|
866
|
+
};
|
|
867
|
+
storage.write(key(), JSON.stringify(value));
|
|
868
|
+
};
|
|
869
|
+
const schedule2 = () => {
|
|
870
|
+
clearTimeout(timer);
|
|
871
|
+
if (snapshot.lines.length) {
|
|
872
|
+
timer = setTimeout(() => {
|
|
873
|
+
void cart.quote().catch(() => {
|
|
874
|
+
});
|
|
875
|
+
}, 300);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
const recompute = (lines, pending = snapshot.issues.filter((i) => i.source === "local"), patch = {}) => {
|
|
879
|
+
const currency = store.peek().currency;
|
|
880
|
+
const result = reconcile(lines, menu, branch, currency, pending);
|
|
881
|
+
const subtotal = result.priced ? sumMoney(
|
|
882
|
+
result.lines.map((l) => l.lineSubtotal),
|
|
883
|
+
money(0, currency)
|
|
884
|
+
) : null;
|
|
885
|
+
publish({
|
|
886
|
+
...patch,
|
|
887
|
+
lines: result.lines,
|
|
888
|
+
count: result.lines.reduce((n, l) => n + l.quantity, 0),
|
|
889
|
+
subtotal,
|
|
890
|
+
issues: result.issues,
|
|
891
|
+
state: attention(result.issues)
|
|
892
|
+
});
|
|
893
|
+
for (const i of result.issues) {
|
|
894
|
+
events.emit("cart_issue", { code: i.code });
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
const changed = (lines = snapshot.lines, patch = {}) => {
|
|
898
|
+
cancel();
|
|
899
|
+
recompute(lines, void 0, patch);
|
|
900
|
+
updatedAt = Date.now();
|
|
901
|
+
persist();
|
|
902
|
+
schedule2();
|
|
903
|
+
};
|
|
904
|
+
const choose = (value) => {
|
|
905
|
+
const found = store.peek()?.branches.find((b) => b.id === value.branchId);
|
|
906
|
+
if (!found) {
|
|
907
|
+
throw new ZappieError(
|
|
908
|
+
"branch_not_found",
|
|
909
|
+
"Load the storefront branch before using its cart"
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
menu = value;
|
|
913
|
+
branch = found;
|
|
914
|
+
selected = found.id;
|
|
915
|
+
};
|
|
916
|
+
const load = (raw, writeBack = true) => {
|
|
917
|
+
cancel();
|
|
918
|
+
let saved;
|
|
919
|
+
try {
|
|
920
|
+
saved = raw ? parseCart(raw) : void 0;
|
|
921
|
+
if (saved && Date.now() - saved.updatedAt > ttl) {
|
|
922
|
+
saved = void 0;
|
|
923
|
+
storage.remove(key());
|
|
924
|
+
events.emit("cart_expired", { branchId: branch.id });
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
|
+
storage.remove(key());
|
|
928
|
+
storage.warning("Invalid saved cart discarded");
|
|
929
|
+
}
|
|
930
|
+
updatedAt = saved?.updatedAt ?? Date.now();
|
|
931
|
+
const idle = Date.now() - updatedAt;
|
|
932
|
+
let fulfilment = saved?.fulfilment ?? null;
|
|
933
|
+
if (fulfilment?.branchId !== branch.id || idle > 864e5 && fulfilment.type === "delivery") {
|
|
934
|
+
fulfilment = null;
|
|
935
|
+
}
|
|
936
|
+
if (fulfilment && "scheduledFor" in fulfilment && fulfilment.scheduledFor && Date.parse(fulfilment.scheduledFor) < Date.now()) {
|
|
937
|
+
fulfilment = { ...fulfilment };
|
|
938
|
+
delete fulfilment.scheduledFor;
|
|
939
|
+
}
|
|
940
|
+
const lines = (saved?.lines ?? []).map((line) => ({
|
|
941
|
+
...line,
|
|
942
|
+
...idle > 864e5 ? { inputs: {} } : {}
|
|
943
|
+
}));
|
|
944
|
+
const result = reconcile(lines, menu, branch, store.peek().currency);
|
|
945
|
+
recompute(result.lines, result.issues, {
|
|
946
|
+
fulfilment,
|
|
947
|
+
promoCode: saved?.promoCode ?? null,
|
|
948
|
+
isHydrated: true
|
|
949
|
+
});
|
|
950
|
+
if (raw !== null && writeBack) {
|
|
951
|
+
persist();
|
|
952
|
+
}
|
|
953
|
+
storage.sweep(prefix, key(), ttl);
|
|
954
|
+
};
|
|
955
|
+
const ensure = () => {
|
|
956
|
+
const id = branch?.id ?? store.peek()?.branches.find(
|
|
957
|
+
(b) => b.id === selected || b.slug === selected || !selected && b.isDefault
|
|
958
|
+
)?.id ?? (!selected ? store.peek()?.branches[0]?.id : void 0);
|
|
959
|
+
if (snapshot.isHydrated) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const loaded = menu ?? (id ? deps.workingSet.peek(id, []) : void 0);
|
|
963
|
+
if (!loaded || !store.peek()) {
|
|
964
|
+
throw new ZappieError(
|
|
965
|
+
"invalid_request",
|
|
966
|
+
"Load the store and menu before editing the cart"
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
choose(loaded);
|
|
970
|
+
const raw = readSaved(branch.id);
|
|
971
|
+
let migrated = [];
|
|
972
|
+
if (raw === null && legacy) {
|
|
973
|
+
try {
|
|
974
|
+
migrated = legacy(storage.read);
|
|
975
|
+
} catch {
|
|
976
|
+
storage.warning("Invalid legacy cart discarded");
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
load(raw);
|
|
980
|
+
if (raw === null && legacy) {
|
|
981
|
+
persist();
|
|
982
|
+
}
|
|
983
|
+
for (const input of migrated.slice(0, 100)) {
|
|
984
|
+
try {
|
|
985
|
+
cart.add(input.itemId, input);
|
|
986
|
+
} catch {
|
|
987
|
+
events.emit("cart_issue", { code: "item_unavailable" });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
const refresh = async () => {
|
|
992
|
+
const id = branch?.id;
|
|
993
|
+
const seq = moveSequence;
|
|
994
|
+
const request = ++refreshSequence;
|
|
995
|
+
const rev = revision;
|
|
996
|
+
if (!id || !menu) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (!snapshot.lines.length) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (menu.completeness !== "partial") {
|
|
1003
|
+
const version = await deps.transport.getMenuVersion({
|
|
1004
|
+
...deps.context(),
|
|
1005
|
+
branchId: id
|
|
1006
|
+
});
|
|
1007
|
+
if (version.version === menu.version) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const value = await deps.workingSet.read(
|
|
1012
|
+
id,
|
|
1013
|
+
snapshot.lines.map((l) => l.itemId),
|
|
1014
|
+
true
|
|
1015
|
+
);
|
|
1016
|
+
if (seq !== moveSequence || request !== refreshSequence || rev !== revision || JSON.stringify(value) === JSON.stringify(menu)) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
menu = value;
|
|
1020
|
+
cancel();
|
|
1021
|
+
recompute(snapshot.lines);
|
|
1022
|
+
persist();
|
|
1023
|
+
schedule2();
|
|
1024
|
+
};
|
|
1025
|
+
const current = (itemId) => {
|
|
1026
|
+
const candidate = deps.workingSet.peek(branch.id, [
|
|
1027
|
+
...snapshot.lines.map((l) => l.itemId),
|
|
1028
|
+
itemId
|
|
1029
|
+
]);
|
|
1030
|
+
if (candidate.items[itemId]) {
|
|
1031
|
+
menu = candidate;
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
const validated = (raw, id, addedAt) => {
|
|
1035
|
+
current(raw.itemId);
|
|
1036
|
+
const item = Object.hasOwn(menu.items, raw.itemId) ? menu.items[raw.itemId] : void 0;
|
|
1037
|
+
const problems = validateLine(item, raw, { menu, branch });
|
|
1038
|
+
if (problems.length || !item) {
|
|
1039
|
+
throw new ZappieCartError(problems);
|
|
1040
|
+
}
|
|
1041
|
+
const input = normalizeInput(item, raw);
|
|
1042
|
+
const variant = resolveVariant(item, input);
|
|
1043
|
+
const choices = (input.choices ?? []).map((c) => ({
|
|
1044
|
+
...c,
|
|
1045
|
+
quantity: c.quantity ?? 1
|
|
1046
|
+
}));
|
|
1047
|
+
const unitPrice = estimateUnitPrice(item, {
|
|
1048
|
+
choices,
|
|
1049
|
+
variantId: variant.id
|
|
1050
|
+
});
|
|
1051
|
+
return freezeLine({
|
|
1052
|
+
id: id ?? `line_${Array.from(globalThis.crypto.getRandomValues(new Uint8Array(10)), (byte) => "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-"[byte & 63]).join("")}`,
|
|
1053
|
+
itemId: item.id,
|
|
1054
|
+
...input.menuId !== void 0 ? { menuId: input.menuId } : {},
|
|
1055
|
+
name: item.name,
|
|
1056
|
+
quantity: input.quantity,
|
|
1057
|
+
choices,
|
|
1058
|
+
inputs: input.inputs ?? {},
|
|
1059
|
+
variantId: variant.id,
|
|
1060
|
+
...input.note !== void 0 ? { note: input.note } : {},
|
|
1061
|
+
unitPrice,
|
|
1062
|
+
lineSubtotal: multiplyMoney(unitPrice, input.quantity),
|
|
1063
|
+
addedAt: addedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1064
|
+
});
|
|
1065
|
+
};
|
|
1066
|
+
const offMenu = events.on("menu:updated", ({ branchId, version }) => {
|
|
1067
|
+
if (branch?.id === branchId) {
|
|
1068
|
+
const value = menus.peek(branchId);
|
|
1069
|
+
if (value && value.completeness !== "partial" && value.version === version && value.version !== menu?.version) {
|
|
1070
|
+
refreshSequence++;
|
|
1071
|
+
menu = value;
|
|
1072
|
+
cancel();
|
|
1073
|
+
recompute(snapshot.lines);
|
|
1074
|
+
persist();
|
|
1075
|
+
schedule2();
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
const cart = {
|
|
1080
|
+
get branchId() {
|
|
1081
|
+
return snapshot.branchId;
|
|
1082
|
+
},
|
|
1083
|
+
get lines() {
|
|
1084
|
+
return snapshot.lines;
|
|
1085
|
+
},
|
|
1086
|
+
get count() {
|
|
1087
|
+
return snapshot.count;
|
|
1088
|
+
},
|
|
1089
|
+
get subtotal() {
|
|
1090
|
+
return snapshot.subtotal;
|
|
1091
|
+
},
|
|
1092
|
+
get fulfilment() {
|
|
1093
|
+
return snapshot.fulfilment;
|
|
1094
|
+
},
|
|
1095
|
+
get promoCode() {
|
|
1096
|
+
return snapshot.promoCode;
|
|
1097
|
+
},
|
|
1098
|
+
get state() {
|
|
1099
|
+
return snapshot.state;
|
|
1100
|
+
},
|
|
1101
|
+
get issues() {
|
|
1102
|
+
return snapshot.issues;
|
|
1103
|
+
},
|
|
1104
|
+
get isHydrated() {
|
|
1105
|
+
return snapshot.isHydrated;
|
|
1106
|
+
},
|
|
1107
|
+
getSnapshot: () => snapshot,
|
|
1108
|
+
subscribe(fn) {
|
|
1109
|
+
listeners.add(fn);
|
|
1110
|
+
return () => {
|
|
1111
|
+
listeners.delete(fn);
|
|
1112
|
+
};
|
|
1113
|
+
},
|
|
1114
|
+
async hydrate() {
|
|
1115
|
+
if (snapshot.isHydrated) {
|
|
1116
|
+
return refresh();
|
|
1117
|
+
}
|
|
1118
|
+
const life = lifecycle;
|
|
1119
|
+
hydration ??= (async () => {
|
|
1120
|
+
const value = await store.get();
|
|
1121
|
+
const target = value.branches.find(
|
|
1122
|
+
(b) => b.id === selected || b.slug === selected
|
|
1123
|
+
) ?? (!selected ? value.branches.find((b) => b.isDefault) ?? value.branches[0] : void 0);
|
|
1124
|
+
if (!target) {
|
|
1125
|
+
throw new ZappieError(
|
|
1126
|
+
"branch_not_found",
|
|
1127
|
+
"Cart branch is unavailable"
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
let inputs = snapshot.lines;
|
|
1131
|
+
if (!snapshot.isHydrated) {
|
|
1132
|
+
const raw = readSaved(target.id);
|
|
1133
|
+
try {
|
|
1134
|
+
const saved = raw ? parseCart(raw) : void 0;
|
|
1135
|
+
inputs = saved && Date.now() - saved.updatedAt <= ttl ? saved.lines : raw === null ? legacy?.(storage.read) ?? [] : [];
|
|
1136
|
+
} catch {
|
|
1137
|
+
inputs = [];
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const loaded = await deps.workingSet.read(
|
|
1141
|
+
target.id,
|
|
1142
|
+
inputs.map((l) => l.itemId)
|
|
1143
|
+
);
|
|
1144
|
+
if (life !== lifecycle) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
choose(loaded);
|
|
1148
|
+
ensure();
|
|
1149
|
+
if (loaded.completeness !== "partial") {
|
|
1150
|
+
await refresh();
|
|
1151
|
+
}
|
|
1152
|
+
})().finally(() => {
|
|
1153
|
+
hydration = void 0;
|
|
1154
|
+
});
|
|
1155
|
+
return hydration;
|
|
1156
|
+
},
|
|
1157
|
+
mount() {
|
|
1158
|
+
mounted++;
|
|
1159
|
+
if (!detach && typeof window !== "undefined") {
|
|
1160
|
+
const onStorage = (event) => {
|
|
1161
|
+
if (branch && event.key === key() && storage.accepts(event.storageArea)) {
|
|
1162
|
+
const raw = event.newValue;
|
|
1163
|
+
let ids;
|
|
1164
|
+
try {
|
|
1165
|
+
ids = raw ? parseCart(raw).lines.map((l) => l.itemId) : [];
|
|
1166
|
+
} catch {
|
|
1167
|
+
load(raw, false);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const cached = deps.workingSet.peek(branch.id, ids);
|
|
1171
|
+
if (cached.completeness !== "partial" || ids.every((id) => cached.items[id])) {
|
|
1172
|
+
menu = cached;
|
|
1173
|
+
load(raw, false);
|
|
1174
|
+
schedule2();
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
const rev = revision;
|
|
1178
|
+
const request = ++refreshSequence;
|
|
1179
|
+
const life = lifecycle;
|
|
1180
|
+
void deps.workingSet.read(branch.id, ids).then((value) => {
|
|
1181
|
+
if (life === lifecycle && rev === revision && request === refreshSequence) {
|
|
1182
|
+
menu = value;
|
|
1183
|
+
load(raw, false);
|
|
1184
|
+
schedule2();
|
|
1185
|
+
}
|
|
1186
|
+
}).catch(
|
|
1187
|
+
(error) => events.emit("error", { error: asError(error) })
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
const onFocus = () => {
|
|
1192
|
+
void refresh().catch(
|
|
1193
|
+
(error) => events.emit("error", { error: asError(error) })
|
|
1194
|
+
);
|
|
1195
|
+
};
|
|
1196
|
+
window.addEventListener("storage", onStorage);
|
|
1197
|
+
window.addEventListener("focus", onFocus);
|
|
1198
|
+
detach = () => {
|
|
1199
|
+
window.removeEventListener("storage", onStorage);
|
|
1200
|
+
window.removeEventListener("focus", onFocus);
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
if (mounted === 1) {
|
|
1204
|
+
void cart.hydrate().catch(
|
|
1205
|
+
(error) => events.emit("error", { error: asError(error) })
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
let active = true;
|
|
1209
|
+
return () => {
|
|
1210
|
+
if (active) {
|
|
1211
|
+
active = false;
|
|
1212
|
+
mounted--;
|
|
1213
|
+
if (mounted === 0) {
|
|
1214
|
+
detach?.();
|
|
1215
|
+
detach = void 0;
|
|
1216
|
+
cancel();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
},
|
|
1221
|
+
dispose() {
|
|
1222
|
+
lifecycle++;
|
|
1223
|
+
moveSequence++;
|
|
1224
|
+
offMenu();
|
|
1225
|
+
cancel();
|
|
1226
|
+
detach?.();
|
|
1227
|
+
detach = void 0;
|
|
1228
|
+
listeners.clear();
|
|
1229
|
+
},
|
|
1230
|
+
validate(input) {
|
|
1231
|
+
ensure();
|
|
1232
|
+
current(input.itemId);
|
|
1233
|
+
const problems = validateLine(
|
|
1234
|
+
Object.hasOwn(menu.items, input.itemId) ? menu.items[input.itemId] : void 0,
|
|
1235
|
+
input,
|
|
1236
|
+
{ menu, branch }
|
|
1237
|
+
);
|
|
1238
|
+
return problems.length ? { ok: false, issues: problems } : { ok: true };
|
|
1239
|
+
},
|
|
1240
|
+
add(item, input = {}) {
|
|
1241
|
+
ensure();
|
|
1242
|
+
let line = validated({
|
|
1243
|
+
...input,
|
|
1244
|
+
itemId: typeof item === "string" ? item : item.id
|
|
1245
|
+
});
|
|
1246
|
+
const existing = snapshot.lines.find(
|
|
1247
|
+
(l) => identity(lineInput(l)) === identity(lineInput(line))
|
|
1248
|
+
);
|
|
1249
|
+
if (existing) {
|
|
1250
|
+
line = validated(
|
|
1251
|
+
{ ...lineInput(line), quantity: existing.quantity + line.quantity },
|
|
1252
|
+
existing.id,
|
|
1253
|
+
existing.addedAt
|
|
1254
|
+
);
|
|
1255
|
+
} else if (snapshot.lines.length >= 100) {
|
|
1256
|
+
throw new ZappieCartError([
|
|
1257
|
+
issue("quantity_limit", "A cart can contain at most 100 lines")
|
|
1258
|
+
]);
|
|
1259
|
+
}
|
|
1260
|
+
changed(
|
|
1261
|
+
existing ? snapshot.lines.map((l) => l.id === existing.id ? line : l) : [...snapshot.lines, line]
|
|
1262
|
+
);
|
|
1263
|
+
events.emit("add_to_cart", {
|
|
1264
|
+
itemId: line.itemId,
|
|
1265
|
+
quantity: line.quantity - (existing?.quantity ?? 0),
|
|
1266
|
+
value: multiplyMoney(
|
|
1267
|
+
line.unitPrice,
|
|
1268
|
+
line.quantity - (existing?.quantity ?? 0)
|
|
1269
|
+
)
|
|
1270
|
+
});
|
|
1271
|
+
return line;
|
|
1272
|
+
},
|
|
1273
|
+
update(lineId, patch) {
|
|
1274
|
+
ensure();
|
|
1275
|
+
const old = snapshot.lines.find((l) => l.id === lineId);
|
|
1276
|
+
if (!old) {
|
|
1277
|
+
throw new ZappieCartError([
|
|
1278
|
+
issue("item_not_found", "Cart line not found")
|
|
1279
|
+
]);
|
|
1280
|
+
}
|
|
1281
|
+
const input = { ...lineInput(old), ...patch };
|
|
1282
|
+
if (patch.choices && !patch.variantId) {
|
|
1283
|
+
delete input.variantId;
|
|
1284
|
+
}
|
|
1285
|
+
let line = validated(input, old.id, old.addedAt);
|
|
1286
|
+
const duplicate = snapshot.lines.find(
|
|
1287
|
+
(l) => l.id !== lineId && identity(lineInput(l)) === identity(lineInput(line))
|
|
1288
|
+
);
|
|
1289
|
+
if (duplicate) {
|
|
1290
|
+
line = validated(
|
|
1291
|
+
{ ...lineInput(line), quantity: line.quantity + duplicate.quantity },
|
|
1292
|
+
old.id,
|
|
1293
|
+
old.addedAt
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
changed(
|
|
1297
|
+
snapshot.lines.filter((l) => l.id !== duplicate?.id).map((l) => l.id === lineId ? line : l)
|
|
1298
|
+
);
|
|
1299
|
+
events.emit("update_cart_line", {
|
|
1300
|
+
lineId,
|
|
1301
|
+
itemId: line.itemId,
|
|
1302
|
+
quantity: line.quantity
|
|
1303
|
+
});
|
|
1304
|
+
return line;
|
|
1305
|
+
},
|
|
1306
|
+
remove(lineId) {
|
|
1307
|
+
ensure();
|
|
1308
|
+
const line = snapshot.lines.find((l) => l.id === lineId);
|
|
1309
|
+
changed(snapshot.lines.filter((l) => l.id !== lineId));
|
|
1310
|
+
if (line) {
|
|
1311
|
+
events.emit("remove_from_cart", {
|
|
1312
|
+
itemId: line.itemId,
|
|
1313
|
+
quantity: line.quantity,
|
|
1314
|
+
value: line.lineSubtotal
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
},
|
|
1318
|
+
clear() {
|
|
1319
|
+
ensure();
|
|
1320
|
+
changed([], { fulfilment: null, promoCode: null });
|
|
1321
|
+
},
|
|
1322
|
+
setFulfilment(value) {
|
|
1323
|
+
ensure();
|
|
1324
|
+
if (!validFulfilment(value) || value.branchId !== branch.id) {
|
|
1325
|
+
throw new ZappieCartError([
|
|
1326
|
+
issue(
|
|
1327
|
+
"fulfilment_unavailable",
|
|
1328
|
+
"Select fulfilment for the cart's branch"
|
|
1329
|
+
)
|
|
1330
|
+
]);
|
|
1331
|
+
}
|
|
1332
|
+
changed(snapshot.lines, { fulfilment: structuredClone(value) });
|
|
1333
|
+
},
|
|
1334
|
+
setPromoCode(code) {
|
|
1335
|
+
ensure();
|
|
1336
|
+
changed(snapshot.lines, { promoCode: code?.trim() || null });
|
|
1337
|
+
},
|
|
1338
|
+
async moveTo(branchId) {
|
|
1339
|
+
ensure();
|
|
1340
|
+
const seq = ++moveSequence;
|
|
1341
|
+
cancel();
|
|
1342
|
+
publish({ state: attention(snapshot.issues) });
|
|
1343
|
+
const value = await deps.workingSet.read(
|
|
1344
|
+
branchId,
|
|
1345
|
+
snapshot.lines.map((l) => l.itemId)
|
|
1346
|
+
);
|
|
1347
|
+
if (seq !== moveSequence) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const oldKey = key();
|
|
1351
|
+
const oldLines = snapshot.lines;
|
|
1352
|
+
choose(value);
|
|
1353
|
+
const kept = oldLines.filter(
|
|
1354
|
+
(line) => !validateLine(value.items[line.itemId], lineInput(line), {
|
|
1355
|
+
menu: value,
|
|
1356
|
+
branch
|
|
1357
|
+
}).length
|
|
1358
|
+
);
|
|
1359
|
+
changed(kept, { fulfilment: null, promoCode: null });
|
|
1360
|
+
const dropped = oldLines.filter((line) => !kept.includes(line)).map((line) => ({
|
|
1361
|
+
...issue(
|
|
1362
|
+
"item_unavailable",
|
|
1363
|
+
`${line.name} is unavailable at this branch`
|
|
1364
|
+
),
|
|
1365
|
+
severity: "warning",
|
|
1366
|
+
lineId: line.id
|
|
1367
|
+
}));
|
|
1368
|
+
const issues = [...snapshot.issues, ...dropped];
|
|
1369
|
+
publish({ issues, state: attention(issues) });
|
|
1370
|
+
if (oldKey !== key()) {
|
|
1371
|
+
storage.remove(oldKey);
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
async quote(opts = {}) {
|
|
1375
|
+
ensure();
|
|
1376
|
+
clearTimeout(timer);
|
|
1377
|
+
if (opts.force) {
|
|
1378
|
+
events.emit("view_cart", {});
|
|
1379
|
+
}
|
|
1380
|
+
const body = {
|
|
1381
|
+
branch_id: branch.id,
|
|
1382
|
+
lines: snapshot.lines.map(wireLine),
|
|
1383
|
+
...snapshot.fulfilment ? { fulfilment: wireFulfilment(snapshot.fulfilment) } : {},
|
|
1384
|
+
...snapshot.promoCode ? { promo_code: snapshot.promoCode } : {},
|
|
1385
|
+
menu_version: menu.version
|
|
1386
|
+
};
|
|
1387
|
+
const seq = revision;
|
|
1388
|
+
const attempt = ++quoteRevision;
|
|
1389
|
+
publish({ state: { status: "quoting" } });
|
|
1390
|
+
try {
|
|
1391
|
+
const client = await quoteClient();
|
|
1392
|
+
return await client.get(body, opts.force ?? false, (quote) => {
|
|
1393
|
+
if (seq !== revision) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const local = snapshot.issues.filter((i) => i.source === "local");
|
|
1397
|
+
const remote = quote.issues.filter(
|
|
1398
|
+
(q) => !local.some(
|
|
1399
|
+
(l) => l.code === q.code && l.lineId === q.lineId && l.groupId === q.groupId
|
|
1400
|
+
)
|
|
1401
|
+
);
|
|
1402
|
+
const issues = [...local, ...remote];
|
|
1403
|
+
publish({ issues, state: attention(issues, quote) });
|
|
1404
|
+
for (const i of remote) {
|
|
1405
|
+
events.emit("cart_issue", { code: i.code });
|
|
1406
|
+
}
|
|
1407
|
+
if (quote.menuVersion !== menu.version || quote.issues.some((i) => i.code === "menu_version_stale")) {
|
|
1408
|
+
void refresh().catch(
|
|
1409
|
+
(error) => events.emit("error", { error: asError(error) })
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
if (seq === revision && attempt === quoteRevision) {
|
|
1415
|
+
const local = snapshot.issues.filter((i) => i.source === "local");
|
|
1416
|
+
publish({ issues: local, state: attention(local) });
|
|
1417
|
+
events.emit("quote_failed", { code: asError(error).code });
|
|
1418
|
+
}
|
|
1419
|
+
throw error instanceof Error && error.name === "AbortError" ? error : asError(error);
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
applyFix(problem, fix = problem.fixes[0]) {
|
|
1423
|
+
if (!snapshot.issues.includes(problem) || !fix || !problem.fixes.some((f) => JSON.stringify(f) === JSON.stringify(fix))) {
|
|
1424
|
+
throw new ZappieError(
|
|
1425
|
+
"invalid_request",
|
|
1426
|
+
"Choose a current cart issue and one of its fixes"
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
switch (fix.action) {
|
|
1430
|
+
case "remove_line": {
|
|
1431
|
+
if (problem.lineId) {
|
|
1432
|
+
cart.remove(problem.lineId);
|
|
1433
|
+
}
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
case "remove_promo":
|
|
1437
|
+
cart.setPromoCode(null);
|
|
1438
|
+
return;
|
|
1439
|
+
case "set_quantity": {
|
|
1440
|
+
if (problem.lineId) {
|
|
1441
|
+
cart.update(problem.lineId, { quantity: fix.quantity });
|
|
1442
|
+
}
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
case "accept_price": {
|
|
1446
|
+
quotes?.accept(problem);
|
|
1447
|
+
const issues = snapshot.issues.filter((i) => i !== problem);
|
|
1448
|
+
const quote = "quote" in snapshot.state ? snapshot.state.quote : void 0;
|
|
1449
|
+
const accepted = quote ? {
|
|
1450
|
+
...quote,
|
|
1451
|
+
issues: quote.issues.filter(
|
|
1452
|
+
(i) => !(i.code === problem.code && i.lineId === problem.lineId)
|
|
1453
|
+
)
|
|
1454
|
+
} : void 0;
|
|
1455
|
+
publish({ issues, state: attention(issues, accepted) });
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
case "choose_region":
|
|
1459
|
+
throw new ZappieError(
|
|
1460
|
+
"invalid_request",
|
|
1461
|
+
"Choose a delivery region and call setFulfilment with deliveryRegionId"
|
|
1462
|
+
);
|
|
1463
|
+
default:
|
|
1464
|
+
throw new ZappieError(
|
|
1465
|
+
"invalid_request",
|
|
1466
|
+
`${fix.action} needs the shopper's selection; use update or setFulfilment`
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
},
|
|
1470
|
+
toCheckoutPayload() {
|
|
1471
|
+
ensure();
|
|
1472
|
+
const blocking = snapshot.issues.filter((i) => i.severity === "blocking");
|
|
1473
|
+
if (!snapshot.lines.length || !snapshot.fulfilment) {
|
|
1474
|
+
blocking.push(
|
|
1475
|
+
issue(
|
|
1476
|
+
"fulfilment_unavailable",
|
|
1477
|
+
"Add items and select fulfilment before checkout"
|
|
1478
|
+
)
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
if (blocking.length) {
|
|
1482
|
+
throw new ZappieCartError(blocking);
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
lines: snapshot.lines.map(wireLine),
|
|
1486
|
+
fulfilment: structuredClone(snapshot.fulfilment),
|
|
1487
|
+
...snapshot.promoCode ? { promo_code: snapshot.promoCode } : {},
|
|
1488
|
+
menu_version: menu.version
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
return {
|
|
1493
|
+
cart: Object.freeze(cart),
|
|
1494
|
+
cartCatalogue: () => menu,
|
|
1495
|
+
setLegacyReader(reader) {
|
|
1496
|
+
legacy = reader;
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// ../sdk/src/cart/working-set.ts
|
|
1502
|
+
function createWorkingSet(deps) {
|
|
1503
|
+
const compose = (branchId, ids, metadata) => {
|
|
1504
|
+
const source = metadata ?? deps.menu.peek(branchId);
|
|
1505
|
+
const items = Object.fromEntries(
|
|
1506
|
+
[...new Set(ids)].flatMap((id) => {
|
|
1507
|
+
const item = deps.peekItem(id, branchId) ?? source?.items[id];
|
|
1508
|
+
return item ? [[id, item]] : [];
|
|
1509
|
+
})
|
|
1510
|
+
);
|
|
1511
|
+
return {
|
|
1512
|
+
id: `branch:${branchId}`,
|
|
1513
|
+
name: "Cart working set",
|
|
1514
|
+
branchId,
|
|
1515
|
+
version: "",
|
|
1516
|
+
...source,
|
|
1517
|
+
kind: "working-set",
|
|
1518
|
+
completeness: "partial",
|
|
1519
|
+
items,
|
|
1520
|
+
categories: (source?.categories ?? []).map((category) => ({
|
|
1521
|
+
...category,
|
|
1522
|
+
itemIds: Object.values(items).filter(
|
|
1523
|
+
(item) => item.placements ? item.placements.some(
|
|
1524
|
+
(p) => p.categoryId === category.id && (source?.menus ?? [
|
|
1525
|
+
{ id: source?.id, categoryIds: [category.id] }
|
|
1526
|
+
]).some(
|
|
1527
|
+
(m) => m.id === p.menuId && m.categoryIds.includes(category.id)
|
|
1528
|
+
)
|
|
1529
|
+
) : category.itemIds.includes(item.id)
|
|
1530
|
+
).map((item) => item.id)
|
|
1531
|
+
}))
|
|
1532
|
+
};
|
|
1533
|
+
};
|
|
1534
|
+
const peek = (branchId, ids) => {
|
|
1535
|
+
const value = deps.menu.peek(branchId);
|
|
1536
|
+
return value && value.completeness !== "partial" ? value : compose(branchId, ids);
|
|
1537
|
+
};
|
|
1538
|
+
return {
|
|
1539
|
+
peek,
|
|
1540
|
+
async read(branchId, ids, fresh = false) {
|
|
1541
|
+
const cached = peek(branchId, ids);
|
|
1542
|
+
if (!ids.length || !fresh && cached.completeness !== "partial") {
|
|
1543
|
+
return cached;
|
|
1544
|
+
}
|
|
1545
|
+
const response = fresh ? await deps.transport.getMenu({
|
|
1546
|
+
...deps.context(),
|
|
1547
|
+
branchId,
|
|
1548
|
+
metadataOnly: true
|
|
1549
|
+
}) : void 0;
|
|
1550
|
+
const metadata = response && "menu" in response ? response.menu : await deps.menu.get({ branch: branchId, metadataOnly: true });
|
|
1551
|
+
if (metadata.branchId !== branchId) {
|
|
1552
|
+
throw new ZappieError(
|
|
1553
|
+
"server_error",
|
|
1554
|
+
"Cart metadata belongs to another branch"
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
deps.menu.hydrate(metadata);
|
|
1558
|
+
if (metadata.completeness !== "partial") {
|
|
1559
|
+
return metadata;
|
|
1560
|
+
}
|
|
1561
|
+
await Promise.all(
|
|
1562
|
+
[...new Set(ids)].map(async (id) => {
|
|
1563
|
+
try {
|
|
1564
|
+
await deps.readItem(id, branchId, fresh);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
if (error.code !== "item_not_found") {
|
|
1567
|
+
throw error;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
})
|
|
1571
|
+
);
|
|
1572
|
+
return compose(branchId, ids, metadata);
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// ../sdk/src/config.ts
|
|
1578
|
+
var API_VERSION = "2026-10-01";
|
|
1579
|
+
function validateConfig(config, secret = false) {
|
|
1580
|
+
if (!config.store?.trim()) {
|
|
1581
|
+
throw new ZappieError("invalid_request", "A storefront slug is required");
|
|
1582
|
+
}
|
|
1583
|
+
if (secret && typeof window !== "undefined") {
|
|
1584
|
+
throw new ZappieError(
|
|
1585
|
+
"invalid_request",
|
|
1586
|
+
"Secret keys cannot be used in a browser"
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
const pattern = secret ? /^sk_(test|live)_.+$/ : /^pk_(test|live)_.+$/;
|
|
1590
|
+
const match = pattern.exec(config.key);
|
|
1591
|
+
if (!match) {
|
|
1592
|
+
throw new ZappieError(
|
|
1593
|
+
"invalid_request",
|
|
1594
|
+
secret ? "A secret sk_test_ or sk_live_ key is required" : "Use a publishable pk_test_ or pk_live_ key; secret keys are never accepted by createZappie"
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
return match[1];
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// ../sdk/src/internal/abort.ts
|
|
1601
|
+
function abortReason(signal) {
|
|
1602
|
+
return signal?.reason ?? new DOMException("The request was aborted", "AbortError");
|
|
1603
|
+
}
|
|
1604
|
+
function throwIfAborted(signal) {
|
|
1605
|
+
if (signal?.aborted) {
|
|
1606
|
+
throw abortReason(signal);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function delay(ms, signal) {
|
|
1610
|
+
return new Promise((resolve, reject) => {
|
|
1611
|
+
throwIfAborted(signal);
|
|
1612
|
+
const abort = () => {
|
|
1613
|
+
clearTimeout(timer);
|
|
1614
|
+
reject(abortReason(signal));
|
|
1615
|
+
};
|
|
1616
|
+
const timer = setTimeout(() => {
|
|
1617
|
+
signal?.removeEventListener("abort", abort);
|
|
1618
|
+
resolve();
|
|
1619
|
+
}, ms);
|
|
1620
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// ../sdk/src/internal/cache.ts
|
|
1625
|
+
var REFRESH_BACKOFF_MS = 1e4;
|
|
1626
|
+
var EMPTY = Object.freeze({
|
|
1627
|
+
fetchedAt: 0,
|
|
1628
|
+
loading: false
|
|
1629
|
+
});
|
|
1630
|
+
var Cache = class {
|
|
1631
|
+
constructor(onError) {
|
|
1632
|
+
this.onError = onError;
|
|
1633
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
1634
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1635
|
+
this.flights = /* @__PURE__ */ new Map();
|
|
1636
|
+
this.subscribe = (fn) => {
|
|
1637
|
+
this.listeners.add(fn);
|
|
1638
|
+
return () => {
|
|
1639
|
+
this.listeners.delete(fn);
|
|
1640
|
+
};
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
snapshot(key) {
|
|
1644
|
+
return this.entries.get(key) ?? EMPTY;
|
|
1645
|
+
}
|
|
1646
|
+
notify() {
|
|
1647
|
+
for (const fn of [...this.listeners]) {
|
|
1648
|
+
fn();
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
set(key, value, etag) {
|
|
1652
|
+
this.entries.set(key, {
|
|
1653
|
+
value,
|
|
1654
|
+
fetchedAt: Date.now(),
|
|
1655
|
+
loading: false,
|
|
1656
|
+
...etag !== void 0 ? { etag } : {}
|
|
1657
|
+
});
|
|
1658
|
+
this.notify();
|
|
1659
|
+
}
|
|
1660
|
+
stale(key) {
|
|
1661
|
+
const entry = this.snapshot(key);
|
|
1662
|
+
return entry.value !== void 0 && Date.now() - entry.fetchedAt >= 6e4;
|
|
1663
|
+
}
|
|
1664
|
+
async get(key, loader, signal) {
|
|
1665
|
+
throwIfAborted(signal);
|
|
1666
|
+
const entry = this.snapshot(key);
|
|
1667
|
+
const age = Date.now() - entry.fetchedAt;
|
|
1668
|
+
if (entry.value !== void 0 && age < 6e4) {
|
|
1669
|
+
return entry.value;
|
|
1670
|
+
}
|
|
1671
|
+
const background = entry.value !== void 0 && age < 6e5;
|
|
1672
|
+
if (background && entry.failedAt !== void 0 && Date.now() - entry.failedAt < REFRESH_BACKOFF_MS) {
|
|
1673
|
+
return entry.value;
|
|
1674
|
+
}
|
|
1675
|
+
let flight = this.flights.get(key);
|
|
1676
|
+
if (!flight) {
|
|
1677
|
+
const controller = new AbortController();
|
|
1678
|
+
flight = {
|
|
1679
|
+
controller,
|
|
1680
|
+
users: 0,
|
|
1681
|
+
keepAlive: background,
|
|
1682
|
+
promise: Promise.resolve()
|
|
1683
|
+
};
|
|
1684
|
+
const current2 = flight;
|
|
1685
|
+
this.flights.set(key, current2);
|
|
1686
|
+
const loading = { ...entry, loading: true };
|
|
1687
|
+
this.entries.set(key, loading);
|
|
1688
|
+
this.notify();
|
|
1689
|
+
current2.promise = Promise.resolve().then(() => {
|
|
1690
|
+
throwIfAborted(controller.signal);
|
|
1691
|
+
return loader(controller.signal, entry);
|
|
1692
|
+
}).then((result) => {
|
|
1693
|
+
if (this.entries.get(key) === loading && !controller.signal.aborted) {
|
|
1694
|
+
this.set(key, result.value, result.etag);
|
|
1695
|
+
result.onStored?.();
|
|
1696
|
+
}
|
|
1697
|
+
return result.value;
|
|
1698
|
+
}).catch((error) => {
|
|
1699
|
+
if (this.entries.get(key) === loading) {
|
|
1700
|
+
this.entries.set(
|
|
1701
|
+
key,
|
|
1702
|
+
controller.signal.aborted ? { ...entry, loading: false } : {
|
|
1703
|
+
...entry,
|
|
1704
|
+
loading: false,
|
|
1705
|
+
error: asError(error),
|
|
1706
|
+
failedAt: Date.now()
|
|
1707
|
+
}
|
|
1708
|
+
);
|
|
1709
|
+
this.notify();
|
|
1710
|
+
if (!controller.signal.aborted) {
|
|
1711
|
+
this.onError(asError(error));
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
throw controller.signal.aborted ? abortReason(controller.signal) : asError(error);
|
|
1715
|
+
}).finally(() => {
|
|
1716
|
+
if (this.flights.get(key) === current2) {
|
|
1717
|
+
this.flights.delete(key);
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
if (background) {
|
|
1722
|
+
flight.keepAlive = true;
|
|
1723
|
+
void flight.promise.catch(() => {
|
|
1724
|
+
});
|
|
1725
|
+
return entry.value;
|
|
1726
|
+
}
|
|
1727
|
+
const current = flight;
|
|
1728
|
+
current.users++;
|
|
1729
|
+
return new Promise((resolve, reject) => {
|
|
1730
|
+
let done = false;
|
|
1731
|
+
const finish = (callback) => {
|
|
1732
|
+
if (done) {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
done = true;
|
|
1736
|
+
signal?.removeEventListener("abort", abort);
|
|
1737
|
+
current.users--;
|
|
1738
|
+
callback();
|
|
1739
|
+
};
|
|
1740
|
+
const abort = () => {
|
|
1741
|
+
finish(() => reject(abortReason(signal)));
|
|
1742
|
+
if (current.users === 0 && !current.keepAlive) {
|
|
1743
|
+
current.controller.abort(abortReason(signal));
|
|
1744
|
+
if (this.flights.get(key) === current) {
|
|
1745
|
+
this.flights.delete(key);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1750
|
+
current.promise.then(
|
|
1751
|
+
(value) => finish(() => resolve(value)),
|
|
1752
|
+
(error) => finish(() => reject(error))
|
|
1753
|
+
);
|
|
1754
|
+
if (signal?.aborted) {
|
|
1755
|
+
abort();
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1761
|
+
// ../sdk/src/internal/emitter.ts
|
|
1762
|
+
function createEmitter() {
|
|
1763
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
1764
|
+
return {
|
|
1765
|
+
on(event, fn) {
|
|
1766
|
+
const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
1767
|
+
listeners.set(event, set);
|
|
1768
|
+
set.add(fn);
|
|
1769
|
+
return () => {
|
|
1770
|
+
set.delete(fn);
|
|
1771
|
+
};
|
|
1772
|
+
},
|
|
1773
|
+
emit(event, payload) {
|
|
1774
|
+
for (const fn of [...listeners.get(event) ?? []]) {
|
|
1775
|
+
try {
|
|
1776
|
+
fn(payload);
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
console.error("Zappie event listener failed", error);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// ../sdk/src/internal/digest.ts
|
|
1786
|
+
async function payloadKey(payload) {
|
|
1787
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
1788
|
+
"SHA-256",
|
|
1789
|
+
new TextEncoder().encode(payload)
|
|
1790
|
+
);
|
|
1791
|
+
return Array.from(
|
|
1792
|
+
new Uint8Array(digest),
|
|
1793
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
1794
|
+
).join("");
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// ../sdk/src/transport/mappers.ts
|
|
1798
|
+
var UnsupportedValue = class extends Error {
|
|
1799
|
+
};
|
|
1800
|
+
function record(value) {
|
|
1801
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1802
|
+
throw new ZappieError("server_error", "Invalid storefront response object");
|
|
1803
|
+
}
|
|
1804
|
+
return value;
|
|
1805
|
+
}
|
|
1806
|
+
function text(value) {
|
|
1807
|
+
if (typeof value !== "string") {
|
|
1808
|
+
throw new ZappieError("server_error", "Invalid storefront response string");
|
|
1809
|
+
}
|
|
1810
|
+
return value;
|
|
1811
|
+
}
|
|
1812
|
+
function list(value) {
|
|
1813
|
+
if (!Array.isArray(value)) {
|
|
1814
|
+
throw new ZappieError("server_error", "Invalid storefront response list");
|
|
1815
|
+
}
|
|
1816
|
+
return value;
|
|
1817
|
+
}
|
|
1818
|
+
function number(value) {
|
|
1819
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1820
|
+
throw new ZappieError("server_error", "Invalid storefront response number");
|
|
1821
|
+
}
|
|
1822
|
+
return value;
|
|
1823
|
+
}
|
|
1824
|
+
function nonnegative(value) {
|
|
1825
|
+
const result = number(value);
|
|
1826
|
+
if (result < 0) {
|
|
1827
|
+
throw new ZappieError(
|
|
1828
|
+
"server_error",
|
|
1829
|
+
"Negative selection or quantity bound"
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
return result;
|
|
1833
|
+
}
|
|
1834
|
+
function bounds(value) {
|
|
1835
|
+
const input = record(value);
|
|
1836
|
+
const min = nonnegative(input.min);
|
|
1837
|
+
if (input.max == null) {
|
|
1838
|
+
return { min };
|
|
1839
|
+
}
|
|
1840
|
+
const max = nonnegative(input.max);
|
|
1841
|
+
if (max < min) {
|
|
1842
|
+
throw new ZappieError(
|
|
1843
|
+
"server_error",
|
|
1844
|
+
"Maximum quantity is below its minimum"
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
return { min, max };
|
|
1848
|
+
}
|
|
1849
|
+
var isString = (value) => typeof value === "string";
|
|
1850
|
+
var isBoolean = (value) => typeof value === "boolean";
|
|
1851
|
+
function opt(key, value, is) {
|
|
1852
|
+
return is(value) ? { [key]: value } : {};
|
|
1853
|
+
}
|
|
1854
|
+
function mapMoney(value) {
|
|
1855
|
+
const v = record(value);
|
|
1856
|
+
return money(number(v.amount_minor), text(v.currency));
|
|
1857
|
+
}
|
|
1858
|
+
function hours(value) {
|
|
1859
|
+
return list(value ?? []).map((slot) => {
|
|
1860
|
+
const v = record(slot);
|
|
1861
|
+
const day = number(v.day);
|
|
1862
|
+
if (!Number.isInteger(day) || day < 0 || day > 6) {
|
|
1863
|
+
throw new ZappieError("server_error", "Invalid opening day");
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
day,
|
|
1867
|
+
opens: text(v.open),
|
|
1868
|
+
closes: text(v.close),
|
|
1869
|
+
...opt("disabled", v.disabled, isBoolean)
|
|
1870
|
+
};
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
function schedule(v) {
|
|
1874
|
+
if (v.hours === void 0 && v.preorder === void 0) {
|
|
1875
|
+
return void 0;
|
|
1876
|
+
}
|
|
1877
|
+
const preorder = v.preorder === void 0 ? void 0 : record(v.preorder);
|
|
1878
|
+
return {
|
|
1879
|
+
openingTimes: hours(v.hours),
|
|
1880
|
+
...preorder ? {
|
|
1881
|
+
preorder: true,
|
|
1882
|
+
preorderNoticeHours: number(preorder.notice_hours),
|
|
1883
|
+
preorderMaxDays: number(preorder.max_days)
|
|
1884
|
+
} : {}
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
function mapGroup(value) {
|
|
1888
|
+
const g = record(value);
|
|
1889
|
+
const selection = { min: g.min, max: g.max };
|
|
1890
|
+
const isAttribute = g.kind === "attribute";
|
|
1891
|
+
const kind = text(g.kind);
|
|
1892
|
+
if (!["attribute", "choice", "text"].includes(kind)) {
|
|
1893
|
+
throw new UnsupportedValue(`option input kind "${kind}"`);
|
|
1894
|
+
}
|
|
1895
|
+
return {
|
|
1896
|
+
id: text(g.id),
|
|
1897
|
+
name: text(g.name),
|
|
1898
|
+
kind: kind === "text" ? "text" : "select",
|
|
1899
|
+
required: typeof g.required === "boolean" ? g.required : number(selection.min) > 0,
|
|
1900
|
+
selection: bounds(selection),
|
|
1901
|
+
isAttribute,
|
|
1902
|
+
choices: list(g.choices ?? []).map((raw) => {
|
|
1903
|
+
const c = record(raw);
|
|
1904
|
+
const price = mapMoney(c.price);
|
|
1905
|
+
return {
|
|
1906
|
+
id: text(c.id),
|
|
1907
|
+
name: text(c.name),
|
|
1908
|
+
price: isAttribute ? money(0, price.currency) : price,
|
|
1909
|
+
...isAttribute && c.price_hint !== void 0 ? { priceHint: mapMoney(c.price_hint) } : {},
|
|
1910
|
+
isAvailable: c.available === true,
|
|
1911
|
+
...c.variable_quantity === true ? {
|
|
1912
|
+
quantity: bounds({
|
|
1913
|
+
min: c.min_quantity ?? 1,
|
|
1914
|
+
max: c.max_quantity
|
|
1915
|
+
})
|
|
1916
|
+
} : {}
|
|
1917
|
+
};
|
|
1918
|
+
}),
|
|
1919
|
+
...opt("helpText", g.help_text, isString),
|
|
1920
|
+
...isString(g.placeholder) ? { input: { placeholder: g.placeholder } } : {},
|
|
1921
|
+
...g.conditions ? {
|
|
1922
|
+
conditions: list(g.conditions).map((raw) => {
|
|
1923
|
+
const c = record(raw);
|
|
1924
|
+
const count = c.count ? record(c.count) : void 0;
|
|
1925
|
+
if (count && !["eq", "gte", "lte"].includes(text(count.op))) {
|
|
1926
|
+
throw new UnsupportedValue(
|
|
1927
|
+
`option count operator "${text(count.op)}"`
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
if (c.match !== void 0 && c.match !== "any" && c.match !== "all") {
|
|
1931
|
+
throw new UnsupportedValue(
|
|
1932
|
+
`option match operator "${String(c.match)}"`
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
if (c.set_required != null && typeof c.set_required !== "boolean") {
|
|
1936
|
+
throw new ZappieError(
|
|
1937
|
+
"server_error",
|
|
1938
|
+
"Invalid conditional required flag"
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
return {
|
|
1942
|
+
groupId: text(c.group_id),
|
|
1943
|
+
choiceIds: list(c.choice_ids ?? []).map(text),
|
|
1944
|
+
match: c.match === "all" ? "all" : "any",
|
|
1945
|
+
...count ? {
|
|
1946
|
+
count: {
|
|
1947
|
+
op: text(count.op),
|
|
1948
|
+
value: nonnegative(count.value)
|
|
1949
|
+
}
|
|
1950
|
+
} : {},
|
|
1951
|
+
...c.set_required !== void 0 ? { setRequired: c.set_required } : {},
|
|
1952
|
+
...c.set_min !== void 0 ? { setMin: c.set_min === null ? null : nonnegative(c.set_min) } : {},
|
|
1953
|
+
...c.set_max !== void 0 ? { setMax: c.set_max === null ? null : nonnegative(c.set_max) } : {}
|
|
1954
|
+
};
|
|
1955
|
+
})
|
|
1956
|
+
} : {}
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
function mapPromo(value) {
|
|
1960
|
+
const p = record(value);
|
|
1961
|
+
return { name: text(p.name) };
|
|
1962
|
+
}
|
|
1963
|
+
function mapItem(value) {
|
|
1964
|
+
const v = record(value);
|
|
1965
|
+
const id = text(v.id);
|
|
1966
|
+
const optionGroups = [];
|
|
1967
|
+
let unsupported = false;
|
|
1968
|
+
for (const raw of list(v.option_groups ?? [])) {
|
|
1969
|
+
try {
|
|
1970
|
+
optionGroups.push(mapGroup(raw));
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
if (!(error instanceof UnsupportedValue)) {
|
|
1973
|
+
throw error;
|
|
1974
|
+
}
|
|
1975
|
+
unsupported = true;
|
|
1976
|
+
console.warn(
|
|
1977
|
+
`Zappie item ${id} uses an ${error.message} this SDK can't validate; it is shown as unavailable`
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
let unresolved = false;
|
|
1982
|
+
const variants = list(v.variants).map((raw) => {
|
|
1983
|
+
const x = record(raw);
|
|
1984
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1985
|
+
return {
|
|
1986
|
+
id: text(x.id),
|
|
1987
|
+
attributeChoices: Object.fromEntries(
|
|
1988
|
+
list(x.choice_ids).map((rawId) => {
|
|
1989
|
+
const choiceId = text(rawId);
|
|
1990
|
+
const groups = optionGroups.filter(
|
|
1991
|
+
(group) => group.isAttribute && group.choices.some((choice) => choice.id === choiceId)
|
|
1992
|
+
);
|
|
1993
|
+
if (groups.length !== 1 || seen.has(groups[0].id)) {
|
|
1994
|
+
unresolved = true;
|
|
1995
|
+
return ["", choiceId];
|
|
1996
|
+
}
|
|
1997
|
+
seen.add(groups[0].id);
|
|
1998
|
+
return [groups[0].id, choiceId];
|
|
1999
|
+
}).filter(([groupId]) => groupId)
|
|
2000
|
+
),
|
|
2001
|
+
price: mapMoney(x.price),
|
|
2002
|
+
...opt("lowStock", x.low_stock, isBoolean),
|
|
2003
|
+
isAvailable: x.available === true
|
|
2004
|
+
};
|
|
2005
|
+
});
|
|
2006
|
+
if (unresolved) {
|
|
2007
|
+
console.warn(
|
|
2008
|
+
`Zappie item ${id} has a variant whose choices don't match its attributes; it is shown as unavailable`
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
if (unsupported || unresolved) {
|
|
2012
|
+
for (const variant of variants) {
|
|
2013
|
+
variant.isAvailable = false;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
if (!variants.length) {
|
|
2017
|
+
throw new ZappieError(
|
|
2018
|
+
"server_error",
|
|
2019
|
+
"An item must carry at least one exact variant"
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
const quantity = { min: v.min_quantity ?? 1, max: v.max_quantity };
|
|
2023
|
+
if (new Set(variants.map((variant) => variant.id)).size !== variants.length) {
|
|
2024
|
+
throw new ZappieError("server_error", "Duplicate variant identity");
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
...v.placements !== void 0 ? {
|
|
2028
|
+
placements: list(v.placements).map((raw) => {
|
|
2029
|
+
const placement = record(raw);
|
|
2030
|
+
return {
|
|
2031
|
+
menuId: text(placement.menu_id),
|
|
2032
|
+
categoryId: text(placement.category_id)
|
|
2033
|
+
};
|
|
2034
|
+
})
|
|
2035
|
+
} : {},
|
|
2036
|
+
id,
|
|
2037
|
+
name: text(v.name),
|
|
2038
|
+
images: list(v.images ?? []).map(text),
|
|
2039
|
+
price: mapMoney(v.price),
|
|
2040
|
+
isAvailable: !unsupported && !unresolved && v.available === true,
|
|
2041
|
+
quantity: { ...bounds(quantity), step: 1 },
|
|
2042
|
+
variants,
|
|
2043
|
+
optionGroups,
|
|
2044
|
+
tags: list(v.tags ?? []).map(text),
|
|
2045
|
+
...v.fulfilment !== void 0 ? { fulfilment: list(v.fulfilment).map(text) } : {},
|
|
2046
|
+
...opt("stockLabel", v.stock_label, isString),
|
|
2047
|
+
...v.promo !== void 0 ? { promo: mapPromo(v.promo) } : {},
|
|
2048
|
+
...opt("description", v.description, isString),
|
|
2049
|
+
...v.previous_price ? { compareAtPrice: mapMoney(v.previous_price) } : {}
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function mapMenu(value, branchId) {
|
|
2053
|
+
const v = record(value);
|
|
2054
|
+
const sources = list(v.menus);
|
|
2055
|
+
if (v.completeness !== void 0 && v.completeness !== "partial" && v.completeness !== "complete") {
|
|
2056
|
+
throw new ZappieError("server_error", "Invalid catalogue completeness");
|
|
2057
|
+
}
|
|
2058
|
+
const categories = /* @__PURE__ */ new Map();
|
|
2059
|
+
const menus = sources.map((raw) => {
|
|
2060
|
+
const m = record(raw);
|
|
2061
|
+
if (m.placement_count !== void 0 && (!Number.isSafeInteger(m.placement_count) || Number(m.placement_count) < 0) || m.paginated !== void 0 && typeof m.paginated !== "boolean") {
|
|
2062
|
+
throw new ZappieError("server_error", "Invalid menu loading metadata");
|
|
2063
|
+
}
|
|
2064
|
+
const categoryIds = list(m.categories ?? []).map((rawCategory) => {
|
|
2065
|
+
const c = record(rawCategory);
|
|
2066
|
+
const id = text(c.id);
|
|
2067
|
+
const previous = categories.get(id);
|
|
2068
|
+
const itemIds = [
|
|
2069
|
+
.../* @__PURE__ */ new Set([
|
|
2070
|
+
...previous?.itemIds ?? [],
|
|
2071
|
+
...list(c.item_ids ?? []).map(text)
|
|
2072
|
+
])
|
|
2073
|
+
];
|
|
2074
|
+
categories.set(id, {
|
|
2075
|
+
id,
|
|
2076
|
+
name: text(c.name),
|
|
2077
|
+
itemIds
|
|
2078
|
+
});
|
|
2079
|
+
return id;
|
|
2080
|
+
});
|
|
2081
|
+
const s = schedule(m);
|
|
2082
|
+
return {
|
|
2083
|
+
id: text(m.id),
|
|
2084
|
+
name: text(m.name),
|
|
2085
|
+
categoryIds,
|
|
2086
|
+
...m.fulfilment !== void 0 ? { fulfilment: list(m.fulfilment).map(text) } : {},
|
|
2087
|
+
...m.placement_count !== void 0 ? { placementCount: m.placement_count } : {},
|
|
2088
|
+
...opt("paginated", m.paginated, isBoolean),
|
|
2089
|
+
...s ? { schedule: s } : {},
|
|
2090
|
+
...opt("infoBanner", m.info_banner, isString)
|
|
2091
|
+
};
|
|
2092
|
+
});
|
|
2093
|
+
const items = Object.fromEntries(
|
|
2094
|
+
Object.entries(record(v.items)).map(([id, item]) => {
|
|
2095
|
+
const mapped = mapItem(item);
|
|
2096
|
+
if (mapped.id !== id) {
|
|
2097
|
+
throw new ZappieError(
|
|
2098
|
+
"server_error",
|
|
2099
|
+
"Item map key does not match its identity"
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
return [id, mapped];
|
|
2103
|
+
})
|
|
2104
|
+
);
|
|
2105
|
+
if (v.branch_id !== void 0 && v.branch_id !== branchId) {
|
|
2106
|
+
throw new ZappieError("server_error", "Menu branch mismatch");
|
|
2107
|
+
}
|
|
2108
|
+
return {
|
|
2109
|
+
id: menus.length === 1 ? menus[0].id : `branch:${branchId}`,
|
|
2110
|
+
branchId,
|
|
2111
|
+
...v.completeness === "partial" ? { completeness: "partial" } : {},
|
|
2112
|
+
name: menus.length === 1 ? menus[0].name : "Catalogue",
|
|
2113
|
+
version: text(v.menu_version),
|
|
2114
|
+
categories: [...categories.values()],
|
|
2115
|
+
items,
|
|
2116
|
+
menus,
|
|
2117
|
+
...menus.length === 1 && menus[0].schedule ? { schedule: menus[0].schedule } : {}
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
function mapSeo(value) {
|
|
2121
|
+
const seo = record(value);
|
|
2122
|
+
if (typeof seo.hidden !== "boolean") {
|
|
2123
|
+
throw new ZappieError("server_error", "Invalid store search visibility");
|
|
2124
|
+
}
|
|
2125
|
+
return {
|
|
2126
|
+
titlePattern: text(seo.title_pattern),
|
|
2127
|
+
description: text(seo.description),
|
|
2128
|
+
shareImageUrl: text(seo.share_image_url),
|
|
2129
|
+
searchConsoleToken: text(seo.search_console_token),
|
|
2130
|
+
hidden: seo.hidden,
|
|
2131
|
+
redirects: list(seo.redirects).map((raw) => {
|
|
2132
|
+
const redirect = record(raw);
|
|
2133
|
+
return { from: text(redirect.from), to: text(redirect.to) };
|
|
2134
|
+
})
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// ../sdk/src/transport/rest.ts
|
|
2139
|
+
var MAX_RETRY_WAIT_MS = 5e3;
|
|
2140
|
+
var wireFulfilment2 = (value) => value === "dineIn" ? "dine_in" : value;
|
|
2141
|
+
var presentationError = () => new ZappieError("server_error", "Invalid storefront presentation");
|
|
2142
|
+
var RestTransport = class {
|
|
2143
|
+
constructor(opts = {}) {
|
|
2144
|
+
const url = new URL(opts.baseUrl ?? "https://api.zappie.ai/storefront/v1");
|
|
2145
|
+
if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
|
|
2146
|
+
throw new ZappieError(
|
|
2147
|
+
"invalid_request",
|
|
2148
|
+
"REST baseUrl requires HTTPS, or HTTP on localhost"
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
this.baseUrl = url.href.replace(/\/$/, "");
|
|
2152
|
+
this.fetcher = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
2153
|
+
}
|
|
2154
|
+
async request(path, ctx, etag, body) {
|
|
2155
|
+
if (typeof window !== "undefined" && ctx.key.startsWith("sk_")) {
|
|
2156
|
+
throw new ZappieError(
|
|
2157
|
+
"invalid_request",
|
|
2158
|
+
"Secret keys cannot be sent from a browser"
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
for (let attempt = 0; ; attempt++) {
|
|
2162
|
+
throwIfAborted(ctx.signal);
|
|
2163
|
+
let response;
|
|
2164
|
+
try {
|
|
2165
|
+
response = await this.fetcher(this.baseUrl + path, {
|
|
2166
|
+
method: body ? "POST" : "GET",
|
|
2167
|
+
...body ? { body: JSON.stringify(body) } : {},
|
|
2168
|
+
// Workers reject redirect: "error"; a redirect is still refused below
|
|
2169
|
+
// as a non-OK response (a 3xx, or opaqueredirect in browsers).
|
|
2170
|
+
redirect: "manual",
|
|
2171
|
+
headers: {
|
|
2172
|
+
...body ? { "Content-Type": "application/json" } : {},
|
|
2173
|
+
...ctx.idempotencyKey ? { "Idempotency-Key": ctx.idempotencyKey } : {},
|
|
2174
|
+
Authorization: `Bearer ${ctx.key}`,
|
|
2175
|
+
"Zappie-Version": ctx.version,
|
|
2176
|
+
...etag ? { "If-None-Match": etag } : {}
|
|
2177
|
+
},
|
|
2178
|
+
...ctx.signal ? { signal: ctx.signal } : {}
|
|
2179
|
+
});
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
throwIfAborted(ctx.signal);
|
|
2182
|
+
if (attempt >= 2) {
|
|
2183
|
+
throw new ZappieError(
|
|
2184
|
+
"network_error",
|
|
2185
|
+
"Could not reach the storefront service",
|
|
2186
|
+
{ cause: error }
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
await delay(100 * 2 ** attempt + Math.random() * 100, ctx.signal);
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
if (response.ok || response.status === 304) {
|
|
2193
|
+
return response;
|
|
2194
|
+
}
|
|
2195
|
+
const retry = response.headers.get("Retry-After");
|
|
2196
|
+
const seconds = retry !== null ? Number(retry) : NaN;
|
|
2197
|
+
const wait2 = retry !== null ? Number.isFinite(seconds) ? Math.max(0, seconds * 1e3) : Math.max(0, Date.parse(retry) - Date.now()) : NaN;
|
|
2198
|
+
const tooLong = Number.isFinite(wait2) && wait2 > MAX_RETRY_WAIT_MS;
|
|
2199
|
+
if (attempt < 2 && !tooLong && (response.status === 429 || response.status >= 500)) {
|
|
2200
|
+
await response.body?.cancel();
|
|
2201
|
+
await delay(
|
|
2202
|
+
Number.isFinite(wait2) ? wait2 : 100 * 2 ** attempt + Math.random() * 100,
|
|
2203
|
+
ctx.signal
|
|
2204
|
+
);
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
2208
|
+
throw wireError(
|
|
2209
|
+
errorBody,
|
|
2210
|
+
response.status,
|
|
2211
|
+
response.headers.get("Zappie-Request-Id") ?? void 0
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
async json(response) {
|
|
2216
|
+
try {
|
|
2217
|
+
return await response.json();
|
|
2218
|
+
} catch (cause) {
|
|
2219
|
+
if (cause instanceof Error && cause.name === "AbortError") {
|
|
2220
|
+
throw cause;
|
|
2221
|
+
}
|
|
2222
|
+
throw new ZappieError(
|
|
2223
|
+
"server_error",
|
|
2224
|
+
"The storefront service returned invalid JSON",
|
|
2225
|
+
{ status: response.status, cause }
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
async quote(ctx, body) {
|
|
2230
|
+
body = (await import("./quote-body-S5LELT7L.js")).quoteBody(body);
|
|
2231
|
+
const request = {
|
|
2232
|
+
...ctx,
|
|
2233
|
+
idempotencyKey: ctx.idempotencyKey ?? await payloadKey(JSON.stringify(body))
|
|
2234
|
+
};
|
|
2235
|
+
const raw = await this.json(
|
|
2236
|
+
await this.request("/quote", request, void 0, body)
|
|
2237
|
+
);
|
|
2238
|
+
return raw;
|
|
2239
|
+
}
|
|
2240
|
+
async getStore(ctx) {
|
|
2241
|
+
const mapper = import("./store-OPM3OFLC.js").catch((cause) => {
|
|
2242
|
+
throw new ZappieError(
|
|
2243
|
+
"server_error",
|
|
2244
|
+
"Could not load the storefront store mapper",
|
|
2245
|
+
{ cause }
|
|
2246
|
+
);
|
|
2247
|
+
});
|
|
2248
|
+
const presentation = import("./presentation-TKOXOILG.js");
|
|
2249
|
+
const response = this.request("/store", ctx).then(
|
|
2250
|
+
(value) => this.json(value)
|
|
2251
|
+
);
|
|
2252
|
+
const [{ mapStore }, { presentStore }, raw] = await Promise.all([
|
|
2253
|
+
mapper,
|
|
2254
|
+
presentation,
|
|
2255
|
+
response
|
|
2256
|
+
]);
|
|
2257
|
+
const store = mapStore(
|
|
2258
|
+
raw,
|
|
2259
|
+
record,
|
|
2260
|
+
text,
|
|
2261
|
+
list,
|
|
2262
|
+
hours,
|
|
2263
|
+
mapMoney,
|
|
2264
|
+
money,
|
|
2265
|
+
opt,
|
|
2266
|
+
isString,
|
|
2267
|
+
isBoolean,
|
|
2268
|
+
mapSeo
|
|
2269
|
+
);
|
|
2270
|
+
return presentStore(store, raw, presentationError);
|
|
2271
|
+
}
|
|
2272
|
+
async getMenu(ctx) {
|
|
2273
|
+
const presentation = import("./presentation-TKOXOILG.js");
|
|
2274
|
+
let cursor;
|
|
2275
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2276
|
+
let output;
|
|
2277
|
+
let etag;
|
|
2278
|
+
do {
|
|
2279
|
+
const query = new URLSearchParams({
|
|
2280
|
+
branch: ctx.branchId,
|
|
2281
|
+
...ctx.fulfilment ? { fulfilment: wireFulfilment2(ctx.fulfilment) } : {},
|
|
2282
|
+
...ctx.metadataOnly ? { metadata_only: "true" } : {},
|
|
2283
|
+
...cursor ? { cursor } : {}
|
|
2284
|
+
});
|
|
2285
|
+
const response = await this.request(
|
|
2286
|
+
`/menus?${query}`,
|
|
2287
|
+
ctx,
|
|
2288
|
+
cursor ? void 0 : ctx.ifNoneMatch
|
|
2289
|
+
);
|
|
2290
|
+
if (response.status === 304) {
|
|
2291
|
+
if (cursor) {
|
|
2292
|
+
throw new ZappieError(
|
|
2293
|
+
"server_error",
|
|
2294
|
+
"Unexpected 304 on a continuation page"
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
return {
|
|
2298
|
+
notModified: true,
|
|
2299
|
+
...response.headers.get("ETag") ? { etag: response.headers.get("ETag") } : {}
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
const raw = record(await this.json(response));
|
|
2303
|
+
const page = (await presentation).presentMenu(
|
|
2304
|
+
mapMenu(raw, ctx.branchId),
|
|
2305
|
+
raw,
|
|
2306
|
+
presentationError
|
|
2307
|
+
);
|
|
2308
|
+
if (ctx.metadataOnly) {
|
|
2309
|
+
if (Object.keys(page.items).length || raw.next_cursor) {
|
|
2310
|
+
throw new ZappieError("server_error", "Invalid metadata response");
|
|
2311
|
+
}
|
|
2312
|
+
return { menu: { ...page, completeness: "partial" } };
|
|
2313
|
+
}
|
|
2314
|
+
if (!output) {
|
|
2315
|
+
output = page;
|
|
2316
|
+
etag = response.headers.get("ETag") ?? void 0;
|
|
2317
|
+
} else {
|
|
2318
|
+
if (page.version !== output.version) {
|
|
2319
|
+
throw new ZappieError(
|
|
2320
|
+
"server_error",
|
|
2321
|
+
"Menu changed while fetching pages; retry the read"
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
Object.assign(output.items, page.items);
|
|
2325
|
+
for (const category of page.categories) {
|
|
2326
|
+
const existing = output.categories.find((c) => c.id === category.id);
|
|
2327
|
+
if (existing) {
|
|
2328
|
+
existing.itemIds = [
|
|
2329
|
+
.../* @__PURE__ */ new Set([...existing.itemIds, ...category.itemIds])
|
|
2330
|
+
];
|
|
2331
|
+
} else {
|
|
2332
|
+
output.categories.push(category);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
for (const section of page.menus ?? []) {
|
|
2336
|
+
const existing = output.menus?.find((m) => m.id === section.id);
|
|
2337
|
+
if (existing) {
|
|
2338
|
+
existing.categoryIds = [
|
|
2339
|
+
.../* @__PURE__ */ new Set([...existing.categoryIds, ...section.categoryIds])
|
|
2340
|
+
];
|
|
2341
|
+
} else {
|
|
2342
|
+
output.menus?.push(section);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
cursor = typeof raw.next_cursor === "string" && raw.next_cursor ? raw.next_cursor : void 0;
|
|
2347
|
+
if (cursor) {
|
|
2348
|
+
if (seen.has(cursor)) {
|
|
2349
|
+
throw new ZappieError(
|
|
2350
|
+
"server_error",
|
|
2351
|
+
"Repeated menu pagination cursor"
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
seen.add(cursor);
|
|
2355
|
+
}
|
|
2356
|
+
} while (cursor);
|
|
2357
|
+
if (output.menus && output.menus.length > 1) {
|
|
2358
|
+
output.id = `branch:${ctx.branchId}`;
|
|
2359
|
+
output.name = "Catalogue";
|
|
2360
|
+
delete output.schedule;
|
|
2361
|
+
}
|
|
2362
|
+
for (const category of output.categories) {
|
|
2363
|
+
for (const id of category.itemIds) {
|
|
2364
|
+
if (!Object.prototype.hasOwnProperty.call(output.items, id)) {
|
|
2365
|
+
throw new ZappieError(
|
|
2366
|
+
"server_error",
|
|
2367
|
+
"Category refers to a missing item"
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
if (Object.keys(output.items).length > 2e3) {
|
|
2373
|
+
console.warn("Zappie menu contains more than 2,000 items");
|
|
2374
|
+
}
|
|
2375
|
+
return { menu: output, ...etag ? { etag } : {} };
|
|
2376
|
+
}
|
|
2377
|
+
async getSelectedMenu(ctx) {
|
|
2378
|
+
return (await import("./selected-menu-AQHWDELR.js")).selectedMenu(
|
|
2379
|
+
ctx,
|
|
2380
|
+
(path) => this.request(path, ctx, ctx.ifNoneMatch),
|
|
2381
|
+
(response) => this.json(response),
|
|
2382
|
+
mapMenu,
|
|
2383
|
+
() => new ZappieError("server_error", "Invalid selected menu")
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
async search(ctx) {
|
|
2387
|
+
return (await import("./search-OF62VKI3.js")).searchPage(
|
|
2388
|
+
ctx,
|
|
2389
|
+
async (path) => this.json(await this.request(path, ctx)),
|
|
2390
|
+
mapMenu,
|
|
2391
|
+
() => new ZappieError("server_error", "Invalid search page")
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
async getCategoryPage(ctx) {
|
|
2395
|
+
const query = new URLSearchParams({
|
|
2396
|
+
branch: ctx.branchId,
|
|
2397
|
+
...ctx.cursor ? { cursor: ctx.cursor } : {}
|
|
2398
|
+
});
|
|
2399
|
+
const raw = await this.json(
|
|
2400
|
+
await this.request(
|
|
2401
|
+
`/menus/${encodeURIComponent(ctx.menuId)}/categories/${encodeURIComponent(ctx.categoryId)}/items?${query}`,
|
|
2402
|
+
ctx
|
|
2403
|
+
)
|
|
2404
|
+
);
|
|
2405
|
+
return (await import("./category-page-I3HDIAJA.js")).categoryPage(
|
|
2406
|
+
record(raw),
|
|
2407
|
+
ctx,
|
|
2408
|
+
mapMenu,
|
|
2409
|
+
() => new ZappieError("server_error", "Invalid category page")
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
async getMenuVersion(ctx) {
|
|
2413
|
+
const value = record(
|
|
2414
|
+
await this.json(
|
|
2415
|
+
await this.request(
|
|
2416
|
+
`/menus/version?${new URLSearchParams({
|
|
2417
|
+
branch: ctx.branchId,
|
|
2418
|
+
...ctx.fulfilment ? { fulfilment: wireFulfilment2(ctx.fulfilment) } : {}
|
|
2419
|
+
})}`,
|
|
2420
|
+
ctx
|
|
2421
|
+
)
|
|
2422
|
+
)
|
|
2423
|
+
);
|
|
2424
|
+
if (typeof value.menu_version !== "string" || typeof value.availability_hash !== "string") {
|
|
2425
|
+
throw new ZappieError("server_error", "Invalid menu version response");
|
|
2426
|
+
}
|
|
2427
|
+
return {
|
|
2428
|
+
version: value.menu_version,
|
|
2429
|
+
availabilityHash: value.availability_hash
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
async getItem(ctx) {
|
|
2433
|
+
const item = mapItem(
|
|
2434
|
+
await this.json(
|
|
2435
|
+
await this.request(
|
|
2436
|
+
`/items/${encodeURIComponent(ctx.itemId)}?${new URLSearchParams({ branch: ctx.branchId })}`,
|
|
2437
|
+
ctx
|
|
2438
|
+
)
|
|
2439
|
+
)
|
|
2440
|
+
);
|
|
2441
|
+
if (item.id !== ctx.itemId) {
|
|
2442
|
+
throw new ZappieError(
|
|
2443
|
+
"server_error",
|
|
2444
|
+
"Item response identity does not match the request"
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
return item;
|
|
2448
|
+
}
|
|
2449
|
+
};
|
|
2450
|
+
|
|
2451
|
+
// ../sdk/src/client.ts
|
|
2452
|
+
function buildClient(input, secret = false) {
|
|
2453
|
+
const mode = validateConfig(input, secret);
|
|
2454
|
+
const config = Object.freeze({
|
|
2455
|
+
...input,
|
|
2456
|
+
version: input.version ?? API_VERSION,
|
|
2457
|
+
telemetry: input.telemetry ?? true
|
|
2458
|
+
});
|
|
2459
|
+
const transport = input.transport ?? new RestTransport({
|
|
2460
|
+
...input.baseUrl ? { baseUrl: input.baseUrl } : {},
|
|
2461
|
+
...input.fetch ? { fetch: input.fetch } : {}
|
|
2462
|
+
});
|
|
2463
|
+
const events = createEmitter();
|
|
2464
|
+
const cache = new Cache((error) => events.emit("error", { error }));
|
|
2465
|
+
const ctx = (signal) => ({
|
|
2466
|
+
key: config.key,
|
|
2467
|
+
version: config.version,
|
|
2468
|
+
...signal ? { signal } : {}
|
|
2469
|
+
});
|
|
2470
|
+
const store = {
|
|
2471
|
+
get: (opts = {}) => cache.get(
|
|
2472
|
+
"store",
|
|
2473
|
+
async (signal) => ({ value: await transport.getStore(ctx(signal)) }),
|
|
2474
|
+
opts.signal
|
|
2475
|
+
),
|
|
2476
|
+
peek: () => cache.snapshot("store").value
|
|
2477
|
+
};
|
|
2478
|
+
const resolveBranch = (value, requested) => {
|
|
2479
|
+
const selected = requested ?? config.branch;
|
|
2480
|
+
const branch = selected ? value.branches.find((b) => b.id === selected || b.slug === selected) : value.branches.find((b) => b.isDefault) ?? value.branches[0];
|
|
2481
|
+
if (!branch) {
|
|
2482
|
+
throw new ZappieError(
|
|
2483
|
+
"branch_not_found",
|
|
2484
|
+
"No matching storefront branch",
|
|
2485
|
+
{ param: "branch" }
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
return branch;
|
|
2489
|
+
};
|
|
2490
|
+
let hydratedBranch;
|
|
2491
|
+
const branchKey = (requested) => {
|
|
2492
|
+
const value = store.peek();
|
|
2493
|
+
if (value) {
|
|
2494
|
+
return resolveBranch(value, requested).id;
|
|
2495
|
+
}
|
|
2496
|
+
const selected = requested ?? config.branch;
|
|
2497
|
+
if (hydratedBranch && (selected === void 0 || selected === config.branch || selected === hydratedBranch)) {
|
|
2498
|
+
return hydratedBranch;
|
|
2499
|
+
}
|
|
2500
|
+
return selected ?? "default";
|
|
2501
|
+
};
|
|
2502
|
+
const menuKey = (requested) => `menu:${branchKey(requested)}`;
|
|
2503
|
+
const menu = {
|
|
2504
|
+
async search(query, opts = {}) {
|
|
2505
|
+
if (!transport.search) {
|
|
2506
|
+
throw new ZappieError(
|
|
2507
|
+
"invalid_request",
|
|
2508
|
+
"Transport does not support search"
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
const branch = resolveBranch(await store.get(opts), opts.branch);
|
|
2512
|
+
return transport.search({
|
|
2513
|
+
...ctx(opts.signal),
|
|
2514
|
+
branchId: branch.id,
|
|
2515
|
+
query,
|
|
2516
|
+
...opts.cursor ? { cursor: opts.cursor } : {}
|
|
2517
|
+
});
|
|
2518
|
+
},
|
|
2519
|
+
async get(opts = {}) {
|
|
2520
|
+
const branch = resolveBranch(await store.get(opts), opts.branch);
|
|
2521
|
+
return cache.get(
|
|
2522
|
+
`${menuKey(branch.id)}${opts.fulfilment ? `:${opts.fulfilment}` : ""}${opts.metadataOnly ? ":partial" : ""}`,
|
|
2523
|
+
async (signal, entry) => {
|
|
2524
|
+
const response = await transport.getMenu({
|
|
2525
|
+
...ctx(signal),
|
|
2526
|
+
branchId: branch.id,
|
|
2527
|
+
fulfilment: opts.fulfilment,
|
|
2528
|
+
...opts.metadataOnly ? { metadataOnly: true } : {},
|
|
2529
|
+
...entry.etag ? { ifNoneMatch: entry.etag } : {}
|
|
2530
|
+
});
|
|
2531
|
+
if ("notModified" in response) {
|
|
2532
|
+
if (!entry.value) {
|
|
2533
|
+
throw new ZappieError(
|
|
2534
|
+
"server_error",
|
|
2535
|
+
"Received 304 without a cached menu"
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
return {
|
|
2539
|
+
value: entry.value,
|
|
2540
|
+
...response.etag ?? entry.etag ? { etag: response.etag ?? entry.etag } : {}
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
if (!opts.metadataOnly && response.menu.completeness === "partial") {
|
|
2544
|
+
throw new ZappieError(
|
|
2545
|
+
"server_error",
|
|
2546
|
+
"Partial catalogue cannot satisfy a complete read"
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
if (response.menu.branchId !== branch.id) {
|
|
2550
|
+
throw new ZappieError(
|
|
2551
|
+
"server_error",
|
|
2552
|
+
"Menu belongs to a different branch"
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
return {
|
|
2556
|
+
value: response.menu,
|
|
2557
|
+
etag: response.etag ?? response.menu.version,
|
|
2558
|
+
onStored: () => {
|
|
2559
|
+
if (entry.value && entry.value.version !== response.menu.version) {
|
|
2560
|
+
events.emit("menu:updated", {
|
|
2561
|
+
branchId: branch.id,
|
|
2562
|
+
version: response.menu.version
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
},
|
|
2568
|
+
opts.signal
|
|
2569
|
+
);
|
|
2570
|
+
},
|
|
2571
|
+
peek: (branch) => cache.snapshot(menuKey(branch)).value ?? cache.snapshot(`${menuKey(branch)}:partial`).value,
|
|
2572
|
+
async page(menuId, categoryId, opts = {}) {
|
|
2573
|
+
if (!transport.getCategoryPage) {
|
|
2574
|
+
throw new ZappieError(
|
|
2575
|
+
"invalid_request",
|
|
2576
|
+
"Transport does not support category pages"
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
const branch = resolveBranch(await store.get(opts), opts.branch);
|
|
2580
|
+
return transport.getCategoryPage({
|
|
2581
|
+
...ctx(opts.signal),
|
|
2582
|
+
branchId: branch.id,
|
|
2583
|
+
menuId,
|
|
2584
|
+
categoryId,
|
|
2585
|
+
...opts.cursor ? { cursor: opts.cursor } : {}
|
|
2586
|
+
});
|
|
2587
|
+
},
|
|
2588
|
+
async version(opts = {}) {
|
|
2589
|
+
const branch = resolveBranch(await store.get(opts), opts.branch);
|
|
2590
|
+
return transport.getMenuVersion({
|
|
2591
|
+
...ctx(opts.signal),
|
|
2592
|
+
branchId: branch.id,
|
|
2593
|
+
fulfilment: opts.fulfilment
|
|
2594
|
+
});
|
|
2595
|
+
},
|
|
2596
|
+
async item(id, opts = {}) {
|
|
2597
|
+
const branch = resolveBranch(await store.get(opts), opts.branch);
|
|
2598
|
+
if (!menu.peek(branch.id) || menu.peek(branch.id)?.completeness === "partial") {
|
|
2599
|
+
await menu.get({ ...opts, metadataOnly: true });
|
|
2600
|
+
}
|
|
2601
|
+
return readItem(id, branch.id, false, opts.signal);
|
|
2602
|
+
},
|
|
2603
|
+
hydrate(value) {
|
|
2604
|
+
hydratedBranch ??= value.branchId;
|
|
2605
|
+
const previous = cache.snapshot(`menu:${value.branchId}`).value;
|
|
2606
|
+
cache.set(
|
|
2607
|
+
`menu:${value.branchId}${value.completeness === "partial" ? ":partial" : ""}`,
|
|
2608
|
+
value,
|
|
2609
|
+
value.version
|
|
2610
|
+
);
|
|
2611
|
+
if (previous && previous.version !== value.version) {
|
|
2612
|
+
events.emit("menu:updated", {
|
|
2613
|
+
branchId: value.branchId,
|
|
2614
|
+
version: value.version
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
const itemValues = /* @__PURE__ */ new Map();
|
|
2620
|
+
const peekItem = (id, branch) => itemValues.get(`${branch}:${id}`);
|
|
2621
|
+
const readItem = async (id, branch, fresh = false, signal) => {
|
|
2622
|
+
const complete = cache.snapshot(`menu:${branch}`).value;
|
|
2623
|
+
if (!fresh && complete && Object.hasOwn(complete.items, id)) {
|
|
2624
|
+
return complete.items[id];
|
|
2625
|
+
}
|
|
2626
|
+
const load = (requestSignal = signal) => transport.getItem({
|
|
2627
|
+
...ctx(requestSignal),
|
|
2628
|
+
branchId: branch,
|
|
2629
|
+
itemId: id
|
|
2630
|
+
});
|
|
2631
|
+
try {
|
|
2632
|
+
const value = fresh ? await load() : await cache.get(
|
|
2633
|
+
`item:${branch}:${id}`,
|
|
2634
|
+
async (requestSignal) => ({ value: await load(requestSignal) }),
|
|
2635
|
+
signal
|
|
2636
|
+
);
|
|
2637
|
+
if (fresh) {
|
|
2638
|
+
cache.set(`item:${branch}:${id}`, value);
|
|
2639
|
+
}
|
|
2640
|
+
itemValues.set(`${branch}:${id}`, value);
|
|
2641
|
+
return value;
|
|
2642
|
+
} catch (error) {
|
|
2643
|
+
if (error.code === "item_not_found") {
|
|
2644
|
+
itemValues.delete(`${branch}:${id}`);
|
|
2645
|
+
cache.set(`item:${branch}:${id}`, void 0);
|
|
2646
|
+
}
|
|
2647
|
+
throw error;
|
|
2648
|
+
}
|
|
2649
|
+
};
|
|
2650
|
+
const workingSet = createWorkingSet({
|
|
2651
|
+
menu,
|
|
2652
|
+
store,
|
|
2653
|
+
transport,
|
|
2654
|
+
context: ctx,
|
|
2655
|
+
peekItem,
|
|
2656
|
+
readItem
|
|
2657
|
+
});
|
|
2658
|
+
const { cart, setLegacyReader, cartCatalogue } = createCart({
|
|
2659
|
+
workingSet,
|
|
2660
|
+
config,
|
|
2661
|
+
menu,
|
|
2662
|
+
store,
|
|
2663
|
+
transport,
|
|
2664
|
+
context: ctx,
|
|
2665
|
+
events
|
|
2666
|
+
});
|
|
2667
|
+
return Object.freeze({
|
|
2668
|
+
cart,
|
|
2669
|
+
config,
|
|
2670
|
+
mode,
|
|
2671
|
+
store,
|
|
2672
|
+
menu,
|
|
2673
|
+
format,
|
|
2674
|
+
on: events.on,
|
|
2675
|
+
_internal: {
|
|
2676
|
+
cartCatalogue,
|
|
2677
|
+
setLegacyReader,
|
|
2678
|
+
transport,
|
|
2679
|
+
cache,
|
|
2680
|
+
emit: events.emit,
|
|
2681
|
+
menuKey,
|
|
2682
|
+
hydrateStore: (value) => cache.set("store", value)
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
function createZappie(config) {
|
|
2687
|
+
const client = buildClient(config);
|
|
2688
|
+
if (client.mode === "test" && typeof window !== "undefined") {
|
|
2689
|
+
void import("./debug-LOXPIQRS.js").then(({ installDebug }) => installDebug(client));
|
|
2690
|
+
}
|
|
2691
|
+
return client;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// ../sdk/src/cart/preview.ts
|
|
2695
|
+
function previewQuantity(value) {
|
|
2696
|
+
return Number.isInteger(value) && value >= 0 && value <= 99 ? value : 0;
|
|
2697
|
+
}
|
|
2698
|
+
function previewItem(item, raw) {
|
|
2699
|
+
const input = normalizeInput(item, raw);
|
|
2700
|
+
const groups = effectiveGroups(item, input).filter(
|
|
2701
|
+
(group) => group.isAvailable !== false
|
|
2702
|
+
);
|
|
2703
|
+
const variantId = resolveVariant(item, input)?.id;
|
|
2704
|
+
const choices = (input.choices ?? []).filter(
|
|
2705
|
+
({ groupId, choiceId }) => groups.some(
|
|
2706
|
+
(group) => group.id === groupId && group.choices.some((choice) => choice.id === choiceId)
|
|
2707
|
+
)
|
|
2708
|
+
).map((selection) => ({
|
|
2709
|
+
...selection,
|
|
2710
|
+
quantity: previewQuantity(selection.quantity)
|
|
2711
|
+
}));
|
|
2712
|
+
const unitPrice = estimateUnitPrice(item, {
|
|
2713
|
+
choices,
|
|
2714
|
+
...variantId ? { variantId } : {}
|
|
2715
|
+
});
|
|
2716
|
+
const total = multiplyMoney(unitPrice, previewQuantity(input.quantity));
|
|
2717
|
+
return { groups, unitPrice, total };
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// ../sdk/src/internal/customer-http.ts
|
|
2721
|
+
function stableErrorCode(status) {
|
|
2722
|
+
if (status === 400 || status === 422) {
|
|
2723
|
+
return "invalid_request";
|
|
2724
|
+
}
|
|
2725
|
+
if (status === 401) {
|
|
2726
|
+
return "unauthorized";
|
|
2727
|
+
}
|
|
2728
|
+
if (status === 404) {
|
|
2729
|
+
return "not_found";
|
|
2730
|
+
}
|
|
2731
|
+
if (status === 429) {
|
|
2732
|
+
return "rate_limited";
|
|
2733
|
+
}
|
|
2734
|
+
return "server_error";
|
|
2735
|
+
}
|
|
2736
|
+
async function apiError(response, fallback) {
|
|
2737
|
+
const body = await response.json().catch(() => ({}));
|
|
2738
|
+
const envelope = body && typeof body === "object" ? body : {};
|
|
2739
|
+
const value = "error" in envelope && envelope.error && typeof envelope.error === "object" ? envelope.error : {};
|
|
2740
|
+
return {
|
|
2741
|
+
apiCode: "code" in value && typeof value.code === "string" ? value.code : "server_error",
|
|
2742
|
+
message: "message" in value && typeof value.message === "string" ? value.message : fallback,
|
|
2743
|
+
..."param" in value && typeof value.param === "string" ? { param: value.param } : {},
|
|
2744
|
+
..."issues" in value ? { issues: value.issues } : {}
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function wait(ms, signal) {
|
|
2748
|
+
signal?.throwIfAborted();
|
|
2749
|
+
return new Promise((resolve, reject) => {
|
|
2750
|
+
const done = () => {
|
|
2751
|
+
signal?.removeEventListener("abort", aborted);
|
|
2752
|
+
resolve();
|
|
2753
|
+
};
|
|
2754
|
+
const timer = setTimeout(done, ms);
|
|
2755
|
+
const aborted = () => {
|
|
2756
|
+
clearTimeout(timer);
|
|
2757
|
+
signal?.removeEventListener("abort", aborted);
|
|
2758
|
+
reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
2759
|
+
};
|
|
2760
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
async function requestWithRetry(fetcher, input, init, message) {
|
|
2764
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2765
|
+
try {
|
|
2766
|
+
return await fetcher(input, {
|
|
2767
|
+
...init,
|
|
2768
|
+
credentials: "same-origin",
|
|
2769
|
+
headers: { Accept: "application/json", ...init.headers }
|
|
2770
|
+
});
|
|
2771
|
+
} catch (cause) {
|
|
2772
|
+
if (init.signal?.aborted) {
|
|
2773
|
+
throw init.signal.reason ?? cause;
|
|
2774
|
+
}
|
|
2775
|
+
if (attempt === 1) {
|
|
2776
|
+
throw new ZappieError("network_error", message, {
|
|
2777
|
+
cause,
|
|
2778
|
+
retryable: true
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
await wait(400, init.signal ?? void 0);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
throw new ZappieError("network_error", message, { retryable: true });
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
export {
|
|
2788
|
+
ZappieError,
|
|
2789
|
+
wireError,
|
|
2790
|
+
money,
|
|
2791
|
+
format,
|
|
2792
|
+
sumMoney,
|
|
2793
|
+
resolveSourceMenu,
|
|
2794
|
+
validateLine,
|
|
2795
|
+
stableErrorCode,
|
|
2796
|
+
apiError,
|
|
2797
|
+
requestWithRetry,
|
|
2798
|
+
payloadKey,
|
|
2799
|
+
record,
|
|
2800
|
+
mapMoney,
|
|
2801
|
+
API_VERSION,
|
|
2802
|
+
validateConfig,
|
|
2803
|
+
RestTransport,
|
|
2804
|
+
createZappie,
|
|
2805
|
+
previewItem
|
|
2806
|
+
};
|
|
2807
|
+
//# sourceMappingURL=chunk-47RIF6JW.js.map
|