@zappie/sdk 0.1.0 → 0.1.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.
Files changed (190) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +55 -313
  3. package/dist/cart/errors.js +13 -0
  4. package/dist/cart/errors.js.map +7 -0
  5. package/dist/cart/index.js +768 -0
  6. package/dist/cart/index.js.map +7 -0
  7. package/dist/cart/item.js +12 -1
  8. package/dist/cart/item.js.map +7 -1
  9. package/dist/cart/preview.js +32 -0
  10. package/dist/cart/preview.js.map +7 -0
  11. package/dist/cart/price.js +25 -0
  12. package/dist/cart/price.js.map +7 -0
  13. package/dist/cart/quote-client.js +76 -0
  14. package/dist/cart/quote-client.js.map +7 -0
  15. package/dist/cart/quote-response.js +178 -0
  16. package/dist/cart/quote-response.js.map +7 -0
  17. package/dist/cart/reconcile.js +108 -0
  18. package/dist/cart/reconcile.js.map +7 -0
  19. package/dist/cart/source-menu.js +21 -0
  20. package/dist/cart/source-menu.js.map +7 -0
  21. package/dist/cart/storage.js +174 -0
  22. package/dist/cart/storage.js.map +7 -0
  23. package/dist/cart/types.js +1 -0
  24. package/dist/cart/types.js.map +7 -0
  25. package/dist/cart/validate.js +265 -0
  26. package/dist/cart/validate.js.map +7 -0
  27. package/dist/cart/wire.js +42 -0
  28. package/dist/cart/wire.js.map +7 -0
  29. package/dist/cart/working-set.js +80 -0
  30. package/dist/cart/working-set.js.map +7 -0
  31. package/dist/checkout/client.js +102 -0
  32. package/dist/checkout/client.js.map +7 -0
  33. package/dist/checkout/error.js +21 -0
  34. package/dist/checkout/error.js.map +7 -0
  35. package/dist/checkout/index.js +13 -1
  36. package/dist/checkout/index.js.map +7 -1
  37. package/dist/checkout/readiness.js +36 -0
  38. package/dist/checkout/readiness.js.map +7 -0
  39. package/dist/checkout/sdk.js +9 -0
  40. package/dist/checkout/sdk.js.map +7 -0
  41. package/dist/checkout/session.js +58 -0
  42. package/dist/checkout/session.js.map +7 -0
  43. package/dist/checkout/types.js +1 -0
  44. package/dist/checkout/types.js.map +7 -0
  45. package/dist/client.js +253 -0
  46. package/dist/client.js.map +7 -0
  47. package/dist/config/index.js +13 -1
  48. package/dist/config/index.js.map +7 -0
  49. package/dist/config.js +28 -0
  50. package/dist/config.js.map +7 -0
  51. package/dist/customer/client.js +149 -0
  52. package/dist/customer/client.js.map +7 -0
  53. package/dist/customer/error.js +20 -0
  54. package/dist/customer/error.js.map +7 -0
  55. package/dist/customer/index.js +8 -3
  56. package/dist/customer/index.js.map +7 -1
  57. package/dist/customer/navigation.js +45 -0
  58. package/dist/customer/navigation.js.map +7 -0
  59. package/dist/customer/react.js +67 -2
  60. package/dist/customer/react.js.map +7 -1
  61. package/dist/customer/request.js +37 -0
  62. package/dist/customer/request.js.map +7 -0
  63. package/dist/customer/sdk.js +6 -0
  64. package/dist/customer/sdk.js.map +7 -0
  65. package/dist/customer/types.js +1 -0
  66. package/dist/customer/types.js.map +7 -0
  67. package/dist/customer/watch-order.js +80 -0
  68. package/dist/customer/watch-order.js.map +7 -0
  69. package/dist/debug.js +78 -1
  70. package/dist/debug.js.map +7 -1
  71. package/dist/errors.js +69 -0
  72. package/dist/errors.js.map +7 -0
  73. package/dist/index.d.ts +2 -2
  74. package/dist/index.js +29 -1
  75. package/dist/index.js.map +7 -1
  76. package/dist/internal/abort.js +29 -0
  77. package/dist/internal/abort.js.map +7 -0
  78. package/dist/internal/cache.js +142 -0
  79. package/dist/internal/cache.js.map +7 -0
  80. package/dist/internal/customer-http.js +74 -0
  81. package/dist/internal/customer-http.js.map +7 -0
  82. package/dist/internal/digest.js +15 -0
  83. package/dist/internal/digest.js.map +7 -0
  84. package/dist/internal/emitter.js +27 -0
  85. package/dist/internal/emitter.js.map +7 -0
  86. package/dist/internal/money-math.js +30 -0
  87. package/dist/internal/money-math.js.map +7 -0
  88. package/dist/internal.js +9 -1
  89. package/dist/internal.js.map +7 -1
  90. package/dist/models.js +1 -0
  91. package/dist/models.js.map +7 -0
  92. package/dist/money.js +58 -0
  93. package/dist/money.js.map +7 -0
  94. package/dist/persist/catalogue.js +206 -2
  95. package/dist/persist/catalogue.js.map +7 -1
  96. package/dist/places/client.js +97 -0
  97. package/dist/places/client.js.map +7 -0
  98. package/dist/places/error.js +20 -0
  99. package/dist/places/error.js.map +7 -0
  100. package/dist/places/index.js +8 -1
  101. package/dist/places/index.js.map +7 -1
  102. package/dist/places/request.js +40 -0
  103. package/dist/places/request.js.map +7 -0
  104. package/dist/places/sdk.js +10 -0
  105. package/dist/places/sdk.js.map +7 -0
  106. package/dist/places/search.js +80 -0
  107. package/dist/places/search.js.map +7 -0
  108. package/dist/places/types.js +1 -0
  109. package/dist/places/types.js.map +7 -0
  110. package/dist/react/cart.js +30 -0
  111. package/dist/react/cart.js.map +7 -0
  112. package/dist/react/hooks.js +91 -0
  113. package/dist/react/hooks.js.map +7 -0
  114. package/dist/react/index.js +18 -1
  115. package/dist/react/index.js.map +7 -1
  116. package/dist/react/provider.js +34 -0
  117. package/dist/react/provider.js.map +7 -0
  118. package/dist/server/index.js +32 -2
  119. package/dist/server/index.js.map +7 -1
  120. package/dist/server/storefront-url/build-v1.js +75 -0
  121. package/dist/server/storefront-url/build-v1.js.map +7 -0
  122. package/dist/server/storefront-url/build-v2.js +79 -0
  123. package/dist/server/storefront-url/build-v2.js.map +7 -0
  124. package/dist/server/storefront-url/build.js +19 -0
  125. package/dist/server/storefront-url/build.js.map +7 -0
  126. package/dist/server/storefront-url/contract.js +25 -0
  127. package/dist/server/storefront-url/contract.js.map +7 -0
  128. package/dist/server/storefront-url/index.js +16 -0
  129. package/dist/server/storefront-url/index.js.map +7 -0
  130. package/dist/server/storefront-url/installation.js +58 -0
  131. package/dist/server/storefront-url/installation.js.map +7 -0
  132. package/dist/server/storefront-url/params.js +72 -0
  133. package/dist/server/storefront-url/params.js.map +7 -0
  134. package/dist/server/storefront-url/parse-v1.js +135 -0
  135. package/dist/server/storefront-url/parse-v1.js.map +7 -0
  136. package/dist/server/storefront-url/parse-v2.js +139 -0
  137. package/dist/server/storefront-url/parse-v2.js.map +7 -0
  138. package/dist/server/storefront-url/parse.js +9 -0
  139. package/dist/server/storefront-url/parse.js.map +7 -0
  140. package/dist/server/storefront-url/redirect.js +30 -0
  141. package/dist/server/storefront-url/redirect.js.map +7 -0
  142. package/dist/server/storefront-url/sign-in.js +17 -0
  143. package/dist/server/storefront-url/sign-in.js.map +7 -0
  144. package/dist/server/storefront-url/types.js +55 -0
  145. package/dist/server/storefront-url/types.js.map +7 -0
  146. package/dist/server/storefront-url/url.js +61 -0
  147. package/dist/server/storefront-url/url.js.map +7 -0
  148. package/dist/server/webhook.js +96 -0
  149. package/dist/server/webhook.js.map +7 -0
  150. package/dist/site-config/helpers.js +59 -0
  151. package/dist/site-config/helpers.js.map +7 -0
  152. package/dist/site-config/sections.js +105 -0
  153. package/dist/site-config/sections.js.map +7 -0
  154. package/dist/site-config/types.js +31 -0
  155. package/dist/site-config/types.js.map +7 -0
  156. package/dist/site-config/validate.js +165 -0
  157. package/dist/site-config/validate.js.map +7 -0
  158. package/dist/template/index.js +46 -3
  159. package/dist/template/index.js.map +7 -1
  160. package/dist/template/react.js +81 -2
  161. package/dist/template/react.js.map +7 -1
  162. package/dist/testing/index.js +451 -2
  163. package/dist/testing/index.js.map +7 -1
  164. package/dist/transport/category-page.js +21 -2
  165. package/dist/transport/category-page.js.map +7 -1
  166. package/dist/transport/mappers.js +326 -0
  167. package/dist/transport/mappers.js.map +7 -0
  168. package/dist/transport/rest.js +313 -0
  169. package/dist/transport/rest.js.map +7 -0
  170. package/dist/transport/search.js +32 -2
  171. package/dist/transport/search.js.map +7 -1
  172. package/dist/transport/selected-menu.js +79 -2
  173. package/dist/transport/selected-menu.js.map +7 -1
  174. package/dist/transport/store.js +120 -2
  175. package/dist/transport/store.js.map +7 -1
  176. package/dist/transport/types.js +1 -0
  177. package/dist/transport/types.js.map +7 -0
  178. package/dist/url/index.js +16 -2
  179. package/dist/url/index.js.map +7 -1
  180. package/dist/visits/index.js +62 -1
  181. package/dist/visits/index.js.map +7 -1
  182. package/dist/visits/page-views.js +103 -0
  183. package/dist/visits/page-views.js.map +7 -0
  184. package/package.json +5 -4
  185. package/dist/cart/quote.js +0 -1
  186. package/dist/cart/quote.js.map +0 -1
  187. package/dist/chunk-I55A5QXJ.js +0 -2
  188. package/dist/chunk-I55A5QXJ.js.map +0 -1
  189. package/dist/chunk-VG7NKIKR.js +0 -2
  190. package/dist/chunk-VG7NKIKR.js.map +0 -1
