@bash-app/bash-common 30.219.0 → 30.221.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 +9 -3
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +19 -1
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +161 -1
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js +20 -0
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/membershipDefinitions.d.ts +4 -2
- package/dist/membershipDefinitions.d.ts.map +1 -1
- package/dist/membershipDefinitions.js +9 -2
- package/dist/membershipDefinitions.js.map +1 -1
- package/dist/partnerStoreTypes.d.ts +37 -0
- package/dist/partnerStoreTypes.d.ts.map +1 -0
- package/dist/partnerStoreTypes.js +2 -0
- package/dist/partnerStoreTypes.js.map +1 -0
- package/dist/utils/__tests__/paymentUtils.test.js +179 -1
- package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
- package/dist/utils/__tests__/urlUtils.externalLink.test.d.ts +2 -0
- package/dist/utils/__tests__/urlUtils.externalLink.test.d.ts.map +1 -0
- package/dist/utils/__tests__/urlUtils.externalLink.test.js +29 -0
- package/dist/utils/__tests__/urlUtils.externalLink.test.js.map +1 -0
- package/dist/utils/paymentUtils.d.ts +66 -1
- package/dist/utils/paymentUtils.d.ts.map +1 -1
- package/dist/utils/paymentUtils.js +104 -3
- package/dist/utils/paymentUtils.js.map +1 -1
- package/dist/utils/urlUtils.d.ts +11 -0
- package/dist/utils/urlUtils.d.ts.map +1 -1
- package/dist/utils/urlUtils.js +40 -0
- package/dist/utils/urlUtils.js.map +1 -1
- package/package.json +2 -2
- package/prisma/schema.prisma +213 -2
- package/src/definitions.ts +29 -2
- package/src/extendedSchemas.ts +42 -0
- package/src/index.ts +6 -0
- package/src/membershipDefinitions.ts +10 -2
- package/src/partnerStoreTypes.ts +36 -0
- package/src/utils/__tests__/paymentUtils.test.ts +239 -0
- package/src/utils/__tests__/urlUtils.externalLink.test.ts +36 -0
- package/src/utils/paymentUtils.ts +156 -1
- package/src/utils/urlUtils.ts +45 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Testing business logic without database dependencies
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { BashPassTier, MembershipTier } from "@prisma/client";
|
|
6
7
|
import {
|
|
7
8
|
calculateDiscountFromPromoCode,
|
|
8
9
|
convertCentsToDollars,
|
|
@@ -15,6 +16,14 @@ import {
|
|
|
15
16
|
calculateTotalBashPoints,
|
|
16
17
|
calculateStripeDonationFee,
|
|
17
18
|
calculateServiceFeeWithMembershipDiscount,
|
|
19
|
+
calculateBashPlatformFeeCents,
|
|
20
|
+
splitBashPlatformFeeForTicketCheckout,
|
|
21
|
+
computeTicketApplicationFeeAmountCents,
|
|
22
|
+
userHasActiveBashPass,
|
|
23
|
+
getEffectiveBashPassTier,
|
|
24
|
+
getBashPassMonthlyCapRemaining,
|
|
25
|
+
higherBashPassTier,
|
|
26
|
+
membershipGrantsBashPassTier,
|
|
18
27
|
} from "../paymentUtils.js";
|
|
19
28
|
|
|
20
29
|
describe("PaymentUtils - Core Functions", () => {
|
|
@@ -93,6 +102,67 @@ describe("PaymentUtils - Core Functions", () => {
|
|
|
93
102
|
test("calculateStripeFee (dollars) aligns with calculateStripeProcessingFeeCents", () => {
|
|
94
103
|
expect(calculateStripeFee(100)).toBe(3.2);
|
|
95
104
|
});
|
|
105
|
+
|
|
106
|
+
test("calculateBashPlatformFeeCents floors 3% of gross", () => {
|
|
107
|
+
expect(calculateBashPlatformFeeCents(10000, 0.03)).toBe(300);
|
|
108
|
+
expect(calculateBashPlatformFeeCents(100, 0.03)).toBe(3);
|
|
109
|
+
expect(calculateBashPlatformFeeCents(0, 0.03)).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("splitBashPlatformFeeForTicketCheckout respects feeHandling and BashPass", () => {
|
|
113
|
+
const base = {
|
|
114
|
+
grossCents: 10000,
|
|
115
|
+
platformFeeEnabled: true,
|
|
116
|
+
platformFeeRate: 0.03,
|
|
117
|
+
};
|
|
118
|
+
expect(splitBashPlatformFeeForTicketCheckout({ ...base, feeHandling: "HostAbsorbs", bashPassWaivesGuestFee: false })).toEqual({
|
|
119
|
+
totalPlatformCents: 300,
|
|
120
|
+
guestPlatformLineCents: 0,
|
|
121
|
+
});
|
|
122
|
+
expect(splitBashPlatformFeeForTicketCheckout({ ...base, feeHandling: "GuestPays", bashPassWaivesGuestFee: false })).toEqual({
|
|
123
|
+
totalPlatformCents: 300,
|
|
124
|
+
guestPlatformLineCents: 300,
|
|
125
|
+
});
|
|
126
|
+
expect(splitBashPlatformFeeForTicketCheckout({ ...base, feeHandling: "Split", bashPassWaivesGuestFee: false })).toEqual({
|
|
127
|
+
totalPlatformCents: 300,
|
|
128
|
+
guestPlatformLineCents: 150,
|
|
129
|
+
});
|
|
130
|
+
expect(splitBashPlatformFeeForTicketCheckout({ ...base, feeHandling: "GuestPays", bashPassWaivesGuestFee: true })).toEqual({
|
|
131
|
+
totalPlatformCents: 300,
|
|
132
|
+
guestPlatformLineCents: 0,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("computeTicketApplicationFeeAmountCents sums Stripe + Bash platform into application fee", () => {
|
|
137
|
+
const r = computeTicketApplicationFeeAmountCents({
|
|
138
|
+
discountedSubtotalCents: 10000,
|
|
139
|
+
platformFeeEnabled: true,
|
|
140
|
+
platformFeeRate: 0.03,
|
|
141
|
+
feeHandling: "HostAbsorbs",
|
|
142
|
+
bashPassWaivesGuestFee: false,
|
|
143
|
+
});
|
|
144
|
+
expect(r.totalChargedCents).toBe(10000);
|
|
145
|
+
expect(r.bashPlatformTotalCents).toBe(300);
|
|
146
|
+
expect(r.stripeProcessingCents).toBe(calculateStripeProcessingFeeCents(10000));
|
|
147
|
+
expect(r.applicationFeeAmountCents).toBe(r.stripeProcessingCents + 300);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("userHasActiveBashPass", () => {
|
|
151
|
+
expect(userHasActiveBashPass({ bashPassStripeSubscriptionId: null, bashPassCurrentPeriodEnd: null })).toBe(false);
|
|
152
|
+
expect(userHasActiveBashPass({ bashPassStripeSubscriptionId: "sub_1", bashPassCurrentPeriodEnd: null })).toBe(true);
|
|
153
|
+
expect(
|
|
154
|
+
userHasActiveBashPass({
|
|
155
|
+
bashPassStripeSubscriptionId: "sub_1",
|
|
156
|
+
bashPassCurrentPeriodEnd: new Date(Date.now() + 86400000),
|
|
157
|
+
})
|
|
158
|
+
).toBe(true);
|
|
159
|
+
expect(
|
|
160
|
+
userHasActiveBashPass({
|
|
161
|
+
bashPassStripeSubscriptionId: "sub_1",
|
|
162
|
+
bashPassCurrentPeriodEnd: new Date(Date.now() - 86400000),
|
|
163
|
+
})
|
|
164
|
+
).toBe(false);
|
|
165
|
+
});
|
|
96
166
|
});
|
|
97
167
|
|
|
98
168
|
describe("Ticket total price (USD)", () => {
|
|
@@ -173,4 +243,173 @@ describe("PaymentUtils - Core Functions", () => {
|
|
|
173
243
|
expect(typeof fee).toBe("number");
|
|
174
244
|
});
|
|
175
245
|
});
|
|
246
|
+
|
|
247
|
+
describe("BashPass tier and monthly cap", () => {
|
|
248
|
+
const future = new Date(Date.now() + 86400000 * 30);
|
|
249
|
+
const past = new Date(Date.now() - 86400000);
|
|
250
|
+
|
|
251
|
+
test("higherBashPassTier picks the higher rank", () => {
|
|
252
|
+
expect(higherBashPassTier(BashPassTier.LITE, BashPassTier.STANDARD)).toBe(BashPassTier.STANDARD);
|
|
253
|
+
expect(higherBashPassTier(BashPassTier.STANDARD, BashPassTier.LITE)).toBe(BashPassTier.STANDARD);
|
|
254
|
+
expect(higherBashPassTier(BashPassTier.STANDARD, BashPassTier.UNLIMITED)).toBe(BashPassTier.UNLIMITED);
|
|
255
|
+
expect(higherBashPassTier(null, BashPassTier.LITE)).toBe(BashPassTier.LITE);
|
|
256
|
+
expect(higherBashPassTier(BashPassTier.LITE, null)).toBe(BashPassTier.LITE);
|
|
257
|
+
expect(higherBashPassTier(null, null)).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("membershipGrantsBashPassTier returns null for Basic or expired membership", () => {
|
|
261
|
+
expect(
|
|
262
|
+
membershipGrantsBashPassTier({
|
|
263
|
+
membershipTier: MembershipTier.Basic,
|
|
264
|
+
membershipExpiresAt: future,
|
|
265
|
+
})
|
|
266
|
+
).toBeNull();
|
|
267
|
+
expect(
|
|
268
|
+
membershipGrantsBashPassTier({
|
|
269
|
+
membershipTier: MembershipTier.Premium,
|
|
270
|
+
membershipExpiresAt: past,
|
|
271
|
+
})
|
|
272
|
+
).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("membershipGrantsBashPassTier maps Premium/Pro/Elite/Legend", () => {
|
|
276
|
+
expect(
|
|
277
|
+
membershipGrantsBashPassTier({ membershipTier: MembershipTier.Premium, membershipExpiresAt: future })
|
|
278
|
+
).toBe(BashPassTier.LITE);
|
|
279
|
+
expect(
|
|
280
|
+
membershipGrantsBashPassTier({ membershipTier: MembershipTier.Pro, membershipExpiresAt: future })
|
|
281
|
+
).toBe(BashPassTier.STANDARD);
|
|
282
|
+
expect(
|
|
283
|
+
membershipGrantsBashPassTier({ membershipTier: MembershipTier.Elite, membershipExpiresAt: future })
|
|
284
|
+
).toBe(BashPassTier.UNLIMITED);
|
|
285
|
+
expect(
|
|
286
|
+
membershipGrantsBashPassTier({ membershipTier: MembershipTier.Legend, membershipExpiresAt: future })
|
|
287
|
+
).toBe(BashPassTier.UNLIMITED);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("getEffectiveBashPassTier uses standalone subscription when active", () => {
|
|
291
|
+
expect(
|
|
292
|
+
getEffectiveBashPassTier({
|
|
293
|
+
bashPassTier: BashPassTier.STANDARD,
|
|
294
|
+
bashPassStripeSubscriptionId: "sub_1",
|
|
295
|
+
bashPassCurrentPeriodEnd: future,
|
|
296
|
+
membershipTier: MembershipTier.Basic,
|
|
297
|
+
membershipExpiresAt: null,
|
|
298
|
+
})
|
|
299
|
+
).toBe(BashPassTier.STANDARD);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("getEffectiveBashPassTier uses membership when no standalone", () => {
|
|
303
|
+
expect(
|
|
304
|
+
getEffectiveBashPassTier({
|
|
305
|
+
bashPassTier: null,
|
|
306
|
+
bashPassStripeSubscriptionId: null,
|
|
307
|
+
bashPassCurrentPeriodEnd: null,
|
|
308
|
+
membershipTier: MembershipTier.Pro,
|
|
309
|
+
membershipExpiresAt: future,
|
|
310
|
+
})
|
|
311
|
+
).toBe(BashPassTier.STANDARD);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("getEffectiveBashPassTier takes higher of membership and standalone", () => {
|
|
315
|
+
expect(
|
|
316
|
+
getEffectiveBashPassTier({
|
|
317
|
+
bashPassTier: BashPassTier.LITE,
|
|
318
|
+
bashPassStripeSubscriptionId: "sub_1",
|
|
319
|
+
bashPassCurrentPeriodEnd: future,
|
|
320
|
+
membershipTier: MembershipTier.Pro,
|
|
321
|
+
membershipExpiresAt: future,
|
|
322
|
+
})
|
|
323
|
+
).toBe(BashPassTier.STANDARD);
|
|
324
|
+
expect(
|
|
325
|
+
getEffectiveBashPassTier({
|
|
326
|
+
bashPassTier: BashPassTier.UNLIMITED,
|
|
327
|
+
bashPassStripeSubscriptionId: "sub_1",
|
|
328
|
+
bashPassCurrentPeriodEnd: future,
|
|
329
|
+
membershipTier: MembershipTier.Premium,
|
|
330
|
+
membershipExpiresAt: future,
|
|
331
|
+
})
|
|
332
|
+
).toBe(BashPassTier.UNLIMITED);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("getEffectiveBashPassTier returns null when neither applies", () => {
|
|
336
|
+
expect(
|
|
337
|
+
getEffectiveBashPassTier({
|
|
338
|
+
bashPassTier: null,
|
|
339
|
+
bashPassStripeSubscriptionId: null,
|
|
340
|
+
bashPassCurrentPeriodEnd: null,
|
|
341
|
+
membershipTier: MembershipTier.Basic,
|
|
342
|
+
membershipExpiresAt: null,
|
|
343
|
+
})
|
|
344
|
+
).toBeNull();
|
|
345
|
+
expect(
|
|
346
|
+
getEffectiveBashPassTier({
|
|
347
|
+
bashPassTier: BashPassTier.LITE,
|
|
348
|
+
bashPassStripeSubscriptionId: null,
|
|
349
|
+
bashPassCurrentPeriodEnd: null,
|
|
350
|
+
membershipTier: MembershipTier.Basic,
|
|
351
|
+
membershipExpiresAt: null,
|
|
352
|
+
})
|
|
353
|
+
).toBeNull();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("getBashPassMonthlyCapRemaining is 0 without effective tier", () => {
|
|
357
|
+
expect(
|
|
358
|
+
getBashPassMonthlyCapRemaining({
|
|
359
|
+
bashPassTier: null,
|
|
360
|
+
bashPassEventsUsedThisMonth: 0,
|
|
361
|
+
bashPassStripeSubscriptionId: null,
|
|
362
|
+
bashPassCurrentPeriodEnd: null,
|
|
363
|
+
membershipTier: MembershipTier.Basic,
|
|
364
|
+
membershipExpiresAt: null,
|
|
365
|
+
})
|
|
366
|
+
).toBe(0);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("getBashPassMonthlyCapRemaining is null for UNLIMITED effective tier", () => {
|
|
370
|
+
expect(
|
|
371
|
+
getBashPassMonthlyCapRemaining({
|
|
372
|
+
bashPassTier: null,
|
|
373
|
+
bashPassEventsUsedThisMonth: 100,
|
|
374
|
+
bashPassStripeSubscriptionId: null,
|
|
375
|
+
bashPassCurrentPeriodEnd: null,
|
|
376
|
+
membershipTier: MembershipTier.Elite,
|
|
377
|
+
membershipExpiresAt: future,
|
|
378
|
+
})
|
|
379
|
+
).toBeNull();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("getBashPassMonthlyCapRemaining subtracts usage for capped tiers", () => {
|
|
383
|
+
expect(
|
|
384
|
+
getBashPassMonthlyCapRemaining({
|
|
385
|
+
bashPassTier: BashPassTier.LITE,
|
|
386
|
+
bashPassEventsUsedThisMonth: 2,
|
|
387
|
+
bashPassStripeSubscriptionId: "sub_1",
|
|
388
|
+
bashPassCurrentPeriodEnd: future,
|
|
389
|
+
membershipTier: MembershipTier.Basic,
|
|
390
|
+
membershipExpiresAt: null,
|
|
391
|
+
})
|
|
392
|
+
).toBe(2);
|
|
393
|
+
expect(
|
|
394
|
+
getBashPassMonthlyCapRemaining({
|
|
395
|
+
bashPassTier: BashPassTier.LITE,
|
|
396
|
+
bashPassEventsUsedThisMonth: 4,
|
|
397
|
+
bashPassStripeSubscriptionId: "sub_1",
|
|
398
|
+
bashPassCurrentPeriodEnd: future,
|
|
399
|
+
membershipTier: MembershipTier.Basic,
|
|
400
|
+
membershipExpiresAt: null,
|
|
401
|
+
})
|
|
402
|
+
).toBe(0);
|
|
403
|
+
expect(
|
|
404
|
+
getBashPassMonthlyCapRemaining({
|
|
405
|
+
bashPassTier: BashPassTier.STANDARD,
|
|
406
|
+
bashPassEventsUsedThisMonth: 8,
|
|
407
|
+
bashPassStripeSubscriptionId: "sub_1",
|
|
408
|
+
bashPassCurrentPeriodEnd: future,
|
|
409
|
+
membershipTier: MembershipTier.Basic,
|
|
410
|
+
membershipExpiresAt: null,
|
|
411
|
+
})
|
|
412
|
+
).toBe(0);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
176
415
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { toExternalLinkHref } from "../urlUtils.js";
|
|
2
|
+
|
|
3
|
+
describe("toExternalLinkHref", () => {
|
|
4
|
+
it("returns # for empty or null", () => {
|
|
5
|
+
expect(toExternalLinkHref(null)).toBe("#");
|
|
6
|
+
expect(toExternalLinkHref(undefined)).toBe("#");
|
|
7
|
+
expect(toExternalLinkHref("")).toBe("#");
|
|
8
|
+
expect(toExternalLinkHref(" ")).toBe("#");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("leaves http(s) URLs unchanged", () => {
|
|
12
|
+
expect(toExternalLinkHref("https://example.com")).toBe("https://example.com");
|
|
13
|
+
expect(toExternalLinkHref("http://example.com/path")).toBe("http://example.com/path");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("prepends https to bare hostnames so browsers do not treat them as relative paths", () => {
|
|
17
|
+
expect(toExternalLinkHref("aavprod.com")).toBe("https://aavprod.com");
|
|
18
|
+
expect(toExternalLinkHref("www.foo.com/bar")).toBe("https://www.foo.com/bar");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("preserves mailto and tel", () => {
|
|
22
|
+
expect(toExternalLinkHref("mailto:a@b.com")).toBe("mailto:a@b.com");
|
|
23
|
+
expect(toExternalLinkHref("tel:+15555551212")).toBe("tel:+15555551212");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("maps Instagram @handle to instagram.com URL", () => {
|
|
27
|
+
expect(toExternalLinkHref("@a_prod_av")).toBe(
|
|
28
|
+
"https://www.instagram.com/a_prod_av/"
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("blocks obvious unsafe schemes", () => {
|
|
33
|
+
expect(toExternalLinkHref("javascript:alert(1)")).toBe("#");
|
|
34
|
+
expect(toExternalLinkHref("data:text/html,hi")).toBe("#");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BashEventPromoCode,
|
|
3
|
+
BashPassTier,
|
|
4
|
+
MembershipTier,
|
|
5
|
+
TicketTier,
|
|
6
|
+
VenuePricingPlan as VenuePricingPlanOption,
|
|
7
|
+
} from "@prisma/client";
|
|
2
8
|
import {
|
|
3
9
|
BASH_EVENT_FEES,
|
|
10
|
+
MEMBERSHIP_BASH_PASS_TIER,
|
|
4
11
|
PARTNER_PAYMENT_FEES,
|
|
5
12
|
SERVICE_BOOKING_FEES,
|
|
6
13
|
STRIPE_PROCESSING_FEE,
|
|
7
14
|
} from "../membershipDefinitions.js";
|
|
8
15
|
import {
|
|
16
|
+
BASH_PASS_EVENT_CAPS,
|
|
17
|
+
BASH_PASS_TIER_RANK,
|
|
9
18
|
NumberOfTicketsForDate,
|
|
10
19
|
PRICE_DOLLARS_AND_CENTS_RATIO,
|
|
11
20
|
} from "../definitions.js";
|
|
@@ -221,6 +230,152 @@ export function calculatePartnerFeeWithMembershipDiscount(
|
|
|
221
230
|
return parseFloat(calculatedFee.toFixed(2));
|
|
222
231
|
}
|
|
223
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Bash **platform** take rate on ticket gross (e.g. 3%). Separate from Stripe card processing pass-through.
|
|
235
|
+
*/
|
|
236
|
+
export function calculateBashPlatformFeeCents(grossCents: number, rate: number): number {
|
|
237
|
+
if (grossCents <= 0 || rate <= 0) return 0;
|
|
238
|
+
return Math.floor(grossCents * rate);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export type BashPlatformTicketFeeSplit = {
|
|
242
|
+
totalPlatformCents: number;
|
|
243
|
+
/** Shown as a separate checkout line when greater than 0 */
|
|
244
|
+
guestPlatformLineCents: number;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Split the Bash platform fee between guest line items vs host absorption using `feeHandling`.
|
|
249
|
+
* BashPass waives the **guest-visible** portion only; the host still funds the full platform fee when the guest pays nothing extra.
|
|
250
|
+
*/
|
|
251
|
+
export function splitBashPlatformFeeForTicketCheckout(args: {
|
|
252
|
+
grossCents: number;
|
|
253
|
+
platformFeeEnabled: boolean;
|
|
254
|
+
platformFeeRate: number;
|
|
255
|
+
feeHandling: string | null | undefined;
|
|
256
|
+
bashPassWaivesGuestFee: boolean;
|
|
257
|
+
}): BashPlatformTicketFeeSplit {
|
|
258
|
+
const { grossCents, platformFeeEnabled, platformFeeRate, feeHandling, bashPassWaivesGuestFee } = args;
|
|
259
|
+
const total = platformFeeEnabled
|
|
260
|
+
? calculateBashPlatformFeeCents(grossCents, platformFeeRate)
|
|
261
|
+
: 0;
|
|
262
|
+
if (total <= 0) {
|
|
263
|
+
return { totalPlatformCents: 0, guestPlatformLineCents: 0 };
|
|
264
|
+
}
|
|
265
|
+
const mode = feeHandling ?? "HostAbsorbs";
|
|
266
|
+
let guestLine = 0;
|
|
267
|
+
if (mode === "GuestPays") {
|
|
268
|
+
guestLine = total;
|
|
269
|
+
} else if (mode === "Split") {
|
|
270
|
+
guestLine = Math.floor(total / 2);
|
|
271
|
+
}
|
|
272
|
+
if (bashPassWaivesGuestFee && guestLine > 0) {
|
|
273
|
+
guestLine = 0;
|
|
274
|
+
}
|
|
275
|
+
return { totalPlatformCents: total, guestPlatformLineCents: guestLine };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function computeTicketApplicationFeeAmountCents(args: {
|
|
279
|
+
discountedSubtotalCents: number;
|
|
280
|
+
platformFeeEnabled: boolean;
|
|
281
|
+
platformFeeRate: number;
|
|
282
|
+
feeHandling: string | null | undefined;
|
|
283
|
+
bashPassWaivesGuestFee: boolean;
|
|
284
|
+
}): {
|
|
285
|
+
totalChargedCents: number;
|
|
286
|
+
stripeProcessingCents: number;
|
|
287
|
+
bashPlatformTotalCents: number;
|
|
288
|
+
applicationFeeAmountCents: number;
|
|
289
|
+
guestPlatformLineCents: number;
|
|
290
|
+
} {
|
|
291
|
+
const split = splitBashPlatformFeeForTicketCheckout({
|
|
292
|
+
grossCents: args.discountedSubtotalCents,
|
|
293
|
+
platformFeeEnabled: args.platformFeeEnabled,
|
|
294
|
+
platformFeeRate: args.platformFeeRate,
|
|
295
|
+
feeHandling: args.feeHandling,
|
|
296
|
+
bashPassWaivesGuestFee: args.bashPassWaivesGuestFee,
|
|
297
|
+
});
|
|
298
|
+
const guestPlatformLineCents = split.guestPlatformLineCents;
|
|
299
|
+
const bashPlatformTotalCents = split.totalPlatformCents;
|
|
300
|
+
const totalChargedCents = args.discountedSubtotalCents + guestPlatformLineCents;
|
|
301
|
+
const stripeProcessingCents = calculateStripeProcessingFeeCents(totalChargedCents);
|
|
302
|
+
const applicationFeeAmountCents = stripeProcessingCents + bashPlatformTotalCents;
|
|
303
|
+
return {
|
|
304
|
+
totalChargedCents,
|
|
305
|
+
stripeProcessingCents,
|
|
306
|
+
bashPlatformTotalCents,
|
|
307
|
+
applicationFeeAmountCents,
|
|
308
|
+
guestPlatformLineCents,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function userHasActiveBashPass(user: {
|
|
313
|
+
bashPassStripeSubscriptionId: string | null | undefined;
|
|
314
|
+
bashPassCurrentPeriodEnd: Date | null | undefined;
|
|
315
|
+
}): boolean {
|
|
316
|
+
if (!user.bashPassStripeSubscriptionId) return false;
|
|
317
|
+
if (!user.bashPassCurrentPeriodEnd) return true;
|
|
318
|
+
return user.bashPassCurrentPeriodEnd > new Date();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** When a user has both membership-included BashPass and a standalone subscription, keep the higher tier. */
|
|
322
|
+
export function higherBashPassTier(
|
|
323
|
+
a: BashPassTier | null,
|
|
324
|
+
b: BashPassTier | null
|
|
325
|
+
): BashPassTier | null {
|
|
326
|
+
if (!a && !b) return null;
|
|
327
|
+
if (!a) return b;
|
|
328
|
+
if (!b) return a;
|
|
329
|
+
return BASH_PASS_TIER_RANK[a] >= BASH_PASS_TIER_RANK[b] ? a : b;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function membershipGrantsBashPassTier(user: {
|
|
333
|
+
membershipTier: MembershipTier;
|
|
334
|
+
membershipExpiresAt: Date | null | undefined;
|
|
335
|
+
}): BashPassTier | null {
|
|
336
|
+
if (user.membershipTier === MembershipTier.Basic) return null;
|
|
337
|
+
if (user.membershipExpiresAt && user.membershipExpiresAt <= new Date()) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return MEMBERSHIP_BASH_PASS_TIER[user.membershipTier] ?? null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Effective BashPass tier from paid membership bundle and/or active standalone BashPass subscription. */
|
|
344
|
+
export function getEffectiveBashPassTier(user: {
|
|
345
|
+
bashPassTier: BashPassTier | null | undefined;
|
|
346
|
+
bashPassStripeSubscriptionId: string | null | undefined;
|
|
347
|
+
bashPassCurrentPeriodEnd: Date | null | undefined;
|
|
348
|
+
membershipTier: MembershipTier;
|
|
349
|
+
membershipExpiresAt: Date | null | undefined;
|
|
350
|
+
}): BashPassTier | null {
|
|
351
|
+
const fromMembership = membershipGrantsBashPassTier(user);
|
|
352
|
+
const fromStandalone =
|
|
353
|
+
userHasActiveBashPass(user) && user.bashPassTier ? user.bashPassTier : null;
|
|
354
|
+
return higherBashPassTier(fromMembership, fromStandalone);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Remaining BashPass redemptions this billing month.
|
|
359
|
+
* - `0` — no BashPass or cap exhausted
|
|
360
|
+
* - positive integer — spots left
|
|
361
|
+
* - `null` — unlimited tier (no monthly cap)
|
|
362
|
+
*/
|
|
363
|
+
export function getBashPassMonthlyCapRemaining(user: {
|
|
364
|
+
bashPassTier: BashPassTier | null | undefined;
|
|
365
|
+
bashPassEventsUsedThisMonth: number;
|
|
366
|
+
bashPassStripeSubscriptionId: string | null | undefined;
|
|
367
|
+
bashPassCurrentPeriodEnd: Date | null | undefined;
|
|
368
|
+
membershipTier: MembershipTier;
|
|
369
|
+
membershipExpiresAt: Date | null | undefined;
|
|
370
|
+
}): number | null {
|
|
371
|
+
const effective = getEffectiveBashPassTier(user);
|
|
372
|
+
if (!effective) return 0;
|
|
373
|
+
const cap = BASH_PASS_EVENT_CAPS[effective];
|
|
374
|
+
if (cap === null) return null;
|
|
375
|
+
const used = user.bashPassEventsUsedThisMonth ?? 0;
|
|
376
|
+
return Math.max(0, cap - used);
|
|
377
|
+
}
|
|
378
|
+
|
|
224
379
|
// ============================================
|
|
225
380
|
// VENUE PRICING PLANS
|
|
226
381
|
// ============================================
|
package/src/utils/urlUtils.ts
CHANGED
|
@@ -206,3 +206,48 @@ export function redirectToStripeWithTimeout(
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Builds a safe absolute URL for `<a href>` from user-edited service/profile links.
|
|
212
|
+
*
|
|
213
|
+
* Browsers resolve `href="example.com"` as a **relative** URL (under the current path),
|
|
214
|
+
* not `https://example.com`, so bare domains must be prefixed with `https://`.
|
|
215
|
+
* Stores sometimes save Instagram as `@handle` instead of a full URL — those are mapped
|
|
216
|
+
* to `https://www.instagram.com/{handle}/`.
|
|
217
|
+
*
|
|
218
|
+
* @returns `"#"` when the value is empty or unsafe (`javascript:`, `data:`).
|
|
219
|
+
*/
|
|
220
|
+
export function toExternalLinkHref(raw: string | undefined | null): string {
|
|
221
|
+
if (raw == null) {
|
|
222
|
+
return "#";
|
|
223
|
+
}
|
|
224
|
+
const trimmed = raw.trim();
|
|
225
|
+
if (trimmed === "") {
|
|
226
|
+
return "#";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const lower = trimmed.toLowerCase();
|
|
230
|
+
if (lower.startsWith("javascript:") || lower.startsWith("data:")) {
|
|
231
|
+
return "#";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
235
|
+
return trimmed;
|
|
236
|
+
}
|
|
237
|
+
if (/^mailto:/i.test(trimmed) || /^tel:/i.test(trimmed)) {
|
|
238
|
+
return trimmed;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Instagram @handle (often stored as a handle instead of a URL)
|
|
242
|
+
if (trimmed.startsWith("@")) {
|
|
243
|
+
const handle = trimmed
|
|
244
|
+
.slice(1)
|
|
245
|
+
.trim()
|
|
246
|
+
.replace(/^@+/, "");
|
|
247
|
+
if (/^[a-zA-Z0-9._]{1,30}$/.test(handle)) {
|
|
248
|
+
return `https://www.instagram.com/${handle}/`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return `https://${trimmed}`;
|
|
253
|
+
}
|