@bash-app/bash-common 30.116.0 → 30.118.0
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/extendedSchemas.d.ts +54 -0
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js +10 -1
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/__tests__/paymentUtils.test.d.ts +6 -0
- package/dist/utils/__tests__/paymentUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/paymentUtils.test.js +77 -0
- package/dist/utils/__tests__/paymentUtils.test.js.map +1 -0
- package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.d.ts +2 -0
- package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.d.ts.map +1 -0
- package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.js +457 -0
- package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.js.map +1 -0
- package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.d.ts +2 -0
- package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.d.ts.map +1 -0
- package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js +480 -0
- package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js.map +1 -0
- package/package.json +2 -2
- package/prisma/COMPREHENSIVE-MIGRATION-README.md +295 -0
- package/prisma/MIGRATION-FILES-GUIDE.md +76 -0
- package/prisma/comprehensive-migration-20260120.sql +5751 -0
- package/prisma/delta-migration-20260120.sql +302 -0
- package/prisma/schema.prisma +253 -13
- package/prisma/verify-migration.sql +132 -0
- package/src/extendedSchemas.ts +10 -1
- package/src/index.ts +4 -0
- package/src/utils/__tests__/paymentUtils.test.ts +95 -0
- package/src/utils/discountEngine/__tests__/bestPriceResolver.test.ts +558 -0
- package/src/utils/discountEngine/__tests__/eligibilityValidator.test.ts +655 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findBestPrice,
|
|
3
|
+
applyGuardrails,
|
|
4
|
+
calculateEffectiveRate,
|
|
5
|
+
BestPriceOptions,
|
|
6
|
+
} from "../bestPriceResolver";
|
|
7
|
+
import { BashEventPromoCode, SpecialOffer, SpecialOfferType } from "@prisma/client";
|
|
8
|
+
|
|
9
|
+
describe("bestPriceResolver", () => {
|
|
10
|
+
const mockPromoCode: BashEventPromoCode = {
|
|
11
|
+
id: "promo-1",
|
|
12
|
+
code: "SUMMER20",
|
|
13
|
+
bashEventId: "event-1",
|
|
14
|
+
discountAmountPercentage: 20,
|
|
15
|
+
discountAmountInCents: null,
|
|
16
|
+
maxRedemptions: null,
|
|
17
|
+
redeemBy: null,
|
|
18
|
+
promoterId: null,
|
|
19
|
+
createdAt: new Date(),
|
|
20
|
+
updatedAt: new Date(),
|
|
21
|
+
} as any;
|
|
22
|
+
|
|
23
|
+
const mockSpecialOffer: SpecialOffer = {
|
|
24
|
+
id: "offer-1",
|
|
25
|
+
ticketTierId: "tier-1",
|
|
26
|
+
offerType: SpecialOfferType.EARLY_BIRD,
|
|
27
|
+
title: "Early Bird",
|
|
28
|
+
description: null,
|
|
29
|
+
discountType: "PERCENTAGE" as any,
|
|
30
|
+
discountValue: 15,
|
|
31
|
+
isActive: true,
|
|
32
|
+
canStackWithPromoCodes: true,
|
|
33
|
+
canStackWithOtherOffers: false,
|
|
34
|
+
displayBadge: true,
|
|
35
|
+
badgeText: "EARLY BIRD",
|
|
36
|
+
maxRedemptions: null,
|
|
37
|
+
currentRedemptions: 0,
|
|
38
|
+
createdAt: new Date(),
|
|
39
|
+
updatedAt: new Date(),
|
|
40
|
+
} as any;
|
|
41
|
+
|
|
42
|
+
describe("findBestPrice - No Discounts", () => {
|
|
43
|
+
it("should return original price when no discounts available", () => {
|
|
44
|
+
const options: BestPriceOptions = {
|
|
45
|
+
ticketPrice: 5000, // $50
|
|
46
|
+
quantity: 2,
|
|
47
|
+
promoCodes: [],
|
|
48
|
+
specialOffers: [],
|
|
49
|
+
allowStacking: true,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = findBestPrice(options);
|
|
53
|
+
|
|
54
|
+
expect(result.originalTotal).toBe(10000); // $100
|
|
55
|
+
expect(result.finalTotal).toBe(10000);
|
|
56
|
+
expect(result.totalSavings).toBe(0);
|
|
57
|
+
expect(result.appliedDiscounts).toHaveLength(0);
|
|
58
|
+
expect(result.explanation).toBe("No discounts applied");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should handle zero quantity", () => {
|
|
62
|
+
const options: BestPriceOptions = {
|
|
63
|
+
ticketPrice: 5000,
|
|
64
|
+
quantity: 0,
|
|
65
|
+
promoCodes: [],
|
|
66
|
+
specialOffers: [],
|
|
67
|
+
allowStacking: true,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const result = findBestPrice(options);
|
|
71
|
+
|
|
72
|
+
expect(result.originalTotal).toBe(0);
|
|
73
|
+
expect(result.finalTotal).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("findBestPrice - Promo Code Only", () => {
|
|
78
|
+
it("should apply single promo code discount", () => {
|
|
79
|
+
const options: BestPriceOptions = {
|
|
80
|
+
ticketPrice: 5000, // $50
|
|
81
|
+
quantity: 2,
|
|
82
|
+
promoCodes: [mockPromoCode], // 20% off
|
|
83
|
+
specialOffers: [],
|
|
84
|
+
allowStacking: true,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const result = findBestPrice(options);
|
|
88
|
+
|
|
89
|
+
expect(result.originalTotal).toBe(10000); // $100
|
|
90
|
+
expect(result.finalTotal).toBe(8000); // $80 after 20% off
|
|
91
|
+
expect(result.totalSavings).toBe(2000); // Saved $20
|
|
92
|
+
expect(result.appliedDiscounts).toHaveLength(1);
|
|
93
|
+
expect(result.appliedDiscounts[0].sourceName).toBe("SUMMER20");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should select best promo code from multiple options", () => {
|
|
97
|
+
const promo30: BashEventPromoCode = {
|
|
98
|
+
...mockPromoCode,
|
|
99
|
+
id: "promo-2",
|
|
100
|
+
code: "MEGA30",
|
|
101
|
+
discountAmountPercentage: 30,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const options: BestPriceOptions = {
|
|
105
|
+
ticketPrice: 5000,
|
|
106
|
+
quantity: 2,
|
|
107
|
+
promoCodes: [mockPromoCode, promo30], // 20% vs 30%
|
|
108
|
+
specialOffers: [],
|
|
109
|
+
allowStacking: false,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const result = findBestPrice(options);
|
|
113
|
+
|
|
114
|
+
expect(result.finalTotal).toBe(7000); // Best: 30% off
|
|
115
|
+
expect(result.appliedDiscounts[0].sourceName).toBe("MEGA30");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should handle fixed amount promo code", () => {
|
|
119
|
+
const fixedPromo: BashEventPromoCode = {
|
|
120
|
+
...mockPromoCode,
|
|
121
|
+
discountAmountPercentage: null,
|
|
122
|
+
discountAmountInCents: 1500, // $15 off
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const options: BestPriceOptions = {
|
|
126
|
+
ticketPrice: 5000,
|
|
127
|
+
quantity: 2,
|
|
128
|
+
promoCodes: [fixedPromo],
|
|
129
|
+
specialOffers: [],
|
|
130
|
+
allowStacking: true,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = findBestPrice(options);
|
|
134
|
+
|
|
135
|
+
expect(result.originalTotal).toBe(10000);
|
|
136
|
+
expect(result.finalTotal).toBe(8500); // $100 - $15
|
|
137
|
+
expect(result.totalSavings).toBe(1500);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("findBestPrice - Special Offer Only", () => {
|
|
142
|
+
it("should apply single special offer discount", () => {
|
|
143
|
+
const options: BestPriceOptions = {
|
|
144
|
+
ticketPrice: 5000,
|
|
145
|
+
quantity: 2,
|
|
146
|
+
promoCodes: [],
|
|
147
|
+
specialOffers: [mockSpecialOffer], // 15% off
|
|
148
|
+
allowStacking: true,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const result = findBestPrice(options);
|
|
152
|
+
|
|
153
|
+
expect(result.finalTotal).toBe(8500); // $85 after 15% off
|
|
154
|
+
expect(result.totalSavings).toBe(1500); // Saved $15
|
|
155
|
+
expect(result.appliedDiscounts).toHaveLength(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should select best offer from multiple options", () => {
|
|
159
|
+
const betterOffer: SpecialOffer = {
|
|
160
|
+
...mockSpecialOffer,
|
|
161
|
+
id: "offer-2",
|
|
162
|
+
discountValue: 25, // 25% off
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const options: BestPriceOptions = {
|
|
166
|
+
ticketPrice: 5000,
|
|
167
|
+
quantity: 2,
|
|
168
|
+
promoCodes: [],
|
|
169
|
+
specialOffers: [mockSpecialOffer, betterOffer], // 15% vs 25%
|
|
170
|
+
allowStacking: false,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const result = findBestPrice(options);
|
|
174
|
+
|
|
175
|
+
expect(result.finalTotal).toBe(7500); // Best: 25% off = $75
|
|
176
|
+
expect(result.appliedDiscounts[0].sourceId).toBe("offer-2");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("findBestPrice - Stacking Discounts", () => {
|
|
181
|
+
it("should stack promo code with special offer when allowed", () => {
|
|
182
|
+
const stackableOffer: SpecialOffer = {
|
|
183
|
+
...mockSpecialOffer,
|
|
184
|
+
canStackWithPromoCodes: true,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const options: BestPriceOptions = {
|
|
188
|
+
ticketPrice: 5000, // $50 per ticket
|
|
189
|
+
quantity: 2, // $100 total
|
|
190
|
+
promoCodes: [mockPromoCode], // 20% off
|
|
191
|
+
specialOffers: [stackableOffer], // 15% off
|
|
192
|
+
allowStacking: true,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const result = findBestPrice(options);
|
|
196
|
+
|
|
197
|
+
// Stacked: 20% off $100 = $80, then 15% off $80 = $68
|
|
198
|
+
expect(result.finalTotal).toBeLessThan(8000); // Better than single discount
|
|
199
|
+
expect(result.appliedDiscounts).toHaveLength(2);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should not stack when offer disallows stacking", () => {
|
|
203
|
+
const nonStackableOffer: SpecialOffer = {
|
|
204
|
+
...mockSpecialOffer,
|
|
205
|
+
canStackWithPromoCodes: false,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const options: BestPriceOptions = {
|
|
209
|
+
ticketPrice: 5000,
|
|
210
|
+
quantity: 2,
|
|
211
|
+
promoCodes: [mockPromoCode], // 20% off
|
|
212
|
+
specialOffers: [nonStackableOffer], // 15% off, no stacking
|
|
213
|
+
allowStacking: true,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const result = findBestPrice(options);
|
|
217
|
+
|
|
218
|
+
// Should pick the better single discount (20%)
|
|
219
|
+
expect(result.appliedDiscounts).toHaveLength(1);
|
|
220
|
+
expect(result.finalTotal).toBe(8000); // 20% off
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should not stack when global stacking is disabled", () => {
|
|
224
|
+
const stackableOffer: SpecialOffer = {
|
|
225
|
+
...mockSpecialOffer,
|
|
226
|
+
canStackWithPromoCodes: true,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const options: BestPriceOptions = {
|
|
230
|
+
ticketPrice: 5000,
|
|
231
|
+
quantity: 2,
|
|
232
|
+
promoCodes: [mockPromoCode],
|
|
233
|
+
specialOffers: [stackableOffer],
|
|
234
|
+
allowStacking: false, // Global override
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const result = findBestPrice(options);
|
|
238
|
+
|
|
239
|
+
// Should pick best single discount
|
|
240
|
+
expect(result.appliedDiscounts).toHaveLength(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should stack multiple offers when both allow it", () => {
|
|
244
|
+
const offer1: SpecialOffer = {
|
|
245
|
+
...mockSpecialOffer,
|
|
246
|
+
id: "offer-1",
|
|
247
|
+
discountValue: 10,
|
|
248
|
+
canStackWithOtherOffers: true,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const offer2: SpecialOffer = {
|
|
252
|
+
...mockSpecialOffer,
|
|
253
|
+
id: "offer-2",
|
|
254
|
+
discountValue: 15,
|
|
255
|
+
canStackWithOtherOffers: true,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const options: BestPriceOptions = {
|
|
259
|
+
ticketPrice: 5000,
|
|
260
|
+
quantity: 2,
|
|
261
|
+
promoCodes: [],
|
|
262
|
+
specialOffers: [offer1, offer2],
|
|
263
|
+
allowStacking: true,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = findBestPrice(options);
|
|
267
|
+
|
|
268
|
+
// Should stack both offers
|
|
269
|
+
expect(result.appliedDiscounts.length).toBeGreaterThan(0);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("findBestPrice - Edge Cases", () => {
|
|
274
|
+
it("should handle 100% discount (free tickets)", () => {
|
|
275
|
+
const freePromo: BashEventPromoCode = {
|
|
276
|
+
...mockPromoCode,
|
|
277
|
+
discountAmountPercentage: 100,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const options: BestPriceOptions = {
|
|
281
|
+
ticketPrice: 5000,
|
|
282
|
+
quantity: 2,
|
|
283
|
+
promoCodes: [freePromo],
|
|
284
|
+
specialOffers: [],
|
|
285
|
+
allowStacking: true,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const result = findBestPrice(options);
|
|
289
|
+
|
|
290
|
+
expect(result.finalTotal).toBe(0);
|
|
291
|
+
expect(result.totalSavings).toBe(10000);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should not allow negative final price", () => {
|
|
295
|
+
const overDiscount: BashEventPromoCode = {
|
|
296
|
+
...mockPromoCode,
|
|
297
|
+
discountAmountPercentage: null,
|
|
298
|
+
discountAmountInCents: 15000, // $150 off on $100 order
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const options: BestPriceOptions = {
|
|
302
|
+
ticketPrice: 5000,
|
|
303
|
+
quantity: 2, // Total: $100
|
|
304
|
+
promoCodes: [overDiscount],
|
|
305
|
+
specialOffers: [],
|
|
306
|
+
allowStacking: true,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const result = findBestPrice(options);
|
|
310
|
+
|
|
311
|
+
expect(result.finalTotal).toBe(0); // Floor at 0, not negative
|
|
312
|
+
expect(result.totalSavings).toBe(10000); // Capped at original price
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should handle very large quantities", () => {
|
|
316
|
+
const options: BestPriceOptions = {
|
|
317
|
+
ticketPrice: 5000,
|
|
318
|
+
quantity: 1000, // Large order
|
|
319
|
+
promoCodes: [mockPromoCode],
|
|
320
|
+
specialOffers: [],
|
|
321
|
+
allowStacking: true,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const result = findBestPrice(options);
|
|
325
|
+
|
|
326
|
+
expect(result.originalTotal).toBe(5000000); // $50,000
|
|
327
|
+
expect(result.finalTotal).toBe(4000000); // 20% off
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("should handle rounding correctly", () => {
|
|
331
|
+
const options: BestPriceOptions = {
|
|
332
|
+
ticketPrice: 3333, // $33.33
|
|
333
|
+
quantity: 3, // Total: $99.99
|
|
334
|
+
promoCodes: [mockPromoCode], // 20% off
|
|
335
|
+
specialOffers: [],
|
|
336
|
+
allowStacking: true,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const result = findBestPrice(options);
|
|
340
|
+
|
|
341
|
+
// Should round down to nearest cent
|
|
342
|
+
expect(Number.isInteger(result.finalTotal)).toBe(true);
|
|
343
|
+
expect(result.finalTotal).toBeGreaterThan(0);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe("applyGuardrails", () => {
|
|
348
|
+
const mockDiscountResult = {
|
|
349
|
+
sourceType: "PROMO_CODE" as const,
|
|
350
|
+
sourceId: "promo-1",
|
|
351
|
+
sourceName: "TEST",
|
|
352
|
+
discountType: "PERCENTAGE" as const,
|
|
353
|
+
discountValue: 50,
|
|
354
|
+
amountDiscounted: 5000,
|
|
355
|
+
originalPrice: 10000,
|
|
356
|
+
finalPrice: 5000,
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
it("should enforce minimum price floor", () => {
|
|
360
|
+
const result = applyGuardrails(mockDiscountResult, 6000); // Min $60
|
|
361
|
+
|
|
362
|
+
expect(result.finalPrice).toBe(6000); // Capped at min
|
|
363
|
+
expect(result.amountDiscounted).toBe(4000); // Adjusted discount
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("should enforce maximum discount percentage", () => {
|
|
367
|
+
const result = applyGuardrails(mockDiscountResult, undefined, 30); // Max 30% off
|
|
368
|
+
|
|
369
|
+
expect(result.amountDiscounted).toBeLessThanOrEqual(3000); // 30% of $100
|
|
370
|
+
expect(result.finalPrice).toBeGreaterThanOrEqual(7000); // At least $70
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should apply both guardrails", () => {
|
|
374
|
+
const result = applyGuardrails(mockDiscountResult, 7500, 30);
|
|
375
|
+
|
|
376
|
+
expect(result.finalPrice).toBeGreaterThanOrEqual(7500); // Min floor
|
|
377
|
+
expect(result.amountDiscounted).toBeLessThanOrEqual(3000); // Max discount
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("should not modify result when within bounds", () => {
|
|
381
|
+
const result = applyGuardrails(mockDiscountResult, 4000, 60);
|
|
382
|
+
|
|
383
|
+
expect(result.finalPrice).toBe(5000); // Unchanged
|
|
384
|
+
expect(result.amountDiscounted).toBe(5000); // Unchanged
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("calculateEffectiveRate", () => {
|
|
389
|
+
it("should calculate effective discount percentage", () => {
|
|
390
|
+
const result = {
|
|
391
|
+
appliedDiscounts: [],
|
|
392
|
+
originalTotal: 10000, // $100
|
|
393
|
+
finalTotal: 7500, // $75
|
|
394
|
+
totalSavings: 2500, // $25
|
|
395
|
+
explanation: "",
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const rate = calculateEffectiveRate(result);
|
|
399
|
+
|
|
400
|
+
expect(rate).toBe(25); // 25% effective discount
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("should handle zero original price", () => {
|
|
404
|
+
const result = {
|
|
405
|
+
appliedDiscounts: [],
|
|
406
|
+
originalTotal: 0,
|
|
407
|
+
finalTotal: 0,
|
|
408
|
+
totalSavings: 0,
|
|
409
|
+
explanation: "",
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const rate = calculateEffectiveRate(result);
|
|
413
|
+
|
|
414
|
+
expect(rate).toBe(0);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("should round down to whole percentage", () => {
|
|
418
|
+
const result = {
|
|
419
|
+
appliedDiscounts: [],
|
|
420
|
+
originalTotal: 10000,
|
|
421
|
+
finalTotal: 6666, // 33.34% off
|
|
422
|
+
totalSavings: 3334,
|
|
423
|
+
explanation: "",
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const rate = calculateEffectiveRate(result);
|
|
427
|
+
|
|
428
|
+
expect(rate).toBe(33); // Rounded down
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("should handle 100% discount", () => {
|
|
432
|
+
const result = {
|
|
433
|
+
appliedDiscounts: [],
|
|
434
|
+
originalTotal: 10000,
|
|
435
|
+
finalTotal: 0,
|
|
436
|
+
totalSavings: 10000,
|
|
437
|
+
explanation: "",
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const rate = calculateEffectiveRate(result);
|
|
441
|
+
|
|
442
|
+
expect(rate).toBe(100);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
describe("findBestPrice - Complex Scenarios", () => {
|
|
447
|
+
it("should choose best combination among many options", () => {
|
|
448
|
+
const promo10: BashEventPromoCode = {
|
|
449
|
+
...mockPromoCode,
|
|
450
|
+
id: "promo-10",
|
|
451
|
+
discountAmountPercentage: 10,
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const promo25: BashEventPromoCode = {
|
|
455
|
+
...mockPromoCode,
|
|
456
|
+
id: "promo-25",
|
|
457
|
+
discountAmountPercentage: 25,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const offer15: SpecialOffer = {
|
|
461
|
+
...mockSpecialOffer,
|
|
462
|
+
id: "offer-15",
|
|
463
|
+
discountValue: 15,
|
|
464
|
+
canStackWithPromoCodes: false,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const offer20: SpecialOffer = {
|
|
468
|
+
...mockSpecialOffer,
|
|
469
|
+
id: "offer-20",
|
|
470
|
+
discountValue: 20,
|
|
471
|
+
canStackWithPromoCodes: true,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const options: BestPriceOptions = {
|
|
475
|
+
ticketPrice: 10000, // $100
|
|
476
|
+
quantity: 1,
|
|
477
|
+
promoCodes: [promo10, promo25],
|
|
478
|
+
specialOffers: [offer15, offer20],
|
|
479
|
+
allowStacking: true,
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const result = findBestPrice(options);
|
|
483
|
+
|
|
484
|
+
// Best should be: 25% promo + 20% offer (if stacked)
|
|
485
|
+
// Or 25% promo alone = $75
|
|
486
|
+
// Stacked: 25% off $100 = $75, then 20% off $75 = $60
|
|
487
|
+
expect(result.finalTotal).toBeLessThanOrEqual(7500);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("should generate correct explanation string", () => {
|
|
491
|
+
const options: BestPriceOptions = {
|
|
492
|
+
ticketPrice: 5000,
|
|
493
|
+
quantity: 2,
|
|
494
|
+
promoCodes: [mockPromoCode],
|
|
495
|
+
specialOffers: [],
|
|
496
|
+
allowStacking: true,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const result = findBestPrice(options);
|
|
500
|
+
|
|
501
|
+
expect(result.explanation).toContain("SUMMER20");
|
|
502
|
+
expect(result.explanation).not.toBe("No discounts applied");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("should handle mixed discount types in stacking", () => {
|
|
506
|
+
const percentPromo: BashEventPromoCode = {
|
|
507
|
+
...mockPromoCode,
|
|
508
|
+
discountAmountPercentage: 20,
|
|
509
|
+
discountAmountInCents: null,
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const fixedOffer: SpecialOffer = {
|
|
513
|
+
...mockSpecialOffer,
|
|
514
|
+
discountType: "FIXED_AMOUNT" as any,
|
|
515
|
+
discountValue: 500, // $5 off (in cents)
|
|
516
|
+
canStackWithPromoCodes: true,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const options: BestPriceOptions = {
|
|
520
|
+
ticketPrice: 5000,
|
|
521
|
+
quantity: 2,
|
|
522
|
+
promoCodes: [percentPromo],
|
|
523
|
+
specialOffers: [fixedOffer],
|
|
524
|
+
allowStacking: true,
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const result = findBestPrice(options);
|
|
528
|
+
|
|
529
|
+
// Should stack: 20% off $100 = $80, then $5 off = $75
|
|
530
|
+
expect(result.finalTotal).toBeLessThan(8000);
|
|
531
|
+
expect(result.appliedDiscounts.length).toBeGreaterThan(0);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
describe("findBestPrice - Attribution", () => {
|
|
536
|
+
it("should track promoter attribution", () => {
|
|
537
|
+
const promoWithPromoter: BashEventPromoCode = {
|
|
538
|
+
...mockPromoCode,
|
|
539
|
+
promoterId: "promoter-123",
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const options: BestPriceOptions = {
|
|
543
|
+
ticketPrice: 5000,
|
|
544
|
+
quantity: 2,
|
|
545
|
+
userId: "user-456",
|
|
546
|
+
promoCodes: [promoWithPromoter],
|
|
547
|
+
specialOffers: [],
|
|
548
|
+
allowStacking: true,
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const result = findBestPrice(options);
|
|
552
|
+
|
|
553
|
+
expect(result.appliedDiscounts[0]).toHaveProperty("attribution");
|
|
554
|
+
expect(result.appliedDiscounts[0].attribution?.userId).toBe("promoter-123");
|
|
555
|
+
expect(result.appliedDiscounts[0].attribution?.commissionAmount).toBeGreaterThan(0);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
});
|