@bash-app/bash-common 30.112.0 → 30.114.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 (40) hide show
  1. package/dist/definitions.d.ts +2 -2
  2. package/dist/definitions.d.ts.map +1 -1
  3. package/dist/definitions.js +2 -2
  4. package/dist/definitions.js.map +1 -1
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/utils/__tests__/recurrenceUtils.test.d.ts +2 -0
  10. package/dist/utils/__tests__/recurrenceUtils.test.d.ts.map +1 -0
  11. package/dist/utils/__tests__/recurrenceUtils.test.js +193 -0
  12. package/dist/utils/__tests__/recurrenceUtils.test.js.map +1 -0
  13. package/dist/utils/discountEngine/bestPriceResolver.d.ts +31 -0
  14. package/dist/utils/discountEngine/bestPriceResolver.d.ts.map +1 -0
  15. package/dist/utils/discountEngine/bestPriceResolver.js +147 -0
  16. package/dist/utils/discountEngine/bestPriceResolver.js.map +1 -0
  17. package/dist/utils/discountEngine/discountCalculator.d.ts +56 -0
  18. package/dist/utils/discountEngine/discountCalculator.d.ts.map +1 -0
  19. package/dist/utils/discountEngine/discountCalculator.js +219 -0
  20. package/dist/utils/discountEngine/discountCalculator.js.map +1 -0
  21. package/dist/utils/discountEngine/eligibilityValidator.d.ts +36 -0
  22. package/dist/utils/discountEngine/eligibilityValidator.d.ts.map +1 -0
  23. package/dist/utils/discountEngine/eligibilityValidator.js +189 -0
  24. package/dist/utils/discountEngine/eligibilityValidator.js.map +1 -0
  25. package/dist/utils/discountEngine/index.d.ts +4 -0
  26. package/dist/utils/discountEngine/index.d.ts.map +1 -0
  27. package/dist/utils/discountEngine/index.js +5 -0
  28. package/dist/utils/discountEngine/index.js.map +1 -0
  29. package/dist/utils/recurrenceUtils.d.ts.map +1 -1
  30. package/dist/utils/recurrenceUtils.js +17 -4
  31. package/dist/utils/recurrenceUtils.js.map +1 -1
  32. package/package.json +1 -1
  33. package/prisma/schema.prisma +448 -118
  34. package/src/definitions.ts +2 -1
  35. package/src/index.ts +2 -0
  36. package/src/utils/discountEngine/bestPriceResolver.ts +212 -0
  37. package/src/utils/discountEngine/discountCalculator.ts +281 -0
  38. package/src/utils/discountEngine/eligibilityValidator.ts +256 -0
  39. package/src/utils/discountEngine/index.ts +5 -0
  40. package/src/utils/recurrenceUtils.ts +20 -4
