@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
@@ -1791,6 +1791,8 @@ model Ticket {
1791
1791
  * Set when we send the "your waitlist window expires soon" reminder (once per ticket).
1792
1792
  */
1793
1793
  waitlistReminderSentAt DateTime?
1794
+ /// When we last sent the 1-hour-before-start reminder (email/push/SMS); avoids duplicate cron sends.
1795
+ lastReminded1hAt DateTime?
1794
1796
  refundRequestedAt DateTime? // Set when RefundPending; cleared once refund processes
1795
1797
  checkInLocation String?
1796
1798
  checkOutLocation String?
@@ -2332,6 +2334,8 @@ model User {
2332
2334
  zipCode String?
2333
2335
  country String? @default("US")
2334
2336
  phone String?
2337
+ /// Set when the user completes SMS phone verification (OTP).
2338
+ phoneVerifiedAt DateTime?
2335
2339
  documentIDId String? @unique
2336
2340
  /// Encrypted-S3 key for the user's stored government-ID image (deleted per retention policy / on ID expiration)
2337
2341
  govIdImageS3Key String?
@@ -2639,6 +2643,24 @@ model User {
2639
2643
  // Vanity paths
2640
2644
  requestedVanityPaths EventVanityPath[] @relation("VanityRequests")
2641
2645
  approvedVanityPaths EventVanityPath[] @relation("VanityApprovals")
2646
+
2647
+ phoneVerificationCodes PhoneVerificationCode[]
2648
+ smsSendLogs SmsSendLog[]
2649
+ }
2650
+
2651
+ model PhoneVerificationCode {
2652
+ id String @id @default(cuid())
2653
+ userId String
2654
+ phoneE164 String
2655
+ codeHash String
2656
+ attempts Int @default(0)
2657
+ expiresAt DateTime
2658
+ consumedAt DateTime?
2659
+ createdAt DateTime @default(now())
2660
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
2661
+
2662
+ @@index([userId])
2663
+ @@index([phoneE164])
2642
2664
  }
2643
2665
 
2644
2666
  model UserPreferences {
@@ -3843,12 +3865,37 @@ model Organization {
3843
3865
  /// Recorded for analytics — informational only, NOT a gate for verification or fee waivers.
3844
3866
  stripeNonprofitPricingStatus StripeNonprofitPricingStatus @default(NotApplied)
3845
3867
 
3868
+ /// Monthly cap on estimated SMS spend (cents) for transactional templated SMS tied to this org.
3869
+ smsMonthlyCapCents Int @default(500)
3870
+ smsMonthlySentCents Int @default(0)
3871
+ smsMonthAnchor DateTime @default(now())
3872
+ smsAutoShutoffAt DateTime?
3873
+
3874
+ smsSendLogs SmsSendLog[]
3875
+
3846
3876
  @@index([ownerId])
3847
3877
  @@index([slug])
3848
3878
  @@index([parentOrganizationId])
3849
3879
  @@index([nonprofitVerifiedAt])
3850
3880
  }
3851
3881
 
3882
+ model SmsSendLog {
3883
+ id String @id @default(cuid())
3884
+ organizationId String?
3885
+ userId String
3886
+ templateKey String
3887
+ phoneE164 String
3888
+ segments Int
3889
+ estCostCents Int
3890
+ snsMessageId String?
3891
+ sentAt DateTime @default(now())
3892
+ organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
3893
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
3894
+
3895
+ @@index([organizationId, sentAt])
3896
+ @@index([userId, sentAt])
3897
+ }
3898
+
3852
3899
  /// Sports-specific metadata for league or team organizations (1:1 with Organization).
3853
3900
  model SportsProfile {
3854
3901
  id String @id @default(cuid())
@@ -14,7 +14,10 @@
14
14
  */
15
15
  import { BashEventType } from "@prisma/client";
16
16
 
17
- import { bashSectionTitleForBashType } from "../definitions.js";
17
+ import {
18
+ bashSectionTitleForBashType,
19
+ canonicalBashEventRowKey,
20
+ } from "../definitions.js";
18
21
 
19
22
  describe("bashSectionTitleForBashType", () => {
20
23
  describe("free-text scraper section keys (non-enum)", () => {
@@ -92,4 +95,48 @@ describe("bashSectionTitleForBashType", () => {
92
95
  expect(bashSectionTitleForBashType("Party")).toBe("Parties");
93
96
  });
94
97
  });
98
+
99
+ describe("keys that diverge from Prisma enum literals (case or display labels)", () => {
100
+ it("lower-case prisma value 'party' → 'Parties'", () => {
101
+ expect(bashSectionTitleForBashType("party")).toBe("Parties");
102
+ });
103
+
104
+ it("lower-case 'fundraiser' → 'Fundraisers'", () => {
105
+ expect(bashSectionTitleForBashType("fundraiser")).toBe("Fundraisers");
106
+ });
107
+
108
+ it("singular display label row key 'House Party' → 'House Parties'", () => {
109
+ expect(bashSectionTitleForBashType("House Party")).toBe("House Parties");
110
+ });
111
+
112
+ it("trimmed stray whitespace still pluralizes", () => {
113
+ expect(bashSectionTitleForBashType(" Fundraiser ")).toBe("Fundraisers");
114
+ });
115
+ });
116
+
117
+ describe("already-plural enum headings", () => {
118
+ it("normalizes casing when the row key matches a pluralized enum title", () => {
119
+ expect(bashSectionTitleForBashType("House Parties")).toBe(
120
+ "House Parties"
121
+ );
122
+ expect(bashSectionTitleForBashType("house parties")).toBe("House Parties");
123
+ expect(bashSectionTitleForBashType("Comedy Shows")).toBe("Comedy Shows");
124
+ });
125
+ });
126
+ });
127
+
128
+ describe("canonicalBashEventRowKey", () => {
129
+ it("maps singular labels and enum literals to the Prisma enum string", () => {
130
+ expect(canonicalBashEventRowKey("House Party")).toBe(
131
+ BashEventType.HouseParty
132
+ );
133
+ expect(canonicalBashEventRowKey(BashEventType.HouseParty)).toBe(
134
+ BashEventType.HouseParty
135
+ );
136
+ expect(canonicalBashEventRowKey("houseparty")).toBe(BashEventType.HouseParty);
137
+ });
138
+
139
+ it("returns trimmed free-text when no enum matches", () => {
140
+ expect(canonicalBashEventRowKey("Graduation")).toBe("Graduation");
141
+ });
95
142
  });
@@ -0,0 +1,136 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ createEmptyFilterFields,
4
+ removeDataFromResult,
5
+ canonicalBashEventRowKey,
6
+ PASSWORD_REQUIREMENTS_REGEX,
7
+ PASSWORD_MIN_LENGTH,
8
+ EMPTY_FILTER_FIELDS,
9
+ ticketIsPaidForEventAdmission,
10
+ isUnpaidPayAtDoorTicket,
11
+ TICKET_PURCHASE_TYPE_COMP,
12
+ TICKET_PURCHASE_TYPE_PAY_AT_DOOR,
13
+ } from "../definitions.js";
14
+
15
+ describe("definitions helpers", () => {
16
+ describe("createEmptyFilterFields", () => {
17
+ test("returns fresh arrays not shared with EMPTY_FILTER_FIELDS", () => {
18
+ const a = createEmptyFilterFields();
19
+ const b = createEmptyFilterFields();
20
+ a.price.push("free");
21
+ expect(b.price).toEqual([]);
22
+ expect(EMPTY_FILTER_FIELDS.price).toEqual([]);
23
+ });
24
+
25
+ test("includes default boolean flags", () => {
26
+ expect(createEmptyFilterFields()).toMatchObject({
27
+ hasOpenTasks: false,
28
+ acceptsBashPoints: false,
29
+ ambashadorOnly: false,
30
+ });
31
+ });
32
+ });
33
+
34
+ describe("removeDataFromResult", () => {
35
+ test("strips data while preserving error metadata", () => {
36
+ const result = {
37
+ data: { id: "1" },
38
+ error: true,
39
+ code: "ERR",
40
+ } as any;
41
+ expect(removeDataFromResult(result)).toEqual({ error: true, code: "ERR" });
42
+ });
43
+
44
+ test("returns undefined for nullish input", () => {
45
+ expect(removeDataFromResult(null)).toBeUndefined();
46
+ expect(removeDataFromResult(undefined)).toBeUndefined();
47
+ });
48
+ });
49
+
50
+ describe("canonicalBashEventRowKey", () => {
51
+ test("normalizes singular labels to enum keys", () => {
52
+ expect(canonicalBashEventRowKey("House Party")).toBe("HouseParty");
53
+ });
54
+
55
+ test("passes through unknown trimmed values", () => {
56
+ expect(canonicalBashEventRowKey(" Custom Row ")).toBe("Custom Row");
57
+ });
58
+
59
+ test("returns empty string for blank input", () => {
60
+ expect(canonicalBashEventRowKey(" ")).toBe("");
61
+ });
62
+ });
63
+
64
+ describe("PASSWORD_REQUIREMENTS_REGEX", () => {
65
+ test("requires min length and mixed character classes", () => {
66
+ expect(PASSWORD_MIN_LENGTH).toBe(8);
67
+ expect(PASSWORD_REQUIREMENTS_REGEX.test("Abcd1234!")).toBe(true);
68
+ expect(PASSWORD_REQUIREMENTS_REGEX.test("Sh0rt!")).toBe(false);
69
+ expect(PASSWORD_REQUIREMENTS_REGEX.test("alllowercase1!")).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe("ticketIsPaidForEventAdmission", () => {
74
+ test("free tiers always admit without payment", () => {
75
+ expect(
76
+ ticketIsPaidForEventAdmission({ purchaseType: "USD", paidOn: null }, 0)
77
+ ).toBe(true);
78
+ });
79
+
80
+ test("comp tickets admit without paidOn", () => {
81
+ expect(
82
+ ticketIsPaidForEventAdmission(
83
+ { purchaseType: TICKET_PURCHASE_TYPE_COMP, paidOn: null },
84
+ 2500
85
+ )
86
+ ).toBe(true);
87
+ });
88
+
89
+ test("pay-at-door requires paidOn when tier has price", () => {
90
+ expect(
91
+ ticketIsPaidForEventAdmission(
92
+ { purchaseType: TICKET_PURCHASE_TYPE_PAY_AT_DOOR, paidOn: null },
93
+ 1000
94
+ )
95
+ ).toBe(false);
96
+ expect(
97
+ ticketIsPaidForEventAdmission(
98
+ { purchaseType: TICKET_PURCHASE_TYPE_PAY_AT_DOOR, paidOn: new Date() },
99
+ 1000
100
+ )
101
+ ).toBe(true);
102
+ });
103
+
104
+ test("standard paid tickets require paidOn", () => {
105
+ expect(
106
+ ticketIsPaidForEventAdmission({ purchaseType: "USD", paidOn: null }, 500)
107
+ ).toBe(false);
108
+ expect(
109
+ ticketIsPaidForEventAdmission({ purchaseType: "USD", paidOn: new Date() }, 500)
110
+ ).toBe(true);
111
+ });
112
+ });
113
+
114
+ describe("isUnpaidPayAtDoorTicket", () => {
115
+ test("true only for pay-at-door without paidOn", () => {
116
+ expect(
117
+ isUnpaidPayAtDoorTicket({
118
+ purchaseType: TICKET_PURCHASE_TYPE_PAY_AT_DOOR,
119
+ paidOn: null,
120
+ })
121
+ ).toBe(true);
122
+ });
123
+
124
+ test("false once paid or for other purchase types", () => {
125
+ expect(
126
+ isUnpaidPayAtDoorTicket({
127
+ purchaseType: TICKET_PURCHASE_TYPE_PAY_AT_DOOR,
128
+ paidOn: new Date(),
129
+ })
130
+ ).toBe(false);
131
+ expect(
132
+ isUnpaidPayAtDoorTicket({ purchaseType: "USD", paidOn: null })
133
+ ).toBe(false);
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,50 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ BASH_EVENT_DATA_TO_CLONE,
4
+ BASH_EVENT_DATA_TO_REMOVE,
5
+ FRONT_END_USER_DATA_TO_SELECT,
6
+ PUBLIC_USER_SUBSCRIPTION_DATA_TO_SELECT,
7
+ TICKET_TIER_DATA_TO_INCLUDE,
8
+ } from "../extendedSchemas.js";
9
+
10
+ describe("extendedSchemas exports", () => {
11
+ test("PUBLIC_USER_SUBSCRIPTION_DATA_TO_SELECT includes subscription counters", () => {
12
+ expect(PUBLIC_USER_SUBSCRIPTION_DATA_TO_SELECT).toMatchObject({
13
+ type: true,
14
+ status: true,
15
+ ownerId: true,
16
+ subscriptionCount: true,
17
+ listedCount: true,
18
+ });
19
+ });
20
+
21
+ test("FRONT_END_USER_DATA_TO_SELECT exposes public profile fields", () => {
22
+ expect(FRONT_END_USER_DATA_TO_SELECT).toMatchObject({
23
+ id: true,
24
+ username: true,
25
+ email: true,
26
+ givenName: true,
27
+ familyName: true,
28
+ });
29
+ });
30
+
31
+ test("TICKET_TIER_DATA_TO_INCLUDE nests tickets and promoCodes", () => {
32
+ expect(TICKET_TIER_DATA_TO_INCLUDE.tickets).toBeDefined();
33
+ expect(TICKET_TIER_DATA_TO_INCLUDE.promoCodes).toBe(true);
34
+ });
35
+
36
+ test("BASH_EVENT_DATA_TO_CLONE lists cloneable relations", () => {
37
+ expect(BASH_EVENT_DATA_TO_CLONE).toEqual([
38
+ "ticketTiers",
39
+ "media",
40
+ "recurrence",
41
+ "invitations",
42
+ ]);
43
+ });
44
+
45
+ test("BASH_EVENT_DATA_TO_REMOVE strips nested creator and tasks", () => {
46
+ expect(BASH_EVENT_DATA_TO_REMOVE).toContain("creator");
47
+ expect(BASH_EVENT_DATA_TO_REMOVE).toContain("eventTasks");
48
+ expect(BASH_EVENT_DATA_TO_REMOVE).toContain("tickets");
49
+ });
50
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { MembershipTier } from "@prisma/client";
3
+ import { BashPassTier } from "@prisma/client";
4
+ import {
5
+ calculateBashEventFee,
6
+ calculateServiceBookingFee,
7
+ calculatePartnerPaymentFee,
8
+ isTrustedHostMembershipTier,
9
+ hasFeatureAccess,
10
+ getRequiredTierForFeature,
11
+ hasFeatureLimit,
12
+ getFeatureLimit,
13
+ hasReachedFeatureLimit,
14
+ MEMBERSHIP_FEATURES,
15
+ BASIC_TIER_MAX_PROMO_CODES_PER_EVENT,
16
+ MEMBERSHIP_BASH_PASS_TIER,
17
+ MEMBERSHIP_TIER_ORDER,
18
+ MEMBERSHIP_TIER_HIERARCHY,
19
+ } from "../membershipDefinitions.js";
20
+
21
+ describe("membershipDefinitions helpers", () => {
22
+ describe("fee calculators", () => {
23
+ test("calculateBashEventFee applies percent, minimum, and cap", () => {
24
+ expect(calculateBashEventFee(10000, MembershipTier.Basic)).toBeGreaterThanOrEqual(0);
25
+ expect(calculateBashEventFee(0, MembershipTier.Premium)).toBe(
26
+ calculateBashEventFee(1, MembershipTier.Premium)
27
+ );
28
+ });
29
+
30
+ test("calculateServiceBookingFee includes Stripe fixed fee", () => {
31
+ const fee = calculateServiceBookingFee(10000, MembershipTier.Basic);
32
+ expect(fee).toBeGreaterThan(0);
33
+ });
34
+
35
+ test("calculatePartnerPaymentFee scales with amount", () => {
36
+ const small = calculatePartnerPaymentFee(1000, MembershipTier.Pro);
37
+ const large = calculatePartnerPaymentFee(100000, MembershipTier.Pro);
38
+ expect(large).toBeGreaterThan(small);
39
+ });
40
+ });
41
+
42
+ describe("isTrustedHostMembershipTier", () => {
43
+ test("Premium and above are trusted", () => {
44
+ expect(isTrustedHostMembershipTier(MembershipTier.Premium)).toBe(true);
45
+ expect(isTrustedHostMembershipTier(MembershipTier.Legend)).toBe(true);
46
+ });
47
+
48
+ test("Basic and missing tier are not trusted", () => {
49
+ expect(isTrustedHostMembershipTier(MembershipTier.Basic)).toBe(false);
50
+ expect(isTrustedHostMembershipTier(null)).toBe(false);
51
+ });
52
+ });
53
+
54
+ describe("hasFeatureAccess", () => {
55
+ test("Basic gets promo codes", () => {
56
+ expect(hasFeatureAccess(MembershipTier.Basic, MEMBERSHIP_FEATURES.PROMO_CODES)).toBe(true);
57
+ });
58
+
59
+ test("Pro unlocks vendor directory", () => {
60
+ expect(hasFeatureAccess(MembershipTier.Pro, MEMBERSHIP_FEATURES.VENDOR_DIRECTORY)).toBe(
61
+ true
62
+ );
63
+ expect(hasFeatureAccess(MembershipTier.Premium, MEMBERSHIP_FEATURES.VENDOR_DIRECTORY)).toBe(
64
+ false
65
+ );
66
+ });
67
+
68
+ test("unknown feature returns false", () => {
69
+ expect(hasFeatureAccess(MembershipTier.Legend, "not_a_feature")).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe("getRequiredTierForFeature", () => {
74
+ test("returns minimum tier that unlocks feature", () => {
75
+ expect(getRequiredTierForFeature(MEMBERSHIP_FEATURES.PROMO_CODES)).toBe(MembershipTier.Basic);
76
+ expect(getRequiredTierForFeature(MEMBERSHIP_FEATURES.VENDOR_DIRECTORY)).toBe(
77
+ MembershipTier.Pro
78
+ );
79
+ });
80
+ });
81
+
82
+ describe("hasFeatureLimit / getFeatureLimit", () => {
83
+ test("reads boolean limits from MEMBERSHIP_LIMITS", () => {
84
+ expect(hasFeatureLimit(MembershipTier.Basic, "canCreateEvents")).toBe(true);
85
+ expect(hasFeatureLimit(MembershipTier.Basic, "analyticsAccess")).toBe(false);
86
+ expect(getFeatureLimit(MembershipTier.Pro, "maxServiceListings")).toBe(3);
87
+ });
88
+ });
89
+
90
+ describe("hasReachedFeatureLimit", () => {
91
+ test("blocks when featured event quota is exhausted", () => {
92
+ expect(hasReachedFeatureLimit(MembershipTier.Pro, "featuredEvents", 1)).toBe(true);
93
+ expect(hasReachedFeatureLimit(MembershipTier.Pro, "featuredEvents", 0)).toBe(false);
94
+ });
95
+ });
96
+
97
+ test("Basic tier promo code cap constant", () => {
98
+ expect(BASIC_TIER_MAX_PROMO_CODES_PER_EVENT).toBe(3);
99
+ });
100
+
101
+ describe("tier ordering constants", () => {
102
+ test("MEMBERSHIP_TIER_HIERARCHY starts at Basic and ends at Legend", () => {
103
+ expect(MEMBERSHIP_TIER_HIERARCHY[0]).toBe(MembershipTier.Basic);
104
+ expect(MEMBERSHIP_TIER_HIERARCHY.at(-1)).toBe(MembershipTier.Legend);
105
+ });
106
+
107
+ test("MEMBERSHIP_TIER_ORDER increases with tier level", () => {
108
+ expect(MEMBERSHIP_TIER_ORDER[MembershipTier.Basic]).toBeLessThan(
109
+ MEMBERSHIP_TIER_ORDER[MembershipTier.Premium]
110
+ );
111
+ expect(MEMBERSHIP_TIER_ORDER[MembershipTier.Pro]).toBeLessThan(
112
+ MEMBERSHIP_TIER_ORDER[MembershipTier.Legend]
113
+ );
114
+ });
115
+ });
116
+
117
+ describe("MEMBERSHIP_BASH_PASS_TIER", () => {
118
+ test("maps paid membership tiers to bundled BashPass levels", () => {
119
+ expect(MEMBERSHIP_BASH_PASS_TIER[MembershipTier.Premium]).toBe(BashPassTier.LITE);
120
+ expect(MEMBERSHIP_BASH_PASS_TIER[MembershipTier.Pro]).toBe(BashPassTier.STANDARD);
121
+ expect(MEMBERSHIP_BASH_PASS_TIER[MembershipTier.Elite]).toBe(BashPassTier.UNLIMITED);
122
+ expect(MEMBERSHIP_BASH_PASS_TIER[MembershipTier.Basic]).toBeUndefined();
123
+ });
124
+ });
125
+
126
+ describe("getRequiredTierForFeature edge cases", () => {
127
+ test("returns null for unknown feature", () => {
128
+ expect(getRequiredTierForFeature("__not_a_real_feature__")).toBeNull();
129
+ });
130
+ });
131
+
132
+ describe("hasReachedFeatureLimit default branch", () => {
133
+ test("returns false for unsupported feature keys", () => {
134
+ expect(hasReachedFeatureLimit(MembershipTier.Pro, "featuredEvents", 999)).toBe(true);
135
+ expect(
136
+ hasReachedFeatureLimit(
137
+ MembershipTier.Pro,
138
+ "unknownFeature" as unknown as "featuredEvents",
139
+ 0
140
+ )
141
+ ).toBe(false);
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Ticket admission helpers — paid vs pay-at-door state.
3
+ */
4
+
5
+ import { describe, expect, test } from "@jest/globals";
6
+ import {
7
+ ticketIsPaidForEventAdmission,
8
+ isUnpaidPayAtDoorTicket,
9
+ TICKET_PURCHASE_TYPE_COMP,
10
+ TICKET_PURCHASE_TYPE_PAY_AT_DOOR,
11
+ TICKET_PURCHASE_TYPE_USD,
12
+ } from "../definitions.js";
13
+
14
+ describe("ticketIsPaidForEventAdmission", () => {
15
+ test("free tiers always admit without payment", () => {
16
+ expect(ticketIsPaidForEventAdmission({ purchaseType: TICKET_PURCHASE_TYPE_USD, paidOn: null }, 0)).toBe(
17
+ true
18
+ );
19
+ expect(
20
+ ticketIsPaidForEventAdmission({ purchaseType: TICKET_PURCHASE_TYPE_PAY_AT_DOOR, paidOn: null }, 0)
21
+ ).toBe(true);
22
+ });
23
+
24
+ test("comp tickets admit regardless of paidOn", () => {
25
+ expect(
26
+ ticketIsPaidForEventAdmission({ purchaseType: TICKET_PURCHASE_TYPE_COMP, paidOn: null }, 2500)
27
+ ).toBe(true);
28
+ });
29
+
30
+ test("paid USD tickets admit when paidOn is set", () => {
31
+ expect(
32
+ ticketIsPaidForEventAdmission(
33
+ { purchaseType: TICKET_PURCHASE_TYPE_USD, paidOn: new Date("2024-01-01") },
34
+ 2500
35
+ )
36
+ ).toBe(true);
37
+ });
38
+
39
+ test("unpaid USD tickets do not admit", () => {
40
+ expect(
41
+ ticketIsPaidForEventAdmission({ purchaseType: TICKET_PURCHASE_TYPE_USD, paidOn: null }, 2500)
42
+ ).toBe(false);
43
+ });
44
+
45
+ test("pay-at-door tickets admit only after paidOn is set", () => {
46
+ const unpaid = { purchaseType: TICKET_PURCHASE_TYPE_PAY_AT_DOOR, paidOn: null };
47
+ const paid = { purchaseType: TICKET_PURCHASE_TYPE_PAY_AT_DOOR, paidOn: "2024-06-01T12:00:00Z" };
48
+ expect(ticketIsPaidForEventAdmission(unpaid, 1500)).toBe(false);
49
+ expect(ticketIsPaidForEventAdmission(paid, 1500)).toBe(true);
50
+ });
51
+ });
52
+
53
+ describe("isUnpaidPayAtDoorTicket", () => {
54
+ test("returns true only for pay-at-door without paidOn", () => {
55
+ expect(isUnpaidPayAtDoorTicket({ purchaseType: TICKET_PURCHASE_TYPE_PAY_AT_DOOR, paidOn: null })).toBe(
56
+ true
57
+ );
58
+ });
59
+
60
+ test("returns false once pay-at-door ticket is marked paid", () => {
61
+ expect(
62
+ isUnpaidPayAtDoorTicket({
63
+ purchaseType: TICKET_PURCHASE_TYPE_PAY_AT_DOOR,
64
+ paidOn: new Date(),
65
+ })
66
+ ).toBe(false);
67
+ });
68
+
69
+ test("returns false for other purchase types even when unpaid", () => {
70
+ expect(isUnpaidPayAtDoorTicket({ purchaseType: TICKET_PURCHASE_TYPE_USD, paidOn: null })).toBe(false);
71
+ expect(isUnpaidPayAtDoorTicket({ purchaseType: TICKET_PURCHASE_TYPE_COMP, paidOn: null })).toBe(false);
72
+ });
73
+ });
@@ -0,0 +1,118 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
2
+ import {
3
+ getStoredUtmFields,
4
+ utmFieldsFromCheckoutArgs,
5
+ utmFieldsFromStripeMetadata,
6
+ utmFieldsToStripeMetadata,
7
+ utmFieldsToTicketColumns,
8
+ } from "../utmAttribution.js";
9
+
10
+ describe("utmAttribution", () => {
11
+ describe("utmFieldsFromCheckoutArgs", () => {
12
+ test("returns undefined when all fields blank", () => {
13
+ expect(utmFieldsFromCheckoutArgs({ utmSource: " ", utmMedium: "", utmCampaign: undefined })).toBeUndefined();
14
+ });
15
+
16
+ test("trims and includes only non-empty fields", () => {
17
+ expect(
18
+ utmFieldsFromCheckoutArgs({
19
+ utmSource: " newsletter ",
20
+ utmMedium: "email",
21
+ })
22
+ ).toEqual({ utmSource: "newsletter", utmMedium: "email" });
23
+ });
24
+
25
+ test("caps field length at 500 characters", () => {
26
+ const long = "a".repeat(600);
27
+ const result = utmFieldsFromCheckoutArgs({ utmSource: long });
28
+ expect(result?.utmSource?.length).toBe(500);
29
+ });
30
+ });
31
+
32
+ describe("utmFieldsToStripeMetadata", () => {
33
+ test("maps to snake_case Stripe keys", () => {
34
+ expect(
35
+ utmFieldsToStripeMetadata({
36
+ utmSource: "google",
37
+ utmCampaign: "spring",
38
+ })
39
+ ).toEqual({ utm_source: "google", utm_campaign: "spring" });
40
+ });
41
+
42
+ test("returns empty object for undefined input", () => {
43
+ expect(utmFieldsToStripeMetadata(undefined)).toEqual({});
44
+ });
45
+ });
46
+
47
+ describe("utmFieldsToTicketColumns", () => {
48
+ test("maps to Prisma Ticket column names", () => {
49
+ expect(
50
+ utmFieldsToTicketColumns({
51
+ utmSource: "fb",
52
+ utmMedium: "cpc",
53
+ utmCampaign: "launch",
54
+ })
55
+ ).toEqual({
56
+ utmSource: "fb",
57
+ utmMedium: "cpc",
58
+ utmCampaign: "launch",
59
+ });
60
+ });
61
+
62
+ test("omits empty fields", () => {
63
+ expect(utmFieldsToTicketColumns({ utmSource: "x" })).toEqual({ utmSource: "x" });
64
+ });
65
+ });
66
+
67
+ describe("utmFieldsFromStripeMetadata", () => {
68
+ test("reads snake_case metadata into camelCase fields", () => {
69
+ expect(
70
+ utmFieldsFromStripeMetadata({
71
+ utm_source: " twitter ",
72
+ utm_medium: "social",
73
+ })
74
+ ).toEqual({ utmSource: "twitter", utmMedium: "social" });
75
+ });
76
+
77
+ test("returns empty object for null metadata", () => {
78
+ expect(utmFieldsFromStripeMetadata(null)).toEqual({});
79
+ });
80
+ });
81
+
82
+ describe("getStoredUtmFields", () => {
83
+ const originalSessionStorage = global.sessionStorage;
84
+
85
+ beforeEach(() => {
86
+ const store = new Map<string, string>();
87
+ Object.defineProperty(global, "sessionStorage", {
88
+ configurable: true,
89
+ value: {
90
+ getItem: (key: string) => store.get(key) ?? null,
91
+ setItem: (key: string, value: string) => {
92
+ store.set(key, value);
93
+ },
94
+ removeItem: (key: string) => {
95
+ store.delete(key);
96
+ },
97
+ clear: () => store.clear(),
98
+ },
99
+ });
100
+ });
101
+
102
+ afterEach(() => {
103
+ Object.defineProperty(global, "sessionStorage", {
104
+ configurable: true,
105
+ value: originalSessionStorage,
106
+ });
107
+ });
108
+
109
+ test("reads previously stored session values", () => {
110
+ sessionStorage.setItem("bash_utm_source", "newsletter");
111
+ sessionStorage.setItem("bash_utm_campaign", "june");
112
+ expect(getStoredUtmFields()).toEqual({
113
+ utmSource: "newsletter",
114
+ utmCampaign: "june",
115
+ });
116
+ });
117
+ });
118
+ });