@bash-app/bash-common 30.246.0 → 30.253.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 (180) hide show
  1. package/README.md +4 -0
  2. package/dist/__tests__/bashSectionTitleForBashType.test.js +32 -1
  3. package/dist/__tests__/bashSectionTitleForBashType.test.js.map +1 -1
  4. package/dist/__tests__/definitionsHelpers.test.d.ts +2 -0
  5. package/dist/__tests__/definitionsHelpers.test.d.ts.map +1 -0
  6. package/dist/__tests__/definitionsHelpers.test.js +85 -0
  7. package/dist/__tests__/definitionsHelpers.test.js.map +1 -0
  8. package/dist/__tests__/extendedSchemas.test.d.ts +2 -0
  9. package/dist/__tests__/extendedSchemas.test.d.ts.map +1 -0
  10. package/dist/__tests__/extendedSchemas.test.js +40 -0
  11. package/dist/__tests__/extendedSchemas.test.js.map +1 -0
  12. package/dist/__tests__/membershipDefinitions.test.d.ts +2 -0
  13. package/dist/__tests__/membershipDefinitions.test.d.ts.map +1 -0
  14. package/dist/__tests__/membershipDefinitions.test.js +95 -0
  15. package/dist/__tests__/membershipDefinitions.test.js.map +1 -0
  16. package/dist/__tests__/ticketAdmission.test.d.ts +5 -0
  17. package/dist/__tests__/ticketAdmission.test.d.ts.map +1 -0
  18. package/dist/__tests__/ticketAdmission.test.js +42 -0
  19. package/dist/__tests__/ticketAdmission.test.js.map +1 -0
  20. package/dist/__tests__/utmAttribution.test.d.ts +2 -0
  21. package/dist/__tests__/utmAttribution.test.d.ts.map +1 -0
  22. package/dist/__tests__/utmAttribution.test.js +92 -0
  23. package/dist/__tests__/utmAttribution.test.js.map +1 -0
  24. package/dist/definitions.d.ts +18 -5
  25. package/dist/definitions.d.ts.map +1 -1
  26. package/dist/definitions.js +84 -22
  27. package/dist/definitions.js.map +1 -1
  28. package/dist/extendedSchemas.d.ts +41 -1
  29. package/dist/extendedSchemas.d.ts.map +1 -1
  30. package/dist/extendedSchemas.js +27 -0
  31. package/dist/extendedSchemas.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/sms/smsTemplates.d.ts +27 -0
  37. package/dist/sms/smsTemplates.d.ts.map +1 -0
  38. package/dist/sms/smsTemplates.js +11 -0
  39. package/dist/sms/smsTemplates.js.map +1 -0
  40. package/dist/utils/__tests__/addressUtils.test.d.ts +2 -0
  41. package/dist/utils/__tests__/addressUtils.test.d.ts.map +1 -0
  42. package/dist/utils/__tests__/addressUtils.test.js +138 -0
  43. package/dist/utils/__tests__/addressUtils.test.js.map +1 -0
  44. package/dist/utils/__tests__/apiUtils.test.d.ts +5 -0
  45. package/dist/utils/__tests__/apiUtils.test.d.ts.map +1 -0
  46. package/dist/utils/__tests__/apiUtils.test.js +161 -0
  47. package/dist/utils/__tests__/apiUtils.test.js.map +1 -0
  48. package/dist/utils/__tests__/arrayUtils.test.d.ts +2 -0
  49. package/dist/utils/__tests__/arrayUtils.test.d.ts.map +1 -0
  50. package/dist/utils/__tests__/arrayUtils.test.js +23 -0
  51. package/dist/utils/__tests__/arrayUtils.test.js.map +1 -0
  52. package/dist/utils/__tests__/blogUtils.test.d.ts +2 -0
  53. package/dist/utils/__tests__/blogUtils.test.d.ts.map +1 -0
  54. package/dist/utils/__tests__/blogUtils.test.js +102 -0
  55. package/dist/utils/__tests__/blogUtils.test.js.map +1 -0
  56. package/dist/utils/__tests__/dateTimeUtils.test.d.ts +2 -0
  57. package/dist/utils/__tests__/dateTimeUtils.test.d.ts.map +1 -0
  58. package/dist/utils/__tests__/dateTimeUtils.test.js +180 -0
  59. package/dist/utils/__tests__/dateTimeUtils.test.js.map +1 -0
  60. package/dist/utils/__tests__/discountEngine/discountCalculator.test.d.ts +2 -0
  61. package/dist/utils/__tests__/discountEngine/discountCalculator.test.d.ts.map +1 -0
  62. package/dist/utils/__tests__/discountEngine/discountCalculator.test.js +356 -0
  63. package/dist/utils/__tests__/discountEngine/discountCalculator.test.js.map +1 -0
  64. package/dist/utils/__tests__/generalDateTimeUtils.test.d.ts +2 -0
  65. package/dist/utils/__tests__/generalDateTimeUtils.test.d.ts.map +1 -0
  66. package/dist/utils/__tests__/generalDateTimeUtils.test.js +26 -0
  67. package/dist/utils/__tests__/generalDateTimeUtils.test.js.map +1 -0
  68. package/dist/utils/__tests__/luxonUtils.test.d.ts +2 -0
  69. package/dist/utils/__tests__/luxonUtils.test.d.ts.map +1 -0
  70. package/dist/utils/__tests__/luxonUtils.test.js +139 -0
  71. package/dist/utils/__tests__/luxonUtils.test.js.map +1 -0
  72. package/dist/utils/__tests__/mathUtils.test.d.ts +5 -0
  73. package/dist/utils/__tests__/mathUtils.test.d.ts.map +1 -0
  74. package/dist/utils/__tests__/mathUtils.test.js +26 -0
  75. package/dist/utils/__tests__/mathUtils.test.js.map +1 -0
  76. package/dist/utils/__tests__/objUtils.test.d.ts +5 -0
  77. package/dist/utils/__tests__/objUtils.test.d.ts.map +1 -0
  78. package/dist/utils/__tests__/objUtils.test.js +41 -0
  79. package/dist/utils/__tests__/objUtils.test.js.map +1 -0
  80. package/dist/utils/__tests__/paymentUtils.test.js +48 -1
  81. package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
  82. package/dist/utils/__tests__/qrCodeUtils.test.js +16 -1
  83. package/dist/utils/__tests__/qrCodeUtils.test.js.map +1 -1
  84. package/dist/utils/__tests__/reviewUtils.test.d.ts +2 -0
  85. package/dist/utils/__tests__/reviewUtils.test.d.ts.map +1 -0
  86. package/dist/utils/__tests__/reviewUtils.test.js +49 -0
  87. package/dist/utils/__tests__/reviewUtils.test.js.map +1 -0
  88. package/dist/utils/__tests__/slugUtils.test.d.ts +2 -0
  89. package/dist/utils/__tests__/slugUtils.test.d.ts.map +1 -0
  90. package/dist/utils/__tests__/slugUtils.test.js +103 -0
  91. package/dist/utils/__tests__/slugUtils.test.js.map +1 -0
  92. package/dist/utils/__tests__/sortUtils.test.d.ts +5 -0
  93. package/dist/utils/__tests__/sortUtils.test.d.ts.map +1 -0
  94. package/dist/utils/__tests__/sortUtils.test.js +28 -0
  95. package/dist/utils/__tests__/sortUtils.test.js.map +1 -0
  96. package/dist/utils/__tests__/stringUtils.test.d.ts +2 -0
  97. package/dist/utils/__tests__/stringUtils.test.d.ts.map +1 -0
  98. package/dist/utils/__tests__/stringUtils.test.js +27 -0
  99. package/dist/utils/__tests__/stringUtils.test.js.map +1 -0
  100. package/dist/utils/__tests__/ticketListUtils.test.d.ts +2 -0
  101. package/dist/utils/__tests__/ticketListUtils.test.d.ts.map +1 -0
  102. package/dist/utils/__tests__/ticketListUtils.test.js +41 -0
  103. package/dist/utils/__tests__/ticketListUtils.test.js.map +1 -0
  104. package/dist/utils/__tests__/typeUtils.test.d.ts +2 -0
  105. package/dist/utils/__tests__/typeUtils.test.d.ts.map +1 -0
  106. package/dist/utils/__tests__/typeUtils.test.js +11 -0
  107. package/dist/utils/__tests__/typeUtils.test.js.map +1 -0
  108. package/dist/utils/__tests__/userSubscriptionUtils.test.d.ts +5 -0
  109. package/dist/utils/__tests__/userSubscriptionUtils.test.d.ts.map +1 -0
  110. package/dist/utils/__tests__/userSubscriptionUtils.test.js +66 -0
  111. package/dist/utils/__tests__/userSubscriptionUtils.test.js.map +1 -0
  112. package/dist/utils/__tests__/userUtils.test.d.ts +2 -0
  113. package/dist/utils/__tests__/userUtils.test.d.ts.map +1 -0
  114. package/dist/utils/__tests__/userUtils.test.js +34 -0
  115. package/dist/utils/__tests__/userUtils.test.js.map +1 -0
  116. package/dist/utils/__tests__/venueTypeFormatter.test.d.ts +2 -0
  117. package/dist/utils/__tests__/venueTypeFormatter.test.d.ts.map +1 -0
  118. package/dist/utils/__tests__/venueTypeFormatter.test.js +25 -0
  119. package/dist/utils/__tests__/venueTypeFormatter.test.js.map +1 -0
  120. package/dist/utils/service/__tests__/regexUtils.test.d.ts +2 -0
  121. package/dist/utils/service/__tests__/regexUtils.test.d.ts.map +1 -0
  122. package/dist/utils/service/__tests__/regexUtils.test.js +51 -0
  123. package/dist/utils/service/__tests__/regexUtils.test.js.map +1 -0
  124. package/dist/utils/service/__tests__/serviceDBUtils.test.d.ts +2 -0
  125. package/dist/utils/service/__tests__/serviceDBUtils.test.d.ts.map +1 -0
  126. package/dist/utils/service/__tests__/serviceDBUtils.test.js +17 -0
  127. package/dist/utils/service/__tests__/serviceDBUtils.test.js.map +1 -0
  128. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.d.ts +2 -0
  129. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.d.ts.map +1 -0
  130. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.js +60 -0
  131. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.js.map +1 -0
  132. package/dist/utils/service/__tests__/serviceUtils.test.d.ts +2 -0
  133. package/dist/utils/service/__tests__/serviceUtils.test.d.ts.map +1 -0
  134. package/dist/utils/service/__tests__/serviceUtils.test.js +135 -0
  135. package/dist/utils/service/__tests__/serviceUtils.test.js.map +1 -0
  136. package/dist/utils/service/serviceFeaturedSortUtils.d.ts.map +1 -1
  137. package/dist/utils/service/serviceFeaturedSortUtils.js +5 -1
  138. package/dist/utils/service/serviceFeaturedSortUtils.js.map +1 -1
  139. package/dist/utils/ticketListUtils.d.ts.map +1 -1
  140. package/dist/utils/ticketListUtils.js +11 -7
  141. package/dist/utils/ticketListUtils.js.map +1 -1
  142. package/package.json +1 -1
  143. package/prisma/schema.prisma +47 -0
  144. package/src/__tests__/bashSectionTitleForBashType.test.ts +48 -1
  145. package/src/__tests__/definitionsHelpers.test.ts +136 -0
  146. package/src/__tests__/extendedSchemas.test.ts +50 -0
  147. package/src/__tests__/membershipDefinitions.test.ts +144 -0
  148. package/src/__tests__/ticketAdmission.test.ts +73 -0
  149. package/src/__tests__/utmAttribution.test.ts +118 -0
  150. package/src/definitions.ts +86 -23
  151. package/src/extendedSchemas.ts +35 -1
  152. package/src/index.ts +1 -0
  153. package/src/sms/smsTemplates.ts +20 -0
  154. package/src/utils/__tests__/addressUtils.test.ts +176 -0
  155. package/src/utils/__tests__/apiUtils.test.ts +219 -0
  156. package/src/utils/__tests__/arrayUtils.test.ts +26 -0
  157. package/src/utils/__tests__/blogUtils.test.ts +145 -0
  158. package/src/utils/__tests__/dateTimeUtils.test.ts +233 -0
  159. package/src/utils/__tests__/discountEngine/discountCalculator.test.ts +410 -0
  160. package/src/utils/__tests__/generalDateTimeUtils.test.ts +34 -0
  161. package/src/utils/__tests__/luxonUtils.test.ts +171 -0
  162. package/src/utils/__tests__/mathUtils.test.ts +30 -0
  163. package/src/utils/__tests__/objUtils.test.ts +48 -0
  164. package/src/utils/__tests__/paymentUtils.test.ts +59 -0
  165. package/src/utils/__tests__/qrCodeUtils.test.ts +28 -1
  166. package/src/utils/__tests__/reviewUtils.test.ts +65 -0
  167. package/src/utils/__tests__/slugUtils.test.ts +143 -0
  168. package/src/utils/__tests__/sortUtils.test.ts +33 -0
  169. package/src/utils/__tests__/stringUtils.test.ts +37 -0
  170. package/src/utils/__tests__/ticketListUtils.test.ts +47 -0
  171. package/src/utils/__tests__/typeUtils.test.ts +12 -0
  172. package/src/utils/__tests__/userSubscriptionUtils.test.ts +87 -0
  173. package/src/utils/__tests__/userUtils.test.ts +41 -0
  174. package/src/utils/__tests__/venueTypeFormatter.test.ts +31 -0
  175. package/src/utils/service/__tests__/regexUtils.test.ts +66 -0
  176. package/src/utils/service/__tests__/serviceDBUtils.test.ts +19 -0
  177. package/src/utils/service/__tests__/serviceFeaturedSortUtils.test.ts +67 -0
  178. package/src/utils/service/__tests__/serviceUtils.test.ts +190 -0
  179. package/src/utils/service/serviceFeaturedSortUtils.ts +5 -1
  180. package/src/utils/ticketListUtils.ts +13 -6
