@teleporthq/teleport-shared 0.43.66 → 0.43.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/__tests__/utils/discount-engine.ts +1548 -0
  2. package/__tests__/utils/product-options.ts +2458 -0
  3. package/__tests__/utils/subscription-access.ts +37 -0
  4. package/dist/cjs/index.d.ts +9 -1
  5. package/dist/cjs/index.d.ts.map +1 -1
  6. package/dist/cjs/index.js +17 -1
  7. package/dist/cjs/index.js.map +1 -1
  8. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  9. package/dist/cjs/utils/cart-currency.d.ts +27 -0
  10. package/dist/cjs/utils/cart-currency.d.ts.map +1 -0
  11. package/dist/cjs/utils/cart-currency.js +31 -0
  12. package/dist/cjs/utils/cart-currency.js.map +1 -0
  13. package/dist/cjs/utils/cart-line-pricing.d.ts +25 -0
  14. package/dist/cjs/utils/cart-line-pricing.d.ts.map +1 -0
  15. package/dist/cjs/utils/cart-line-pricing.js +29 -0
  16. package/dist/cjs/utils/cart-line-pricing.js.map +1 -0
  17. package/dist/cjs/utils/discount-engine.d.ts +74 -0
  18. package/dist/cjs/utils/discount-engine.d.ts.map +1 -0
  19. package/dist/cjs/utils/discount-engine.js +435 -0
  20. package/dist/cjs/utils/discount-engine.js.map +1 -0
  21. package/dist/cjs/utils/product-options.d.ts +25 -0
  22. package/dist/cjs/utils/product-options.d.ts.map +1 -0
  23. package/dist/cjs/utils/product-options.js +1155 -0
  24. package/dist/cjs/utils/product-options.js.map +1 -0
  25. package/dist/cjs/utils/session-cookie-resolver.d.ts +85 -0
  26. package/dist/cjs/utils/session-cookie-resolver.d.ts.map +1 -0
  27. package/dist/cjs/utils/session-cookie-resolver.js +108 -0
  28. package/dist/cjs/utils/session-cookie-resolver.js.map +1 -0
  29. package/dist/cjs/utils/string-utils.d.ts +8 -0
  30. package/dist/cjs/utils/string-utils.d.ts.map +1 -1
  31. package/dist/cjs/utils/string-utils.js +14 -1
  32. package/dist/cjs/utils/string-utils.js.map +1 -1
  33. package/dist/cjs/utils/subscription-access.d.ts +36 -0
  34. package/dist/cjs/utils/subscription-access.d.ts.map +1 -0
  35. package/dist/cjs/utils/subscription-access.js +61 -0
  36. package/dist/cjs/utils/subscription-access.js.map +1 -0
  37. package/dist/cjs/utils/table-access.d.ts +114 -0
  38. package/dist/cjs/utils/table-access.d.ts.map +1 -0
  39. package/dist/cjs/utils/table-access.js +238 -0
  40. package/dist/cjs/utils/table-access.js.map +1 -0
  41. package/dist/cjs/utils/web-push.d.ts +43 -0
  42. package/dist/cjs/utils/web-push.d.ts.map +1 -0
  43. package/dist/cjs/utils/web-push.js +62 -0
  44. package/dist/cjs/utils/web-push.js.map +1 -0
  45. package/dist/esm/index.d.ts +9 -1
  46. package/dist/esm/index.d.ts.map +1 -1
  47. package/dist/esm/index.js +9 -1
  48. package/dist/esm/index.js.map +1 -1
  49. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  50. package/dist/esm/utils/cart-currency.d.ts +27 -0
  51. package/dist/esm/utils/cart-currency.d.ts.map +1 -0
  52. package/dist/esm/utils/cart-currency.js +27 -0
  53. package/dist/esm/utils/cart-currency.js.map +1 -0
  54. package/dist/esm/utils/cart-line-pricing.d.ts +25 -0
  55. package/dist/esm/utils/cart-line-pricing.d.ts.map +1 -0
  56. package/dist/esm/utils/cart-line-pricing.js +25 -0
  57. package/dist/esm/utils/cart-line-pricing.js.map +1 -0
  58. package/dist/esm/utils/discount-engine.d.ts +74 -0
  59. package/dist/esm/utils/discount-engine.d.ts.map +1 -0
  60. package/dist/esm/utils/discount-engine.js +431 -0
  61. package/dist/esm/utils/discount-engine.js.map +1 -0
  62. package/dist/esm/utils/product-options.d.ts +25 -0
  63. package/dist/esm/utils/product-options.d.ts.map +1 -0
  64. package/dist/esm/utils/product-options.js +1151 -0
  65. package/dist/esm/utils/product-options.js.map +1 -0
  66. package/dist/esm/utils/session-cookie-resolver.d.ts +85 -0
  67. package/dist/esm/utils/session-cookie-resolver.d.ts.map +1 -0
  68. package/dist/esm/utils/session-cookie-resolver.js +103 -0
  69. package/dist/esm/utils/session-cookie-resolver.js.map +1 -0
  70. package/dist/esm/utils/string-utils.d.ts +8 -0
  71. package/dist/esm/utils/string-utils.d.ts.map +1 -1
  72. package/dist/esm/utils/string-utils.js +12 -0
  73. package/dist/esm/utils/string-utils.js.map +1 -1
  74. package/dist/esm/utils/subscription-access.d.ts +36 -0
  75. package/dist/esm/utils/subscription-access.d.ts.map +1 -0
  76. package/dist/esm/utils/subscription-access.js +57 -0
  77. package/dist/esm/utils/subscription-access.js.map +1 -0
  78. package/dist/esm/utils/table-access.d.ts +114 -0
  79. package/dist/esm/utils/table-access.d.ts.map +1 -0
  80. package/dist/esm/utils/table-access.js +233 -0
  81. package/dist/esm/utils/table-access.js.map +1 -0
  82. package/dist/esm/utils/web-push.d.ts +43 -0
  83. package/dist/esm/utils/web-push.d.ts.map +1 -0
  84. package/dist/esm/utils/web-push.js +59 -0
  85. package/dist/esm/utils/web-push.js.map +1 -0
  86. package/package.json +3 -3
  87. package/src/index.ts +16 -0
  88. package/src/utils/cart-currency.ts +76 -0
  89. package/src/utils/cart-line-pricing.ts +151 -0
  90. package/src/utils/discount-engine.ts +437 -0
  91. package/src/utils/product-options.ts +1150 -0
  92. package/src/utils/session-cookie-resolver.ts +305 -0
  93. package/src/utils/string-utils.ts +12 -0
  94. package/src/utils/subscription-access.ts +59 -0
  95. package/src/utils/table-access.ts +516 -0
  96. package/src/utils/web-push.ts +61 -0
