@fire-spark/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3399 -0
- package/dist/index.js +5021 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,5021 @@
|
|
|
1
|
+
import advancedFormat from "dayjs/plugin/advancedFormat.js";
|
|
2
|
+
import isBetween from "dayjs/plugin/isBetween.js";
|
|
3
|
+
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
|
|
4
|
+
import timezone from "dayjs/plugin/timezone.js";
|
|
5
|
+
import utc from "dayjs/plugin/utc.js";
|
|
6
|
+
import weekOfYear from "dayjs/plugin/weekOfYear.js";
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
17
|
+
key = keys[i];
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
19
|
+
get: ((k) => from[k]).bind(null, key),
|
|
20
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return to;
|
|
24
|
+
};
|
|
25
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
26
|
+
value: mod,
|
|
27
|
+
enumerable: true
|
|
28
|
+
}) : target, mod));
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/productCustomization/catalog.utils.ts
|
|
31
|
+
const resolvePricingModifier = (product, groupId) => {
|
|
32
|
+
return product.pricing.modifiers.find((modifier) => modifier.modifier_group_id === groupId);
|
|
33
|
+
};
|
|
34
|
+
const inferControlType = (pricingModifier, group) => {
|
|
35
|
+
if (pricingModifier?.display_type) return pricingModifier.display_type;
|
|
36
|
+
const gMin = group.minimum_quantity;
|
|
37
|
+
const gMax = group.maximum_quantity;
|
|
38
|
+
const modifierMax = pricingModifier?.maximum_quantity;
|
|
39
|
+
if (gMin === 1 && gMax === 1) return "RADIO";
|
|
40
|
+
if (gMin === 0 && gMax === 1) return "CHECKBOX";
|
|
41
|
+
if (modifierMax === void 0 || modifierMax > 1) return "QUANTITY";
|
|
42
|
+
return "CHECKBOX";
|
|
43
|
+
};
|
|
44
|
+
const billableQuantity = (quantity, chargeAbove) => {
|
|
45
|
+
return Math.max(0, quantity - (chargeAbove ?? 0));
|
|
46
|
+
};
|
|
47
|
+
const pushUnique = (list, id) => {
|
|
48
|
+
if (!list.includes(id)) list.push(id);
|
|
49
|
+
};
|
|
50
|
+
const collectModifierIdsByGroupId = (products) => {
|
|
51
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const product of products) for (const entry of product.pricing.modifiers ?? []) {
|
|
53
|
+
const groupId = entry.modifier_group_id;
|
|
54
|
+
let list = byGroup.get(groupId);
|
|
55
|
+
if (!list) {
|
|
56
|
+
list = [];
|
|
57
|
+
byGroup.set(groupId, list);
|
|
58
|
+
}
|
|
59
|
+
pushUnique(list, product.id);
|
|
60
|
+
}
|
|
61
|
+
return byGroup;
|
|
62
|
+
};
|
|
63
|
+
const collectReferencedGroupIds = (products) => {
|
|
64
|
+
const ids = /* @__PURE__ */ new Set();
|
|
65
|
+
for (const product of products) for (const groupId of product.pricing.modifier_group_ids ?? []) ids.add(groupId);
|
|
66
|
+
return ids;
|
|
67
|
+
};
|
|
68
|
+
const inferGroupFromProducts = (groupId, modifierIds, products) => {
|
|
69
|
+
let minimum_quantity = 0;
|
|
70
|
+
let maximum_quantity = 1;
|
|
71
|
+
let charge_above_quantity = null;
|
|
72
|
+
for (const modifierId of modifierIds) {
|
|
73
|
+
const entry = products.find((entry) => entry.id === modifierId)?.pricing.modifiers?.find((modifier) => modifier.modifier_group_id === groupId);
|
|
74
|
+
if (!entry) continue;
|
|
75
|
+
minimum_quantity = Math.max(minimum_quantity, entry.minimum_quantity ?? 0);
|
|
76
|
+
maximum_quantity = Math.max(maximum_quantity, entry.maximum_quantity ?? 1);
|
|
77
|
+
if (entry.charge_above_quantity != null) charge_above_quantity = entry.charge_above_quantity;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id: groupId,
|
|
81
|
+
title: { en_us: groupId },
|
|
82
|
+
description: null,
|
|
83
|
+
display_type: "LIST",
|
|
84
|
+
modifier_ids: modifierIds,
|
|
85
|
+
minimum_quantity,
|
|
86
|
+
maximum_quantity,
|
|
87
|
+
charge_above_quantity,
|
|
88
|
+
metadata: {}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
/** Fill lookup gaps when modifier products exist but group rows were trimmed. */
|
|
92
|
+
const resolveModifierGroupsForLookup = (products, modifierGroups) => {
|
|
93
|
+
const byId = new Map(modifierGroups.map((group) => [group.id, group]));
|
|
94
|
+
const idsByGroup = collectModifierIdsByGroupId(products);
|
|
95
|
+
const referenced = collectReferencedGroupIds(products);
|
|
96
|
+
const needed = /* @__PURE__ */ new Set([...referenced, ...idsByGroup.keys()]);
|
|
97
|
+
for (const groupId of needed) {
|
|
98
|
+
if (byId.has(groupId)) continue;
|
|
99
|
+
const modifierIds = idsByGroup.get(groupId) ?? [];
|
|
100
|
+
if (modifierIds.length === 0) continue;
|
|
101
|
+
byId.set(groupId, inferGroupFromProducts(groupId, modifierIds, products));
|
|
102
|
+
}
|
|
103
|
+
return [...byId.values()];
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/productCustomization/clamp.utils.ts
|
|
107
|
+
const clampModifierQuantity = (type, quantity, min, max) => {
|
|
108
|
+
let next = Math.max(0, Math.floor(quantity));
|
|
109
|
+
if (type === "RADIO" || type === "CHECKBOX") next = next > 0 ? 1 : 0;
|
|
110
|
+
if (next === 0) return 0;
|
|
111
|
+
let lower = min;
|
|
112
|
+
if (type === "QUANTITY") lower = 0;
|
|
113
|
+
return Math.min(max, Math.max(lower, next));
|
|
114
|
+
};
|
|
115
|
+
const clampProductQuantity = (quantity, min, max) => {
|
|
116
|
+
return Math.min(max, Math.max(min, Math.max(0, Math.floor(quantity))));
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region ../../node_modules/.pnpm/big.js@6.2.1/node_modules/big.js/big.mjs
|
|
120
|
+
/************************************** EDITABLE DEFAULTS *****************************************/
|
|
121
|
+
var DP = 20;
|
|
122
|
+
var RM = 1;
|
|
123
|
+
var MAX_DP = 1e6;
|
|
124
|
+
var MAX_POWER = 1e6;
|
|
125
|
+
var NE = -7;
|
|
126
|
+
var PE = 21;
|
|
127
|
+
var STRICT = false;
|
|
128
|
+
var NAME = "[big.js] ";
|
|
129
|
+
var INVALID = NAME + "Invalid ";
|
|
130
|
+
var INVALID_DP = INVALID + "decimal places";
|
|
131
|
+
var INVALID_RM = INVALID + "rounding mode";
|
|
132
|
+
var DIV_BY_ZERO = NAME + "Division by zero";
|
|
133
|
+
var P = {};
|
|
134
|
+
var UNDEFINED = void 0;
|
|
135
|
+
var NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
|
|
136
|
+
function _Big_() {
|
|
137
|
+
function Big(n) {
|
|
138
|
+
var x = this;
|
|
139
|
+
if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n);
|
|
140
|
+
if (n instanceof Big) {
|
|
141
|
+
x.s = n.s;
|
|
142
|
+
x.e = n.e;
|
|
143
|
+
x.c = n.c.slice();
|
|
144
|
+
} else {
|
|
145
|
+
if (typeof n !== "string") {
|
|
146
|
+
if (Big.strict === true && typeof n !== "bigint") throw TypeError(INVALID + "value");
|
|
147
|
+
n = n === 0 && 1 / n < 0 ? "-0" : String(n);
|
|
148
|
+
}
|
|
149
|
+
parse(x, n);
|
|
150
|
+
}
|
|
151
|
+
x.constructor = Big;
|
|
152
|
+
}
|
|
153
|
+
Big.prototype = P;
|
|
154
|
+
Big.DP = DP;
|
|
155
|
+
Big.RM = RM;
|
|
156
|
+
Big.NE = NE;
|
|
157
|
+
Big.PE = PE;
|
|
158
|
+
Big.strict = STRICT;
|
|
159
|
+
Big.roundDown = 0;
|
|
160
|
+
Big.roundHalfUp = 1;
|
|
161
|
+
Big.roundHalfEven = 2;
|
|
162
|
+
Big.roundUp = 3;
|
|
163
|
+
return Big;
|
|
164
|
+
}
|
|
165
|
+
function parse(x, n) {
|
|
166
|
+
var e, i, nl;
|
|
167
|
+
if (!NUMERIC.test(n)) throw Error(INVALID + "number");
|
|
168
|
+
x.s = n.charAt(0) == "-" ? (n = n.slice(1), -1) : 1;
|
|
169
|
+
if ((e = n.indexOf(".")) > -1) n = n.replace(".", "");
|
|
170
|
+
if ((i = n.search(/e/i)) > 0) {
|
|
171
|
+
if (e < 0) e = i;
|
|
172
|
+
e += +n.slice(i + 1);
|
|
173
|
+
n = n.substring(0, i);
|
|
174
|
+
} else if (e < 0) e = n.length;
|
|
175
|
+
nl = n.length;
|
|
176
|
+
for (i = 0; i < nl && n.charAt(i) == "0";) ++i;
|
|
177
|
+
if (i == nl) x.c = [x.e = 0];
|
|
178
|
+
else {
|
|
179
|
+
for (; nl > 0 && n.charAt(--nl) == "0";);
|
|
180
|
+
x.e = e - i - 1;
|
|
181
|
+
x.c = [];
|
|
182
|
+
for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++);
|
|
183
|
+
}
|
|
184
|
+
return x;
|
|
185
|
+
}
|
|
186
|
+
function round(x, sd, rm, more) {
|
|
187
|
+
var xc = x.c;
|
|
188
|
+
if (rm === UNDEFINED) rm = x.constructor.RM;
|
|
189
|
+
if (rm !== 0 && rm !== 1 && rm !== 2 && rm !== 3) throw Error(INVALID_RM);
|
|
190
|
+
if (sd < 1) {
|
|
191
|
+
more = rm === 3 && (more || !!xc[0]) || sd === 0 && (rm === 1 && xc[0] >= 5 || rm === 2 && (xc[0] > 5 || xc[0] === 5 && (more || xc[1] !== UNDEFINED)));
|
|
192
|
+
xc.length = 1;
|
|
193
|
+
if (more) {
|
|
194
|
+
x.e = x.e - sd + 1;
|
|
195
|
+
xc[0] = 1;
|
|
196
|
+
} else xc[0] = x.e = 0;
|
|
197
|
+
} else if (sd < xc.length) {
|
|
198
|
+
more = rm === 1 && xc[sd] >= 5 || rm === 2 && (xc[sd] > 5 || xc[sd] === 5 && (more || xc[sd + 1] !== UNDEFINED || xc[sd - 1] & 1)) || rm === 3 && (more || !!xc[0]);
|
|
199
|
+
xc.length = sd;
|
|
200
|
+
if (more) for (; ++xc[--sd] > 9;) {
|
|
201
|
+
xc[sd] = 0;
|
|
202
|
+
if (sd === 0) {
|
|
203
|
+
++x.e;
|
|
204
|
+
xc.unshift(1);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (sd = xc.length; !xc[--sd];) xc.pop();
|
|
209
|
+
}
|
|
210
|
+
return x;
|
|
211
|
+
}
|
|
212
|
+
function stringify(x, doExponential, isNonzero) {
|
|
213
|
+
var e = x.e, s = x.c.join(""), n = s.length;
|
|
214
|
+
if (doExponential) s = s.charAt(0) + (n > 1 ? "." + s.slice(1) : "") + (e < 0 ? "e" : "e+") + e;
|
|
215
|
+
else if (e < 0) {
|
|
216
|
+
for (; ++e;) s = "0" + s;
|
|
217
|
+
s = "0." + s;
|
|
218
|
+
} else if (e > 0) {
|
|
219
|
+
if (++e > n) for (e -= n; e--;) s += "0";
|
|
220
|
+
else if (e < n) s = s.slice(0, e) + "." + s.slice(e);
|
|
221
|
+
} else if (n > 1) s = s.charAt(0) + "." + s.slice(1);
|
|
222
|
+
return x.s < 0 && isNonzero ? "-" + s : s;
|
|
223
|
+
}
|
|
224
|
+
P.abs = function() {
|
|
225
|
+
var x = new this.constructor(this);
|
|
226
|
+
x.s = 1;
|
|
227
|
+
return x;
|
|
228
|
+
};
|
|
229
|
+
P.cmp = function(y) {
|
|
230
|
+
var isneg, x = this, xc = x.c, yc = (y = new x.constructor(y)).c, i = x.s, j = y.s, k = x.e, l = y.e;
|
|
231
|
+
if (!xc[0] || !yc[0]) return !xc[0] ? !yc[0] ? 0 : -j : i;
|
|
232
|
+
if (i != j) return i;
|
|
233
|
+
isneg = i < 0;
|
|
234
|
+
if (k != l) return k > l ^ isneg ? 1 : -1;
|
|
235
|
+
j = (k = xc.length) < (l = yc.length) ? k : l;
|
|
236
|
+
for (i = -1; ++i < j;) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ isneg ? 1 : -1;
|
|
237
|
+
return k == l ? 0 : k > l ^ isneg ? 1 : -1;
|
|
238
|
+
};
|
|
239
|
+
P.div = function(y) {
|
|
240
|
+
var x = this, Big = x.constructor, a = x.c, b = (y = new Big(y)).c, k = x.s == y.s ? 1 : -1, dp = Big.DP;
|
|
241
|
+
if (dp !== ~~dp || dp < 0 || dp > MAX_DP) throw Error(INVALID_DP);
|
|
242
|
+
if (!b[0]) throw Error(DIV_BY_ZERO);
|
|
243
|
+
if (!a[0]) {
|
|
244
|
+
y.s = k;
|
|
245
|
+
y.c = [y.e = 0];
|
|
246
|
+
return y;
|
|
247
|
+
}
|
|
248
|
+
var bl, bt, n, cmp, ri, bz = b.slice(), ai = bl = b.length, al = a.length, r = a.slice(0, bl), rl = r.length, q = y, qc = q.c = [], qi = 0, p = dp + (q.e = x.e - y.e) + 1;
|
|
249
|
+
q.s = k;
|
|
250
|
+
k = p < 0 ? 0 : p;
|
|
251
|
+
bz.unshift(0);
|
|
252
|
+
for (; rl++ < bl;) r.push(0);
|
|
253
|
+
do {
|
|
254
|
+
for (n = 0; n < 10; n++) {
|
|
255
|
+
if (bl != (rl = r.length)) cmp = bl > rl ? 1 : -1;
|
|
256
|
+
else for (ri = -1, cmp = 0; ++ri < bl;) if (b[ri] != r[ri]) {
|
|
257
|
+
cmp = b[ri] > r[ri] ? 1 : -1;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
if (cmp < 0) {
|
|
261
|
+
for (bt = rl == bl ? b : bz; rl;) {
|
|
262
|
+
if (r[--rl] < bt[rl]) {
|
|
263
|
+
ri = rl;
|
|
264
|
+
for (; ri && !r[--ri];) r[ri] = 9;
|
|
265
|
+
--r[ri];
|
|
266
|
+
r[rl] += 10;
|
|
267
|
+
}
|
|
268
|
+
r[rl] -= bt[rl];
|
|
269
|
+
}
|
|
270
|
+
for (; !r[0];) r.shift();
|
|
271
|
+
} else break;
|
|
272
|
+
}
|
|
273
|
+
qc[qi++] = cmp ? n : ++n;
|
|
274
|
+
if (r[0] && cmp) r[rl] = a[ai] || 0;
|
|
275
|
+
else r = [a[ai]];
|
|
276
|
+
} while ((ai++ < al || r[0] !== UNDEFINED) && k--);
|
|
277
|
+
if (!qc[0] && qi != 1) {
|
|
278
|
+
qc.shift();
|
|
279
|
+
q.e--;
|
|
280
|
+
p--;
|
|
281
|
+
}
|
|
282
|
+
if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED);
|
|
283
|
+
return q;
|
|
284
|
+
};
|
|
285
|
+
P.eq = function(y) {
|
|
286
|
+
return this.cmp(y) === 0;
|
|
287
|
+
};
|
|
288
|
+
P.gt = function(y) {
|
|
289
|
+
return this.cmp(y) > 0;
|
|
290
|
+
};
|
|
291
|
+
P.gte = function(y) {
|
|
292
|
+
return this.cmp(y) > -1;
|
|
293
|
+
};
|
|
294
|
+
P.lt = function(y) {
|
|
295
|
+
return this.cmp(y) < 0;
|
|
296
|
+
};
|
|
297
|
+
P.lte = function(y) {
|
|
298
|
+
return this.cmp(y) < 1;
|
|
299
|
+
};
|
|
300
|
+
P.minus = P.sub = function(y) {
|
|
301
|
+
var i, j, t, xlty, x = this, Big = x.constructor, a = x.s, b = (y = new Big(y)).s;
|
|
302
|
+
if (a != b) {
|
|
303
|
+
y.s = -b;
|
|
304
|
+
return x.plus(y);
|
|
305
|
+
}
|
|
306
|
+
var xc = x.c.slice(), xe = x.e, yc = y.c, ye = y.e;
|
|
307
|
+
if (!xc[0] || !yc[0]) {
|
|
308
|
+
if (yc[0]) y.s = -b;
|
|
309
|
+
else if (xc[0]) y = new Big(x);
|
|
310
|
+
else y.s = 1;
|
|
311
|
+
return y;
|
|
312
|
+
}
|
|
313
|
+
if (a = xe - ye) {
|
|
314
|
+
if (xlty = a < 0) {
|
|
315
|
+
a = -a;
|
|
316
|
+
t = xc;
|
|
317
|
+
} else {
|
|
318
|
+
ye = xe;
|
|
319
|
+
t = yc;
|
|
320
|
+
}
|
|
321
|
+
t.reverse();
|
|
322
|
+
for (b = a; b--;) t.push(0);
|
|
323
|
+
t.reverse();
|
|
324
|
+
} else {
|
|
325
|
+
j = ((xlty = xc.length < yc.length) ? xc : yc).length;
|
|
326
|
+
for (a = b = 0; b < j; b++) if (xc[b] != yc[b]) {
|
|
327
|
+
xlty = xc[b] < yc[b];
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (xlty) {
|
|
332
|
+
t = xc;
|
|
333
|
+
xc = yc;
|
|
334
|
+
yc = t;
|
|
335
|
+
y.s = -y.s;
|
|
336
|
+
}
|
|
337
|
+
if ((b = (j = yc.length) - (i = xc.length)) > 0) for (; b--;) xc[i++] = 0;
|
|
338
|
+
for (b = i; j > a;) {
|
|
339
|
+
if (xc[--j] < yc[j]) {
|
|
340
|
+
for (i = j; i && !xc[--i];) xc[i] = 9;
|
|
341
|
+
--xc[i];
|
|
342
|
+
xc[j] += 10;
|
|
343
|
+
}
|
|
344
|
+
xc[j] -= yc[j];
|
|
345
|
+
}
|
|
346
|
+
for (; xc[--b] === 0;) xc.pop();
|
|
347
|
+
for (; xc[0] === 0;) {
|
|
348
|
+
xc.shift();
|
|
349
|
+
--ye;
|
|
350
|
+
}
|
|
351
|
+
if (!xc[0]) {
|
|
352
|
+
y.s = 1;
|
|
353
|
+
xc = [ye = 0];
|
|
354
|
+
}
|
|
355
|
+
y.c = xc;
|
|
356
|
+
y.e = ye;
|
|
357
|
+
return y;
|
|
358
|
+
};
|
|
359
|
+
P.mod = function(y) {
|
|
360
|
+
var ygtx, x = this, Big = x.constructor, a = x.s, b = (y = new Big(y)).s;
|
|
361
|
+
if (!y.c[0]) throw Error(DIV_BY_ZERO);
|
|
362
|
+
x.s = y.s = 1;
|
|
363
|
+
ygtx = y.cmp(x) == 1;
|
|
364
|
+
x.s = a;
|
|
365
|
+
y.s = b;
|
|
366
|
+
if (ygtx) return new Big(x);
|
|
367
|
+
a = Big.DP;
|
|
368
|
+
b = Big.RM;
|
|
369
|
+
Big.DP = Big.RM = 0;
|
|
370
|
+
x = x.div(y);
|
|
371
|
+
Big.DP = a;
|
|
372
|
+
Big.RM = b;
|
|
373
|
+
return this.minus(x.times(y));
|
|
374
|
+
};
|
|
375
|
+
P.neg = function() {
|
|
376
|
+
var x = new this.constructor(this);
|
|
377
|
+
x.s = -x.s;
|
|
378
|
+
return x;
|
|
379
|
+
};
|
|
380
|
+
P.plus = P.add = function(y) {
|
|
381
|
+
var e, k, t, x = this, Big = x.constructor;
|
|
382
|
+
y = new Big(y);
|
|
383
|
+
if (x.s != y.s) {
|
|
384
|
+
y.s = -y.s;
|
|
385
|
+
return x.minus(y);
|
|
386
|
+
}
|
|
387
|
+
var xe = x.e, xc = x.c, ye = y.e, yc = y.c;
|
|
388
|
+
if (!xc[0] || !yc[0]) {
|
|
389
|
+
if (!yc[0]) if (xc[0]) y = new Big(x);
|
|
390
|
+
else y.s = x.s;
|
|
391
|
+
return y;
|
|
392
|
+
}
|
|
393
|
+
xc = xc.slice();
|
|
394
|
+
if (e = xe - ye) {
|
|
395
|
+
if (e > 0) {
|
|
396
|
+
ye = xe;
|
|
397
|
+
t = yc;
|
|
398
|
+
} else {
|
|
399
|
+
e = -e;
|
|
400
|
+
t = xc;
|
|
401
|
+
}
|
|
402
|
+
t.reverse();
|
|
403
|
+
for (; e--;) t.push(0);
|
|
404
|
+
t.reverse();
|
|
405
|
+
}
|
|
406
|
+
if (xc.length - yc.length < 0) {
|
|
407
|
+
t = yc;
|
|
408
|
+
yc = xc;
|
|
409
|
+
xc = t;
|
|
410
|
+
}
|
|
411
|
+
e = yc.length;
|
|
412
|
+
for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0;
|
|
413
|
+
if (k) {
|
|
414
|
+
xc.unshift(k);
|
|
415
|
+
++ye;
|
|
416
|
+
}
|
|
417
|
+
for (e = xc.length; xc[--e] === 0;) xc.pop();
|
|
418
|
+
y.c = xc;
|
|
419
|
+
y.e = ye;
|
|
420
|
+
return y;
|
|
421
|
+
};
|
|
422
|
+
P.pow = function(n) {
|
|
423
|
+
var x = this, one = new x.constructor("1"), y = one, isneg = n < 0;
|
|
424
|
+
if (n !== ~~n || n < -1e6 || n > MAX_POWER) throw Error(INVALID + "exponent");
|
|
425
|
+
if (isneg) n = -n;
|
|
426
|
+
for (;;) {
|
|
427
|
+
if (n & 1) y = y.times(x);
|
|
428
|
+
n >>= 1;
|
|
429
|
+
if (!n) break;
|
|
430
|
+
x = x.times(x);
|
|
431
|
+
}
|
|
432
|
+
return isneg ? one.div(y) : y;
|
|
433
|
+
};
|
|
434
|
+
P.prec = function(sd, rm) {
|
|
435
|
+
if (sd !== ~~sd || sd < 1 || sd > MAX_DP) throw Error(INVALID + "precision");
|
|
436
|
+
return round(new this.constructor(this), sd, rm);
|
|
437
|
+
};
|
|
438
|
+
P.round = function(dp, rm) {
|
|
439
|
+
if (dp === UNDEFINED) dp = 0;
|
|
440
|
+
else if (dp !== ~~dp || dp < -1e6 || dp > MAX_DP) throw Error(INVALID_DP);
|
|
441
|
+
return round(new this.constructor(this), dp + this.e + 1, rm);
|
|
442
|
+
};
|
|
443
|
+
P.sqrt = function() {
|
|
444
|
+
var r, c, t, x = this, Big = x.constructor, s = x.s, e = x.e, half = new Big("0.5");
|
|
445
|
+
if (!x.c[0]) return new Big(x);
|
|
446
|
+
if (s < 0) throw Error(NAME + "No square root");
|
|
447
|
+
s = Math.sqrt(x + "");
|
|
448
|
+
if (s === 0 || s === Infinity) {
|
|
449
|
+
c = x.c.join("");
|
|
450
|
+
if (!(c.length + e & 1)) c += "0";
|
|
451
|
+
s = Math.sqrt(c);
|
|
452
|
+
e = ((e + 1) / 2 | 0) - (e < 0 || e & 1);
|
|
453
|
+
r = new Big((s == Infinity ? "5e" : (s = s.toExponential()).slice(0, s.indexOf("e") + 1)) + e);
|
|
454
|
+
} else r = new Big(s + "");
|
|
455
|
+
e = r.e + (Big.DP += 4);
|
|
456
|
+
do {
|
|
457
|
+
t = r;
|
|
458
|
+
r = half.times(t.plus(x.div(t)));
|
|
459
|
+
} while (t.c.slice(0, e).join("") !== r.c.slice(0, e).join(""));
|
|
460
|
+
return round(r, (Big.DP -= 4) + r.e + 1, Big.RM);
|
|
461
|
+
};
|
|
462
|
+
P.times = P.mul = function(y) {
|
|
463
|
+
var c, x = this, Big = x.constructor, xc = x.c, yc = (y = new Big(y)).c, a = xc.length, b = yc.length, i = x.e, j = y.e;
|
|
464
|
+
y.s = x.s == y.s ? 1 : -1;
|
|
465
|
+
if (!xc[0] || !yc[0]) {
|
|
466
|
+
y.c = [y.e = 0];
|
|
467
|
+
return y;
|
|
468
|
+
}
|
|
469
|
+
y.e = i + j;
|
|
470
|
+
if (a < b) {
|
|
471
|
+
c = xc;
|
|
472
|
+
xc = yc;
|
|
473
|
+
yc = c;
|
|
474
|
+
j = a;
|
|
475
|
+
a = b;
|
|
476
|
+
b = j;
|
|
477
|
+
}
|
|
478
|
+
for (c = new Array(j = a + b); j--;) c[j] = 0;
|
|
479
|
+
for (i = b; i--;) {
|
|
480
|
+
b = 0;
|
|
481
|
+
for (j = a + i; j > i;) {
|
|
482
|
+
b = c[j] + yc[i] * xc[j - i - 1] + b;
|
|
483
|
+
c[j--] = b % 10;
|
|
484
|
+
b = b / 10 | 0;
|
|
485
|
+
}
|
|
486
|
+
c[j] = b;
|
|
487
|
+
}
|
|
488
|
+
if (b) ++y.e;
|
|
489
|
+
else c.shift();
|
|
490
|
+
for (i = c.length; !c[--i];) c.pop();
|
|
491
|
+
y.c = c;
|
|
492
|
+
return y;
|
|
493
|
+
};
|
|
494
|
+
P.toExponential = function(dp, rm) {
|
|
495
|
+
var x = this, n = x.c[0];
|
|
496
|
+
if (dp !== UNDEFINED) {
|
|
497
|
+
if (dp !== ~~dp || dp < 0 || dp > MAX_DP) throw Error(INVALID_DP);
|
|
498
|
+
x = round(new x.constructor(x), ++dp, rm);
|
|
499
|
+
for (; x.c.length < dp;) x.c.push(0);
|
|
500
|
+
}
|
|
501
|
+
return stringify(x, true, !!n);
|
|
502
|
+
};
|
|
503
|
+
P.toFixed = function(dp, rm) {
|
|
504
|
+
var x = this, n = x.c[0];
|
|
505
|
+
if (dp !== UNDEFINED) {
|
|
506
|
+
if (dp !== ~~dp || dp < 0 || dp > MAX_DP) throw Error(INVALID_DP);
|
|
507
|
+
x = round(new x.constructor(x), dp + x.e + 1, rm);
|
|
508
|
+
for (dp = dp + x.e + 1; x.c.length < dp;) x.c.push(0);
|
|
509
|
+
}
|
|
510
|
+
return stringify(x, false, !!n);
|
|
511
|
+
};
|
|
512
|
+
P[Symbol.for("nodejs.util.inspect.custom")] = P.toJSON = P.toString = function() {
|
|
513
|
+
var x = this, Big = x.constructor;
|
|
514
|
+
return stringify(x, x.e <= Big.NE || x.e >= Big.PE, !!x.c[0]);
|
|
515
|
+
};
|
|
516
|
+
P.toNumber = function() {
|
|
517
|
+
var n = Number(stringify(this, true, true));
|
|
518
|
+
if (this.constructor.strict === true && !this.eq(n.toString())) throw Error(NAME + "Imprecise conversion");
|
|
519
|
+
return n;
|
|
520
|
+
};
|
|
521
|
+
P.toPrecision = function(sd, rm) {
|
|
522
|
+
var x = this, Big = x.constructor, n = x.c[0];
|
|
523
|
+
if (sd !== UNDEFINED) {
|
|
524
|
+
if (sd !== ~~sd || sd < 1 || sd > MAX_DP) throw Error(INVALID + "precision");
|
|
525
|
+
x = round(new Big(x), sd, rm);
|
|
526
|
+
for (; x.c.length < sd;) x.c.push(0);
|
|
527
|
+
}
|
|
528
|
+
return stringify(x, sd <= x.e || x.e <= Big.NE || x.e >= Big.PE, !!n);
|
|
529
|
+
};
|
|
530
|
+
P.valueOf = function() {
|
|
531
|
+
var x = this, Big = x.constructor;
|
|
532
|
+
if (Big.strict === true) throw Error(NAME + "valueOf disallowed");
|
|
533
|
+
return stringify(x, x.e <= Big.NE || x.e >= Big.PE, true);
|
|
534
|
+
};
|
|
535
|
+
var Big = _Big_();
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/productCustomization/pricing.ts
|
|
538
|
+
const forEachBillableUnit = (group, modifiers, onUnit) => {
|
|
539
|
+
const groupChargeAbove = group.charge_above_quantity ?? 0;
|
|
540
|
+
let chargedCount = 0;
|
|
541
|
+
for (const modifier of modifiers) {
|
|
542
|
+
if (modifier.quantity <= 0) continue;
|
|
543
|
+
const pricingModifier = resolvePricingModifier(modifier.data, group.id);
|
|
544
|
+
const unit = new Big(pricingModifier?.price ?? modifier.data.pricing.price);
|
|
545
|
+
const modifierChargeAbove = pricingModifier?.charge_above_quantity ?? 0;
|
|
546
|
+
let remaining = billableQuantity(modifier.quantity, modifierChargeAbove);
|
|
547
|
+
while (remaining > 0) {
|
|
548
|
+
onUnit(modifier, unit, chargedCount >= groupChargeAbove);
|
|
549
|
+
chargedCount += 1;
|
|
550
|
+
remaining -= 1;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const computeGroupModifierLinePrices = (group, modifiers) => {
|
|
555
|
+
const lineByPath = /* @__PURE__ */ new Map();
|
|
556
|
+
for (const modifier of modifiers) lineByPath.set(modifier.path, new Big(0));
|
|
557
|
+
forEachBillableUnit(group, modifiers, (modifier, unit, isCharged) => {
|
|
558
|
+
if (!isCharged) return;
|
|
559
|
+
lineByPath.set(modifier.path, lineByPath.get(modifier.path).plus(unit));
|
|
560
|
+
});
|
|
561
|
+
for (const modifier of modifiers) modifier.linePrice = lineByPath.get(modifier.path)?.toNumber() ?? 0;
|
|
562
|
+
};
|
|
563
|
+
const computeGroupModifiersPrice = (group, modifiers) => {
|
|
564
|
+
let total = new Big(0);
|
|
565
|
+
forEachBillableUnit(group, modifiers, (_modifier, unit, isCharged) => {
|
|
566
|
+
if (isCharged) total = total.plus(unit);
|
|
567
|
+
});
|
|
568
|
+
for (const modifier of modifiers) {
|
|
569
|
+
if (!modifier.product) continue;
|
|
570
|
+
if (modifier.quantity <= 0) continue;
|
|
571
|
+
const scaled = new Big(computeProductModifiersPrice(modifier.product)).times(modifier.quantity);
|
|
572
|
+
total = total.plus(scaled);
|
|
573
|
+
}
|
|
574
|
+
return total.toNumber();
|
|
575
|
+
};
|
|
576
|
+
const computeProductModifiersPrice = (product) => {
|
|
577
|
+
let total = new Big(0);
|
|
578
|
+
for (const group of product.groups) total = total.plus(computeGroupModifiersPrice(group.data, group.modifiers));
|
|
579
|
+
return total.toNumber();
|
|
580
|
+
};
|
|
581
|
+
const computeTotalPrice = (product) => {
|
|
582
|
+
const unit = new Big(product.data.pricing.price);
|
|
583
|
+
const mods = computeProductModifiersPrice(product);
|
|
584
|
+
return unit.plus(mods).times(product.quantity).toNumber();
|
|
585
|
+
};
|
|
586
|
+
//#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
|
+
//#region src/productCustomization/refresh.ts
|
|
604
|
+
const refreshModifierFields = (modifier, group, store) => {
|
|
605
|
+
const pricingModifier = resolvePricingModifier(modifier.data, group.data.id);
|
|
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) => {
|
|
613
|
+
product.quantity = getQuantity(store, product.path);
|
|
614
|
+
for (const group of product.groups) {
|
|
615
|
+
for (const modifier of group.modifiers) {
|
|
616
|
+
refreshModifierFields(modifier, group, store);
|
|
617
|
+
if (modifier.product) {
|
|
618
|
+
modifier.product.quantity = modifier.quantity;
|
|
619
|
+
refreshProductTree(modifier.product, store);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
computeGroupModifierLinePrices(group.data, group.modifiers);
|
|
623
|
+
group.selectedQuantity = group.modifiers.reduce((sum, modifier) => {
|
|
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
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/productCustomization/validation.ts
|
|
680
|
+
const finalize = (status, soft) => {
|
|
681
|
+
if (soft && status === "INVALID") return "INCOMPLETE";
|
|
682
|
+
return status;
|
|
683
|
+
};
|
|
684
|
+
const validateModifier = (modifier, options) => {
|
|
685
|
+
const soft = options?.soft ?? false;
|
|
686
|
+
const { quantity, min, max } = modifier;
|
|
687
|
+
let status;
|
|
688
|
+
if (quantity === 0) status = min > 0 ? "INCOMPLETE" : "IDLE";
|
|
689
|
+
else if (quantity > max) status = "INVALID";
|
|
690
|
+
else if (quantity < min) status = "INCOMPLETE";
|
|
691
|
+
else status = "VALID";
|
|
692
|
+
if (quantity > 0 && modifier.product) {
|
|
693
|
+
const nestedStatus = validateProduct(modifier.product, options);
|
|
694
|
+
if (nestedStatus === "INVALID") status = "INVALID";
|
|
695
|
+
else if (status === "VALID" && nestedStatus !== "VALID") status = "INCOMPLETE";
|
|
696
|
+
}
|
|
697
|
+
return finalize(status, soft);
|
|
698
|
+
};
|
|
699
|
+
const validateGroup = (group, options) => {
|
|
700
|
+
const soft = options?.soft ?? false;
|
|
701
|
+
const { selectedQuantity, min, max, modifiers } = group;
|
|
702
|
+
if (selectedQuantity > max) return finalize("INVALID", soft);
|
|
703
|
+
let hasInvalid = false;
|
|
704
|
+
let hasIncomplete = false;
|
|
705
|
+
for (const modifier of modifiers) {
|
|
706
|
+
modifier.status = validateModifier(modifier, options);
|
|
707
|
+
if (modifier.status === "INVALID") hasInvalid = true;
|
|
708
|
+
if (modifier.quantity > 0 && modifier.status !== "VALID") hasIncomplete = true;
|
|
709
|
+
}
|
|
710
|
+
if (hasInvalid) return finalize("INVALID", soft);
|
|
711
|
+
if (selectedQuantity < min) return "INCOMPLETE";
|
|
712
|
+
if (selectedQuantity === 0 && min === 0) return "VALID";
|
|
713
|
+
if (hasIncomplete) return "INCOMPLETE";
|
|
714
|
+
return "VALID";
|
|
715
|
+
};
|
|
716
|
+
const validateProduct = (product, options) => {
|
|
717
|
+
const soft = options?.soft ?? false;
|
|
718
|
+
const { quantity, min, max, groups } = product;
|
|
719
|
+
if (quantity === 0) {
|
|
720
|
+
product.status = min > 0 ? "INCOMPLETE" : "IDLE";
|
|
721
|
+
return product.status;
|
|
722
|
+
}
|
|
723
|
+
let hasInvalid = false;
|
|
724
|
+
let hasIncomplete = false;
|
|
725
|
+
for (const group of groups) {
|
|
726
|
+
group.selectedQuantity = group.modifiers.reduce((sum, modifier) => {
|
|
727
|
+
return sum + modifier.quantity;
|
|
728
|
+
}, 0);
|
|
729
|
+
group.status = validateGroup(group, options);
|
|
730
|
+
if (group.status === "INVALID") hasInvalid = true;
|
|
731
|
+
if (group.status !== "VALID") hasIncomplete = true;
|
|
732
|
+
}
|
|
733
|
+
if (quantity > max) {
|
|
734
|
+
product.status = finalize("INVALID", soft);
|
|
735
|
+
return product.status;
|
|
736
|
+
}
|
|
737
|
+
if (quantity < min) {
|
|
738
|
+
product.status = "INCOMPLETE";
|
|
739
|
+
return product.status;
|
|
740
|
+
}
|
|
741
|
+
if (hasInvalid) {
|
|
742
|
+
product.status = finalize("INVALID", soft);
|
|
743
|
+
return product.status;
|
|
744
|
+
}
|
|
745
|
+
if (hasIncomplete) {
|
|
746
|
+
product.status = "INCOMPLETE";
|
|
747
|
+
return product.status;
|
|
748
|
+
}
|
|
749
|
+
product.status = "VALID";
|
|
750
|
+
return product.status;
|
|
751
|
+
};
|
|
752
|
+
const runAfterChange = (runtime) => {
|
|
753
|
+
refreshProductTree(runtime.root, runtime.store);
|
|
754
|
+
validateProduct(runtime.root, { soft: true });
|
|
755
|
+
runtime.listeners.forEach((listener) => listener());
|
|
756
|
+
};
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/productCustomization/createCtx.ts
|
|
759
|
+
const createModifierCtx = (runtime, productCtx, group, modifierProduct, modifierPath) => {
|
|
760
|
+
const pricingModifier = resolvePricingModifier(...[modifierProduct, group.data.id]);
|
|
761
|
+
const type = inferControlType(pricingModifier, group.data);
|
|
762
|
+
const modifier = {
|
|
763
|
+
path: modifierPath,
|
|
764
|
+
min: pricingModifier?.minimum_quantity ?? 0,
|
|
765
|
+
max: pricingModifier?.maximum_quantity ?? group.data.maximum_quantity,
|
|
766
|
+
quantity: 0,
|
|
767
|
+
required: (pricingModifier?.minimum_quantity ?? 0) > 0,
|
|
768
|
+
status: "IDLE",
|
|
769
|
+
data: modifierProduct,
|
|
770
|
+
type,
|
|
771
|
+
product: null,
|
|
772
|
+
linePrice: 0,
|
|
773
|
+
onChange: () => {}
|
|
774
|
+
};
|
|
775
|
+
modifier.onChange = (quantity) => {
|
|
776
|
+
const next = clampModifierQuantity(...[
|
|
777
|
+
type,
|
|
778
|
+
quantity,
|
|
779
|
+
modifier.min,
|
|
780
|
+
modifier.max
|
|
781
|
+
]);
|
|
782
|
+
if (type === "RADIO" && next > 0) for (const sibling of group.modifiers) {
|
|
783
|
+
if (sibling.path === modifier.path) continue;
|
|
784
|
+
setQuantity(runtime.store, sibling.path, 0);
|
|
785
|
+
clearSubtree(runtime.store, sibling.path);
|
|
786
|
+
}
|
|
787
|
+
if (next <= 0) {
|
|
788
|
+
clearSubtree(runtime.store, modifierPath);
|
|
789
|
+
setQuantity(runtime.store, modifierPath, 0);
|
|
790
|
+
} else setQuantity(runtime.store, modifierPath, next);
|
|
791
|
+
runAfterChange(runtime);
|
|
792
|
+
};
|
|
793
|
+
if (modifierProduct.pricing.modifier_group_ids.length > 0) modifier.product = createProductCtx(...[
|
|
794
|
+
runtime,
|
|
795
|
+
modifierProduct,
|
|
796
|
+
modifierPath,
|
|
797
|
+
false
|
|
798
|
+
]);
|
|
799
|
+
return modifier;
|
|
800
|
+
};
|
|
801
|
+
const createModifierGroupCtx = (runtime, productCtx, group, productPath) => {
|
|
802
|
+
const groupPath = `${productPath}/group/${group.id}`;
|
|
803
|
+
const groupCtx = {
|
|
804
|
+
path: groupPath,
|
|
805
|
+
min: group.minimum_quantity,
|
|
806
|
+
max: group.maximum_quantity,
|
|
807
|
+
required: group.minimum_quantity > 0,
|
|
808
|
+
status: "IDLE",
|
|
809
|
+
data: group,
|
|
810
|
+
display: group.display_type ?? "LIST",
|
|
811
|
+
selectedQuantity: 0,
|
|
812
|
+
modifiers: []
|
|
813
|
+
};
|
|
814
|
+
for (const modifierId of group.modifier_ids) {
|
|
815
|
+
const modifierProduct = runtime.lookup.productsById[modifierId];
|
|
816
|
+
if (!modifierProduct) continue;
|
|
817
|
+
const modifierPath = `${groupPath}/modifier/${modifierId}`;
|
|
818
|
+
const args1 = [
|
|
819
|
+
runtime,
|
|
820
|
+
productCtx,
|
|
821
|
+
groupCtx
|
|
822
|
+
];
|
|
823
|
+
const args2 = [modifierProduct, modifierPath];
|
|
824
|
+
const ctx = createModifierCtx(...args1, ...args2);
|
|
825
|
+
groupCtx.modifiers.push(ctx);
|
|
826
|
+
}
|
|
827
|
+
return groupCtx;
|
|
828
|
+
};
|
|
829
|
+
const createProductCtx = (runtime, product, path, isRoot) => {
|
|
830
|
+
const productCtx = {
|
|
831
|
+
path,
|
|
832
|
+
min: product.pricing.minimum_quantity,
|
|
833
|
+
max: product.pricing.maximum_quantity,
|
|
834
|
+
quantity: 0,
|
|
835
|
+
required: product.pricing.minimum_quantity > 0,
|
|
836
|
+
status: "IDLE",
|
|
837
|
+
data: product,
|
|
838
|
+
groups: [],
|
|
839
|
+
onChange: () => {},
|
|
840
|
+
validate: () => "IDLE",
|
|
841
|
+
reset: () => {}
|
|
842
|
+
};
|
|
843
|
+
productCtx.groups = product.pricing.modifier_group_ids.map((groupId) => {
|
|
844
|
+
const group = runtime.lookup.modifierGroupsById[groupId];
|
|
845
|
+
if (!group) return null;
|
|
846
|
+
return createModifierGroupCtx(runtime, productCtx, group, path);
|
|
847
|
+
}).filter((entry) => entry !== null);
|
|
848
|
+
productCtx.onChange = (quantity) => {
|
|
849
|
+
const next = clampProductQuantity(quantity, productCtx.min, productCtx.max);
|
|
850
|
+
if (next <= 0) clearSubtree(runtime.store, path);
|
|
851
|
+
setQuantity(runtime.store, path, next);
|
|
852
|
+
runAfterChange(runtime);
|
|
853
|
+
};
|
|
854
|
+
productCtx.validate = (options) => {
|
|
855
|
+
refreshProductTree(runtime.root, runtime.store);
|
|
856
|
+
return validateProduct(runtime.root, options);
|
|
857
|
+
};
|
|
858
|
+
productCtx.reset = (options) => {
|
|
859
|
+
clearSubtree(runtime.store, path);
|
|
860
|
+
if (options?.force) {
|
|
861
|
+
if (isRoot) applyCatalogDefaults(runtime.store, runtime.lookup, product, path);
|
|
862
|
+
} else {
|
|
863
|
+
for (const [key, value] of Object.entries(runtime.store.initialQuantities)) if (key === path || key.startsWith(`${path}/`)) setQuantity(runtime.store, key, value);
|
|
864
|
+
if (isRoot && getQuantity(runtime.store, path) === 0) setQuantity(runtime.store, path, product.pricing.minimum_quantity);
|
|
865
|
+
}
|
|
866
|
+
runAfterChange(runtime);
|
|
867
|
+
};
|
|
868
|
+
return productCtx;
|
|
869
|
+
};
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region src/productCustomization/createSession.ts
|
|
872
|
+
const createSession = (lookup, productId, options) => {
|
|
873
|
+
const product = lookup.productsById[productId];
|
|
874
|
+
if (!product) throw new Error(`Product "${productId}" was not found in menu.`);
|
|
875
|
+
const rootPath = `/${productId}`;
|
|
876
|
+
const store = {
|
|
877
|
+
quantities: {},
|
|
878
|
+
initialQuantities: {}
|
|
879
|
+
};
|
|
880
|
+
const seed = options?.snapshot?.quantities ?? options?.initial ?? null;
|
|
881
|
+
if (seed) {
|
|
882
|
+
applyInitialQuantities(store, lookup, product, rootPath, seed);
|
|
883
|
+
store.initialQuantities = cloneQuantities(store.quantities);
|
|
884
|
+
} else {
|
|
885
|
+
applyCatalogDefaults(store, lookup, product, rootPath);
|
|
886
|
+
store.initialQuantities = cloneQuantities(store.quantities);
|
|
887
|
+
}
|
|
888
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
889
|
+
const runtime = {
|
|
890
|
+
lookup,
|
|
891
|
+
store,
|
|
892
|
+
listeners,
|
|
893
|
+
root: null
|
|
894
|
+
};
|
|
895
|
+
runtime.root = createProductCtx(runtime, product, rootPath, true);
|
|
896
|
+
refreshProductTree(runtime.root, runtime.store);
|
|
897
|
+
validateProduct(runtime.root);
|
|
898
|
+
return {
|
|
899
|
+
root: runtime.root,
|
|
900
|
+
subscribe: (listener) => {
|
|
901
|
+
listeners.add(listener);
|
|
902
|
+
return () => listeners.delete(listener);
|
|
903
|
+
},
|
|
904
|
+
snapshot: () => {
|
|
905
|
+
return {
|
|
906
|
+
productId,
|
|
907
|
+
quantities: cloneQuantities(store.quantities)
|
|
908
|
+
};
|
|
909
|
+
},
|
|
910
|
+
totalPrice: () => {
|
|
911
|
+
refreshProductTree(runtime.root, runtime.store);
|
|
912
|
+
return computeTotalPrice(runtime.root);
|
|
913
|
+
},
|
|
914
|
+
validate: (options) => {
|
|
915
|
+
refreshProductTree(runtime.root, runtime.store);
|
|
916
|
+
const status = validateProduct(runtime.root, options);
|
|
917
|
+
runtime.listeners.forEach((listener) => listener());
|
|
918
|
+
return status;
|
|
919
|
+
},
|
|
920
|
+
reset: (options) => runtime.root.reset(options)
|
|
921
|
+
};
|
|
922
|
+
};
|
|
923
|
+
//#endregion
|
|
924
|
+
//#region src/productCustomization/flattenCategories.ts
|
|
925
|
+
const nestMenuCategories = (sections) => {
|
|
926
|
+
const roots = [];
|
|
927
|
+
const stack = [];
|
|
928
|
+
for (const section of sections) {
|
|
929
|
+
while (stack.length > 0) {
|
|
930
|
+
const top = stack[stack.length - 1];
|
|
931
|
+
if (top && top.origin < section.depth) break;
|
|
932
|
+
stack.pop();
|
|
933
|
+
}
|
|
934
|
+
const b1 = {
|
|
935
|
+
category: section.category,
|
|
936
|
+
depth: stack.length
|
|
937
|
+
};
|
|
938
|
+
const b2 = {
|
|
939
|
+
instanceId: section.instanceId,
|
|
940
|
+
children: []
|
|
941
|
+
};
|
|
942
|
+
const node = {
|
|
943
|
+
...b1,
|
|
944
|
+
...b2
|
|
945
|
+
};
|
|
946
|
+
const parent = stack[stack.length - 1];
|
|
947
|
+
if (parent) parent.node.children.push(node);
|
|
948
|
+
else roots.push(node);
|
|
949
|
+
stack.push({
|
|
950
|
+
origin: section.depth,
|
|
951
|
+
node
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
return roots;
|
|
955
|
+
};
|
|
956
|
+
const flattenMenuCategories = (categories, categoriesById, options) => {
|
|
957
|
+
const inputIds = new Set(categories.map((category) => category.id));
|
|
958
|
+
const nestedInInput = /* @__PURE__ */ new Set();
|
|
959
|
+
for (const category of categories) for (const item of category.items) {
|
|
960
|
+
if (item.type === "PRODUCT") continue;
|
|
961
|
+
if (item.id === category.id) continue;
|
|
962
|
+
if (inputIds.has(item.id)) nestedInInput.add(item.id);
|
|
963
|
+
}
|
|
964
|
+
const sections = [];
|
|
965
|
+
const include = options?.include;
|
|
966
|
+
const walk = (category, ancestry) => {
|
|
967
|
+
if (ancestry.includes(category.id)) return;
|
|
968
|
+
if (include && !include(category)) return;
|
|
969
|
+
const path = [...ancestry, category.id];
|
|
970
|
+
const instanceId = path.join("/");
|
|
971
|
+
sections.push({
|
|
972
|
+
category,
|
|
973
|
+
depth: ancestry.length,
|
|
974
|
+
instanceId
|
|
975
|
+
});
|
|
976
|
+
for (const item of category.items) {
|
|
977
|
+
if (item.type === "PRODUCT") continue;
|
|
978
|
+
if (item.id === category.id) continue;
|
|
979
|
+
const child = categoriesById.get(item.id);
|
|
980
|
+
if (child) walk(child, path);
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
for (const category of categories) {
|
|
984
|
+
if (nestedInInput.has(category.id)) continue;
|
|
985
|
+
walk(category, []);
|
|
986
|
+
}
|
|
987
|
+
return sections;
|
|
988
|
+
};
|
|
989
|
+
//#endregion
|
|
990
|
+
//#region src/productCustomization/fromOrderLines.ts
|
|
991
|
+
/**
|
|
992
|
+
* Inverse of {@link toOrderLines}: rebuild a {@link Snapshot} from a root cart
|
|
993
|
+
* line so {@link createSession} can reopen it via `{ snapshot }`.
|
|
994
|
+
*/
|
|
995
|
+
const fromOrderLines = (root, lines, lookup) => {
|
|
996
|
+
const quantities = { [`/${root.id}`]: root.quantity };
|
|
997
|
+
const walk = (parentProductId, parentUid, pathPrefix) => {
|
|
998
|
+
const parentProduct = lookup.productsById[parentProductId];
|
|
999
|
+
if (!parentProduct) return;
|
|
1000
|
+
for (const child of lines) {
|
|
1001
|
+
if (child.parent_uid !== parentUid) continue;
|
|
1002
|
+
let matchedPath = null;
|
|
1003
|
+
for (const groupId of parentProduct.pricing.modifier_group_ids) {
|
|
1004
|
+
if (!lookup.modifierGroupsById[groupId]?.modifier_ids.includes(child.id)) continue;
|
|
1005
|
+
matchedPath = `${pathPrefix}/group/${groupId}/modifier/${child.id}`;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
if (!matchedPath) continue;
|
|
1009
|
+
quantities[matchedPath] = child.quantity;
|
|
1010
|
+
walk(child.id, child.uid, matchedPath);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
walk(root.id, root.uid, `/${root.id}`);
|
|
1014
|
+
return {
|
|
1015
|
+
productId: root.id,
|
|
1016
|
+
quantities
|
|
1017
|
+
};
|
|
1018
|
+
};
|
|
1019
|
+
//#endregion
|
|
1020
|
+
//#region src/productCustomization/menu.ts
|
|
1021
|
+
const indexMenu = (menu) => {
|
|
1022
|
+
const productsById = {};
|
|
1023
|
+
for (const product of menu.products) productsById[product.id] = product;
|
|
1024
|
+
const modifierGroupsById = {};
|
|
1025
|
+
const modifierGroups = resolveModifierGroupsForLookup(...[menu.products, menu.modifier_groups]);
|
|
1026
|
+
for (const group of modifierGroups) modifierGroupsById[group.id] = group;
|
|
1027
|
+
const categoriesById = {};
|
|
1028
|
+
for (const category of menu.categories) categoriesById[category.id] = category;
|
|
1029
|
+
const recommendationsById = {};
|
|
1030
|
+
for (const recommendation of menu.recommendations ?? []) recommendationsById[recommendation.id] = recommendation;
|
|
1031
|
+
return {
|
|
1032
|
+
productsById,
|
|
1033
|
+
modifierGroupsById,
|
|
1034
|
+
categoriesById,
|
|
1035
|
+
recommendationsById
|
|
1036
|
+
};
|
|
1037
|
+
};
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/shared/randomUUID.ts
|
|
1040
|
+
/** UUID v4 — works when `crypto.randomUUID` is missing (React Native / Hermes). */
|
|
1041
|
+
const randomUUID = () => {
|
|
1042
|
+
const c = globalThis.crypto;
|
|
1043
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
1044
|
+
if (typeof c?.getRandomValues !== "function") throw new Error("crypto.getRandomValues is required");
|
|
1045
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
1046
|
+
c.getRandomValues(bytes);
|
|
1047
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
1048
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1049
|
+
const toHex = (b) => b.toString(16).padStart(2, "0");
|
|
1050
|
+
const hex = Array.from(bytes, toHex).join("");
|
|
1051
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1052
|
+
};
|
|
1053
|
+
//#endregion
|
|
1054
|
+
//#region src/productCustomization/toOrderLines.ts
|
|
1055
|
+
const displayName = (text, locale) => {
|
|
1056
|
+
if (text[locale]) return text[locale];
|
|
1057
|
+
return Object.values(text)[0] ?? "";
|
|
1058
|
+
};
|
|
1059
|
+
const applyUnitMoney = (config) => {
|
|
1060
|
+
const qty = new Big(config.quantity);
|
|
1061
|
+
const unitDiscount = config.unitSubtotal.times(config.discountRate);
|
|
1062
|
+
const net = config.unitSubtotal.minus(unitDiscount);
|
|
1063
|
+
let unitTax = net.times(config.taxRate);
|
|
1064
|
+
let unitTotal = net.plus(unitTax);
|
|
1065
|
+
if (config.isTaxInclusive) {
|
|
1066
|
+
const divisor = new Big(1).plus(config.taxRate);
|
|
1067
|
+
unitTax = net.times(config.taxRate).div(divisor);
|
|
1068
|
+
unitTotal = net;
|
|
1069
|
+
}
|
|
1070
|
+
const r1 = { tax_rate: config.taxRate };
|
|
1071
|
+
const r2 = { discount_rate: config.discountRate };
|
|
1072
|
+
const r3 = { unit_subtotal: config.unitSubtotal.toNumber() };
|
|
1073
|
+
const r4 = { unit_discount: unitDiscount.toNumber() };
|
|
1074
|
+
const r5 = { unit_tax_total: unitTax.toNumber() };
|
|
1075
|
+
const r6 = { unit_total: unitTotal.toNumber() };
|
|
1076
|
+
const r7 = { subtotal: config.unitSubtotal.times(qty).toNumber() };
|
|
1077
|
+
const r8 = { discount_total: unitDiscount.times(qty).toNumber() };
|
|
1078
|
+
const r9 = { tax_total: unitTax.times(qty).toNumber() };
|
|
1079
|
+
const r10 = { total: unitTotal.times(qty).toNumber() };
|
|
1080
|
+
return {
|
|
1081
|
+
...r1,
|
|
1082
|
+
...r2,
|
|
1083
|
+
...r3,
|
|
1084
|
+
...r4,
|
|
1085
|
+
...r5,
|
|
1086
|
+
...r6,
|
|
1087
|
+
...r7,
|
|
1088
|
+
...r8,
|
|
1089
|
+
...r9,
|
|
1090
|
+
...r10
|
|
1091
|
+
};
|
|
1092
|
+
};
|
|
1093
|
+
const emptyGroupMoney = () => {
|
|
1094
|
+
const g1 = {
|
|
1095
|
+
unit_group_subtotal: 0,
|
|
1096
|
+
unit_group_discount: 0
|
|
1097
|
+
};
|
|
1098
|
+
const g2 = {
|
|
1099
|
+
unit_group_tax_total: 0,
|
|
1100
|
+
unit_group_total: 0
|
|
1101
|
+
};
|
|
1102
|
+
const g3 = {
|
|
1103
|
+
group_subtotal: 0,
|
|
1104
|
+
group_discount: 0
|
|
1105
|
+
};
|
|
1106
|
+
const g4 = {
|
|
1107
|
+
group_tax_total: 0,
|
|
1108
|
+
group_total: 0
|
|
1109
|
+
};
|
|
1110
|
+
return {
|
|
1111
|
+
...g1,
|
|
1112
|
+
...g2,
|
|
1113
|
+
...g3,
|
|
1114
|
+
...g4
|
|
1115
|
+
};
|
|
1116
|
+
};
|
|
1117
|
+
const lineCatalogFields = (product) => {
|
|
1118
|
+
return {
|
|
1119
|
+
metadata: { ...product.metadata ?? {} },
|
|
1120
|
+
bundled_items: [...product.bundled_items ?? []]
|
|
1121
|
+
};
|
|
1122
|
+
};
|
|
1123
|
+
const buildLine = (args) => {
|
|
1124
|
+
const b1 = {
|
|
1125
|
+
unitSubtotal: args.unitSubtotal,
|
|
1126
|
+
quantity: args.quantity
|
|
1127
|
+
};
|
|
1128
|
+
const b2 = {
|
|
1129
|
+
taxRate: args.taxRate,
|
|
1130
|
+
discountRate: 0
|
|
1131
|
+
};
|
|
1132
|
+
const b3 = { isTaxInclusive: args.isTaxInclusive };
|
|
1133
|
+
const moneyArgs = {
|
|
1134
|
+
...b1,
|
|
1135
|
+
...b2,
|
|
1136
|
+
...b3
|
|
1137
|
+
};
|
|
1138
|
+
const money = applyUnitMoney(moneyArgs);
|
|
1139
|
+
const imageUrl = args.product.image_url ?? void 0;
|
|
1140
|
+
const imagePatch = imageUrl ? { image_url: imageUrl } : {};
|
|
1141
|
+
const catalog = lineCatalogFields(args.product);
|
|
1142
|
+
const groupMoney = emptyGroupMoney();
|
|
1143
|
+
const r1 = {
|
|
1144
|
+
type: "PRODUCT",
|
|
1145
|
+
uid: randomUUID()
|
|
1146
|
+
};
|
|
1147
|
+
const r2 = {
|
|
1148
|
+
id: args.product.id,
|
|
1149
|
+
parent_uid: args.parent_uid
|
|
1150
|
+
};
|
|
1151
|
+
const r3 = {
|
|
1152
|
+
parent_id: args.parent_id,
|
|
1153
|
+
name: args.name
|
|
1154
|
+
};
|
|
1155
|
+
const r4 = {
|
|
1156
|
+
...imagePatch,
|
|
1157
|
+
...catalog
|
|
1158
|
+
};
|
|
1159
|
+
const r5 = {
|
|
1160
|
+
quantity: args.quantity,
|
|
1161
|
+
redeem: [],
|
|
1162
|
+
...money
|
|
1163
|
+
};
|
|
1164
|
+
const r6 = {
|
|
1165
|
+
...groupMoney,
|
|
1166
|
+
children: []
|
|
1167
|
+
};
|
|
1168
|
+
return {
|
|
1169
|
+
...r1,
|
|
1170
|
+
...r2,
|
|
1171
|
+
...r3,
|
|
1172
|
+
...r4,
|
|
1173
|
+
...r5,
|
|
1174
|
+
...r6
|
|
1175
|
+
};
|
|
1176
|
+
};
|
|
1177
|
+
const finalizeGroupMoney = (node) => {
|
|
1178
|
+
for (const child of node.children) finalizeGroupMoney(child);
|
|
1179
|
+
let groupSubtotal = new Big(node.subtotal);
|
|
1180
|
+
let groupDiscount = new Big(node.discount_total);
|
|
1181
|
+
let groupTax = new Big(node.tax_total);
|
|
1182
|
+
let groupTotal = new Big(node.total);
|
|
1183
|
+
let childSubtotal = new Big(0);
|
|
1184
|
+
let childDiscount = new Big(0);
|
|
1185
|
+
let childTax = new Big(0);
|
|
1186
|
+
let childTotal = new Big(0);
|
|
1187
|
+
for (const child of node.children) {
|
|
1188
|
+
childSubtotal = childSubtotal.plus(child.group_subtotal);
|
|
1189
|
+
childDiscount = childDiscount.plus(child.group_discount);
|
|
1190
|
+
childTax = childTax.plus(child.group_tax_total);
|
|
1191
|
+
childTotal = childTotal.plus(child.group_total);
|
|
1192
|
+
}
|
|
1193
|
+
const qty = new Big(node.quantity);
|
|
1194
|
+
groupSubtotal = groupSubtotal.plus(childSubtotal.times(qty));
|
|
1195
|
+
groupDiscount = groupDiscount.plus(childDiscount.times(qty));
|
|
1196
|
+
groupTax = groupTax.plus(childTax.times(qty));
|
|
1197
|
+
groupTotal = groupTotal.plus(childTotal.times(qty));
|
|
1198
|
+
node.group_subtotal = groupSubtotal.toNumber();
|
|
1199
|
+
node.group_discount = groupDiscount.toNumber();
|
|
1200
|
+
node.group_tax_total = groupTax.toNumber();
|
|
1201
|
+
node.unit_group_subtotal = groupSubtotal.div(qty).toNumber();
|
|
1202
|
+
node.unit_group_discount = groupDiscount.div(qty).toNumber();
|
|
1203
|
+
node.unit_group_tax_total = groupTax.div(qty).toNumber();
|
|
1204
|
+
node.group_total = groupTotal.toNumber();
|
|
1205
|
+
node.unit_group_total = groupTotal.div(qty).toNumber();
|
|
1206
|
+
};
|
|
1207
|
+
const flatten = (node, out) => {
|
|
1208
|
+
const { children, ...line } = node;
|
|
1209
|
+
out.push(line);
|
|
1210
|
+
for (const child of children) flatten(child, out);
|
|
1211
|
+
};
|
|
1212
|
+
const appendModifierLines = (config) => {
|
|
1213
|
+
for (const modifier of config.modifiers) {
|
|
1214
|
+
if (modifier.quantity <= 0) continue;
|
|
1215
|
+
const groupId = modifier.path.split("/group/")[1]?.split("/")[0];
|
|
1216
|
+
let pricing;
|
|
1217
|
+
if (groupId) pricing = resolvePricingModifier(modifier.data, groupId);
|
|
1218
|
+
const taxRate = pricing?.tax_rate ?? modifier.data.pricing.tax_rate;
|
|
1219
|
+
const fromGroup = pricing?.is_tax_inclusive;
|
|
1220
|
+
const fromData = modifier.data.pricing.is_tax_inclusive;
|
|
1221
|
+
const isTaxInclusive = fromGroup ?? fromData;
|
|
1222
|
+
let unitSubtotal = new Big(0);
|
|
1223
|
+
if (modifier.quantity > 0) unitSubtotal = new Big(modifier.linePrice).div(modifier.quantity);
|
|
1224
|
+
const name = displayName(modifier.data.name, config.locale);
|
|
1225
|
+
const l1 = {
|
|
1226
|
+
product: modifier.data,
|
|
1227
|
+
name
|
|
1228
|
+
};
|
|
1229
|
+
const l2 = {
|
|
1230
|
+
quantity: modifier.quantity,
|
|
1231
|
+
unitSubtotal
|
|
1232
|
+
};
|
|
1233
|
+
const l3 = {
|
|
1234
|
+
taxRate,
|
|
1235
|
+
isTaxInclusive
|
|
1236
|
+
};
|
|
1237
|
+
const l4 = { parent_uid: config.parentUid };
|
|
1238
|
+
const l5 = { parent_id: config.parentId };
|
|
1239
|
+
const line = buildLine({
|
|
1240
|
+
...l1,
|
|
1241
|
+
...l2,
|
|
1242
|
+
...l3,
|
|
1243
|
+
...l4,
|
|
1244
|
+
...l5
|
|
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
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
/** Flatten a customized product tree into priced OrderLine rows. */
|
|
1266
|
+
const toOrderLines = (root, locale) => {
|
|
1267
|
+
if (root.quantity <= 0) return [];
|
|
1268
|
+
const name = displayName(root.data.name, locale);
|
|
1269
|
+
const unitSubtotal = new Big(root.data.pricing.price);
|
|
1270
|
+
const t1 = {
|
|
1271
|
+
product: root.data,
|
|
1272
|
+
name,
|
|
1273
|
+
quantity: root.quantity
|
|
1274
|
+
};
|
|
1275
|
+
const t2 = {
|
|
1276
|
+
unitSubtotal,
|
|
1277
|
+
taxRate: root.data.pricing.tax_rate
|
|
1278
|
+
};
|
|
1279
|
+
const t3 = { isTaxInclusive: root.data.pricing.is_tax_inclusive };
|
|
1280
|
+
const t4 = {
|
|
1281
|
+
parent_uid: null,
|
|
1282
|
+
parent_id: null
|
|
1283
|
+
};
|
|
1284
|
+
const rootLine = buildLine({
|
|
1285
|
+
...t1,
|
|
1286
|
+
...t2,
|
|
1287
|
+
...t3,
|
|
1288
|
+
...t4
|
|
1289
|
+
});
|
|
1290
|
+
for (const group of root.groups) {
|
|
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
|
+
}
|
|
1306
|
+
finalizeGroupMoney(rootLine);
|
|
1307
|
+
const lines = [];
|
|
1308
|
+
flatten(rootLine, lines);
|
|
1309
|
+
return lines;
|
|
1310
|
+
};
|
|
1311
|
+
//#endregion
|
|
1312
|
+
//#region src/shared/baseUrl.ts
|
|
1313
|
+
const DEFAULT_BASE_URL = "https://firespark.cloud";
|
|
1314
|
+
const resolveBaseUrl = (baseUrl) => {
|
|
1315
|
+
return (baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1316
|
+
};
|
|
1317
|
+
//#endregion
|
|
1318
|
+
//#region src/auth/exchange.ts
|
|
1319
|
+
const exchangeSubjectToken = async (subjectToken, options = {}) => {
|
|
1320
|
+
const base = resolveBaseUrl(options.baseUrl);
|
|
1321
|
+
const response = await (options.fetchImpl ?? fetch)(`${base}/api/storefront/v1/oauth/exchange`, {
|
|
1322
|
+
method: "POST",
|
|
1323
|
+
headers: { "Content-Type": "application/json" },
|
|
1324
|
+
body: JSON.stringify({
|
|
1325
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
1326
|
+
subject_token: subjectToken,
|
|
1327
|
+
subject_token_type: "urn:ietf:params:oauth:token-type:id_token"
|
|
1328
|
+
})
|
|
1329
|
+
});
|
|
1330
|
+
if (!response.ok) {
|
|
1331
|
+
const message = await response.text();
|
|
1332
|
+
throw new Error(message || `Token exchange failed (${response.status})`);
|
|
1333
|
+
}
|
|
1334
|
+
const json = await response.json();
|
|
1335
|
+
if (!json.data?.access_token) throw new Error("Token exchange response missing access_token");
|
|
1336
|
+
return json.data;
|
|
1337
|
+
};
|
|
1338
|
+
//#endregion
|
|
1339
|
+
//#region src/auth/auth.ts
|
|
1340
|
+
const DEFAULT_TOKEN_SKEW_MS = 3e4;
|
|
1341
|
+
let state = null;
|
|
1342
|
+
const configure = (options) => {
|
|
1343
|
+
state = {
|
|
1344
|
+
getSubjectToken: options.getSubjectToken ?? null,
|
|
1345
|
+
getDirectToken: options.getAccessToken ?? null,
|
|
1346
|
+
baseUrl: resolveBaseUrl(options.baseUrl),
|
|
1347
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
1348
|
+
tokenSkewMs: options.tokenSkewMs ?? DEFAULT_TOKEN_SKEW_MS,
|
|
1349
|
+
accessToken: null,
|
|
1350
|
+
expiresAt: 0
|
|
1351
|
+
};
|
|
1352
|
+
};
|
|
1353
|
+
const getBaseUrl = () => {
|
|
1354
|
+
if (!state) throw new Error("auth.configure() must be called before getBaseUrl()");
|
|
1355
|
+
return state.baseUrl;
|
|
1356
|
+
};
|
|
1357
|
+
const clear = () => {
|
|
1358
|
+
if (!state) return;
|
|
1359
|
+
state.accessToken = null;
|
|
1360
|
+
state.expiresAt = 0;
|
|
1361
|
+
};
|
|
1362
|
+
const issueToken = async (current) => {
|
|
1363
|
+
if (current.getDirectToken) return current.getDirectToken();
|
|
1364
|
+
if (!current.getSubjectToken) throw new Error("auth.configure() needs getSubjectToken or getAccessToken");
|
|
1365
|
+
return exchangeSubjectToken(await current.getSubjectToken(), {
|
|
1366
|
+
baseUrl: current.baseUrl,
|
|
1367
|
+
fetchImpl: current.fetchImpl
|
|
1368
|
+
});
|
|
1369
|
+
};
|
|
1370
|
+
const getAccessToken = async () => {
|
|
1371
|
+
if (!state) throw new Error("auth.configure() must be called before getAccessToken()");
|
|
1372
|
+
const now = Date.now();
|
|
1373
|
+
if (state.accessToken && state.expiresAt > now + state.tokenSkewMs) return state.accessToken;
|
|
1374
|
+
const issued = await issueToken(state);
|
|
1375
|
+
state.accessToken = issued.access_token;
|
|
1376
|
+
state.expiresAt = now + issued.expires_in * 1e3;
|
|
1377
|
+
return state.accessToken;
|
|
1378
|
+
};
|
|
1379
|
+
//#endregion
|
|
1380
|
+
//#region src/auth/index.ts
|
|
1381
|
+
const auth = {
|
|
1382
|
+
configure,
|
|
1383
|
+
getAccessToken,
|
|
1384
|
+
getBaseUrl,
|
|
1385
|
+
clear,
|
|
1386
|
+
exchangeSubjectToken,
|
|
1387
|
+
resolveBaseUrl
|
|
1388
|
+
};
|
|
1389
|
+
//#endregion
|
|
1390
|
+
//#region src/order/customer.ts
|
|
1391
|
+
const splitName = (name) => {
|
|
1392
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
1393
|
+
const first_name = parts[0] ?? name;
|
|
1394
|
+
return {
|
|
1395
|
+
first_name,
|
|
1396
|
+
last_name: parts.length > 1 ? parts.slice(1).join(" ") : first_name,
|
|
1397
|
+
display_name: name
|
|
1398
|
+
};
|
|
1399
|
+
};
|
|
1400
|
+
/** Builds the order `customer` snapshot sent on create/update. */
|
|
1401
|
+
const buildCustomerSnapshot = (source) => {
|
|
1402
|
+
const names = splitName(source.name);
|
|
1403
|
+
return { customer_info: {
|
|
1404
|
+
display_name: names.display_name,
|
|
1405
|
+
first_name: names.first_name,
|
|
1406
|
+
last_name: names.last_name,
|
|
1407
|
+
email: source.email ?? "unknown@example.com",
|
|
1408
|
+
phone: source.phone ?? "+10000000000"
|
|
1409
|
+
} };
|
|
1410
|
+
};
|
|
1411
|
+
//#endregion
|
|
1412
|
+
//#region src/services/http.ts
|
|
1413
|
+
var StorefrontHttpError = class extends Error {
|
|
1414
|
+
status;
|
|
1415
|
+
code;
|
|
1416
|
+
constructor(message, status, code) {
|
|
1417
|
+
super(message);
|
|
1418
|
+
this.name = "StorefrontHttpError";
|
|
1419
|
+
this.status = status;
|
|
1420
|
+
this.code = code;
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
const isStorefrontNotFound = (error) => {
|
|
1424
|
+
return error instanceof StorefrontHttpError && error.status === 404;
|
|
1425
|
+
};
|
|
1426
|
+
const httpBaseUrl = (options) => {
|
|
1427
|
+
if (options.baseUrl) return resolveBaseUrl(options.baseUrl);
|
|
1428
|
+
try {
|
|
1429
|
+
return auth.getBaseUrl();
|
|
1430
|
+
} catch {
|
|
1431
|
+
return resolveBaseUrl();
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
const resolveAccessToken = async (options) => {
|
|
1435
|
+
if (options.accessToken) return options.accessToken;
|
|
1436
|
+
return auth.getAccessToken();
|
|
1437
|
+
};
|
|
1438
|
+
const withAuthRetry = async (options, run) => {
|
|
1439
|
+
const response = await run(await resolveAccessToken(options));
|
|
1440
|
+
if (response.status !== 401 || options.accessToken) return response;
|
|
1441
|
+
auth.clear();
|
|
1442
|
+
return run(await resolveAccessToken(options));
|
|
1443
|
+
};
|
|
1444
|
+
/** Low-level storefront fetch. Prefer typed services over calling this directly. */
|
|
1445
|
+
const storefrontRequest = async (path, init = {}, options = {}) => {
|
|
1446
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1447
|
+
const url = `${httpBaseUrl(options)}${path.startsWith("/") ? path : `/${path}`}`;
|
|
1448
|
+
return withAuthRetry(options, (accessToken) => {
|
|
1449
|
+
const headers = new Headers(init.headers);
|
|
1450
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
1451
|
+
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
1452
|
+
const signal = options.signal ?? init.signal;
|
|
1453
|
+
const req = {
|
|
1454
|
+
...init,
|
|
1455
|
+
headers,
|
|
1456
|
+
signal
|
|
1457
|
+
};
|
|
1458
|
+
return fetchImpl(url, req);
|
|
1459
|
+
});
|
|
1460
|
+
};
|
|
1461
|
+
const storefrontJson = async (path, init = {}, options = {}) => {
|
|
1462
|
+
const response = await storefrontRequest(path, init, options);
|
|
1463
|
+
if (!response.ok) throw new StorefrontHttpError(await response.text() || response.statusText, response.status);
|
|
1464
|
+
const json = await response.json();
|
|
1465
|
+
if (json.data === void 0) throw new Error("Storefront response missing data");
|
|
1466
|
+
return json.data;
|
|
1467
|
+
};
|
|
1468
|
+
const storefrontVoid = async (path, init = {}, options = {}) => {
|
|
1469
|
+
const response = await storefrontRequest(path, init, options);
|
|
1470
|
+
if (!response.ok) throw new StorefrontHttpError(await response.text() || response.statusText, response.status);
|
|
1471
|
+
};
|
|
1472
|
+
const parseNdjsonDataLine = (line) => {
|
|
1473
|
+
const trimmed = line.trim();
|
|
1474
|
+
if (!trimmed) return null;
|
|
1475
|
+
const json = JSON.parse(trimmed);
|
|
1476
|
+
if (json.data === void 0) throw new Error("Storefront response missing data");
|
|
1477
|
+
return json.data;
|
|
1478
|
+
};
|
|
1479
|
+
const readNdjsonLines = async function* (body) {
|
|
1480
|
+
const reader = body.getReader();
|
|
1481
|
+
const decoder = new TextDecoder();
|
|
1482
|
+
let buffer = "";
|
|
1483
|
+
try {
|
|
1484
|
+
while (true) {
|
|
1485
|
+
const { done, value } = await reader.read();
|
|
1486
|
+
if (value) buffer += decoder.decode(value, { stream: true });
|
|
1487
|
+
const lines = buffer.split("\n");
|
|
1488
|
+
buffer = lines.pop() ?? "";
|
|
1489
|
+
for (const line of lines) yield line;
|
|
1490
|
+
if (done) {
|
|
1491
|
+
if (buffer) yield buffer;
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
} finally {
|
|
1496
|
+
await reader.cancel().catch(() => void 0);
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
/** Parse an NDJSON storefront body and yield each `{ data }` payload. */
|
|
1500
|
+
const storefrontNdjson = async function* (path, init = {}, options = {}) {
|
|
1501
|
+
const response = await storefrontRequest(path, init, options);
|
|
1502
|
+
if (!response.ok) throw new StorefrontHttpError(await response.text() || response.statusText, response.status);
|
|
1503
|
+
if (response.body) {
|
|
1504
|
+
for await (const line of readNdjsonLines(response.body)) {
|
|
1505
|
+
const data = parseNdjsonDataLine(line);
|
|
1506
|
+
if (data !== null) yield data;
|
|
1507
|
+
}
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const text = await response.text();
|
|
1511
|
+
for (const line of text.split("\n")) {
|
|
1512
|
+
const data = parseNdjsonDataLine(line);
|
|
1513
|
+
if (data !== null) yield data;
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
//#endregion
|
|
1517
|
+
//#region src/services/preferences.ts
|
|
1518
|
+
const preferencesQuery = (scope) => {
|
|
1519
|
+
const params = new URLSearchParams();
|
|
1520
|
+
if (scope.fulfillment_id) params.set("fulfillment_id", scope.fulfillment_id);
|
|
1521
|
+
if (scope.store_id) params.set("store_id", scope.store_id);
|
|
1522
|
+
if (scope.channel_id) params.set("channel_id", scope.channel_id);
|
|
1523
|
+
if (scope.brand_id) params.set("brand_id", scope.brand_id);
|
|
1524
|
+
const query = params.toString();
|
|
1525
|
+
const path = "/api/storefront/v1/preferences";
|
|
1526
|
+
return query ? `${path}?${query}` : path;
|
|
1527
|
+
};
|
|
1528
|
+
const getPreferences = (scope = {}, options) => {
|
|
1529
|
+
return storefrontJson(...[
|
|
1530
|
+
preferencesQuery(scope),
|
|
1531
|
+
{},
|
|
1532
|
+
options
|
|
1533
|
+
]);
|
|
1534
|
+
};
|
|
1535
|
+
//#endregion
|
|
1536
|
+
//#region src/services/availability.ts
|
|
1537
|
+
const getAvailability = async (scope = {}, options) => {
|
|
1538
|
+
return (await getPreferences(scope, options)).availability;
|
|
1539
|
+
};
|
|
1540
|
+
//#endregion
|
|
1541
|
+
//#region src/services/cms.ts
|
|
1542
|
+
const cmsBlockPath = (name) => {
|
|
1543
|
+
return `/api/storefront/v1/cms/blocks/${encodeURIComponent(name)}`;
|
|
1544
|
+
};
|
|
1545
|
+
const getCmsBlock = (name, options) => {
|
|
1546
|
+
return storefrontJson(cmsBlockPath(name), {}, options);
|
|
1547
|
+
};
|
|
1548
|
+
//#endregion
|
|
1549
|
+
//#region src/services/customers.ts
|
|
1550
|
+
const customerPath = (id) => {
|
|
1551
|
+
return `/api/storefront/v1/customers/${encodeURIComponent(id)}`;
|
|
1552
|
+
};
|
|
1553
|
+
const getCustomer = (id, options) => {
|
|
1554
|
+
return storefrontJson(customerPath(id), {}, options);
|
|
1555
|
+
};
|
|
1556
|
+
const createCustomer = (body, options) => {
|
|
1557
|
+
return storefrontJson("/api/storefront/v1/customers", {
|
|
1558
|
+
method: "POST",
|
|
1559
|
+
body: JSON.stringify(body)
|
|
1560
|
+
}, options);
|
|
1561
|
+
};
|
|
1562
|
+
const identifyAnonymousCustomer = (id, body, options) => {
|
|
1563
|
+
return storefrontJson(`${customerPath(id)}/identify/anonymous`, {
|
|
1564
|
+
method: "PATCH",
|
|
1565
|
+
body: JSON.stringify(body)
|
|
1566
|
+
}, options);
|
|
1567
|
+
};
|
|
1568
|
+
const updateCustomer = (id, body, options) => {
|
|
1569
|
+
return storefrontJson(customerPath(id), {
|
|
1570
|
+
method: "PATCH",
|
|
1571
|
+
body: JSON.stringify(body)
|
|
1572
|
+
}, options);
|
|
1573
|
+
};
|
|
1574
|
+
const deleteCustomer = (id, options) => {
|
|
1575
|
+
return storefrontVoid(customerPath(id), { method: "DELETE" }, options);
|
|
1576
|
+
};
|
|
1577
|
+
//#endregion
|
|
1578
|
+
//#region src/services/fulfillment.ts
|
|
1579
|
+
const fulfillmentPath = (id) => {
|
|
1580
|
+
const base = "/api/storefront/v1/fulfillment";
|
|
1581
|
+
if (!id) return base;
|
|
1582
|
+
return `${base}/${encodeURIComponent(id)}`;
|
|
1583
|
+
};
|
|
1584
|
+
const withChannelQuery = (path, input = {}) => {
|
|
1585
|
+
const params = new URLSearchParams();
|
|
1586
|
+
if (input.channel_id) params.set("channel_id", input.channel_id);
|
|
1587
|
+
const query = params.toString();
|
|
1588
|
+
if (!query) return path;
|
|
1589
|
+
return `${path}?${query}`;
|
|
1590
|
+
};
|
|
1591
|
+
const listFulfillment = (input = {}, options) => {
|
|
1592
|
+
return storefrontJson(withChannelQuery(fulfillmentPath(), input), {}, options);
|
|
1593
|
+
};
|
|
1594
|
+
const getFulfillment = (id, input = {}, options) => {
|
|
1595
|
+
return storefrontJson(withChannelQuery(fulfillmentPath(id), input), {}, options);
|
|
1596
|
+
};
|
|
1597
|
+
//#endregion
|
|
1598
|
+
//#region src/services/kiosk.ts
|
|
1599
|
+
const kioskUrl = (path, options) => {
|
|
1600
|
+
const override = options?.baseUrl;
|
|
1601
|
+
const base = override ? resolveBaseUrl(override) : resolveBaseUrl();
|
|
1602
|
+
if (!path.startsWith("/")) return `${base}/${path}`;
|
|
1603
|
+
return `${base}${path}`;
|
|
1604
|
+
};
|
|
1605
|
+
const kioskFetch = async (path, init, options) => {
|
|
1606
|
+
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
1607
|
+
const headers = new Headers(init.headers);
|
|
1608
|
+
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
1609
|
+
const signal = options?.signal ?? init.signal;
|
|
1610
|
+
const req = {
|
|
1611
|
+
...init,
|
|
1612
|
+
headers,
|
|
1613
|
+
signal
|
|
1614
|
+
};
|
|
1615
|
+
return fetchImpl(kioskUrl(path, options), req);
|
|
1616
|
+
};
|
|
1617
|
+
const readError = async (response) => {
|
|
1618
|
+
const text = await response.text();
|
|
1619
|
+
try {
|
|
1620
|
+
return JSON.parse(text);
|
|
1621
|
+
} catch {
|
|
1622
|
+
return { error: text || response.statusText };
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
const isDeviceNotLinked = (error) => {
|
|
1626
|
+
if (!(error instanceof StorefrontHttpError)) return false;
|
|
1627
|
+
if (error.code === "DEVICE_NOT_LINKED") return true;
|
|
1628
|
+
if (error.status !== 404) return false;
|
|
1629
|
+
return error.message.includes("DEVICE_NOT_LINKED");
|
|
1630
|
+
};
|
|
1631
|
+
const isKioskUnauthorized = (error) => {
|
|
1632
|
+
return error instanceof StorefrontHttpError && error.status === 401;
|
|
1633
|
+
};
|
|
1634
|
+
const throwKioskError = (body, status) => {
|
|
1635
|
+
throw new StorefrontHttpError(body.error ?? "Request failed", status, body.code);
|
|
1636
|
+
};
|
|
1637
|
+
const getKioskConfig = async (input, options) => {
|
|
1638
|
+
const path = "/api/storefront/v1/kiosk/devices/link";
|
|
1639
|
+
const init = {
|
|
1640
|
+
method: "POST",
|
|
1641
|
+
body: JSON.stringify(input)
|
|
1642
|
+
};
|
|
1643
|
+
const response = await kioskFetch(path, init, options);
|
|
1644
|
+
if (!response.ok) {
|
|
1645
|
+
const body = await readError(response);
|
|
1646
|
+
throwKioskError(body, response.status);
|
|
1647
|
+
}
|
|
1648
|
+
return (await response.json()).data;
|
|
1649
|
+
};
|
|
1650
|
+
const postKioskHeartbeat = async (input, options) => {
|
|
1651
|
+
const path = "/api/storefront/v1/kiosk/devices/me";
|
|
1652
|
+
const init = {
|
|
1653
|
+
method: "POST",
|
|
1654
|
+
body: JSON.stringify(input)
|
|
1655
|
+
};
|
|
1656
|
+
const response = await kioskFetch(path, init, options);
|
|
1657
|
+
if (!response.ok) {
|
|
1658
|
+
const body = await readError(response);
|
|
1659
|
+
throwKioskError(body, response.status);
|
|
1660
|
+
}
|
|
1661
|
+
return await response.json();
|
|
1662
|
+
};
|
|
1663
|
+
const postKioskHealthSamples = async (input, options) => {
|
|
1664
|
+
const path = "/api/storefront/v1/kiosk/devices/me/health";
|
|
1665
|
+
const init = {
|
|
1666
|
+
method: "POST",
|
|
1667
|
+
body: JSON.stringify(input)
|
|
1668
|
+
};
|
|
1669
|
+
const response = await kioskFetch(path, init, options);
|
|
1670
|
+
if (!response.ok) {
|
|
1671
|
+
const body = await readError(response);
|
|
1672
|
+
throwKioskError(body, response.status);
|
|
1673
|
+
}
|
|
1674
|
+
return await response.json();
|
|
1675
|
+
};
|
|
1676
|
+
const patchKioskOverrides = async (input, options) => {
|
|
1677
|
+
const path = "/api/storefront/v1/kiosk/devices/me";
|
|
1678
|
+
const init = {
|
|
1679
|
+
method: "PATCH",
|
|
1680
|
+
body: JSON.stringify(input)
|
|
1681
|
+
};
|
|
1682
|
+
const response = await kioskFetch(path, init, options);
|
|
1683
|
+
if (!response.ok) {
|
|
1684
|
+
const body = await readError(response);
|
|
1685
|
+
throwKioskError(body, response.status);
|
|
1686
|
+
}
|
|
1687
|
+
return (await response.json()).data;
|
|
1688
|
+
};
|
|
1689
|
+
const unlinkKioskSelf = async (input, options) => {
|
|
1690
|
+
const path = "/api/storefront/v1/kiosk/devices/me";
|
|
1691
|
+
const init = {
|
|
1692
|
+
method: "DELETE",
|
|
1693
|
+
body: JSON.stringify(input)
|
|
1694
|
+
};
|
|
1695
|
+
const response = await kioskFetch(path, init, options);
|
|
1696
|
+
if (!response.ok) {
|
|
1697
|
+
const body = await readError(response);
|
|
1698
|
+
throwKioskError(body, response.status);
|
|
1699
|
+
}
|
|
1700
|
+
return (await response.json()).data;
|
|
1701
|
+
};
|
|
1702
|
+
/** 403 wrong or expired code, 429 too many tries for the current code. */
|
|
1703
|
+
const verifyKioskAdminCode = async (input, options) => {
|
|
1704
|
+
const path = "/api/storefront/v1/kiosk/devices/me/admin-code";
|
|
1705
|
+
const init = {
|
|
1706
|
+
method: "POST",
|
|
1707
|
+
body: JSON.stringify(input)
|
|
1708
|
+
};
|
|
1709
|
+
const response = await kioskFetch(path, init, options);
|
|
1710
|
+
if (!response.ok) {
|
|
1711
|
+
const body = await readError(response);
|
|
1712
|
+
throwKioskError(body, response.status);
|
|
1713
|
+
}
|
|
1714
|
+
return (await response.json()).data;
|
|
1715
|
+
};
|
|
1716
|
+
const isKioskAdminCodeInvalid = (error) => {
|
|
1717
|
+
return error instanceof StorefrontHttpError && error.status === 403;
|
|
1718
|
+
};
|
|
1719
|
+
const isKioskAdminCodeLocked = (error) => {
|
|
1720
|
+
return error instanceof StorefrontHttpError && error.status === 429;
|
|
1721
|
+
};
|
|
1722
|
+
const fetchKioskRealtimeToken = async (input, options) => {
|
|
1723
|
+
const path = "/api/storefront/v1/kiosk/realtime/token";
|
|
1724
|
+
const init = {
|
|
1725
|
+
method: "POST",
|
|
1726
|
+
body: JSON.stringify(input)
|
|
1727
|
+
};
|
|
1728
|
+
const response = await kioskFetch(path, init, options);
|
|
1729
|
+
if (!response.ok) {
|
|
1730
|
+
const body = await readError(response);
|
|
1731
|
+
throwKioskError(body, response.status);
|
|
1732
|
+
}
|
|
1733
|
+
return (await response.json()).data;
|
|
1734
|
+
};
|
|
1735
|
+
//#endregion
|
|
1736
|
+
//#region src/services/menus.ts
|
|
1737
|
+
const menuQuery = (scope, stream) => {
|
|
1738
|
+
const params = new URLSearchParams();
|
|
1739
|
+
if (scope.fulfillment_id) params.set("fulfillment_id", scope.fulfillment_id);
|
|
1740
|
+
if (scope.store_id) params.set("store_id", scope.store_id);
|
|
1741
|
+
if (scope.channel_id) params.set("channel_id", scope.channel_id);
|
|
1742
|
+
if (scope.brand_id) params.set("brand_id", scope.brand_id);
|
|
1743
|
+
if (stream?.streaming) params.set("streaming", "true");
|
|
1744
|
+
if (stream?.category_limit != null) params.set("category_limit", String(stream.category_limit));
|
|
1745
|
+
if (stream?.product_limit != null) params.set("product_limit", String(stream.product_limit));
|
|
1746
|
+
const query = params.toString();
|
|
1747
|
+
return `/api/storefront/v1/menus${query ? `?${query}` : ""}`;
|
|
1748
|
+
};
|
|
1749
|
+
const listMenus = (scope = {}, options) => {
|
|
1750
|
+
return storefrontJson(menuQuery(scope), {}, options);
|
|
1751
|
+
};
|
|
1752
|
+
const listMenusStream = (scope = {}, options = {}) => {
|
|
1753
|
+
const { category_limit, product_limit, ...http } = options;
|
|
1754
|
+
return storefrontNdjson(menuQuery(scope, {
|
|
1755
|
+
streaming: true,
|
|
1756
|
+
category_limit,
|
|
1757
|
+
product_limit
|
|
1758
|
+
}), {}, http);
|
|
1759
|
+
};
|
|
1760
|
+
//#endregion
|
|
1761
|
+
//#region src/order/OrderValidationError.ts
|
|
1762
|
+
var OrderValidationError = class extends Error {
|
|
1763
|
+
issues;
|
|
1764
|
+
lines;
|
|
1765
|
+
constructor(issues, lines) {
|
|
1766
|
+
super("Order validation failed");
|
|
1767
|
+
this.name = "OrderValidationError";
|
|
1768
|
+
this.issues = issues;
|
|
1769
|
+
this.lines = lines;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
const isOrderValidationError = (error) => {
|
|
1773
|
+
return error instanceof OrderValidationError;
|
|
1774
|
+
};
|
|
1775
|
+
//#endregion
|
|
1776
|
+
//#region src/services/orders.ts
|
|
1777
|
+
const orderPath = (id) => {
|
|
1778
|
+
const base = "/api/storefront/v1/orders";
|
|
1779
|
+
if (!id) return base;
|
|
1780
|
+
return `${base}/${encodeURIComponent(id)}`;
|
|
1781
|
+
};
|
|
1782
|
+
const listQuery = (input = {}) => {
|
|
1783
|
+
const params = new URLSearchParams();
|
|
1784
|
+
if (input.store_id) params.set("store_id", input.store_id);
|
|
1785
|
+
if (input.channel_id) params.set("channel_id", input.channel_id);
|
|
1786
|
+
if (input.fulfillment_id) params.set("fulfillment_id", input.fulfillment_id);
|
|
1787
|
+
if (input.brand_id) params.set("brand_id", input.brand_id);
|
|
1788
|
+
if (input.status) params.set("status", input.status);
|
|
1789
|
+
if (input.limit != null) params.set("limit", String(input.limit));
|
|
1790
|
+
if (input.cursor) params.set("cursor", input.cursor);
|
|
1791
|
+
const query = params.toString();
|
|
1792
|
+
if (!query) return orderPath();
|
|
1793
|
+
return `${orderPath()}?${query}`;
|
|
1794
|
+
};
|
|
1795
|
+
const listOrders = (input = {}, options) => {
|
|
1796
|
+
return storefrontJson(listQuery(input), {}, options);
|
|
1797
|
+
};
|
|
1798
|
+
const createOrder = (body, options) => {
|
|
1799
|
+
return storefrontJson(orderPath(), {
|
|
1800
|
+
method: "POST",
|
|
1801
|
+
body: JSON.stringify(body)
|
|
1802
|
+
}, options);
|
|
1803
|
+
};
|
|
1804
|
+
const getOrder = (id, options) => {
|
|
1805
|
+
return storefrontJson(orderPath(id), {}, options);
|
|
1806
|
+
};
|
|
1807
|
+
const placeOrder = (id, body = {}, options) => {
|
|
1808
|
+
const init = {
|
|
1809
|
+
method: "POST",
|
|
1810
|
+
body: JSON.stringify(body)
|
|
1811
|
+
};
|
|
1812
|
+
return storefrontJson(...[
|
|
1813
|
+
`${orderPath(id)}/place`,
|
|
1814
|
+
init,
|
|
1815
|
+
options
|
|
1816
|
+
]);
|
|
1817
|
+
};
|
|
1818
|
+
const getOrderFulfillment = (id, options) => {
|
|
1819
|
+
return storefrontJson(...[
|
|
1820
|
+
`${orderPath(id)}/fulfillment`,
|
|
1821
|
+
{},
|
|
1822
|
+
options
|
|
1823
|
+
]);
|
|
1824
|
+
};
|
|
1825
|
+
const LIST_FULFILLMENT_LIMIT = 20;
|
|
1826
|
+
const listOrderFulfillments = async (ids, options) => {
|
|
1827
|
+
if (ids.length === 0) return [];
|
|
1828
|
+
const unique = [...new Set(ids)];
|
|
1829
|
+
const pages = [];
|
|
1830
|
+
for (let i = 0; i < unique.length; i += LIST_FULFILLMENT_LIMIT) pages.push(unique.slice(i, i + LIST_FULFILLMENT_LIMIT));
|
|
1831
|
+
return (await Promise.all(pages.map((page) => {
|
|
1832
|
+
const params = new URLSearchParams();
|
|
1833
|
+
params.set("ids", page.join(","));
|
|
1834
|
+
return storefrontJson(`${orderPath()}/fulfillments?${params.toString()}`, {}, options);
|
|
1835
|
+
}))).flat();
|
|
1836
|
+
};
|
|
1837
|
+
const updateOrder = (id, body, options) => {
|
|
1838
|
+
return storefrontJson(orderPath(id), {
|
|
1839
|
+
method: "PATCH",
|
|
1840
|
+
body: JSON.stringify(body)
|
|
1841
|
+
}, options);
|
|
1842
|
+
};
|
|
1843
|
+
const cancelOrder = (id, options) => {
|
|
1844
|
+
return storefrontJson(`${orderPath(id)}/cancel`, { method: "POST" }, options);
|
|
1845
|
+
};
|
|
1846
|
+
const refundOrder = (id, options) => {
|
|
1847
|
+
return storefrontJson(`${orderPath(id)}/refund`, { method: "POST" }, options);
|
|
1848
|
+
};
|
|
1849
|
+
const reorderOrder = (id, body = {}, options) => {
|
|
1850
|
+
return storefrontJson(`${orderPath(id)}/reorder`, {
|
|
1851
|
+
method: "POST",
|
|
1852
|
+
body: JSON.stringify(body)
|
|
1853
|
+
}, options);
|
|
1854
|
+
};
|
|
1855
|
+
const parseValidationDetails = (body) => {
|
|
1856
|
+
if (!body || typeof body !== "object") return null;
|
|
1857
|
+
const details = body.details;
|
|
1858
|
+
if (!details || typeof details !== "object") return null;
|
|
1859
|
+
const record = details;
|
|
1860
|
+
if (!Array.isArray(record.issues) || !record.lines) return null;
|
|
1861
|
+
return {
|
|
1862
|
+
issues: record.issues,
|
|
1863
|
+
lines: record.lines
|
|
1864
|
+
};
|
|
1865
|
+
};
|
|
1866
|
+
const validateOrder = async (body, options) => {
|
|
1867
|
+
const response = await storefrontRequest(orderPath("validate"), {
|
|
1868
|
+
method: "POST",
|
|
1869
|
+
body: JSON.stringify(body)
|
|
1870
|
+
}, options);
|
|
1871
|
+
if (response.ok) {
|
|
1872
|
+
const json = await response.json();
|
|
1873
|
+
if (json.data === void 0) throw new Error("Storefront response missing data");
|
|
1874
|
+
return json.data;
|
|
1875
|
+
}
|
|
1876
|
+
let parsed = null;
|
|
1877
|
+
const text = await response.text();
|
|
1878
|
+
try {
|
|
1879
|
+
parsed = JSON.parse(text);
|
|
1880
|
+
} catch {
|
|
1881
|
+
parsed = null;
|
|
1882
|
+
}
|
|
1883
|
+
if (response.status === 422) {
|
|
1884
|
+
const details = parseValidationDetails(parsed);
|
|
1885
|
+
if (details) throw new OrderValidationError(details.issues, details.lines);
|
|
1886
|
+
}
|
|
1887
|
+
throw new StorefrontHttpError(text || response.statusText, response.status);
|
|
1888
|
+
};
|
|
1889
|
+
//#endregion
|
|
1890
|
+
//#region src/services/payments.ts
|
|
1891
|
+
const listPayments = (input, options) => {
|
|
1892
|
+
const params = new URLSearchParams();
|
|
1893
|
+
for (const [key, value] of Object.entries(input)) if (value) params.set(key, value);
|
|
1894
|
+
return storefrontJson(`/api/storefront/v1/payments?${params.toString()}`, {}, options);
|
|
1895
|
+
};
|
|
1896
|
+
//#endregion
|
|
1897
|
+
//#region src/services/stores.ts
|
|
1898
|
+
const storesPath = (suffix) => {
|
|
1899
|
+
const base = "/api/storefront/v1/stores";
|
|
1900
|
+
if (!suffix) return base;
|
|
1901
|
+
return `${base}/${suffix}`;
|
|
1902
|
+
};
|
|
1903
|
+
const withQuery = (path, entries) => {
|
|
1904
|
+
const params = new URLSearchParams();
|
|
1905
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
1906
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
1907
|
+
params.set(key, String(value));
|
|
1908
|
+
}
|
|
1909
|
+
const query = params.toString();
|
|
1910
|
+
if (!query) return path;
|
|
1911
|
+
return `${path}?${query}`;
|
|
1912
|
+
};
|
|
1913
|
+
/** Active stores of the merchant/env in the access token (one page). */
|
|
1914
|
+
const listStores = (input = {}, options) => {
|
|
1915
|
+
return storefrontJson(withQuery(storesPath(), { ...input }), {}, options);
|
|
1916
|
+
};
|
|
1917
|
+
const listNearbyStores = (input, options) => {
|
|
1918
|
+
const query = {
|
|
1919
|
+
...input,
|
|
1920
|
+
limit: input.limit ?? 20
|
|
1921
|
+
};
|
|
1922
|
+
return storefrontJson(withQuery(storesPath("nearby"), query), {}, options);
|
|
1923
|
+
};
|
|
1924
|
+
const getStore = (id, options) => {
|
|
1925
|
+
return storefrontJson(storesPath(encodeURIComponent(id)), {}, options);
|
|
1926
|
+
};
|
|
1927
|
+
//#endregion
|
|
1928
|
+
//#region src/services/index.ts
|
|
1929
|
+
const services = {
|
|
1930
|
+
preferences: { get: getPreferences },
|
|
1931
|
+
availability: { get: getAvailability },
|
|
1932
|
+
cms: { getBlock: getCmsBlock },
|
|
1933
|
+
customers: {
|
|
1934
|
+
get: getCustomer,
|
|
1935
|
+
post: createCustomer,
|
|
1936
|
+
patch: updateCustomer,
|
|
1937
|
+
delete: deleteCustomer,
|
|
1938
|
+
identifyAnonymous: identifyAnonymousCustomer
|
|
1939
|
+
},
|
|
1940
|
+
fulfillment: {
|
|
1941
|
+
list: listFulfillment,
|
|
1942
|
+
get: getFulfillment
|
|
1943
|
+
},
|
|
1944
|
+
menus: {
|
|
1945
|
+
get: listMenus,
|
|
1946
|
+
stream: listMenusStream
|
|
1947
|
+
},
|
|
1948
|
+
orders: {
|
|
1949
|
+
list: listOrders,
|
|
1950
|
+
post: createOrder,
|
|
1951
|
+
get: getOrder,
|
|
1952
|
+
patch: updateOrder,
|
|
1953
|
+
cancel: cancelOrder,
|
|
1954
|
+
refund: refundOrder,
|
|
1955
|
+
reorder: reorderOrder,
|
|
1956
|
+
validate: validateOrder,
|
|
1957
|
+
place: placeOrder,
|
|
1958
|
+
getFulfillment: getOrderFulfillment,
|
|
1959
|
+
listFulfillments: listOrderFulfillments
|
|
1960
|
+
},
|
|
1961
|
+
payments: { list: listPayments },
|
|
1962
|
+
stores: {
|
|
1963
|
+
list: listStores,
|
|
1964
|
+
nearby: listNearbyStores,
|
|
1965
|
+
get: getStore
|
|
1966
|
+
},
|
|
1967
|
+
kiosk: {
|
|
1968
|
+
getConfig: getKioskConfig,
|
|
1969
|
+
heartbeat: postKioskHeartbeat,
|
|
1970
|
+
postHealthSamples: postKioskHealthSamples,
|
|
1971
|
+
patchOverrides: patchKioskOverrides,
|
|
1972
|
+
realtimeToken: fetchKioskRealtimeToken,
|
|
1973
|
+
unlink: unlinkKioskSelf,
|
|
1974
|
+
verifyAdminCode: verifyKioskAdminCode,
|
|
1975
|
+
isAdminCodeInvalid: isKioskAdminCodeInvalid,
|
|
1976
|
+
isAdminCodeLocked: isKioskAdminCodeLocked,
|
|
1977
|
+
isDeviceNotLinked,
|
|
1978
|
+
isKioskUnauthorized
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
//#endregion
|
|
1982
|
+
//#region src/order/retry.ts
|
|
1983
|
+
const defaultSleep = (ms) => {
|
|
1984
|
+
return new Promise((resolve) => {
|
|
1985
|
+
setTimeout(resolve, ms);
|
|
1986
|
+
});
|
|
1987
|
+
};
|
|
1988
|
+
/** 1 attempt + `extraAttempts` retries with exponential backoff. */
|
|
1989
|
+
const withExponentialRetry = async (run, options = {}) => {
|
|
1990
|
+
const extraAttempts = options.extraAttempts ?? 3;
|
|
1991
|
+
const baseDelayMs = options.baseDelayMs ?? 100;
|
|
1992
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
1993
|
+
let lastError;
|
|
1994
|
+
for (let attempt = 0; attempt <= extraAttempts; attempt += 1) try {
|
|
1995
|
+
return await run();
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
lastError = error;
|
|
1998
|
+
if (attempt === extraAttempts) break;
|
|
1999
|
+
await sleep(baseDelayMs * 2 ** attempt);
|
|
2000
|
+
}
|
|
2001
|
+
throw lastError;
|
|
2002
|
+
};
|
|
2003
|
+
//#endregion
|
|
2004
|
+
//#region src/order/flush.ts
|
|
2005
|
+
var OrderSyncPendingError = class extends Error {
|
|
2006
|
+
constructor() {
|
|
2007
|
+
super("Order changes are not synced yet");
|
|
2008
|
+
this.name = "OrderSyncPendingError";
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
const createFlushController = (runtime) => {
|
|
2012
|
+
let timer = null;
|
|
2013
|
+
let inFlight = false;
|
|
2014
|
+
let dirtyDuringFlight = false;
|
|
2015
|
+
let current = null;
|
|
2016
|
+
let pending = false;
|
|
2017
|
+
const cancel = () => {
|
|
2018
|
+
if (timer) clearTimeout(timer);
|
|
2019
|
+
timer = null;
|
|
2020
|
+
dirtyDuringFlight = false;
|
|
2021
|
+
pending = false;
|
|
2022
|
+
};
|
|
2023
|
+
const patchBody = (order) => {
|
|
2024
|
+
const b1 = {
|
|
2025
|
+
store_id: order.store_id,
|
|
2026
|
+
channel_id: order.channel_id
|
|
2027
|
+
};
|
|
2028
|
+
const b2 = { fulfillment_id: order.fulfillment_id };
|
|
2029
|
+
const b3 = {
|
|
2030
|
+
brand_id: order.brand_id,
|
|
2031
|
+
lines: order.lines
|
|
2032
|
+
};
|
|
2033
|
+
const b4 = {
|
|
2034
|
+
totals: order.totals,
|
|
2035
|
+
metadata: order.metadata
|
|
2036
|
+
};
|
|
2037
|
+
const b5 = { customer: order.customer };
|
|
2038
|
+
const b6 = { payment_intents: order.payment_intents };
|
|
2039
|
+
const b7 = { payment_status: order.payment_status };
|
|
2040
|
+
return {
|
|
2041
|
+
...b1,
|
|
2042
|
+
...b2,
|
|
2043
|
+
...b3,
|
|
2044
|
+
...b4,
|
|
2045
|
+
...b5,
|
|
2046
|
+
...b6,
|
|
2047
|
+
...b7
|
|
2048
|
+
};
|
|
2049
|
+
};
|
|
2050
|
+
const insertBody = (order) => {
|
|
2051
|
+
const b1 = {
|
|
2052
|
+
id: order.id,
|
|
2053
|
+
store_id: order.store_id
|
|
2054
|
+
};
|
|
2055
|
+
const b2 = { channel_id: order.channel_id };
|
|
2056
|
+
const b3 = { fulfillment_id: order.fulfillment_id };
|
|
2057
|
+
const b4 = {
|
|
2058
|
+
brand_id: order.brand_id,
|
|
2059
|
+
is_anonymous: order.is_anonymous
|
|
2060
|
+
};
|
|
2061
|
+
const b5 = {
|
|
2062
|
+
customer: order.customer,
|
|
2063
|
+
lines: order.lines
|
|
2064
|
+
};
|
|
2065
|
+
const b6 = {
|
|
2066
|
+
totals: order.totals,
|
|
2067
|
+
metadata: order.metadata
|
|
2068
|
+
};
|
|
2069
|
+
const b7 = { payment_intents: order.payment_intents };
|
|
2070
|
+
const b8 = { payment_status: order.payment_status };
|
|
2071
|
+
return {
|
|
2072
|
+
...b1,
|
|
2073
|
+
...b2,
|
|
2074
|
+
...b3,
|
|
2075
|
+
...b4,
|
|
2076
|
+
...b5,
|
|
2077
|
+
...b6,
|
|
2078
|
+
...b7,
|
|
2079
|
+
...b8
|
|
2080
|
+
};
|
|
2081
|
+
};
|
|
2082
|
+
const flushNow = () => {
|
|
2083
|
+
if (runtime.isDeleted()) return Promise.resolve();
|
|
2084
|
+
if (inFlight) return current ?? Promise.resolve();
|
|
2085
|
+
current = run().finally(() => {
|
|
2086
|
+
current = null;
|
|
2087
|
+
});
|
|
2088
|
+
return current;
|
|
2089
|
+
};
|
|
2090
|
+
const drain = async () => {
|
|
2091
|
+
for (;;) {
|
|
2092
|
+
if (runtime.isDeleted()) return;
|
|
2093
|
+
if (current) {
|
|
2094
|
+
await current;
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
if (!timer) return;
|
|
2098
|
+
clearTimeout(timer);
|
|
2099
|
+
timer = null;
|
|
2100
|
+
await flushNow();
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
const settle = async () => {
|
|
2104
|
+
await drain();
|
|
2105
|
+
if (!pending || runtime.isDeleted()) return;
|
|
2106
|
+
await flushNow();
|
|
2107
|
+
await drain();
|
|
2108
|
+
if (pending) throw new OrderSyncPendingError();
|
|
2109
|
+
};
|
|
2110
|
+
const resume = async () => {
|
|
2111
|
+
if (!pending) return;
|
|
2112
|
+
await flushNow();
|
|
2113
|
+
};
|
|
2114
|
+
const run = async () => {
|
|
2115
|
+
const batchBase = structuredClone(runtime.getSynced());
|
|
2116
|
+
const payload = structuredClone(runtime.getDraft());
|
|
2117
|
+
const wasPersisted = runtime.isPersisted();
|
|
2118
|
+
inFlight = true;
|
|
2119
|
+
dirtyDuringFlight = false;
|
|
2120
|
+
try {
|
|
2121
|
+
const updated = await withExponentialRetry(() => {
|
|
2122
|
+
if (!wasPersisted) return services.orders.post(insertBody(payload), runtime.http);
|
|
2123
|
+
const p1 = [
|
|
2124
|
+
payload.id,
|
|
2125
|
+
patchBody(payload),
|
|
2126
|
+
runtime.http
|
|
2127
|
+
];
|
|
2128
|
+
return services.orders.patch(...p1);
|
|
2129
|
+
}, { sleep: runtime.sleep });
|
|
2130
|
+
pending = false;
|
|
2131
|
+
runtime.setPersisted(true);
|
|
2132
|
+
runtime.setSynced(updated);
|
|
2133
|
+
const reusedOpen = !wasPersisted && updated.id !== payload.id;
|
|
2134
|
+
const keepLocal = payload.lines.length > 0 || runtime.fresh === true;
|
|
2135
|
+
if (dirtyDuringFlight || reusedOpen && keepLocal) {
|
|
2136
|
+
const d1 = {
|
|
2137
|
+
...runtime.getDraft(),
|
|
2138
|
+
uid: updated.uid
|
|
2139
|
+
};
|
|
2140
|
+
const d2 = {
|
|
2141
|
+
id: updated.id,
|
|
2142
|
+
customer_id: updated.customer_id
|
|
2143
|
+
};
|
|
2144
|
+
runtime.setDraft({
|
|
2145
|
+
...d1,
|
|
2146
|
+
...d2
|
|
2147
|
+
});
|
|
2148
|
+
inFlight = false;
|
|
2149
|
+
runtime.notify();
|
|
2150
|
+
schedule();
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
runtime.setDraft(updated);
|
|
2154
|
+
inFlight = false;
|
|
2155
|
+
runtime.notify();
|
|
2156
|
+
} catch (error) {
|
|
2157
|
+
if (!(error instanceof StorefrontHttpError)) {
|
|
2158
|
+
pending = true;
|
|
2159
|
+
dirtyDuringFlight = false;
|
|
2160
|
+
inFlight = false;
|
|
2161
|
+
runtime.notify();
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
pending = false;
|
|
2165
|
+
runtime.setDraft(batchBase);
|
|
2166
|
+
runtime.setSynced(batchBase);
|
|
2167
|
+
runtime.setPersisted(wasPersisted);
|
|
2168
|
+
dirtyDuringFlight = false;
|
|
2169
|
+
inFlight = false;
|
|
2170
|
+
if (timer) clearTimeout(timer);
|
|
2171
|
+
timer = null;
|
|
2172
|
+
runtime.notify();
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
const schedule = () => {
|
|
2176
|
+
if (runtime.isDeleted()) return;
|
|
2177
|
+
if (inFlight) {
|
|
2178
|
+
dirtyDuringFlight = true;
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
if (timer) clearTimeout(timer);
|
|
2182
|
+
timer = setTimeout(() => {
|
|
2183
|
+
timer = null;
|
|
2184
|
+
flushNow();
|
|
2185
|
+
}, runtime.debounceMs);
|
|
2186
|
+
};
|
|
2187
|
+
const markDirtyDuringFlight = () => {
|
|
2188
|
+
if (inFlight) dirtyDuringFlight = true;
|
|
2189
|
+
};
|
|
2190
|
+
return {
|
|
2191
|
+
schedule,
|
|
2192
|
+
markDirtyDuringFlight,
|
|
2193
|
+
cancel,
|
|
2194
|
+
flushNow,
|
|
2195
|
+
settle,
|
|
2196
|
+
resume,
|
|
2197
|
+
getInFlight: () => inFlight,
|
|
2198
|
+
getPending: () => pending
|
|
2199
|
+
};
|
|
2200
|
+
};
|
|
2201
|
+
//#endregion
|
|
2202
|
+
//#region src/order/lineFingerprint.ts
|
|
2203
|
+
/** Stable fingerprint for a bare (unconfigured) product. */
|
|
2204
|
+
const bareProductFingerprint = (productId) => {
|
|
2205
|
+
return `${productId}[]`;
|
|
2206
|
+
};
|
|
2207
|
+
/**
|
|
2208
|
+
* Stable fingerprint for a root line + its modifier tree (not just product id).
|
|
2209
|
+
* Root quantity is excluded so the same customization can merge across qty.
|
|
2210
|
+
*/
|
|
2211
|
+
const computeLineFingerprint = (root, lines) => {
|
|
2212
|
+
const childKeys = (parentUid) => {
|
|
2213
|
+
const keys = lines.filter((line) => {
|
|
2214
|
+
return line.parent_uid === parentUid;
|
|
2215
|
+
}).map((line) => {
|
|
2216
|
+
return `${line.id}x${line.quantity}(${childKeys(line.uid)})`;
|
|
2217
|
+
});
|
|
2218
|
+
keys.sort((left, right) => left.localeCompare(right));
|
|
2219
|
+
return keys.join(",");
|
|
2220
|
+
};
|
|
2221
|
+
return `${root.id}[${childKeys(root.uid)}]`;
|
|
2222
|
+
};
|
|
2223
|
+
//#endregion
|
|
2224
|
+
//#region src/order/lines.ts
|
|
2225
|
+
const removeSubtree = (lines, rootUid) => {
|
|
2226
|
+
const drop = /* @__PURE__ */ new Set([rootUid]);
|
|
2227
|
+
let grew = true;
|
|
2228
|
+
while (grew) {
|
|
2229
|
+
grew = false;
|
|
2230
|
+
for (const line of lines) if (line.parent_uid && drop.has(line.parent_uid) && !drop.has(line.uid)) {
|
|
2231
|
+
drop.add(line.uid);
|
|
2232
|
+
grew = true;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return lines.filter((line) => !drop.has(line.uid));
|
|
2236
|
+
};
|
|
2237
|
+
const scaleLineQuantity = (line, nextQty) => {
|
|
2238
|
+
const qty = new Big(nextQty);
|
|
2239
|
+
const subtotal = new Big(line.unit_subtotal).times(qty).toNumber();
|
|
2240
|
+
const discount_total = new Big(line.unit_discount).times(qty).toNumber();
|
|
2241
|
+
const tax_total = new Big(line.unit_tax_total).times(qty).toNumber();
|
|
2242
|
+
const total = new Big(line.unit_total).times(qty).toNumber();
|
|
2243
|
+
const gSub = new Big(line.unit_group_subtotal).times(qty).toNumber();
|
|
2244
|
+
const gDisc = new Big(line.unit_group_discount).times(qty).toNumber();
|
|
2245
|
+
const gTax = new Big(line.unit_group_tax_total).times(qty).toNumber();
|
|
2246
|
+
const gTot = new Big(line.unit_group_total).times(qty).toNumber();
|
|
2247
|
+
const b1 = {
|
|
2248
|
+
...line,
|
|
2249
|
+
quantity: nextQty,
|
|
2250
|
+
subtotal,
|
|
2251
|
+
discount_total
|
|
2252
|
+
};
|
|
2253
|
+
const b2 = {
|
|
2254
|
+
tax_total,
|
|
2255
|
+
total,
|
|
2256
|
+
group_subtotal: gSub,
|
|
2257
|
+
group_discount: gDisc
|
|
2258
|
+
};
|
|
2259
|
+
const b3 = {
|
|
2260
|
+
group_tax_total: gTax,
|
|
2261
|
+
group_total: gTot
|
|
2262
|
+
};
|
|
2263
|
+
return {
|
|
2264
|
+
...b1,
|
|
2265
|
+
...b2,
|
|
2266
|
+
...b3
|
|
2267
|
+
};
|
|
2268
|
+
};
|
|
2269
|
+
const collectDescendantUids = (lines, rootUid) => {
|
|
2270
|
+
const uids = /* @__PURE__ */ new Set([rootUid]);
|
|
2271
|
+
let grew = true;
|
|
2272
|
+
while (grew) {
|
|
2273
|
+
grew = false;
|
|
2274
|
+
for (const line of lines) if (line.parent_uid && uids.has(line.parent_uid) && !uids.has(line.uid)) {
|
|
2275
|
+
uids.add(line.uid);
|
|
2276
|
+
grew = true;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
return uids;
|
|
2280
|
+
};
|
|
2281
|
+
/** Partition a flat batch into root trees (root first, then descendants). */
|
|
2282
|
+
const partitionTrees = (batch) => {
|
|
2283
|
+
const roots = batch.filter((line) => line.parent_uid == null);
|
|
2284
|
+
const trees = [];
|
|
2285
|
+
for (const root of roots) {
|
|
2286
|
+
const uids = collectDescendantUids(batch, root.uid);
|
|
2287
|
+
trees.push(batch.filter((line) => uids.has(line.uid)));
|
|
2288
|
+
}
|
|
2289
|
+
return trees;
|
|
2290
|
+
};
|
|
2291
|
+
const addLines = (lines, incoming) => {
|
|
2292
|
+
const batch = Array.isArray(incoming) ? incoming : [incoming];
|
|
2293
|
+
let next = lines;
|
|
2294
|
+
for (const tree of partitionTrees(batch)) {
|
|
2295
|
+
const root = tree.find((line) => line.parent_uid == null);
|
|
2296
|
+
if (!root) continue;
|
|
2297
|
+
const fingerprint = computeLineFingerprint(root, tree);
|
|
2298
|
+
const existingRoot = next.find((line) => {
|
|
2299
|
+
if (line.parent_uid != null) return false;
|
|
2300
|
+
return computeLineFingerprint(line, next) === fingerprint;
|
|
2301
|
+
});
|
|
2302
|
+
if (existingRoot) {
|
|
2303
|
+
const mergedQty = existingRoot.quantity + root.quantity;
|
|
2304
|
+
next = next.map((line) => {
|
|
2305
|
+
if (line.uid !== existingRoot.uid) return line;
|
|
2306
|
+
return scaleLineQuantity(line, mergedQty);
|
|
2307
|
+
});
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
next = [...next, ...tree];
|
|
2311
|
+
}
|
|
2312
|
+
return next;
|
|
2313
|
+
};
|
|
2314
|
+
const removeLine = (lines, lineUid) => {
|
|
2315
|
+
return removeSubtree(lines, lineUid);
|
|
2316
|
+
};
|
|
2317
|
+
const subtractLine = (lines, lineUid, quantity = 1) => {
|
|
2318
|
+
const target = lines.find((line) => line.uid === lineUid);
|
|
2319
|
+
if (!target) return lines;
|
|
2320
|
+
const nextQty = target.quantity - quantity;
|
|
2321
|
+
if (nextQty <= 0) return removeSubtree(lines, lineUid);
|
|
2322
|
+
return lines.map((line) => {
|
|
2323
|
+
if (line.uid !== lineUid) return line;
|
|
2324
|
+
return scaleLineQuantity(line, nextQty);
|
|
2325
|
+
});
|
|
2326
|
+
};
|
|
2327
|
+
/** Drop a root subtree, then add the replacement tree (may merge by fingerprint). */
|
|
2328
|
+
const replaceLine = (lines, lineUid, next) => {
|
|
2329
|
+
return addLines(removeSubtree(lines, lineUid), next);
|
|
2330
|
+
};
|
|
2331
|
+
const emptyLines = () => {
|
|
2332
|
+
return [];
|
|
2333
|
+
};
|
|
2334
|
+
//#endregion
|
|
2335
|
+
//#region src/schedule/schedule.ts
|
|
2336
|
+
const DAY_OF_WEEK = [
|
|
2337
|
+
"sunday",
|
|
2338
|
+
"monday",
|
|
2339
|
+
"tuesday",
|
|
2340
|
+
"wednesday",
|
|
2341
|
+
"thursday",
|
|
2342
|
+
"friday",
|
|
2343
|
+
"saturday"
|
|
2344
|
+
];
|
|
2345
|
+
const timeToMinutes = (time) => {
|
|
2346
|
+
const [hours = "0", minutes = "0"] = time.split(":");
|
|
2347
|
+
return Number(hours) * 60 + Number(minutes);
|
|
2348
|
+
};
|
|
2349
|
+
const hasSpecificDates = (period) => {
|
|
2350
|
+
return period.start_date != null && period.end_date != null;
|
|
2351
|
+
};
|
|
2352
|
+
const periodCoversDate = (period, now) => {
|
|
2353
|
+
if (!hasSpecificDates(period)) return false;
|
|
2354
|
+
const nowMs = now.getTime();
|
|
2355
|
+
const startMs = Date.parse(period.start_date);
|
|
2356
|
+
const endMs = Date.parse(period.end_date);
|
|
2357
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) return false;
|
|
2358
|
+
return startMs <= nowMs && nowMs <= endMs;
|
|
2359
|
+
};
|
|
2360
|
+
const timeMatches = (period, now) => {
|
|
2361
|
+
if (!period.start_time || !period.end_time) return false;
|
|
2362
|
+
const nowMinutes = now.getHours() * 60 + now.getMinutes();
|
|
2363
|
+
const start = timeToMinutes(period.start_time);
|
|
2364
|
+
const end = timeToMinutes(period.end_time);
|
|
2365
|
+
return nowMinutes >= start && nowMinutes < end;
|
|
2366
|
+
};
|
|
2367
|
+
const isClosedAt = (period, now) => {
|
|
2368
|
+
if (!period.closed) return false;
|
|
2369
|
+
if (!period.start_time || !period.end_time) return true;
|
|
2370
|
+
return timeMatches(period, now);
|
|
2371
|
+
};
|
|
2372
|
+
const isOpenAt = (period, now) => {
|
|
2373
|
+
if (period.closed) return false;
|
|
2374
|
+
return timeMatches(period, now);
|
|
2375
|
+
};
|
|
2376
|
+
const isPeriodsActiveAt = (periods, now) => {
|
|
2377
|
+
return getOpenPeriodAt(periods, now) != null;
|
|
2378
|
+
};
|
|
2379
|
+
const getOpenPeriodAt = (periods, now) => {
|
|
2380
|
+
if (periods.length === 0) return null;
|
|
2381
|
+
const coveringSpecific = periods.filter((period) => {
|
|
2382
|
+
return periodCoversDate(period, now);
|
|
2383
|
+
});
|
|
2384
|
+
const fallback = periods.filter((period) => !hasSpecificDates(period));
|
|
2385
|
+
const specificOpens = coveringSpecific.filter((period) => !period.closed);
|
|
2386
|
+
if (coveringSpecific.filter((period) => period.closed).some((period) => isClosedAt(period, now))) return null;
|
|
2387
|
+
if (specificOpens.length > 0) return specificOpens.find((period) => isOpenAt(period, now)) ?? null;
|
|
2388
|
+
if (fallback.some((period) => isClosedAt(period, now))) return null;
|
|
2389
|
+
return fallback.filter((period) => !period.closed).find((period) => isOpenAt(period, now)) ?? null;
|
|
2390
|
+
};
|
|
2391
|
+
const atTimeOnDate = (day, time) => {
|
|
2392
|
+
const [hours = "0", minutes = "0"] = time.split(":");
|
|
2393
|
+
const next = new Date(day);
|
|
2394
|
+
next.setHours(Number(hours), Number(minutes), 0, 0);
|
|
2395
|
+
return next;
|
|
2396
|
+
};
|
|
2397
|
+
const isWithinPeriodDates = (period, instantMs) => {
|
|
2398
|
+
if (!hasSpecificDates(period)) return true;
|
|
2399
|
+
const startMs = Date.parse(period.start_date);
|
|
2400
|
+
const endMs = Date.parse(period.end_date);
|
|
2401
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) return false;
|
|
2402
|
+
return startMs <= instantMs && instantMs <= endMs;
|
|
2403
|
+
};
|
|
2404
|
+
const dayPeriods = (schedule, instant) => {
|
|
2405
|
+
const day = DAY_OF_WEEK[instant.getDay()];
|
|
2406
|
+
if (!day) return [];
|
|
2407
|
+
return schedule.find((row) => row.day_of_week === day)?.periods ?? [];
|
|
2408
|
+
};
|
|
2409
|
+
const getActiveUntil = (periods, now) => {
|
|
2410
|
+
const open = getOpenPeriodAt(periods, now);
|
|
2411
|
+
if (!open?.end_time) return null;
|
|
2412
|
+
let untilMs = atTimeOnDate(now, open.end_time).getTime();
|
|
2413
|
+
const openIsSpecific = hasSpecificDates(open);
|
|
2414
|
+
for (const period of periods) {
|
|
2415
|
+
if (!period.closed || !period.start_time) continue;
|
|
2416
|
+
const closeMs = atTimeOnDate(now, period.start_time).getTime();
|
|
2417
|
+
if (closeMs <= now.getTime() || closeMs >= untilMs) continue;
|
|
2418
|
+
if (!isWithinPeriodDates(period, closeMs)) continue;
|
|
2419
|
+
if (!hasSpecificDates(period) && openIsSpecific) continue;
|
|
2420
|
+
untilMs = closeMs;
|
|
2421
|
+
}
|
|
2422
|
+
return new Date(untilMs);
|
|
2423
|
+
};
|
|
2424
|
+
const getNextStart = (schedule, now) => {
|
|
2425
|
+
const nowMs = now.getTime();
|
|
2426
|
+
let nextStartMs = null;
|
|
2427
|
+
const consider = (candidate, periods) => {
|
|
2428
|
+
const startMs = candidate.getTime();
|
|
2429
|
+
if (startMs <= nowMs) return;
|
|
2430
|
+
if (!isPeriodsActiveAt(periods, candidate)) return;
|
|
2431
|
+
if (nextStartMs === null || startMs < nextStartMs) nextStartMs = startMs;
|
|
2432
|
+
};
|
|
2433
|
+
for (let dayOffset = 0; dayOffset < 8; dayOffset += 1) {
|
|
2434
|
+
const candidateDate = new Date(now);
|
|
2435
|
+
candidateDate.setDate(candidateDate.getDate() + dayOffset);
|
|
2436
|
+
candidateDate.setSeconds(0, 0);
|
|
2437
|
+
const periods = dayPeriods(schedule, candidateDate);
|
|
2438
|
+
if (periods.length === 0) continue;
|
|
2439
|
+
for (const period of periods) {
|
|
2440
|
+
if (period.closed || !period.start_time) continue;
|
|
2441
|
+
const start = atTimeOnDate(candidateDate, period.start_time);
|
|
2442
|
+
if (!isWithinPeriodDates(period, start.getTime())) continue;
|
|
2443
|
+
consider(start, periods);
|
|
2444
|
+
}
|
|
2445
|
+
for (const period of periods) {
|
|
2446
|
+
if (!period.closed || !period.end_time) continue;
|
|
2447
|
+
const reopen = atTimeOnDate(candidateDate, period.end_time);
|
|
2448
|
+
if (!isWithinPeriodDates(period, reopen.getTime())) continue;
|
|
2449
|
+
consider(reopen, periods);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return nextStartMs === null ? null : new Date(nextStartMs);
|
|
2453
|
+
};
|
|
2454
|
+
/** Whether `schedule` is in hours now, and until / next open. */
|
|
2455
|
+
const getScheduleWindow = (schedule, now = /* @__PURE__ */ new Date()) => {
|
|
2456
|
+
if (schedule == null) return {
|
|
2457
|
+
active: true,
|
|
2458
|
+
until: null
|
|
2459
|
+
};
|
|
2460
|
+
const periods = dayPeriods(schedule, now);
|
|
2461
|
+
if (isPeriodsActiveAt(periods, now)) return {
|
|
2462
|
+
active: true,
|
|
2463
|
+
until: getActiveUntil(periods, now)
|
|
2464
|
+
};
|
|
2465
|
+
return {
|
|
2466
|
+
active: false,
|
|
2467
|
+
next: getNextStart(schedule, now)
|
|
2468
|
+
};
|
|
2469
|
+
};
|
|
2470
|
+
const isOnSchedule = (schedule, now) => {
|
|
2471
|
+
return getScheduleWindow(schedule, now).active;
|
|
2472
|
+
};
|
|
2473
|
+
//#endregion
|
|
2474
|
+
//#region src/order/recommendations.ts
|
|
2475
|
+
const DEFAULT_RECOMMENDATION_TYPES = ["CROSS_SELL"];
|
|
2476
|
+
const linkedRecommendationIds = (pricing) => {
|
|
2477
|
+
return [...pricing.recommendations_ids ?? [], ...(pricing.recommendations ?? []).map((link) => link.recommendation_id)];
|
|
2478
|
+
};
|
|
2479
|
+
const collectRecommendations = (config) => {
|
|
2480
|
+
const { lines, lookup, quantity, types } = config;
|
|
2481
|
+
if (quantity <= 0) return [];
|
|
2482
|
+
const typeSet = new Set(types ?? DEFAULT_RECOMMENDATION_TYPES);
|
|
2483
|
+
const roots = lines.filter((line) => line.parent_uid == null);
|
|
2484
|
+
const fingerprints = roots.map((root) => computeLineFingerprint(root, lines));
|
|
2485
|
+
const cartKeys = new Set(fingerprints);
|
|
2486
|
+
const seenTargetIds = /* @__PURE__ */ new Set();
|
|
2487
|
+
const products = [];
|
|
2488
|
+
for (const root of roots) {
|
|
2489
|
+
const source = lookup.productsById[root.id];
|
|
2490
|
+
if (!source) continue;
|
|
2491
|
+
for (const recommendationId of linkedRecommendationIds(source.pricing)) {
|
|
2492
|
+
const recommendation = lookup.recommendationsById[recommendationId];
|
|
2493
|
+
if (!recommendation || !typeSet.has(recommendation.type)) continue;
|
|
2494
|
+
const targetId = recommendation.target_product_id;
|
|
2495
|
+
if (seenTargetIds.has(targetId)) continue;
|
|
2496
|
+
if (cartKeys.has(bareProductFingerprint(targetId))) continue;
|
|
2497
|
+
const target = lookup.productsById[targetId];
|
|
2498
|
+
if (!target || target.status !== "ACTIVE") continue;
|
|
2499
|
+
if (!isOnSchedule(target.availability.schedules)) continue;
|
|
2500
|
+
seenTargetIds.add(targetId);
|
|
2501
|
+
products.push(target);
|
|
2502
|
+
if (products.length >= quantity) return products;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return products;
|
|
2506
|
+
};
|
|
2507
|
+
//#endregion
|
|
2508
|
+
//#region src/order/totals.ts
|
|
2509
|
+
/** Sum root line group totals into order totals (Big.js). */
|
|
2510
|
+
const recomputeTotals = (lines, currency = "USD") => {
|
|
2511
|
+
let subtotal = new Big(0);
|
|
2512
|
+
let discountTotal = new Big(0);
|
|
2513
|
+
let taxTotal = new Big(0);
|
|
2514
|
+
let total = new Big(0);
|
|
2515
|
+
for (const line of lines) {
|
|
2516
|
+
if (line.parent_uid != null) continue;
|
|
2517
|
+
subtotal = subtotal.plus(line.group_subtotal);
|
|
2518
|
+
discountTotal = discountTotal.plus(line.group_discount);
|
|
2519
|
+
taxTotal = taxTotal.plus(line.group_tax_total);
|
|
2520
|
+
total = total.plus(line.group_total);
|
|
2521
|
+
}
|
|
2522
|
+
let taxRate = 0;
|
|
2523
|
+
let discountRate = 0;
|
|
2524
|
+
if (subtotal.gt(0)) {
|
|
2525
|
+
taxRate = taxTotal.div(subtotal).toNumber();
|
|
2526
|
+
discountRate = discountTotal.div(subtotal).toNumber();
|
|
2527
|
+
}
|
|
2528
|
+
const discount_total = discountTotal.toNumber();
|
|
2529
|
+
const tax_total = taxTotal.toNumber();
|
|
2530
|
+
const b1 = {
|
|
2531
|
+
currency,
|
|
2532
|
+
subtotal: subtotal.toNumber()
|
|
2533
|
+
};
|
|
2534
|
+
const b2 = {
|
|
2535
|
+
discount_rate: discountRate,
|
|
2536
|
+
discount_total
|
|
2537
|
+
};
|
|
2538
|
+
const b3 = {
|
|
2539
|
+
tax_rate: taxRate,
|
|
2540
|
+
tax_total,
|
|
2541
|
+
total: total.toNumber()
|
|
2542
|
+
};
|
|
2543
|
+
return {
|
|
2544
|
+
...b1,
|
|
2545
|
+
...b2,
|
|
2546
|
+
...b3
|
|
2547
|
+
};
|
|
2548
|
+
};
|
|
2549
|
+
const emptyTotals = (currency = "USD") => {
|
|
2550
|
+
const b1 = {
|
|
2551
|
+
currency,
|
|
2552
|
+
subtotal: 0,
|
|
2553
|
+
discount_rate: 0,
|
|
2554
|
+
discount_total: 0
|
|
2555
|
+
};
|
|
2556
|
+
const b2 = {
|
|
2557
|
+
tax_rate: 0,
|
|
2558
|
+
tax_total: 0,
|
|
2559
|
+
total: 0
|
|
2560
|
+
};
|
|
2561
|
+
return {
|
|
2562
|
+
...b1,
|
|
2563
|
+
...b2
|
|
2564
|
+
};
|
|
2565
|
+
};
|
|
2566
|
+
//#endregion
|
|
2567
|
+
//#region src/order/Order.ts
|
|
2568
|
+
const cloneOrder = (order) => {
|
|
2569
|
+
return structuredClone(order);
|
|
2570
|
+
};
|
|
2571
|
+
const buildHttpOptions = (options) => {
|
|
2572
|
+
return {
|
|
2573
|
+
baseUrl: options.baseUrl,
|
|
2574
|
+
accessToken: options.accessToken,
|
|
2575
|
+
fetchImpl: options.fetchImpl
|
|
2576
|
+
};
|
|
2577
|
+
};
|
|
2578
|
+
/** Empty / whitespace brand_id → omit (API rejects `""`). */
|
|
2579
|
+
const optionalBrandId = (value) => {
|
|
2580
|
+
if (value == null) return void 0;
|
|
2581
|
+
const trimmed = value.trim();
|
|
2582
|
+
if (!trimmed) return void 0;
|
|
2583
|
+
return trimmed;
|
|
2584
|
+
};
|
|
2585
|
+
const buildInsertFromOptions = (options) => {
|
|
2586
|
+
if (!options.scope) throw new Error("Order requires scope when fromOrderId is omitted");
|
|
2587
|
+
if (!options.customer) throw new Error("Order requires customer when fromOrderId is omitted");
|
|
2588
|
+
const orderId = options.orderId ?? `ord-${randomUUID().slice(0, 8)}`;
|
|
2589
|
+
const brand_id = optionalBrandId(options.scope.brand_id);
|
|
2590
|
+
const customer = buildCustomerSnapshot(options.customer);
|
|
2591
|
+
const is_anonymous = options.isAnonymous ?? false;
|
|
2592
|
+
const b1 = {
|
|
2593
|
+
id: orderId,
|
|
2594
|
+
store_id: options.scope.store_id
|
|
2595
|
+
};
|
|
2596
|
+
const b2 = { channel_id: options.scope.channel_id };
|
|
2597
|
+
const b3 = {
|
|
2598
|
+
fulfillment_id: options.scope.fulfillment_id,
|
|
2599
|
+
brand_id
|
|
2600
|
+
};
|
|
2601
|
+
const b4 = {
|
|
2602
|
+
is_anonymous,
|
|
2603
|
+
customer,
|
|
2604
|
+
lines: [],
|
|
2605
|
+
redeem: []
|
|
2606
|
+
};
|
|
2607
|
+
const b5 = {
|
|
2608
|
+
totals: emptyTotals(),
|
|
2609
|
+
metadata: {}
|
|
2610
|
+
};
|
|
2611
|
+
return {
|
|
2612
|
+
...b1,
|
|
2613
|
+
...b2,
|
|
2614
|
+
...b3,
|
|
2615
|
+
...b4,
|
|
2616
|
+
...b5
|
|
2617
|
+
};
|
|
2618
|
+
};
|
|
2619
|
+
const buildInsertFromSource = (options, source) => {
|
|
2620
|
+
const orderId = options.orderId ?? `ord-${randomUUID().slice(0, 8)}`;
|
|
2621
|
+
const store_id = options.scope?.store_id ?? source.store_id;
|
|
2622
|
+
const channel_id = options.scope?.channel_id ?? source.channel_id;
|
|
2623
|
+
const fulfillment_id = options.scope?.fulfillment_id ?? source.fulfillment_id;
|
|
2624
|
+
let brand_id = optionalBrandId(source.brand_id);
|
|
2625
|
+
if (options.scope?.brand_id !== void 0) brand_id = optionalBrandId(options.scope.brand_id);
|
|
2626
|
+
let customer = source.customer;
|
|
2627
|
+
if (options.customer) customer = buildCustomerSnapshot(options.customer);
|
|
2628
|
+
const lines = structuredClone(source.lines);
|
|
2629
|
+
const is_anonymous = options.isAnonymous ?? source.is_anonymous;
|
|
2630
|
+
const totals = recomputeTotals(lines, source.totals.currency);
|
|
2631
|
+
const b1 = {
|
|
2632
|
+
id: orderId,
|
|
2633
|
+
store_id,
|
|
2634
|
+
channel_id
|
|
2635
|
+
};
|
|
2636
|
+
const b2 = {
|
|
2637
|
+
fulfillment_id,
|
|
2638
|
+
brand_id,
|
|
2639
|
+
is_anonymous
|
|
2640
|
+
};
|
|
2641
|
+
const redeem = source.redeem ?? [];
|
|
2642
|
+
const b3 = {
|
|
2643
|
+
customer,
|
|
2644
|
+
lines,
|
|
2645
|
+
totals,
|
|
2646
|
+
metadata: {},
|
|
2647
|
+
redeem
|
|
2648
|
+
};
|
|
2649
|
+
return {
|
|
2650
|
+
...b1,
|
|
2651
|
+
...b2,
|
|
2652
|
+
...b3
|
|
2653
|
+
};
|
|
2654
|
+
};
|
|
2655
|
+
const toRedeem = (items) => {
|
|
2656
|
+
return (items ?? []).map((item) => ({
|
|
2657
|
+
...item,
|
|
2658
|
+
amount: item.amount ?? 1
|
|
2659
|
+
}));
|
|
2660
|
+
};
|
|
2661
|
+
/** Local draft until the first mutating flush POSTs to the API. */
|
|
2662
|
+
const buildLocalDraft = (insert) => {
|
|
2663
|
+
const lines = (insert.lines ?? []).map((line) => {
|
|
2664
|
+
const patch = {
|
|
2665
|
+
metadata: line.metadata ?? {},
|
|
2666
|
+
bundled_items: line.bundled_items ?? [],
|
|
2667
|
+
redeem: toRedeem(line.redeem)
|
|
2668
|
+
};
|
|
2669
|
+
return {
|
|
2670
|
+
...line,
|
|
2671
|
+
...patch
|
|
2672
|
+
};
|
|
2673
|
+
});
|
|
2674
|
+
const brand_id = insert.brand_id ?? null;
|
|
2675
|
+
const is_anonymous = insert.is_anonymous ?? false;
|
|
2676
|
+
const totals = insert.totals ?? emptyTotals();
|
|
2677
|
+
const metadata = insert.metadata ?? {};
|
|
2678
|
+
const payment_intents = (insert.payment_intents ?? []).map((intent) => {
|
|
2679
|
+
const metadata = intent.metadata ?? {};
|
|
2680
|
+
return {
|
|
2681
|
+
...intent,
|
|
2682
|
+
metadata
|
|
2683
|
+
};
|
|
2684
|
+
});
|
|
2685
|
+
const payment_status = insert.payment_status ?? "IDLE";
|
|
2686
|
+
const b1 = {
|
|
2687
|
+
uid: randomUUID(),
|
|
2688
|
+
id: insert.id,
|
|
2689
|
+
brand_id
|
|
2690
|
+
};
|
|
2691
|
+
const b2 = {
|
|
2692
|
+
store_id: insert.store_id,
|
|
2693
|
+
channel_id: insert.channel_id
|
|
2694
|
+
};
|
|
2695
|
+
const b3 = {
|
|
2696
|
+
customer_id: "",
|
|
2697
|
+
fulfillment_id: insert.fulfillment_id
|
|
2698
|
+
};
|
|
2699
|
+
const redeem = toRedeem(insert.redeem);
|
|
2700
|
+
const b4 = {
|
|
2701
|
+
is_anonymous,
|
|
2702
|
+
lines,
|
|
2703
|
+
customer: insert.customer,
|
|
2704
|
+
redeem
|
|
2705
|
+
};
|
|
2706
|
+
const b5 = {
|
|
2707
|
+
payment_intents,
|
|
2708
|
+
totals,
|
|
2709
|
+
metadata
|
|
2710
|
+
};
|
|
2711
|
+
const b6 = { payment_status };
|
|
2712
|
+
const b7 = {
|
|
2713
|
+
fulfillment_status: "IDLE",
|
|
2714
|
+
status: "OPEN"
|
|
2715
|
+
};
|
|
2716
|
+
const b8 = {
|
|
2717
|
+
issued_at: null,
|
|
2718
|
+
resolved_at: null
|
|
2719
|
+
};
|
|
2720
|
+
const p12 = {
|
|
2721
|
+
...b1,
|
|
2722
|
+
...b2
|
|
2723
|
+
};
|
|
2724
|
+
const p34 = {
|
|
2725
|
+
...b3,
|
|
2726
|
+
...b4
|
|
2727
|
+
};
|
|
2728
|
+
const p56 = {
|
|
2729
|
+
...b5,
|
|
2730
|
+
...b6
|
|
2731
|
+
};
|
|
2732
|
+
const p78 = {
|
|
2733
|
+
...b7,
|
|
2734
|
+
...b8
|
|
2735
|
+
};
|
|
2736
|
+
const p1234 = {
|
|
2737
|
+
...p12,
|
|
2738
|
+
...p34
|
|
2739
|
+
};
|
|
2740
|
+
const p5678 = {
|
|
2741
|
+
...p56,
|
|
2742
|
+
...p78
|
|
2743
|
+
};
|
|
2744
|
+
return {
|
|
2745
|
+
...p1234,
|
|
2746
|
+
...p5678
|
|
2747
|
+
};
|
|
2748
|
+
};
|
|
2749
|
+
const sameBrand = (left, right) => {
|
|
2750
|
+
return (optionalBrandId(left) ?? null) === (optionalBrandId(right) ?? null);
|
|
2751
|
+
};
|
|
2752
|
+
/** Newest OPEN order for customer + scope, if any. */
|
|
2753
|
+
const findOpenOrder = async (scope, http) => {
|
|
2754
|
+
const brandId = optionalBrandId(scope.brand_id);
|
|
2755
|
+
return (await services.orders.list({
|
|
2756
|
+
store_id: scope.store_id,
|
|
2757
|
+
channel_id: scope.channel_id,
|
|
2758
|
+
fulfillment_id: scope.fulfillment_id,
|
|
2759
|
+
...brandId ? { brand_id: brandId } : {},
|
|
2760
|
+
status: "OPEN",
|
|
2761
|
+
limit: 20
|
|
2762
|
+
}, http)).find((order) => sameBrand(order.brand_id, brandId)) ?? null;
|
|
2763
|
+
};
|
|
2764
|
+
var Order = class Order {
|
|
2765
|
+
draft;
|
|
2766
|
+
synced;
|
|
2767
|
+
deleted = false;
|
|
2768
|
+
persisted;
|
|
2769
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2770
|
+
externalNotify;
|
|
2771
|
+
http;
|
|
2772
|
+
flush;
|
|
2773
|
+
constructor(config, notify) {
|
|
2774
|
+
this.draft = cloneOrder(config.order);
|
|
2775
|
+
this.synced = cloneOrder(config.order);
|
|
2776
|
+
this.persisted = config.persisted;
|
|
2777
|
+
this.http = config.http;
|
|
2778
|
+
this.externalNotify = notify;
|
|
2779
|
+
this.flush = createFlushController({
|
|
2780
|
+
getDraft: () => this.draft,
|
|
2781
|
+
setDraft: (next) => {
|
|
2782
|
+
this.draft = next;
|
|
2783
|
+
},
|
|
2784
|
+
getSynced: () => this.synced,
|
|
2785
|
+
setSynced: (next) => {
|
|
2786
|
+
this.synced = next;
|
|
2787
|
+
},
|
|
2788
|
+
isDeleted: () => this.deleted,
|
|
2789
|
+
isPersisted: () => this.persisted,
|
|
2790
|
+
setPersisted: (next) => {
|
|
2791
|
+
this.persisted = next;
|
|
2792
|
+
},
|
|
2793
|
+
notify: () => this.notify(),
|
|
2794
|
+
http: this.http,
|
|
2795
|
+
debounceMs: config.debounceMs,
|
|
2796
|
+
fresh: config.fresh ?? false,
|
|
2797
|
+
sleep: config.sleep
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Opens a cart session: reuses an existing OPEN order for the scope, or a
|
|
2802
|
+
* local draft. Does not POST until `persist()` or the first mutating flush.
|
|
2803
|
+
*/
|
|
2804
|
+
static async open(options, notify) {
|
|
2805
|
+
const http = buildHttpOptions(options);
|
|
2806
|
+
const debounceMs = options.debounceMs ?? 300;
|
|
2807
|
+
let body;
|
|
2808
|
+
let scope;
|
|
2809
|
+
if (options.fromOrderId) {
|
|
2810
|
+
const source = await services.orders.get(options.fromOrderId, http);
|
|
2811
|
+
body = buildInsertFromSource(options, source);
|
|
2812
|
+
scope = {
|
|
2813
|
+
store_id: body.store_id,
|
|
2814
|
+
channel_id: body.channel_id,
|
|
2815
|
+
fulfillment_id: body.fulfillment_id,
|
|
2816
|
+
brand_id: optionalBrandId(body.brand_id)
|
|
2817
|
+
};
|
|
2818
|
+
} else {
|
|
2819
|
+
body = buildInsertFromOptions(options);
|
|
2820
|
+
scope = {
|
|
2821
|
+
store_id: options.scope.store_id,
|
|
2822
|
+
channel_id: options.scope.channel_id,
|
|
2823
|
+
fulfillment_id: options.scope.fulfillment_id,
|
|
2824
|
+
brand_id: optionalBrandId(options.scope.brand_id)
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
const reuse = options.reuseOpen ?? true;
|
|
2828
|
+
const existing = reuse ? await findOpenOrder(scope, http) : null;
|
|
2829
|
+
if (existing) return new Order({
|
|
2830
|
+
order: existing,
|
|
2831
|
+
http,
|
|
2832
|
+
debounceMs,
|
|
2833
|
+
persisted: true
|
|
2834
|
+
}, notify);
|
|
2835
|
+
return new Order({
|
|
2836
|
+
order: buildLocalDraft(body),
|
|
2837
|
+
http,
|
|
2838
|
+
debounceMs,
|
|
2839
|
+
persisted: false,
|
|
2840
|
+
fresh: !reuse
|
|
2841
|
+
}, notify);
|
|
2842
|
+
}
|
|
2843
|
+
static async reorder(orderId, options = {}, notify) {
|
|
2844
|
+
const http = buildHttpOptions(options);
|
|
2845
|
+
const debounceMs = options.debounceMs ?? 300;
|
|
2846
|
+
const strategy = options.strategy ?? "replace";
|
|
2847
|
+
const dto = await services.orders.reorder(orderId, { strategy }, http);
|
|
2848
|
+
return new Order({
|
|
2849
|
+
order: dto,
|
|
2850
|
+
http,
|
|
2851
|
+
debounceMs,
|
|
2852
|
+
persisted: true
|
|
2853
|
+
}, notify);
|
|
2854
|
+
}
|
|
2855
|
+
/** Opens a session and POSTs immediately if no OPEN order exists yet. */
|
|
2856
|
+
static async create(options, notify) {
|
|
2857
|
+
const order = await Order.open(options, notify);
|
|
2858
|
+
if (!order.isPersisted()) await order.persist();
|
|
2859
|
+
return order;
|
|
2860
|
+
}
|
|
2861
|
+
/** @internal test hook — create without POST */
|
|
2862
|
+
static fromExisting(config, notify) {
|
|
2863
|
+
return new Order({
|
|
2864
|
+
order: config.order,
|
|
2865
|
+
http: config.http,
|
|
2866
|
+
debounceMs: config.debounceMs ?? 300,
|
|
2867
|
+
persisted: config.persisted ?? true,
|
|
2868
|
+
sleep: config.sleep
|
|
2869
|
+
}, notify);
|
|
2870
|
+
}
|
|
2871
|
+
/** Whether this session has a server row yet. */
|
|
2872
|
+
isPersisted() {
|
|
2873
|
+
return this.persisted;
|
|
2874
|
+
}
|
|
2875
|
+
/** Persist a local draft via POST (no-op if already on the server). */
|
|
2876
|
+
async persist() {
|
|
2877
|
+
if (this.deleted || this.persisted) return;
|
|
2878
|
+
await this.flush.flushNow();
|
|
2879
|
+
}
|
|
2880
|
+
/** Whether local edits failed to reach the server (e.g. offline). */
|
|
2881
|
+
hasPendingSync() {
|
|
2882
|
+
return this.flush.getPending();
|
|
2883
|
+
}
|
|
2884
|
+
/** Retries pending edits; call when connectivity returns. */
|
|
2885
|
+
resume() {
|
|
2886
|
+
return this.flush.resume();
|
|
2887
|
+
}
|
|
2888
|
+
/** Abandons the session locally: pending edits are dropped, no request. */
|
|
2889
|
+
dispose() {
|
|
2890
|
+
this.flush.cancel();
|
|
2891
|
+
this.deleted = true;
|
|
2892
|
+
}
|
|
2893
|
+
subscribe(listener) {
|
|
2894
|
+
this.listeners.add(listener);
|
|
2895
|
+
return () => {
|
|
2896
|
+
this.listeners.delete(listener);
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
getOrder() {
|
|
2900
|
+
return cloneOrder(this.draft);
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Upsell / cross-sell / down-sell products for items currently in the cart.
|
|
2904
|
+
* Skips targets whose base (unconfigured) customization is already in the cart —
|
|
2905
|
+
* same product id with a different modifier tree is still allowed.
|
|
2906
|
+
*/
|
|
2907
|
+
getRecommendations(lookup, quantity, types = DEFAULT_RECOMMENDATION_TYPES) {
|
|
2908
|
+
if (!lookup) return [];
|
|
2909
|
+
return collectRecommendations({
|
|
2910
|
+
lines: this.draft.lines,
|
|
2911
|
+
lookup,
|
|
2912
|
+
quantity,
|
|
2913
|
+
types
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Rebuild a customization {@link Snapshot} for a root cart line so callers
|
|
2918
|
+
* can reopen it with `createSession(lookup, snapshot.productId, { snapshot })`.
|
|
2919
|
+
*/
|
|
2920
|
+
getCustomizationSnapshot(rootLineUid, lookup) {
|
|
2921
|
+
const root = this.draft.lines.find((line) => line.uid === rootLineUid);
|
|
2922
|
+
if (!root || root.parent_uid != null) return null;
|
|
2923
|
+
return fromOrderLines(root, this.draft.lines, lookup);
|
|
2924
|
+
}
|
|
2925
|
+
addLine(lines) {
|
|
2926
|
+
if (this.deleted) return;
|
|
2927
|
+
const nextLines = addLines(this.draft.lines, lines);
|
|
2928
|
+
this.draft = {
|
|
2929
|
+
...this.draft,
|
|
2930
|
+
lines: nextLines,
|
|
2931
|
+
totals: recomputeTotals(nextLines, this.draft.totals.currency)
|
|
2932
|
+
};
|
|
2933
|
+
this.scheduleAfterMutate();
|
|
2934
|
+
}
|
|
2935
|
+
subtractLine(lineUid, quantity = 1) {
|
|
2936
|
+
if (this.deleted) return;
|
|
2937
|
+
const nextLines = subtractLine(this.draft.lines, lineUid, quantity);
|
|
2938
|
+
this.draft = {
|
|
2939
|
+
...this.draft,
|
|
2940
|
+
lines: nextLines,
|
|
2941
|
+
totals: recomputeTotals(nextLines, this.draft.totals.currency)
|
|
2942
|
+
};
|
|
2943
|
+
this.scheduleAfterMutate();
|
|
2944
|
+
}
|
|
2945
|
+
removeLine(lineUid) {
|
|
2946
|
+
if (this.deleted) return;
|
|
2947
|
+
const nextLines = removeLine(this.draft.lines, lineUid);
|
|
2948
|
+
this.draft = {
|
|
2949
|
+
...this.draft,
|
|
2950
|
+
lines: nextLines,
|
|
2951
|
+
totals: recomputeTotals(nextLines, this.draft.totals.currency)
|
|
2952
|
+
};
|
|
2953
|
+
this.scheduleAfterMutate();
|
|
2954
|
+
}
|
|
2955
|
+
replaceLine(lineUid, lines) {
|
|
2956
|
+
if (this.deleted) return;
|
|
2957
|
+
const nextLines = replaceLine(this.draft.lines, lineUid, lines);
|
|
2958
|
+
this.draft = {
|
|
2959
|
+
...this.draft,
|
|
2960
|
+
lines: nextLines,
|
|
2961
|
+
totals: recomputeTotals(nextLines, this.draft.totals.currency)
|
|
2962
|
+
};
|
|
2963
|
+
this.scheduleAfterMutate();
|
|
2964
|
+
}
|
|
2965
|
+
emptyOrder() {
|
|
2966
|
+
if (this.deleted) return;
|
|
2967
|
+
const nextLines = emptyLines();
|
|
2968
|
+
this.draft = {
|
|
2969
|
+
...this.draft,
|
|
2970
|
+
lines: nextLines,
|
|
2971
|
+
redeem: [],
|
|
2972
|
+
totals: recomputeTotals(nextLines, this.draft.totals.currency)
|
|
2973
|
+
};
|
|
2974
|
+
this.scheduleAfterMutate();
|
|
2975
|
+
}
|
|
2976
|
+
setScope(scope) {
|
|
2977
|
+
if (this.deleted) return;
|
|
2978
|
+
this.draft = {
|
|
2979
|
+
...this.draft,
|
|
2980
|
+
store_id: scope.store_id ?? this.draft.store_id,
|
|
2981
|
+
channel_id: scope.channel_id ?? this.draft.channel_id,
|
|
2982
|
+
fulfillment_id: scope.fulfillment_id ?? this.draft.fulfillment_id,
|
|
2983
|
+
brand_id: scope.brand_id === void 0 ? this.draft.brand_id : optionalBrandId(scope.brand_id)
|
|
2984
|
+
};
|
|
2985
|
+
this.scheduleAfterMutate();
|
|
2986
|
+
}
|
|
2987
|
+
/** Shallow-merges into order metadata; `null` values are kept as-is. */
|
|
2988
|
+
setMetadata(patch) {
|
|
2989
|
+
if (this.deleted) return;
|
|
2990
|
+
const metadata = {
|
|
2991
|
+
...this.draft.metadata ?? {},
|
|
2992
|
+
...patch
|
|
2993
|
+
};
|
|
2994
|
+
this.draft = {
|
|
2995
|
+
...this.draft,
|
|
2996
|
+
metadata
|
|
2997
|
+
};
|
|
2998
|
+
this.scheduleAfterMutate();
|
|
2999
|
+
}
|
|
3000
|
+
setCustomerDetails(details) {
|
|
3001
|
+
if (this.deleted) return;
|
|
3002
|
+
const hasAddressInfo = details.address_info !== void 0;
|
|
3003
|
+
const hasBillingInfo = details.billing_info !== void 0;
|
|
3004
|
+
const address_info = !hasAddressInfo ? this.draft.customer.address_info : details.address_info;
|
|
3005
|
+
const billing_info = !hasBillingInfo ? this.draft.customer.billing_info : details.billing_info;
|
|
3006
|
+
const customer = {
|
|
3007
|
+
...this.draft.customer,
|
|
3008
|
+
address_info,
|
|
3009
|
+
billing_info
|
|
3010
|
+
};
|
|
3011
|
+
this.draft = {
|
|
3012
|
+
...this.draft,
|
|
3013
|
+
customer
|
|
3014
|
+
};
|
|
3015
|
+
this.scheduleAfterMutate();
|
|
3016
|
+
}
|
|
3017
|
+
async cancelOrder() {
|
|
3018
|
+
if (this.deleted) return;
|
|
3019
|
+
this.flush.cancel();
|
|
3020
|
+
if (this.persisted) {
|
|
3021
|
+
const next = await services.orders.cancel(this.draft.id, this.http);
|
|
3022
|
+
this.draft = cloneOrder(next);
|
|
3023
|
+
this.synced = cloneOrder(next);
|
|
3024
|
+
} else {
|
|
3025
|
+
let payment_status = "CANCELLED";
|
|
3026
|
+
if (this.draft.payment_status === "PAID") payment_status = "PAID";
|
|
3027
|
+
if (this.draft.payment_status === "REFUNDED") payment_status = "REFUNDED";
|
|
3028
|
+
const resolved_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3029
|
+
let status = "VOIDED";
|
|
3030
|
+
if (this.draft.payment_status === "PAID") status = "REVERSED";
|
|
3031
|
+
const d1 = { status };
|
|
3032
|
+
const d2 = { fulfillment_status: status };
|
|
3033
|
+
const next = {
|
|
3034
|
+
...this.draft,
|
|
3035
|
+
...d1,
|
|
3036
|
+
...d2,
|
|
3037
|
+
payment_status,
|
|
3038
|
+
resolved_at
|
|
3039
|
+
};
|
|
3040
|
+
this.draft = next;
|
|
3041
|
+
this.synced = cloneOrder(next);
|
|
3042
|
+
}
|
|
3043
|
+
this.deleted = true;
|
|
3044
|
+
this.notify();
|
|
3045
|
+
}
|
|
3046
|
+
async refundOrder() {
|
|
3047
|
+
if (this.deleted) return;
|
|
3048
|
+
this.flush.cancel();
|
|
3049
|
+
if (!this.persisted) return;
|
|
3050
|
+
const next = await services.orders.refund(this.draft.id, this.http);
|
|
3051
|
+
this.draft = cloneOrder(next);
|
|
3052
|
+
this.synced = cloneOrder(next);
|
|
3053
|
+
this.deleted = true;
|
|
3054
|
+
this.notify();
|
|
3055
|
+
}
|
|
3056
|
+
async validateOrder() {
|
|
3057
|
+
const draft = this.getOrder();
|
|
3058
|
+
const b1 = {
|
|
3059
|
+
store_id: draft.store_id,
|
|
3060
|
+
channel_id: draft.channel_id
|
|
3061
|
+
};
|
|
3062
|
+
const b2 = { fulfillment_id: draft.fulfillment_id };
|
|
3063
|
+
const b3 = {
|
|
3064
|
+
brand_id: draft.brand_id,
|
|
3065
|
+
is_anonymous: draft.is_anonymous
|
|
3066
|
+
};
|
|
3067
|
+
const b4 = {
|
|
3068
|
+
lines: draft.lines,
|
|
3069
|
+
redeem: draft.redeem ?? []
|
|
3070
|
+
};
|
|
3071
|
+
const b5 = { totals: draft.totals };
|
|
3072
|
+
const body = {
|
|
3073
|
+
...b1,
|
|
3074
|
+
...b2,
|
|
3075
|
+
...b3,
|
|
3076
|
+
...b4,
|
|
3077
|
+
...b5
|
|
3078
|
+
};
|
|
3079
|
+
await services.orders.validate(body, this.http);
|
|
3080
|
+
}
|
|
3081
|
+
async placeOrder(input = {}) {
|
|
3082
|
+
await this.persist();
|
|
3083
|
+
await this.flush.settle();
|
|
3084
|
+
await this.validateOrder();
|
|
3085
|
+
try {
|
|
3086
|
+
await services.orders.place(this.draft.id, input, this.http);
|
|
3087
|
+
} catch (error) {
|
|
3088
|
+
if (!(error instanceof StorefrontHttpError) || error.status !== 409) throw error;
|
|
3089
|
+
}
|
|
3090
|
+
const next = await services.orders.get(this.draft.id, this.http);
|
|
3091
|
+
this.draft = cloneOrder(next);
|
|
3092
|
+
this.synced = cloneOrder(next);
|
|
3093
|
+
this.notify();
|
|
3094
|
+
}
|
|
3095
|
+
scheduleAfterMutate() {
|
|
3096
|
+
this.flush.markDirtyDuringFlight();
|
|
3097
|
+
this.notify();
|
|
3098
|
+
this.flush.schedule();
|
|
3099
|
+
}
|
|
3100
|
+
notify() {
|
|
3101
|
+
this.externalNotify?.();
|
|
3102
|
+
this.listeners.forEach((listener) => listener());
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
//#endregion
|
|
3106
|
+
//#region src/order/fulfillmentTrack.ts
|
|
3107
|
+
const isPrimaryPathKind = (pathKind) => {
|
|
3108
|
+
return pathKind === "default" || pathKind === "success" || pathKind == null;
|
|
3109
|
+
};
|
|
3110
|
+
const pathKindForTarget = (target) => {
|
|
3111
|
+
if (!target || target.data.kind !== "terminal") return "default";
|
|
3112
|
+
const status = target.data.mappedStatus;
|
|
3113
|
+
if (status === "VOIDED") return "cancel";
|
|
3114
|
+
if (status === "REVERSED") return "refund";
|
|
3115
|
+
if (status === "REFUNDED" || status === "RETURNED") return "refund";
|
|
3116
|
+
return "success";
|
|
3117
|
+
};
|
|
3118
|
+
const pickLabel = (label, fallback) => {
|
|
3119
|
+
if (typeof label === "string") return label.trim() || fallback;
|
|
3120
|
+
if (!label) return fallback;
|
|
3121
|
+
const en = label.en_us?.trim() || label.en?.trim();
|
|
3122
|
+
if (en) return en;
|
|
3123
|
+
for (const value of Object.values(label)) {
|
|
3124
|
+
const trimmed = value.trim();
|
|
3125
|
+
if (trimmed) return trimmed;
|
|
3126
|
+
}
|
|
3127
|
+
return fallback;
|
|
3128
|
+
};
|
|
3129
|
+
const durationMs = (entered_at, exited_at) => {
|
|
3130
|
+
if (!exited_at) return null;
|
|
3131
|
+
const start = Date.parse(entered_at);
|
|
3132
|
+
const end = Date.parse(exited_at);
|
|
3133
|
+
if (Number.isNaN(start) || Number.isNaN(end)) return null;
|
|
3134
|
+
return Math.max(0, end - start);
|
|
3135
|
+
};
|
|
3136
|
+
const currentNodes = (workflow, currentNodeIds) => {
|
|
3137
|
+
if (currentNodeIds.length === 0) return [];
|
|
3138
|
+
const current = new Set(currentNodeIds);
|
|
3139
|
+
return workflow.nodes.filter((node) => current.has(node.id));
|
|
3140
|
+
};
|
|
3141
|
+
const visitedStates = (workflow) => {
|
|
3142
|
+
return workflow.nodes.filter((node) => node.data.entered_at).toSorted((a, b) => {
|
|
3143
|
+
const left = a.data.entered_at ?? "";
|
|
3144
|
+
const right = b.data.entered_at ?? "";
|
|
3145
|
+
return left.localeCompare(right);
|
|
3146
|
+
}).map((node) => {
|
|
3147
|
+
const entered_at = node.data.entered_at ?? "";
|
|
3148
|
+
const exited_at = node.data.exited_at;
|
|
3149
|
+
const b1 = {
|
|
3150
|
+
id: node.id,
|
|
3151
|
+
label: node.data.label
|
|
3152
|
+
};
|
|
3153
|
+
const b2 = {
|
|
3154
|
+
mappedStatus: node.data.mappedStatus,
|
|
3155
|
+
entered_at
|
|
3156
|
+
};
|
|
3157
|
+
const b3 = {
|
|
3158
|
+
exited_at,
|
|
3159
|
+
durationMs: durationMs(entered_at, exited_at)
|
|
3160
|
+
};
|
|
3161
|
+
return {
|
|
3162
|
+
...b1,
|
|
3163
|
+
...b2,
|
|
3164
|
+
...b3
|
|
3165
|
+
};
|
|
3166
|
+
});
|
|
3167
|
+
};
|
|
3168
|
+
/** Walk primary-path milestones; one meta entry per order index (deduped). */
|
|
3169
|
+
const walkPrimaryMilestones = (workflow) => {
|
|
3170
|
+
const nodesById = new Map(workflow.nodes.map((node) => [node.id, node]));
|
|
3171
|
+
const primaryIn = /* @__PURE__ */ new Map();
|
|
3172
|
+
const primaryOut = /* @__PURE__ */ new Map();
|
|
3173
|
+
for (const node of workflow.nodes) {
|
|
3174
|
+
primaryIn.set(node.id, []);
|
|
3175
|
+
primaryOut.set(node.id, []);
|
|
3176
|
+
}
|
|
3177
|
+
for (const edge of workflow.edges) {
|
|
3178
|
+
if (!nodesById.has(edge.source) || !nodesById.has(edge.target)) continue;
|
|
3179
|
+
const target = nodesById.get(edge.target);
|
|
3180
|
+
const pathKind = edge.data?.pathKind ?? pathKindForTarget(target);
|
|
3181
|
+
if (!isPrimaryPathKind(pathKind)) continue;
|
|
3182
|
+
primaryOut.get(edge.source)?.push(edge.target);
|
|
3183
|
+
primaryIn.get(edge.target)?.push(edge.source);
|
|
3184
|
+
}
|
|
3185
|
+
const orders = /* @__PURE__ */ new Map();
|
|
3186
|
+
const carryById = /* @__PURE__ */ new Map();
|
|
3187
|
+
const metaByOrder = /* @__PURE__ */ new Map();
|
|
3188
|
+
const parentsReady = (id) => {
|
|
3189
|
+
return (primaryIn.get(id) ?? []).every((parentId) => carryById.has(parentId));
|
|
3190
|
+
};
|
|
3191
|
+
const queue = workflow.nodes.filter((node) => {
|
|
3192
|
+
return (primaryIn.get(node.id) ?? []).length === 0;
|
|
3193
|
+
}).map((node) => node.id);
|
|
3194
|
+
const queued = new Set(queue);
|
|
3195
|
+
while (queue.length > 0) {
|
|
3196
|
+
const id = queue.shift();
|
|
3197
|
+
queued.delete(id);
|
|
3198
|
+
if (carryById.has(id)) continue;
|
|
3199
|
+
const parentCarries = (primaryIn.get(id) ?? []).map((parentId) => {
|
|
3200
|
+
return carryById.get(parentId) ?? 0;
|
|
3201
|
+
});
|
|
3202
|
+
let parentCarry = 0;
|
|
3203
|
+
if (parentCarries.length > 0) parentCarry = Math.max(...parentCarries);
|
|
3204
|
+
const node = nodesById.get(id);
|
|
3205
|
+
const milestone = node?.data.milestone?.trim();
|
|
3206
|
+
let carry = parentCarry;
|
|
3207
|
+
if (milestone) {
|
|
3208
|
+
const order = parentCarry + 1;
|
|
3209
|
+
orders.set(id, order);
|
|
3210
|
+
carry = order;
|
|
3211
|
+
const fallback = milestone.replaceAll("_", " ");
|
|
3212
|
+
const meta = {
|
|
3213
|
+
id,
|
|
3214
|
+
milestone,
|
|
3215
|
+
label: pickLabel(node?.data.label, fallback)
|
|
3216
|
+
};
|
|
3217
|
+
metaByOrder.set(order, meta);
|
|
3218
|
+
}
|
|
3219
|
+
carryById.set(id, carry);
|
|
3220
|
+
for (const child of primaryOut.get(id) ?? []) {
|
|
3221
|
+
if (carryById.has(child) || queued.has(child)) continue;
|
|
3222
|
+
if (!parentsReady(child)) continue;
|
|
3223
|
+
queue.push(child);
|
|
3224
|
+
queued.add(child);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
let total = 0;
|
|
3228
|
+
for (const order of orders.values()) if (order > total) total = order;
|
|
3229
|
+
if (total === 0) return null;
|
|
3230
|
+
return {
|
|
3231
|
+
orders,
|
|
3232
|
+
carryById,
|
|
3233
|
+
metaByOrder,
|
|
3234
|
+
total
|
|
3235
|
+
};
|
|
3236
|
+
};
|
|
3237
|
+
const resolveCurrentMilestoneOrder = (workflow, currentNodeIds, walk) => {
|
|
3238
|
+
const { orders, carryById } = walk;
|
|
3239
|
+
let current = 0;
|
|
3240
|
+
for (const id of currentNodeIds) {
|
|
3241
|
+
const carry = carryById.get(id) ?? 0;
|
|
3242
|
+
if (carry > current) current = carry;
|
|
3243
|
+
}
|
|
3244
|
+
if (current === 0) for (const node of workflow.nodes) {
|
|
3245
|
+
if (!node.data.milestone?.trim()) continue;
|
|
3246
|
+
if (!node.data.entered_at) continue;
|
|
3247
|
+
const order = orders.get(node.id) ?? 0;
|
|
3248
|
+
if (order > current) current = order;
|
|
3249
|
+
}
|
|
3250
|
+
return current;
|
|
3251
|
+
};
|
|
3252
|
+
const completedByOrder = (workflow, walk) => {
|
|
3253
|
+
const result = /* @__PURE__ */ new Map();
|
|
3254
|
+
for (const node of workflow.nodes) {
|
|
3255
|
+
const order = walk.orders.get(node.id);
|
|
3256
|
+
if (order == null) continue;
|
|
3257
|
+
if (!node.data.entered_at) continue;
|
|
3258
|
+
result.set(order, true);
|
|
3259
|
+
}
|
|
3260
|
+
return result;
|
|
3261
|
+
};
|
|
3262
|
+
const stepsFromWalk = (workflow, walk) => {
|
|
3263
|
+
const completed = completedByOrder(workflow, walk);
|
|
3264
|
+
const steps = [];
|
|
3265
|
+
for (let order = 1; order <= walk.total; order++) {
|
|
3266
|
+
const meta = walk.metaByOrder.get(order);
|
|
3267
|
+
if (!meta) continue;
|
|
3268
|
+
const b1 = {
|
|
3269
|
+
id: meta.id,
|
|
3270
|
+
milestone: meta.milestone,
|
|
3271
|
+
order
|
|
3272
|
+
};
|
|
3273
|
+
const b2 = {
|
|
3274
|
+
completed: completed.get(order) === true,
|
|
3275
|
+
label: meta.label
|
|
3276
|
+
};
|
|
3277
|
+
steps.push({
|
|
3278
|
+
...b1,
|
|
3279
|
+
...b2
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
return steps;
|
|
3283
|
+
};
|
|
3284
|
+
/** Last milestone is done once current has reached the end (e.g. Fulfilled). */
|
|
3285
|
+
const isMilestoneStepDone = (order, current, total) => {
|
|
3286
|
+
if (order < current) return true;
|
|
3287
|
+
return current >= total && order === current;
|
|
3288
|
+
};
|
|
3289
|
+
/** Deduped primary-path milestones (one per order index). */
|
|
3290
|
+
const milestoneTrack = (workflow) => {
|
|
3291
|
+
const walk = walkPrimaryMilestones(workflow);
|
|
3292
|
+
if (!walk) return [];
|
|
3293
|
+
return stepsFromWalk(workflow, walk);
|
|
3294
|
+
};
|
|
3295
|
+
/** Ordered milestones + current index from `current_node_ids` (dashboard algo). */
|
|
3296
|
+
const milestoneSteps = (workflow, currentNodeIds) => {
|
|
3297
|
+
const walk = walkPrimaryMilestones(workflow);
|
|
3298
|
+
if (!walk) return null;
|
|
3299
|
+
const current = resolveCurrentMilestoneOrder(workflow, currentNodeIds, walk);
|
|
3300
|
+
if (current === 0) return null;
|
|
3301
|
+
const steps = stepsFromWalk(workflow, walk);
|
|
3302
|
+
if (steps.length === 0) return null;
|
|
3303
|
+
return {
|
|
3304
|
+
steps,
|
|
3305
|
+
current,
|
|
3306
|
+
total: walk.total
|
|
3307
|
+
};
|
|
3308
|
+
};
|
|
3309
|
+
//#endregion
|
|
3310
|
+
//#region src/order/validate.ts
|
|
3311
|
+
const PRICE_FIELDS = [
|
|
3312
|
+
"unit_subtotal",
|
|
3313
|
+
"subtotal",
|
|
3314
|
+
"unit_total",
|
|
3315
|
+
"total",
|
|
3316
|
+
"unit_group_subtotal",
|
|
3317
|
+
"group_subtotal",
|
|
3318
|
+
"unit_group_total",
|
|
3319
|
+
"group_total"
|
|
3320
|
+
];
|
|
3321
|
+
const TAX_FIELDS = [
|
|
3322
|
+
"unit_tax_total",
|
|
3323
|
+
"tax_total",
|
|
3324
|
+
"unit_group_tax_total",
|
|
3325
|
+
"group_tax_total"
|
|
3326
|
+
];
|
|
3327
|
+
const DISCOUNT_FIELDS = [
|
|
3328
|
+
"discount_rate",
|
|
3329
|
+
"unit_discount",
|
|
3330
|
+
"discount_total",
|
|
3331
|
+
"unit_group_discount",
|
|
3332
|
+
"group_discount"
|
|
3333
|
+
];
|
|
3334
|
+
const TOTALS_FIELDS = [
|
|
3335
|
+
"subtotal",
|
|
3336
|
+
"discount_total",
|
|
3337
|
+
"tax_total",
|
|
3338
|
+
"total",
|
|
3339
|
+
"discount_rate",
|
|
3340
|
+
"tax_rate"
|
|
3341
|
+
];
|
|
3342
|
+
const MONEY_SELF_CHECKS = [
|
|
3343
|
+
["unit_subtotal", "subtotal"],
|
|
3344
|
+
["unit_discount", "discount_total"],
|
|
3345
|
+
["unit_tax_total", "tax_total"],
|
|
3346
|
+
["unit_total", "total"]
|
|
3347
|
+
];
|
|
3348
|
+
const pushLineIssue = (lines, uid, issue) => {
|
|
3349
|
+
const list = lines[uid] ?? [];
|
|
3350
|
+
list.push(issue);
|
|
3351
|
+
lines[uid] = list;
|
|
3352
|
+
};
|
|
3353
|
+
const bigEq = (a, b) => {
|
|
3354
|
+
return new Big(a).eq(new Big(b));
|
|
3355
|
+
};
|
|
3356
|
+
const bigStr = (n) => {
|
|
3357
|
+
return new Big(n).toString();
|
|
3358
|
+
};
|
|
3359
|
+
const rateEq = (a, b) => {
|
|
3360
|
+
if (a == null && b == null) return true;
|
|
3361
|
+
if (a == null || b == null) return false;
|
|
3362
|
+
return bigEq(a, b);
|
|
3363
|
+
};
|
|
3364
|
+
const rateStr = (n) => {
|
|
3365
|
+
if (n == null) return "null";
|
|
3366
|
+
return bigStr(n);
|
|
3367
|
+
};
|
|
3368
|
+
const childrenOf = (lines, parentUid) => {
|
|
3369
|
+
const children = lines.filter((line) => {
|
|
3370
|
+
return line.parent_uid === parentUid;
|
|
3371
|
+
});
|
|
3372
|
+
children.sort((a, b) => a.id.localeCompare(b.id));
|
|
3373
|
+
return children;
|
|
3374
|
+
};
|
|
3375
|
+
const matchModifier = (parent, childId, lookup) => {
|
|
3376
|
+
for (const groupId of parent.pricing.modifier_group_ids) if (lookup.modifierGroupsById[groupId]?.modifier_ids.includes(childId)) return true;
|
|
3377
|
+
return false;
|
|
3378
|
+
};
|
|
3379
|
+
const collectUnmatchedModifiers = (lines, lookup, parentProductId, parentUid, out) => {
|
|
3380
|
+
const parent = lookup.productsById[parentProductId];
|
|
3381
|
+
if (!parent) return;
|
|
3382
|
+
for (const child of lines) {
|
|
3383
|
+
if (child.parent_uid !== parentUid) continue;
|
|
3384
|
+
if (child.type === "FEE") continue;
|
|
3385
|
+
if (!matchModifier(parent, child.id, lookup)) {
|
|
3386
|
+
pushLineIssue(out, child.uid, { code: "MODIFIER_NOT_FOUND" });
|
|
3387
|
+
continue;
|
|
3388
|
+
}
|
|
3389
|
+
collectUnmatchedModifiers(lines, lookup, child.id, child.uid, out);
|
|
3390
|
+
}
|
|
3391
|
+
};
|
|
3392
|
+
const checkFeeSelfConsistency = (line, out) => {
|
|
3393
|
+
const qty = new Big(line.quantity);
|
|
3394
|
+
for (const [unitField, totalField] of MONEY_SELF_CHECKS) {
|
|
3395
|
+
const expected = new Big(line[unitField]).times(qty);
|
|
3396
|
+
const actual = new Big(line[totalField]);
|
|
3397
|
+
if (expected.eq(actual)) continue;
|
|
3398
|
+
pushLineIssue(out, line.uid, {
|
|
3399
|
+
code: "QUANTITY_MISMATCH",
|
|
3400
|
+
field: totalField,
|
|
3401
|
+
expected: expected.toString(),
|
|
3402
|
+
actual: actual.toString()
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
};
|
|
3406
|
+
const feeMoneyEq = (actual, expected) => {
|
|
3407
|
+
if (!bigEq(actual.unit_subtotal, expected.unit_subtotal)) return false;
|
|
3408
|
+
if (!bigEq(actual.unit_tax_total, expected.unit_tax_total)) return false;
|
|
3409
|
+
if (!bigEq(actual.unit_total, expected.unit_total)) return false;
|
|
3410
|
+
return rateEq(actual.tax_rate, expected.tax_rate);
|
|
3411
|
+
};
|
|
3412
|
+
const compareExpectedFeeLines = (submitted, expected, out) => {
|
|
3413
|
+
const expectedById = new Map(expected.map((line) => [line.id, line]));
|
|
3414
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3415
|
+
for (const line of submitted) {
|
|
3416
|
+
if (line.type !== "FEE") continue;
|
|
3417
|
+
if (seen.has(line.id)) {
|
|
3418
|
+
pushLineIssue(out, line.uid, {
|
|
3419
|
+
code: "FEE_MISMATCH",
|
|
3420
|
+
field: "id"
|
|
3421
|
+
});
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
3424
|
+
seen.add(line.id);
|
|
3425
|
+
const exp = expectedById.get(line.id);
|
|
3426
|
+
if (!exp) {
|
|
3427
|
+
pushLineIssue(out, line.uid, {
|
|
3428
|
+
code: "FEE_MISMATCH",
|
|
3429
|
+
field: "id"
|
|
3430
|
+
});
|
|
3431
|
+
continue;
|
|
3432
|
+
}
|
|
3433
|
+
if (feeMoneyEq(line, exp)) continue;
|
|
3434
|
+
const b1 = {
|
|
3435
|
+
code: "FEE_MISMATCH",
|
|
3436
|
+
field: "total"
|
|
3437
|
+
};
|
|
3438
|
+
const b2 = {
|
|
3439
|
+
expected: bigStr(exp.total),
|
|
3440
|
+
actual: bigStr(line.total)
|
|
3441
|
+
};
|
|
3442
|
+
pushLineIssue(out, line.uid, {
|
|
3443
|
+
...b1,
|
|
3444
|
+
...b2
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
for (const exp of expected) {
|
|
3448
|
+
if (seen.has(exp.id)) continue;
|
|
3449
|
+
const b1 = {
|
|
3450
|
+
code: "FEE_MISMATCH",
|
|
3451
|
+
field: "total"
|
|
3452
|
+
};
|
|
3453
|
+
const b2 = {
|
|
3454
|
+
expected: bigStr(exp.total),
|
|
3455
|
+
actual: "0"
|
|
3456
|
+
};
|
|
3457
|
+
pushLineIssue(out, exp.uid, {
|
|
3458
|
+
...b1,
|
|
3459
|
+
...b2
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
};
|
|
3463
|
+
const compareMoneyFields = (actual, expected, out) => {
|
|
3464
|
+
for (const field of PRICE_FIELDS) {
|
|
3465
|
+
if (bigEq(actual[field], expected[field])) continue;
|
|
3466
|
+
pushLineIssue(out, actual.uid, {
|
|
3467
|
+
code: "PRICE_MISMATCH",
|
|
3468
|
+
field,
|
|
3469
|
+
expected: bigStr(expected[field]),
|
|
3470
|
+
actual: bigStr(actual[field])
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
for (const field of TAX_FIELDS) {
|
|
3474
|
+
if (bigEq(actual[field], expected[field])) continue;
|
|
3475
|
+
pushLineIssue(out, actual.uid, {
|
|
3476
|
+
code: "TAX_MISMATCH",
|
|
3477
|
+
field,
|
|
3478
|
+
expected: bigStr(expected[field]),
|
|
3479
|
+
actual: bigStr(actual[field])
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
if (!rateEq(actual.tax_rate, expected.tax_rate)) {
|
|
3483
|
+
const b1 = {
|
|
3484
|
+
code: "TAX_MISMATCH",
|
|
3485
|
+
field: "tax_rate"
|
|
3486
|
+
};
|
|
3487
|
+
const b2 = { expected: rateStr(expected.tax_rate) };
|
|
3488
|
+
const b3 = { actual: rateStr(actual.tax_rate) };
|
|
3489
|
+
const p12 = {
|
|
3490
|
+
...b1,
|
|
3491
|
+
...b2
|
|
3492
|
+
};
|
|
3493
|
+
pushLineIssue(out, actual.uid, {
|
|
3494
|
+
...p12,
|
|
3495
|
+
...b3
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
for (const field of DISCOUNT_FIELDS) {
|
|
3499
|
+
if (bigEq(actual[field], expected[field])) continue;
|
|
3500
|
+
pushLineIssue(out, actual.uid, {
|
|
3501
|
+
code: "DISCOUNT_MISMATCH",
|
|
3502
|
+
field,
|
|
3503
|
+
expected: bigStr(expected[field]),
|
|
3504
|
+
actual: bigStr(actual[field])
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
const compareTrees = (actualLines, expectedLines, actualUid, expectedUid, out) => {
|
|
3509
|
+
const actual = actualLines.find((line) => line.uid === actualUid);
|
|
3510
|
+
const expected = expectedLines.find((line) => line.uid === expectedUid);
|
|
3511
|
+
if (!actual || !expected) {
|
|
3512
|
+
if (actual) pushLineIssue(out, actual.uid, { code: "STRUCTURE_MISMATCH" });
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
if (actual.id !== expected.id) {
|
|
3516
|
+
pushLineIssue(out, actual.uid, {
|
|
3517
|
+
code: "STRUCTURE_MISMATCH",
|
|
3518
|
+
field: "id",
|
|
3519
|
+
expected: expected.id,
|
|
3520
|
+
actual: actual.id
|
|
3521
|
+
});
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
if (actual.quantity !== expected.quantity) pushLineIssue(out, actual.uid, {
|
|
3525
|
+
code: "QUANTITY_MISMATCH",
|
|
3526
|
+
field: "quantity",
|
|
3527
|
+
expected: String(expected.quantity),
|
|
3528
|
+
actual: String(actual.quantity)
|
|
3529
|
+
});
|
|
3530
|
+
compareMoneyFields(actual, expected, out);
|
|
3531
|
+
const actualChildren = childrenOf(actualLines, actual.uid);
|
|
3532
|
+
const expectedChildren = childrenOf(expectedLines, expected.uid);
|
|
3533
|
+
if (actualChildren.length !== expectedChildren.length) pushLineIssue(out, actual.uid, {
|
|
3534
|
+
code: "STRUCTURE_MISMATCH",
|
|
3535
|
+
field: "children",
|
|
3536
|
+
expected: String(expectedChildren.length),
|
|
3537
|
+
actual: String(actualChildren.length)
|
|
3538
|
+
});
|
|
3539
|
+
const len = Math.min(actualChildren.length, expectedChildren.length);
|
|
3540
|
+
for (let i = 0; i < len; i++) compareTrees(actualLines, expectedLines, actualChildren[i].uid, expectedChildren[i].uid, out);
|
|
3541
|
+
};
|
|
3542
|
+
const checkRootProductQuantities = (roots, lookup, out) => {
|
|
3543
|
+
const qtyByProductId = {};
|
|
3544
|
+
for (const root of roots) {
|
|
3545
|
+
if (root.type === "FEE") continue;
|
|
3546
|
+
const current = qtyByProductId[root.id];
|
|
3547
|
+
if (current) {
|
|
3548
|
+
current.sum += root.quantity;
|
|
3549
|
+
current.uids.push(root.uid);
|
|
3550
|
+
continue;
|
|
3551
|
+
}
|
|
3552
|
+
qtyByProductId[root.id] = {
|
|
3553
|
+
sum: root.quantity,
|
|
3554
|
+
uids: [root.uid]
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
for (const productId of Object.keys(qtyByProductId)) {
|
|
3558
|
+
const product = lookup.productsById[productId];
|
|
3559
|
+
const agg = qtyByProductId[productId];
|
|
3560
|
+
if (!product || !agg) continue;
|
|
3561
|
+
const min = product.pricing.minimum_quantity;
|
|
3562
|
+
const max = product.pricing.maximum_quantity;
|
|
3563
|
+
const { sum, uids } = agg;
|
|
3564
|
+
if (sum > max) {
|
|
3565
|
+
const b1 = { code: "QUANTITY_ABOVE_MAXIMUM" };
|
|
3566
|
+
const b2 = {
|
|
3567
|
+
field: "quantity",
|
|
3568
|
+
expected: String(max)
|
|
3569
|
+
};
|
|
3570
|
+
const issue = {
|
|
3571
|
+
...b1,
|
|
3572
|
+
...b2,
|
|
3573
|
+
actual: String(sum)
|
|
3574
|
+
};
|
|
3575
|
+
for (const uid of uids) pushLineIssue(out, uid, issue);
|
|
3576
|
+
}
|
|
3577
|
+
if (sum < min) {
|
|
3578
|
+
const b1 = { code: "QUANTITY_BELOW_MINIMUM" };
|
|
3579
|
+
const b2 = {
|
|
3580
|
+
field: "quantity",
|
|
3581
|
+
expected: String(min)
|
|
3582
|
+
};
|
|
3583
|
+
const issue = {
|
|
3584
|
+
...b1,
|
|
3585
|
+
...b2,
|
|
3586
|
+
actual: String(sum)
|
|
3587
|
+
};
|
|
3588
|
+
for (const uid of uids) pushLineIssue(out, uid, issue);
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
};
|
|
3592
|
+
const validateProductRoot = (root, lines, lookup, locale, out) => {
|
|
3593
|
+
const product = lookup.productsById[root.id];
|
|
3594
|
+
if (!product) {
|
|
3595
|
+
pushLineIssue(out, root.uid, { code: "PRODUCT_NOT_FOUND" });
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
const status = product.availability.status;
|
|
3599
|
+
if (status === "HIDDEN") pushLineIssue(out, root.uid, { code: "HIDDEN" });
|
|
3600
|
+
if (status === "OUT_OF_STOCK") pushLineIssue(out, root.uid, { code: "OUT_OF_STOCK" });
|
|
3601
|
+
if (!isOnSchedule(product.availability.schedules)) pushLineIssue(out, root.uid, { code: "OUTSIDE_HOURS" });
|
|
3602
|
+
collectUnmatchedModifiers(lines, lookup, root.id, root.uid, out);
|
|
3603
|
+
const snapshot = fromOrderLines(root, lines, lookup);
|
|
3604
|
+
const session = createSession(lookup, root.id, { snapshot });
|
|
3605
|
+
if (session.validate() !== "VALID") pushLineIssue(out, root.uid, { code: "MODIFIER_RULES_INVALID" });
|
|
3606
|
+
const expected = toOrderLines(session.root, locale);
|
|
3607
|
+
const expectedRoot = expected.find((line) => line.parent_uid == null);
|
|
3608
|
+
if (!expectedRoot) {
|
|
3609
|
+
pushLineIssue(out, root.uid, { code: "STRUCTURE_MISMATCH" });
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
compareTrees(lines, expected, root.uid, expectedRoot.uid, out);
|
|
3613
|
+
};
|
|
3614
|
+
/** Compare cart lines/totals against a fresh menu (exact Big.js equality). */
|
|
3615
|
+
const validateOrderAgainstMenu = (input) => {
|
|
3616
|
+
const locale = input.locale ?? "en_us";
|
|
3617
|
+
const issues = [];
|
|
3618
|
+
const lineIssues = {};
|
|
3619
|
+
if (!input.menu) return {
|
|
3620
|
+
ok: false,
|
|
3621
|
+
issues: [{ code: "NO_MENU" }],
|
|
3622
|
+
lines: {}
|
|
3623
|
+
};
|
|
3624
|
+
if (!input.menuActive) issues.push({ code: "MENU_CLOSED" });
|
|
3625
|
+
const roots = input.lines.filter((line) => line.parent_uid == null);
|
|
3626
|
+
if (roots.length === 0) {
|
|
3627
|
+
issues.push({ code: "EMPTY_ORDER" });
|
|
3628
|
+
return {
|
|
3629
|
+
ok: false,
|
|
3630
|
+
issues,
|
|
3631
|
+
lines: lineIssues
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
const lookup = indexMenu(input.menu);
|
|
3635
|
+
for (const root of roots) {
|
|
3636
|
+
if (root.type === "FEE") {
|
|
3637
|
+
checkFeeSelfConsistency(root, lineIssues);
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
validateProductRoot(root, input.lines, lookup, locale, lineIssues);
|
|
3641
|
+
}
|
|
3642
|
+
checkRootProductQuantities(roots, lookup, lineIssues);
|
|
3643
|
+
if (input.expectedFeeLines) compareExpectedFeeLines(input.lines, input.expectedFeeLines, lineIssues);
|
|
3644
|
+
const expectedTotals = recomputeTotals(input.lines, input.totals.currency);
|
|
3645
|
+
for (const field of TOTALS_FIELDS) {
|
|
3646
|
+
if (bigEq(input.totals[field], expectedTotals[field])) continue;
|
|
3647
|
+
issues.push({
|
|
3648
|
+
code: "TOTALS_MISMATCH",
|
|
3649
|
+
field,
|
|
3650
|
+
expected: bigStr(expectedTotals[field]),
|
|
3651
|
+
actual: bigStr(input.totals[field])
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
const hasLineIssues = Object.keys(lineIssues).length > 0;
|
|
3655
|
+
if (issues.length === 0 && !hasLineIssues) return { ok: true };
|
|
3656
|
+
return {
|
|
3657
|
+
ok: false,
|
|
3658
|
+
issues,
|
|
3659
|
+
lines: lineIssues
|
|
3660
|
+
};
|
|
3661
|
+
};
|
|
3662
|
+
const descendantUids = (lines, rootUid) => {
|
|
3663
|
+
const uids = /* @__PURE__ */ new Set([rootUid]);
|
|
3664
|
+
let grew = true;
|
|
3665
|
+
while (grew) {
|
|
3666
|
+
grew = false;
|
|
3667
|
+
for (const line of lines) {
|
|
3668
|
+
if (!line.parent_uid) continue;
|
|
3669
|
+
if (!uids.has(line.parent_uid) || uids.has(line.uid)) continue;
|
|
3670
|
+
uids.add(line.uid);
|
|
3671
|
+
grew = true;
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
return uids;
|
|
3675
|
+
};
|
|
3676
|
+
const issuesForTree = (lines, rootUid, lineIssues) => {
|
|
3677
|
+
const uids = descendantUids(lines, rootUid);
|
|
3678
|
+
const issues = [];
|
|
3679
|
+
for (const uid of uids) {
|
|
3680
|
+
const list = lineIssues[uid];
|
|
3681
|
+
if (list) issues.push(...list);
|
|
3682
|
+
}
|
|
3683
|
+
return issues;
|
|
3684
|
+
};
|
|
3685
|
+
const collectOrderLineDiffs = (input) => {
|
|
3686
|
+
const locale = input.locale ?? "en_us";
|
|
3687
|
+
const roots = input.lines.filter((line) => {
|
|
3688
|
+
return line.parent_uid == null && line.type === "PRODUCT";
|
|
3689
|
+
});
|
|
3690
|
+
const lookup = input.menu ? indexMenu(input.menu) : null;
|
|
3691
|
+
const diffs = [];
|
|
3692
|
+
for (const root of roots) {
|
|
3693
|
+
const issues = issuesForTree(input.lines, root.uid, input.lineIssues);
|
|
3694
|
+
if (issues.length === 0) continue;
|
|
3695
|
+
const treeUids = descendantUids(input.lines, root.uid);
|
|
3696
|
+
const previous = input.lines.filter((line) => treeUids.has(line.uid));
|
|
3697
|
+
const missing = issues.some((issue) => {
|
|
3698
|
+
return issue.code === "PRODUCT_NOT_FOUND" || issue.code === "HIDDEN";
|
|
3699
|
+
});
|
|
3700
|
+
const product = lookup?.productsById[root.id];
|
|
3701
|
+
if (missing || !product || !lookup) {
|
|
3702
|
+
diffs.push({
|
|
3703
|
+
uid: root.uid,
|
|
3704
|
+
previous,
|
|
3705
|
+
next: null,
|
|
3706
|
+
issues
|
|
3707
|
+
});
|
|
3708
|
+
continue;
|
|
3709
|
+
}
|
|
3710
|
+
try {
|
|
3711
|
+
const snapshot = fromOrderLines(root, input.lines, lookup);
|
|
3712
|
+
const next = toOrderLines(createSession(lookup, root.id, { snapshot }).root, locale);
|
|
3713
|
+
diffs.push({
|
|
3714
|
+
uid: root.uid,
|
|
3715
|
+
previous,
|
|
3716
|
+
next,
|
|
3717
|
+
issues
|
|
3718
|
+
});
|
|
3719
|
+
} catch {
|
|
3720
|
+
diffs.push({
|
|
3721
|
+
uid: root.uid,
|
|
3722
|
+
previous,
|
|
3723
|
+
next: null,
|
|
3724
|
+
issues
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
return diffs;
|
|
3729
|
+
};
|
|
3730
|
+
//#endregion
|
|
3731
|
+
//#region src/rules/assert.ts
|
|
3732
|
+
const assert = (value, message) => {
|
|
3733
|
+
if (value) return;
|
|
3734
|
+
throw new Error(message);
|
|
3735
|
+
};
|
|
3736
|
+
//#endregion
|
|
3737
|
+
//#region src/rules/common.ts
|
|
3738
|
+
const primitiveError = (primitive) => {
|
|
3739
|
+
return `Condition primitive must be ${primitive}`;
|
|
3740
|
+
};
|
|
3741
|
+
const typeError = (type) => {
|
|
3742
|
+
return `Condition type must be ${type}`;
|
|
3743
|
+
};
|
|
3744
|
+
const defaultConditionError = "Condition was not met";
|
|
3745
|
+
const getLineByProductId = (lines, productId) => {
|
|
3746
|
+
if (!productId) return void 0;
|
|
3747
|
+
return lines?.find((line) => line.id === productId);
|
|
3748
|
+
};
|
|
3749
|
+
const countCartProducts = (lines, includeModifiers) => {
|
|
3750
|
+
if (!lines?.length) return 0;
|
|
3751
|
+
return lines.filter((line) => {
|
|
3752
|
+
if (line.type === "FEE") return false;
|
|
3753
|
+
if (includeModifiers) return true;
|
|
3754
|
+
if (!(line.parent_uid == null)) return false;
|
|
3755
|
+
return line.type === "PRODUCT" || line.type == null;
|
|
3756
|
+
}).reduce((sum, line) => sum + line.quantity, 0);
|
|
3757
|
+
};
|
|
3758
|
+
const getLineTagValues = (lines, tag) => {
|
|
3759
|
+
if (!tag) return [];
|
|
3760
|
+
const values = [];
|
|
3761
|
+
for (const line of lines ?? []) {
|
|
3762
|
+
const value = line.tags?.[tag];
|
|
3763
|
+
if (typeof value === "undefined") continue;
|
|
3764
|
+
values.push(value);
|
|
3765
|
+
}
|
|
3766
|
+
return values;
|
|
3767
|
+
};
|
|
3768
|
+
//#endregion
|
|
3769
|
+
//#region src/rules/booleanMatcher.ts
|
|
3770
|
+
const matchBooleanOperator = (condition, matcher) => {
|
|
3771
|
+
const { primitive, operator, value } = condition;
|
|
3772
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
3773
|
+
if (typeof matcher === "undefined") return false;
|
|
3774
|
+
assert(typeof value === "boolean", "Invalid boolean value");
|
|
3775
|
+
switch (operator) {
|
|
3776
|
+
case "is": return matcher === value;
|
|
3777
|
+
case "is not": return matcher !== value;
|
|
3778
|
+
default: throw new Error(`Operator ${operator} not supported in boolean matcher`);
|
|
3779
|
+
}
|
|
3780
|
+
};
|
|
3781
|
+
const isAnonymousCustomer = (condition, isAnonymous) => {
|
|
3782
|
+
const { type, primitive, value } = condition;
|
|
3783
|
+
const matcherType = "Customer is anonymous";
|
|
3784
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
3785
|
+
assert(type === matcherType, typeError(matcherType));
|
|
3786
|
+
assert(value != null, "Invalid value");
|
|
3787
|
+
return matchBooleanOperator(condition, isAnonymous ?? false);
|
|
3788
|
+
};
|
|
3789
|
+
const orderIncludesProductWithTag$3 = (condition, matcher) => {
|
|
3790
|
+
const { type, primitive, value } = condition;
|
|
3791
|
+
const matcherType = "Order includes product with tag";
|
|
3792
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
3793
|
+
assert(type === matcherType, typeError(matcherType));
|
|
3794
|
+
assert(value != null, "Invalid value");
|
|
3795
|
+
return matchBooleanOperator(condition, matcher);
|
|
3796
|
+
};
|
|
3797
|
+
const isAnonymousOrder = (condition, isAnonymous) => {
|
|
3798
|
+
const { type, primitive, value } = condition;
|
|
3799
|
+
const matcherType = "Is anonymous order";
|
|
3800
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
3801
|
+
assert(type === matcherType, typeError(matcherType));
|
|
3802
|
+
assert(value != null, "Invalid value");
|
|
3803
|
+
return matchBooleanOperator(condition, isAnonymous ?? false);
|
|
3804
|
+
};
|
|
3805
|
+
//#endregion
|
|
3806
|
+
//#region src/rules/currencyMatcher.ts
|
|
3807
|
+
const matchCurrencyOperator = (condition, matcher) => {
|
|
3808
|
+
const { primitive, operator, value } = condition;
|
|
3809
|
+
assert(primitive === "currency", primitiveError("currency"));
|
|
3810
|
+
if (typeof matcher === "undefined") return false;
|
|
3811
|
+
assert(typeof value === "number", "Invalid currency value");
|
|
3812
|
+
switch (operator) {
|
|
3813
|
+
case "equal to": return matcher === value;
|
|
3814
|
+
case "not equal to": return matcher !== value;
|
|
3815
|
+
case "greater than": return matcher > value;
|
|
3816
|
+
case "greater than or equal to": return matcher >= value;
|
|
3817
|
+
case "less than": return matcher < value;
|
|
3818
|
+
case "less than or equal to": return matcher <= value;
|
|
3819
|
+
default: throw new Error(`Operator ${operator} not supported in currency matcher`);
|
|
3820
|
+
}
|
|
3821
|
+
};
|
|
3822
|
+
const orderIncludesProductWithTag$2 = (condition, matcher) => {
|
|
3823
|
+
const { type, primitive, value } = condition;
|
|
3824
|
+
const matcherType = "Order includes product with tag";
|
|
3825
|
+
assert(primitive === "currency", primitiveError("currency"));
|
|
3826
|
+
assert(type === matcherType, typeError(matcherType));
|
|
3827
|
+
assert(value != null, "Invalid value");
|
|
3828
|
+
return matchCurrencyOperator(condition, matcher);
|
|
3829
|
+
};
|
|
3830
|
+
const orderTotal = (condition, total) => {
|
|
3831
|
+
const { type, primitive, value } = condition;
|
|
3832
|
+
const matcherType = "Order total";
|
|
3833
|
+
assert(primitive === "currency", primitiveError("currency"));
|
|
3834
|
+
assert(type === matcherType, typeError(matcherType));
|
|
3835
|
+
assert(value != null, "Invalid value");
|
|
3836
|
+
return matchCurrencyOperator(condition, total);
|
|
3837
|
+
};
|
|
3838
|
+
//#endregion
|
|
3839
|
+
//#region src/rules/customMatcher.ts
|
|
3840
|
+
const MULTI_CUSTOM_TYPES = [
|
|
3841
|
+
"Fulfillments",
|
|
3842
|
+
"Stores",
|
|
3843
|
+
"Channels",
|
|
3844
|
+
"Brands"
|
|
3845
|
+
];
|
|
3846
|
+
const isMultiCustomType = (type) => {
|
|
3847
|
+
return MULTI_CUSTOM_TYPES.includes(type);
|
|
3848
|
+
};
|
|
3849
|
+
const splitCsv = (value) => {
|
|
3850
|
+
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
3851
|
+
};
|
|
3852
|
+
const matchCustomEquality = (condition, matcher) => {
|
|
3853
|
+
const { primitive, operator, value } = condition;
|
|
3854
|
+
assert(primitive === "custom", primitiveError("custom"));
|
|
3855
|
+
if (typeof matcher === "undefined") return false;
|
|
3856
|
+
assert(typeof value === "string", "Invalid custom value");
|
|
3857
|
+
switch (operator) {
|
|
3858
|
+
case "is": return matcher === value;
|
|
3859
|
+
case "is not": return matcher !== value;
|
|
3860
|
+
default: throw new Error(`Operator ${operator} not supported in custom matcher`);
|
|
3861
|
+
}
|
|
3862
|
+
};
|
|
3863
|
+
const matchCustomMulti = (condition, matcher) => {
|
|
3864
|
+
const { primitive, operator, value } = condition;
|
|
3865
|
+
assert(primitive === "custom", primitiveError("custom"));
|
|
3866
|
+
if (typeof matcher === "undefined") return false;
|
|
3867
|
+
assert(typeof value === "string", "Invalid custom value");
|
|
3868
|
+
const allowed = splitCsv(value);
|
|
3869
|
+
if (allowed.length === 0) return false;
|
|
3870
|
+
const candidates = [matcher];
|
|
3871
|
+
if (operator === "is any of") return allowed.some((id) => candidates.includes(id));
|
|
3872
|
+
if (operator === "is none of") return !allowed.some((id) => candidates.includes(id));
|
|
3873
|
+
if (operator === "is all of") return allowed.every((id) => candidates.includes(id));
|
|
3874
|
+
throw new Error(`Operator ${operator} not supported in custom matcher`);
|
|
3875
|
+
};
|
|
3876
|
+
const matchCustom = (condition, matcher) => {
|
|
3877
|
+
if (isMultiCustomType(condition.type)) return matchCustomMulti(condition, matcher);
|
|
3878
|
+
return matchCustomEquality(condition, matcher);
|
|
3879
|
+
};
|
|
3880
|
+
const assertCustom = (condition, order) => {
|
|
3881
|
+
const { type } = condition;
|
|
3882
|
+
switch (type) {
|
|
3883
|
+
case "Fulfillment":
|
|
3884
|
+
case "Fulfillments": return matchCustom(condition, order.fulfillment_id);
|
|
3885
|
+
case "Store":
|
|
3886
|
+
case "Stores": return matchCustom(condition, order.store_id);
|
|
3887
|
+
case "Channel":
|
|
3888
|
+
case "Channels": return matchCustom(condition, order.channel_id);
|
|
3889
|
+
case "Brand":
|
|
3890
|
+
case "Brands": return matchCustom(condition, order.brand_id);
|
|
3891
|
+
default: throw new Error(`Condition ${type} not supported in custom matcher`);
|
|
3892
|
+
}
|
|
3893
|
+
};
|
|
3894
|
+
//#endregion
|
|
3895
|
+
//#region src/rules/enumMatcher.ts
|
|
3896
|
+
const matchEnumOperator = (condition, matcher) => {
|
|
3897
|
+
const { primitive, operator, value } = condition;
|
|
3898
|
+
assert(primitive === "enum", primitiveError("enum"));
|
|
3899
|
+
if (typeof matcher === "undefined") return false;
|
|
3900
|
+
switch (operator) {
|
|
3901
|
+
case "is":
|
|
3902
|
+
assert(typeof value === "string", "Expected a single enum value");
|
|
3903
|
+
return matcher === value;
|
|
3904
|
+
case "is not":
|
|
3905
|
+
assert(typeof value === "string", "Expected a single enum value");
|
|
3906
|
+
return matcher !== value;
|
|
3907
|
+
case "is any of":
|
|
3908
|
+
assert(Array.isArray(value), "Expected a list of enum values");
|
|
3909
|
+
return value.includes(matcher);
|
|
3910
|
+
case "is none of":
|
|
3911
|
+
assert(Array.isArray(value), "Expected a list of enum values");
|
|
3912
|
+
return !value.includes(matcher);
|
|
3913
|
+
default: throw new Error(`Operator ${operator} not supported in enum matcher`);
|
|
3914
|
+
}
|
|
3915
|
+
};
|
|
3916
|
+
const paymentCollectAt = (condition, collectAt) => {
|
|
3917
|
+
const matcherType = "Payment collect at";
|
|
3918
|
+
assert(condition.primitive === "enum", primitiveError("enum"));
|
|
3919
|
+
assert(condition.type === matcherType, typeError(matcherType));
|
|
3920
|
+
return matchEnumOperator(condition, collectAt);
|
|
3921
|
+
};
|
|
3922
|
+
const paymentMethod = (condition, method) => {
|
|
3923
|
+
const matcherType = "Payment method";
|
|
3924
|
+
assert(condition.primitive === "enum", primitiveError("enum"));
|
|
3925
|
+
assert(condition.type === matcherType, typeError(matcherType));
|
|
3926
|
+
return matchEnumOperator(condition, method);
|
|
3927
|
+
};
|
|
3928
|
+
const paymentStatus = (condition, status) => {
|
|
3929
|
+
const matcherType = "Payment status";
|
|
3930
|
+
assert(condition.primitive === "enum", primitiveError("enum"));
|
|
3931
|
+
assert(condition.type === matcherType, typeError(matcherType));
|
|
3932
|
+
return matchEnumOperator(condition, status);
|
|
3933
|
+
};
|
|
3934
|
+
const CONDITION_INTERPRETATION_DEFAULT = "server";
|
|
3935
|
+
const isInterpretationCompatible = (interpretation, runtime, fallback) => {
|
|
3936
|
+
if (!runtime) return true;
|
|
3937
|
+
const value = interpretation ?? fallback;
|
|
3938
|
+
if (value === "both") return true;
|
|
3939
|
+
return value === runtime;
|
|
3940
|
+
};
|
|
3941
|
+
//#endregion
|
|
3942
|
+
//#region src/rules/numberMatcher.ts
|
|
3943
|
+
const matchNumberOperator = (condition, matcher) => {
|
|
3944
|
+
const { primitive, operator, value } = condition;
|
|
3945
|
+
assert(primitive === "number", primitiveError("number"));
|
|
3946
|
+
if (typeof matcher === "undefined") return false;
|
|
3947
|
+
assert(typeof value === "number", "Invalid number value");
|
|
3948
|
+
switch (operator) {
|
|
3949
|
+
case "equal to": return matcher === value;
|
|
3950
|
+
case "not equal to": return matcher !== value;
|
|
3951
|
+
case "greater than": return matcher > value;
|
|
3952
|
+
case "greater than or equal to": return matcher >= value;
|
|
3953
|
+
case "less than": return matcher < value;
|
|
3954
|
+
case "less than or equal to": return matcher <= value;
|
|
3955
|
+
default: throw new Error(`Operator ${operator} not supported in number matcher`);
|
|
3956
|
+
}
|
|
3957
|
+
};
|
|
3958
|
+
const orderIncludesProduct = (condition, quantity) => {
|
|
3959
|
+
const { type, primitive, value } = condition;
|
|
3960
|
+
const matcherType = "Order includes product";
|
|
3961
|
+
assert(primitive === "number", primitiveError("number"));
|
|
3962
|
+
assert(type === matcherType, typeError(matcherType));
|
|
3963
|
+
assert(value != null, "Invalid value");
|
|
3964
|
+
return matchNumberOperator(condition, quantity ?? 0);
|
|
3965
|
+
};
|
|
3966
|
+
const cartProductCount = (condition, count) => {
|
|
3967
|
+
const { type, primitive, value } = condition;
|
|
3968
|
+
const matcherType = "Cart product count";
|
|
3969
|
+
assert(primitive === "number", primitiveError("number"));
|
|
3970
|
+
assert(type === matcherType, typeError(matcherType));
|
|
3971
|
+
assert(value != null, "Invalid value");
|
|
3972
|
+
return matchNumberOperator(condition, count);
|
|
3973
|
+
};
|
|
3974
|
+
const orderIncludesProductWithTag$1 = (condition, matcher) => {
|
|
3975
|
+
const { type, primitive, value } = condition;
|
|
3976
|
+
const matcherType = "Order includes product with tag";
|
|
3977
|
+
assert(primitive === "number", primitiveError("number"));
|
|
3978
|
+
assert(type === matcherType, typeError(matcherType));
|
|
3979
|
+
assert(value != null, "Invalid value");
|
|
3980
|
+
return matchNumberOperator(condition, matcher);
|
|
3981
|
+
};
|
|
3982
|
+
//#endregion
|
|
3983
|
+
//#region src/rules/stringCondition.utils.ts
|
|
3984
|
+
const getCaseInsensitive = (condition) => {
|
|
3985
|
+
return condition.caseInsensitive ?? false;
|
|
3986
|
+
};
|
|
3987
|
+
const normalizeStringForMatch = (value, caseInsensitive) => {
|
|
3988
|
+
if (caseInsensitive) return value.toLowerCase();
|
|
3989
|
+
return value;
|
|
3990
|
+
};
|
|
3991
|
+
const normalizePairForMatch = (matcher, expected, caseInsensitive) => {
|
|
3992
|
+
return {
|
|
3993
|
+
matcher: normalizeStringForMatch(matcher, caseInsensitive),
|
|
3994
|
+
expected: normalizeStringForMatch(expected, caseInsensitive)
|
|
3995
|
+
};
|
|
3996
|
+
};
|
|
3997
|
+
//#endregion
|
|
3998
|
+
//#region src/rules/stringMatcher.ts
|
|
3999
|
+
const matchStringOperator = (condition, matcher) => {
|
|
4000
|
+
const { primitive, operator, value } = condition;
|
|
4001
|
+
assert(primitive === "string", primitiveError("string"));
|
|
4002
|
+
if (typeof matcher === "undefined") return false;
|
|
4003
|
+
const caseInsensitive = getCaseInsensitive(condition);
|
|
4004
|
+
const pair = normalizePairForMatch(matcher, String(value), caseInsensitive);
|
|
4005
|
+
const normalizedMatcher = pair.matcher;
|
|
4006
|
+
const normalizedExpected = pair.expected;
|
|
4007
|
+
switch (operator) {
|
|
4008
|
+
case "equal to": return normalizedMatcher === normalizedExpected;
|
|
4009
|
+
case "not equal to": return normalizedMatcher !== normalizedExpected;
|
|
4010
|
+
case "starts with": return normalizedMatcher.startsWith(normalizedExpected);
|
|
4011
|
+
case "ends with": return normalizedMatcher.endsWith(normalizedExpected);
|
|
4012
|
+
case "contains": return normalizedMatcher.includes(normalizedExpected);
|
|
4013
|
+
default: throw new Error(`Operator ${operator} not supported in string matcher`);
|
|
4014
|
+
}
|
|
4015
|
+
};
|
|
4016
|
+
const orderIncludesProductWithTag = (condition, matcher) => {
|
|
4017
|
+
const { type, primitive, value } = condition;
|
|
4018
|
+
const matcherType = "Order includes product with tag";
|
|
4019
|
+
assert(primitive === "string", primitiveError("string"));
|
|
4020
|
+
assert(type === matcherType, typeError(matcherType));
|
|
4021
|
+
assert(value != null, "Invalid value");
|
|
4022
|
+
return matchStringOperator(condition, matcher);
|
|
4023
|
+
};
|
|
4024
|
+
//#endregion
|
|
4025
|
+
//#region src/rules/monitoringMatcher.ts
|
|
4026
|
+
const windowOf = (condition) => {
|
|
4027
|
+
if (!("window_minutes" in condition)) return void 0;
|
|
4028
|
+
return condition.window_minutes;
|
|
4029
|
+
};
|
|
4030
|
+
const accumulatedEvents = (condition, count) => {
|
|
4031
|
+
const { type, primitive, value } = condition;
|
|
4032
|
+
const matcherType = "Accumulated events";
|
|
4033
|
+
assert(primitive === "number", primitiveError("number"));
|
|
4034
|
+
assert(type === matcherType, typeError(matcherType));
|
|
4035
|
+
assert(value != null, "Invalid value");
|
|
4036
|
+
if (typeof count === "undefined") return false;
|
|
4037
|
+
return matchNumberOperator(condition, count);
|
|
4038
|
+
};
|
|
4039
|
+
const apiErrorRate = (condition, rate) => {
|
|
4040
|
+
const { type, primitive, value } = condition;
|
|
4041
|
+
const matcherType = "API error rate";
|
|
4042
|
+
assert(primitive === "number", primitiveError("number"));
|
|
4043
|
+
assert(type === matcherType, typeError(matcherType));
|
|
4044
|
+
assert(value != null, "Invalid value");
|
|
4045
|
+
if (typeof rate === "undefined") return false;
|
|
4046
|
+
return matchNumberOperator(condition, rate);
|
|
4047
|
+
};
|
|
4048
|
+
const httpStatus = (condition, status) => {
|
|
4049
|
+
const { type, primitive, value } = condition;
|
|
4050
|
+
const matcherType = "HTTP status";
|
|
4051
|
+
assert(primitive === "number", primitiveError("number"));
|
|
4052
|
+
assert(type === matcherType, typeError(matcherType));
|
|
4053
|
+
assert(value != null, "Invalid value");
|
|
4054
|
+
if (typeof status === "undefined") return false;
|
|
4055
|
+
return matchNumberOperator(condition, status);
|
|
4056
|
+
};
|
|
4057
|
+
const requestPath = (condition, path) => {
|
|
4058
|
+
const { type, primitive, value } = condition;
|
|
4059
|
+
const matcherType = "Request path";
|
|
4060
|
+
assert(primitive === "string", primitiveError("string"));
|
|
4061
|
+
assert(type === matcherType, typeError(matcherType));
|
|
4062
|
+
assert(value != null, "Invalid value");
|
|
4063
|
+
return matchStringOperator(condition, path);
|
|
4064
|
+
};
|
|
4065
|
+
const failureMessage = (condition, message) => {
|
|
4066
|
+
const { type, primitive, value } = condition;
|
|
4067
|
+
const matcherType = "Failure message";
|
|
4068
|
+
assert(primitive === "string", primitiveError("string"));
|
|
4069
|
+
assert(type === matcherType, typeError(matcherType));
|
|
4070
|
+
assert(value != null, "Invalid value");
|
|
4071
|
+
return matchStringOperator(condition, message);
|
|
4072
|
+
};
|
|
4073
|
+
const windowMinutes = (condition) => {
|
|
4074
|
+
return windowOf(condition);
|
|
4075
|
+
};
|
|
4076
|
+
//#endregion
|
|
4077
|
+
//#region src/rules/recurringDateCron.ts
|
|
4078
|
+
var import_dayjs_min = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
4079
|
+
(function(t, e) {
|
|
4080
|
+
"object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs = e();
|
|
4081
|
+
})(exports, (function() {
|
|
4082
|
+
"use strict";
|
|
4083
|
+
var t = 1e3, e = 6e4, n = 36e5, r = "millisecond", i = "second", s = "minute", u = "hour", a = "day", o = "week", c = "month", f = "quarter", h = "year", d = "date", l = "Invalid Date", $ = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/, y = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g, M = {
|
|
4084
|
+
name: "en",
|
|
4085
|
+
weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
|
|
4086
|
+
months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
|
|
4087
|
+
ordinal: function(t) {
|
|
4088
|
+
var e = [
|
|
4089
|
+
"th",
|
|
4090
|
+
"st",
|
|
4091
|
+
"nd",
|
|
4092
|
+
"rd"
|
|
4093
|
+
], n = t % 100;
|
|
4094
|
+
return "[" + t + (e[(n - 20) % 10] || e[n] || e[0]) + "]";
|
|
4095
|
+
}
|
|
4096
|
+
}, m = function(t, e, n) {
|
|
4097
|
+
var r = String(t);
|
|
4098
|
+
return !r || r.length >= e ? t : "" + Array(e + 1 - r.length).join(n) + t;
|
|
4099
|
+
}, v = {
|
|
4100
|
+
s: m,
|
|
4101
|
+
z: function(t) {
|
|
4102
|
+
var e = -t.utcOffset(), n = Math.abs(e), r = Math.floor(n / 60), i = n % 60;
|
|
4103
|
+
return (e <= 0 ? "+" : "-") + m(r, 2, "0") + ":" + m(i, 2, "0");
|
|
4104
|
+
},
|
|
4105
|
+
m: function t(e, n) {
|
|
4106
|
+
if (e.date() < n.date()) return -t(n, e);
|
|
4107
|
+
var r = 12 * (n.year() - e.year()) + (n.month() - e.month()), i = e.clone().add(r, c), s = n - i < 0, u = e.clone().add(r + (s ? -1 : 1), c);
|
|
4108
|
+
return +(-(r + (n - i) / (s ? i - u : u - i)) || 0);
|
|
4109
|
+
},
|
|
4110
|
+
a: function(t) {
|
|
4111
|
+
return t < 0 ? Math.ceil(t) || 0 : Math.floor(t);
|
|
4112
|
+
},
|
|
4113
|
+
p: function(t) {
|
|
4114
|
+
return {
|
|
4115
|
+
M: c,
|
|
4116
|
+
y: h,
|
|
4117
|
+
w: o,
|
|
4118
|
+
d: a,
|
|
4119
|
+
D: d,
|
|
4120
|
+
h: u,
|
|
4121
|
+
m: s,
|
|
4122
|
+
s: i,
|
|
4123
|
+
ms: r,
|
|
4124
|
+
Q: f
|
|
4125
|
+
}[t] || String(t || "").toLowerCase().replace(/s$/, "");
|
|
4126
|
+
},
|
|
4127
|
+
u: function(t) {
|
|
4128
|
+
return void 0 === t;
|
|
4129
|
+
}
|
|
4130
|
+
}, g = "en", D = {};
|
|
4131
|
+
D[g] = M;
|
|
4132
|
+
var p = function(t) {
|
|
4133
|
+
return t instanceof b;
|
|
4134
|
+
}, S = function t(e, n, r) {
|
|
4135
|
+
var i;
|
|
4136
|
+
if (!e) return g;
|
|
4137
|
+
if ("string" == typeof e) {
|
|
4138
|
+
var s = e.toLowerCase();
|
|
4139
|
+
D[s] && (i = s), n && (D[s] = n, i = s);
|
|
4140
|
+
var u = e.split("-");
|
|
4141
|
+
if (!i && u.length > 1) return t(u[0]);
|
|
4142
|
+
} else {
|
|
4143
|
+
var a = e.name;
|
|
4144
|
+
D[a] = e, i = a;
|
|
4145
|
+
}
|
|
4146
|
+
return !r && i && (g = i), i || !r && g;
|
|
4147
|
+
}, w = function(t, e) {
|
|
4148
|
+
if (p(t)) return t.clone();
|
|
4149
|
+
var n = "object" == typeof e ? e : {};
|
|
4150
|
+
return n.date = t, n.args = arguments, new b(n);
|
|
4151
|
+
}, O = v;
|
|
4152
|
+
O.l = S, O.i = p, O.w = function(t, e) {
|
|
4153
|
+
return w(t, {
|
|
4154
|
+
locale: e.$L,
|
|
4155
|
+
utc: e.$u,
|
|
4156
|
+
x: e.$x,
|
|
4157
|
+
$offset: e.$offset
|
|
4158
|
+
});
|
|
4159
|
+
};
|
|
4160
|
+
var b = function() {
|
|
4161
|
+
function M(t) {
|
|
4162
|
+
this.$L = S(t.locale, null, !0), this.parse(t);
|
|
4163
|
+
}
|
|
4164
|
+
var m = M.prototype;
|
|
4165
|
+
return m.parse = function(t) {
|
|
4166
|
+
this.$d = function(t) {
|
|
4167
|
+
var e = t.date, n = t.utc;
|
|
4168
|
+
if (null === e) return /* @__PURE__ */ new Date(NaN);
|
|
4169
|
+
if (O.u(e)) return /* @__PURE__ */ new Date();
|
|
4170
|
+
if (e instanceof Date) return new Date(e);
|
|
4171
|
+
if ("string" == typeof e && !/Z$/i.test(e)) {
|
|
4172
|
+
var r = e.match($);
|
|
4173
|
+
if (r) {
|
|
4174
|
+
var i = r[2] - 1 || 0, s = (r[7] || "0").substring(0, 3);
|
|
4175
|
+
return n ? new Date(Date.UTC(r[1], i, r[3] || 1, r[4] || 0, r[5] || 0, r[6] || 0, s)) : new Date(r[1], i, r[3] || 1, r[4] || 0, r[5] || 0, r[6] || 0, s);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
return new Date(e);
|
|
4179
|
+
}(t), this.$x = t.x || {}, this.init();
|
|
4180
|
+
}, m.init = function() {
|
|
4181
|
+
var t = this.$d;
|
|
4182
|
+
this.$y = t.getFullYear(), this.$M = t.getMonth(), this.$D = t.getDate(), this.$W = t.getDay(), this.$H = t.getHours(), this.$m = t.getMinutes(), this.$s = t.getSeconds(), this.$ms = t.getMilliseconds();
|
|
4183
|
+
}, m.$utils = function() {
|
|
4184
|
+
return O;
|
|
4185
|
+
}, m.isValid = function() {
|
|
4186
|
+
return !(this.$d.toString() === l);
|
|
4187
|
+
}, m.isSame = function(t, e) {
|
|
4188
|
+
var n = w(t);
|
|
4189
|
+
return this.startOf(e) <= n && n <= this.endOf(e);
|
|
4190
|
+
}, m.isAfter = function(t, e) {
|
|
4191
|
+
return w(t) < this.startOf(e);
|
|
4192
|
+
}, m.isBefore = function(t, e) {
|
|
4193
|
+
return this.endOf(e) < w(t);
|
|
4194
|
+
}, m.$g = function(t, e, n) {
|
|
4195
|
+
return O.u(t) ? this[e] : this.set(n, t);
|
|
4196
|
+
}, m.unix = function() {
|
|
4197
|
+
return Math.floor(this.valueOf() / 1e3);
|
|
4198
|
+
}, m.valueOf = function() {
|
|
4199
|
+
return this.$d.getTime();
|
|
4200
|
+
}, m.startOf = function(t, e) {
|
|
4201
|
+
var n = this, r = !!O.u(e) || e, f = O.p(t), l = function(t, e) {
|
|
4202
|
+
var i = O.w(n.$u ? Date.UTC(n.$y, e, t) : new Date(n.$y, e, t), n);
|
|
4203
|
+
return r ? i : i.endOf(a);
|
|
4204
|
+
}, $ = function(t, e) {
|
|
4205
|
+
return O.w(n.toDate()[t].apply(n.toDate("s"), (r ? [
|
|
4206
|
+
0,
|
|
4207
|
+
0,
|
|
4208
|
+
0,
|
|
4209
|
+
0
|
|
4210
|
+
] : [
|
|
4211
|
+
23,
|
|
4212
|
+
59,
|
|
4213
|
+
59,
|
|
4214
|
+
999
|
|
4215
|
+
]).slice(e)), n);
|
|
4216
|
+
}, y = this.$W, M = this.$M, m = this.$D, v = "set" + (this.$u ? "UTC" : "");
|
|
4217
|
+
switch (f) {
|
|
4218
|
+
case h: return r ? l(1, 0) : l(31, 11);
|
|
4219
|
+
case c: return r ? l(1, M) : l(0, M + 1);
|
|
4220
|
+
case o:
|
|
4221
|
+
var g = this.$locale().weekStart || 0, D = (y < g ? y + 7 : y) - g;
|
|
4222
|
+
return l(r ? m - D : m + (6 - D), M);
|
|
4223
|
+
case a:
|
|
4224
|
+
case d: return $(v + "Hours", 0);
|
|
4225
|
+
case u: return $(v + "Minutes", 1);
|
|
4226
|
+
case s: return $(v + "Seconds", 2);
|
|
4227
|
+
case i: return $(v + "Milliseconds", 3);
|
|
4228
|
+
default: return this.clone();
|
|
4229
|
+
}
|
|
4230
|
+
}, m.endOf = function(t) {
|
|
4231
|
+
return this.startOf(t, !1);
|
|
4232
|
+
}, m.$set = function(t, e) {
|
|
4233
|
+
var n, o = O.p(t), f = "set" + (this.$u ? "UTC" : ""), l = (n = {}, n[a] = f + "Date", n[d] = f + "Date", n[c] = f + "Month", n[h] = f + "FullYear", n[u] = f + "Hours", n[s] = f + "Minutes", n[i] = f + "Seconds", n[r] = f + "Milliseconds", n)[o], $ = o === a ? this.$D + (e - this.$W) : e;
|
|
4234
|
+
if (o === c || o === h) {
|
|
4235
|
+
var y = this.clone().set(d, 1);
|
|
4236
|
+
y.$d[l]($), y.init(), this.$d = y.set(d, Math.min(this.$D, y.daysInMonth())).$d;
|
|
4237
|
+
} else l && this.$d[l]($);
|
|
4238
|
+
return this.init(), this;
|
|
4239
|
+
}, m.set = function(t, e) {
|
|
4240
|
+
return this.clone().$set(t, e);
|
|
4241
|
+
}, m.get = function(t) {
|
|
4242
|
+
return this[O.p(t)]();
|
|
4243
|
+
}, m.add = function(r, f) {
|
|
4244
|
+
var d, l = this;
|
|
4245
|
+
r = Number(r);
|
|
4246
|
+
var $ = O.p(f), y = function(t) {
|
|
4247
|
+
var e = w(l);
|
|
4248
|
+
return O.w(e.date(e.date() + Math.round(t * r)), l);
|
|
4249
|
+
};
|
|
4250
|
+
if ($ === c) return this.set(c, this.$M + r);
|
|
4251
|
+
if ($ === h) return this.set(h, this.$y + r);
|
|
4252
|
+
if ($ === a) return y(1);
|
|
4253
|
+
if ($ === o) return y(7);
|
|
4254
|
+
var M = (d = {}, d[s] = e, d[u] = n, d[i] = t, d)[$] || 1, m = this.$d.getTime() + r * M;
|
|
4255
|
+
return O.w(m, this);
|
|
4256
|
+
}, m.subtract = function(t, e) {
|
|
4257
|
+
return this.add(-1 * t, e);
|
|
4258
|
+
}, m.format = function(t) {
|
|
4259
|
+
var e = this, n = this.$locale();
|
|
4260
|
+
if (!this.isValid()) return n.invalidDate || l;
|
|
4261
|
+
var r = t || "YYYY-MM-DDTHH:mm:ssZ", i = O.z(this), s = this.$H, u = this.$m, a = this.$M, o = n.weekdays, c = n.months, f = n.meridiem, h = function(t, n, i, s) {
|
|
4262
|
+
return t && (t[n] || t(e, r)) || i[n].slice(0, s);
|
|
4263
|
+
}, d = function(t) {
|
|
4264
|
+
return O.s(s % 12 || 12, t, "0");
|
|
4265
|
+
}, $ = f || function(t, e, n) {
|
|
4266
|
+
var r = t < 12 ? "AM" : "PM";
|
|
4267
|
+
return n ? r.toLowerCase() : r;
|
|
4268
|
+
};
|
|
4269
|
+
return r.replace(y, (function(t, r) {
|
|
4270
|
+
return r || function(t) {
|
|
4271
|
+
switch (t) {
|
|
4272
|
+
case "YY": return String(e.$y).slice(-2);
|
|
4273
|
+
case "YYYY": return O.s(e.$y, 4, "0");
|
|
4274
|
+
case "M": return a + 1;
|
|
4275
|
+
case "MM": return O.s(a + 1, 2, "0");
|
|
4276
|
+
case "MMM": return h(n.monthsShort, a, c, 3);
|
|
4277
|
+
case "MMMM": return h(c, a);
|
|
4278
|
+
case "D": return e.$D;
|
|
4279
|
+
case "DD": return O.s(e.$D, 2, "0");
|
|
4280
|
+
case "d": return String(e.$W);
|
|
4281
|
+
case "dd": return h(n.weekdaysMin, e.$W, o, 2);
|
|
4282
|
+
case "ddd": return h(n.weekdaysShort, e.$W, o, 3);
|
|
4283
|
+
case "dddd": return o[e.$W];
|
|
4284
|
+
case "H": return String(s);
|
|
4285
|
+
case "HH": return O.s(s, 2, "0");
|
|
4286
|
+
case "h": return d(1);
|
|
4287
|
+
case "hh": return d(2);
|
|
4288
|
+
case "a": return $(s, u, !0);
|
|
4289
|
+
case "A": return $(s, u, !1);
|
|
4290
|
+
case "m": return String(u);
|
|
4291
|
+
case "mm": return O.s(u, 2, "0");
|
|
4292
|
+
case "s": return String(e.$s);
|
|
4293
|
+
case "ss": return O.s(e.$s, 2, "0");
|
|
4294
|
+
case "SSS": return O.s(e.$ms, 3, "0");
|
|
4295
|
+
case "Z": return i;
|
|
4296
|
+
}
|
|
4297
|
+
return null;
|
|
4298
|
+
}(t) || i.replace(":", "");
|
|
4299
|
+
}));
|
|
4300
|
+
}, m.utcOffset = function() {
|
|
4301
|
+
return 15 * -Math.round(this.$d.getTimezoneOffset() / 15);
|
|
4302
|
+
}, m.diff = function(r, d, l) {
|
|
4303
|
+
var $, y = this, M = O.p(d), m = w(r), v = (m.utcOffset() - this.utcOffset()) * e, g = this - m, D = function() {
|
|
4304
|
+
return O.m(y, m);
|
|
4305
|
+
};
|
|
4306
|
+
switch (M) {
|
|
4307
|
+
case h:
|
|
4308
|
+
$ = D() / 12;
|
|
4309
|
+
break;
|
|
4310
|
+
case c:
|
|
4311
|
+
$ = D();
|
|
4312
|
+
break;
|
|
4313
|
+
case f:
|
|
4314
|
+
$ = D() / 3;
|
|
4315
|
+
break;
|
|
4316
|
+
case o:
|
|
4317
|
+
$ = (g - v) / 6048e5;
|
|
4318
|
+
break;
|
|
4319
|
+
case a:
|
|
4320
|
+
$ = (g - v) / 864e5;
|
|
4321
|
+
break;
|
|
4322
|
+
case u:
|
|
4323
|
+
$ = g / n;
|
|
4324
|
+
break;
|
|
4325
|
+
case s:
|
|
4326
|
+
$ = g / e;
|
|
4327
|
+
break;
|
|
4328
|
+
case i:
|
|
4329
|
+
$ = g / t;
|
|
4330
|
+
break;
|
|
4331
|
+
default: $ = g;
|
|
4332
|
+
}
|
|
4333
|
+
return l ? $ : O.a($);
|
|
4334
|
+
}, m.daysInMonth = function() {
|
|
4335
|
+
return this.endOf(c).$D;
|
|
4336
|
+
}, m.$locale = function() {
|
|
4337
|
+
return D[this.$L];
|
|
4338
|
+
}, m.locale = function(t, e) {
|
|
4339
|
+
if (!t) return this.$L;
|
|
4340
|
+
var n = this.clone(), r = S(t, e, !0);
|
|
4341
|
+
return r && (n.$L = r), n;
|
|
4342
|
+
}, m.clone = function() {
|
|
4343
|
+
return O.w(this.$d, this);
|
|
4344
|
+
}, m.toDate = function() {
|
|
4345
|
+
return new Date(this.valueOf());
|
|
4346
|
+
}, m.toJSON = function() {
|
|
4347
|
+
return this.isValid() ? this.toISOString() : null;
|
|
4348
|
+
}, m.toISOString = function() {
|
|
4349
|
+
return this.$d.toISOString();
|
|
4350
|
+
}, m.toString = function() {
|
|
4351
|
+
return this.$d.toUTCString();
|
|
4352
|
+
}, M;
|
|
4353
|
+
}(), _ = b.prototype;
|
|
4354
|
+
return w.prototype = _, [
|
|
4355
|
+
["$ms", r],
|
|
4356
|
+
["$s", i],
|
|
4357
|
+
["$m", s],
|
|
4358
|
+
["$H", u],
|
|
4359
|
+
["$W", a],
|
|
4360
|
+
["$M", c],
|
|
4361
|
+
["$y", h],
|
|
4362
|
+
["$D", d]
|
|
4363
|
+
].forEach((function(t) {
|
|
4364
|
+
_[t[1]] = function(e) {
|
|
4365
|
+
return this.$g(e, t[0], t[1]);
|
|
4366
|
+
};
|
|
4367
|
+
})), w.extend = function(t, e) {
|
|
4368
|
+
return t.$i || (t(e, b, w), t.$i = !0), w;
|
|
4369
|
+
}, w.locale = S, w.isDayjs = p, w.unix = function(t) {
|
|
4370
|
+
return w(1e3 * t);
|
|
4371
|
+
}, w.en = D[g], w.Ls = D, w.p = {}, w;
|
|
4372
|
+
}));
|
|
4373
|
+
})))(), 1);
|
|
4374
|
+
import_dayjs_min.default.extend(utc);
|
|
4375
|
+
import_dayjs_min.default.extend(timezone);
|
|
4376
|
+
import_dayjs_min.default.extend(isBetween);
|
|
4377
|
+
import_dayjs_min.default.extend(isSameOrAfter);
|
|
4378
|
+
import_dayjs_min.default.extend(weekOfYear);
|
|
4379
|
+
import_dayjs_min.default.extend(advancedFormat);
|
|
4380
|
+
var RecurringDateCron = class {
|
|
4381
|
+
startDate;
|
|
4382
|
+
endDate;
|
|
4383
|
+
repeat;
|
|
4384
|
+
timezone;
|
|
4385
|
+
constructor(values, timezone) {
|
|
4386
|
+
this.startDate = new Date(values.startDate);
|
|
4387
|
+
this.endDate = new Date(values.endDate);
|
|
4388
|
+
this.repeat = values.repeat;
|
|
4389
|
+
this.timezone = timezone;
|
|
4390
|
+
}
|
|
4391
|
+
day = (...params) => {
|
|
4392
|
+
return (0, import_dayjs_min.default)(...params).tz(this.timezone);
|
|
4393
|
+
};
|
|
4394
|
+
getExpression() {
|
|
4395
|
+
const start = this.day(this.startDate);
|
|
4396
|
+
const end = this.day(this.endDate);
|
|
4397
|
+
const startDayOfMonth = start.date();
|
|
4398
|
+
const endDayOfMonth = end.date();
|
|
4399
|
+
const dayOfMonthRange = `${startDayOfMonth}-${endDayOfMonth}`;
|
|
4400
|
+
const sameDayOfMonth = startDayOfMonth === endDayOfMonth;
|
|
4401
|
+
const startDayOfWeek = start.day();
|
|
4402
|
+
const endDayOfWeek = end.day();
|
|
4403
|
+
const dayOfWeekRange = `${startDayOfWeek}-${endDayOfWeek}`;
|
|
4404
|
+
const sameDayOfWeek = startDayOfWeek === endDayOfWeek;
|
|
4405
|
+
const dayOfMonth = sameDayOfMonth ? startDayOfMonth : dayOfMonthRange;
|
|
4406
|
+
const dayOfWeek = sameDayOfWeek ? startDayOfWeek : dayOfWeekRange;
|
|
4407
|
+
const startOfMonth = start.clone().startOf("month");
|
|
4408
|
+
const weeks = Math.ceil(Math.abs(start.diff(startOfMonth, "weeks", true)));
|
|
4409
|
+
switch (this.repeat) {
|
|
4410
|
+
case "NONE": return `0 0`;
|
|
4411
|
+
case "DAILY": return `* *`;
|
|
4412
|
+
case "WEEKDAYS": return `* 1-5`;
|
|
4413
|
+
case "WEEKENDS": return `* 6,0`;
|
|
4414
|
+
case "WEEKLY": return `* ${dayOfWeek}`;
|
|
4415
|
+
case "FORTNIGHTLY": return `F ${dayOfWeek}`;
|
|
4416
|
+
case "MONTHLY": return `${dayOfMonth} *`;
|
|
4417
|
+
case "LAST_DAY_OF_MONTH": return `L *`;
|
|
4418
|
+
case "WEEK_OF_MONTH": return `${weeks}W ${dayOfWeek}`;
|
|
4419
|
+
default: throw new Error(`Unsupported repeat value: ${this.repeat}`);
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
getDescription(cron) {
|
|
4423
|
+
const start = this.day(this.startDate);
|
|
4424
|
+
const end = this.day(this.endDate);
|
|
4425
|
+
const isSameDay = start.isSame(end, "day");
|
|
4426
|
+
const fromDate = start.format("MMMM Do");
|
|
4427
|
+
const toDate = end.format("MMMM Do");
|
|
4428
|
+
const fromTime = start.format("HH:mm");
|
|
4429
|
+
const toTime = end.format("HH:mm");
|
|
4430
|
+
const fromDayOfMonth = start.format("Do");
|
|
4431
|
+
const toDayOfMonth = end.format("Do");
|
|
4432
|
+
const isStartOfDay = start.isSame(start.clone().startOf("day"), "minutes");
|
|
4433
|
+
const isEndOfDay = end.isSame(end.clone().endOf("day"), "minutes");
|
|
4434
|
+
const isFullDay = isStartOfDay && isEndOfDay && isSameDay;
|
|
4435
|
+
assert(cron, "Invalid recurring date cron expression");
|
|
4436
|
+
const segments = cron.split(" ");
|
|
4437
|
+
const first = segments[0];
|
|
4438
|
+
const second = segments[1];
|
|
4439
|
+
assert(first && second, "Invalid recurring date cron expression");
|
|
4440
|
+
const compoundFromToDate = `from ${fromDate} at ${fromTime} to ${toDate} at ${toTime}`;
|
|
4441
|
+
const singleFromToDate = `on ${fromDate} from ${fromTime} to ${toTime}`;
|
|
4442
|
+
const fullDayFromToDay = `on ${fromDate}`;
|
|
4443
|
+
let fromToDate = isSameDay ? singleFromToDate : compoundFromToDate;
|
|
4444
|
+
if (isFullDay) fromToDate = fullDayFromToDay;
|
|
4445
|
+
const fromToTime = isFullDay ? "" : ` from ${fromTime} to ${toTime}`;
|
|
4446
|
+
const fromDayOfWeek = start.format("dddd");
|
|
4447
|
+
const fromToDayOfWeek = `${fromDayOfWeek}s to ${end.format("dddd")}s`;
|
|
4448
|
+
const dayOfWeek = isSameDay ? `on ${fromDayOfWeek}s` : fromToDayOfWeek;
|
|
4449
|
+
const singleDayOfMonth = `on the ${fromDayOfMonth}`;
|
|
4450
|
+
const fromToDayOfMonth = `on the ${fromDayOfMonth} to ${toDayOfMonth}`;
|
|
4451
|
+
const dayOfMonth = isSameDay ? singleDayOfMonth : fromToDayOfMonth;
|
|
4452
|
+
const weeks = Number(first.slice(0, -1));
|
|
4453
|
+
let weekOfMonth = "1st";
|
|
4454
|
+
if (weeks === 2) weekOfMonth = "2nd";
|
|
4455
|
+
if (weeks === 3) weekOfMonth = "3rd";
|
|
4456
|
+
if (weeks === 4) weekOfMonth = "4th";
|
|
4457
|
+
if (weeks === 5) weekOfMonth = "5th";
|
|
4458
|
+
const startsOn = `, starting on ${start.format("MMMM Do")}`;
|
|
4459
|
+
switch (this.repeat) {
|
|
4460
|
+
case "NONE": return `Once ${fromToDate}`;
|
|
4461
|
+
case "DAILY": return `Daily${fromToTime}${startsOn}`;
|
|
4462
|
+
case "WEEKDAYS": return `On weekdays${fromToTime}${startsOn}`;
|
|
4463
|
+
case "WEEKENDS": return `On weekends${fromToTime}${startsOn}`;
|
|
4464
|
+
case "WEEKLY": return `Weekly ${dayOfWeek}${fromToTime}${startsOn}`;
|
|
4465
|
+
case "FORTNIGHTLY": return `Fortnightly ${dayOfWeek}${fromToTime}${startsOn}`;
|
|
4466
|
+
case "MONTHLY": return `Monthly ${dayOfMonth}${fromToTime}${startsOn}`;
|
|
4467
|
+
case "LAST_DAY_OF_MONTH": return `Every Last day of the month${fromToTime}${startsOn}`;
|
|
4468
|
+
case "WEEK_OF_MONTH": return `Every ${weekOfMonth} ${fromDayOfWeek} of the month${fromToTime}${startsOn}`;
|
|
4469
|
+
default: throw new Error(`Unsupported repeat value: ${this.repeat}`);
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
get result() {
|
|
4473
|
+
const expression = this.getExpression();
|
|
4474
|
+
const description = this.getDescription(expression);
|
|
4475
|
+
return {
|
|
4476
|
+
startDate: this.startDate,
|
|
4477
|
+
endDate: this.endDate,
|
|
4478
|
+
repeat: this.repeat,
|
|
4479
|
+
expression,
|
|
4480
|
+
description
|
|
4481
|
+
};
|
|
4482
|
+
}
|
|
4483
|
+
match = (date) => {
|
|
4484
|
+
const dateObj = this.day(date);
|
|
4485
|
+
if (!dateObj.isValid()) return false;
|
|
4486
|
+
if (this.repeat === "NONE") return dateObj.isBetween(this.startDate, this.endDate, "minutes", "[]");
|
|
4487
|
+
const started = dateObj.isSameOrAfter(this.startDate, "day");
|
|
4488
|
+
const isWeekday = dateObj.day() >= 1 && dateObj.day() <= 5;
|
|
4489
|
+
const isWeekend = dateObj.day() === 0 || dateObj.day() === 6;
|
|
4490
|
+
const dayOfWeek = dateObj.day();
|
|
4491
|
+
const firstDayOfWeek = this.startDate.getDay();
|
|
4492
|
+
const lastDayOfWeek = this.endDate.getDay();
|
|
4493
|
+
const isBetweenWeekDays = dayOfWeek >= firstDayOfWeek && dayOfWeek <= lastDayOfWeek;
|
|
4494
|
+
const dayOfMonth = dateObj.date();
|
|
4495
|
+
const firstDayOfMonth = this.startDate.getDate();
|
|
4496
|
+
const lastDayOfMonth = this.endDate.getDate();
|
|
4497
|
+
const isBetweenDaysOfMonth = dayOfMonth >= firstDayOfMonth && dayOfMonth <= lastDayOfMonth;
|
|
4498
|
+
let start = this.day(this.startDate);
|
|
4499
|
+
start = start.year(dateObj.year());
|
|
4500
|
+
start = start.month(dateObj.month());
|
|
4501
|
+
start = start.date(dateObj.date());
|
|
4502
|
+
let end = this.day(this.endDate);
|
|
4503
|
+
end = end.year(dateObj.year());
|
|
4504
|
+
end = end.month(dateObj.month());
|
|
4505
|
+
end = end.date(dateObj.date());
|
|
4506
|
+
const betweenTime = dateObj.isBetween(start, end, "minutes", "[]");
|
|
4507
|
+
switch (this.repeat) {
|
|
4508
|
+
case "DAILY":
|
|
4509
|
+
if (!started) return false;
|
|
4510
|
+
return betweenTime;
|
|
4511
|
+
case "WEEKDAYS":
|
|
4512
|
+
if (!started || !isWeekday) return false;
|
|
4513
|
+
return betweenTime;
|
|
4514
|
+
case "WEEKENDS":
|
|
4515
|
+
if (!started || !isWeekend) return false;
|
|
4516
|
+
return betweenTime;
|
|
4517
|
+
case "WEEKLY":
|
|
4518
|
+
if (!started || !isBetweenWeekDays) return false;
|
|
4519
|
+
return betweenTime;
|
|
4520
|
+
case "FORTNIGHTLY": {
|
|
4521
|
+
if (!started || !isBetweenWeekDays) return false;
|
|
4522
|
+
const weekOfYear = dateObj.week();
|
|
4523
|
+
const conditionWeekOfYear = this.day(this.startDate).week();
|
|
4524
|
+
if (!(weekOfYear % 2 === conditionWeekOfYear % 2)) return false;
|
|
4525
|
+
return betweenTime;
|
|
4526
|
+
}
|
|
4527
|
+
case "MONTHLY":
|
|
4528
|
+
if (!started || !isBetweenDaysOfMonth) return false;
|
|
4529
|
+
return betweenTime;
|
|
4530
|
+
case "LAST_DAY_OF_MONTH":
|
|
4531
|
+
if (!started) return false;
|
|
4532
|
+
if (!(dayOfMonth === dateObj.endOf("month").date())) return false;
|
|
4533
|
+
return betweenTime;
|
|
4534
|
+
case "WEEK_OF_MONTH": {
|
|
4535
|
+
if (!started || !isBetweenWeekDays) return false;
|
|
4536
|
+
const firstDay = dateObj.clone().startOf("month");
|
|
4537
|
+
const startDate = this.day(this.startDate);
|
|
4538
|
+
const conditionFirstDay = startDate.clone().startOf("month");
|
|
4539
|
+
if (!(startDate.diff(conditionFirstDay, "weeks") + 1 === Math.abs(dateObj.diff(firstDay, "weeks") + 1))) return false;
|
|
4540
|
+
return betweenTime;
|
|
4541
|
+
}
|
|
4542
|
+
default: throw new Error(`Unsupported repeat value: ${this.repeat}`);
|
|
4543
|
+
}
|
|
4544
|
+
};
|
|
4545
|
+
};
|
|
4546
|
+
//#endregion
|
|
4547
|
+
//#region src/rules/recurringDateMatcher.ts
|
|
4548
|
+
const matchRecurringDateOperator = (condition, matcher) => {
|
|
4549
|
+
const { primitive, operator } = condition;
|
|
4550
|
+
assert(primitive === "recurring date", primitiveError("recurring date"));
|
|
4551
|
+
if (typeof matcher === "undefined") return false;
|
|
4552
|
+
switch (operator) {
|
|
4553
|
+
case "falls within period": return new RecurringDateCron(condition).match(matcher);
|
|
4554
|
+
default: {
|
|
4555
|
+
const msg = `Operator ${operator} not supported in recurring date matcher`;
|
|
4556
|
+
throw new Error(msg);
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
};
|
|
4560
|
+
const currentDate = (condition, referenceDate) => {
|
|
4561
|
+
const { type, primitive, value } = condition;
|
|
4562
|
+
const matcherType = "Current date";
|
|
4563
|
+
assert(primitive === "recurring date", primitiveError("recurring date"));
|
|
4564
|
+
assert(type === matcherType, typeError(matcherType));
|
|
4565
|
+
assert(value != null, "Invalid value value");
|
|
4566
|
+
const current = referenceDate ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4567
|
+
return matchRecurringDateOperator(condition, current);
|
|
4568
|
+
};
|
|
4569
|
+
//#endregion
|
|
4570
|
+
//#region src/rules/RulesEngine.ts
|
|
4571
|
+
var RulesEngine = class {
|
|
4572
|
+
order;
|
|
4573
|
+
customer;
|
|
4574
|
+
payment;
|
|
4575
|
+
monitoring;
|
|
4576
|
+
referenceDate;
|
|
4577
|
+
runtime;
|
|
4578
|
+
constructor(params = {}) {
|
|
4579
|
+
const { order, customer, payment, monitoring, referenceDate } = params;
|
|
4580
|
+
const { runtime } = params;
|
|
4581
|
+
this.order = order;
|
|
4582
|
+
this.customer = customer;
|
|
4583
|
+
this.payment = payment;
|
|
4584
|
+
this.monitoring = monitoring;
|
|
4585
|
+
this.referenceDate = referenceDate;
|
|
4586
|
+
this.runtime = runtime;
|
|
4587
|
+
}
|
|
4588
|
+
getDependency(prop, type) {
|
|
4589
|
+
const value = this[prop];
|
|
4590
|
+
const errorMsg = `You cannot use "${type}" condition without sending ${String(prop)}`;
|
|
4591
|
+
assert(value !== void 0, errorMsg);
|
|
4592
|
+
return value;
|
|
4593
|
+
}
|
|
4594
|
+
selectError = (prev, next) => {
|
|
4595
|
+
if (!prev) return next;
|
|
4596
|
+
return next === "Condition was not met" ? prev : next;
|
|
4597
|
+
};
|
|
4598
|
+
getProductId = (condition) => {
|
|
4599
|
+
if (!("product_id" in condition) || !condition.product_id) throw new Error("Invalid product_id");
|
|
4600
|
+
return condition.product_id;
|
|
4601
|
+
};
|
|
4602
|
+
getProductTag = (condition) => {
|
|
4603
|
+
if (!("tag" in condition) || !condition.tag) throw new Error("Invalid tag");
|
|
4604
|
+
return condition.tag;
|
|
4605
|
+
};
|
|
4606
|
+
matchOrderProductTag = (condition, match) => {
|
|
4607
|
+
const order = this.getDependency("order", condition.type);
|
|
4608
|
+
const tag = this.getProductTag(condition);
|
|
4609
|
+
const values = getLineTagValues(order.lines, tag);
|
|
4610
|
+
for (const value of values) if (match(value)) return true;
|
|
4611
|
+
return false;
|
|
4612
|
+
};
|
|
4613
|
+
assertString = (condition) => {
|
|
4614
|
+
const { primitive, type } = condition;
|
|
4615
|
+
assert(primitive === "string", primitiveError("string"));
|
|
4616
|
+
if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, (value) => {
|
|
4617
|
+
if (typeof value !== "string") return false;
|
|
4618
|
+
return orderIncludesProductWithTag(condition, value);
|
|
4619
|
+
});
|
|
4620
|
+
if (type === "Request path") {
|
|
4621
|
+
const monitoring = this.getDependency("monitoring", type);
|
|
4622
|
+
return requestPath(condition, monitoring.path);
|
|
4623
|
+
}
|
|
4624
|
+
if (type === "Failure message") {
|
|
4625
|
+
const monitoring = this.getDependency("monitoring", type);
|
|
4626
|
+
return failureMessage(condition, monitoring.message);
|
|
4627
|
+
}
|
|
4628
|
+
throw new Error(`Condition ${type} not supported in string matcher`);
|
|
4629
|
+
};
|
|
4630
|
+
assertNumber = (condition) => {
|
|
4631
|
+
const { primitive, type } = condition;
|
|
4632
|
+
assert(primitive === "number", primitiveError("number"));
|
|
4633
|
+
if (type === "Cart product count") {
|
|
4634
|
+
const order = this.getDependency("order", type);
|
|
4635
|
+
const include = "include_modifiers" in condition && Boolean(condition.include_modifiers);
|
|
4636
|
+
const count = countCartProducts(order.lines, include);
|
|
4637
|
+
return cartProductCount(condition, count);
|
|
4638
|
+
}
|
|
4639
|
+
if (type === "Order includes product") {
|
|
4640
|
+
const order = this.getDependency("order", type);
|
|
4641
|
+
const productId = this.getProductId(condition);
|
|
4642
|
+
const line = getLineByProductId(order.lines, productId);
|
|
4643
|
+
return orderIncludesProduct(condition, line?.quantity);
|
|
4644
|
+
}
|
|
4645
|
+
if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, (value) => {
|
|
4646
|
+
if (typeof value !== "number") return false;
|
|
4647
|
+
return orderIncludesProductWithTag$1(condition, value);
|
|
4648
|
+
});
|
|
4649
|
+
if (type === "Accumulated events") {
|
|
4650
|
+
const monitoring = this.getDependency("monitoring", type);
|
|
4651
|
+
const window = windowMinutes(condition);
|
|
4652
|
+
if (window == null) return false;
|
|
4653
|
+
const count = monitoring.event_counts[window];
|
|
4654
|
+
return accumulatedEvents(condition, count);
|
|
4655
|
+
}
|
|
4656
|
+
if (type === "API error rate") {
|
|
4657
|
+
const monitoring = this.getDependency("monitoring", type);
|
|
4658
|
+
const window = windowMinutes(condition);
|
|
4659
|
+
if (window == null) return false;
|
|
4660
|
+
const rate = monitoring.error_rates[window];
|
|
4661
|
+
return apiErrorRate(condition, rate);
|
|
4662
|
+
}
|
|
4663
|
+
if (type === "HTTP status") {
|
|
4664
|
+
const monitoring = this.getDependency("monitoring", type);
|
|
4665
|
+
return httpStatus(condition, monitoring.http_status);
|
|
4666
|
+
}
|
|
4667
|
+
throw new Error(`Condition ${type} not supported in number matcher`);
|
|
4668
|
+
};
|
|
4669
|
+
assertBoolean = (condition) => {
|
|
4670
|
+
const { primitive, type } = condition;
|
|
4671
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
4672
|
+
if (type === "Is anonymous order") {
|
|
4673
|
+
const order = this.getDependency("order", type);
|
|
4674
|
+
return isAnonymousOrder(condition, order.is_anonymous);
|
|
4675
|
+
}
|
|
4676
|
+
if (type === "Customer is anonymous") {
|
|
4677
|
+
const isAnonymous = this.getDependency("customer", type).is_anonymous;
|
|
4678
|
+
return isAnonymousCustomer(condition, isAnonymous);
|
|
4679
|
+
}
|
|
4680
|
+
if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, (value) => {
|
|
4681
|
+
if (typeof value !== "boolean") return false;
|
|
4682
|
+
return orderIncludesProductWithTag$3(condition, value);
|
|
4683
|
+
});
|
|
4684
|
+
throw new Error(`Condition ${type} not supported in boolean matcher`);
|
|
4685
|
+
};
|
|
4686
|
+
assertCurrency = (condition) => {
|
|
4687
|
+
const { primitive, type } = condition;
|
|
4688
|
+
assert(primitive === "currency", primitiveError("currency"));
|
|
4689
|
+
if (type === "Order total") {
|
|
4690
|
+
const order = this.getDependency("order", type);
|
|
4691
|
+
return orderTotal(condition, order.total);
|
|
4692
|
+
}
|
|
4693
|
+
if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, (value) => {
|
|
4694
|
+
if (typeof value !== "number") return false;
|
|
4695
|
+
return orderIncludesProductWithTag$2(condition, value);
|
|
4696
|
+
});
|
|
4697
|
+
throw new Error(`Condition ${type} not supported in currency matcher`);
|
|
4698
|
+
};
|
|
4699
|
+
assertEnum = (condition) => {
|
|
4700
|
+
const { primitive, type } = condition;
|
|
4701
|
+
assert(primitive === "enum", primitiveError("enum"));
|
|
4702
|
+
const payment = this.getDependency("payment", type);
|
|
4703
|
+
switch (type) {
|
|
4704
|
+
case "Payment collect at": return paymentCollectAt(condition, payment.collect_at);
|
|
4705
|
+
case "Payment method": return paymentMethod(condition, payment.method);
|
|
4706
|
+
case "Payment status": return paymentStatus(condition, payment.status);
|
|
4707
|
+
default: throw new Error(`Condition ${type} not supported in enum matcher`);
|
|
4708
|
+
}
|
|
4709
|
+
};
|
|
4710
|
+
assertAbsoluteDate = (condition) => {
|
|
4711
|
+
const { primitive, type } = condition;
|
|
4712
|
+
assert(primitive === "absolute date", primitiveError("absolute date"));
|
|
4713
|
+
const msg = `Condition ${type} not supported in absolute date matcher`;
|
|
4714
|
+
throw new Error(msg);
|
|
4715
|
+
};
|
|
4716
|
+
assertRelativeDate = (condition) => {
|
|
4717
|
+
const { primitive, type } = condition;
|
|
4718
|
+
assert(primitive === "relative date", primitiveError("relative date"));
|
|
4719
|
+
const msg = `Condition ${type} not supported in relative date matcher`;
|
|
4720
|
+
throw new Error(msg);
|
|
4721
|
+
};
|
|
4722
|
+
assertRecurringDate = (condition) => {
|
|
4723
|
+
const { primitive, type } = condition;
|
|
4724
|
+
assert(primitive === "recurring date", primitiveError("recurring date"));
|
|
4725
|
+
if (type === "Current date") return currentDate(condition, this.referenceDate);
|
|
4726
|
+
const msg = `Condition ${type} not supported in recurring date matcher`;
|
|
4727
|
+
throw new Error(msg);
|
|
4728
|
+
};
|
|
4729
|
+
assertCustom = (condition) => {
|
|
4730
|
+
const order = this.getDependency("order", condition.type);
|
|
4731
|
+
return assertCustom(condition, order);
|
|
4732
|
+
};
|
|
4733
|
+
assertCondition = (condition) => {
|
|
4734
|
+
const { primitive } = condition;
|
|
4735
|
+
switch (primitive) {
|
|
4736
|
+
case "string": return this.assertString(condition);
|
|
4737
|
+
case "number": return this.assertNumber(condition);
|
|
4738
|
+
case "boolean": return this.assertBoolean(condition);
|
|
4739
|
+
case "currency": return this.assertCurrency(condition);
|
|
4740
|
+
case "enum": return this.assertEnum(condition);
|
|
4741
|
+
case "absolute date": return this.assertAbsoluteDate(condition);
|
|
4742
|
+
case "relative date": return this.assertRelativeDate(condition);
|
|
4743
|
+
case "recurring date": return this.assertRecurringDate(condition);
|
|
4744
|
+
case "custom": return this.assertCustom(condition);
|
|
4745
|
+
default: throw new Error(`Unsupported primitive: ${primitive}`);
|
|
4746
|
+
}
|
|
4747
|
+
};
|
|
4748
|
+
matchCondition = (condition) => {
|
|
4749
|
+
try {
|
|
4750
|
+
if (this.assertCondition(condition)) return {
|
|
4751
|
+
match: condition,
|
|
4752
|
+
error: null
|
|
4753
|
+
};
|
|
4754
|
+
return {
|
|
4755
|
+
match: null,
|
|
4756
|
+
error: condition.invalidMessage ?? "Condition was not met"
|
|
4757
|
+
};
|
|
4758
|
+
} catch (error) {
|
|
4759
|
+
return {
|
|
4760
|
+
match: null,
|
|
4761
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4762
|
+
};
|
|
4763
|
+
}
|
|
4764
|
+
};
|
|
4765
|
+
matchAndConditions = (andConditions) => {
|
|
4766
|
+
let error = null;
|
|
4767
|
+
for (const condition of andConditions) {
|
|
4768
|
+
const interp = condition.interpretation;
|
|
4769
|
+
if (!isInterpretationCompatible(interp, this.runtime, "server")) return {
|
|
4770
|
+
match: null,
|
|
4771
|
+
error: defaultConditionError
|
|
4772
|
+
};
|
|
4773
|
+
const result = this.matchCondition(condition);
|
|
4774
|
+
if (!result.match) {
|
|
4775
|
+
error = this.selectError(error, result.error);
|
|
4776
|
+
return {
|
|
4777
|
+
match: null,
|
|
4778
|
+
error: error ?? "Condition was not met"
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
return {
|
|
4783
|
+
match: andConditions,
|
|
4784
|
+
error: null
|
|
4785
|
+
};
|
|
4786
|
+
};
|
|
4787
|
+
matchOrConditions = (orConditions) => {
|
|
4788
|
+
let error = null;
|
|
4789
|
+
for (const andConditions of orConditions) {
|
|
4790
|
+
const result = this.matchAndConditions(andConditions);
|
|
4791
|
+
if (result.match) return {
|
|
4792
|
+
match: result.match,
|
|
4793
|
+
error: null
|
|
4794
|
+
};
|
|
4795
|
+
error = this.selectError(error, result.error);
|
|
4796
|
+
}
|
|
4797
|
+
return {
|
|
4798
|
+
match: null,
|
|
4799
|
+
error: error ?? "Condition was not met"
|
|
4800
|
+
};
|
|
4801
|
+
};
|
|
4802
|
+
matchRules = (rules, options) => {
|
|
4803
|
+
const exclusive = options?.exclusive ?? false;
|
|
4804
|
+
const matched = [];
|
|
4805
|
+
for (const rule of rules) {
|
|
4806
|
+
if (!isInterpretationCompatible(rule.interpretation, this.runtime, "both")) continue;
|
|
4807
|
+
const conditions = rule.conditions ?? [];
|
|
4808
|
+
if (conditions.length === 0) {
|
|
4809
|
+
matched.push(rule);
|
|
4810
|
+
if (exclusive) return matched;
|
|
4811
|
+
continue;
|
|
4812
|
+
}
|
|
4813
|
+
if (!this.matchOrConditions(conditions).match) continue;
|
|
4814
|
+
matched.push(rule);
|
|
4815
|
+
if (exclusive) return matched;
|
|
4816
|
+
}
|
|
4817
|
+
return matched;
|
|
4818
|
+
};
|
|
4819
|
+
};
|
|
4820
|
+
//#endregion
|
|
4821
|
+
//#region src/rules/absoluteDateMatcher.ts
|
|
4822
|
+
const matchAbsoluteDateOperator = (condition, matcher) => {
|
|
4823
|
+
const { primitive, operator, value } = condition;
|
|
4824
|
+
assert(primitive === "absolute date", primitiveError("absolute date"));
|
|
4825
|
+
if (typeof matcher === "undefined") return false;
|
|
4826
|
+
const matcherDate = (0, import_dayjs_min.default)(matcher);
|
|
4827
|
+
const expectedDate = (0, import_dayjs_min.default)(value);
|
|
4828
|
+
switch (operator) {
|
|
4829
|
+
case "is": return matcherDate.isSame(expectedDate, "day");
|
|
4830
|
+
case "is not": return !matcherDate.isSame(expectedDate, "day");
|
|
4831
|
+
case "is before": return matcherDate.isBefore(expectedDate, "day");
|
|
4832
|
+
case "is after": return matcherDate.isAfter(expectedDate, "day");
|
|
4833
|
+
default: {
|
|
4834
|
+
const msg = `Operator ${operator} not supported in absolute date matcher`;
|
|
4835
|
+
throw new Error(msg);
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
};
|
|
4839
|
+
//#endregion
|
|
4840
|
+
//#region src/rules/relativeDateMatcher.ts
|
|
4841
|
+
const matchRelativeDateOperator = (condition, matcher, referenceDate) => {
|
|
4842
|
+
const { primitive, operator, value } = condition;
|
|
4843
|
+
assert(primitive === "relative date", primitiveError("relative date"));
|
|
4844
|
+
if (typeof matcher === "undefined") return false;
|
|
4845
|
+
assert(typeof value === "string", "Invalid relative date value");
|
|
4846
|
+
const [amountString, unitRaw] = value.split("/");
|
|
4847
|
+
assert(amountString && unitRaw, "Amount & Unit required");
|
|
4848
|
+
const amount = parseInt(amountString, 10);
|
|
4849
|
+
const unit = unitRaw;
|
|
4850
|
+
const issueDate = (0, import_dayjs_min.default)(referenceDate ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
4851
|
+
const matcherDate = (0, import_dayjs_min.default)(matcher);
|
|
4852
|
+
switch (operator) {
|
|
4853
|
+
case "occurred": {
|
|
4854
|
+
const endDate = issueDate.subtract(amount, unit).endOf(unit);
|
|
4855
|
+
const startDate = endDate.startOf(unit);
|
|
4856
|
+
const sameStart = matcherDate.isSame(startDate, unit);
|
|
4857
|
+
const afterStart = matcherDate.isAfter(startDate);
|
|
4858
|
+
const sameEnd = matcherDate.isSame(endDate, unit);
|
|
4859
|
+
const beforeEnd = matcherDate.isBefore(endDate);
|
|
4860
|
+
return (sameStart || afterStart) && (sameEnd || beforeEnd);
|
|
4861
|
+
}
|
|
4862
|
+
case "occurred in the last": {
|
|
4863
|
+
const targetDate = issueDate.subtract(amount, unit);
|
|
4864
|
+
const sameTarget = matcherDate.isSame(targetDate, unit);
|
|
4865
|
+
const afterTarget = matcherDate.isAfter(targetDate);
|
|
4866
|
+
const sameIssue = matcherDate.isSame(issueDate, unit);
|
|
4867
|
+
const beforeIssue = matcherDate.isBefore(issueDate);
|
|
4868
|
+
return (sameTarget || afterTarget) && (sameIssue || beforeIssue);
|
|
4869
|
+
}
|
|
4870
|
+
case "occurred more than": {
|
|
4871
|
+
const targetDate = issueDate.subtract(amount, unit);
|
|
4872
|
+
return matcherDate.isBefore(targetDate, unit);
|
|
4873
|
+
}
|
|
4874
|
+
default: {
|
|
4875
|
+
const msg = `Operator ${operator} not supported in relative date matcher`;
|
|
4876
|
+
throw new Error(msg);
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
};
|
|
4880
|
+
//#endregion
|
|
4881
|
+
//#region src/rules/checkout.ts
|
|
4882
|
+
const DEFAULT_SHIPPING_FIELDS = {
|
|
4883
|
+
address_line1: "required",
|
|
4884
|
+
address_line2: "optional",
|
|
4885
|
+
city: "optional",
|
|
4886
|
+
state: "optional",
|
|
4887
|
+
zip: "optional",
|
|
4888
|
+
country: "optional",
|
|
4889
|
+
reference: "optional",
|
|
4890
|
+
instructions: "optional"
|
|
4891
|
+
};
|
|
4892
|
+
const DEFAULT_BILLING_FIELDS = {
|
|
4893
|
+
type: "required",
|
|
4894
|
+
legal_name: "required",
|
|
4895
|
+
address_line1: "required",
|
|
4896
|
+
address_line2: "optional",
|
|
4897
|
+
city: "optional",
|
|
4898
|
+
state: "optional",
|
|
4899
|
+
zip: "optional",
|
|
4900
|
+
country: "optional",
|
|
4901
|
+
tax_id: "required",
|
|
4902
|
+
tax_id_type: "required"
|
|
4903
|
+
};
|
|
4904
|
+
const getCheckoutFieldValue = (info, key) => {
|
|
4905
|
+
if (!info) return void 0;
|
|
4906
|
+
const top = info[key];
|
|
4907
|
+
if (top != null && top !== "") return top;
|
|
4908
|
+
const metadata = info.metadata;
|
|
4909
|
+
if (!metadata || typeof metadata !== "object") return void 0;
|
|
4910
|
+
if (Array.isArray(metadata)) return void 0;
|
|
4911
|
+
return metadata[key];
|
|
4912
|
+
};
|
|
4913
|
+
const isCheckoutFieldFilled = (info, key) => {
|
|
4914
|
+
const value = getCheckoutFieldValue(info, key);
|
|
4915
|
+
if (value == null) return false;
|
|
4916
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
4917
|
+
return true;
|
|
4918
|
+
};
|
|
4919
|
+
const fallbackRequired = {
|
|
4920
|
+
fallback: true,
|
|
4921
|
+
interpretation: "both",
|
|
4922
|
+
effect: { mode: "required" },
|
|
4923
|
+
conditions: []
|
|
4924
|
+
};
|
|
4925
|
+
const defaultCheckoutPreferences = {
|
|
4926
|
+
shipping: {
|
|
4927
|
+
fields: DEFAULT_SHIPPING_FIELDS,
|
|
4928
|
+
rules: [fallbackRequired]
|
|
4929
|
+
},
|
|
4930
|
+
billing: {
|
|
4931
|
+
fields: DEFAULT_BILLING_FIELDS,
|
|
4932
|
+
rules: [fallbackRequired]
|
|
4933
|
+
}
|
|
4934
|
+
};
|
|
4935
|
+
const factsFromOrder = (order) => {
|
|
4936
|
+
const brand_id = order.brand_id ?? void 0;
|
|
4937
|
+
const b1 = {
|
|
4938
|
+
total: order.totals?.total ?? 0,
|
|
4939
|
+
is_anonymous: order.is_anonymous ?? false,
|
|
4940
|
+
brand_id
|
|
4941
|
+
};
|
|
4942
|
+
const b2 = {
|
|
4943
|
+
store_id: order.store_id,
|
|
4944
|
+
channel_id: order.channel_id
|
|
4945
|
+
};
|
|
4946
|
+
const b3 = {
|
|
4947
|
+
fulfillment_id: order.fulfillment_id,
|
|
4948
|
+
lines: order.lines
|
|
4949
|
+
};
|
|
4950
|
+
return {
|
|
4951
|
+
...b1,
|
|
4952
|
+
...b2,
|
|
4953
|
+
...b3
|
|
4954
|
+
};
|
|
4955
|
+
};
|
|
4956
|
+
const checkoutFromMeta = (order) => {
|
|
4957
|
+
const raw = order.metadata?.checkout;
|
|
4958
|
+
if (!raw || typeof raw !== "object") return null;
|
|
4959
|
+
const checkout = raw;
|
|
4960
|
+
if (!checkout.shipping?.rules || !checkout.billing?.rules) return null;
|
|
4961
|
+
return checkout;
|
|
4962
|
+
};
|
|
4963
|
+
const resolveSection = (section, engine) => {
|
|
4964
|
+
const ranked = section.rules.filter((rule) => !rule.fallback);
|
|
4965
|
+
const matched = engine.matchRules(ranked, { exclusive: true });
|
|
4966
|
+
const fallback = section.rules.find((rule) => rule.fallback);
|
|
4967
|
+
return (matched[0] ?? fallback)?.effect.mode ?? "required";
|
|
4968
|
+
};
|
|
4969
|
+
/** Only these fulfillment types ship to the customer's address. */
|
|
4970
|
+
const SHIPPING_FULFILLMENT_TYPES = /* @__PURE__ */ new Set(["DELIVERY"]);
|
|
4971
|
+
const resolveCheckout = (order, options) => {
|
|
4972
|
+
const fromMeta = checkoutFromMeta(order);
|
|
4973
|
+
const checkout = options?.checkout ?? fromMeta ?? defaultCheckoutPreferences;
|
|
4974
|
+
const runtime = options?.runtime ?? "client";
|
|
4975
|
+
const referenceDate = options?.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4976
|
+
const facts = factsFromOrder(order);
|
|
4977
|
+
const engine = new RulesEngine({
|
|
4978
|
+
order: facts,
|
|
4979
|
+
customer: { is_anonymous: facts.is_anonymous },
|
|
4980
|
+
referenceDate,
|
|
4981
|
+
runtime
|
|
4982
|
+
});
|
|
4983
|
+
const type = options?.fulfillmentType;
|
|
4984
|
+
const ships = !type || SHIPPING_FULFILLMENT_TYPES.has(type);
|
|
4985
|
+
let shipping = "hidden";
|
|
4986
|
+
if (ships) shipping = resolveSection(checkout.shipping, engine);
|
|
4987
|
+
const billing = resolveSection(checkout.billing, engine);
|
|
4988
|
+
const shippingFields = checkout.shipping.fields ?? DEFAULT_SHIPPING_FIELDS;
|
|
4989
|
+
const billingFields = checkout.billing.fields ?? DEFAULT_BILLING_FIELDS;
|
|
4990
|
+
return {
|
|
4991
|
+
shipping,
|
|
4992
|
+
billing,
|
|
4993
|
+
shippingFields,
|
|
4994
|
+
billingFields
|
|
4995
|
+
};
|
|
4996
|
+
};
|
|
4997
|
+
//#endregion
|
|
4998
|
+
//#region src/rules/payments.ts
|
|
4999
|
+
const runsOnClient = (condition) => {
|
|
5000
|
+
const fallback = CONDITION_INTERPRETATION_DEFAULT;
|
|
5001
|
+
return isInterpretationCompatible(condition.interpretation, "client", fallback);
|
|
5002
|
+
};
|
|
5003
|
+
/**
|
|
5004
|
+
* Client pre-filter for a payment's conditions against a local order.
|
|
5005
|
+
* Server-only conditions count as met; place order checks them all.
|
|
5006
|
+
*/
|
|
5007
|
+
const paymentMatchesClientConditions = (conditions, order, options) => {
|
|
5008
|
+
const groups = (conditions ?? []).map((group) => group.filter(runsOnClient));
|
|
5009
|
+
if (groups.length === 0) return true;
|
|
5010
|
+
if (groups.some((group) => group.length === 0)) return true;
|
|
5011
|
+
const facts = factsFromOrder(order);
|
|
5012
|
+
const engine = new RulesEngine({
|
|
5013
|
+
order: facts,
|
|
5014
|
+
customer: { is_anonymous: facts.is_anonymous },
|
|
5015
|
+
referenceDate: options?.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5016
|
+
runtime: "client"
|
|
5017
|
+
});
|
|
5018
|
+
return Boolean(engine.matchOrConditions(groups).match);
|
|
5019
|
+
};
|
|
5020
|
+
//#endregion
|
|
5021
|
+
export { DEFAULT_RECOMMENDATION_TYPES, Order, OrderSyncPendingError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, collectOrderLineDiffs, countCartProducts, createSession, currentNodes, defaultConditionError, flattenMenuCategories, fromOrderLines, getScheduleWindow, indexMenu, isCheckoutFieldFilled, isDeviceNotLinked, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isStorefrontNotFound, listMenusStream, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, paymentMatchesClientConditions, recomputeTotals, resolveCheckout, services, storefrontJson, toOrderLines, validateOrderAgainstMenu, visitedStates };
|