@@ -0,0 +1,768 @@
1
+ // src/cart/index.ts
2
+ import { asError, ZappieError } from "../errors.js";
3
+ import { multiplyMoney, sumMoney } from "../internal/money-math.js";
4
+ import { money } from "../money.js";
5
+ import { ZappieCartError } from "./errors.js";
6
+ import { estimateUnitPrice } from "./price.js";
7
+ import { freezeLine, lineInput, reconcile } from "./reconcile.js";
8
+ import { createCartStorage, parseCart, validFulfilment } from "./storage.js";
9
+ import {
10
+ issue,
11
+ normalizeInput,
12
+ resolveVariant,
13
+ validateLine
14
+ } from "./validate.js";
15
+ import { wireFulfilment, wireLine } from "./wire.js";
16
+ var EMPTY_CART = Object.freeze({
17
+ lines: Object.freeze([]),
18
+ count: 0,
19
+ subtotal: null,
20
+ fulfilment: null,
21
+ promoCode: null,
22
+ state: { status: "ready" },
23
+ issues: Object.freeze([]),
24
+ isHydrated: false
25
+ });
26
+ function deepFreeze(value) {
27
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
28
+ Object.freeze(value);
29
+ for (const child of Object.values(value)) {
30
+ deepFreeze(child);
31
+ }
32
+ }
33
+ return value;
34
+ }
35
+ function identity(input) {
36
+ return JSON.stringify([
37
+ input.itemId,
38
+ input.menuId,
39
+ input.variantId,
40
+ input.note ?? "",
41
+ [...input.choices ?? []].sort(
42
+ (a, b) => a.groupId.localeCompare(b.groupId) || a.choiceId.localeCompare(b.choiceId)
43
+ ),
44
+ Object.entries(input.inputs ?? {}).sort(([a], [b]) => a.localeCompare(b))
45
+ ]);
46
+ }
47
+ function createCart(deps) {
48
+ const { config, menu: menus, store, events } = deps;
49
+ const ttl = config.cart?.ttl ?? 7 * 24 * 60 * 60 * 1e3;
50
+ if (!Number.isFinite(ttl) || ttl <= 0) {
51
+ throw new ZappieError(
52
+ "invalid_request",
53
+ "Cart ttl must be positive milliseconds"
54
+ );
55
+ }
56
+ let snapshot = EMPTY_CART;
57
+ let menu;
58
+ let branch;
59
+ let selected = config.branch;
60
+ let updatedAt = 0;
61
+ let timer;
62
+ let revision = 0;
63
+ let quoteRevision = 0;
64
+ let moveSequence = 0;
65
+ let refreshSequence = 0;
66
+ let mounted = 0;
67
+ let detach;
68
+ let hydration;
69
+ let lifecycle = 0;
70
+ let legacy;
71
+ const listeners = /* @__PURE__ */ new Set();
72
+ const prefix = `zappie:cart:v1:${config.store}:`;
73
+ const scope = config.cart?.storageScope;
74
+ const keyFor = (id) => `${prefix}${id}${scope ? `:${scope}` : ""}`;
75
+ const key = () => keyFor(branch.id);
76
+ const storage = createCartStorage(
77
+ config.storage,
78
+ (message) => events.emit("warning", { message })
79
+ );
80
+ const readSaved = (id) => {
81
+ let raw = storage.read(keyFor(id));
82
+ if (raw === null && scope) {
83
+ raw = storage.read(prefix + id);
84
+ if (raw !== null) {
85
+ storage.remove(prefix + id);
86
+ storage.write(keyFor(id), raw);
87
+ }
88
+ }
89
+ return raw;
90
+ };
91
+ let quotes;
92
+ let loadingQuotes;
93
+ const quoteClient = () => loadingQuotes ?? (loadingQuotes = import("./quote-client.js").then(
94
+ ({ createQuoteClient }) => quotes = createQuoteClient(deps.transport, deps.context),
95
+ (error) => {
96
+ loadingQuotes = void 0;
97
+ throw error;
98
+ }
99
+ ));
100
+ const publish = (patch) => {
101
+ snapshot = deepFreeze({
102
+ ...snapshot,
103
+ ...patch,
104
+ branchId: branch?.id
105
+ });
106
+ for (const fn of [...listeners]) {
107
+ try {
108
+ fn();
109
+ } catch (error) {
110
+ events.emit("error", { error: asError(error) });
111
+ }
112
+ }
113
+ };
114
+ const attention = (issues, quote) => issues.some((i) => i.severity !== "info") ? { status: "needs_attention", issues, ...quote ? { quote } : {} } : quote ? { status: "quoted", quote } : { status: "ready" };
115
+ const cancel = () => {
116
+ revision++;
117
+ quoteRevision++;
118
+ clearTimeout(timer);
119
+ quotes?.cancel();
120
+ };
121
+ const persist = () => {
122
+ const value = {
123
+ v: 1,
124
+ lines: snapshot.lines.map((line) => ({
125
+ ...lineInput(line),
126
+ id: line.id,
127
+ name: line.name,
128
+ addedAt: line.addedAt,
129
+ unitPrice: line.unitPrice
130
+ })),
131
+ fulfilment: snapshot.fulfilment,
132
+ promoCode: snapshot.promoCode,
133
+ menuVersion: menu.version,
134
+ updatedAt
135
+ };
136
+ storage.write(key(), JSON.stringify(value));
137
+ };
138
+ const schedule = () => {
139
+ clearTimeout(timer);
140
+ if (snapshot.lines.length) {
141
+ timer = setTimeout(() => {
142
+ void cart.quote().catch(() => {
143
+ });
144
+ }, 300);
145
+ }
146
+ };
147
+ const recompute = (lines, pending = snapshot.issues.filter((i) => i.source === "local"), patch = {}) => {
148
+ const currency = store.peek().currency;
149
+ const result = reconcile(lines, menu, branch, currency, pending);
150
+ const subtotal = result.priced ? sumMoney(
151
+ result.lines.map((l) => l.lineSubtotal),
152
+ money(0, currency)
153
+ ) : null;
154
+ publish({
155
+ ...patch,
156
+ lines: result.lines,
157
+ count: result.lines.reduce((n, l) => n + l.quantity, 0),
158
+ subtotal,
159
+ issues: result.issues,
160
+ state: attention(result.issues)
161
+ });
162
+ for (const i of result.issues) {
163
+ events.emit("cart_issue", { code: i.code });
164
+ }
165
+ };
166
+ const changed = (lines = snapshot.lines, patch = {}) => {
167
+ cancel();
168
+ recompute(lines, void 0, patch);
169
+ updatedAt = Date.now();
170
+ persist();
171
+ schedule();
172
+ };
173
+ const choose = (value) => {
174
+ const found = store.peek()?.branches.find((b) => b.id === value.branchId);
175
+ if (!found) {
176
+ throw new ZappieError(
177
+ "branch_not_found",
178
+ "Load the storefront branch before using its cart"
179
+ );
180
+ }
181
+ menu = value;
182
+ branch = found;
183
+ selected = found.id;
184
+ };
185
+ const load = (raw, writeBack = true) => {
186
+ cancel();
187
+ let saved;
188
+ try {
189
+ saved = raw ? parseCart(raw) : void 0;
190
+ if (saved && Date.now() - saved.updatedAt > ttl) {
191
+ saved = void 0;
192
+ storage.remove(key());
193
+ events.emit("cart_expired", { branchId: branch.id });
194
+ }
195
+ } catch {
196
+ storage.remove(key());
197
+ storage.warning("Invalid saved cart discarded");
198
+ }
199
+ updatedAt = saved?.updatedAt ?? Date.now();
200
+ const idle = Date.now() - updatedAt;
201
+ let fulfilment = saved?.fulfilment ?? null;
202
+ if (fulfilment?.branchId !== branch.id || idle > 864e5 && fulfilment.type === "delivery") {
203
+ fulfilment = null;
204
+ }
205
+ if (fulfilment && "scheduledFor" in fulfilment && fulfilment.scheduledFor && Date.parse(fulfilment.scheduledFor) < Date.now()) {
206
+ fulfilment = { ...fulfilment };
207
+ delete fulfilment.scheduledFor;
208
+ }
209
+ const lines = (saved?.lines ?? []).map((line) => ({
210
+ ...line,
211
+ ...idle > 864e5 ? { inputs: {} } : {}
212
+ }));
213
+ const result = reconcile(lines, menu, branch, store.peek().currency);
214
+ recompute(result.lines, result.issues, {
215
+ fulfilment,
216
+ promoCode: saved?.promoCode ?? null,
217
+ isHydrated: true
218
+ });
219
+ if (raw !== null && writeBack) {
220
+ persist();
221
+ }
222
+ storage.sweep(prefix, key(), ttl);
223
+ };
224
+ const ensure = () => {
225
+ const id = branch?.id ?? store.peek()?.branches.find(
226
+ (b) => b.id === selected || b.slug === selected || !selected && b.isDefault
227
+ )?.id ?? (!selected ? store.peek()?.branches[0]?.id : void 0);
228
+ if (snapshot.isHydrated) {
229
+ return;
230
+ }
231
+ const loaded = menu ?? (id ? deps.workingSet.peek(id, []) : void 0);
232
+ if (!loaded || !store.peek()) {
233
+ throw new ZappieError(
234
+ "invalid_request",
235
+ "Load the store and menu before editing the cart"
236
+ );
237
+ }
238
+ choose(loaded);
239
+ const raw = readSaved(branch.id);
240
+ let migrated = [];
241
+ if (raw === null && legacy) {
242
+ try {
243
+ migrated = legacy(storage.read);
244
+ } catch {
245
+ storage.warning("Invalid legacy cart discarded");
246
+ }
247
+ }
248
+ load(raw);
249
+ if (raw === null && legacy) {
250
+ persist();
251
+ }
252
+ for (const input of migrated.slice(0, 100)) {
253
+ try {
254
+ cart.add(input.itemId, input);
255
+ } catch {
256
+ events.emit("cart_issue", { code: "item_unavailable" });
257
+ }
258
+ }
259
+ };
260
+ const refresh = async () => {
261
+ const id = branch?.id;
262
+ const seq = moveSequence;
263
+ const request = ++refreshSequence;
264
+ const rev = revision;
265
+ if (!id || !menu) {
266
+ return;
267
+ }
268
+ if (!snapshot.lines.length) {
269
+ return;
270
+ }
271
+ if (menu.completeness !== "partial") {
272
+ const version = await deps.transport.getMenuVersion({
273
+ ...deps.context(),
274
+ branchId: id
275
+ });
276
+ if (version.version === menu.version) {
277
+ return;
278
+ }
279
+ }
280
+ const value = await deps.workingSet.read(
281
+ id,
282
+ snapshot.lines.map((l) => l.itemId),
283
+ true
284
+ );
285
+ if (seq !== moveSequence || request !== refreshSequence || rev !== revision || JSON.stringify(value) === JSON.stringify(menu)) {
286
+ return;
287
+ }
288
+ menu = value;
289
+ cancel();
290
+ recompute(snapshot.lines);
291
+ persist();
292
+ schedule();
293
+ };
294
+ const current = (itemId) => {
295
+ const candidate = deps.workingSet.peek(branch.id, [
296
+ ...snapshot.lines.map((l) => l.itemId),
297
+ itemId
298
+ ]);
299
+ if (candidate.items[itemId]) {
300
+ menu = candidate;
301
+ }
302
+ };
303
+ const validated = (raw, id, addedAt) => {
304
+ current(raw.itemId);
305
+ const item = Object.hasOwn(menu.items, raw.itemId) ? menu.items[raw.itemId] : void 0;
306
+ const problems = validateLine(item, raw, { menu, branch });
307
+ if (problems.length || !item) {
308
+ throw new ZappieCartError(problems);
309
+ }
310
+ const input = normalizeInput(item, raw);
311
+ const variant = resolveVariant(item, input);
312
+ const choices = (input.choices ?? []).map((c) => ({
313
+ ...c,
314
+ quantity: c.quantity ?? 1
315
+ }));
316
+ const unitPrice = estimateUnitPrice(item, {
317
+ choices,
318
+ variantId: variant.id
319
+ });
320
+ return freezeLine({
321
+ id: id ?? `line_${Array.from(globalThis.crypto.getRandomValues(new Uint8Array(10)), (byte) => "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-"[byte & 63]).join("")}`,
322
+ itemId: item.id,
323
+ ...input.menuId !== void 0 ? { menuId: input.menuId } : {},
324
+ name: item.name,
325
+ quantity: input.quantity,
326
+ choices,
327
+ inputs: input.inputs ?? {},
328
+ variantId: variant.id,
329
+ ...input.note !== void 0 ? { note: input.note } : {},
330
+ unitPrice,
331
+ lineSubtotal: multiplyMoney(unitPrice, input.quantity),
332
+ addedAt: addedAt ?? (/* @__PURE__ */ new Date()).toISOString()
333
+ });
334
+ };
335
+ const offMenu = events.on("menu:updated", ({ branchId, version }) => {
336
+ if (branch?.id === branchId) {
337
+ const value = menus.peek(branchId);
338
+ if (value && value.completeness !== "partial" && value.version === version && value.version !== menu?.version) {
339
+ refreshSequence++;
340
+ menu = value;
341
+ cancel();
342
+ recompute(snapshot.lines);
343
+ persist();
344
+ schedule();
345
+ }
346
+ }
347
+ });
348
+ const cart = {
349
+ get branchId() {
350
+ return snapshot.branchId;
351
+ },
352
+ get lines() {
353
+ return snapshot.lines;
354
+ },
355
+ get count() {
356
+ return snapshot.count;
357
+ },
358
+ get subtotal() {
359
+ return snapshot.subtotal;
360
+ },
361
+ get fulfilment() {
362
+ return snapshot.fulfilment;
363
+ },
364
+ get promoCode() {
365
+ return snapshot.promoCode;
366
+ },
367
+ get state() {
368
+ return snapshot.state;
369
+ },
370
+ get issues() {
371
+ return snapshot.issues;
372
+ },
373
+ get isHydrated() {
374
+ return snapshot.isHydrated;
375
+ },
376
+ getSnapshot: () => snapshot,
377
+ subscribe(fn) {
378
+ listeners.add(fn);
379
+ return () => {
380
+ listeners.delete(fn);
381
+ };
382
+ },
383
+ async hydrate() {
384
+ if (snapshot.isHydrated) {
385
+ return refresh();
386
+ }
387
+ const life = lifecycle;
388
+ hydration ?? (hydration = (async () => {
389
+ const value = await store.get();
390
+ const target = value.branches.find(
391
+ (b) => b.id === selected || b.slug === selected
392
+ ) ?? (!selected ? value.branches.find((b) => b.isDefault) ?? value.branches[0] : void 0);
393
+ if (!target) {
394
+ throw new ZappieError(
395
+ "branch_not_found",
396
+ "Cart branch is unavailable"
397
+ );
398
+ }
399
+ let inputs = snapshot.lines;
400
+ if (!snapshot.isHydrated) {
401
+ const raw = readSaved(target.id);
402
+ try {
403
+ const saved = raw ? parseCart(raw) : void 0;
404
+ inputs = saved && Date.now() - saved.updatedAt <= ttl ? saved.lines : raw === null ? legacy?.(storage.read) ?? [] : [];
405
+ } catch {
406
+ inputs = [];
407
+ }
408
+ }
409
+ const loaded = await deps.workingSet.read(
410
+ target.id,
411
+ inputs.map((l) => l.itemId)
412
+ );
413
+ if (life !== lifecycle) {
414
+ return;
415
+ }
416
+ choose(loaded);
417
+ ensure();
418
+ if (loaded.completeness !== "partial") {
419
+ await refresh();
420
+ }
421
+ })().finally(() => {
422
+ hydration = void 0;
423
+ }));
424
+ return hydration;
425
+ },
426
+ mount() {
427
+ mounted++;
428
+ if (!detach && typeof window !== "undefined") {
429
+ const onStorage = (event) => {
430
+ if (branch && event.key === key() && storage.accepts(event.storageArea)) {
431
+ const raw = event.newValue;
432
+ let ids;
433
+ try {
434
+ ids = raw ? parseCart(raw).lines.map((l) => l.itemId) : [];
435
+ } catch {
436
+ load(raw, false);
437
+ return;
438
+ }
439
+ const cached = deps.workingSet.peek(branch.id, ids);
440
+ if (cached.completeness !== "partial" || ids.every((id) => cached.items[id])) {
441
+ menu = cached;
442
+ load(raw, false);
443
+ schedule();
444
+ return;
445
+ }
446
+ const rev = revision;
447
+ const request = ++refreshSequence;
448
+ const life = lifecycle;
449
+ void deps.workingSet.read(branch.id, ids).then((value) => {
450
+ if (life === lifecycle && rev === revision && request === refreshSequence) {
451
+ menu = value;
452
+ load(raw, false);
453
+ schedule();
454
+ }
455
+ }).catch(
456
+ (error) => events.emit("error", { error: asError(error) })
457
+ );
458
+ }
459
+ };
460
+ const onFocus = () => {
461
+ void refresh().catch(
462
+ (error) => events.emit("error", { error: asError(error) })
463
+ );
464
+ };
465
+ window.addEventListener("storage", onStorage);
466
+ window.addEventListener("focus", onFocus);
467
+ detach = () => {
468
+ window.removeEventListener("storage", onStorage);
469
+ window.removeEventListener("focus", onFocus);
470
+ };
471
+ }
472
+ if (mounted === 1) {
473
+ void cart.hydrate().catch(
474
+ (error) => events.emit("error", { error: asError(error) })
475
+ );
476
+ }
477
+ let active = true;
478
+ return () => {
479
+ if (active) {
480
+ active = false;
481
+ mounted--;
482
+ if (mounted === 0) {
483
+ detach?.();
484
+ detach = void 0;
485
+ cancel();
486
+ }
487
+ }
488
+ };
489
+ },
490
+ dispose() {
491
+ lifecycle++;
492
+ moveSequence++;
493
+ offMenu();
494
+ cancel();
495
+ detach?.();
496
+ detach = void 0;
497
+ listeners.clear();
498
+ },
499
+ validate(input) {
500
+ ensure();
501
+ current(input.itemId);
502
+ const problems = validateLine(
503
+ Object.hasOwn(menu.items, input.itemId) ? menu.items[input.itemId] : void 0,
504
+ input,
505
+ { menu, branch }
506
+ );
507
+ return problems.length ? { ok: false, issues: problems } : { ok: true };
508
+ },
509
+ add(item, input = {}) {
510
+ ensure();
511
+ let line = validated({
512
+ ...input,
513
+ itemId: typeof item === "string" ? item : item.id
514
+ });
515
+ const existing = snapshot.lines.find(
516
+ (l) => identity(lineInput(l)) === identity(lineInput(line))
517
+ );
518
+ if (existing) {
519
+ line = validated(
520
+ { ...lineInput(line), quantity: existing.quantity + line.quantity },
521
+ existing.id,
522
+ existing.addedAt
523
+ );
524
+ } else if (snapshot.lines.length >= 100) {
525
+ throw new ZappieCartError([
526
+ issue("quantity_limit", "A cart can contain at most 100 lines")
527
+ ]);
528
+ }
529
+ changed(
530
+ existing ? snapshot.lines.map((l) => l.id === existing.id ? line : l) : [...snapshot.lines, line]
531
+ );
532
+ events.emit("add_to_cart", {
533
+ itemId: line.itemId,
534
+ quantity: line.quantity - (existing?.quantity ?? 0),
535
+ value: multiplyMoney(
536
+ line.unitPrice,
537
+ line.quantity - (existing?.quantity ?? 0)
538
+ )
539
+ });
540
+ return line;
541
+ },
542
+ update(lineId, patch) {
543
+ ensure();
544
+ const old = snapshot.lines.find((l) => l.id === lineId);
545
+ if (!old) {
546
+ throw new ZappieCartError([
547
+ issue("item_not_found", "Cart line not found")
548
+ ]);
549
+ }
550
+ const input = { ...lineInput(old), ...patch };
551
+ if (patch.choices && !patch.variantId) {
552
+ delete input.variantId;
553
+ }
554
+ let line = validated(input, old.id, old.addedAt);
555
+ const duplicate = snapshot.lines.find(
556
+ (l) => l.id !== lineId && identity(lineInput(l)) === identity(lineInput(line))
557
+ );
558
+ if (duplicate) {
559
+ line = validated(
560
+ { ...lineInput(line), quantity: line.quantity + duplicate.quantity },
561
+ old.id,
562
+ old.addedAt
563
+ );
564
+ }
565
+ changed(
566
+ snapshot.lines.filter((l) => l.id !== duplicate?.id).map((l) => l.id === lineId ? line : l)
567
+ );
568
+ events.emit("update_cart_line", {
569
+ lineId,
570
+ itemId: line.itemId,
571
+ quantity: line.quantity
572
+ });
573
+ return line;
574
+ },
575
+ remove(lineId) {
576
+ ensure();
577
+ const line = snapshot.lines.find((l) => l.id === lineId);
578
+ changed(snapshot.lines.filter((l) => l.id !== lineId));
579
+ if (line) {
580
+ events.emit("remove_from_cart", {
581
+ itemId: line.itemId,
582
+ quantity: line.quantity,
583
+ value: line.lineSubtotal
584
+ });
585
+ }
586
+ },
587
+ clear() {
588
+ ensure();
589
+ changed([], { fulfilment: null, promoCode: null });
590
+ },
591
+ setFulfilment(value) {
592
+ ensure();
593
+ if (!validFulfilment(value) || value.branchId !== branch.id) {
594
+ throw new ZappieCartError([
595
+ issue(
596
+ "fulfilment_unavailable",
597
+ "Select fulfilment for the cart's branch"
598
+ )
599
+ ]);
600
+ }
601
+ changed(snapshot.lines, { fulfilment: structuredClone(value) });
602
+ },
603
+ setPromoCode(code) {
604
+ ensure();
605
+ changed(snapshot.lines, { promoCode: code?.trim() || null });
606
+ },
607
+ async moveTo(branchId) {
608
+ ensure();
609
+ const seq = ++moveSequence;
610
+ cancel();
611
+ publish({ state: attention(snapshot.issues) });
612
+ const value = await deps.workingSet.read(
613
+ branchId,
614
+ snapshot.lines.map((l) => l.itemId)
615
+ );
616
+ if (seq !== moveSequence) {
617
+ return;
618
+ }
619
+ const oldKey = key();
620
+ const oldLines = snapshot.lines;
621
+ choose(value);
622
+ const kept = oldLines.filter(
623
+ (line) => !validateLine(value.items[line.itemId], lineInput(line), {
624
+ menu: value,
625
+ branch
626
+ }).length
627
+ );
628
+ changed(kept, { fulfilment: null, promoCode: null });
629
+ const dropped = oldLines.filter((line) => !kept.includes(line)).map((line) => ({
630
+ ...issue(
631
+ "item_unavailable",
632
+ `${line.name} is unavailable at this branch`
633
+ ),
634
+ severity: "warning",
635
+ lineId: line.id
636
+ }));
637
+ const issues = [...snapshot.issues, ...dropped];
638
+ publish({ issues, state: attention(issues) });
639
+ if (oldKey !== key()) {
640
+ storage.remove(oldKey);
641
+ }
642
+ },
643
+ async quote(opts = {}) {
644
+ ensure();
645
+ clearTimeout(timer);
646
+ if (opts.force) {
647
+ events.emit("view_cart", {});
648
+ }
649
+ const body = {
650
+ branch_id: branch.id,
651
+ lines: snapshot.lines.map(wireLine),
652
+ ...snapshot.fulfilment ? { fulfilment: wireFulfilment(snapshot.fulfilment) } : {},
653
+ ...snapshot.promoCode ? { promo_code: snapshot.promoCode } : {},
654
+ menu_version: menu.version
655
+ };
656
+ const seq = revision;
657
+ const attempt = ++quoteRevision;
658
+ publish({ state: { status: "quoting" } });
659
+ try {
660
+ const client = await quoteClient();
661
+ return await client.get(body, opts.force ?? false, (quote) => {
662
+ if (seq !== revision) {
663
+ return;
664
+ }
665
+ const local = snapshot.issues.filter((i) => i.source === "local");
666
+ const remote = quote.issues.filter(
667
+ (q) => !local.some(
668
+ (l) => l.code === q.code && l.lineId === q.lineId && l.groupId === q.groupId
669
+ )
670
+ );
671
+ const issues = [...local, ...remote];
672
+ publish({ issues, state: attention(issues, quote) });
673
+ for (const i of remote) {
674
+ events.emit("cart_issue", { code: i.code });
675
+ }
676
+ if (quote.menuVersion !== menu.version || quote.issues.some((i) => i.code === "menu_version_stale")) {
677
+ void refresh().catch(
678
+ (error) => events.emit("error", { error: asError(error) })
679
+ );
680
+ }
681
+ });
682
+ } catch (error) {
683
+ if (seq === revision && attempt === quoteRevision) {
684
+ const local = snapshot.issues.filter((i) => i.source === "local");
685
+ publish({ issues: local, state: attention(local) });
686
+ events.emit("quote_failed", { code: asError(error).code });
687
+ }
688
+ throw error instanceof Error && error.name === "AbortError" ? error : asError(error);
689
+ }
690
+ },
691
+ applyFix(problem, fix = problem.fixes[0]) {
692
+ if (!snapshot.issues.includes(problem) || !fix || !problem.fixes.some((f) => JSON.stringify(f) === JSON.stringify(fix))) {
693
+ throw new ZappieError(
694
+ "invalid_request",
695
+ "Choose a current cart issue and one of its fixes"
696
+ );
697
+ }
698
+ switch (fix.action) {
699
+ case "remove_line": {
700
+ if (problem.lineId) {
701
+ cart.remove(problem.lineId);
702
+ }
703
+ return;
704
+ }
705
+ case "remove_promo":
706
+ cart.setPromoCode(null);
707
+ return;
708
+ case "set_quantity": {
709
+ if (problem.lineId) {
710
+ cart.update(problem.lineId, { quantity: fix.quantity });
711
+ }
712
+ return;
713
+ }
714
+ case "accept_price": {
715
+ quotes?.accept(problem);
716
+ const issues = snapshot.issues.filter((i) => i !== problem);
717
+ const quote = "quote" in snapshot.state ? snapshot.state.quote : void 0;
718
+ const accepted = quote ? {
719
+ ...quote,
720
+ issues: quote.issues.filter(
721
+ (i) => !(i.code === problem.code && i.lineId === problem.lineId)
722
+ )
723
+ } : void 0;
724
+ publish({ issues, state: attention(issues, accepted) });
725
+ return;
726
+ }
727
+ default:
728
+ throw new ZappieError(
729
+ "invalid_request",
730
+ `${fix.action} needs the shopper's selection; use update or setFulfilment`
731
+ );
732
+ }
733
+ },
734
+ toCheckoutPayload() {
735
+ ensure();
736
+ const blocking = snapshot.issues.filter((i) => i.severity === "blocking");
737
+ if (!snapshot.lines.length || !snapshot.fulfilment) {
738
+ blocking.push(
739
+ issue(
740
+ "fulfilment_unavailable",
741
+ "Add items and select fulfilment before checkout"
742
+ )
743
+ );
744
+ }
745
+ if (blocking.length) {
746
+ throw new ZappieCartError(blocking);
747
+ }
748
+ return {
749
+ lines: snapshot.lines.map(wireLine),
750
+ fulfilment: structuredClone(snapshot.fulfilment),
751
+ ...snapshot.promoCode ? { promo_code: snapshot.promoCode } : {},
752
+ menu_version: menu.version
753
+ };
754
+ }
755
+ };
756
+ return {
757
+ cart: Object.freeze(cart),
758
+ cartCatalogue: () => menu,
759
+ setLegacyReader(reader) {
760
+ legacy = reader;
761
+ }
762
+ };
763
+ }
764
+ export {
765
+ EMPTY_CART,
766
+ createCart
767
+ };
768
+ //# sourceMappingURL=index.js.map