@@ -0,0 +1,256 @@
1
+ import { BashEventPromoCode, SpecialOffer, SpecialOfferType } from "@prisma/client";
2
+
3
+ export interface EligibilityResult {
4
+ eligible: boolean;
5
+ reason?: string;
6
+ }
7
+
8
+ /**
9
+ * Validate if a user is eligible to use a promo code
10
+ */
11
+ export const validatePromoCodeEligibility = (
12
+ promoCode: BashEventPromoCode,
13
+ userId: string,
14
+ usersWhoUsedCode: string[] = []
15
+ ): EligibilityResult => {
16
+ // Check if expired
17
+ if (promoCode.redeemBy && new Date() > promoCode.redeemBy) {
18
+ return {
19
+ eligible: false,
20
+ reason: `This promo code expired on ${promoCode.redeemBy.toLocaleDateString()}`,
21
+ };
22
+ }
23
+
24
+ // Check max redemptions
25
+ if (promoCode.maxRedemptions && usersWhoUsedCode.length >= promoCode.maxRedemptions) {
26
+ return {
27
+ eligible: false,
28
+ reason: "This promo code has reached its maximum number of uses",
29
+ };
30
+ }
31
+
32
+ // Check if user already used this code
33
+ if (usersWhoUsedCode.includes(userId)) {
34
+ return {
35
+ eligible: false,
36
+ reason: "You have already used this promo code",
37
+ };
38
+ }
39
+
40
+ return { eligible: true };
41
+ };
42
+
43
+ /**
44
+ * Validate if a user is eligible for a special offer
45
+ */
46
+ export const validateOfferEligibility = (
47
+ offer: SpecialOffer,
48
+ userId: string,
49
+ quantity: number,
50
+ currentDate: Date,
51
+ userRedemptionCount: number = 0
52
+ ): EligibilityResult => {
53
+ // Check if offer is active
54
+ if (!offer.isActive) {
55
+ return {
56
+ eligible: false,
57
+ reason: "This offer is no longer active",
58
+ };
59
+ }
60
+
61
+ // Check start date
62
+ if (offer.startDate && currentDate < offer.startDate) {
63
+ return {
64
+ eligible: false,
65
+ reason: `This offer starts on ${offer.startDate.toLocaleDateString()}`,
66
+ };
67
+ }
68
+
69
+ // Check end date
70
+ if (offer.endDate && currentDate > offer.endDate) {
71
+ return {
72
+ eligible: false,
73
+ reason: `This offer ended on ${offer.endDate.toLocaleDateString()}`,
74
+ };
75
+ }
76
+
77
+ // Check early bird cutoff
78
+ if (offer.availableUntil && currentDate > offer.availableUntil) {
79
+ return {
80
+ eligible: false,
81
+ reason: "The early bird period has ended",
82
+ };
83
+ }
84
+
85
+ // Check price increase countdown
86
+ if (offer.priceIncreasesAt && currentDate > offer.priceIncreasesAt) {
87
+ return {
88
+ eligible: false,
89
+ reason: "This special pricing is no longer available",
90
+ };
91
+ }
92
+
93
+ // Check max redemptions (total)
94
+ if (offer.maxRedemptions && offer.currentRedemptions >= offer.maxRedemptions) {
95
+ return {
96
+ eligible: false,
97
+ reason: "This offer has reached its maximum number of uses",
98
+ };
99
+ }
100
+
101
+ // Check max redemptions per user
102
+ if (offer.maxPerUser && userRedemptionCount >= offer.maxPerUser) {
103
+ return {
104
+ eligible: false,
105
+ reason: "You have reached the maximum number of times you can use this offer",
106
+ };
107
+ }
108
+
109
+ // Check limited quantity
110
+ if (offer.limitedQuantity && offer.currentRedemptions >= offer.limitedQuantity) {
111
+ return {
112
+ eligible: false,
113
+ reason: "This offer is sold out",
114
+ };
115
+ }
116
+
117
+ // Check quantity requirements for specific offer types
118
+ if (offer.offerType === SpecialOfferType.GROUP_DISCOUNT) {
119
+ if (offer.minGroupSize && quantity < offer.minGroupSize) {
120
+ return {
121
+ eligible: false,
122
+ reason: `You must purchase at least ${offer.minGroupSize} tickets to qualify for this group discount`,
123
+ };
124
+ }
125
+ if (offer.maxGroupSize && quantity > offer.maxGroupSize) {
126
+ return {
127
+ eligible: false,
128
+ reason: `This group discount only applies to groups of ${offer.maxGroupSize} or fewer`,
129
+ };
130
+ }
131
+ }
132
+
133
+ if (offer.offerType === SpecialOfferType.BOGO) {
134
+ const buyQuantity = offer.buyQuantity || 1;
135
+ const getQuantity = offer.getQuantity || 1;
136
+ const minQuantity = buyQuantity + getQuantity;
137
+
138
+ if (quantity < minQuantity) {
139
+ return {
140
+ eligible: false,
141
+ reason: `You must purchase at least ${minQuantity} tickets for this Buy ${buyQuantity} Get ${getQuantity} offer`,
142
+ };
143
+ }
144
+ }
145
+
146
+ // All checks passed
147
+ return { eligible: true };
148
+ };
149
+
150
+ /**
151
+ * Validate if an offer can stack with promo codes
152
+ */
153
+ export const validateOfferStacking = (
154
+ offer: SpecialOffer,
155
+ hasPromoCode: boolean,
156
+ hasOtherOffer: boolean
157
+ ): EligibilityResult => {
158
+ if (hasPromoCode && !offer.canStackWithPromoCodes) {
159
+ return {
160
+ eligible: false,
161
+ reason: "This offer cannot be combined with promo codes",
162
+ };
163
+ }
164
+
165
+ if (hasOtherOffer && !offer.canStackWithOtherOffers) {
166
+ return {
167
+ eligible: false,
168
+ reason: "This offer cannot be combined with other offers",
169
+ };
170
+ }
171
+
172
+ return { eligible: true };
173
+ };
174
+
175
+ /**
176
+ * Validate loyalty offer requirements
177
+ */
178
+ export const validateLoyaltyRequirements = (
179
+ offer: SpecialOffer,
180
+ userEventAttendanceCount: number,
181
+ isHostEvent: boolean
182
+ ): EligibilityResult => {
183
+ if (offer.offerType !== SpecialOfferType.LOYALTY) {
184
+ return { eligible: true };
185
+ }
186
+
187
+ // Check event attendance requirement
188
+ if (offer.requiresEventAttendance && userEventAttendanceCount < offer.requiresEventAttendance) {
189
+ return {
190
+ eligible: false,
191
+ reason: `You must have attended at least ${offer.requiresEventAttendance} events to qualify for this loyalty offer`,
192
+ };
193
+ }
194
+
195
+ // Check if it's host-specific loyalty
196
+ if (offer.hostSpecificLoyalty && !isHostEvent) {
197
+ return {
198
+ eligible: false,
199
+ reason: "This loyalty offer only applies to events by this host",
200
+ };
201
+ }
202
+
203
+ return { eligible: true };
204
+ };
205
+
206
+ /**
207
+ * Batch validate multiple offers for a user
208
+ */
209
+ export const validateMultipleOffers = (
210
+ offers: SpecialOffer[],
211
+ userId: string,
212
+ quantity: number,
213
+ currentDate: Date,
214
+ userRedemptions: Map<string, number>
215
+ ): Map<string, EligibilityResult> => {
216
+ const results = new Map<string, EligibilityResult>();
217
+
218
+ offers.forEach(offer => {
219
+ const userCount = userRedemptions.get(offer.id) || 0;
220
+ const result = validateOfferEligibility(offer, userId, quantity, currentDate, userCount);
221
+ results.set(offer.id, result);
222
+ });
223
+
224
+ return results;
225
+ };
226
+
227
+ /**
228
+ * Get time remaining until offer expires (for countdown timers)
229
+ */
230
+ export const getTimeRemaining = (offer: SpecialOffer, currentDate: Date): {
231
+ expired: boolean;
232
+ days: number;
233
+ hours: number;
234
+ minutes: number;
235
+ seconds: number;
236
+ } => {
237
+ const expiryDate = offer.endDate || offer.priceIncreasesAt || offer.availableUntil;
238
+
239
+ if (!expiryDate) {
240
+ return { expired: false, days: 999, hours: 0, minutes: 0, seconds: 0 };
241
+ }
242
+
243
+ const diff = expiryDate.getTime() - currentDate.getTime();
244
+
245
+ if (diff <= 0) {
246
+ return { expired: true, days: 0, hours: 0, minutes: 0, seconds: 0 };
247
+ }
248
+
249
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
250
+ const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
251
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
252
+ const seconds = Math.floor((diff % (1000 * 60)) / 1000);
253
+
254
+ return { expired: false, days, hours, minutes, seconds };
255
+ };
256
+
@@ -0,0 +1,5 @@
1
+ // Unified Discount Engine - All discount calculation and validation logic
2
+ export * from './discountCalculator';
3
+ export * from './bestPriceResolver';
4
+ export * from './eligibilityValidator';
5
+
@@ -74,6 +74,11 @@ function getRecurringBashEventPossibleDatesTimesInternal(
74
74
  console.error(`Unsupported frequency: ${recurrence.frequency}`);
75
75
  }
