@bash-app/bash-common 30.113.0 → 30.115.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/definitions.d.ts +2 -2
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +2 -2
- package/dist/definitions.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/__tests__/recurrenceUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/recurrenceUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/recurrenceUtils.test.js +193 -0
- package/dist/utils/__tests__/recurrenceUtils.test.js.map +1 -0
- package/dist/utils/discountEngine/bestPriceResolver.d.ts +31 -0
- package/dist/utils/discountEngine/bestPriceResolver.d.ts.map +1 -0
- package/dist/utils/discountEngine/bestPriceResolver.js +147 -0
- package/dist/utils/discountEngine/bestPriceResolver.js.map +1 -0
- package/dist/utils/discountEngine/discountCalculator.d.ts +56 -0
- package/dist/utils/discountEngine/discountCalculator.d.ts.map +1 -0
- package/dist/utils/discountEngine/discountCalculator.js +219 -0
- package/dist/utils/discountEngine/discountCalculator.js.map +1 -0
- package/dist/utils/discountEngine/eligibilityValidator.d.ts +36 -0
- package/dist/utils/discountEngine/eligibilityValidator.d.ts.map +1 -0
- package/dist/utils/discountEngine/eligibilityValidator.js +189 -0
- package/dist/utils/discountEngine/eligibilityValidator.js.map +1 -0
- package/dist/utils/discountEngine/index.d.ts +4 -0
- package/dist/utils/discountEngine/index.d.ts.map +1 -0
- package/dist/utils/discountEngine/index.js +5 -0
- package/dist/utils/discountEngine/index.js.map +1 -0
- package/dist/utils/recurrenceUtils.d.ts.map +1 -1
- package/dist/utils/recurrenceUtils.js +17 -4
- package/dist/utils/recurrenceUtils.js.map +1 -1
- package/package.json +5 -5
- package/prisma/schema.prisma +387 -81
- package/src/definitions.ts +2 -1
- package/src/index.ts +2 -0
- package/src/utils/discountEngine/bestPriceResolver.ts +212 -0
- package/src/utils/discountEngine/discountCalculator.ts +281 -0
- package/src/utils/discountEngine/eligibilityValidator.ts +256 -0
- package/src/utils/discountEngine/index.ts +5 -0
- 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
|
+
|
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|