@@ -16,6 +16,7 @@ import {
16
16
  calculateTotalBashPoints,
17
17
  calculateStripeDonationFee,
18
18
  calculateServiceFeeWithMembershipDiscount,
19
+ calculatePartnerFeeWithMembershipDiscount,
19
20
  calculateBashPlatformFeeCents,
20
21
  splitBashPlatformFeeForTicketCheckout,
21
22
  computeTicketApplicationFeeAmountCents,
@@ -24,7 +25,9 @@ import {
24
25
  getBashPassMonthlyCapRemaining,
25
26
  higherBashPassTier,
26
27
  membershipGrantsBashPassTier,
28
+ VENUE_PRICING_PLAN_DATA,
27
29
  } from "../paymentUtils.js";
30
+ import { VenuePricingPlan } from "@prisma/client";
28
31
 
29
32
  describe("PaymentUtils - Core Functions", () => {
30
33
  describe("Currency Conversion", () => {
@@ -337,6 +340,62 @@ describe("PaymentUtils - Core Functions", () => {
337
340
  });
338
341
  });
339
342
 
343
+ describe("Partner fee with membership discount", () => {
344
+ test("calculatePartnerFeeWithMembershipDiscount scales with amount", () => {
345
+ const small = calculatePartnerFeeWithMembershipDiscount(1000, MembershipTier.Basic);
346
+ const large = calculatePartnerFeeWithMembershipDiscount(100000, MembershipTier.Basic);
347
+ expect(large).toBeGreaterThan(small);
348
+ });
349
+
350
+ test("calculatePartnerFeeWithMembershipDiscount defaults to Basic tier", () => {
351
+ const withTier = calculatePartnerFeeWithMembershipDiscount(5000, MembershipTier.Pro);
352
+ const defaulted = calculatePartnerFeeWithMembershipDiscount(5000, null);
353
+ expect(typeof withTier).toBe("number");
354
+ expect(typeof defaulted).toBe("number");
355
+ });
356
+ });
357
+
358
+ describe("VENUE_PRICING_PLAN_DATA", () => {
359
+ test("Percentage plan uses pay-per-booking copy and fee", () => {
360
+ const plan = VENUE_PRICING_PLAN_DATA[VenuePricingPlan.Percentage];
361
+ expect(plan.name).toBe("Pay per booking");
362
+ expect(plan.percentageFee).toBeGreaterThan(0);
363
+ expect(plan.monthlyFee).toBe(0);
364
+ });
365
+
366
+ test("Subscription plan is flat monthly with no percentage fee", () => {
367
+ const plan = VENUE_PRICING_PLAN_DATA[VenuePricingPlan.Subscription];
368
+ expect(plan.monthlyFee).toBe(100);
369
+ expect(plan.percentageFee).toBe(0);
370
+ });
371
+ });
372
+
373
+ describe("splitBashPlatformFeeForTicketCheckout edge cases", () => {
374
+ test("returns zeros when platform fee disabled", () => {
375
+ expect(
376
+ splitBashPlatformFeeForTicketCheckout({
377
+ grossCents: 10000,
378
+ platformFeeEnabled: false,
379
+ platformFeeRate: 0.03,
380
+ feeHandling: "GuestPays",
381
+ bashPassWaivesGuestFee: false,
382
+ })
383
+ ).toEqual({ totalPlatformCents: 0, guestPlatformLineCents: 0 });
384
+ });
385
+
386
+ test("defaults feeHandling to HostAbsorbs", () => {
387
+ expect(
388
+ splitBashPlatformFeeForTicketCheckout({
389
+ grossCents: 10000,
390
+ platformFeeEnabled: true,
391
+ platformFeeRate: 0.03,
392
+ feeHandling: null,
393
+ bashPassWaivesGuestFee: false,
394
+ })
395
+ ).toEqual({ totalPlatformCents: 300, guestPlatformLineCents: 0 });
396
+ });
397
+ });
398
+
340
399
  describe("BashPass tier and monthly cap", () => {
341
400
  const future = new Date(Date.now() + 86400000 * 30);
342
401
  const past = new Date(Date.now() - 86400000);
@@ -18,12 +18,18 @@
18
18
  *
19
19
  * Bugs in any of these route door-scanners to the wrong page (= entry chaos).
20
20
  */
21
+ import QRCode from "qrcode";
22
+
21
23
  import {
22
24
  bashEventPrismaCheckoutQrCodeUrl,
23
25
  bashEventQrCodeUrl,
24
26
  createQrCodeImage,
25
27
  } from "../qrCodeUtils.js";
26
28
 
29
+ jest.mock("qrcode", () => ({
30
+ toDataURL: jest.fn(),
31
+ }));
32
+
27
33
  // ============================================================================
28
34
  // bashEventPrismaCheckoutQrCodeUrl — checkout-id deep link
29
35
  // ============================================================================
@@ -120,22 +126,43 @@ describe("bashEventQrCodeUrl", () => {
120
126
  // ============================================================================
121
127
 
122
128
  describe("createQrCodeImage", () => {
129
+ beforeEach(() => {
130
+ jest.clearAllMocks();
131
+ });
132
+
123
133
  it("returns a base64-encoded PNG data URL", async () => {
134
+ const mockDataUrl =
135
+ "data:image/png;base64," +
136
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==".repeat(
137
+ 3
138
+ );
139
+ (QRCode.toDataURL as jest.Mock).mockResolvedValue(mockDataUrl);
140
+
124
141
  const dataUrl = await createQrCodeImage(
125
142
  "https://bash.app/ticket-details/evt_1/ticket/abc"
126
143
  );
127
144
  expect(dataUrl).toMatch(/^data:image\/png;base64,/);
128
- // Sanity: a real QR image will have *meaningful* base64 length, not a few bytes.
129
145
  expect(dataUrl.length).toBeGreaterThan(200);
146
+ expect(QRCode.toDataURL).toHaveBeenCalledWith(
147
+ "https://bash.app/ticket-details/evt_1/ticket/abc",
148
+ { errorCorrectionLevel: "high" }
149
+ );
130
150
  });
131
151
 
132
152
  it("produces a deterministic PNG for the same input (no nonce / timestamp leakage)", async () => {
153
+ const mockDataUrl = "data:image/png;base64,same-payload";
154
+ (QRCode.toDataURL as jest.Mock).mockResolvedValue(mockDataUrl);
155
+
133
156
  const a = await createQrCodeImage("ticket://abc");
134
157
  const b = await createQrCodeImage("ticket://abc");
135
158
  expect(a).toBe(b);
136
159
  });
137
160
 
138
161
  it("produces DIFFERENT output for different payloads", async () => {
162
+ (QRCode.toDataURL as jest.Mock)
163
+ .mockResolvedValueOnce("data:image/png;base64,abc")
164
+ .mockResolvedValueOnce("data:image/png;base64,xyz");
165
+
139
166
  const a = await createQrCodeImage("ticket://abc");
140
167
  const b = await createQrCodeImage("ticket://xyz");
141
168
  expect(a).not.toBe(b);
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ calculateAverageRating,
4
+ generateReviewStats,
5
+ formatRatingWithStars,
6
+ isEligibleToReview,
7
+ } from "../reviewUtils.js";
8
+ import type { Review } from "../../definitions.js";
9
+
10
+ const reviews: Review[] = [
11
+ { rating: 5, id: "1" } as Review,
12
+ { rating: 4, id: "2" } as Review,
13
+ { rating: 3, id: "3" } as Review,
14
+ ];
15
+
16
+ describe("reviewUtils", () => {
17
+ describe("calculateAverageRating", () => {
18
+ test("returns 0 for empty list", () => {
19
+ expect(calculateAverageRating([])).toBe(0);
20
+ });
21
+
22
+ test("averages ratings to one decimal", () => {
23
+ expect(calculateAverageRating(reviews)).toBe(4);
24
+ });
25
+ });
26
+
27
+ describe("generateReviewStats", () => {
28
+ test("builds distribution and totals", () => {
29
+ expect(generateReviewStats(reviews)).toEqual({
30
+ averageRating: 4,
31
+ totalRatings: 3,
32
+ ratings: { 1: 0, 2: 0, 3: 1, 4: 1, 5: 1 },
33
+ });
34
+ });
35
+
36
+ test("ignores out-of-range ratings", () => {
37
+ const mixed = [{ rating: 6, id: "x" } as Review, { rating: 0, id: "y" } as Review];
38
+ expect(generateReviewStats(mixed).ratings).toEqual({ 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 });
39
+ });
40
+ });
41
+
42
+ describe("formatRatingWithStars", () => {
43
+ test("includes full stars and half star", () => {
44
+ expect(formatRatingWithStars(4.5)).toBe("4.5 ★★★★½");
45
+ });
46
+
47
+ test("rounds down fractional stars below 0.5", () => {
48
+ expect(formatRatingWithStars(3.2)).toBe("3.2 ★★★");
49
+ });
50
+ });
51
+
52
+ describe("isEligibleToReview", () => {
53
+ test("allows attendee who has not reviewed and is not the host", () => {
54
+ expect(isEligibleToReview("user-1", "event-1", "host-1", [], ["event-1"])).toBe(true);
55
+ });
56
+
57
+ test("blocks host, non-attendees, and duplicate reviews", () => {
58
+ expect(isEligibleToReview("host-1", "event-1", "host-1", [], ["event-1"])).toBe(false);
59
+ expect(isEligibleToReview("user-1", "event-1", "host-1", [], [])).toBe(false);
60
+ expect(isEligibleToReview("user-1", "event-1", "host-1", ["event-1"], ["event-1"])).toBe(
61
+ false
62
+ );
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,143 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ BASH_DETAIL_URL,
4
+ BASH_EVENT_WIZARD_HOW_MAJOR_STEP,
5
+ BASH_EVENT_WIZARD_HOW_SUBSTEP_PAYOUT,
6
+ BASH_EVENT_WIZARD_PATH_PREFIX,
7
+ } from "../../definitions.js";
8
+ import {
9
+ generateSlug,
10
+ generateUniqueSlug,
11
+ generateBashDetailUrl,
12
+ getPublicBashDetailPath,
13
+ getBashEventWizardStepPath,
14
+ getBashEventWizardPayoutStepPath,
15
+ parseBashUrlParams,
16
+ validateSlug,
17
+ } from "../slugUtils.js";
18
+
19
+ describe("generateSlug", () => {
20
+ test("lowercases and slugifies a title", () => {
21
+ expect(generateSlug("Summer Bash 2026!")).toBe("summer-bash-2026");
22
+ });
23
+
24
+ test("removes special characters", () => {
25
+ expect(generateSlug("Hello *World*")).toBe("hello-world");
26
+ });
27
+
28
+ test("handles empty string", () => {
29
+ expect(generateSlug("")).toBe("");
30
+ });
31
+ });
32
+
33
+ describe("generateUniqueSlug", () => {
34
+ test("returns base slug when available", async () => {
35
+ const checkSlugExists = async () => false;
36
+ await expect(generateUniqueSlug("My Event", checkSlugExists)).resolves.toBe("my-event");
37
+ });
38
+
39
+ test("appends counter when slug exists", async () => {
40
+ const taken = new Set(["my-event", "my-event-1"]);
41
+ const checkSlugExists = async (slug: string) => taken.has(slug);
42
+ await expect(generateUniqueSlug("My Event", checkSlugExists)).resolves.toBe("my-event-2");
43
+ });
44
+
45
+ test("passes excludeId to checkSlugExists", async () => {
46
+ const calls: Array<{ slug: string; excludeId?: string }> = [];
47
+ const checkSlugExists = async (slug: string, excludeId?: string) => {
48
+ calls.push({ slug, excludeId });
49
+ return false;
50
+ };
51
+ await generateUniqueSlug("My Event", checkSlugExists, "event-123");
52
+ expect(calls).toEqual([{ slug: "my-event", excludeId: "event-123" }]);
53
+ });
54
+ });
55
+
56
+ describe("generateBashDetailUrl", () => {
57
+ test("builds id-slug path", () => {
58
+ expect(generateBashDetailUrl("abc123", "summer-bash")).toBe("/bash/abc123-summer-bash");
59
+ });
60
+ });
61
+
62
+ describe("getPublicBashDetailPath", () => {
63
+ test("uses slug path when slug is present", () => {
64
+ expect(getPublicBashDetailPath("abc123", "summer-bash")).toBe(
65
+ "/bash/abc123-summer-bash"
66
+ );
67
+ });
68
+
69
+ test("trims slug before use", () => {
70
+ expect(getPublicBashDetailPath("abc123", " summer-bash ")).toBe(
71
+ "/bash/abc123-summer-bash"
72
+ );
73
+ });
74
+
75
+ test("falls back to id-only legacy path when slug missing", () => {
76
+ expect(getPublicBashDetailPath("abc123", null)).toBe(`${BASH_DETAIL_URL}/abc123`);
77
+ expect(getPublicBashDetailPath("abc123", undefined)).toBe(`${BASH_DETAIL_URL}/abc123`);
78
+ expect(getPublicBashDetailPath("abc123", " ")).toBe(`${BASH_DETAIL_URL}/abc123`);
79
+ });
80
+ });
81
+
82
+ describe("getBashEventWizardStepPath", () => {
83
+ test("builds wizard step path", () => {
84
+ expect(getBashEventWizardStepPath("evt-1", 3, 2)).toBe(
85
+ `${BASH_EVENT_WIZARD_PATH_PREFIX}/evt-1/3/2`
86
+ );
87
+ });
88
+ });
89
+
90
+ describe("getBashEventWizardPayoutStepPath", () => {
91
+ test("returns How → Payout step path", () => {
92
+ expect(getBashEventWizardPayoutStepPath("evt-1")).toBe(
93
+ `${BASH_EVENT_WIZARD_PATH_PREFIX}/evt-1/${BASH_EVENT_WIZARD_HOW_MAJOR_STEP}/${BASH_EVENT_WIZARD_HOW_SUBSTEP_PAYOUT}`
94
+ );
95
+ });
96
+ });
97
+
98
+ describe("parseBashUrlParams", () => {
99
+ test("parses id-slug format", () => {
100
+ expect(parseBashUrlParams("abc123-summer-bash")).toEqual({
101
+ id: "abc123",
102
+ slug: "summer-bash",
103
+ });
104
+ });
105
+
106
+ test("parses id-only legacy format", () => {
107
+ expect(parseBashUrlParams("abc123")).toEqual({ id: "abc123", slug: null });
108
+ });
109
+
110
+ test("returns null for empty param", () => {
111
+ expect(parseBashUrlParams("")).toBeNull();
112
+ });
113
+
114
+ test("returns null when id portion is empty", () => {
115
+ expect(parseBashUrlParams("-slug-only")).toBeNull();
116
+ });
117
+
118
+ test("returns slug null when trailing dash has no slug", () => {
119
+ expect(parseBashUrlParams("abc123-")).toEqual({ id: "abc123", slug: null });
120
+ });
121
+
122
+ test("uses first dash as separator only", () => {
123
+ expect(parseBashUrlParams("abc-my-cool-event")).toEqual({
124
+ id: "abc",
125
+ slug: "my-cool-event",
126
+ });
127
+ });
128
+ });
129
+
130
+ describe("validateSlug", () => {
131
+ test.each([
132
+ ["valid-slug", true],
133
+ ["abc123", true],
134
+ ["a-b-c", true],
135
+ ["UPPERCASE", false],
136
+ ["has spaces", false],
137
+ ["has_underscore", false],
138
+ ["special!", false],
139
+ ["", false],
140
+ ])("validateSlug(%s) → %s", (slug, expected) => {
141
+ expect(validateSlug(slug)).toBe(expected);
142
+ });
143
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Tests for tier sort ordering.
3
+ */
4
+
5
+ import { describe, expect, test } from "@jest/globals";
6
+ import { sortTiers } from "../sortUtils.js";
7
+
8
+ describe("sortUtils", () => {
9
+ describe("sortTiers", () => {
10
+ test("returns 0 when sortOrder matches", () => {
11
+ expect(sortTiers({ sortOrder: 2 }, { sortOrder: 2 })).toBe(0);
12
+ });
13
+
14
+ test("sorts by sortOrder ascending", () => {
15
+ expect(sortTiers({ sortOrder: 1 }, { sortOrder: 3 })).toBeLessThan(0);
16
+ expect(sortTiers({ sortOrder: 5 }, { sortOrder: 2 })).toBeGreaterThan(0);
17
+ });
18
+
19
+ test("falls back to price when sortOrder absent", () => {
20
+ expect(sortTiers({ price: 10 }, { price: 20 })).toBeLessThan(0);
21
+ expect(sortTiers({ price: 20 }, { price: 10 })).toBeGreaterThan(0);
22
+ });
23
+
24
+ test("empty values sort before defined values", () => {
25
+ expect(sortTiers({}, { sortOrder: 1 })).toBeLessThan(0);
26
+ expect(sortTiers({ sortOrder: 1 }, {})).toBeGreaterThan(0);
27
+ });
28
+
29
+ test("returns 0 when both tiers lack comparable fields", () => {
30
+ expect(sortTiers({}, {})).toBe(0);
31
+ });
32
+ });
33
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { formatString } from "../stringUtils.js";
3
+
4
+ describe("formatString", () => {
5
+ test("replaces placeholders with string values", () => {
6
+ expect(formatString("Hello ${name}!", { name: "World" })).toBe("Hello World!");
7
+ });
8
+
9
+ test("replaces placeholders with numeric values", () => {
10
+ expect(formatString("Count: ${count}", { count: 42 })).toBe("Count: 42");
11
+ });
12
+
13
+ test("replaces multiple placeholders", () => {
14
+ expect(
15
+ formatString("${greeting}, ${name}!", { greeting: "Hi", name: "Alex" })
16
+ ).toBe("Hi, Alex!");
17
+ });
18
+
19
+ test("uses empty string for missing keys", () => {
20
+ expect(formatString("Hello ${name}!", {})).toBe("Hello !");
21
+ });
22
+
23
+ test("leaves text without placeholders unchanged", () => {
24
+ expect(formatString("No placeholders here", { unused: "x" })).toBe(
25
+ "No placeholders here"
26
+ );
27
+ });
28
+
29
+ test("handles empty template", () => {
30
+ expect(formatString("", { key: "value" })).toBe("");
31
+ });
32
+
33
+ test("only matches word-character keys", () => {
34
+ expect(formatString("${key_1} ${key2}", { key_1: "a", key2: "b" })).toBe("a b");
35
+ expect(formatString("${key-name}", {})).toBe("${key-name}");
36
+ });
37
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { DateTime } from "luxon";
3
+ import { ticketListToString, ticketListStrToTicketList } from "../ticketListUtils.js";
4
+ import type { NumberOfTicketsForDate } from "../../definitions.js";
5
+
6
+ describe("ticketListUtils", () => {
7
+ const isoDate = "2024-07-04T18:00:00.000Z";
8
+
9
+ test("round-trips ticket list through base64 encoding", () => {
10
+ const ticketList = new Map<string, NumberOfTicketsForDate[]>([
11
+ [
12
+ "tier-abc",
13
+ [
14
+ { numberOfTickets: 2, ticketDateTime: isoDate },
15
+ { numberOfTickets: 1, ticketDateTime: isoDate },
16
+ ],
17
+ ],
18
+ ["tier-xyz", [{ numberOfTickets: 3, ticketDateTime: isoDate }]],
19
+ ]);
20
+
21
+ const encoded = ticketListToString(ticketList);
22
+ expect(encoded).not.toContain("+");
23
+ expect(encoded).not.toContain("/");
24
+
25
+ const decoded = ticketListStrToTicketList(encoded);
26
+ expect(decoded.size).toBe(2);
27
+ expect(decoded.get("tier-abc")).toHaveLength(2);
28
+ expect(decoded.get("tier-abc")![0].numberOfTickets).toBe(2);
29
+ expect(decoded.get("tier-xyz")![0].numberOfTickets).toBe(3);
30
+ });
31
+
32
+ test("omits tiers with zero tickets", () => {
33
+ const ticketList = new Map<string, NumberOfTicketsForDate[]>([
34
+ ["tier-empty", [{ numberOfTickets: 0, ticketDateTime: isoDate }]],
35
+ ["tier-live", [{ numberOfTickets: 1, ticketDateTime: isoDate }]],
36
+ ]);
37
+ const decoded = ticketListStrToTicketList(ticketListToString(ticketList));
38
+ expect(decoded.has("tier-empty")).toBe(false);
39
+ expect(decoded.get("tier-live")).toHaveLength(1);
40
+ });
41
+
42
+ test("decodes legacy plain strings for backward compatibility", () => {
43
+ const plain = `tier1__2;;${DateTime.fromISO(isoDate).toISO()}~~`;
44
+ const decoded = ticketListStrToTicketList(plain);
45
+ expect(decoded.get("tier1")![0].numberOfTickets).toBe(2);
46
+ });
47
+ });
@@ -0,0 +1,12 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { createAllTrueObject } from "../typeUtils.js";
3
+
4
+ describe("createAllTrueObject", () => {
5
+ test("maps every key to true", () => {
6
+ expect(createAllTrueObject(["a", "b", "c"])).toEqual({ a: true, b: true, c: true });
7
+ });
8
+
9
+ test("returns empty object for empty key list", () => {
10
+ expect(createAllTrueObject([])).toEqual({});
11
+ });
12
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Tests for service subscription tier mappings.
3
+ */
4
+
5
+ import { ServiceTypes, MembershipTier } from "@prisma/client";
6
+ import { describe, expect, test } from "@jest/globals";
7
+ import {
8
+ SERVICE_TIER_TO_SERVICES_LIST,
9
+ SERVICE_TIER_FROM_SERVICE_TYPE,
10
+ USER_SUBSCRIPTION_TYPES,
11
+ ServiceSubscriptionTier,
12
+ } from "../userSubscriptionUtils.js";
13
+
14
+ describe("userSubscriptionUtils", () => {
15
+ describe("SERVICE_TIER_TO_SERVICES_LIST", () => {
16
+ test("Free and Ally tiers include event and entertainment services", () => {
17
+ expect(SERVICE_TIER_TO_SERVICES_LIST.Free).toEqual([
18
+ ServiceTypes.EventServices,
19
+ ServiceTypes.EntertainmentServices,
20
+ ]);
21
+ expect(SERVICE_TIER_TO_SERVICES_LIST.Ally).toEqual([
22
+ ServiceTypes.EventServices,
23
+ ServiceTypes.EntertainmentServices,
24
+ ]);
25
+ });
26
+
27
+ test("Partner and Patron tiers map to vendor and venue service groups", () => {
28
+ expect(SERVICE_TIER_TO_SERVICES_LIST.Partner).toEqual([
29
+ ServiceTypes.Vendors,
30
+ ServiceTypes.Exhibitors,
31
+ ServiceTypes.Sponsors,
32
+ ]);
33
+ expect(SERVICE_TIER_TO_SERVICES_LIST.Patron).toEqual([
34
+ ServiceTypes.Venues,
35
+ ServiceTypes.Organizations,
36
+ ]);
37
+ });
38
+ });
39
+
40
+ describe("SERVICE_TIER_FROM_SERVICE_TYPE", () => {
41
+ test("maps all service types listed in tier config", () => {
42
+ const configuredTypes = Object.values(SERVICE_TIER_TO_SERVICES_LIST).flat();
43
+ for (const serviceType of configuredTypes) {
44
+ expect(SERVICE_TIER_FROM_SERVICE_TYPE[serviceType]).toBeDefined();
45
+ }
46
+ });
47
+
48
+ test("defaults EventServices and EntertainmentServices to Free", () => {
49
+ expect(SERVICE_TIER_FROM_SERVICE_TYPE[ServiceTypes.EventServices]).toBe(
50
+ ServiceSubscriptionTier.Free
51
+ );
52
+ expect(SERVICE_TIER_FROM_SERVICE_TYPE[ServiceTypes.EntertainmentServices]).toBe(
53
+ ServiceSubscriptionTier.Free
54
+ );
55
+ });
56
+
57
+ test("maps vendor-related types to Partner", () => {
58
+ expect(SERVICE_TIER_FROM_SERVICE_TYPE[ServiceTypes.Vendors]).toBe(ServiceSubscriptionTier.Partner);
59
+ expect(SERVICE_TIER_FROM_SERVICE_TYPE[ServiceTypes.Exhibitors]).toBe(
60
+ ServiceSubscriptionTier.Partner
61
+ );
62
+ expect(SERVICE_TIER_FROM_SERVICE_TYPE[ServiceTypes.Sponsors]).toBe(ServiceSubscriptionTier.Partner);
63
+ });
64
+
65
+ test("maps venue-related types to Patron", () => {
66
+ expect(SERVICE_TIER_FROM_SERVICE_TYPE[ServiceTypes.Venues]).toBe(ServiceSubscriptionTier.Patron);
67
+ expect(SERVICE_TIER_FROM_SERVICE_TYPE[ServiceTypes.Organizations]).toBe(
68
+ ServiceSubscriptionTier.Patron
69
+ );
70
+ });
71
+ });
72
+
73
+ describe("USER_SUBSCRIPTION_TYPES", () => {
74
+ test("Basic tier is free and typed as MembershipTier.Basic", () => {
75
+ expect(USER_SUBSCRIPTION_TYPES.Basic).toEqual({
76
+ name: "Basic",
77
+ description: "Basic",
78
+ price: 0,
79
+ type: MembershipTier.Basic,
80
+ });
81
+ });
82
+
83
+ test("only Basic tier is currently defined", () => {
84
+ expect(Object.keys(USER_SUBSCRIPTION_TYPES)).toEqual(["Basic"]);
85
+ });
86
+ });
87
+ });
@@ -0,0 +1,41 @@
1
+ import { Gender } from "@prisma/client";
2
+ import { describe, expect, test } from "@jest/globals";
3
+ import { getDisplayGender, type GenderDisplayUser } from "../userUtils.js";
4
+
5
+ describe("getDisplayGender", () => {
6
+ test("returns trimmed genderIdentity when present", () => {
7
+ const user: GenderDisplayUser = {
8
+ gender: Gender.Male,
9
+ genderIdentity: " Non-binary ",
10
+ };
11
+ expect(getDisplayGender(user)).toBe("Non-binary");
12
+ });
13
+
14
+ test("genderIdentity wins over gender enum", () => {
15
+ const user: GenderDisplayUser = {
16
+ gender: Gender.Female,
17
+ genderIdentity: "Genderfluid",
18
+ };
19
+ expect(getDisplayGender(user)).toBe("Genderfluid");
20
+ });
21
+
22
+ test("returns gender enum when genderIdentity is absent", () => {
23
+ expect(getDisplayGender({ gender: Gender.Male })).toBe(Gender.Male);
24
+ expect(getDisplayGender({ gender: Gender.Female })).toBe(Gender.Female);
25
+ });
26
+
27
+ test("returns null when genderIdentity is blank whitespace", () => {
28
+ expect(getDisplayGender({ genderIdentity: " ", gender: Gender.Male })).toBe(
29
+ Gender.Male
30
+ );
31
+ });
32
+
33
+ test("returns null when neither field is set", () => {
34
+ expect(getDisplayGender({})).toBeNull();
35
+ expect(getDisplayGender({ gender: null, genderIdentity: null })).toBeNull();
36
+ });
37
+
38
+ test("returns null when genderIdentity is empty and gender is null", () => {
39
+ expect(getDisplayGender({ genderIdentity: "", gender: null })).toBeNull();
40
+ });
41
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { formatVenueType, formatVenueTypes } from "../venueTypeFormatter.js";
3
+
4
+ describe("venueTypeFormatter", () => {
5
+ describe("formatVenueType", () => {
6
+ test("inserts spaces before capital letters", () => {
7
+ expect(formatVenueType("OutdoorSpace")).toBe("Outdoor Space");
8
+ expect(formatVenueType("WeddingVenue")).toBe("Wedding Venue");
9
+ });
10
+
11
+ test("returns empty string for falsy input", () => {
12
+ expect(formatVenueType("")).toBe("");
13
+ });
14
+ });
15
+
16
+ describe("formatVenueTypes", () => {
17
+ test("formats up to maxDisplay types", () => {
18
+ expect(formatVenueTypes(["Restaurant", "Bar"], 2)).toBe("Restaurant, Bar");
19
+ });
20
+
21
+ test("appends +N more when truncated", () => {
22
+ expect(formatVenueTypes(["Restaurant", "Bar", "Nightclub", "Rooftop"], 2)).toBe(
23
+ "Restaurant, Bar +2 more"
24
+ );
25
+ });
26
+
27
+ test("returns empty string for empty array", () => {
28
+ expect(formatVenueTypes([])).toBe("");
29
+ });
30
+ });
31
+ });