76
76
 
77
+ // If repeatCount is specified, limit to that many occurrences
78
+ if (recurrence.repeatCount && recurrence.repeatCount > 0) {
79
+ recurrenceDates = recurrenceDates.slice(0, recurrence.repeatCount);
80
+ }
81
+
77
82
  return recurrenceDates
78
83
  .map((date: DateTime): string =>
79
84
  date.toFormat(LUXON_DATETIME_FORMAT_ISO_LIKE)
@@ -108,7 +113,12 @@ function getDailyRecurringDates(
108
113
  }
109
114
 
110
115
  const interval = recurrence.interval ?? 1;
111
- const recurrenceEnds = dateTimeFromDate(recurrence.ends).endOf("day");
116
+
117
+ // Default to 1 year from now if no end date specified
118
+ const recurrenceEnds = recurrence.ends
119
+ ? dateTimeFromDate(recurrence.ends).endOf("day")
120
+ : DateTime.local().plus({ years: 1 }).endOf("day");
121
+
112
122
  const numberOfDays = recurrenceEnds.diff(beginningDateTime, "days").days;
113
123
  const numberOfOccurrences = Math.ceil(numberOfDays / interval);
114
124
 
@@ -141,7 +151,9 @@ function getWeeklyRecurringDates(
141
151
  }
142
152
 
143
153
  const interval = recurrence.interval ?? 1;
144
- const recurrenceEnds = dateTimeFromDate(recurrence.ends).endOf("day"); // Get the end of the day to not miss the last day
154
+ const recurrenceEnds = recurrence.ends
155
+ ? dateTimeFromDate(recurrence.ends).endOf("day")
156
+ : DateTime.local().plus({ years: 1 }).endOf("day");
145
157
  const numberOfDays = recurrenceEnds.diff(beginningDateTime, "days").days;
146
158
  const numberOfWeeks = Math.ceil(numberOfDays / 7 / interval); // Get the number of days and then round up so that we include the ending date
147
159
 
@@ -203,7 +215,9 @@ function getMonthlyRecurringDates(
203
215
  }
204
216
 
205
217
  const interval = recurrence.interval ?? 1;
206
- const recurrenceEnds = dateTimeFromDate(recurrence.ends).endOf("day");
218
+ const recurrenceEnds = recurrence.ends
219
+ ? dateTimeFromDate(recurrence.ends).endOf("day")
220
+ : DateTime.local().plus({ years: 1 }).endOf("day");
207
221
  const dayOfMonth = recurrence.repeatOnDayOfMonth ?? beginningDateTime.day;
208
222
 
209
223
  const recurrenceDates: DateTime[] = [];
@@ -248,7 +262,9 @@ function getYearlyRecurringDates(
248
262
  }
249
263
 
250
264
  const interval = recurrence.interval ?? 1;
251
- const recurrenceEnds = dateTimeFromDate(recurrence.ends).endOf("day");
265
+ const recurrenceEnds = recurrence.ends
266
+ ? dateTimeFromDate(recurrence.ends).endOf("day")
267
+ : DateTime.local().plus({ years: 20 }).endOf("day");
252
268
 
253
269
  // Use the repeatYearlyDate if provided, otherwise use the beginning date
254
270
  const baseDate = recurrence.repeatYearlyDate