@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
@@ -1,5 +1,4 @@
1
1
  import {
2
- AgePolicy,
3
2
  BashEventType,
4
3
  BracketType,
5
4
  CompetitionType,
@@ -18,6 +17,7 @@ import {
18
17
  User,
19
18
  } from "@prisma/client";
20
19
  import type {
20
+ AgePolicy as AgePolicyEnum,
21
21
  BashPassTier,
22
22
  BashEventDressTags,
23
23
  BashEventVibeTags,
@@ -28,6 +28,7 @@ import type {
28
28
  } from "@prisma/client";
29
29
  import {
30
30
  CheckoutExt,
31
+ PublicTeaserUser,
31
32
  PublicUser,
32
33
  ServiceBookingExt,
33
34
  type UserExt,
@@ -79,6 +80,15 @@ export const GovIdRetentionPolicy = {
79
80
 
80
81
  export type GovIdRetentionPolicy = GovIdRetentionPolicyEnum;
81
82
 
83
+ /** Mirrors Prisma `AgePolicy` — `@prisma/client` browser bundle omits runtime enum exports. */
84
+ export const AgePolicy = {
85
+ AllAges: "AllAges",
86
+ EighteenPlus: "EighteenPlus",
87
+ TwentyOnePlus: "TwentyOnePlus",
88
+ } as const satisfies Record<AgePolicyEnum, AgePolicyEnum>;
89
+
90
+ export type AgePolicy = AgePolicyEnum;
91
+
82
92
  export const PASSWORD_MIN_LENGTH = 8 as const;
83
93
  export const PASSWORD_REQUIREMENTS_REGEX = new RegExp(
84
94
  String.raw`^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&^#])[A-Za-z\d@$!%*?&^#]{${PASSWORD_MIN_LENGTH},}$`
@@ -708,7 +718,7 @@ export interface LoginSession {
708
718
  }
709
719
 
710
720
  export interface AttendeeOfBashEvent {
711
- user: PublicUser;
721
+ user: PublicUser | PublicTeaserUser;
712
722
  tickets: Ticket[];
713
723
  }
714
724
 
@@ -1114,7 +1124,6 @@ export const YearsOfExperienceToString: { [key in YearsOfExperienceEnum]: string
1114
1124
 
1115
1125
  // Export Prisma enums for use in frontend
1116
1126
  export {
1117
- AgePolicy,
1118
1127
  BracketType,
1119
1128
  CompetitionType,
1120
1129
  EntertainmentServiceType,
@@ -1481,24 +1490,25 @@ const WHOLE_WORD_BASH_LABEL_PLURAL: ReadonlyMap<string, string> = new Map([
1481
1490
  ]);
1482
1491
 
1483
1492
  function pluralizeBashEventSingularLabel(label: string): string {
1484
- const manual = BASH_EVENT_SINGULAR_LABEL_TO_PLURAL_SECTION.get(label);
1493
+ const t = label.trim();
1494
+ const manual = BASH_EVENT_SINGULAR_LABEL_TO_PLURAL_SECTION.get(t);
1485
1495
  if (manual) return manual;
1486
1496
 
1487
- const wholeWord = WHOLE_WORD_BASH_LABEL_PLURAL.get(label);
1497
+ const wholeWord = WHOLE_WORD_BASH_LABEL_PLURAL.get(t);
1488
1498
  if (wholeWord) return wholeWord;
1489
1499
 
1490
- if (label === "Party") return "Parties";
1491
- if (label.endsWith(" Party")) {
1492
- return `${label.slice(0, -" Party".length)} Parties`;
1500
+ if (t === "Party") return "Parties";
1501
+ if (t.endsWith(" Party")) {
1502
+ return `${t.slice(0, -" Party".length)} Parties`;
1493
1503
  }
1494
- if (label.endsWith("-Party")) {
1495
- return `${label.slice(0, -"-Party".length)}-Parties`;
1504
+ if (t.endsWith("-Party")) {
1505
+ return `${t.slice(0, -"-Party".length)}-Parties`;
1496
1506
  }
1497
- if (label.endsWith(" Party)")) {
1498
- return `${label.replace(/ Party\)$/, " Parties)")}`;
1507
+ if (t.endsWith(" Party)")) {
1508
+ return `${t.replace(/ Party\)$/, " Parties)")}`;
1499
1509
  }
1500
- if (label.endsWith(" Rave")) {
1501
- return `${label.slice(0, -" Rave".length)} Raves`;
1510
+ if (t.endsWith(" Rave")) {
1511
+ return `${t.slice(0, -" Rave".length)} Raves`;
1502
1512
  }
1503
1513
 
1504
1514
  const replacements: ReadonlyArray<[suffix: string, plural: string]> = [
@@ -1535,8 +1545,8 @@ function pluralizeBashEventSingularLabel(label: string): string {
1535
1545
  ];
1536
1546
 
1537
1547
  for (const [suffix, plural] of replacements) {
1538
- if (label.endsWith(suffix)) {
1539
- return `${label.slice(0, -suffix.length)}${plural}`;
1548
+ if (t.endsWith(suffix)) {
1549
+ return `${t.slice(0, -suffix.length)}${plural}`;
1540
1550
  }
1541
1551
  }
1542
1552
 
@@ -1547,32 +1557,85 @@ function pluralizeBashEventSingularLabel(label: string): string {
1547
1557
  // - a future enum value not yet covered above
1548
1558
  // For all of these, returning the label verbatim is correct. Appending
1549
1559
  // " bashes" produces "Raves bashes" / "Comedy bashes", which reads wrong.
1550
- return label;
1560
+ return t;
1551
1561
  }
1552
1562
 
1553
1563
  const BASH_EVENT_TYPE_ENUM_VALUES: ReadonlySet<string> = new Set(
1554
1564
  Object.values(BashEventType) as string[]
1555
1565
  );
1556
1566
 
1567
+ function resolveCanonicalBashEventTypeKey(raw: string): string | undefined {
1568
+ const t = raw.trim();
1569
+ if (!t) return undefined;
1570
+ if (BASH_EVENT_TYPE_ENUM_VALUES.has(t)) return t;
1571
+ const lower = t.toLowerCase();
1572
+ for (const v of BASH_EVENT_TYPE_ENUM_VALUES) {
1573
+ if (v.toLowerCase() === lower) return v;
1574
+ }
1575
+ return undefined;
1576
+ }
1577
+
1578
+ /**
1579
+ * When the row key matches a `BashEventTypeToString` value (e.g. "House Party")
1580
+ * but not the Prisma enum literal (`HouseParty`), still pluralize like the enum.
1581
+ */
1582
+ function resolveBashEventEnumKeyFromSingularLabel(raw: string): string | undefined {
1583
+ const t = raw.trim();
1584
+ if (!t) return undefined;
1585
+ const lower = t.toLowerCase();
1586
+ for (const enumKey of BASH_EVENT_TYPE_ENUM_VALUES) {
1587
+ const k = enumKey as BashEventType;
1588
+ const label = BashEventTypeToString[k];
1589
+ if (label === t || label.toLowerCase() === lower) return enumKey;
1590
+ }
1591
+ return undefined;
1592
+ }
1593
+
1594
+ /**
1595
+ * Stable row key for homepage/category grouping when `eventType` may be stored
1596
+ * as a Prisma enum literal (e.g. `HouseParty`) or as the singular UI label
1597
+ * (`House Party`).
1598
+ */
1599
+ export function canonicalBashEventRowKey(raw: string): string {
1600
+ const trimmed = raw.trim();
1601
+ if (!trimmed) return trimmed;
1602
+ const enumKey =
1603
+ resolveCanonicalBashEventTypeKey(trimmed) ??
1604
+ resolveBashEventEnumKeyFromSingularLabel(trimmed);
1605
+ if (enumKey != null) return enumKey;
1606
+ return trimmed;
1607
+ }
1608
+
1557
1609
  /**
1558
1610
  * Heading shown on the home page row and the `/category/...` page for a bash
1559
1611
  * `eventType` / section key. Uses plural-friendly copy for Prisma
1560
1612
  * `BashEventType` values; keeps synthetic rows and membership tier rows clear.
1561
1613
  */
1562
1614
  export function bashSectionTitleForBashType(bashType: string): string {
1563
- const synthetic = SYNTHETIC_BASH_SECTION_TITLE[bashType];
1615
+ const raw = bashType.trim();
1616
+ const lower = raw.toLowerCase();
1617
+
1618
+ const synthetic = SYNTHETIC_BASH_SECTION_TITLE[raw];
1564
1619
  if (synthetic != null) return synthetic;
1620
+ if (/\bEvents$/i.test(raw)) return raw;
1565
1621
 
1566
- if (/\bEvents$/i.test(bashType)) {
1567
- return bashType;
1622
+ for (const enumKey of BASH_EVENT_TYPE_ENUM_VALUES) {
1623
+ const singular = BashEventTypeToString[enumKey as BashEventType];
1624
+ const pluralHeading = pluralizeBashEventSingularLabel(singular);
1625
+ if (pluralHeading.toLowerCase() === lower) {
1626
+ return pluralHeading;
1627
+ }
1568
1628
  }
1569
1629
 
1570
- if (BASH_EVENT_TYPE_ENUM_VALUES.has(bashType)) {
1571
- const singular = BashEventTypeToString[bashType as BashEventType];
1630
+ const enumKey =
1631
+ resolveCanonicalBashEventTypeKey(raw) ??
1632
+ resolveBashEventEnumKeyFromSingularLabel(raw);
1633
+ if (enumKey != null) {
1634
+ const singular = BashEventTypeToString[enumKey as BashEventType];
1572
1635
  return pluralizeBashEventSingularLabel(singular);
1573
1636
  }
1574
1637
 
1575
- const spaced = insertSpacesInCamelCase(bashType);
1638
+ const spaced = insertSpacesInCamelCase(raw);
1576
1639
  return pluralizeBashEventSingularLabel(spaced);
1577
1640
  }
1578
1641
 
@@ -199,6 +199,25 @@ export const FRONT_END_USER_DATA_TO_SELECT = {
199
199
  },
200
200
  } satisfies Prisma.UserSelect;
201
201
 
202
+ /**
203
+ * Minimal `User` fields for public teaser surfaces (event detail avatars, checkout rows,
204
+ * non-privileged attendee rosters). Omits `email` and other account-level PII.
205
+ */
206
+ export const PUBLIC_TEASER_USER_DATA_TO_SELECT = {
207
+ id: true,
208
+ username: true,
209
+ givenName: true,
210
+ familyName: true,
211
+ image: true,
212
+ imageUpdatedAt: true,
213
+ uploadedImage: true,
214
+ uploadedImageUpdatedAt: true,
215
+ } satisfies Prisma.UserSelect;
216
+
217
+ export type PublicTeaserUser = Prisma.UserGetPayload<{
218
+ select: typeof PUBLIC_TEASER_USER_DATA_TO_SELECT;
219
+ }>;
220
+
202
221
  export const PRIVATE_USER_ACCOUNT_TO_SELECT = {
203
222
  ...FRONT_END_USER_DATA_TO_SELECT,
204
223
  /** Self session only — preferred payment rail (Stripe vs Square). */
@@ -213,6 +232,8 @@ export const PRIVATE_USER_ACCOUNT_TO_SELECT = {
213
232
  zipCode: true,
214
233
  country: true,
215
234
  phone: true,
235
+ /** SMS verification — self account only. */
236
+ phoneVerifiedAt: true,
216
237
  membershipExpiresAt: true,
217
238
  bashPassStripeSubscriptionId: true,
218
239
  bashPassCurrentPeriodEnd: true,
@@ -1287,7 +1308,8 @@ export type TicketFlexible = Ticket & {
1287
1308
  };
1288
1309
 
1289
1310
  export interface CheckoutExt extends Checkout {
1290
- owner: PublicUser;
1311
+ /** Full owner row for hosts/organizers; teaser row (no email) for other authenticated viewers. */
1312
+ owner: PublicUser | PublicTeaserUser;
1291
1313
  tickets: Ticket[];
1292
1314
  }
1293
1315
 
@@ -1302,6 +1324,18 @@ export const CHECKOUT_DATA_TO_INCLUDE = {
1302
1324
  },
1303
1325
  } satisfies Prisma.CheckoutInclude;
1304
1326
 
1327
+ /** Checkout list for non-privileged viewers — buyer email omitted on `owner`. */
1328
+ export const CHECKOUT_DATA_TO_INCLUDE_TEASER_OWNER = {
1329
+ owner: {
1330
+ select: PUBLIC_TEASER_USER_DATA_TO_SELECT,
1331
+ },
1332
+ tickets: {
1333
+ select: {
1334
+ ownerId: true,
1335
+ },
1336
+ },
1337
+ } satisfies Prisma.CheckoutInclude;
1338
+
1305
1339
  export interface ReviewExt extends Review {
1306
1340
  comments?: BashComment[];
1307
1341
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./definitions.js";
2
+ export * from "./sms/smsTemplates.js";
2
3
  export * from "./legalTemplates.js";
3
4
  export * from "./utmAttribution.js";
4
5
  export * from "./venueLoyaltyRedemption.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * System SMS template keys — bodies are rendered in the API (URLs, env).
3
+ * Consumers use these keys for type-safe `sendTemplatedSms` calls.
4
+ */
5
+ export const SMS_TEMPLATE_KEYS = [
6
+ "EVENT_REMINDER_1H",
7
+ "EVENT_VENUE_CHANGED",
8
+ "EVENT_WAITLIST_APPROVED",
9
+ "TICKET_CONFIRMED",
10
+ ] as const;
11
+
12
+ export type SmsTemplateKey = (typeof SMS_TEMPLATE_KEYS)[number];
13
+
14
+ /** Typed context per template (API maps these to final message strings). */
15
+ export type SmsTemplateContext = {
16
+ EVENT_REMINDER_1H: { eventTitle: string; shortUrl: string };
17
+ EVENT_VENUE_CHANGED: { eventTitle: string; shortUrl: string };
18
+ EVENT_WAITLIST_APPROVED: { eventTitle: string; shortUrl: string };
19
+ TICKET_CONFIRMED: { eventTitle: string; ticketCount: number; ticketsUrl?: string };
20
+ };
@@ -0,0 +1,176 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ addressHasEnoughDataForGeolocation,
4
+ addressValuesToDatabaseAddressString,
5
+ databaseAddressStringToAddressValues,
6
+ formatLocalityForCardDisplay,
7
+ databaseAddressStringToOneLineString,
8
+ databaseAddressStringToDisplayString,
9
+ addressToDisplayString,
10
+ extractAddressComponents,
11
+ } from "../addressUtils.js";
12
+
13
+ describe("addressUtils", () => {
14
+ describe("addressHasEnoughDataForGeolocation", () => {
15
+ test("returns true when place, city, and state are set", () => {
16
+ expect(
17
+ addressHasEnoughDataForGeolocation({
18
+ place: "Madison Square Garden",
19
+ city: "New York",
20
+ state: "NY",
21
+ } as any)
22
+ ).toBe(true);
23
+ });
24
+
25
+ test("returns true when street, city, and state are set", () => {
26
+ expect(
27
+ addressHasEnoughDataForGeolocation({
28
+ street: "4 Pennsylvania Plaza",
29
+ city: "New York",
30
+ state: "NY",
31
+ } as any)
32
+ ).toBe(true);
33
+ });
34
+
35
+ test("returns false when city or state is missing", () => {
36
+ expect(
37
+ addressHasEnoughDataForGeolocation({ place: "Venue", city: "NYC", state: "" } as any)
38
+ ).toBe(false);
39
+ });
40
+ });
41
+
42
+ describe("addressValuesToDatabaseAddressString", () => {
43
+ test("joins fields with pipe delimiter", () => {
44
+ expect(
45
+ addressValuesToDatabaseAddressString({
46
+ place: "The Loft",
47
+ street: "123 Main St",
48
+ city: "Brooklyn",
49
+ state: "NY",
50
+ zipCode: "11201",
51
+ country: "USA",
52
+ })
53
+ ).toBe("The Loft|123 Main St|Brooklyn|NY|11201|USA");
54
+ });
55
+ });
56
+
57
+ describe("databaseAddressStringToAddressValues", () => {
58
+ test("parses pipe-delimited strings", () => {
59
+ expect(databaseAddressStringToAddressValues("Venue|1 Ave|NYC|NY|10001|USA")).toEqual({
60
+ place: "Venue",
61
+ street: "1 Ave",
62
+ city: "NYC",
63
+ state: "NY",
64
+ zipCode: "10001",
65
+ country: "USA",
66
+ });
67
+ });
68
+
69
+ test("parses legacy bullet-delimited 5-part strings", () => {
70
+ expect(
71
+ databaseAddressStringToAddressValues("123 Main St • Brooklyn • NY • 11201 • USA")
72
+ ).toEqual({
73
+ place: "",
74
+ street: "123 Main St",
75
+ city: "Brooklyn",
76
+ state: "NY",
77
+ zipCode: "11201",
78
+ country: "USA",
79
+ });
80
+ });
81
+
82
+ test("treats undelimited string as place-only", () => {
83
+ expect(databaseAddressStringToAddressValues("Central Park")).toEqual({
84
+ place: "Central Park",
85
+ street: "",
86
+ city: "",
87
+ state: "",
88
+ zipCode: "",
89
+ country: "",
90
+ });
91
+ });
92
+
93
+ test("returns empty address for null/undefined", () => {
94
+ expect(databaseAddressStringToAddressValues(null)).toEqual({
95
+ place: "",
96
+ street: "",
97
+ city: "",
98
+ state: "",
99
+ zipCode: "",
100
+ country: "",
101
+ });
102
+ });
103
+ });
104
+
105
+ describe("formatLocalityForCardDisplay", () => {
106
+ test("joins city, state, and country", () => {
107
+ expect(formatLocalityForCardDisplay("Brooklyn", "NY", "USA")).toBe("Brooklyn, NY, USA");
108
+ });
109
+
110
+ test("skips empty parts", () => {
111
+ expect(formatLocalityForCardDisplay("Austin", null, "USA")).toBe("Austin, USA");
112
+ });
113
+ });
114
+
115
+ describe("databaseAddressStringToOneLineString", () => {
116
+ test("formats full pipe-delimited address", () => {
117
+ const db = addressValuesToDatabaseAddressString({
118
+ place: "Rooftop Bar",
119
+ street: "99 King St",
120
+ city: "Seattle",
121
+ state: "WA",
122
+ zipCode: "98101",
123
+ country: "USA",
124
+ });
125
+ expect(databaseAddressStringToOneLineString(db)).toBe(
126
+ "Rooftop Bar, 99 King St, Seattle, WA 98101"
127
+ );
128
+ });
129
+
130
+ test("returns place-only for undelimited strings", () => {
131
+ expect(databaseAddressStringToOneLineString("Secret Warehouse")).toBe("Secret Warehouse");
132
+ });
133
+ });
134
+
135
+ describe("addressToDisplayString", () => {
136
+ test("builds comma-separated line with zip", () => {
137
+ expect(
138
+ addressToDisplayString({
139
+ place: "Studio",
140
+ street: "5th Ave",
141
+ city: "NYC",
142
+ state: "NY",
143
+ zipCode: "10003",
144
+ country: "USA",
145
+ })
146
+ ).toBe("Studio, 5th Ave, NYC, NY 10003");
147
+ });
148
+ });
149
+
150
+ describe("extractAddressComponents", () => {
151
+ test("maps Google place components to IAddress", () => {
152
+ expect(
153
+ extractAddressComponents({
154
+ place_id: "place-123",
155
+ name: "Bash HQ",
156
+ address_components: [
157
+ { types: ["street_number"], long_name: "100", short_name: "100" },
158
+ { types: ["route"], long_name: "Broadway", short_name: "Broadway" },
159
+ { types: ["locality"], long_name: "New York", short_name: "New York" },
160
+ { types: ["administrative_area_level_1"], long_name: "New York", short_name: "NY" },
161
+ { types: ["postal_code"], long_name: "10005", short_name: "10005" },
162
+ { types: ["country"], long_name: "United States", short_name: "US" },
163
+ ],
164
+ })
165
+ ).toEqual({
166
+ googlePlaceId: "place-123",
167
+ place: "Bash HQ",
168
+ street: "100 Broadway",
169
+ city: "New York",
170
+ state: "NY",
171
+ zipCode: "10005",
172
+ country: "United States",
173
+ });
174
+ });
175
+ });
176
+ });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Tests for shared contact/user helpers and environment detection.
3
+ */
4
+
5
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
6
+ import {
7
+ getUserName,
8
+ getUserImage,
9
+ isContactAndNotUser,
10
+ getContactOrUserEmail,
11
+ getUserIdAndNotContactId,
12
+ getBashEventDetailUrlParamsFromBashEvent,
13
+ isProduction,
14
+ } from "../apiUtils.js";
15
+
16
+ describe("apiUtils", () => {
17
+ describe("isContactAndNotUser", () => {
18
+ test("returns true when contactOwnerId is set", () => {
19
+ expect(isContactAndNotUser({ contactOwnerId: "owner-1", fullName: "Jane" })).toBe(true);
20
+ });
21
+
22
+ test("returns false for user-shaped objects", () => {
23
+ expect(isContactAndNotUser({ id: "user-1", email: "jane@example.com" })).toBe(false);
24
+ });
25
+ });
26
+
27
+ describe("getUserName", () => {
28
+ test("returns empty string for undefined", () => {
29
+ expect(getUserName(undefined)).toBe("");
30
+ });
31
+
32
+ test("returns contact fullName when present", () => {
33
+ expect(
34
+ getUserName({ contactOwnerId: "owner-1", fullName: "Jane Contact", contactEmail: "j@x.com" })
35
+ ).toBe("Jane Contact");
36
+ });
37
+
38
+ test("falls back to contactEmail for contacts without fullName", () => {
39
+ expect(getUserName({ contactOwnerId: "owner-1", contactEmail: "j@x.com" })).toBe("j@x.com");
40
+ });
41
+
42
+ test("combines givenName and familyName for users", () => {
43
+ expect(getUserName({ givenName: "Jane", familyName: "Doe", email: "j@x.com" })).toBe("Jane Doe");
44
+ });
45
+
46
+ test("falls back to email when name parts missing", () => {
47
+ expect(getUserName({ email: "solo@example.com" })).toBe("solo@example.com");
48
+ });
49
+ });
50
+
51
+ describe("getUserImage", () => {
52
+ const noProfilePicture = "/noProfilePicture.svg";
53
+
54
+ test("returns empty string for undefined", () => {
55
+ expect(getUserImage(undefined)).toBe("");
56
+ });
57
+
58
+ test("returns placeholder for contacts", () => {
59
+ expect(getUserImage({ contactOwnerId: "owner-1", fullName: "Jane" })).toBe(noProfilePicture);
60
+ });
61
+
62
+ test("returns uploaded image when only upload exists", () => {
63
+ expect(getUserImage({ uploadedImage: "https://cdn.example/upload.jpg" })).toBe(
64
+ "https://cdn.example/upload.jpg"
65
+ );
66
+ });
67
+
68
+ test("returns google image when only google image exists", () => {
69
+ expect(getUserImage({ image: "https://lh3.google.com/photo.jpg" })).toBe(
70
+ "https://lh3.google.com/photo.jpg"
71
+ );
72
+ });
73
+
74
+ test("treats whitespace-only image URLs as missing", () => {
75
+ expect(getUserImage({ uploadedImage: " ", image: "https://google.com/p.jpg" })).toBe(
76
+ "https://google.com/p.jpg"
77
+ );
78
+ });
79
+
80
+ test("prefers most recently updated image when both exist", () => {
81
+ const older = new Date("2024-01-01");
82
+ const newer = new Date("2024-06-01");
83
+ expect(
84
+ getUserImage({
85
+ uploadedImage: "https://cdn.example/upload.jpg",
86
+ uploadedImageUpdatedAt: newer,
87
+ image: "https://lh3.google.com/photo.jpg",
88
+ imageUpdatedAt: older,
89
+ })
90
+ ).toBe("https://cdn.example/upload.jpg");
91
+ expect(
92
+ getUserImage({
93
+ uploadedImage: "https://cdn.example/upload.jpg",
94
+ uploadedImageUpdatedAt: older,
95
+ image: "https://lh3.google.com/photo.jpg",
96
+ imageUpdatedAt: newer,
97
+ })
98
+ ).toBe("https://lh3.google.com/photo.jpg");
99
+ });
100
+
101
+ test("falls back to uploadedImage when both exist without timestamps", () => {
102
+ expect(
103
+ getUserImage({
104
+ uploadedImage: "https://cdn.example/upload.jpg",
105
+ image: "https://lh3.google.com/photo.jpg",
106
+ })
107
+ ).toBe("https://cdn.example/upload.jpg");
108
+ });
109
+
110
+ test("returns placeholder when user has no images", () => {
111
+ expect(getUserImage({ email: "j@x.com" })).toBe(noProfilePicture);
112
+ });
113
+ });
114
+
115
+ describe("getContactOrUserEmail", () => {
116
+ test("returns contactEmail for contacts", () => {
117
+ expect(getContactOrUserEmail({ contactOwnerId: "o1", contactEmail: "c@x.com" })).toBe("c@x.com");
118
+ });
119
+
120
+ test("returns email for users", () => {
121
+ expect(getContactOrUserEmail({ id: "u1", email: "u@x.com" })).toBe("u@x.com");
122
+ });
123
+ });
124
+
125
+ describe("getUserIdAndNotContactId", () => {
126
+ test("returns null for contacts", () => {
127
+ expect(getUserIdAndNotContactId({ contactOwnerId: "o1" })).toBeNull();
128
+ });
129
+
130
+ test("returns user id for users", () => {
131
+ expect(getUserIdAndNotContactId({ id: "user-42" })).toBe("user-42");
132
+ });
133
+ });
134
+
135
+ describe("getBashEventDetailUrlParamsFromBashEvent", () => {
136
+ test("returns empty string for undefined event", () => {
137
+ expect(getBashEventDetailUrlParamsFromBashEvent(undefined)).toBe("");
138
+ });
139
+
140
+ test("returns empty string when title/desc/cover are absent", () => {
141
+ expect(
142
+ getBashEventDetailUrlParamsFromBashEvent({
143
+ title: "",
144
+ description: null,
145
+ coverPhoto: null,
146
+ } as any)
147
+ ).toBe("");
148
+ });
149
+
150
+ test("truncates long descriptions before building params", () => {
151
+ const longDesc = "abcdefghijklmnop";
152
+ const result = getBashEventDetailUrlParamsFromBashEvent(
153
+ { title: "Party", description: longDesc, coverPhoto: null } as any,
154
+ "cover.jpg"
155
+ );
156
+ expect(result).toBe("");
157
+ });
158
+ });
159
+
160
+ describe("isProduction", () => {
161
+ const envKeys = ["NODE_ENV", "ENVIRONMENT", "BASH_ENV"] as const;
162
+ const saved: Partial<Record<(typeof envKeys)[number], string | undefined>> = {};
163
+
164
+ beforeEach(() => {
165
+ for (const key of envKeys) {
166
+ saved[key] = process.env[key];
167
+ }
168
+ });
169
+
170
+ afterEach(() => {
171
+ for (const key of envKeys) {
172
+ if (saved[key] === undefined) {
173
+ delete process.env[key];
174
+ } else {
175
+ process.env[key] = saved[key];
176
+ }
177
+ }
178
+ });
179
+
180
+ test("returns false when ENVIRONMENT contains qa", () => {
181
+ process.env.NODE_ENV = "production";
182
+ process.env.ENVIRONMENT = "qa-staging";
183
+ expect(isProduction()).toBe(false);
184
+ });
185
+
186
+ test("returns false when BASH_ENV contains qa", () => {
187
+ process.env.NODE_ENV = "production";
188
+ process.env.BASH_ENV = "QA";
189
+ expect(isProduction()).toBe(false);
190
+ });
191
+
192
+ test("returns true only when NODE_ENV is production and env is explicitly production", () => {
193
+ process.env.NODE_ENV = "production";
194
+ process.env.ENVIRONMENT = "production";
195
+ delete process.env.BASH_ENV;
196
+ expect(isProduction()).toBe(true);
197
+ });
198
+
199
+ test("returns true when BASH_ENV is production", () => {
200
+ process.env.NODE_ENV = "production";
201
+ delete process.env.ENVIRONMENT;
202
+ process.env.BASH_ENV = "production";
203
+ expect(isProduction()).toBe(true);
204
+ });
205
+
206
+ test("returns false in development even with production env vars", () => {
207
+ process.env.NODE_ENV = "development";
208
+ process.env.ENVIRONMENT = "production";
209
+ expect(isProduction()).toBe(false);
210
+ });
211
+
212
+ test("returns false when NODE_ENV is production but env vars are unset", () => {
213
+ process.env.NODE_ENV = "production";
214
+ delete process.env.ENVIRONMENT;
215
+ delete process.env.BASH_ENV;
216
+ expect(isProduction()).toBe(false);
217
+ });
218
+ });
219
+ });