@fire-spark/core 0.0.2 → 0.0.3
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/dist/index.d.ts +95 -4
- package/dist/index.js +820 -247
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -27,10 +27,37 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
enumerable: true
|
|
28
28
|
}) : target, mod));
|
|
29
29
|
//#endregion
|
|
30
|
+
//#region src/productCustomization/clamp.utils.ts
|
|
31
|
+
const clampModifierQuantity = (type, quantity, min, max) => {
|
|
32
|
+
let next = Math.max(0, Math.floor(quantity));
|
|
33
|
+
if (type === "RADIO" || type === "CHECKBOX") next = next > 0 ? 1 : 0;
|
|
34
|
+
if (next === 0) return 0;
|
|
35
|
+
let lower = min;
|
|
36
|
+
if (type === "QUANTITY") lower = 0;
|
|
37
|
+
return Math.min(max, Math.max(lower, next));
|
|
38
|
+
};
|
|
39
|
+
const clampProductQuantity = (quantity, min, max) => {
|
|
40
|
+
return Math.min(max, Math.max(min, Math.max(0, Math.floor(quantity))));
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
30
43
|
//#region src/productCustomization/catalog.utils.ts
|
|
31
44
|
const resolvePricingModifier = (product, groupId) => {
|
|
32
45
|
return product.pricing.modifiers.find((modifier) => modifier.modifier_group_id === groupId);
|
|
33
46
|
};
|
|
47
|
+
/** Unit price and tax of a modifier in a group. A variant base listed in the
|
|
48
|
+
* group keeps the limits; the variant it resolves to sets what is charged. */
|
|
49
|
+
const modifierUnitPricing = (listed, ordered, groupId) => {
|
|
50
|
+
const product = ordered ?? listed;
|
|
51
|
+
const entry = resolvePricingModifier(product, groupId);
|
|
52
|
+
const { price, tax_rate, is_tax_inclusive } = product.pricing;
|
|
53
|
+
const taxRate = entry?.tax_rate ?? tax_rate;
|
|
54
|
+
const isTaxInclusive = entry?.is_tax_inclusive ?? is_tax_inclusive;
|
|
55
|
+
return {
|
|
56
|
+
price: entry?.price ?? price,
|
|
57
|
+
taxRate,
|
|
58
|
+
isTaxInclusive
|
|
59
|
+
};
|
|
60
|
+
};
|
|
34
61
|
const inferControlType = (pricingModifier, group) => {
|
|
35
62
|
if (pricingModifier?.display_type) return pricingModifier.display_type;
|
|
36
63
|
const gMin = group.minimum_quantity;
|
|
@@ -65,12 +92,12 @@ const collectReferencedGroupIds = (products) => {
|
|
|
65
92
|
for (const product of products) for (const groupId of product.pricing.modifier_group_ids ?? []) ids.add(groupId);
|
|
66
93
|
return ids;
|
|
67
94
|
};
|
|
68
|
-
const inferGroupFromProducts = (groupId, modifierIds,
|
|
95
|
+
const inferGroupFromProducts = (groupId, modifierIds, productsById) => {
|
|
69
96
|
let minimum_quantity = 0;
|
|
70
97
|
let maximum_quantity = 1;
|
|
71
98
|
let charge_above_quantity = null;
|
|
72
99
|
for (const modifierId of modifierIds) {
|
|
73
|
-
const entry =
|
|
100
|
+
const entry = productsById.get(modifierId)?.pricing.modifiers?.find((modifier) => modifier.modifier_group_id === groupId);
|
|
74
101
|
if (!entry) continue;
|
|
75
102
|
minimum_quantity = Math.max(minimum_quantity, entry.minimum_quantity ?? 0);
|
|
76
103
|
maximum_quantity = Math.max(maximum_quantity, entry.maximum_quantity ?? 1);
|
|
@@ -94,26 +121,324 @@ const resolveModifierGroupsForLookup = (products, modifierGroups) => {
|
|
|
94
121
|
const idsByGroup = collectModifierIdsByGroupId(products);
|
|
95
122
|
const referenced = collectReferencedGroupIds(products);
|
|
96
123
|
const needed = /* @__PURE__ */ new Set([...referenced, ...idsByGroup.keys()]);
|
|
124
|
+
let productsById = null;
|
|
97
125
|
for (const groupId of needed) {
|
|
98
126
|
if (byId.has(groupId)) continue;
|
|
99
127
|
const modifierIds = idsByGroup.get(groupId) ?? [];
|
|
100
128
|
if (modifierIds.length === 0) continue;
|
|
101
|
-
|
|
129
|
+
productsById ??= new Map(products.map((product) => [product.id, product]));
|
|
130
|
+
const args = [
|
|
131
|
+
groupId,
|
|
132
|
+
modifierIds,
|
|
133
|
+
productsById
|
|
134
|
+
];
|
|
135
|
+
byId.set(groupId, inferGroupFromProducts(...args));
|
|
102
136
|
}
|
|
103
137
|
return [...byId.values()];
|
|
104
138
|
};
|
|
105
139
|
//#endregion
|
|
106
|
-
//#region src/productCustomization/
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
140
|
+
//#region src/productCustomization/store.ts
|
|
141
|
+
const createStore = () => {
|
|
142
|
+
return {
|
|
143
|
+
quantities: {},
|
|
144
|
+
initialQuantities: {},
|
|
145
|
+
selections: {},
|
|
146
|
+
initialSelections: {}
|
|
147
|
+
};
|
|
114
148
|
};
|
|
115
|
-
const
|
|
116
|
-
return
|
|
149
|
+
const getQuantity = (store, path) => {
|
|
150
|
+
return store.quantities[path] ?? 0;
|
|
151
|
+
};
|
|
152
|
+
const setQuantity = (store, path, quantity) => {
|
|
153
|
+
if (quantity <= 0) delete store.quantities[path];
|
|
154
|
+
if (quantity <= 0) return;
|
|
155
|
+
store.quantities[path] = quantity;
|
|
156
|
+
};
|
|
157
|
+
const isInSubtree = (key, path) => {
|
|
158
|
+
return key === path || key.startsWith(`${path}/`);
|
|
159
|
+
};
|
|
160
|
+
const clearSubtree = (store, path) => {
|
|
161
|
+
for (const key of Object.keys(store.quantities)) if (isInSubtree(key, path)) delete store.quantities[key];
|
|
162
|
+
for (const key of Object.keys(store.selections)) if (isInSubtree(key, path)) delete store.selections[key];
|
|
163
|
+
};
|
|
164
|
+
/** True when `productId` already sits on `path` (root or any modifier), so
|
|
165
|
+
* descending into it again would loop through a cyclic menu. */
|
|
166
|
+
const pathVisits = (path, productId) => {
|
|
167
|
+
if (isInSubtree(path, `/${productId}`)) return true;
|
|
168
|
+
const segment = `/modifier/${productId}`;
|
|
169
|
+
return path.endsWith(segment) || path.includes(`${segment}/`);
|
|
170
|
+
};
|
|
171
|
+
/** Entries at or below `path`, e.g. to restore one product's subtree. */
|
|
172
|
+
const pickSubtree = (entries, path) => {
|
|
173
|
+
const picked = {};
|
|
174
|
+
for (const [key, value] of Object.entries(entries)) if (isInSubtree(key, path)) picked[key] = value;
|
|
175
|
+
return picked;
|
|
176
|
+
};
|
|
177
|
+
const cloneQuantities = (quantities) => {
|
|
178
|
+
return { ...quantities };
|
|
179
|
+
};
|
|
180
|
+
const cloneSelections = (selections) => {
|
|
181
|
+
const next = {};
|
|
182
|
+
for (const [key, value] of Object.entries(selections)) next[key] = { ...value };
|
|
183
|
+
return next;
|
|
184
|
+
};
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/productCustomization/variants.ts
|
|
187
|
+
const isSelectable = (product) => {
|
|
188
|
+
if (product.status === "INACTIVE") return false;
|
|
189
|
+
return product.availability.status !== "HIDDEN";
|
|
190
|
+
};
|
|
191
|
+
const isInStock = (product) => {
|
|
192
|
+
return product.availability.status !== "OUT_OF_STOCK";
|
|
193
|
+
};
|
|
194
|
+
const isVariantBase = (product) => {
|
|
195
|
+
return (product.pricing.variant_group_ids ?? []).length > 0;
|
|
196
|
+
};
|
|
197
|
+
const variantGroupsOf = (lookup, base) => {
|
|
198
|
+
const groups = [];
|
|
199
|
+
for (const groupId of base.pricing.variant_group_ids ?? []) {
|
|
200
|
+
const group = lookup.variantGroupsById?.[groupId];
|
|
201
|
+
if (group) groups.push(group);
|
|
202
|
+
}
|
|
203
|
+
return groups;
|
|
204
|
+
};
|
|
205
|
+
const selectionFor = (groups, entry) => {
|
|
206
|
+
const selection = {};
|
|
207
|
+
for (const group of groups) {
|
|
208
|
+
const option = group.variants.find((variant) => {
|
|
209
|
+
return entry.variant_group_option_ids.includes(variant.id);
|
|
210
|
+
});
|
|
211
|
+
if (option) selection[group.id] = option.id;
|
|
212
|
+
}
|
|
213
|
+
return selection;
|
|
214
|
+
};
|
|
215
|
+
/** Base selector for a product: itself when it has variant groups, else the
|
|
216
|
+
* first base it resolves from (with its options preselected). */
|
|
217
|
+
const resolveVariantBase = (lookup, product) => {
|
|
218
|
+
if (isVariantBase(product)) return {
|
|
219
|
+
base: product,
|
|
220
|
+
selection: {}
|
|
221
|
+
};
|
|
222
|
+
for (const entry of product.pricing.variants ?? []) {
|
|
223
|
+
const base = lookup.productsById[entry.product_id];
|
|
224
|
+
if (!base || !isVariantBase(base)) continue;
|
|
225
|
+
return {
|
|
226
|
+
base,
|
|
227
|
+
selection: selectionFor(variantGroupsOf(lookup, base), entry)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
};
|
|
232
|
+
const variantEntryFor = (product, baseId) => {
|
|
233
|
+
return product.pricing.variants?.find((entry) => entry.product_id === baseId);
|
|
234
|
+
};
|
|
235
|
+
const buildVariantCandidates = (lookup, base) => {
|
|
236
|
+
const byId = /* @__PURE__ */ new Map();
|
|
237
|
+
for (const group of variantGroupsOf(lookup, base)) for (const option of group.variants) for (const productId of option.product_ids) {
|
|
238
|
+
const product = lookup.productsById[productId];
|
|
239
|
+
if (!product || !isSelectable(product)) continue;
|
|
240
|
+
if (byId.has(productId)) continue;
|
|
241
|
+
const entry = variantEntryFor(product, base.id);
|
|
242
|
+
if (!entry) continue;
|
|
243
|
+
const optionIds = new Set(entry.variant_group_option_ids);
|
|
244
|
+
byId.set(productId, {
|
|
245
|
+
product,
|
|
246
|
+
optionIds
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return [...byId.values()];
|
|
250
|
+
};
|
|
251
|
+
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
252
|
+
const variantIndexFor = (lookup, base) => {
|
|
253
|
+
let byBase = indexCache.get(lookup);
|
|
254
|
+
if (!byBase) {
|
|
255
|
+
byBase = /* @__PURE__ */ new Map();
|
|
256
|
+
indexCache.set(lookup, byBase);
|
|
257
|
+
}
|
|
258
|
+
const cached = byBase.get(base.id);
|
|
259
|
+
if (cached) return cached;
|
|
260
|
+
const index = {
|
|
261
|
+
groups: variantGroupsOf(lookup, base),
|
|
262
|
+
candidates: buildVariantCandidates(lookup, base)
|
|
263
|
+
};
|
|
264
|
+
byBase.set(base.id, index);
|
|
265
|
+
return index;
|
|
266
|
+
};
|
|
267
|
+
const variantCandidates = (lookup, base) => {
|
|
268
|
+
return variantIndexFor(lookup, base).candidates;
|
|
269
|
+
};
|
|
270
|
+
/** Normalized selection for a base and the product it resolves to, if any. */
|
|
271
|
+
const resolveVariantSelection = (lookup, base, selection) => {
|
|
272
|
+
const { groups, candidates } = variantIndexFor(lookup, base);
|
|
273
|
+
const next = normalizeSelection(groups, candidates, selection);
|
|
274
|
+
return {
|
|
275
|
+
selection: next,
|
|
276
|
+
product: findVariantProduct(groups, candidates, next)
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
/** Options of `base` that resolve to `product`, or null if it isn't one. */
|
|
280
|
+
const selectionForVariant = (lookup, base, product) => {
|
|
281
|
+
const entry = variantEntryFor(product, base.id);
|
|
282
|
+
if (!entry) return null;
|
|
283
|
+
return selectionFor(variantIndexFor(lookup, base).groups, entry);
|
|
284
|
+
};
|
|
285
|
+
/** True when `product` is what `base` resolves to for its own options. */
|
|
286
|
+
const resolvesFromBase = (lookup, base, product) => {
|
|
287
|
+
if (!isVariantBase(base)) return false;
|
|
288
|
+
const selection = selectionForVariant(lookup, base, product);
|
|
289
|
+
if (!selection) return false;
|
|
290
|
+
const { groups, candidates } = variantIndexFor(lookup, base);
|
|
291
|
+
return findVariantProduct(groups, candidates, selection)?.id === product.id;
|
|
292
|
+
};
|
|
293
|
+
/** True when some active, visible base still resolves to this product. */
|
|
294
|
+
const isOfferedVariant = (lookup, product) => {
|
|
295
|
+
return (product.pricing.variants ?? []).some((entry) => {
|
|
296
|
+
const base = lookup.productsById[entry.product_id];
|
|
297
|
+
if (!base || !isSelectable(base)) return false;
|
|
298
|
+
return resolvesFromBase(lookup, base, product);
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
const matching = (candidates, optionIds) => {
|
|
302
|
+
return candidates.filter((candidate) => {
|
|
303
|
+
return optionIds.every((id) => candidate.optionIds.has(id));
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
const priorOptionIds = (groups, index, selection) => {
|
|
307
|
+
const ids = [];
|
|
308
|
+
for (const group of groups.slice(0, index)) {
|
|
309
|
+
const selected = selection[group.id];
|
|
310
|
+
if (selected) ids.push(selected);
|
|
311
|
+
}
|
|
312
|
+
return ids;
|
|
313
|
+
};
|
|
314
|
+
const visibleOptions = (candidates, group, prior) => {
|
|
315
|
+
return group.variants.flatMap((option) => {
|
|
316
|
+
const matches = matching(candidates, [...prior, option.id]);
|
|
317
|
+
if (matches.length === 0) return [];
|
|
318
|
+
return [{
|
|
319
|
+
option,
|
|
320
|
+
disabled: !matches.some((match) => isInStock(match.product))
|
|
321
|
+
}];
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
/** Drops picks no longer reachable from earlier groups and auto-picks groups
|
|
325
|
+
* left with a single enabled option. Out-of-stock picks are kept so an
|
|
326
|
+
* explicit variant still resolves (and reports OUT_OF_STOCK downstream). */
|
|
327
|
+
const normalizeSelection = (groups, candidates, selection) => {
|
|
328
|
+
const next = {};
|
|
329
|
+
groups.forEach((group, index) => {
|
|
330
|
+
const prior = priorOptionIds(groups, index, next);
|
|
331
|
+
const visible = visibleOptions(candidates, group, prior);
|
|
332
|
+
const current = selection[group.id];
|
|
333
|
+
const kept = visible.find((entry) => entry.option.id === current);
|
|
334
|
+
if (kept) {
|
|
335
|
+
next[group.id] = kept.option.id;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const enabled = visible.filter((entry) => !entry.disabled);
|
|
339
|
+
if (enabled.length === 1 && enabled[0]) next[group.id] = enabled[0].option.id;
|
|
340
|
+
});
|
|
341
|
+
return next;
|
|
342
|
+
};
|
|
343
|
+
const findVariantProduct = (groups, candidates, selection) => {
|
|
344
|
+
const optionIds = groups.map((group) => selection[group.id]);
|
|
345
|
+
if (optionIds.some((id) => !id)) return null;
|
|
346
|
+
const [match] = matching(candidates, optionIds);
|
|
347
|
+
return match?.product ?? null;
|
|
348
|
+
};
|
|
349
|
+
const buildVariantGroupCtxs = (groups, candidates, selection, onSelect, pathPrefix = "") => {
|
|
350
|
+
return groups.map((group, index) => {
|
|
351
|
+
const path = `${pathPrefix}/variant/${group.id}`;
|
|
352
|
+
const prior = priorOptionIds(groups, index, selection);
|
|
353
|
+
const selectedId = selection[group.id] ?? null;
|
|
354
|
+
const options = visibleOptions(candidates, group, prior).map((entry) => {
|
|
355
|
+
return {
|
|
356
|
+
path: `${path}/option/${entry.option.id}`,
|
|
357
|
+
data: entry.option,
|
|
358
|
+
selected: entry.option.id === selectedId,
|
|
359
|
+
disabled: entry.disabled,
|
|
360
|
+
select: () => {
|
|
361
|
+
if (!entry.disabled) onSelect(group.id, entry.option.id);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
path,
|
|
367
|
+
data: group,
|
|
368
|
+
selectedId,
|
|
369
|
+
status: selectedId ? "VALID" : "INCOMPLETE",
|
|
370
|
+
options
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/productCustomization/seed.ts
|
|
376
|
+
/** Product whose modifier groups hang below a modifier path: a variant base
|
|
377
|
+
* defers to the variant its stored options resolve to (auto-picks persist). */
|
|
378
|
+
const customizedProduct = (store, lookup, product, path) => {
|
|
379
|
+
if (!isVariantBase(product)) return product;
|
|
380
|
+
const { selection, product: resolved } = resolveVariantSelection(...[
|
|
381
|
+
lookup,
|
|
382
|
+
product,
|
|
383
|
+
store.selections[path] ?? {}
|
|
384
|
+
]);
|
|
385
|
+
if (Object.keys(selection).length > 0) store.selections[path] = selection;
|
|
386
|
+
else delete store.selections[path];
|
|
387
|
+
return resolved ?? product;
|
|
388
|
+
};
|
|
389
|
+
const applyCatalogDefaults = (store, lookup, product, path) => {
|
|
390
|
+
setQuantity(store, path, product.pricing.minimum_quantity);
|
|
391
|
+
applyNestedDefaults(store, lookup, product, path);
|
|
392
|
+
};
|
|
393
|
+
const applyNestedDefaults = (store, lookup, product, path) => {
|
|
394
|
+
for (const groupId of product.pricing.modifier_group_ids) {
|
|
395
|
+
const group = lookup.modifierGroupsById[groupId];
|
|
396
|
+
if (!group) continue;
|
|
397
|
+
let radioDefaultApplied = false;
|
|
398
|
+
for (const modifierId of group.modifier_ids) {
|
|
399
|
+
const modifierProduct = lookup.productsById[modifierId];
|
|
400
|
+
if (!modifierProduct || pathVisits(path, modifierId)) continue;
|
|
401
|
+
const modifierPath = `${path}/group/${groupId}/modifier/${modifierId}`;
|
|
402
|
+
const pricingModifier = resolvePricingModifier(modifierProduct, groupId);
|
|
403
|
+
const defaultQuantity = pricingModifier?.default_quantity ?? 0;
|
|
404
|
+
if (defaultQuantity <= 0) continue;
|
|
405
|
+
if (inferControlType(pricingModifier, group) === "RADIO") {
|
|
406
|
+
if (radioDefaultApplied) continue;
|
|
407
|
+
radioDefaultApplied = true;
|
|
408
|
+
}
|
|
409
|
+
setQuantity(store, modifierPath, defaultQuantity);
|
|
410
|
+
applyNestedDefaults(store, lookup, customizedProduct(...[
|
|
411
|
+
store,
|
|
412
|
+
lookup,
|
|
413
|
+
modifierProduct,
|
|
414
|
+
modifierPath
|
|
415
|
+
]), modifierPath);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const applyNestedInitial = (store, lookup, product, path, quantities) => {
|
|
420
|
+
for (const groupId of product.pricing.modifier_group_ids) {
|
|
421
|
+
const group = lookup.modifierGroupsById[groupId];
|
|
422
|
+
if (!group) continue;
|
|
423
|
+
for (const modifierId of group.modifier_ids) {
|
|
424
|
+
const modifierProduct = lookup.productsById[modifierId];
|
|
425
|
+
if (!modifierProduct || pathVisits(path, modifierId)) continue;
|
|
426
|
+
const modifierPath = `${path}/group/${groupId}/modifier/${modifierId}`;
|
|
427
|
+
if (quantities[modifierPath] !== void 0) setQuantity(store, modifierPath, quantities[modifierPath]);
|
|
428
|
+
if (getQuantity(store, modifierPath) <= 0) continue;
|
|
429
|
+
const nested = customizedProduct(...[
|
|
430
|
+
store,
|
|
431
|
+
lookup,
|
|
432
|
+
modifierProduct,
|
|
433
|
+
modifierPath
|
|
434
|
+
]);
|
|
435
|
+
applyNestedInitial(store, lookup, nested, modifierPath, quantities);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
const applyInitialQuantities = (store, lookup, product, path, quantities) => {
|
|
440
|
+
setQuantity(store, path, quantities[path] ?? product.pricing.minimum_quantity);
|
|
441
|
+
applyNestedInitial(store, lookup, product, path, quantities);
|
|
117
442
|
};
|
|
118
443
|
//#endregion
|
|
119
444
|
//#region ../../node_modules/.pnpm/big.js@6.2.1/node_modules/big.js/big.mjs
|
|
@@ -541,7 +866,11 @@ const forEachBillableUnit = (group, modifiers, onUnit) => {
|
|
|
541
866
|
for (const modifier of modifiers) {
|
|
542
867
|
if (modifier.quantity <= 0) continue;
|
|
543
868
|
const pricingModifier = resolvePricingModifier(modifier.data, group.id);
|
|
544
|
-
const unit = new Big(
|
|
869
|
+
const unit = new Big(modifierUnitPricing(...[
|
|
870
|
+
modifier.data,
|
|
871
|
+
modifier.resolved,
|
|
872
|
+
group.id
|
|
873
|
+
]).price);
|
|
545
874
|
const modifierChargeAbove = pricingModifier?.charge_above_quantity ?? 0;
|
|
546
875
|
let remaining = billableQuantity(modifier.quantity, modifierChargeAbove);
|
|
547
876
|
while (remaining > 0) {
|
|
@@ -584,95 +913,20 @@ const computeTotalPrice = (product) => {
|
|
|
584
913
|
return unit.plus(mods).times(product.quantity).toNumber();
|
|
585
914
|
};
|
|
586
915
|
//#endregion
|
|
587
|
-
//#region src/productCustomization/store.ts
|
|
588
|
-
const getQuantity = (store, path) => {
|
|
589
|
-
return store.quantities[path] ?? 0;
|
|
590
|
-
};
|
|
591
|
-
const setQuantity = (store, path, quantity) => {
|
|
592
|
-
if (quantity <= 0) delete store.quantities[path];
|
|
593
|
-
if (quantity <= 0) return;
|
|
594
|
-
store.quantities[path] = quantity;
|
|
595
|
-
};
|
|
596
|
-
const clearSubtree = (store, path) => {
|
|
597
|
-
for (const key of Object.keys(store.quantities)) if (key === path || key.startsWith(`${path}/`)) delete store.quantities[key];
|
|
598
|
-
};
|
|
599
|
-
const cloneQuantities = (quantities) => {
|
|
600
|
-
return { ...quantities };
|
|
601
|
-
};
|
|
602
|
-
//#endregion
|
|
603
916
|
//#region src/productCustomization/refresh.ts
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
modifier.min = pricingModifier?.minimum_quantity ?? 0;
|
|
607
|
-
modifier.max = pricingModifier?.maximum_quantity ?? group.data.maximum_quantity;
|
|
608
|
-
modifier.required = modifier.min > 0;
|
|
609
|
-
modifier.quantity = getQuantity(store, modifier.path);
|
|
610
|
-
modifier.type = inferControlType(pricingModifier, group.data);
|
|
611
|
-
};
|
|
612
|
-
const refreshProductTree = (product, store) => {
|
|
917
|
+
const refreshProductTree = (product, runtime) => {
|
|
918
|
+
const { store, variantSyncs } = runtime;
|
|
613
919
|
product.quantity = getQuantity(store, product.path);
|
|
614
920
|
for (const group of product.groups) {
|
|
921
|
+
let selectedQuantity = 0;
|
|
615
922
|
for (const modifier of group.modifiers) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
923
|
+
modifier.quantity = getQuantity(store, modifier.path);
|
|
924
|
+
selectedQuantity += modifier.quantity;
|
|
925
|
+
variantSyncs.get(modifier)?.();
|
|
926
|
+
if (modifier.product) refreshProductTree(modifier.product, runtime);
|
|
621
927
|
}
|
|
622
928
|
computeGroupModifierLinePrices(group.data, group.modifiers);
|
|
623
|
-
group.selectedQuantity =
|
|
624
|
-
return sum + modifier.quantity;
|
|
625
|
-
}, 0);
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
//#endregion
|
|
629
|
-
//#region src/productCustomization/seed.ts
|
|
630
|
-
const applyCatalogDefaults = (store, lookup, product, path) => {
|
|
631
|
-
setQuantity(store, path, product.pricing.minimum_quantity);
|
|
632
|
-
applyNestedDefaults(store, lookup, product, path);
|
|
633
|
-
};
|
|
634
|
-
const applyNestedDefaults = (store, lookup, product, path) => {
|
|
635
|
-
for (const groupId of product.pricing.modifier_group_ids) {
|
|
636
|
-
const group = lookup.modifierGroupsById[groupId];
|
|
637
|
-
if (!group) continue;
|
|
638
|
-
let radioDefaultApplied = false;
|
|
639
|
-
for (const modifierId of group.modifier_ids) {
|
|
640
|
-
const modifierProduct = lookup.productsById[modifierId];
|
|
641
|
-
if (!modifierProduct) continue;
|
|
642
|
-
const modifierPath = `${path}/group/${groupId}/modifier/${modifierId}`;
|
|
643
|
-
const pricingModifier = resolvePricingModifier(modifierProduct, groupId);
|
|
644
|
-
const defaultQuantity = pricingModifier?.default_quantity ?? 0;
|
|
645
|
-
if (defaultQuantity <= 0) continue;
|
|
646
|
-
if (inferControlType(pricingModifier, group) === "RADIO") {
|
|
647
|
-
if (radioDefaultApplied) continue;
|
|
648
|
-
radioDefaultApplied = true;
|
|
649
|
-
}
|
|
650
|
-
setQuantity(store, modifierPath, defaultQuantity);
|
|
651
|
-
applyNestedDefaults(store, lookup, modifierProduct, modifierPath);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
const applyInitialQuantities = (store, lookup, product, path, quantities) => {
|
|
656
|
-
if (quantities[path] !== void 0) setQuantity(store, path, quantities[path]);
|
|
657
|
-
else setQuantity(store, path, product.pricing.minimum_quantity);
|
|
658
|
-
for (const groupId of product.pricing.modifier_group_ids) {
|
|
659
|
-
const group = lookup.modifierGroupsById[groupId];
|
|
660
|
-
if (!group) continue;
|
|
661
|
-
for (const modifierId of group.modifier_ids) {
|
|
662
|
-
const modifierProduct = lookup.productsById[modifierId];
|
|
663
|
-
if (!modifierProduct) continue;
|
|
664
|
-
const modifierPath = `${path}/group/${groupId}/modifier/${modifierId}`;
|
|
665
|
-
if (quantities[modifierPath] !== void 0) setQuantity(store, modifierPath, quantities[modifierPath]);
|
|
666
|
-
if (getQuantity(store, modifierPath) > 0) {
|
|
667
|
-
const args1 = [
|
|
668
|
-
store,
|
|
669
|
-
lookup,
|
|
670
|
-
modifierProduct
|
|
671
|
-
];
|
|
672
|
-
const args2 = [modifierPath, quantities];
|
|
673
|
-
applyInitialQuantities(...args1, ...args2);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
929
|
+
group.selectedQuantity = selectedQuantity;
|
|
676
930
|
}
|
|
677
931
|
};
|
|
678
932
|
//#endregion
|
|
@@ -694,6 +948,7 @@ const validateModifier = (modifier, options) => {
|
|
|
694
948
|
if (nestedStatus === "INVALID") status = "INVALID";
|
|
695
949
|
else if (status === "VALID" && nestedStatus !== "VALID") status = "INCOMPLETE";
|
|
696
950
|
}
|
|
951
|
+
if (quantity > 0 && status === "VALID" && !modifier.resolved) status = "INCOMPLETE";
|
|
697
952
|
return finalize(status, soft);
|
|
698
953
|
};
|
|
699
954
|
const validateGroup = (group, options) => {
|
|
@@ -749,26 +1004,92 @@ const validateProduct = (product, options) => {
|
|
|
749
1004
|
product.status = "VALID";
|
|
750
1005
|
return product.status;
|
|
751
1006
|
};
|
|
1007
|
+
const validateRoot = (runtime, options) => {
|
|
1008
|
+
refreshProductTree(runtime.root, runtime);
|
|
1009
|
+
const status = validateProduct(runtime.root, options);
|
|
1010
|
+
if (status === "IDLE" || !runtime.pendingVariants?.()) return status;
|
|
1011
|
+
runtime.root.status = "INCOMPLETE";
|
|
1012
|
+
return runtime.root.status;
|
|
1013
|
+
};
|
|
752
1014
|
const runAfterChange = (runtime) => {
|
|
753
|
-
|
|
754
|
-
validateProduct(runtime.root, { soft: true });
|
|
1015
|
+
validateRoot(runtime, { soft: true });
|
|
755
1016
|
runtime.listeners.forEach((listener) => listener());
|
|
756
1017
|
};
|
|
757
1018
|
//#endregion
|
|
758
1019
|
//#region src/productCustomization/createCtx.ts
|
|
759
|
-
const
|
|
1020
|
+
const retargetModifierProduct = (runtime, ctx, product) => {
|
|
1021
|
+
const { store, lookup } = runtime;
|
|
1022
|
+
const prefix = `${ctx.path}/group/`;
|
|
1023
|
+
const carried = {};
|
|
1024
|
+
for (const [key, value] of Object.entries(store.quantities)) {
|
|
1025
|
+
if (!key.startsWith(prefix)) continue;
|
|
1026
|
+
carried[key] = value;
|
|
1027
|
+
delete store.quantities[key];
|
|
1028
|
+
}
|
|
1029
|
+
const args = [
|
|
1030
|
+
store,
|
|
1031
|
+
lookup,
|
|
1032
|
+
product,
|
|
1033
|
+
ctx.path
|
|
1034
|
+
];
|
|
1035
|
+
if (Object.keys(carried).length > 0) applyNestedInitial(...args, carried);
|
|
1036
|
+
else if (getQuantity(store, ctx.path) > 0) applyNestedDefaults(...args);
|
|
1037
|
+
assignProduct(runtime, ctx, product);
|
|
1038
|
+
};
|
|
1039
|
+
const attachModifierVariants = (runtime, modifier) => {
|
|
1040
|
+
const { store, lookup } = runtime;
|
|
1041
|
+
const base = modifier.data;
|
|
1042
|
+
const path = modifier.path;
|
|
1043
|
+
const { groups, candidates } = variantIndexFor(lookup, base);
|
|
1044
|
+
let lastKey = null;
|
|
1045
|
+
const selectHandler = (groupId, optionId) => {
|
|
1046
|
+
const current = store.selections[path] ?? {};
|
|
1047
|
+
store.selections[path] = {
|
|
1048
|
+
...current,
|
|
1049
|
+
[groupId]: optionId
|
|
1050
|
+
};
|
|
1051
|
+
if (getQuantity(store, path) > 0) {
|
|
1052
|
+
runAfterChange(runtime);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
modifier.onChange(Math.max(1, modifier.min));
|
|
1056
|
+
};
|
|
1057
|
+
const sync = () => {
|
|
1058
|
+
if (JSON.stringify(store.selections[path] ?? {}) === lastKey) return;
|
|
1059
|
+
const target = customizedProduct(store, lookup, base, path);
|
|
1060
|
+
const selection = store.selections[path] ?? {};
|
|
1061
|
+
lastKey = JSON.stringify(selection);
|
|
1062
|
+
modifier.resolved = target === base ? null : target;
|
|
1063
|
+
if (!modifier.product) modifier.product = createProductCtx(runtime, target, path, false);
|
|
1064
|
+
else if (modifier.product.data.id !== target.id) retargetModifierProduct(runtime, modifier.product, target);
|
|
1065
|
+
modifier.variants = buildVariantGroupCtxs(...[
|
|
1066
|
+
groups,
|
|
1067
|
+
candidates,
|
|
1068
|
+
selection,
|
|
1069
|
+
selectHandler,
|
|
1070
|
+
path
|
|
1071
|
+
]);
|
|
1072
|
+
};
|
|
1073
|
+
runtime.variantSyncs.set(modifier, sync);
|
|
1074
|
+
sync();
|
|
1075
|
+
};
|
|
1076
|
+
const createModifierCtx = (runtime, group, modifierProduct, parentPath) => {
|
|
1077
|
+
const modifierPath = `${group.path}/modifier/${modifierProduct.id}`;
|
|
760
1078
|
const pricingModifier = resolvePricingModifier(...[modifierProduct, group.data.id]);
|
|
761
1079
|
const type = inferControlType(pricingModifier, group.data);
|
|
1080
|
+
const min = pricingModifier?.minimum_quantity ?? 0;
|
|
762
1081
|
const modifier = {
|
|
763
1082
|
path: modifierPath,
|
|
764
|
-
min
|
|
1083
|
+
min,
|
|
765
1084
|
max: pricingModifier?.maximum_quantity ?? group.data.maximum_quantity,
|
|
766
|
-
quantity:
|
|
767
|
-
required:
|
|
1085
|
+
quantity: getQuantity(runtime.store, modifierPath),
|
|
1086
|
+
required: min > 0,
|
|
768
1087
|
status: "IDLE",
|
|
769
1088
|
data: modifierProduct,
|
|
770
1089
|
type,
|
|
771
1090
|
product: null,
|
|
1091
|
+
variants: [],
|
|
1092
|
+
resolved: modifierProduct,
|
|
772
1093
|
linePrice: 0,
|
|
773
1094
|
onChange: () => {}
|
|
774
1095
|
};
|
|
@@ -790,7 +1111,9 @@ const createModifierCtx = (runtime, productCtx, group, modifierProduct, modifier
|
|
|
790
1111
|
} else setQuantity(runtime.store, modifierPath, next);
|
|
791
1112
|
runAfterChange(runtime);
|
|
792
1113
|
};
|
|
793
|
-
if (modifierProduct.
|
|
1114
|
+
if (pathVisits(parentPath, modifierProduct.id)) return modifier;
|
|
1115
|
+
if (isVariantBase(modifierProduct)) attachModifierVariants(runtime, modifier);
|
|
1116
|
+
else if (modifierProduct.pricing.modifier_group_ids.length > 0) modifier.product = createProductCtx(...[
|
|
794
1117
|
runtime,
|
|
795
1118
|
modifierProduct,
|
|
796
1119
|
modifierPath,
|
|
@@ -798,10 +1121,9 @@ const createModifierCtx = (runtime, productCtx, group, modifierProduct, modifier
|
|
|
798
1121
|
]);
|
|
799
1122
|
return modifier;
|
|
800
1123
|
};
|
|
801
|
-
const createModifierGroupCtx = (runtime,
|
|
802
|
-
const groupPath = `${productPath}/group/${group.id}`;
|
|
1124
|
+
const createModifierGroupCtx = (runtime, group, productPath) => {
|
|
803
1125
|
const groupCtx = {
|
|
804
|
-
path:
|
|
1126
|
+
path: `${productPath}/group/${group.id}`,
|
|
805
1127
|
min: group.minimum_quantity,
|
|
806
1128
|
max: group.maximum_quantity,
|
|
807
1129
|
required: group.minimum_quantity > 0,
|
|
@@ -814,25 +1136,36 @@ const createModifierGroupCtx = (runtime, productCtx, group, productPath) => {
|
|
|
814
1136
|
for (const modifierId of group.modifier_ids) {
|
|
815
1137
|
const modifierProduct = runtime.lookup.productsById[modifierId];
|
|
816
1138
|
if (!modifierProduct) continue;
|
|
817
|
-
const
|
|
818
|
-
const args1 = [
|
|
1139
|
+
const args = [
|
|
819
1140
|
runtime,
|
|
820
|
-
|
|
821
|
-
|
|
1141
|
+
groupCtx,
|
|
1142
|
+
modifierProduct,
|
|
1143
|
+
productPath
|
|
822
1144
|
];
|
|
823
|
-
|
|
824
|
-
const ctx = createModifierCtx(...args1, ...args2);
|
|
825
|
-
groupCtx.modifiers.push(ctx);
|
|
1145
|
+
groupCtx.modifiers.push(createModifierCtx(...args));
|
|
826
1146
|
}
|
|
827
1147
|
return groupCtx;
|
|
828
1148
|
};
|
|
1149
|
+
const assignProduct = (runtime, productCtx, product) => {
|
|
1150
|
+
productCtx.data = product;
|
|
1151
|
+
productCtx.min = product.pricing.minimum_quantity;
|
|
1152
|
+
productCtx.max = product.pricing.maximum_quantity;
|
|
1153
|
+
productCtx.required = product.pricing.minimum_quantity > 0;
|
|
1154
|
+
const groups = [];
|
|
1155
|
+
for (const groupId of product.pricing.modifier_group_ids) {
|
|
1156
|
+
const group = runtime.lookup.modifierGroupsById[groupId];
|
|
1157
|
+
if (!group) continue;
|
|
1158
|
+
groups.push(createModifierGroupCtx(runtime, group, productCtx.path));
|
|
1159
|
+
}
|
|
1160
|
+
productCtx.groups = groups;
|
|
1161
|
+
};
|
|
829
1162
|
const createProductCtx = (runtime, product, path, isRoot) => {
|
|
830
1163
|
const productCtx = {
|
|
831
1164
|
path,
|
|
832
|
-
min:
|
|
833
|
-
max:
|
|
834
|
-
quantity:
|
|
835
|
-
required:
|
|
1165
|
+
min: 0,
|
|
1166
|
+
max: 0,
|
|
1167
|
+
quantity: getQuantity(runtime.store, path),
|
|
1168
|
+
required: false,
|
|
836
1169
|
status: "IDLE",
|
|
837
1170
|
data: product,
|
|
838
1171
|
groups: [],
|
|
@@ -840,28 +1173,23 @@ const createProductCtx = (runtime, product, path, isRoot) => {
|
|
|
840
1173
|
validate: () => "IDLE",
|
|
841
1174
|
reset: () => {}
|
|
842
1175
|
};
|
|
843
|
-
productCtx
|
|
844
|
-
const group = runtime.lookup.modifierGroupsById[groupId];
|
|
845
|
-
if (!group) return null;
|
|
846
|
-
return createModifierGroupCtx(runtime, productCtx, group, path);
|
|
847
|
-
}).filter((entry) => entry !== null);
|
|
1176
|
+
assignProduct(runtime, productCtx, product);
|
|
848
1177
|
productCtx.onChange = (quantity) => {
|
|
849
1178
|
const next = clampProductQuantity(quantity, productCtx.min, productCtx.max);
|
|
850
1179
|
if (next <= 0) clearSubtree(runtime.store, path);
|
|
851
1180
|
setQuantity(runtime.store, path, next);
|
|
852
1181
|
runAfterChange(runtime);
|
|
853
1182
|
};
|
|
854
|
-
productCtx.validate = (options) =>
|
|
855
|
-
refreshProductTree(runtime.root, runtime.store);
|
|
856
|
-
return validateProduct(runtime.root, options);
|
|
857
|
-
};
|
|
1183
|
+
productCtx.validate = (options) => validateRoot(runtime, options);
|
|
858
1184
|
productCtx.reset = (options) => {
|
|
859
|
-
|
|
1185
|
+
const { store, lookup } = runtime;
|
|
1186
|
+
clearSubtree(store, path);
|
|
860
1187
|
if (options?.force) {
|
|
861
|
-
if (isRoot) applyCatalogDefaults(
|
|
1188
|
+
if (isRoot) applyCatalogDefaults(store, lookup, productCtx.data, path);
|
|
862
1189
|
} else {
|
|
863
|
-
|
|
864
|
-
|
|
1190
|
+
Object.assign(store.quantities, pickSubtree(store.initialQuantities, path));
|
|
1191
|
+
for (const [key, value] of Object.entries(pickSubtree(store.initialSelections, path))) store.selections[key] = { ...value };
|
|
1192
|
+
if (isRoot && getQuantity(store, path) === 0) setQuantity(store, path, productCtx.data.pricing.minimum_quantity);
|
|
865
1193
|
}
|
|
866
1194
|
runAfterChange(runtime);
|
|
867
1195
|
};
|
|
@@ -869,55 +1197,146 @@ const createProductCtx = (runtime, product, path, isRoot) => {
|
|
|
869
1197
|
};
|
|
870
1198
|
//#endregion
|
|
871
1199
|
//#region src/productCustomization/createSession.ts
|
|
1200
|
+
const reroot = (entries, from, to) => {
|
|
1201
|
+
const next = {};
|
|
1202
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
1203
|
+
if (key === from) next[to] = value;
|
|
1204
|
+
if (key.startsWith(`${from}/`)) next[`${to}${key.slice(from.length)}`] = value;
|
|
1205
|
+
}
|
|
1206
|
+
return next;
|
|
1207
|
+
};
|
|
872
1208
|
const createSession = (lookup, productId, options) => {
|
|
873
1209
|
const product = lookup.productsById[productId];
|
|
874
1210
|
if (!product) throw new Error(`Product "${productId}" was not found in menu.`);
|
|
875
|
-
const
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
initialQuantities: {}
|
|
879
|
-
};
|
|
1211
|
+
const store = createStore();
|
|
1212
|
+
const snapshotVariants = options?.snapshot?.variants;
|
|
1213
|
+
if (snapshotVariants) store.selections = cloneSelections(snapshotVariants);
|
|
880
1214
|
const seed = options?.snapshot?.quantities ?? options?.initial ?? null;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1215
|
+
const seedArgs = [
|
|
1216
|
+
store,
|
|
1217
|
+
lookup,
|
|
1218
|
+
product,
|
|
1219
|
+
`/${product.id}`
|
|
1220
|
+
];
|
|
1221
|
+
if (seed) applyInitialQuantities(...seedArgs, seed);
|
|
1222
|
+
else applyCatalogDefaults(...seedArgs);
|
|
888
1223
|
const listeners = /* @__PURE__ */ new Set();
|
|
889
1224
|
const runtime = {
|
|
890
1225
|
lookup,
|
|
891
1226
|
store,
|
|
892
1227
|
listeners,
|
|
1228
|
+
variantSyncs: /* @__PURE__ */ new WeakMap(),
|
|
893
1229
|
root: null
|
|
894
1230
|
};
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1231
|
+
const variantBase = resolveVariantBase(lookup, product);
|
|
1232
|
+
const { groups, candidates } = variantBase ? variantIndexFor(lookup, variantBase.base) : {
|
|
1233
|
+
groups: [],
|
|
1234
|
+
candidates: []
|
|
1235
|
+
};
|
|
1236
|
+
let selection = {};
|
|
1237
|
+
let variantCtxs = [];
|
|
1238
|
+
const moveStoreRoot = (from, next) => {
|
|
1239
|
+
const to = `/${next.id}`;
|
|
1240
|
+
const carried = reroot(store.quantities, from, to);
|
|
1241
|
+
const quantity = getQuantity(store, from);
|
|
1242
|
+
store.quantities = {};
|
|
1243
|
+
store.selections = reroot(store.selections, from, to);
|
|
1244
|
+
const hasModifiers = Object.keys(carried).length > 1;
|
|
1245
|
+
const args = [
|
|
1246
|
+
store,
|
|
1247
|
+
lookup,
|
|
1248
|
+
next,
|
|
1249
|
+
to
|
|
1250
|
+
];
|
|
1251
|
+
if (hasModifiers) applyInitialQuantities(...args, carried);
|
|
1252
|
+
else applyCatalogDefaults(...args);
|
|
1253
|
+
const { minimum_quantity: min, maximum_quantity: max } = next.pricing;
|
|
1254
|
+
setQuantity(store, to, clampProductQuantity(quantity, min, max));
|
|
1255
|
+
};
|
|
1256
|
+
const resolveRoot = (nextSelection, current) => {
|
|
1257
|
+
if (!variantBase) return current;
|
|
1258
|
+
selection = normalizeSelection(groups, candidates, nextSelection);
|
|
1259
|
+
const next = findVariantProduct(groups, candidates, selection) ?? variantBase.base;
|
|
1260
|
+
if (next.id !== current.id) moveStoreRoot(`/${current.id}`, next);
|
|
1261
|
+
return next;
|
|
1262
|
+
};
|
|
1263
|
+
const rebuildVariantCtxs = () => {
|
|
1264
|
+
if (!variantBase) return;
|
|
1265
|
+
variantCtxs = buildVariantGroupCtxs(...[
|
|
1266
|
+
groups,
|
|
1267
|
+
candidates,
|
|
1268
|
+
selection
|
|
1269
|
+
], selectHandler);
|
|
1270
|
+
};
|
|
1271
|
+
const applySelection = (nextSelection) => {
|
|
1272
|
+
const current = runtime.root.data;
|
|
1273
|
+
const next = resolveRoot(nextSelection, current);
|
|
1274
|
+
if (next.id !== current.id) runtime.root = createProductCtx(runtime, next, `/${next.id}`, true);
|
|
1275
|
+
rebuildVariantCtxs();
|
|
1276
|
+
};
|
|
1277
|
+
const selectHandler = (groupId, optionId) => {
|
|
1278
|
+
applySelection({
|
|
1279
|
+
...selection,
|
|
1280
|
+
[groupId]: optionId
|
|
1281
|
+
});
|
|
1282
|
+
runAfterChange(runtime);
|
|
1283
|
+
};
|
|
1284
|
+
runtime.pendingVariants = () => {
|
|
1285
|
+
return variantBase !== null && runtime.root.data.id === variantBase.base.id;
|
|
1286
|
+
};
|
|
1287
|
+
const rootProduct = resolveRoot(variantBase?.selection ?? {}, product);
|
|
1288
|
+
runtime.root = createProductCtx(runtime, rootProduct, `/${rootProduct.id}`, true);
|
|
1289
|
+
rebuildVariantCtxs();
|
|
1290
|
+
const initialSelection = { ...selection };
|
|
1291
|
+
store.initialQuantities = cloneQuantities(store.quantities);
|
|
1292
|
+
store.initialSelections = cloneSelections(store.selections);
|
|
1293
|
+
validateRoot(runtime);
|
|
1294
|
+
const activeSelections = () => {
|
|
1295
|
+
const variants = {};
|
|
1296
|
+
for (const [path, entry] of Object.entries(store.selections)) if (getQuantity(store, path) > 0) variants[path] = { ...entry };
|
|
1297
|
+
return variants;
|
|
1298
|
+
};
|
|
898
1299
|
return {
|
|
899
|
-
root
|
|
1300
|
+
get root() {
|
|
1301
|
+
return runtime.root;
|
|
1302
|
+
},
|
|
1303
|
+
get base() {
|
|
1304
|
+
return variantBase?.base ?? null;
|
|
1305
|
+
},
|
|
1306
|
+
get variants() {
|
|
1307
|
+
return variantCtxs;
|
|
1308
|
+
},
|
|
900
1309
|
subscribe: (listener) => {
|
|
901
1310
|
listeners.add(listener);
|
|
902
1311
|
return () => listeners.delete(listener);
|
|
903
1312
|
},
|
|
904
1313
|
snapshot: () => {
|
|
1314
|
+
const productId = runtime.root.data.id;
|
|
1315
|
+
const quantities = cloneQuantities(store.quantities);
|
|
1316
|
+
const variants = activeSelections();
|
|
1317
|
+
if (Object.keys(variants).length === 0) return {
|
|
1318
|
+
productId,
|
|
1319
|
+
quantities
|
|
1320
|
+
};
|
|
905
1321
|
return {
|
|
906
1322
|
productId,
|
|
907
|
-
quantities
|
|
1323
|
+
quantities,
|
|
1324
|
+
variants
|
|
908
1325
|
};
|
|
909
1326
|
},
|
|
910
1327
|
totalPrice: () => {
|
|
911
|
-
refreshProductTree(runtime.root, runtime
|
|
1328
|
+
refreshProductTree(runtime.root, runtime);
|
|
912
1329
|
return computeTotalPrice(runtime.root);
|
|
913
1330
|
},
|
|
914
|
-
validate: (
|
|
915
|
-
|
|
916
|
-
const status = validateProduct(runtime.root, options);
|
|
1331
|
+
validate: (validateOptions) => {
|
|
1332
|
+
const status = validateRoot(runtime, validateOptions);
|
|
917
1333
|
runtime.listeners.forEach((listener) => listener());
|
|
918
1334
|
return status;
|
|
919
1335
|
},
|
|
920
|
-
reset: (
|
|
1336
|
+
reset: (resetOptions) => {
|
|
1337
|
+
applySelection(initialSelection);
|
|
1338
|
+
runtime.root.reset(resetOptions);
|
|
1339
|
+
}
|
|
921
1340
|
};
|
|
922
1341
|
};
|
|
923
1342
|
//#endregion
|
|
@@ -988,33 +1407,74 @@ const flattenMenuCategories = (categories, categoriesById, options) => {
|
|
|
988
1407
|
};
|
|
989
1408
|
//#endregion
|
|
990
1409
|
//#region src/productCustomization/fromOrderLines.ts
|
|
1410
|
+
/** Group slot of a child line: listed directly, or as the variant of a base
|
|
1411
|
+
* listed in one of the parent's modifier groups. */
|
|
1412
|
+
const matchModifierLine = (lookup, parent, childId) => {
|
|
1413
|
+
const groups = parent.pricing.modifier_group_ids.flatMap((groupId) => {
|
|
1414
|
+
const group = lookup.modifierGroupsById[groupId];
|
|
1415
|
+
return group ? [group] : [];
|
|
1416
|
+
});
|
|
1417
|
+
for (const group of groups) {
|
|
1418
|
+
if (!group.modifier_ids.includes(childId)) continue;
|
|
1419
|
+
return {
|
|
1420
|
+
groupId: group.id,
|
|
1421
|
+
modifierId: childId,
|
|
1422
|
+
selection: null
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const child = lookup.productsById[childId];
|
|
1426
|
+
for (const entry of child?.pricing.variants ?? []) {
|
|
1427
|
+
const base = lookup.productsById[entry.product_id];
|
|
1428
|
+
if (!child || !base) continue;
|
|
1429
|
+
const group = groups.find((g) => g.modifier_ids.includes(base.id));
|
|
1430
|
+
if (!group) continue;
|
|
1431
|
+
const selection = selectionForVariant(lookup, base, child);
|
|
1432
|
+
if (!selection) continue;
|
|
1433
|
+
return {
|
|
1434
|
+
groupId: group.id,
|
|
1435
|
+
modifierId: base.id,
|
|
1436
|
+
selection
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
return null;
|
|
1440
|
+
};
|
|
991
1441
|
/**
|
|
992
1442
|
* Inverse of {@link toOrderLines}: rebuild a {@link Snapshot} from a root cart
|
|
993
1443
|
* line so {@link createSession} can reopen it via `{ snapshot }`.
|
|
994
1444
|
*/
|
|
995
1445
|
const fromOrderLines = (root, lines, lookup) => {
|
|
996
1446
|
const quantities = { [`/${root.id}`]: root.quantity };
|
|
997
|
-
const
|
|
998
|
-
|
|
1447
|
+
const variants = {};
|
|
1448
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
1449
|
+
for (const line of lines) {
|
|
1450
|
+
if (line.parent_uid == null) continue;
|
|
1451
|
+
const siblings = childrenByParent.get(line.parent_uid) ?? [];
|
|
1452
|
+
siblings.push(line);
|
|
1453
|
+
childrenByParent.set(line.parent_uid, siblings);
|
|
1454
|
+
}
|
|
1455
|
+
const walk = (parentLine, pathPrefix) => {
|
|
1456
|
+
const parentProduct = lookup.productsById[parentLine.id];
|
|
999
1457
|
if (!parentProduct) return;
|
|
1000
|
-
for (const child of
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
if (!matchedPath) continue;
|
|
1009
|
-
quantities[matchedPath] = child.quantity;
|
|
1010
|
-
walk(child.id, child.uid, matchedPath);
|
|
1458
|
+
for (const child of childrenByParent.get(parentLine.uid) ?? []) {
|
|
1459
|
+
const match = matchModifierLine(lookup, parentProduct, child.id);
|
|
1460
|
+
if (!match) continue;
|
|
1461
|
+
const { groupId, modifierId, selection } = match;
|
|
1462
|
+
const path = `${pathPrefix}/group/${groupId}/modifier/${modifierId}`;
|
|
1463
|
+
quantities[path] = child.quantity;
|
|
1464
|
+
if (selection) variants[path] = selection;
|
|
1465
|
+
walk(child, path);
|
|
1011
1466
|
}
|
|
1012
1467
|
};
|
|
1013
|
-
walk(root
|
|
1014
|
-
return {
|
|
1468
|
+
walk(root, `/${root.id}`);
|
|
1469
|
+
if (Object.keys(variants).length === 0) return {
|
|
1015
1470
|
productId: root.id,
|
|
1016
1471
|
quantities
|
|
1017
1472
|
};
|
|
1473
|
+
return {
|
|
1474
|
+
productId: root.id,
|
|
1475
|
+
quantities,
|
|
1476
|
+
variants
|
|
1477
|
+
};
|
|
1018
1478
|
};
|
|
1019
1479
|
//#endregion
|
|
1020
1480
|
//#region src/productCustomization/menu.ts
|
|
@@ -1024,6 +1484,8 @@ const indexMenu = (menu) => {
|
|
|
1024
1484
|
const modifierGroupsById = {};
|
|
1025
1485
|
const modifierGroups = resolveModifierGroupsForLookup(...[menu.products, menu.modifier_groups]);
|
|
1026
1486
|
for (const group of modifierGroups) modifierGroupsById[group.id] = group;
|
|
1487
|
+
const variantGroupsById = {};
|
|
1488
|
+
for (const group of menu.variant_groups ?? []) variantGroupsById[group.id] = group;
|
|
1027
1489
|
const categoriesById = {};
|
|
1028
1490
|
for (const category of menu.categories) categoriesById[category.id] = category;
|
|
1029
1491
|
const recommendationsById = {};
|
|
@@ -1031,11 +1493,139 @@ const indexMenu = (menu) => {
|
|
|
1031
1493
|
return {
|
|
1032
1494
|
productsById,
|
|
1033
1495
|
modifierGroupsById,
|
|
1496
|
+
variantGroupsById,
|
|
1034
1497
|
categoriesById,
|
|
1035
1498
|
recommendationsById
|
|
1036
1499
|
};
|
|
1037
1500
|
};
|
|
1038
1501
|
//#endregion
|
|
1502
|
+
//#region src/productCustomization/startingPrice.ts
|
|
1503
|
+
const marginalCost = (candidate) => {
|
|
1504
|
+
const { quantity, chargeAbove, unit, nested } = candidate;
|
|
1505
|
+
return quantity >= chargeAbove ? unit.plus(nested) : nested;
|
|
1506
|
+
};
|
|
1507
|
+
const pickCheapest = (candidates) => {
|
|
1508
|
+
let best = null;
|
|
1509
|
+
for (const candidate of candidates) {
|
|
1510
|
+
if (candidate.quantity >= candidate.max) continue;
|
|
1511
|
+
if (!best || marginalCost(candidate).lt(marginalCost(best))) best = candidate;
|
|
1512
|
+
}
|
|
1513
|
+
return best;
|
|
1514
|
+
};
|
|
1515
|
+
const groupCost = (group, candidates) => {
|
|
1516
|
+
const groupChargeAbove = group.charge_above_quantity ?? 0;
|
|
1517
|
+
let chargedCount = 0;
|
|
1518
|
+
let total = new Big(0);
|
|
1519
|
+
for (const candidate of candidates) {
|
|
1520
|
+
if (candidate.quantity <= 0) continue;
|
|
1521
|
+
const billable = billableQuantity(...[candidate.quantity, candidate.chargeAbove]);
|
|
1522
|
+
const freeLeft = Math.max(0, groupChargeAbove - chargedCount);
|
|
1523
|
+
const charged = billable - Math.min(billable, freeLeft);
|
|
1524
|
+
chargedCount += billable;
|
|
1525
|
+
total = total.plus(candidate.unit.times(charged));
|
|
1526
|
+
total = total.plus(candidate.nested.times(candidate.quantity));
|
|
1527
|
+
}
|
|
1528
|
+
return total;
|
|
1529
|
+
};
|
|
1530
|
+
const cheapestModifierUnit = (lookup, groupId, listed, walk) => {
|
|
1531
|
+
const { state, visiting } = walk;
|
|
1532
|
+
const options = isVariantBase(listed) ? variantCandidates(lookup, listed).map((c) => c.product) : [listed];
|
|
1533
|
+
let best = null;
|
|
1534
|
+
for (const ordered of options) {
|
|
1535
|
+
if (ordered !== listed && visiting.has(ordered.id)) {
|
|
1536
|
+
state.cycleHits += 1;
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
const unit = new Big(modifierUnitPricing(...[
|
|
1540
|
+
listed,
|
|
1541
|
+
ordered,
|
|
1542
|
+
groupId
|
|
1543
|
+
]).price);
|
|
1544
|
+
const nested = nestedModifiersCost(lookup, ordered, state, visiting);
|
|
1545
|
+
if (best && unit.plus(nested).gte(best.unit.plus(best.nested))) continue;
|
|
1546
|
+
best = {
|
|
1547
|
+
unit,
|
|
1548
|
+
nested
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
return best;
|
|
1552
|
+
};
|
|
1553
|
+
const cheapestGroupCost = (lookup, group, state, visiting) => {
|
|
1554
|
+
const candidates = [];
|
|
1555
|
+
for (const modifierId of group.modifier_ids) {
|
|
1556
|
+
const modifierProduct = lookup.productsById[modifierId];
|
|
1557
|
+
if (!modifierProduct) continue;
|
|
1558
|
+
if (visiting.has(modifierId)) {
|
|
1559
|
+
state.cycleHits += 1;
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
const pricingModifier = resolvePricingModifier(modifierProduct, group.id);
|
|
1563
|
+
const cost = cheapestModifierUnit(lookup, group.id, modifierProduct, {
|
|
1564
|
+
state,
|
|
1565
|
+
visiting
|
|
1566
|
+
});
|
|
1567
|
+
if (!cost) continue;
|
|
1568
|
+
const quantity = pricingModifier?.minimum_quantity ?? 0;
|
|
1569
|
+
const max = pricingModifier?.maximum_quantity ?? group.maximum_quantity;
|
|
1570
|
+
const chargeAbove = pricingModifier?.charge_above_quantity ?? 0;
|
|
1571
|
+
candidates.push({
|
|
1572
|
+
quantity,
|
|
1573
|
+
max,
|
|
1574
|
+
...cost,
|
|
1575
|
+
chargeAbove
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
const selected = candidates.reduce((sum, c) => sum + c.quantity, 0);
|
|
1579
|
+
let remaining = group.minimum_quantity - selected;
|
|
1580
|
+
while (remaining > 0) {
|
|
1581
|
+
const cheapest = pickCheapest(candidates);
|
|
1582
|
+
if (!cheapest) break;
|
|
1583
|
+
const { quantity, chargeAbove, max } = cheapest;
|
|
1584
|
+
const step = Math.min(remaining, (quantity < chargeAbove ? Math.min(chargeAbove, max) : max) - quantity);
|
|
1585
|
+
cheapest.quantity += step;
|
|
1586
|
+
remaining -= step;
|
|
1587
|
+
}
|
|
1588
|
+
return groupCost(group, candidates);
|
|
1589
|
+
};
|
|
1590
|
+
const nestedModifiersCost = (lookup, product, state, visiting) => {
|
|
1591
|
+
const cached = state.cache.get(product.id);
|
|
1592
|
+
if (cached) return cached;
|
|
1593
|
+
const cycleHitsBefore = state.cycleHits;
|
|
1594
|
+
const nextVisiting = new Set(visiting).add(product.id);
|
|
1595
|
+
let total = new Big(0);
|
|
1596
|
+
for (const groupId of product.pricing.modifier_group_ids) {
|
|
1597
|
+
const group = lookup.modifierGroupsById[groupId];
|
|
1598
|
+
if (!group) continue;
|
|
1599
|
+
const args = [
|
|
1600
|
+
lookup,
|
|
1601
|
+
group,
|
|
1602
|
+
state,
|
|
1603
|
+
nextVisiting
|
|
1604
|
+
];
|
|
1605
|
+
total = total.plus(cheapestGroupCost(...args));
|
|
1606
|
+
}
|
|
1607
|
+
if (state.cycleHits === cycleHitsBefore) state.cache.set(product.id, total);
|
|
1608
|
+
return total;
|
|
1609
|
+
};
|
|
1610
|
+
/** Lowest unit price reachable while satisfying every required modifier.
|
|
1611
|
+
* For a variant base, the cheapest of its variant products. */
|
|
1612
|
+
const computeStartingPrice = (lookup, product, cache = /* @__PURE__ */ new Map()) => {
|
|
1613
|
+
if (isVariantBase(product)) {
|
|
1614
|
+
const prices = variantCandidates(lookup, product).map((c) => computeStartingPrice(lookup, c.product, cache));
|
|
1615
|
+
if (prices.length > 0) return Math.min(...prices);
|
|
1616
|
+
}
|
|
1617
|
+
const modifiers = nestedModifiersCost(...[
|
|
1618
|
+
lookup,
|
|
1619
|
+
product,
|
|
1620
|
+
{
|
|
1621
|
+
cache,
|
|
1622
|
+
cycleHits: 0
|
|
1623
|
+
},
|
|
1624
|
+
/* @__PURE__ */ new Set()
|
|
1625
|
+
]);
|
|
1626
|
+
return new Big(product.pricing.price).plus(modifiers).toNumber();
|
|
1627
|
+
};
|
|
1628
|
+
//#endregion
|
|
1039
1629
|
//#region src/shared/randomUUID.ts
|
|
1040
1630
|
/** UUID v4 — works when `crypto.randomUUID` is missing (React Native / Hermes). */
|
|
1041
1631
|
const randomUUID = () => {
|
|
@@ -1209,62 +1799,44 @@ const flatten = (node, out) => {
|
|
|
1209
1799
|
out.push(line);
|
|
1210
1800
|
for (const child of children) flatten(child, out);
|
|
1211
1801
|
};
|
|
1212
|
-
const
|
|
1213
|
-
for (const modifier of
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
});
|
|
1246
|
-
config.parent.children.push(line);
|
|
1247
|
-
if (modifier.product) for (const group of modifier.product.groups) {
|
|
1248
|
-
const n1 = { modifiers: group.modifiers };
|
|
1249
|
-
const n2 = {
|
|
1250
|
-
parentUid: line.uid,
|
|
1251
|
-
parentId: line.id
|
|
1252
|
-
};
|
|
1253
|
-
const n3 = {
|
|
1254
|
-
parent: line,
|
|
1255
|
-
locale: config.locale
|
|
1256
|
-
};
|
|
1257
|
-
appendModifierLines({
|
|
1258
|
-
...n1,
|
|
1259
|
-
...n2,
|
|
1260
|
-
...n3
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1802
|
+
const appendGroupLines = (groups, parent, locale) => {
|
|
1803
|
+
for (const group of groups) for (const modifier of group.modifiers) appendModifierLine(modifier, group.data.id, parent, locale);
|
|
1804
|
+
};
|
|
1805
|
+
const appendModifierLine = (modifier, groupId, parent, locale) => {
|
|
1806
|
+
const product = modifier.resolved;
|
|
1807
|
+
if (modifier.quantity <= 0 || !product) return;
|
|
1808
|
+
const { taxRate, isTaxInclusive } = modifierUnitPricing(...[
|
|
1809
|
+
modifier.data,
|
|
1810
|
+
product,
|
|
1811
|
+
groupId
|
|
1812
|
+
]);
|
|
1813
|
+
const unitSubtotal = new Big(modifier.linePrice).div(modifier.quantity);
|
|
1814
|
+
const l1 = {
|
|
1815
|
+
product,
|
|
1816
|
+
name: displayName(product.name, locale),
|
|
1817
|
+
quantity: modifier.quantity,
|
|
1818
|
+
unitSubtotal
|
|
1819
|
+
};
|
|
1820
|
+
const l2 = {
|
|
1821
|
+
taxRate,
|
|
1822
|
+
isTaxInclusive
|
|
1823
|
+
};
|
|
1824
|
+
const l3 = {
|
|
1825
|
+
parent_uid: parent.uid,
|
|
1826
|
+
parent_id: parent.id
|
|
1827
|
+
};
|
|
1828
|
+
const line = buildLine({
|
|
1829
|
+
...l1,
|
|
1830
|
+
...l2,
|
|
1831
|
+
...l3
|
|
1832
|
+
});
|
|
1833
|
+
parent.children.push(line);
|
|
1834
|
+
if (modifier.product) appendGroupLines(modifier.product.groups, line, locale);
|
|
1264
1835
|
};
|
|
1265
1836
|
/** Flatten a customized product tree into priced OrderLine rows. */
|
|
1266
1837
|
const toOrderLines = (root, locale) => {
|
|
1267
1838
|
if (root.quantity <= 0) return [];
|
|
1839
|
+
if (isVariantBase(root.data)) return [];
|
|
1268
1840
|
const name = displayName(root.data.name, locale);
|
|
1269
1841
|
const unitSubtotal = new Big(root.data.pricing.price);
|
|
1270
1842
|
const t1 = {
|
|
@@ -1287,22 +1859,7 @@ const toOrderLines = (root, locale) => {
|
|
|
1287
1859
|
...t3,
|
|
1288
1860
|
...t4
|
|
1289
1861
|
});
|
|
1290
|
-
|
|
1291
|
-
const n1 = { modifiers: group.modifiers };
|
|
1292
|
-
const n2 = {
|
|
1293
|
-
parentUid: rootLine.uid,
|
|
1294
|
-
parentId: rootLine.id
|
|
1295
|
-
};
|
|
1296
|
-
const n3 = {
|
|
1297
|
-
parent: rootLine,
|
|
1298
|
-
locale
|
|
1299
|
-
};
|
|
1300
|
-
appendModifierLines({
|
|
1301
|
-
...n1,
|
|
1302
|
-
...n2,
|
|
1303
|
-
...n3
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1862
|
+
appendGroupLines(root.groups, rootLine, locale);
|
|
1306
1863
|
finalizeGroupMoney(rootLine);
|
|
1307
1864
|
const lines = [];
|
|
1308
1865
|
flatten(rootLine, lines);
|
|
@@ -1921,8 +2478,8 @@ const listNearbyStores = (input, options) => {
|
|
|
1921
2478
|
};
|
|
1922
2479
|
return storefrontJson(withQuery(storesPath("nearby"), query), {}, options);
|
|
1923
2480
|
};
|
|
1924
|
-
const getStore = (id, options) => {
|
|
1925
|
-
return storefrontJson(storesPath(encodeURIComponent(id)), {}, options);
|
|
2481
|
+
const getStore = (id, input = {}, options) => {
|
|
2482
|
+
return storefrontJson(withQuery(storesPath(encodeURIComponent(id)), { ...input }), {}, options);
|
|
1926
2483
|
};
|
|
1927
2484
|
//#endregion
|
|
1928
2485
|
//#region src/services/index.ts
|
|
@@ -3746,20 +4303,27 @@ const childrenOf = (lines, parentUid) => {
|
|
|
3746
4303
|
children.sort((a, b) => a.id.localeCompare(b.id));
|
|
3747
4304
|
return children;
|
|
3748
4305
|
};
|
|
3749
|
-
const matchModifier = (parent, childId, lookup) => {
|
|
3750
|
-
for (const groupId of parent.pricing.modifier_group_ids) if (lookup.modifierGroupsById[groupId]?.modifier_ids.includes(childId)) return true;
|
|
3751
|
-
return false;
|
|
3752
|
-
};
|
|
3753
4306
|
const collectUnmatchedModifiers = (lines, lookup, parentProductId, parentUid, out) => {
|
|
3754
4307
|
const parent = lookup.productsById[parentProductId];
|
|
3755
4308
|
if (!parent) return;
|
|
3756
4309
|
for (const child of lines) {
|
|
3757
4310
|
if (child.parent_uid !== parentUid) continue;
|
|
3758
4311
|
if (child.type === "FEE") continue;
|
|
3759
|
-
|
|
4312
|
+
const childProduct = lookup.productsById[child.id];
|
|
4313
|
+
if (childProduct && isVariantBase(childProduct)) {
|
|
4314
|
+
pushLineIssue(out, child.uid, { code: "VARIANT_BASE_NOT_ORDERABLE" });
|
|
4315
|
+
continue;
|
|
4316
|
+
}
|
|
4317
|
+
const match = matchModifierLine(lookup, parent, child.id);
|
|
4318
|
+
if (!match) {
|
|
3760
4319
|
pushLineIssue(out, child.uid, { code: "MODIFIER_NOT_FOUND" });
|
|
3761
4320
|
continue;
|
|
3762
4321
|
}
|
|
4322
|
+
const base = lookup.productsById[match.modifierId];
|
|
4323
|
+
if (match.selection && childProduct && base && !resolvesFromBase(lookup, base, childProduct)) {
|
|
4324
|
+
pushLineIssue(out, child.uid, { code: "VARIANT_NOT_OFFERED" });
|
|
4325
|
+
continue;
|
|
4326
|
+
}
|
|
3763
4327
|
collectUnmatchedModifiers(lines, lookup, child.id, child.uid, out);
|
|
3764
4328
|
}
|
|
3765
4329
|
};
|
|
@@ -3969,6 +4533,11 @@ const validateProductRoot = (root, lines, lookup, locale, out) => {
|
|
|
3969
4533
|
pushLineIssue(out, root.uid, { code: "PRODUCT_NOT_FOUND" });
|
|
3970
4534
|
return;
|
|
3971
4535
|
}
|
|
4536
|
+
if (isVariantBase(product)) {
|
|
4537
|
+
pushLineIssue(out, root.uid, { code: "VARIANT_BASE_NOT_ORDERABLE" });
|
|
4538
|
+
return;
|
|
4539
|
+
}
|
|
4540
|
+
if ((product.pricing.variants ?? []).length > 0 && !isOfferedVariant(lookup, product)) pushLineIssue(out, root.uid, { code: "VARIANT_NOT_OFFERED" });
|
|
3972
4541
|
const status = product.availability.status;
|
|
3973
4542
|
if (status === "HIDDEN") pushLineIssue(out, root.uid, { code: "HIDDEN" });
|
|
3974
4543
|
if (status === "OUT_OF_STOCK") pushLineIssue(out, root.uid, { code: "OUT_OF_STOCK" });
|
|
@@ -4203,6 +4772,12 @@ const issuesForTree = (lines, rootUid, lineIssues) => {
|
|
|
4203
4772
|
}
|
|
4204
4773
|
return issues;
|
|
4205
4774
|
};
|
|
4775
|
+
const UNRECOVERABLE = /* @__PURE__ */ new Set([
|
|
4776
|
+
"PRODUCT_NOT_FOUND",
|
|
4777
|
+
"HIDDEN",
|
|
4778
|
+
"VARIANT_BASE_NOT_ORDERABLE",
|
|
4779
|
+
"VARIANT_NOT_OFFERED"
|
|
4780
|
+
]);
|
|
4206
4781
|
const collectOrderLineDiffs = (input) => {
|
|
4207
4782
|
const locale = input.locale ?? "en_us";
|
|
4208
4783
|
const roots = input.lines.filter((line) => {
|
|
@@ -4215,9 +4790,7 @@ const collectOrderLineDiffs = (input) => {
|
|
|
4215
4790
|
if (issues.length === 0) continue;
|
|
4216
4791
|
const treeUids = descendantUids(input.lines, root.uid);
|
|
4217
4792
|
const previous = input.lines.filter((line) => treeUids.has(line.uid));
|
|
4218
|
-
const missing = issues.some((issue) =>
|
|
4219
|
-
return issue.code === "PRODUCT_NOT_FOUND" || issue.code === "HIDDEN";
|
|
4220
|
-
});
|
|
4793
|
+
const missing = issues.some((issue) => UNRECOVERABLE.has(issue.code));
|
|
4221
4794
|
const product = lookup?.productsById[root.id];
|
|
4222
4795
|
if (missing || !product || !lookup) {
|
|
4223
4796
|
diffs.push({
|
|
@@ -5539,4 +6112,4 @@ const paymentMatchesClientConditions = (conditions, order, options) => {
|
|
|
5539
6112
|
return Boolean(engine.matchOrConditions(groups).match);
|
|
5540
6113
|
};
|
|
5541
6114
|
//#endregion
|
|
5542
|
-
export { DEFAULT_RECOMMENDATION_TYPES, Order, OrderSyncPendingError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, collectOrderLineDiffs, countCartProducts, createSession, currentNodes, defaultConditionError, flattenMenuCategories, fromOrderLines, genId, genOrderId, getScheduleWindow, indexMenu, isCheckoutFieldFilled, isDeviceNotLinked, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isStorefrontNotFound, listMenusStream, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, paymentMatchesClientConditions, recomputeRedemptions, recomputeTotals, resolveCheckout, services, storefrontJson, toOrderLines, validateOrderAgainstMenu, visitedStates };
|
|
6115
|
+
export { DEFAULT_RECOMMENDATION_TYPES, Order, OrderSyncPendingError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, collectOrderLineDiffs, computeStartingPrice, countCartProducts, createSession, currentNodes, defaultConditionError, flattenMenuCategories, fromOrderLines, genId, genOrderId, getScheduleWindow, indexMenu, isCheckoutFieldFilled, isDeviceNotLinked, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isStorefrontNotFound, isVariantBase, listMenusStream, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, paymentMatchesClientConditions, recomputeRedemptions, recomputeTotals, resolveCheckout, services, storefrontJson, toOrderLines, validateOrderAgainstMenu, visitedStates };
|