@@ -0,0 +1,1548 @@
1
+ import { generateDiscountEngineHelperCode } from '../../src/utils/discount-engine'
2
+
3
+ /**
4
+ * The discount engine — the ES5 copy the published storefront runs.
5
+ *
6
+ * ⚠️ The fixture table below is SHARED with the editor/canvas copy in
7
+ * teleport-gui (`features/e-commerce/utils/__tests__/discount-engine-parity.spec.ts`).
8
+ * Two specs, one table: that is what proves the canvas, the checkout
9
+ * workflows and the deployed store price the same discounts for the same
10
+ * basket. Edit both tables or neither.
11
+ */
12
+
13
+ interface DiscountEngineApi {
14
+ normalizeRule: (row: Record<string, unknown>, source: string) => Record<string, unknown>
15
+ linesFromCart: (items: unknown) => unknown[]
16
+ resolve: (input: Record<string, unknown>) => Record<string, unknown>
17
+ tender: (input: Record<string, unknown>) => Record<string, unknown>
18
+ }
19
+
20
+ const loadApi = (): DiscountEngineApi =>
21
+ new Function(
22
+ generateDiscountEngineHelperCode() +
23
+ '\nreturn { normalizeRule: __deNormalizeRule, linesFromCart: __deLinesFromCart,' +
24
+ ' resolve: __deResolve, tender: __deTender };'
25
+ )() as DiscountEngineApi
26
+
27
+ // ── SHARED PARITY TABLE ─────────────────────────────────────────────────────
28
+ // Byte-identical in teleport-gui `discount-engine-parity.spec.ts` and
29
+ // teleport-code-generators `teleport-shared/__tests__/utils/discount-engine.ts`.
30
+ // Rows are what `/api/data/.../select` returns: strings for DECIMAL columns,
31
+ // driver booleans ('t'/'f'), nullable text. Cart items are `cart-get-items`
32
+ // lines (NET prices). Edit both tables or neither.
33
+ interface DiscountEngineFixture {
34
+ name: string
35
+ rules: Array<Record<string, unknown>>
36
+ voucher: Record<string, unknown> | null
37
+ cartItems: Array<Record<string, unknown>>
38
+ taxRatePercent: number
39
+ shippingPrice: number
40
+ customer: { isFirstOrder: boolean | null }
41
+ legacy: boolean
42
+ /** The set the shopper picked, when the table states one. */
43
+ choice?: string
44
+ expected: {
45
+ goodsDiscount: number
46
+ shippingDiscount: number
47
+ automaticGoodsDiscount: number
48
+ voucherGoodsDiscount: number
49
+ voucherApplied: boolean
50
+ voucherRejection: string
51
+ appliedIds: string[]
52
+ eligibleProductIds: string[]
53
+ options: Array<{ key: string; label: string; saving: number; selected: boolean }>
54
+ choice: string
55
+ }
56
+ }
57
+
58
+ interface GiftCardTenderFixture {
59
+ name: string
60
+ input: {
61
+ balance: number
62
+ total: number
63
+ currency: string
64
+ hasGiftCardLines: boolean
65
+ hasRecurringLines?: boolean
66
+ recurringTenderAllowed?: boolean
67
+ hasRecurringTrial?: boolean
68
+ }
69
+ expected: { applied: number; amountDue: number }
70
+ }
71
+
72
+ const PARITY_CART = [
73
+ { productId: 'p1', price: 100, quantity: 2, categoryIds: ['c1', 'root'] },
74
+ { productId: 'p2', price: 50, quantity: 1, categoryIds: ['c2', 'root'] },
75
+ ]
76
+
77
+ const PARITY_CART_WITH_GIFT_CARD = [
78
+ ...PARITY_CART,
79
+ { productId: 'g1', price: 40, quantity: 1, categoryIds: [], isGiftCard: 't' },
80
+ ]
81
+
82
+ const PARITY_CART_WITH_SUBSCRIPTION = [
83
+ ...PARITY_CART,
84
+ { productId: 's1', price: 30, quantity: 1, categoryIds: ['root'], isRecurring: 'true' },
85
+ ]
86
+
87
+ const NO_CUSTOMER = { isFirstOrder: null }
88
+
89
+ const AUTO_TEN_PERCENT = {
90
+ id: 'a-ten',
91
+ name: 'Ten percent',
92
+ discount_type: 'percentage',
93
+ discount_value: '10.00',
94
+ applies_to_all_products: 't',
95
+ priority: 1,
96
+ stackable: 't',
97
+ }
98
+
99
+ const AUTO_FIFTY_FIXED = {
100
+ id: 'a-fifty',
101
+ name: 'Fifty off',
102
+ discount_type: 'fixed',
103
+ discount_value: '50.00',
104
+ applies_to_all_products: true,
105
+ priority: 2,
106
+ stackable: 'true',
107
+ }
108
+
109
+ const VOUCHER_TEN_PERCENT = {
110
+ id: 'v-ten',
111
+ code: 'SAVE10',
112
+ discount_type: 'percentage',
113
+ discount_value: '10',
114
+ applies_to_all_products: 't',
115
+ }
116
+
117
+ const AUTO_TWENTY_EXCLUSIVE = {
118
+ id: 'a-twenty-x',
119
+ name: 'Twenty off',
120
+ discount_type: 'fixed',
121
+ discount_value: '20',
122
+ applies_to_all_products: 't',
123
+ priority: 5,
124
+ stackable: 'f',
125
+ }
126
+
127
+ const DISCOUNT_ENGINE_FIXTURES: DiscountEngineFixture[] = [
128
+ {
129
+ name: 'nothing applied',
130
+ rules: [],
131
+ voucher: null,
132
+ cartItems: PARITY_CART,
133
+ taxRatePercent: 0,
134
+ shippingPrice: 10,
135
+ customer: NO_CUSTOMER,
136
+ legacy: false,
137
+ expected: {
138
+ goodsDiscount: 0,
139
+ shippingDiscount: 0,
140
+ automaticGoodsDiscount: 0,
141
+ voucherGoodsDiscount: 0,
142
+ voucherApplied: false,
143
+ voucherRejection: '',
144
+ appliedIds: [],
145
+ eligibleProductIds: [],
146
+ options: [],
147
+ choice: '',
148
+ },
149
+ },
150
+ {
151
+ name: 'voucher percentage on the whole cart',
152
+ rules: [],
153
+ voucher: VOUCHER_TEN_PERCENT,
154
+ cartItems: PARITY_CART,
155
+ taxRatePercent: 0,
156
+ shippingPrice: 10,
157
+ customer: NO_CUSTOMER,
158
+ legacy: false,
159
+ expected: {
160
+ goodsDiscount: 25,
161
+ shippingDiscount: 0,
162
+ automaticGoodsDiscount: 0,
163
+ voucherGoodsDiscount: 25,
164
+ voucherApplied: true,
165
+ voucherRejection: '',
166
+ appliedIds: ['v-ten'],
167
+ eligibleProductIds: ['p1', 'p2'],
168
+ options: [],
169
+ choice: '',
170
+ },
171
+ },
172
+ {
173
+ name: 'voucher percentage on gross prices (tax added on top, per-unit rounding)',
174
+ rules: [],
175
+ voucher: VOUCHER_TEN_PERCENT,
176
+ cartItems: [{ productId: 'p1', price: 23.79, quantity: 3 }],
177
+ taxRatePercent: 19,
178
+ shippingPrice: 0,
179
+ customer: NO_CUSTOMER,
180
+ legacy: false,
181
+ expected: {
182
+ goodsDiscount: 8.49,
183
+ shippingDiscount: 0,
184
+ automaticGoodsDiscount: 0,
185
+ voucherGoodsDiscount: 8.49,
186
+ voucherApplied: true,
187
+ voucherRejection: '',
188
+ appliedIds: ['v-ten'],
189
+ eligibleProductIds: ['p1'],
190
+ options: [],
191
+ choice: '',
192
+ },
193
+ },
194
+ {
195
+ name: 'voucher percentage above 100 clamps to the eligible gross',
196
+ rules: [],
197
+ voucher: { ...VOUCHER_TEN_PERCENT, discount_value: 150 },
198
+ cartItems: PARITY_CART,
199
+ taxRatePercent: 0,
200
+ shippingPrice: 10,
201
+ customer: NO_CUSTOMER,
202
+ legacy: false,
203
+ expected: {
204
+ goodsDiscount: 250,
205
+ shippingDiscount: 0,
206
+ automaticGoodsDiscount: 0,
207
+ voucherGoodsDiscount: 250,
208
+ voucherApplied: true,
209
+ voucherRejection: '',
210
+ appliedIds: ['v-ten'],
211
+ eligibleProductIds: ['p1', 'p2'],
212
+ options: [],
213
+ choice: '',
214
+ },
215
+ },
216
+ {
217
+ name: 'voucher fixed amount larger than the cart clamps',
218
+ rules: [],
219
+ voucher: { id: 'v-big', code: 'BIG', discount_type: 'fixed', discount_value: '999' },
220
+ cartItems: PARITY_CART,
221
+ taxRatePercent: 0,
222
+ shippingPrice: 10,
223
+ customer: NO_CUSTOMER,
224
+ legacy: false,
225
+ expected: {
226
+ goodsDiscount: 250,
227
+ shippingDiscount: 0,
228
+ automaticGoodsDiscount: 0,
229
+ voucherGoodsDiscount: 250,
230
+ voucherApplied: true,
231
+ voucherRejection: '',
232
+ appliedIds: ['v-big'],
233
+ eligibleProductIds: ['p1', 'p2'],
234
+ options: [],
235
+ choice: '',
236
+ },
237
+ },
238
+ {
239
+ name: 'voucher restricted to one product',
240
+ rules: [],
241
+ voucher: {
242
+ id: 'v-p2',
243
+ code: 'P2ONLY',
244
+ discount_type: 'fixed',
245
+ discount_value: '10',
246
+ applies_to_all_products: 'f',
247
+ product_ids: '["p2"]',
248
+ },
249
+ cartItems: PARITY_CART,
250
+ taxRatePercent: 0,
251
+ shippingPrice: 10,
252
+ customer: NO_CUSTOMER,
253
+ legacy: false,
254
+ expected: {
255
+ goodsDiscount: 10,
256
+ shippingDiscount: 0,
257
+ automaticGoodsDiscount: 0,
258
+ voucherGoodsDiscount: 10,
259
+ voucherApplied: true,
260
+ voucherRejection: '',
261
+ appliedIds: ['v-p2'],
262
+ eligibleProductIds: ['p2'],
263
+ options: [],
264
+ choice: '',
265
+ },
266
+ },
267
+ {
268
+ name: 'voucher restricted to a product not in the cart is a no-op',
269
+ rules: [],
270
+ voucher: {
271
+ id: 'v-p9',
272
+ code: 'P9',
273
+ discount_type: 'percentage',
274
+ discount_value: '10',
275
+ applies_to_all_products: false,
276
+ product_ids: ['p9'],
277
+ },
278
+ cartItems: PARITY_CART,
279
+ taxRatePercent: 0,
280
+ shippingPrice: 10,
281
+ customer: NO_CUSTOMER,
282
+ legacy: false,
283
+ expected: {
284
+ goodsDiscount: 0,
285
+ shippingDiscount: 0,
286
+ automaticGoodsDiscount: 0,
287
+ voucherGoodsDiscount: 0,
288
+ voucherApplied: false,
289
+ voucherRejection: 'no-op',
290
+ appliedIds: [],
291
+ eligibleProductIds: [],
292
+ options: [],
293
+ choice: '',
294
+ },
295
+ },
296
+ {
297
+ name: 'malformed product_ids reads as no restriction',
298
+ rules: [],
299
+ voucher: {
300
+ id: 'v-bad',
301
+ code: 'BAD',
302
+ discount_type: 'fixed',
303
+ discount_value: '5',
304
+ applies_to_all_products: 'f',
305
+ product_ids: '{not json',
306
+ },
307
+ cartItems: PARITY_CART,
308
+ taxRatePercent: 0,
309
+ shippingPrice: 10,
310
+ customer: NO_CUSTOMER,
311
+ legacy: false,
312
+ expected: {
313
+ goodsDiscount: 0,
314
+ shippingDiscount: 0,
315
+ automaticGoodsDiscount: 0,
316
+ voucherGoodsDiscount: 0,
317
+ voucherApplied: false,
318
+ voucherRejection: 'no-op',
319
+ appliedIds: [],
320
+ eligibleProductIds: [],
321
+ options: [],
322
+ choice: '',
323
+ },
324
+ },
325
+ {
326
+ name: 'free shipping voucher waives the fee',
327
+ rules: [],
328
+ voucher: { id: 'v-ship', code: 'SHIPFREE', discount_type: 'free_shipping', discount_value: 0 },
329
+ cartItems: PARITY_CART,
330
+ taxRatePercent: 0,
331
+ shippingPrice: 7.5,
332
+ customer: NO_CUSTOMER,
333
+ legacy: false,
334
+ expected: {
335
+ goodsDiscount: 0,
336
+ shippingDiscount: 7.5,
337
+ automaticGoodsDiscount: 0,
338
+ voucherGoodsDiscount: 0,
339
+ voucherApplied: true,
340
+ voucherRejection: '',
341
+ appliedIds: ['v-ship'],
342
+ eligibleProductIds: ['p1', 'p2'],
343
+ options: [],
344
+ choice: '',
345
+ },
346
+ },
347
+ {
348
+ name: 'free shipping voucher when shipping is already free is a no-op',
349
+ rules: [],
350
+ voucher: { id: 'v-ship', code: 'SHIPFREE', discount_type: 'free_shipping', discount_value: 0 },
351
+ cartItems: PARITY_CART,
352
+ taxRatePercent: 0,
353
+ shippingPrice: 0,
354
+ customer: NO_CUSTOMER,
355
+ legacy: false,
356
+ expected: {
357
+ goodsDiscount: 0,
358
+ shippingDiscount: 0,
359
+ automaticGoodsDiscount: 0,
360
+ voucherGoodsDiscount: 0,
361
+ voucherApplied: false,
362
+ voucherRejection: 'no-op',
363
+ appliedIds: [],
364
+ eligibleProductIds: ['p1', 'p2'],
365
+ options: [],
366
+ choice: '',
367
+ },
368
+ },
369
+ {
370
+ name: 'zero-quantity and empty carts price nothing',
371
+ rules: [AUTO_TEN_PERCENT],
372
+ voucher: VOUCHER_TEN_PERCENT,
373
+ cartItems: [{ productId: 'p1', price: 100, quantity: 0 }],
374
+ taxRatePercent: 0,
375
+ shippingPrice: 10,
376
+ customer: NO_CUSTOMER,
377
+ legacy: false,
378
+ expected: {
379
+ goodsDiscount: 0,
380
+ shippingDiscount: 0,
381
+ automaticGoodsDiscount: 0,
382
+ voucherGoodsDiscount: 0,
383
+ voucherApplied: false,
384
+ voucherRejection: 'no-op',
385
+ appliedIds: [],
386
+ eligibleProductIds: [],
387
+ options: [],
388
+ choice: '',
389
+ },
390
+ },
391
+ {
392
+ name: 'automatic rule then a stackable voucher price sequentially',
393
+ rules: [AUTO_TEN_PERCENT],
394
+ voucher: { id: 'v-20', code: 'TWENTY', discount_type: 'fixed', discount_value: '20' },
395
+ cartItems: PARITY_CART,
396
+ taxRatePercent: 0,
397
+ shippingPrice: 10,
398
+ customer: NO_CUSTOMER,
399
+ legacy: false,
400
+ expected: {
401
+ goodsDiscount: 45,
402
+ shippingDiscount: 0,
403
+ automaticGoodsDiscount: 25,
404
+ voucherGoodsDiscount: 20,
405
+ voucherApplied: true,
406
+ voucherRejection: '',
407
+ appliedIds: ['a-ten', 'v-20'],
408
+ eligibleProductIds: ['p1', 'p2'],
409
+ options: [],
410
+ choice: '',
411
+ },
412
+ },
413
+ {
414
+ name: 'two automatic rules stack in priority order on the remaining value',
415
+ rules: [AUTO_FIFTY_FIXED, AUTO_TEN_PERCENT],
416
+ voucher: null,
417
+ cartItems: PARITY_CART,
418
+ taxRatePercent: 0,
419
+ shippingPrice: 10,
420
+ customer: NO_CUSTOMER,
421
+ legacy: false,
422
+ expected: {
423
+ goodsDiscount: 75,
424
+ shippingDiscount: 0,
425
+ automaticGoodsDiscount: 75,
426
+ voucherGoodsDiscount: 0,
427
+ voucherApplied: false,
428
+ voucherRejection: '',
429
+ appliedIds: ['a-ten', 'a-fifty'],
430
+ eligibleProductIds: [],
431
+ options: [],
432
+ choice: '',
433
+ },
434
+ },
435
+ {
436
+ name: 'a percentage after a fixed rule prices the remainder',
437
+ rules: [{ ...AUTO_FIFTY_FIXED, priority: 0 }, AUTO_TEN_PERCENT],
438
+ voucher: null,
439
+ cartItems: PARITY_CART,
440
+ taxRatePercent: 0,
441
+ shippingPrice: 10,
442
+ customer: NO_CUSTOMER,
443
+ legacy: false,
444
+ expected: {
445
+ goodsDiscount: 70,
446
+ shippingDiscount: 0,
447
+ automaticGoodsDiscount: 70,
448
+ voucherGoodsDiscount: 0,
449
+ voucherApplied: false,
450
+ voucherRejection: '',
451
+ appliedIds: ['a-fifty', 'a-ten'],
452
+ eligibleProductIds: [],
453
+ options: [],
454
+ choice: '',
455
+ },
456
+ },
457
+ {
458
+ name: 'an exclusive automatic rule rejects the voucher',
459
+ rules: [{ ...AUTO_TEN_PERCENT, stackable: 'f' }],
460
+ voucher: VOUCHER_TEN_PERCENT,
461
+ cartItems: PARITY_CART,
462
+ taxRatePercent: 0,
463
+ shippingPrice: 10,
464
+ customer: NO_CUSTOMER,
465
+ legacy: false,
466
+ expected: {
467
+ goodsDiscount: 25,
468
+ shippingDiscount: 0,
469
+ automaticGoodsDiscount: 25,
470
+ voucherGoodsDiscount: 0,
471
+ voucherApplied: false,
472
+ voucherRejection: 'not-stackable',
473
+ appliedIds: ['a-ten'],
474
+ eligibleProductIds: ['p1', 'p2'],
475
+ options: [
476
+ { key: 'a-ten', label: 'Ten percent', saving: 25, selected: true },
477
+ { key: 'combination', label: 'Code SAVE10', saving: 25, selected: false },
478
+ ],
479
+ choice: 'a-ten',
480
+ },
481
+ },
482
+ {
483
+ name: 'an exclusive voucher that saves less than the combination is refused',
484
+ rules: [AUTO_TEN_PERCENT, AUTO_FIFTY_FIXED],
485
+ voucher: {
486
+ id: 'v-x',
487
+ code: 'EXCL',
488
+ discount_type: 'fixed',
489
+ discount_value: '20',
490
+ stackable: false,
491
+ },
492
+ cartItems: PARITY_CART,
493
+ taxRatePercent: 0,
494
+ shippingPrice: 10,
495
+ customer: NO_CUSTOMER,
496
+ legacy: false,
497
+ expected: {
498
+ goodsDiscount: 75,
499
+ shippingDiscount: 0,
500
+ automaticGoodsDiscount: 75,
501
+ voucherGoodsDiscount: 0,
502
+ voucherApplied: false,
503
+ voucherRejection: 'not-stackable',
504
+ appliedIds: ['a-ten', 'a-fifty'],
505
+ eligibleProductIds: ['p1', 'p2'],
506
+ options: [
507
+ { key: 'voucher', label: 'Code EXCL', saving: 20, selected: false },
508
+ { key: 'combination', label: 'Ten percent, Fifty off', saving: 75, selected: true },
509
+ ],
510
+ choice: 'combination',
511
+ },
512
+ },
513
+ {
514
+ name: 'an exclusive voucher that saves more than the combination replaces it',
515
+ rules: [AUTO_TEN_PERCENT, AUTO_FIFTY_FIXED],
516
+ voucher: {
517
+ id: 'v-x',
518
+ code: 'EXCL',
519
+ discount_type: 'fixed',
520
+ discount_value: '100',
521
+ stackable: false,
522
+ },
523
+ cartItems: PARITY_CART,
524
+ taxRatePercent: 0,
525
+ shippingPrice: 10,
526
+ customer: NO_CUSTOMER,
527
+ legacy: false,
528
+ expected: {
529
+ goodsDiscount: 100,
530
+ shippingDiscount: 0,
531
+ automaticGoodsDiscount: 0,
532
+ voucherGoodsDiscount: 100,
533
+ voucherApplied: true,
534
+ voucherRejection: '',
535
+ appliedIds: ['v-x'],
536
+ eligibleProductIds: ['p1', 'p2'],
537
+ options: [
538
+ { key: 'voucher', label: 'Code EXCL', saving: 100, selected: true },
539
+ { key: 'combination', label: 'Ten percent, Fifty off', saving: 75, selected: false },
540
+ ],
541
+ choice: 'voucher',
542
+ },
543
+ },
544
+ {
545
+ name: 'an exclusive voucher that does not apply leaves the automatic rules alone',
546
+ rules: [AUTO_TEN_PERCENT],
547
+ voucher: {
548
+ id: 'v-x9',
549
+ code: 'EXCL9',
550
+ discount_type: 'fixed',
551
+ discount_value: '20',
552
+ stackable: false,
553
+ applies_to_all_products: 'f',
554
+ product_ids: '["p9"]',
555
+ },
556
+ cartItems: PARITY_CART,
557
+ taxRatePercent: 0,
558
+ shippingPrice: 10,
559
+ customer: NO_CUSTOMER,
560
+ legacy: false,
561
+ expected: {
562
+ goodsDiscount: 25,
563
+ shippingDiscount: 0,
564
+ automaticGoodsDiscount: 25,
565
+ voucherGoodsDiscount: 0,
566
+ voucherApplied: false,
567
+ voucherRejection: 'no-op',
568
+ appliedIds: ['a-ten'],
569
+ eligibleProductIds: [],
570
+ options: [],
571
+ choice: '',
572
+ },
573
+ },
574
+ {
575
+ name: 'an exclusive automatic rule that saves more wins over the combination',
576
+ rules: [AUTO_TEN_PERCENT, { ...AUTO_FIFTY_FIXED, stackable: false }],
577
+ voucher: null,
578
+ cartItems: PARITY_CART,
579
+ taxRatePercent: 0,
580
+ shippingPrice: 10,
581
+ customer: NO_CUSTOMER,
582
+ legacy: false,
583
+ expected: {
584
+ goodsDiscount: 50,
585
+ shippingDiscount: 0,
586
+ automaticGoodsDiscount: 50,
587
+ voucherGoodsDiscount: 0,
588
+ voucherApplied: false,
589
+ voucherRejection: '',
590
+ appliedIds: ['a-fifty'],
591
+ eligibleProductIds: [],
592
+ options: [
593
+ { key: 'a-fifty', label: 'Fifty off', saving: 50, selected: true },
594
+ { key: 'combination', label: 'Ten percent', saving: 25, selected: false },
595
+ ],
596
+ choice: 'a-fifty',
597
+ },
598
+ },
599
+ {
600
+ name: 'an exclusive automatic rule wins over the combination whatever its priority',
601
+ rules: [AUTO_TEN_PERCENT, { ...AUTO_FIFTY_FIXED, priority: 0, stackable: false }],
602
+ voucher: null,
603
+ cartItems: PARITY_CART,
604
+ taxRatePercent: 0,
605
+ shippingPrice: 10,
606
+ customer: NO_CUSTOMER,
607
+ legacy: false,
608
+ expected: {
609
+ goodsDiscount: 50,
610
+ shippingDiscount: 0,
611
+ automaticGoodsDiscount: 50,
612
+ voucherGoodsDiscount: 0,
613
+ voucherApplied: false,
614
+ voucherRejection: '',
615
+ appliedIds: ['a-fifty'],
616
+ eligibleProductIds: [],
617
+ options: [
618
+ { key: 'a-fifty', label: 'Fifty off', saving: 50, selected: true },
619
+ { key: 'combination', label: 'Ten percent', saving: 25, selected: false },
620
+ ],
621
+ choice: 'a-fifty',
622
+ },
623
+ },
624
+ {
625
+ name: 'the combination wins over an exclusive rule that saves less',
626
+ rules: [
627
+ AUTO_TEN_PERCENT,
628
+ AUTO_FIFTY_FIXED,
629
+ {
630
+ id: 'a-sixty',
631
+ name: 'Sixty off',
632
+ discount_type: 'fixed',
633
+ discount_value: '60',
634
+ applies_to_all_products: 't',
635
+ priority: 0,
636
+ stackable: 'f',
637
+ },
638
+ ],
639
+ voucher: null,
640
+ cartItems: PARITY_CART,
641
+ taxRatePercent: 0,
642
+ shippingPrice: 10,
643
+ customer: NO_CUSTOMER,
644
+ legacy: false,
645
+ expected: {
646
+ goodsDiscount: 75,
647
+ shippingDiscount: 0,
648
+ automaticGoodsDiscount: 75,
649
+ voucherGoodsDiscount: 0,
650
+ voucherApplied: false,
651
+ voucherRejection: '',
652
+ appliedIds: ['a-ten', 'a-fifty'],
653
+ eligibleProductIds: [],
654
+ options: [
655
+ { key: 'a-sixty', label: 'Sixty off', saving: 60, selected: false },
656
+ { key: 'combination', label: 'Ten percent, Fifty off', saving: 75, selected: true },
657
+ ],
658
+ choice: 'combination',
659
+ },
660
+ },
661
+ {
662
+ name: 'a tie between an exclusive rule and the combination goes to the exclusive rule',
663
+ rules: [
664
+ AUTO_TEN_PERCENT,
665
+ {
666
+ id: 'a-25',
667
+ name: 'Twenty-five off',
668
+ discount_type: 'fixed',
669
+ discount_value: '25',
670
+ applies_to_all_products: 't',
671
+ priority: 5,
672
+ stackable: 'f',
673
+ },
674
+ ],
675
+ voucher: null,
676
+ cartItems: PARITY_CART,
677
+ taxRatePercent: 0,
678
+ shippingPrice: 10,
679
+ customer: NO_CUSTOMER,
680
+ legacy: false,
681
+ expected: {
682
+ goodsDiscount: 25,
683
+ shippingDiscount: 0,
684
+ automaticGoodsDiscount: 25,
685
+ voucherGoodsDiscount: 0,
686
+ voucherApplied: false,
687
+ voucherRejection: '',
688
+ appliedIds: ['a-25'],
689
+ eligibleProductIds: [],
690
+ options: [
691
+ { key: 'a-25', label: 'Twenty-five off', saving: 25, selected: true },
692
+ { key: 'combination', label: 'Ten percent', saving: 25, selected: false },
693
+ ],
694
+ choice: 'a-25',
695
+ },
696
+ },
697
+ {
698
+ name: 'a combinable voucher joins the combination an exclusive rule is measured against',
699
+ rules: [AUTO_TEN_PERCENT, { ...AUTO_FIFTY_FIXED, stackable: 'f' }],
700
+ voucher: VOUCHER_TEN_PERCENT,
701
+ cartItems: PARITY_CART,
702
+ taxRatePercent: 0,
703
+ shippingPrice: 10,
704
+ customer: NO_CUSTOMER,
705
+ legacy: false,
706
+ expected: {
707
+ goodsDiscount: 50,
708
+ shippingDiscount: 0,
709
+ automaticGoodsDiscount: 50,
710
+ voucherGoodsDiscount: 0,
711
+ voucherApplied: false,
712
+ voucherRejection: 'not-stackable',
713
+ appliedIds: ['a-fifty'],
714
+ eligibleProductIds: ['p1', 'p2'],
715
+ options: [
716
+ { key: 'a-fifty', label: 'Fifty off', saving: 50, selected: true },
717
+ { key: 'combination', label: 'Ten percent, Code SAVE10', saving: 47.5, selected: false },
718
+ ],
719
+ choice: 'a-fifty',
720
+ },
721
+ },
722
+ {
723
+ name: 'a combination with the voucher that saves more keeps the voucher',
724
+ rules: [AUTO_TEN_PERCENT, { ...AUTO_FIFTY_FIXED, discount_value: '40', stackable: 'f' }],
725
+ voucher: VOUCHER_TEN_PERCENT,
726
+ cartItems: PARITY_CART,
727
+ taxRatePercent: 0,
728
+ shippingPrice: 10,
729
+ customer: NO_CUSTOMER,
730
+ legacy: false,
731
+ expected: {
732
+ goodsDiscount: 47.5,
733
+ shippingDiscount: 0,
734
+ automaticGoodsDiscount: 25,
735
+ voucherGoodsDiscount: 22.5,
736
+ voucherApplied: true,
737
+ voucherRejection: '',
738
+ appliedIds: ['a-ten', 'v-ten'],
739
+ eligibleProductIds: ['p1', 'p2'],
740
+ options: [
741
+ { key: 'a-fifty', label: 'Fifty off', saving: 40, selected: false },
742
+ { key: 'combination', label: 'Ten percent, Code SAVE10', saving: 47.5, selected: true },
743
+ ],
744
+ choice: 'combination',
745
+ },
746
+ },
747
+ {
748
+ name: 'a voucher outside the chosen set is refused for its own reason when it would not apply anyway',
749
+ rules: [{ ...AUTO_FIFTY_FIXED, stackable: 'f' }],
750
+ voucher: { ...VOUCHER_TEN_PERCENT, min_order_value: '999' },
751
+ cartItems: PARITY_CART,
752
+ taxRatePercent: 0,
753
+ shippingPrice: 10,
754
+ customer: NO_CUSTOMER,
755
+ legacy: false,
756
+ expected: {
757
+ goodsDiscount: 50,
758
+ shippingDiscount: 0,
759
+ automaticGoodsDiscount: 50,
760
+ voucherGoodsDiscount: 0,
761
+ voucherApplied: false,
762
+ voucherRejection: 'min-order',
763
+ appliedIds: ['a-fifty'],
764
+ eligibleProductIds: ['p1', 'p2'],
765
+ options: [],
766
+ choice: '',
767
+ },
768
+ },
769
+ {
770
+ name: 'an exclusive free-shipping rule loses to a combination that saves more',
771
+ rules: [
772
+ AUTO_TEN_PERCENT,
773
+ {
774
+ id: 'a-ship-x',
775
+ name: 'Free delivery',
776
+ discount_type: 'free_shipping',
777
+ discount_value: 0,
778
+ applies_to_all_products: 't',
779
+ priority: 0,
780
+ stackable: 'f',
781
+ },
782
+ ],
783
+ voucher: null,
784
+ cartItems: PARITY_CART,
785
+ taxRatePercent: 0,
786
+ shippingPrice: 10,
787
+ customer: NO_CUSTOMER,
788
+ legacy: false,
789
+ expected: {
790
+ goodsDiscount: 25,
791
+ shippingDiscount: 0,
792
+ automaticGoodsDiscount: 25,
793
+ voucherGoodsDiscount: 0,
794
+ voucherApplied: false,
795
+ voucherRejection: '',
796
+ appliedIds: ['a-ten'],
797
+ eligibleProductIds: [],
798
+ options: [
799
+ { key: 'a-ship-x', label: 'Free delivery', saving: 10, selected: false },
800
+ { key: 'combination', label: 'Ten percent', saving: 25, selected: true },
801
+ ],
802
+ choice: 'combination',
803
+ },
804
+ },
805
+ {
806
+ name: 'minimum order value below the cart total rejects the voucher',
807
+ rules: [],
808
+ voucher: { ...VOUCHER_TEN_PERCENT, min_order_value: '300.00' },
809
+ cartItems: PARITY_CART,
810
+ taxRatePercent: 0,
811
+ shippingPrice: 10,
812
+ customer: NO_CUSTOMER,
813
+ legacy: false,
814
+ expected: {
815
+ goodsDiscount: 0,
816
+ shippingDiscount: 0,
817
+ automaticGoodsDiscount: 0,
818
+ voucherGoodsDiscount: 0,
819
+ voucherApplied: false,
820
+ voucherRejection: 'min-order',
821
+ appliedIds: [],
822
+ eligibleProductIds: ['p1', 'p2'],
823
+ options: [],
824
+ choice: '',
825
+ },
826
+ },
827
+ {
828
+ name: 'minimum order value is met by the gross total',
829
+ rules: [],
830
+ voucher: { ...VOUCHER_TEN_PERCENT, min_order_value: '300.00' },
831
+ cartItems: PARITY_CART,
832
+ taxRatePercent: 20,
833
+ shippingPrice: 10,
834
+ customer: NO_CUSTOMER,
835
+ legacy: false,
836
+ expected: {
837
+ goodsDiscount: 30,
838
+ shippingDiscount: 0,
839
+ automaticGoodsDiscount: 0,
840
+ voucherGoodsDiscount: 30,
841
+ voucherApplied: true,
842
+ voucherRejection: '',
843
+ appliedIds: ['v-ten'],
844
+ eligibleProductIds: ['p1', 'p2'],
845
+ options: [],
846
+ choice: '',
847
+ },
848
+ },
849
+ {
850
+ name: 'minimum quantity counts eligible units',
851
+ rules: [{ ...AUTO_TEN_PERCENT, min_quantity: 4 }],
852
+ voucher: { ...VOUCHER_TEN_PERCENT, min_quantity: '3' },
853
+ cartItems: PARITY_CART,
854
+ taxRatePercent: 0,
855
+ shippingPrice: 10,
856
+ customer: NO_CUSTOMER,
857
+ legacy: false,
858
+ expected: {
859
+ goodsDiscount: 25,
860
+ shippingDiscount: 0,
861
+ automaticGoodsDiscount: 0,
862
+ voucherGoodsDiscount: 25,
863
+ voucherApplied: true,
864
+ voucherRejection: '',
865
+ appliedIds: ['v-ten'],
866
+ eligibleProductIds: ['p1', 'p2'],
867
+ options: [],
868
+ choice: '',
869
+ },
870
+ },
871
+ {
872
+ name: 'a first-order rule needs a customer known to be new',
873
+ rules: [{ ...AUTO_TEN_PERCENT, first_order_only: 't' }],
874
+ voucher: { ...VOUCHER_TEN_PERCENT, first_order_only: true },
875
+ cartItems: PARITY_CART,
876
+ taxRatePercent: 0,
877
+ shippingPrice: 10,
878
+ customer: NO_CUSTOMER,
879
+ legacy: false,
880
+ expected: {
881
+ goodsDiscount: 0,
882
+ shippingDiscount: 0,
883
+ automaticGoodsDiscount: 0,
884
+ voucherGoodsDiscount: 0,
885
+ voucherApplied: false,
886
+ voucherRejection: 'first-order',
887
+ appliedIds: [],
888
+ eligibleProductIds: ['p1', 'p2'],
889
+ options: [],
890
+ choice: '',
891
+ },
892
+ },
893
+ {
894
+ name: 'a first-order rule applies to a new customer',
895
+ rules: [{ ...AUTO_TEN_PERCENT, first_order_only: 't' }],
896
+ voucher: null,
897
+ cartItems: PARITY_CART,
898
+ taxRatePercent: 0,
899
+ shippingPrice: 10,
900
+ customer: { isFirstOrder: true },
901
+ legacy: false,
902
+ expected: {
903
+ goodsDiscount: 25,
904
+ shippingDiscount: 0,
905
+ automaticGoodsDiscount: 25,
906
+ voucherGoodsDiscount: 0,
907
+ voucherApplied: false,
908
+ voucherRejection: '',
909
+ appliedIds: ['a-ten'],
910
+ eligibleProductIds: [],
911
+ options: [],
912
+ choice: '',
913
+ },
914
+ },
915
+ {
916
+ name: 'a category scope matches through the line categories',
917
+ rules: [],
918
+ voucher: {
919
+ id: 'v-cat',
920
+ code: 'CAT2',
921
+ discount_type: 'percentage',
922
+ discount_value: '10',
923
+ applies_to_all_products: 'f',
924
+ product_ids: null,
925
+ category_ids: '["c2"]',
926
+ },
927
+ cartItems: PARITY_CART,
928
+ taxRatePercent: 0,
929
+ shippingPrice: 10,
930
+ customer: NO_CUSTOMER,
931
+ legacy: false,
932
+ expected: {
933
+ goodsDiscount: 5,
934
+ shippingDiscount: 0,
935
+ automaticGoodsDiscount: 0,
936
+ voucherGoodsDiscount: 5,
937
+ voucherApplied: true,
938
+ voucherRejection: '',
939
+ appliedIds: ['v-cat'],
940
+ eligibleProductIds: ['p2'],
941
+ options: [],
942
+ choice: '',
943
+ },
944
+ },
945
+ {
946
+ name: 'a gift-card line is never eligible and never counts toward the minimum',
947
+ rules: [],
948
+ voucher: { ...VOUCHER_TEN_PERCENT, min_order_value: '260' },
949
+ cartItems: PARITY_CART_WITH_GIFT_CARD,
950
+ taxRatePercent: 0,
951
+ shippingPrice: 10,
952
+ customer: NO_CUSTOMER,
953
+ legacy: false,
954
+ expected: {
955
+ goodsDiscount: 0,
956
+ shippingDiscount: 0,
957
+ automaticGoodsDiscount: 0,
958
+ voucherGoodsDiscount: 0,
959
+ voucherApplied: false,
960
+ voucherRejection: 'min-order',
961
+ appliedIds: [],
962
+ eligibleProductIds: ['p1', 'p2'],
963
+ options: [],
964
+ choice: '',
965
+ },
966
+ },
967
+ {
968
+ name: 'a gift-card line is skipped by every rule',
969
+ rules: [AUTO_TEN_PERCENT],
970
+ voucher: null,
971
+ cartItems: PARITY_CART_WITH_GIFT_CARD,
972
+ taxRatePercent: 0,
973
+ shippingPrice: 10,
974
+ customer: NO_CUSTOMER,
975
+ legacy: false,
976
+ expected: {
977
+ goodsDiscount: 25,
978
+ shippingDiscount: 0,
979
+ automaticGoodsDiscount: 25,
980
+ voucherGoodsDiscount: 0,
981
+ voucherApplied: false,
982
+ voucherRejection: '',
983
+ appliedIds: ['a-ten'],
984
+ eligibleProductIds: [],
985
+ options: [],
986
+ choice: '',
987
+ },
988
+ },
989
+ {
990
+ name: 'buy 2 get 1 frees the cheapest unit of every full group',
991
+ rules: [],
992
+ voucher: {
993
+ id: 'v-bogo',
994
+ code: 'BOGO',
995
+ discount_type: 'buy_x_get_y',
996
+ discount_value: '0',
997
+ buy_quantity: '2',
998
+ get_quantity: '1',
999
+ },
1000
+ cartItems: PARITY_CART,
1001
+ taxRatePercent: 0,
1002
+ shippingPrice: 10,
1003
+ customer: NO_CUSTOMER,
1004
+ legacy: false,
1005
+ expected: {
1006
+ goodsDiscount: 50,
1007
+ shippingDiscount: 0,
1008
+ automaticGoodsDiscount: 0,
1009
+ voucherGoodsDiscount: 50,
1010
+ voucherApplied: true,
1011
+ voucherRejection: '',
1012
+ appliedIds: ['v-bogo'],
1013
+ eligibleProductIds: ['p1', 'p2'],
1014
+ options: [],
1015
+ choice: '',
1016
+ },
1017
+ },
1018
+ {
1019
+ name: 'buy 1 get 2 frees two units when the group is full',
1020
+ rules: [
1021
+ {
1022
+ id: 'a-bogo',
1023
+ name: 'Buy one get two',
1024
+ discount_type: 'buy_x_get_y',
1025
+ discount_value: 0,
1026
+ buy_quantity: 1,
1027
+ get_quantity: 2,
1028
+ applies_to_all_products: 't',
1029
+ },
1030
+ ],
1031
+ voucher: null,
1032
+ cartItems: PARITY_CART,
1033
+ taxRatePercent: 0,
1034
+ shippingPrice: 10,
1035
+ customer: NO_CUSTOMER,
1036
+ legacy: false,
1037
+ expected: {
1038
+ goodsDiscount: 150,
1039
+ shippingDiscount: 0,
1040
+ automaticGoodsDiscount: 150,
1041
+ voucherGoodsDiscount: 0,
1042
+ voucherApplied: false,
1043
+ voucherRejection: '',
1044
+ appliedIds: ['a-bogo'],
1045
+ eligibleProductIds: [],
1046
+ options: [],
1047
+ choice: '',
1048
+ },
1049
+ },
1050
+ {
1051
+ name: 'buy X get Y with too few units is a no-op',
1052
+ rules: [],
1053
+ voucher: {
1054
+ id: 'v-bogo4',
1055
+ code: 'BOGO4',
1056
+ discount_type: 'buy_x_get_y',
1057
+ discount_value: '0',
1058
+ buy_quantity: 3,
1059
+ get_quantity: 1,
1060
+ },
1061
+ cartItems: PARITY_CART,
1062
+ taxRatePercent: 0,
1063
+ shippingPrice: 10,
1064
+ customer: NO_CUSTOMER,
1065
+ legacy: false,
1066
+ expected: {
1067
+ goodsDiscount: 0,
1068
+ shippingDiscount: 0,
1069
+ automaticGoodsDiscount: 0,
1070
+ voucherGoodsDiscount: 0,
1071
+ voucherApplied: false,
1072
+ voucherRejection: 'no-op',
1073
+ appliedIds: [],
1074
+ eligibleProductIds: ['p1', 'p2'],
1075
+ options: [],
1076
+ choice: '',
1077
+ },
1078
+ },
1079
+ {
1080
+ name: 'free shipping is waived once however many rules say so',
1081
+ rules: [
1082
+ {
1083
+ id: 'a-ship',
1084
+ name: 'Free delivery',
1085
+ discount_type: 'free_shipping',
1086
+ discount_value: 0,
1087
+ applies_to_all_products: 't',
1088
+ priority: 1,
1089
+ },
1090
+ ],
1091
+ voucher: { id: 'v-ship', code: 'SHIPFREE', discount_type: 'free_shipping', discount_value: 0 },
1092
+ cartItems: PARITY_CART,
1093
+ taxRatePercent: 0,
1094
+ shippingPrice: 10,
1095
+ customer: NO_CUSTOMER,
1096
+ legacy: false,
1097
+ expected: {
1098
+ goodsDiscount: 0,
1099
+ shippingDiscount: 10,
1100
+ automaticGoodsDiscount: 0,
1101
+ voucherGoodsDiscount: 0,
1102
+ voucherApplied: false,
1103
+ voucherRejection: 'no-op',
1104
+ appliedIds: ['a-ship'],
1105
+ eligibleProductIds: ['p1', 'p2'],
1106
+ options: [],
1107
+ choice: '',
1108
+ },
1109
+ },
1110
+ {
1111
+ name: 'legacy mode ignores automatic rules and conditions',
1112
+ rules: [AUTO_TEN_PERCENT],
1113
+ voucher: { ...VOUCHER_TEN_PERCENT, min_order_value: '999', first_order_only: 't' },
1114
+ cartItems: PARITY_CART,
1115
+ taxRatePercent: 0,
1116
+ shippingPrice: 10,
1117
+ customer: NO_CUSTOMER,
1118
+ legacy: true,
1119
+ expected: {
1120
+ goodsDiscount: 25,
1121
+ shippingDiscount: 0,
1122
+ automaticGoodsDiscount: 0,
1123
+ voucherGoodsDiscount: 25,
1124
+ voucherApplied: true,
1125
+ voucherRejection: '',
1126
+ appliedIds: ['v-ten'],
1127
+ eligibleProductIds: ['p1', 'p2'],
1128
+ options: [],
1129
+ choice: '',
1130
+ },
1131
+ },
1132
+ {
1133
+ name: 'legacy mode prices a buy X get Y voucher as a no-op',
1134
+ rules: [],
1135
+ voucher: {
1136
+ id: 'v-bogo',
1137
+ code: 'BOGO',
1138
+ discount_type: 'buy_x_get_y',
1139
+ discount_value: '0',
1140
+ buy_quantity: '2',
1141
+ get_quantity: '1',
1142
+ },
1143
+ cartItems: PARITY_CART,
1144
+ taxRatePercent: 0,
1145
+ shippingPrice: 10,
1146
+ customer: NO_CUSTOMER,
1147
+ legacy: true,
1148
+ expected: {
1149
+ goodsDiscount: 0,
1150
+ shippingDiscount: 0,
1151
+ automaticGoodsDiscount: 0,
1152
+ voucherGoodsDiscount: 0,
1153
+ voucherApplied: false,
1154
+ voucherRejection: 'no-op',
1155
+ appliedIds: [],
1156
+ eligibleProductIds: ['p1', 'p2'],
1157
+ options: [],
1158
+ choice: '',
1159
+ },
1160
+ },
1161
+ {
1162
+ name: 'legacy mode ignores a category scope',
1163
+ rules: [],
1164
+ voucher: {
1165
+ id: 'v-cat',
1166
+ code: 'CAT2',
1167
+ discount_type: 'percentage',
1168
+ discount_value: '10',
1169
+ applies_to_all_products: 'f',
1170
+ product_ids: null,
1171
+ category_ids: '["c2"]',
1172
+ },
1173
+ cartItems: PARITY_CART,
1174
+ taxRatePercent: 0,
1175
+ shippingPrice: 10,
1176
+ customer: NO_CUSTOMER,
1177
+ legacy: true,
1178
+ expected: {
1179
+ goodsDiscount: 0,
1180
+ shippingDiscount: 0,
1181
+ automaticGoodsDiscount: 0,
1182
+ voucherGoodsDiscount: 0,
1183
+ voucherApplied: false,
1184
+ voucherRejection: 'no-op',
1185
+ appliedIds: [],
1186
+ eligibleProductIds: [],
1187
+ options: [],
1188
+ choice: '',
1189
+ },
1190
+ },
1191
+ {
1192
+ name: 'an unknown discount type is a no-op',
1193
+ rules: [{ ...AUTO_TEN_PERCENT, discount_type: 'tiered' }],
1194
+ voucher: { ...VOUCHER_TEN_PERCENT, discount_type: 'mystery' },
1195
+ cartItems: PARITY_CART,
1196
+ taxRatePercent: 0,
1197
+ shippingPrice: 10,
1198
+ customer: NO_CUSTOMER,
1199
+ legacy: false,
1200
+ expected: {
1201
+ goodsDiscount: 0,
1202
+ shippingDiscount: 0,
1203
+ automaticGoodsDiscount: 0,
1204
+ voucherGoodsDiscount: 0,
1205
+ voucherApplied: false,
1206
+ voucherRejection: 'no-op',
1207
+ appliedIds: [],
1208
+ eligibleProductIds: ['p1', 'p2'],
1209
+ options: [],
1210
+ choice: '',
1211
+ },
1212
+ },
1213
+ {
1214
+ name: 'stacked cent rounding: three cheap units at 10% then a fixed rule',
1215
+ rules: [AUTO_TEN_PERCENT, { ...AUTO_FIFTY_FIXED, discount_value: '0.30' }],
1216
+ voucher: null,
1217
+ cartItems: [{ productId: 'p1', price: 0.15, quantity: 3 }],
1218
+ taxRatePercent: 0,
1219
+ shippingPrice: 0,
1220
+ customer: NO_CUSTOMER,
1221
+ legacy: false,
1222
+ expected: {
1223
+ goodsDiscount: 0.35,
1224
+ shippingDiscount: 0,
1225
+ automaticGoodsDiscount: 0.35,
1226
+ voucherGoodsDiscount: 0,
1227
+ voucherApplied: false,
1228
+ voucherRejection: '',
1229
+ appliedIds: ['a-ten', 'a-fifty'],
1230
+ eligibleProductIds: [],
1231
+ options: [],
1232
+ choice: '',
1233
+ },
1234
+ },
1235
+ {
1236
+ name: 'a fixed rule never exceeds what is left of its lines',
1237
+ rules: [
1238
+ { ...AUTO_TEN_PERCENT, discount_value: '100' },
1239
+ { ...AUTO_FIFTY_FIXED, discount_value: '10' },
1240
+ ],
1241
+ voucher: null,
1242
+ cartItems: PARITY_CART,
1243
+ taxRatePercent: 0,
1244
+ shippingPrice: 10,
1245
+ customer: NO_CUSTOMER,
1246
+ legacy: false,
1247
+ expected: {
1248
+ goodsDiscount: 250,
1249
+ shippingDiscount: 0,
1250
+ automaticGoodsDiscount: 250,
1251
+ voucherGoodsDiscount: 0,
1252
+ voucherApplied: false,
1253
+ voucherRejection: '',
1254
+ appliedIds: ['a-ten'],
1255
+ eligibleProductIds: [],
1256
+ options: [],
1257
+ choice: '',
1258
+ },
1259
+ },
1260
+ {
1261
+ name: 'a recurring line is never discounted and counts for no minimum',
1262
+ rules: [{ ...AUTO_TEN_PERCENT, min_order_value: '260' }],
1263
+ voucher: VOUCHER_TEN_PERCENT,
1264
+ cartItems: PARITY_CART_WITH_SUBSCRIPTION,
1265
+ taxRatePercent: 0,
1266
+ shippingPrice: 10,
1267
+ customer: NO_CUSTOMER,
1268
+ legacy: false,
1269
+ expected: {
1270
+ goodsDiscount: 25,
1271
+ shippingDiscount: 0,
1272
+ automaticGoodsDiscount: 0,
1273
+ voucherGoodsDiscount: 25,
1274
+ voucherApplied: true,
1275
+ voucherRejection: '',
1276
+ appliedIds: ['v-ten'],
1277
+ eligibleProductIds: ['p1', 'p2'],
1278
+ options: [],
1279
+ choice: '',
1280
+ },
1281
+ },
1282
+ {
1283
+ name: 'two sets save something: the shopper picks the exclusive rule over the larger combination',
1284
+ rules: [AUTO_TEN_PERCENT, AUTO_TWENTY_EXCLUSIVE],
1285
+ voucher: null,
1286
+ cartItems: PARITY_CART,
1287
+ taxRatePercent: 0,
1288
+ shippingPrice: 10,
1289
+ customer: NO_CUSTOMER,
1290
+ legacy: false,
1291
+ choice: 'a-twenty-x',
1292
+ expected: {
1293
+ goodsDiscount: 20,
1294
+ shippingDiscount: 0,
1295
+ automaticGoodsDiscount: 20,
1296
+ voucherGoodsDiscount: 0,
1297
+ voucherApplied: false,
1298
+ voucherRejection: '',
1299
+ appliedIds: ['a-twenty-x'],
1300
+ eligibleProductIds: [],
1301
+ options: [
1302
+ { key: 'a-twenty-x', label: 'Twenty off', saving: 20, selected: true },
1303
+ { key: 'combination', label: 'Ten percent', saving: 25, selected: false },
1304
+ ],
1305
+ choice: 'a-twenty-x',
1306
+ },
1307
+ },
1308
+ {
1309
+ name: 'a choice naming no set falls back to the best set, and the options say so',
1310
+ rules: [AUTO_TEN_PERCENT, AUTO_TWENTY_EXCLUSIVE],
1311
+ voucher: null,
1312
+ cartItems: PARITY_CART,
1313
+ taxRatePercent: 0,
1314
+ shippingPrice: 10,
1315
+ customer: NO_CUSTOMER,
1316
+ legacy: false,
1317
+ choice: 'not-a-set',
1318
+ expected: {
1319
+ goodsDiscount: 25,
1320
+ shippingDiscount: 0,
1321
+ automaticGoodsDiscount: 25,
1322
+ voucherGoodsDiscount: 0,
1323
+ voucherApplied: false,
1324
+ voucherRejection: '',
1325
+ appliedIds: ['a-ten'],
1326
+ eligibleProductIds: [],
1327
+ options: [
1328
+ { key: 'a-twenty-x', label: 'Twenty off', saving: 20, selected: false },
1329
+ { key: 'combination', label: 'Ten percent', saving: 25, selected: true },
1330
+ ],
1331
+ choice: 'combination',
1332
+ },
1333
+ },
1334
+ {
1335
+ name: 'the shopper keeps their exclusive voucher over a larger exclusive rule',
1336
+ rules: [AUTO_FIFTY_FIXED],
1337
+ voucher: { ...VOUCHER_TEN_PERCENT, stackable: 'f' },
1338
+ cartItems: PARITY_CART,
1339
+ taxRatePercent: 0,
1340
+ shippingPrice: 10,
1341
+ customer: NO_CUSTOMER,
1342
+ legacy: false,
1343
+ choice: 'voucher',
1344
+ expected: {
1345
+ goodsDiscount: 25,
1346
+ shippingDiscount: 0,
1347
+ automaticGoodsDiscount: 0,
1348
+ voucherGoodsDiscount: 25,
1349
+ voucherApplied: true,
1350
+ voucherRejection: '',
1351
+ appliedIds: ['v-ten'],
1352
+ eligibleProductIds: ['p1', 'p2'],
1353
+ options: [
1354
+ { key: 'voucher', label: 'Code SAVE10', saving: 25, selected: true },
1355
+ { key: 'combination', label: 'Fifty off', saving: 50, selected: false },
1356
+ ],
1357
+ choice: 'voucher',
1358
+ },
1359
+ },
1360
+ {
1361
+ name: 'a combinable voucher is named inside the combination option',
1362
+ rules: [AUTO_TEN_PERCENT, AUTO_TWENTY_EXCLUSIVE],
1363
+ voucher: VOUCHER_TEN_PERCENT,
1364
+ cartItems: PARITY_CART,
1365
+ taxRatePercent: 0,
1366
+ shippingPrice: 10,
1367
+ customer: NO_CUSTOMER,
1368
+ legacy: false,
1369
+ choice: 'combination',
1370
+ expected: {
1371
+ goodsDiscount: 47.5,
1372
+ shippingDiscount: 0,
1373
+ automaticGoodsDiscount: 25,
1374
+ voucherGoodsDiscount: 22.5,
1375
+ voucherApplied: true,
1376
+ voucherRejection: '',
1377
+ appliedIds: ['a-ten', 'v-ten'],
1378
+ eligibleProductIds: ['p1', 'p2'],
1379
+ options: [
1380
+ { key: 'a-twenty-x', label: 'Twenty off', saving: 20, selected: false },
1381
+ { key: 'combination', label: 'Ten percent, Code SAVE10', saving: 47.5, selected: true },
1382
+ ],
1383
+ choice: 'combination',
1384
+ },
1385
+ },
1386
+ {
1387
+ name: 'one set saving something offers no choice, whatever the shopper says',
1388
+ rules: [AUTO_TEN_PERCENT],
1389
+ voucher: null,
1390
+ cartItems: PARITY_CART,
1391
+ taxRatePercent: 0,
1392
+ shippingPrice: 10,
1393
+ customer: NO_CUSTOMER,
1394
+ legacy: false,
1395
+ choice: 'combination',
1396
+ expected: {
1397
+ goodsDiscount: 25,
1398
+ shippingDiscount: 0,
1399
+ automaticGoodsDiscount: 25,
1400
+ voucherGoodsDiscount: 0,
1401
+ voucherApplied: false,
1402
+ voucherRejection: '',
1403
+ appliedIds: ['a-ten'],
1404
+ eligibleProductIds: [],
1405
+ options: [],
1406
+ choice: '',
1407
+ },
1408
+ },
1409
+ ]
1410
+
1411
+ const GIFT_CARD_TENDER_FIXTURES: GiftCardTenderFixture[] = [
1412
+ {
1413
+ name: 'the balance covers the whole order',
1414
+ input: { balance: 100, total: 80, currency: 'USD', hasGiftCardLines: false },
1415
+ expected: { applied: 80, amountDue: 0 },
1416
+ },
1417
+ {
1418
+ name: 'the balance covers part of the order',
1419
+ input: { balance: 25, total: 80, currency: 'USD', hasGiftCardLines: false },
1420
+ expected: { applied: 25, amountDue: 55 },
1421
+ },
1422
+ {
1423
+ name: 'a remainder under the minimum charge stays on the card',
1424
+ input: { balance: 79.8, total: 80, currency: 'USD', hasGiftCardLines: false },
1425
+ expected: { applied: 79.5, amountDue: 0.5 },
1426
+ },
1427
+ {
1428
+ name: 'the minimum charge follows the currency',
1429
+ input: { balance: 979, total: 1000, currency: 'JPY', hasGiftCardLines: false },
1430
+ expected: { applied: 950, amountDue: 50 },
1431
+ },
1432
+ {
1433
+ name: 'a total below the minimum charge is left to the provider',
1434
+ input: { balance: 0.1, total: 0.4, currency: 'USD', hasGiftCardLines: false },
1435
+ expected: { applied: 0, amountDue: 0.4 },
1436
+ },
1437
+ {
1438
+ name: 'a cart that buys a gift card cannot pay with one',
1439
+ input: { balance: 100, total: 80, currency: 'USD', hasGiftCardLines: true },
1440
+ expected: { applied: 0, amountDue: 80 },
1441
+ },
1442
+ {
1443
+ name: 'no balance, nothing applied',
1444
+ input: { balance: 0, total: 80, currency: 'EUR', hasGiftCardLines: false },
1445
+ expected: { applied: 0, amountDue: 80 },
1446
+ },
1447
+ {
1448
+ name: 'a zero total needs no tender',
1449
+ input: { balance: 30, total: 0, currency: 'EUR', hasGiftCardLines: false },
1450
+ expected: { applied: 0, amountDue: 0 },
1451
+ },
1452
+ {
1453
+ name: 'a recurring cart pays the first charge with the card, never below the minimum',
1454
+ input: {
1455
+ balance: 100,
1456
+ total: 30,
1457
+ currency: 'USD',
1458
+ hasGiftCardLines: false,
1459
+ hasRecurringLines: true,
1460
+ recurringTenderAllowed: true,
1461
+ },
1462
+ expected: { applied: 29.5, amountDue: 0.5 },
1463
+ },
1464
+ {
1465
+ name: 'a recurring cart takes a card smaller than the first charge in full',
1466
+ input: {
1467
+ balance: 10,
1468
+ total: 30,
1469
+ currency: 'USD',
1470
+ hasGiftCardLines: false,
1471
+ hasRecurringLines: true,
1472
+ recurringTenderAllowed: true,
1473
+ },
1474
+ expected: { applied: 10, amountDue: 20 },
1475
+ },
1476
+ {
1477
+ name: 'a recurring cart that starts with a free trial has nothing for a card to pay',
1478
+ input: {
1479
+ balance: 100,
1480
+ total: 30,
1481
+ currency: 'USD',
1482
+ hasGiftCardLines: false,
1483
+ hasRecurringLines: true,
1484
+ recurringTenderAllowed: true,
1485
+ hasRecurringTrial: true,
1486
+ },
1487
+ expected: { applied: 0, amountDue: 30 },
1488
+ },
1489
+ {
1490
+ name: 'a recurring cart on a checkout that cannot reduce the first charge pays nothing with a card',
1491
+ input: {
1492
+ balance: 100,
1493
+ total: 30,
1494
+ currency: 'USD',
1495
+ hasGiftCardLines: false,
1496
+ hasRecurringLines: true,
1497
+ },
1498
+ expected: { applied: 0, amountDue: 30 },
1499
+ },
1500
+ ]
1501
+ // ── END SHARED PARITY TABLE ─────────────────────────────────────────────────
1502
+
1503
+ describe('discount engine — storefront copy against the shared parity table', () => {
1504
+ const api = loadApi()
1505
+
1506
+ it.each(DISCOUNT_ENGINE_FIXTURES.map((fixture) => [fixture.name, fixture]))(
1507
+ '%s',
1508
+ (_name, fixture) => {
1509
+ const result = api.resolve({
1510
+ rules: fixture.rules.map((row) => api.normalizeRule(row, 'automatic')),
1511
+ voucher: fixture.voucher ? api.normalizeRule(fixture.voucher, 'voucher') : null,
1512
+ lines: api.linesFromCart(fixture.cartItems),
1513
+ taxRatePercent: fixture.taxRatePercent,
1514
+ shippingPrice: fixture.shippingPrice,
1515
+ customer: fixture.customer,
1516
+ legacy: fixture.legacy,
1517
+ choice: fixture.choice,
1518
+ })
1519
+ const applied = result.applied as Array<{ id: string }>
1520
+ expect({
1521
+ goodsDiscount: result.goodsDiscount,
1522
+ shippingDiscount: result.shippingDiscount,
1523
+ automaticGoodsDiscount: result.automaticGoodsDiscount,
1524
+ voucherGoodsDiscount: result.voucherGoodsDiscount,
1525
+ voucherApplied: result.voucherApplied,
1526
+ voucherRejection: result.voucherRejection,
1527
+ appliedIds: applied.map((entry) => entry.id),
1528
+ eligibleProductIds: result.eligibleProductIds,
1529
+ options: result.options,
1530
+ choice: result.choice,
1531
+ }).toEqual(fixture.expected)
1532
+ }
1533
+ )
1534
+
1535
+ it.each(GIFT_CARD_TENDER_FIXTURES.map((fixture) => [fixture.name, fixture]))(
1536
+ 'tender: %s',
1537
+ (_name, fixture) => {
1538
+ expect(api.tender(fixture.input)).toEqual(fixture.expected)
1539
+ }
1540
+ )
1541
+
1542
+ it('is made of function expressions only, so it can be spliced into a serialized handler', () => {
1543
+ const source = generateDiscountEngineHelperCode()
1544
+ expect(source).not.toMatch(/^\s*function\s+__de/m)
1545
+ expect(source).toMatch(/var __deResolve = function \(input\)/)
1546
+ expect(source).not.toContain('`')
1547
+ })
1548
+ })