@zappie/verify 0.0.0-stage → 0.1.1

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 ADDED
@@ -0,0 +1,11 @@
1
+ # @zappie/verify
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 38f23ac: `VERSION` (`@zappie/sdk`) and `FOUNDATIONS_VERSION` (`@zappie/foundations`) report the installed version. In 0.1.0 they said `"0.0.0"`. Verify reports now carry the real `verifyVersion`, and the test-mode debug channel the real SDK version.
8
+ - eeccfed: First release on npm. `zappie check` and `zappie verify` now work in any project that installs these as dev dependencies. `@zappie/verify` takes `@playwright/test` as a peer dependency.
9
+ - Updated dependencies [38f23ac]
10
+ - Updated dependencies [1e6f420]
11
+ - @zappie/sdk@0.1.1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zappie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,56 @@
1
- # Temporary Holding Version
1
+ # @zappie/verify
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The checks behind `zappie verify`. They open your storefront in a real browser and walk the journeys a shopper takes: the store loads, the menu matches Zappie's, an item goes in the cart, totals match Zappie's quote, issues can be fixed and checkout is ready. Each failure comes with a hint about what to fix.
4
+
5
+ ```sh
6
+ npm install --save-dev @zappie/verify @playwright/test
7
+ npx playwright install chromium
8
+ ```
9
+
10
+ `@playwright/test` is a peer dependency, so you choose its version and install its browser once.
11
+
12
+ ## Use it
13
+
14
+ Run it against your built project, or against a URL, with a publishable test key:
15
+
16
+ ```sh
17
+ ZAPPIE_KEY=pk_test_... npx zappie verify
18
+ ZAPPIE_KEY=pk_test_... npx zappie verify https://your-store.example
19
+ ```
20
+
21
+ With no URL, it builds your project, starts a local preview, runs the checks and stops the preview. It only accepts test keys, so it never touches live orders.
22
+
23
+ | Option | What it does |
24
+ | -------------------------------- | ---------------------------------------------------- |
25
+ | `--suite standard` or `extended` | The standard checks, or those plus the extended ones |
26
+ | `--report <file>` | Save the full report as JSON |
27
+ | `--json` | Print the report as JSON |
28
+
29
+ ## From code
30
+
31
+ ```ts
32
+ import { printSummary, verifyProject } from "@zappie/verify";
33
+
34
+ const report = await verifyProject({
35
+ cwd: process.cwd(),
36
+ key: process.env.ZAPPIE_KEY ?? "",
37
+ suite: "standard",
38
+ packageManager: "npm",
39
+ });
40
+ console.log(printSummary(report));
41
+ ```
42
+
43
+ The report's shape is the JSON Schema at `@zappie/verify/schema/verify-report.v1.json`.
44
+
45
+ ## Docs
46
+
47
+ - [CLI: zappie verify](https://docs.zappie.ai/cli/verify)
48
+ - [Verify checks](https://docs.zappie.ai/verify)
49
+
50
+ ## Status
51
+
52
+ `@zappie/verify` is at `0.x`. Its checks and report can change in a minor release while it settles. Each change is listed in `CHANGELOG.md`.
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,21 @@
1
+ // ../sdk/src/transport/category-page.ts
2
+ function categoryPage(raw, ctx, mapMenu, invalid) {
3
+ const menu = mapMenu(raw, ctx.branchId);
4
+ if (raw.next_cursor != null && (typeof raw.next_cursor !== "string" || !raw.next_cursor || raw.next_cursor === ctx.cursor) || menu.categories.some((c) => c.id !== ctx.categoryId) || Object.keys(menu.items).some(
5
+ (id) => !menu.categories.some((c) => c.itemIds.includes(id))
6
+ ) || !menu.menus?.some(
7
+ (m) => m.id === ctx.menuId && m.categoryIds.includes(ctx.categoryId)
8
+ )) {
9
+ throw invalid();
10
+ }
11
+ return {
12
+ menu: { ...menu, completeness: "partial" },
13
+ menuId: ctx.menuId,
14
+ categoryId: ctx.categoryId,
15
+ ...typeof raw.next_cursor === "string" && raw.next_cursor ? { nextCursor: raw.next_cursor } : {}
16
+ };
17
+ }
18
+ export {
19
+ categoryPage
20
+ };
21
+ //# sourceMappingURL=category-page-UBHOB5IN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../sdk/src/transport/category-page.ts"],"sourcesContent":["import type { CategoryPage, Menu } from \"../models\";\n\n/**\n * Validates and maps one category page. Only progressive catalogues read\n * pages, so this loads on the first `menu.page()` call and stays out of the\n * core bundle, like the quote client (PRD 03 §4). The mapper and error come\n * in as arguments: importing them would split the core's shared code into a\n * separate chunk, which costs the core more than this module saves.\n */\nexport function categoryPage(\n raw: Record<string, unknown>,\n ctx: {\n branchId: string;\n menuId: string;\n categoryId: string;\n cursor?: string;\n },\n mapMenu: (value: unknown, branchId: string) => Menu,\n invalid: () => Error,\n): CategoryPage {\n const menu = mapMenu(raw, ctx.branchId);\n if (\n (raw.next_cursor != null &&\n (typeof raw.next_cursor !== \"string\" ||\n !raw.next_cursor ||\n raw.next_cursor === ctx.cursor)) ||\n menu.categories.some((c) => c.id !== ctx.categoryId) ||\n Object.keys(menu.items).some(\n (id) => !menu.categories.some((c) => c.itemIds.includes(id)),\n ) ||\n !menu.menus?.some(\n (m) => m.id === ctx.menuId && m.categoryIds.includes(ctx.categoryId),\n )\n ) {\n throw invalid();\n }\n return {\n menu: { ...menu, completeness: \"partial\" },\n menuId: ctx.menuId,\n categoryId: ctx.categoryId,\n ...(typeof raw.next_cursor === \"string\" && raw.next_cursor\n ? { nextCursor: raw.next_cursor }\n : {}),\n };\n}\n"],"mappings":";AASO,SAAS,aACd,KACA,KAMA,SACA,SACc;AACd,QAAM,OAAO,QAAQ,KAAK,IAAI,QAAQ;AACtC,MACG,IAAI,eAAe,SACjB,OAAO,IAAI,gBAAgB,YAC1B,CAAC,IAAI,eACL,IAAI,gBAAgB,IAAI,WAC5B,KAAK,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,IAAI,UAAU,KACnD,OAAO,KAAK,KAAK,KAAK,EAAE;AAAA,IACtB,CAAC,OAAO,CAAC,KAAK,WAAW,KAAK,CAAC,MAAM,EAAE,QAAQ,SAAS,EAAE,CAAC;AAAA,EAC7D,KACA,CAAC,KAAK,OAAO;AAAA,IACX,CAAC,MAAM,EAAE,OAAO,IAAI,UAAU,EAAE,YAAY,SAAS,IAAI,UAAU;AAAA,EACrE,GACA;AACA,UAAM,QAAQ;AAAA,EAChB;AACA,SAAO;AAAA,IACL,MAAM,EAAE,GAAG,MAAM,cAAc,UAAU;AAAA,IACzC,QAAQ,IAAI;AAAA,IACZ,YAAY,IAAI;AAAA,IAChB,GAAI,OAAO,IAAI,gBAAgB,YAAY,IAAI,cAC3C,EAAE,YAAY,IAAI,YAAY,IAC9B,CAAC;AAAA,EACP;AACF;","names":[]}
@@ -0,0 +1,485 @@
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/internal/digest.ts
65
+ async function payloadKey(payload) {
66
+ const digest = await globalThis.crypto.subtle.digest(
67
+ "SHA-256",
68
+ new TextEncoder().encode(payload)
69
+ );
70
+ return Array.from(
71
+ new Uint8Array(digest),
72
+ (byte) => byte.toString(16).padStart(2, "0")
73
+ ).join("");
74
+ }
75
+
76
+ // ../sdk/src/money.ts
77
+ 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 ";
78
+ var THREE_DIGIT_CURRENCIES = " BHD JOD KWD LYD OMR TND ";
79
+ var FOUR_DIGIT_CURRENCIES = " CLF UYW ";
80
+ var API_ONLY_CURRENCIES = " BOV CHE CHW CLF COU MXV USN UYI UYW VED ";
81
+ var contains = (codes2, currency) => codes2.includes(` ${currency} `);
82
+ var intlCurrencies;
83
+ function isCurrency(code) {
84
+ if (!/^[A-Z]{3}$/.test(code)) {
85
+ return false;
86
+ }
87
+ if (typeof Intl.supportedValuesOf !== "function") {
88
+ return true;
89
+ }
90
+ intlCurrencies ??= new Set(Intl.supportedValuesOf("currency"));
91
+ return contains(API_ONLY_CURRENCIES, code) || intlCurrencies.has(code);
92
+ }
93
+ function currencyDigits(currency) {
94
+ if (contains(ZERO_DIGIT_CURRENCIES, currency)) {
95
+ return 0;
96
+ }
97
+ if (contains(THREE_DIGIT_CURRENCIES, currency)) {
98
+ return 3;
99
+ }
100
+ return contains(FOUR_DIGIT_CURRENCIES, currency) ? 4 : 2;
101
+ }
102
+ function money(amountMinor, currency) {
103
+ const normalized = typeof currency === "string" ? currency.trim().toUpperCase() : "";
104
+ if (!Number.isSafeInteger(amountMinor) || !isCurrency(normalized)) {
105
+ throw new ZappieError(
106
+ "invalid_request",
107
+ "Money requires safe integer minor units and a valid ISO 4217 currency"
108
+ );
109
+ }
110
+ return Object.freeze({ amountMinor, currency: normalized });
111
+ }
112
+ var format = {
113
+ money(value, opts = {}) {
114
+ const normalized = money(value.amountMinor, value.currency);
115
+ const locale = normalized.currency === "NGN" ? "en-NG" : "en";
116
+ const digits = currencyDigits(normalized.currency);
117
+ return new Intl.NumberFormat(locale, {
118
+ style: "currency",
119
+ currency: normalized.currency,
120
+ currencyDisplay: "narrowSymbol",
121
+ minimumFractionDigits: opts.showZeroDecimals || normalized.amountMinor % 10 ** digits !== 0 ? digits : 0,
122
+ maximumFractionDigits: digits,
123
+ notation: opts.compact ? "compact" : "standard",
124
+ signDisplay: opts.sign ?? "auto"
125
+ }).format(normalized.amountMinor / 10 ** digits);
126
+ }
127
+ };
128
+
129
+ // ../sdk/src/internal/money-math.ts
130
+ function sumMoney(values, zero) {
131
+ let amount = zero.amountMinor;
132
+ for (const value of values) {
133
+ if (value.currency !== zero.currency) {
134
+ throw new ZappieError(
135
+ "invalid_request",
136
+ "Cannot combine different currencies"
137
+ );
138
+ }
139
+ amount = money(amount + value.amountMinor, zero.currency).amountMinor;
140
+ }
141
+ return money(amount, zero.currency);
142
+ }
143
+ function multiplyMoney(value, quantity) {
144
+ if (!Number.isSafeInteger(quantity)) {
145
+ throw new ZappieError(
146
+ "invalid_request",
147
+ "Money requires an integer quantity"
148
+ );
149
+ }
150
+ return money(value.amountMinor * quantity, value.currency);
151
+ }
152
+
153
+ // ../sdk/src/transport/mappers.ts
154
+ var UnsupportedValue = class extends Error {
155
+ };
156
+ function record(value) {
157
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
158
+ throw new ZappieError("server_error", "Invalid storefront response object");
159
+ }
160
+ return value;
161
+ }
162
+ function text(value) {
163
+ if (typeof value !== "string") {
164
+ throw new ZappieError("server_error", "Invalid storefront response string");
165
+ }
166
+ return value;
167
+ }
168
+ function list(value) {
169
+ if (!Array.isArray(value)) {
170
+ throw new ZappieError("server_error", "Invalid storefront response list");
171
+ }
172
+ return value;
173
+ }
174
+ function number(value) {
175
+ if (typeof value !== "number" || !Number.isFinite(value)) {
176
+ throw new ZappieError("server_error", "Invalid storefront response number");
177
+ }
178
+ return value;
179
+ }
180
+ function nonnegative(value) {
181
+ const result = number(value);
182
+ if (result < 0) {
183
+ throw new ZappieError(
184
+ "server_error",
185
+ "Negative selection or quantity bound"
186
+ );
187
+ }
188
+ return result;
189
+ }
190
+ function bounds(value) {
191
+ const input = record(value);
192
+ const min = nonnegative(input.min);
193
+ if (input.max == null) {
194
+ return { min };
195
+ }
196
+ const max = nonnegative(input.max);
197
+ if (max < min) {
198
+ throw new ZappieError(
199
+ "server_error",
200
+ "Maximum quantity is below its minimum"
201
+ );
202
+ }
203
+ return { min, max };
204
+ }
205
+ var isString = (value) => typeof value === "string";
206
+ var isBoolean = (value) => typeof value === "boolean";
207
+ function opt(key, value, is) {
208
+ return is(value) ? { [key]: value } : {};
209
+ }
210
+ function mapMoney(value) {
211
+ const v = record(value);
212
+ return money(
213
+ number(v.amount_minor ?? v.amountMinor),
214
+ text(v.currency)
215
+ );
216
+ }
217
+ function hours(value) {
218
+ return list(value ?? []).map((slot) => {
219
+ const v = record(slot);
220
+ const day = number(v.day);
221
+ if (!Number.isInteger(day) || day < 0 || day > 6) {
222
+ throw new ZappieError("server_error", "Invalid opening day");
223
+ }
224
+ return {
225
+ day,
226
+ opens: text(v.opens ?? v.open ?? v.start),
227
+ closes: text(v.closes ?? v.close ?? v.end),
228
+ ...opt("disabled", v.disabled, isBoolean)
229
+ };
230
+ });
231
+ }
232
+ function schedule(value) {
233
+ if (value === void 0) {
234
+ return void 0;
235
+ }
236
+ const v = record(value);
237
+ return {
238
+ openingTimes: hours(v.opening_times ?? v.openingTimes ?? v.hours ?? []),
239
+ ...opt("preorder", v.preorder, isBoolean),
240
+ ...v.preorder_notice_hours !== void 0 ? { preorderNoticeHours: number(v.preorder_notice_hours) } : {},
241
+ ...v.preorder_max_days !== void 0 ? { preorderMaxDays: number(v.preorder_max_days) } : {}
242
+ };
243
+ }
244
+ function mapGroup(value) {
245
+ const g = record(value);
246
+ const selection = record(g.selection ?? { min: g.min ?? 0, max: g.max });
247
+ const isAttribute = g.is_attribute === true;
248
+ const kind = text(g.kind ?? "select");
249
+ if (!["select", "text", "number", "date", "time", "toggle"].includes(kind)) {
250
+ throw new UnsupportedValue(`option input kind "${kind}"`);
251
+ }
252
+ return {
253
+ id: text(g.id),
254
+ name: text(g.name),
255
+ kind,
256
+ required: typeof g.required === "boolean" ? g.required : number(selection.min) > 0,
257
+ selection: bounds(selection),
258
+ isAttribute,
259
+ choices: list(g.choices ?? []).map((raw) => {
260
+ const c = record(raw);
261
+ const price = mapMoney(c.price);
262
+ return {
263
+ id: text(c.id),
264
+ name: text(c.name),
265
+ price: isAttribute ? money(0, price.currency) : price,
266
+ ...isAttribute && c.price_hint !== void 0 ? { priceHint: mapMoney(c.price_hint) } : {},
267
+ isAvailable: (c.is_available ?? c.available) === true,
268
+ ...c.quantity ? { quantity: bounds(c.quantity) } : {}
269
+ };
270
+ }),
271
+ ...opt("helpText", g.help_text, isString),
272
+ ...g.input ? {
273
+ input: {
274
+ ...record(g.input).max_length !== void 0 ? { maxLength: number(record(g.input).max_length) } : {},
275
+ ...typeof record(g.input).placeholder === "string" ? { placeholder: text(record(g.input).placeholder) } : {}
276
+ }
277
+ } : {},
278
+ ...opt("isAvailable", g.is_available, isBoolean),
279
+ ...opt("isInformational", g.is_informational, isBoolean),
280
+ ...g.conditions ? {
281
+ conditions: list(g.conditions).map((raw) => {
282
+ const c = record(raw);
283
+ const count = c.count ? record(c.count) : void 0;
284
+ if (count && !["eq", "gte", "lte"].includes(text(count.op))) {
285
+ throw new UnsupportedValue(
286
+ `option count operator "${text(count.op)}"`
287
+ );
288
+ }
289
+ if (c.match !== void 0 && c.match !== "any" && c.match !== "all") {
290
+ throw new UnsupportedValue(
291
+ `option match operator "${String(c.match)}"`
292
+ );
293
+ }
294
+ if (c.set_required != null && typeof c.set_required !== "boolean") {
295
+ throw new ZappieError(
296
+ "server_error",
297
+ "Invalid conditional required flag"
298
+ );
299
+ }
300
+ return {
301
+ groupId: text(c.group_id),
302
+ choiceIds: list(c.choice_ids ?? []).map(text),
303
+ match: c.match === "all" ? "all" : "any",
304
+ ...count ? {
305
+ count: {
306
+ op: text(count.op),
307
+ value: nonnegative(count.value)
308
+ }
309
+ } : {},
310
+ ...c.set_required !== void 0 ? { setRequired: c.set_required } : {},
311
+ ...c.set_min !== void 0 ? { setMin: c.set_min === null ? null : nonnegative(c.set_min) } : {},
312
+ ...c.set_max !== void 0 ? { setMax: c.set_max === null ? null : nonnegative(c.set_max) } : {}
313
+ };
314
+ })
315
+ } : {}
316
+ };
317
+ }
318
+ function mapPromo(value) {
319
+ const p = record(value);
320
+ return { name: text(p.name) };
321
+ }
322
+ function mapItem(value) {
323
+ const v = record(value);
324
+ const id = text(v.id);
325
+ const optionGroups = [];
326
+ let unsupported = false;
327
+ for (const raw of list(v.option_groups ?? [])) {
328
+ try {
329
+ optionGroups.push(mapGroup(raw));
330
+ } catch (error) {
331
+ if (!(error instanceof UnsupportedValue)) {
332
+ throw error;
333
+ }
334
+ unsupported = true;
335
+ console.warn(
336
+ `Zappie item ${id} uses an ${error.message} this SDK can't validate; it is shown as unavailable`
337
+ );
338
+ }
339
+ }
340
+ const variants = list(v.variants).map((raw) => {
341
+ const x = record(raw);
342
+ return {
343
+ id: text(x.id),
344
+ attributeChoices: Object.fromEntries(
345
+ Object.entries(record(x.attribute_choices ?? {})).map(([k, v2]) => [
346
+ k,
347
+ text(v2)
348
+ ])
349
+ ),
350
+ price: mapMoney(x.price),
351
+ ...opt("lowStock", x.low_stock, isBoolean),
352
+ isAvailable: !unsupported && (x.is_available ?? x.available) === true
353
+ };
354
+ });
355
+ if (!variants.length) {
356
+ throw new ZappieError(
357
+ "server_error",
358
+ "An item must carry at least one exact variant"
359
+ );
360
+ }
361
+ const quantity = record(
362
+ v.quantity ?? { min: 1, step: 1, max: v.max_quantity }
363
+ );
364
+ const step = number(quantity.step);
365
+ if (step <= 0 || new Set(variants.map((variant) => variant.id)).size !== variants.length) {
366
+ throw new ZappieError(
367
+ "server_error",
368
+ "Invalid quantity step or duplicate variant identity"
369
+ );
370
+ }
371
+ return {
372
+ ...v.placements !== void 0 ? {
373
+ placements: list(v.placements).map((raw) => {
374
+ const placement = record(raw);
375
+ return {
376
+ menuId: text(placement.menu_id),
377
+ categoryId: text(placement.category_id)
378
+ };
379
+ })
380
+ } : {},
381
+ id,
382
+ name: text(v.name),
383
+ images: list(v.images ?? []).map(text),
384
+ price: mapMoney(v.price),
385
+ isAvailable: !unsupported && (v.is_available ?? v.available) === true,
386
+ quantity: { ...bounds(quantity), step },
387
+ variants,
388
+ optionGroups,
389
+ tags: list(v.tags ?? []).map(text),
390
+ ...opt("stockLabel", v.stock_label, isString),
391
+ ...v.promo !== void 0 ? { promo: mapPromo(v.promo) } : {},
392
+ ...opt("description", v.description, isString),
393
+ ...v.compare_at_price ? { compareAtPrice: mapMoney(v.compare_at_price) } : {}
394
+ };
395
+ }
396
+ function mapMenu(value, branchId) {
397
+ const v = record(value);
398
+ const sources = list(v.menus ?? [v]);
399
+ if (v.completeness !== void 0 && v.completeness !== "partial" && v.completeness !== "complete") {
400
+ throw new ZappieError("server_error", "Invalid catalogue completeness");
401
+ }
402
+ const categories = /* @__PURE__ */ new Map();
403
+ const menus = sources.map((raw) => {
404
+ const m = record(raw);
405
+ if (m.placement_count !== void 0 && (!Number.isSafeInteger(m.placement_count) || Number(m.placement_count) < 0) || m.paginated !== void 0 && typeof m.paginated !== "boolean") {
406
+ throw new ZappieError("server_error", "Invalid menu loading metadata");
407
+ }
408
+ const categoryIds = list(m.categories ?? []).map((rawCategory) => {
409
+ const c = record(rawCategory);
410
+ const id = text(c.id);
411
+ const previous = categories.get(id);
412
+ const itemIds = [
413
+ .../* @__PURE__ */ new Set([
414
+ ...previous?.itemIds ?? [],
415
+ ...list(c.item_ids ?? []).map(text)
416
+ ])
417
+ ];
418
+ categories.set(id, {
419
+ id,
420
+ name: text(c.name),
421
+ itemIds,
422
+ ...opt("description", c.description, isString)
423
+ });
424
+ return id;
425
+ });
426
+ const s = schedule(m.schedule);
427
+ return {
428
+ id: text(m.id),
429
+ name: text(m.name),
430
+ categoryIds,
431
+ ...m.placement_count !== void 0 ? { placementCount: m.placement_count } : {},
432
+ ...opt("paginated", m.paginated, isBoolean),
433
+ ...s ? { schedule: s } : {},
434
+ ...opt("infoBanner", m.info_banner, isString)
435
+ };
436
+ });
437
+ const items = Object.fromEntries(
438
+ Object.entries(record(v.items)).map(([id, item]) => {
439
+ const mapped = mapItem(item);
440
+ if (mapped.id !== id) {
441
+ throw new ZappieError(
442
+ "server_error",
443
+ "Item map key does not match its identity"
444
+ );
445
+ }
446
+ return [id, mapped];
447
+ })
448
+ );
449
+ if (v.branch_id !== void 0 && v.branch_id !== branchId) {
450
+ throw new ZappieError("server_error", "Menu branch mismatch");
451
+ }
452
+ return {
453
+ id: menus.length === 1 ? menus[0].id : `branch:${branchId}`,
454
+ branchId,
455
+ ...v.completeness === "partial" ? { completeness: "partial" } : {},
456
+ name: menus.length === 1 ? menus[0].name : "Catalogue",
457
+ version: text(v.menu_version ?? v.version),
458
+ categories: [...categories.values()],
459
+ items,
460
+ menus,
461
+ ...menus.length === 1 && menus[0].schedule ? { schedule: menus[0].schedule } : {}
462
+ };
463
+ }
464
+
465
+ export {
466
+ ZappieError,
467
+ asError,
468
+ wireError,
469
+ money,
470
+ format,
471
+ sumMoney,
472
+ multiplyMoney,
473
+ payloadKey,
474
+ record,
475
+ text,
476
+ list,
477
+ isString,
478
+ isBoolean,
479
+ opt,
480
+ mapMoney,
481
+ hours,
482
+ mapItem,
483
+ mapMenu
484
+ };
485
+ //# sourceMappingURL=chunk-WUWUDCYO.js.map