@bash-app/bash-common 30.117.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.
Files changed (31) hide show
  1. package/dist/extendedSchemas.d.ts +54 -0
  2. package/dist/extendedSchemas.d.ts.map +1 -1
  3. package/dist/extendedSchemas.js +10 -1
  4. package/dist/extendedSchemas.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/utils/__tests__/paymentUtils.test.d.ts +6 -0
  9. package/dist/utils/__tests__/paymentUtils.test.d.ts.map +1 -0
  10. package/dist/utils/__tests__/paymentUtils.test.js +77 -0
  11. package/dist/utils/__tests__/paymentUtils.test.js.map +1 -0
  12. package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.d.ts +2 -0
  13. package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.d.ts.map +1 -0
  14. package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.js +457 -0
  15. package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.js.map +1 -0
  16. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.d.ts +2 -0
  17. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.d.ts.map +1 -0
  18. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js +480 -0
  19. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js.map +1 -0
  20. package/package.json +2 -2
  21. package/prisma/COMPREHENSIVE-MIGRATION-README.md +295 -0
  22. package/prisma/MIGRATION-FILES-GUIDE.md +76 -0
  23. package/prisma/comprehensive-migration-20260120.sql +5751 -0
  24. package/prisma/delta-migration-20260120.sql +302 -0
  25. package/prisma/schema.prisma +253 -13
  26. package/prisma/verify-migration.sql +132 -0
  27. package/src/extendedSchemas.ts +10 -1
  28. package/src/index.ts +4 -0
  29. package/src/utils/__tests__/paymentUtils.test.ts +95 -0
  30. package/src/utils/discountEngine/__tests__/bestPriceResolver.test.ts +558 -0
  31. 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
+ });