@bash-app/bash-common 30.400.0 → 30.406.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 (47) hide show
  1. package/dist/__tests__/audienceImport.test.d.ts +2 -0
  2. package/dist/__tests__/audienceImport.test.d.ts.map +1 -0
  3. package/dist/__tests__/audienceImport.test.js +39 -0
  4. package/dist/__tests__/audienceImport.test.js.map +1 -0
  5. package/dist/__tests__/definitionsHelpers.test.js +34 -1
  6. package/dist/__tests__/definitionsHelpers.test.js.map +1 -1
  7. package/dist/__tests__/eventStaffRoles.test.d.ts +2 -0
  8. package/dist/__tests__/eventStaffRoles.test.d.ts.map +1 -0
  9. package/dist/__tests__/eventStaffRoles.test.js +37 -0
  10. package/dist/__tests__/eventStaffRoles.test.js.map +1 -0
  11. package/dist/audienceImport.d.ts +71 -0
  12. package/dist/audienceImport.d.ts.map +1 -0
  13. package/dist/audienceImport.js +115 -0
  14. package/dist/audienceImport.js.map +1 -0
  15. package/dist/definitions.d.ts +51 -2
  16. package/dist/definitions.d.ts.map +1 -1
  17. package/dist/definitions.js +223 -150
  18. package/dist/definitions.js.map +1 -1
  19. package/dist/eventStaffRoles.d.ts +39 -0
  20. package/dist/eventStaffRoles.d.ts.map +1 -0
  21. package/dist/eventStaffRoles.js +125 -0
  22. package/dist/eventStaffRoles.js.map +1 -0
  23. package/dist/extendedSchemas.d.ts +22 -1
  24. package/dist/extendedSchemas.d.ts.map +1 -1
  25. package/dist/extendedSchemas.js.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/utils/__tests__/locationVisibility.test.js +27 -1
  31. package/dist/utils/__tests__/locationVisibility.test.js.map +1 -1
  32. package/dist/utils/locationVisibility.d.ts +9 -0
  33. package/dist/utils/locationVisibility.d.ts.map +1 -1
  34. package/dist/utils/locationVisibility.js +13 -0
  35. package/dist/utils/locationVisibility.js.map +1 -1
  36. package/package.json +1 -1
  37. package/prisma/schema.prisma +154 -3
  38. package/src/__tests__/audienceImport.test.ts +58 -0
  39. package/src/__tests__/definitionsHelpers.test.ts +50 -0
  40. package/src/__tests__/eventStaffRoles.test.ts +50 -0
  41. package/src/audienceImport.ts +181 -0
  42. package/src/definitions.ts +260 -151
  43. package/src/eventStaffRoles.ts +168 -0
  44. package/src/extendedSchemas.ts +22 -1
  45. package/src/index.ts +2 -0
  46. package/src/utils/__tests__/locationVisibility.test.ts +38 -0
  47. package/src/utils/locationVisibility.ts +16 -0
@@ -4,7 +4,6 @@ import {
4
4
  Contact,
5
5
  DietaryOption,
6
6
  EntertainmentServiceType,
7
- IncomeRange,
8
7
  JudgingType,
9
8
  MembershipTier,
10
9
  MusicGenreType,
@@ -17,6 +16,7 @@ import {
17
16
  } from "@prisma/client";
18
17
  import type {
19
18
  AgePolicy as AgePolicyEnum,
19
+ IncomeRange,
20
20
  BashPassTier,
21
21
  BashEventDressTags,
22
22
  BashEventVibeTags,
@@ -134,6 +134,109 @@ export const AgePolicy = {
134
134
 
135
135
  export type AgePolicy = AgePolicyEnum;
136
136
 
137
+ export const INTRO_INTENT_OPTIONS = [
138
+ { id: "friends", label: "Making friends" },
139
+ { id: "professional", label: "Professional networking" },
140
+ { id: "mentorship", label: "Finding a mentor or mentee" },
141
+ ] as const;
142
+
143
+ export type IntroIntentId = (typeof INTRO_INTENT_OPTIONS)[number]["id"];
144
+
145
+ export const INTRO_INTENT_IDS: readonly IntroIntentId[] =
146
+ INTRO_INTENT_OPTIONS.map((option) => option.id);
147
+
148
+ export function isIntroIntentId(value: string): value is IntroIntentId {
149
+ return (INTRO_INTENT_IDS as readonly string[]).includes(value);
150
+ }
151
+
152
+ export const LOCUS_OF_CONTROL_OPTIONS = [
153
+ { id: "internal", label: "Internal — I shape my own outcomes" },
154
+ { id: "external", label: "External — circumstances shape outcomes" },
155
+ { id: "mixed", label: "Mixed — bit of both" },
156
+ ] as const;
157
+
158
+ export type LocusOfControlId = (typeof LOCUS_OF_CONTROL_OPTIONS)[number]["id"];
159
+
160
+ export const LOCUS_OF_CONTROL_IDS: readonly LocusOfControlId[] =
161
+ LOCUS_OF_CONTROL_OPTIONS.map((option) => option.id);
162
+
163
+ export function isLocusOfControlId(value: string): value is LocusOfControlId {
164
+ return (LOCUS_OF_CONTROL_IDS as readonly string[]).includes(value);
165
+ }
166
+
167
+ export const PERSONALITY_TYPE_MAX_LENGTH = 4;
168
+
169
+ export function normalizePersonalityType(
170
+ value: string | null | undefined
171
+ ): string | null {
172
+ if (!value) return null;
173
+ const normalized = value
174
+ .toUpperCase()
175
+ .replace(/[^A-Z]/g, "")
176
+ .slice(0, PERSONALITY_TYPE_MAX_LENGTH);
177
+ return normalized.length > 0 ? normalized : null;
178
+ }
179
+
180
+ /** True if two MBTI codes share the same second letter (S/N axis). */
181
+ export function hasSensingIntuitionOverlap(a: string, b: string): boolean {
182
+ return (
183
+ a.length >= 2 &&
184
+ b.length >= 2 &&
185
+ a[1].toUpperCase() === b[1].toUpperCase()
186
+ );
187
+ }
188
+
189
+ export function personalityOverlapHint(
190
+ a: string | null | undefined,
191
+ b: string | null | undefined
192
+ ): string | null {
193
+ if (!a || !b || !hasSensingIntuitionOverlap(a, b)) return null;
194
+ return `You both picked ${a[1].toUpperCase()} — good icebreaker.`;
195
+ }
196
+
197
+ export function sharedIntroIntentHint(
198
+ viewerIntents: readonly string[] | null | undefined,
199
+ guestIntents: readonly string[] | null | undefined
200
+ ): string | null {
201
+ if (!viewerIntents?.length || !guestIntents?.length) return null;
202
+ const guest = new Set(guestIntents);
203
+ const match = viewerIntents.find((id) => guest.has(id));
204
+ if (!match || !isIntroIntentId(match)) return null;
205
+ const label = INTRO_INTENT_OPTIONS.find((option) => option.id === match)?.label;
206
+ return label ? `You're both here for ${label.toLowerCase()}.` : null;
207
+ }
208
+
209
+ export function locusOfControlShortLabel(
210
+ id: string | null | undefined
211
+ ): string | null {
212
+ if (!id || !isLocusOfControlId(id)) return null;
213
+ if (id === "internal") return "Internal";
214
+ if (id === "external") return "External";
215
+ return "Mixed";
216
+ }
217
+
218
+ export const INTEREST_EXPRESSION_MODES = ["named", "anonymous"] as const;
219
+
220
+ export type InterestExpressionMode = (typeof INTEREST_EXPRESSION_MODES)[number];
221
+
222
+ export function isInterestExpressionMode(
223
+ value: string
224
+ ): value is InterestExpressionMode {
225
+ return (INTEREST_EXPRESSION_MODES as readonly string[]).includes(value);
226
+ }
227
+
228
+ export const INTROS_DISCLAIMER =
229
+ "Intros connects you only with people who have also explicitly opted in. Bash does not conduct background checks, verify identities beyond basic account registration, or screen users for any purpose. This feature supports platonic and professional connections only — romantic or sexual contact is a violation of our Community Guidelines. Always meet at the event, in public. Use the Report button if anyone makes you uncomfortable. By turning on Intros, you agree that Bash is not responsible for interactions between users.";
230
+
231
+ export const INTROS_DISCLAIMER_SHORT =
232
+ "Bash does not background-check users. Meet at the event, in public. Romantic or sexual contact is prohibited. Bash is not responsible for interactions between users.";
233
+
234
+ /** @deprecated Use INTROS_DISCLAIMER */
235
+ export const SCENE_INTROS_DISCLAIMER = INTROS_DISCLAIMER;
236
+
237
+ /** @deprecated Use INTROS_DISCLAIMER_SHORT */
238
+ export const SCENE_INTROS_DISCLAIMER_SHORT = INTROS_DISCLAIMER_SHORT;
239
+
137
240
  export const PASSWORD_MIN_LENGTH = 8 as const;
138
241
  export const PASSWORD_REQUIREMENTS_REGEX = new RegExp(
139
242
  String.raw`^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&^#])[A-Za-z\d@$!%*?&^#]{${PASSWORD_MIN_LENGTH},}$`
@@ -1366,6 +1469,8 @@ export interface StripeConfirmPaymentResult {
1366
1469
  ticketsSubtotalCents?: number;
1367
1470
  /** Actual amount charged to the guest (cents), including pass-through fees when applicable */
1368
1471
  totalPaidCents?: number;
1472
+ /** True when this is the buyer's first ticket purchase on Bash ever. */
1473
+ isFirstTicketPurchase?: boolean;
1369
1474
  }
1370
1475
 
1371
1476
  export interface IMediaUploadArgs {
@@ -1649,96 +1754,101 @@ export type BashEventTypeToStringType = {
1649
1754
  [key in BashEventType]: string;
1650
1755
  };
1651
1756
  export { BashEventType };
1757
+ /**
1758
+ * Keys are string literals (not `BashEventType.*`) so this module can load
1759
+ * when Jest's `@prisma/client` cache is briefly incomplete after resetModules
1760
+ * (see api/jest.setup.afterEnv.cjs). Same pattern as SERVICE_TYPE_LABELS.
1761
+ */
1652
1762
  export const BashEventTypeToString: BashEventTypeToStringType = {
1653
- [BashEventType.AfterParty]: "After-Party",
1654
- [BashEventType.AnniversaryCelebration]: "Anniversary Celebration",
1655
- [BashEventType.ArtAndCraftNight]: "Art and Craft Night",
1656
- [BashEventType.ArtExhibitOpening]: "Art Exhibit Opening",
1657
- [BashEventType.BBQCookout]: "BBQ Cookout",
1658
- [BashEventType.BabyShower]: "Baby Shower",
1659
- [BashEventType.BachelorParty]: "Bachelor Party",
1660
- [BashEventType.BacheloretteParty]: "Bachelorette Party",
1661
- [BashEventType.BeachParty]: "Beach Party",
1662
- [BashEventType.Birthday]: "Birthday",
1663
- [BashEventType.BoatParty]: "Boat Party",
1664
- [BashEventType.Bonfire]: "Bonfire",
1665
- [BashEventType.BridalShower]: "Bridal Shower",
1666
- [BashEventType.BrunchGathering]: "Brunch Gathering",
1667
- [BashEventType.CarShow]: "Car Show",
1668
- [BashEventType.Carnival]: "Carnival",
1669
- [BashEventType.CasinoNight]: "Casino Night",
1670
- [BashEventType.CasualMixer]: "Casual Mixer",
1671
- [BashEventType.CharityGala]: "Charity Gala",
1672
- [BashEventType.ChristmasParty]: "Christmas Party",
1673
- [BashEventType.ChurchEvent]: "Church Event",
1674
- [BashEventType.CitySponsored]: "City-Sponsored Event",
1675
- [BashEventType.CocktailParty]: "Cocktail Party",
1676
- [BashEventType.CollegeParty]: "College Party",
1677
- [BashEventType.ComedyShow]: "Comedy Show",
1678
- [BashEventType.Concert]: "Concert",
1679
- [BashEventType.Conference]: "Conference",
1680
- [BashEventType.Convention]: "Convention",
1681
- [BashEventType.CorporateEvent]: "Corporate Event",
1682
- [BashEventType.Cruise]: "Cruise",
1683
- [BashEventType.DanceParty]: "Dance Party",
1684
- [BashEventType.DiscoNight]: "Disco Night",
1685
- [BashEventType.EasterGathering]: "Easter Gathering",
1686
- [BashEventType.EngagementParty]: "Engagement Party",
1687
- [BashEventType.Fair]: "Fair",
1688
- [BashEventType.FarewellParty]: "Farewell Party",
1689
- [BashEventType.FashionShow]: "Fashion Show",
1690
- [BashEventType.Festival]: "Festival",
1691
- [BashEventType.FestivalFilm]: "Film Festival",
1692
- [BashEventType.FestivalFood]: "Food Festival",
1693
- [BashEventType.Fundraiser]: "Fundraiser",
1694
- [BashEventType.GameNight]: "Game Night",
1695
- [BashEventType.GamingEvent]: "Gaming Event",
1696
- [BashEventType.GardenParty]: "Garden Party",
1697
- [BashEventType.GraduationParty]: "Graduation Party",
1698
- [BashEventType.HalloweenParty]: "Halloween Party",
1699
- [BashEventType.HanukkahParty]: "Hanukkah Party",
1700
- [BashEventType.HolidayParty]: "Holiday Party",
1701
- [BashEventType.HouseParty]: "House Party",
1702
- [BashEventType.HousewarmingParty]: "Housewarming Party",
1703
- [BashEventType.KaraokeNight]: "Karaoke Night",
1704
- [BashEventType.LaunchParty]: "Launch Party",
1705
- [BashEventType.LuxuryRetreat]: "Luxury Retreat",
1706
- [BashEventType.MansionParty]: "Mansion Party",
1707
- [BashEventType.MardiGras]: "Mardi Gras",
1708
- [BashEventType.Memorial]: "Memorial",
1709
- [BashEventType.MovieNight]: "Movie Night",
1710
- [BashEventType.MoviePremiere]: "Movie Premiere",
1711
- [BashEventType.MusicFestival]: "Music Festival",
1712
- [BashEventType.NewYearsEveCelebration]: "New Year's Eve Celebration",
1713
- [BashEventType.OfficeParty]: "Office Party",
1714
- [BashEventType.OpenMicNight]: "Open Mic Night",
1715
- [BashEventType.OutdoorActivity]: "Outdoor Activity",
1716
- [BashEventType.Parade]: "Parade",
1717
- [BashEventType.Party]: "Party",
1718
- [BashEventType.PoolParty]: "Pool Party",
1719
- [BashEventType.Potluck]: "Potluck",
1720
- [BashEventType.PreParty]: "Pre-Party",
1721
- [BashEventType.ProfessionalNetworkingEvent]: "Professional Networking Event",
1722
- [BashEventType.PubCrawl]: "Pub Crawl",
1723
- [BashEventType.Rave]: "Rave",
1724
- [BashEventType.RetirementCelebration]: "Retirement Celebration",
1725
- [BashEventType.Reunion]: "Reunion",
1726
- [BashEventType.SchoolEvent]: "School Event",
1727
- [BashEventType.SocialClubEvent]: "Social Club Event",
1728
- [BashEventType.SportsWatchParty]: "Sports Watch Party",
1729
- [BashEventType.ThanksgivingDinner]: "Thanksgiving Dinner",
1730
- [BashEventType.ThemedDinnerParty]: "Themed Dinner Party",
1731
- [BashEventType.ThemedParty]: "Themed Party",
1732
- [BashEventType.ThemedPubCrawl]: "Themed Pub Crawl",
1733
- [BashEventType.Tournament]: "Tournament",
1734
- [BashEventType.TradeShow]: "Trade Show",
1735
- [BashEventType.TriviaNight]: "Trivia Night",
1736
- [BashEventType.ValentinesDayParty]: "Valentine's Day Party",
1737
- [BashEventType.WeddingReception]: "Wedding Reception",
1738
- [BashEventType.WelcomeHomeParty]: "Welcome Home Party",
1739
- [BashEventType.WineTastingEvent]: "Wine Tasting Event",
1740
- [BashEventType.Workshop]: "Workshop",
1741
- [BashEventType.Other]: "Other",
1763
+ AfterParty: "After-Party",
1764
+ AnniversaryCelebration: "Anniversary Celebration",
1765
+ ArtAndCraftNight: "Art and Craft Night",
1766
+ ArtExhibitOpening: "Art Exhibit Opening",
1767
+ BBQCookout: "BBQ Cookout",
1768
+ BabyShower: "Baby Shower",
1769
+ BachelorParty: "Bachelor Party",
1770
+ BacheloretteParty: "Bachelorette Party",
1771
+ BeachParty: "Beach Party",
1772
+ Birthday: "Birthday",
1773
+ BoatParty: "Boat Party",
1774
+ Bonfire: "Bonfire",
1775
+ BridalShower: "Bridal Shower",
1776
+ BrunchGathering: "Brunch Gathering",
1777
+ CarShow: "Car Show",
1778
+ Carnival: "Carnival",
1779
+ CasinoNight: "Casino Night",
1780
+ CasualMixer: "Casual Mixer",
1781
+ CharityGala: "Charity Gala",
1782
+ ChristmasParty: "Christmas Party",
1783
+ ChurchEvent: "Church Event",
1784
+ CitySponsored: "City-Sponsored Event",
1785
+ CocktailParty: "Cocktail Party",
1786
+ CollegeParty: "College Party",
1787
+ ComedyShow: "Comedy Show",
1788
+ Concert: "Concert",
1789
+ Conference: "Conference",
1790
+ Convention: "Convention",
1791
+ CorporateEvent: "Corporate Event",
1792
+ Cruise: "Cruise",
1793
+ DanceParty: "Dance Party",
1794
+ DiscoNight: "Disco Night",
1795
+ EasterGathering: "Easter Gathering",
1796
+ EngagementParty: "Engagement Party",
1797
+ Fair: "Fair",
1798
+ FarewellParty: "Farewell Party",
1799
+ FashionShow: "Fashion Show",
1800
+ Festival: "Festival",
1801
+ FestivalFilm: "Film Festival",
1802
+ FestivalFood: "Food Festival",
1803
+ Fundraiser: "Fundraiser",
1804
+ GameNight: "Game Night",
1805
+ GamingEvent: "Gaming Event",
1806
+ GardenParty: "Garden Party",
1807
+ GraduationParty: "Graduation Party",
1808
+ HalloweenParty: "Halloween Party",
1809
+ HanukkahParty: "Hanukkah Party",
1810
+ HolidayParty: "Holiday Party",
1811
+ HouseParty: "House Party",
1812
+ HousewarmingParty: "Housewarming Party",
1813
+ KaraokeNight: "Karaoke Night",
1814
+ LaunchParty: "Launch Party",
1815
+ LuxuryRetreat: "Luxury Retreat",
1816
+ MansionParty: "Mansion Party",
1817
+ MardiGras: "Mardi Gras",
1818
+ Memorial: "Memorial",
1819
+ MovieNight: "Movie Night",
1820
+ MoviePremiere: "Movie Premiere",
1821
+ MusicFestival: "Music Festival",
1822
+ NewYearsEveCelebration: "New Year's Eve Celebration",
1823
+ OfficeParty: "Office Party",
1824
+ OpenMicNight: "Open Mic Night",
1825
+ OutdoorActivity: "Outdoor Activity",
1826
+ Parade: "Parade",
1827
+ Party: "Party",
1828
+ PoolParty: "Pool Party",
1829
+ Potluck: "Potluck",
1830
+ PreParty: "Pre-Party",
1831
+ ProfessionalNetworkingEvent: "Professional Networking Event",
1832
+ PubCrawl: "Pub Crawl",
1833
+ Rave: "Rave",
1834
+ RetirementCelebration: "Retirement Celebration",
1835
+ Reunion: "Reunion",
1836
+ SchoolEvent: "School Event",
1837
+ SocialClubEvent: "Social Club Event",
1838
+ SportsWatchParty: "Sports Watch Party",
1839
+ ThanksgivingDinner: "Thanksgiving Dinner",
1840
+ ThemedDinnerParty: "Themed Dinner Party",
1841
+ ThemedParty: "Themed Party",
1842
+ ThemedPubCrawl: "Themed Pub Crawl",
1843
+ Tournament: "Tournament",
1844
+ TradeShow: "Trade Show",
1845
+ TriviaNight: "Trivia Night",
1846
+ ValentinesDayParty: "Valentine's Day Party",
1847
+ WeddingReception: "Wedding Reception",
1848
+ WelcomeHomeParty: "Welcome Home Party",
1849
+ WineTastingEvent: "Wine Tasting Event",
1850
+ Workshop: "Workshop",
1851
+ Other: "Other",
1742
1852
  };
1743
1853
 
1744
1854
  /**
@@ -1751,47 +1861,47 @@ export type DietaryOptionToStringType = {
1751
1861
  [key in DietaryOption]: string;
1752
1862
  };
1753
1863
  export const DietaryOptionToString: DietaryOptionToStringType = {
1754
- [DietaryOption.GlutenFreeOptions]: "Gluten-Free Options Available",
1755
- [DietaryOption.VeganOptions]: "Vegan Options Available",
1756
- [DietaryOption.VegetarianOptions]: "Vegetarian Options Available",
1757
- [DietaryOption.DairyFreeOptions]: "Dairy-Free Options Available",
1758
- [DietaryOption.NutFreeOptions]: "Nut-Free Options Available",
1759
- [DietaryOption.HalalOptions]: "Halal Options Available",
1760
- [DietaryOption.KosherOptions]: "Kosher Options Available",
1761
- [DietaryOption.SugarFreeOptions]: "Low-Sugar / Sugar-Free Options Available",
1762
- [DietaryOption.KetoOptions]: "Keto-Friendly Options Available",
1763
- [DietaryOption.PaleoOptions]: "Paleo-Friendly Options Available",
1764
- [DietaryOption.AlcoholFreeOptions]: "Non-Alcoholic Options Available",
1864
+ GlutenFreeOptions: "Gluten-Free Options Available",
1865
+ VeganOptions: "Vegan Options Available",
1866
+ VegetarianOptions: "Vegetarian Options Available",
1867
+ DairyFreeOptions: "Dairy-Free Options Available",
1868
+ NutFreeOptions: "Nut-Free Options Available",
1869
+ HalalOptions: "Halal Options Available",
1870
+ KosherOptions: "Kosher Options Available",
1871
+ SugarFreeOptions: "Low-Sugar / Sugar-Free Options Available",
1872
+ KetoOptions: "Keto-Friendly Options Available",
1873
+ PaleoOptions: "Paleo-Friendly Options Available",
1874
+ AlcoholFreeOptions: "Non-Alcoholic Options Available",
1765
1875
  };
1766
1876
 
1767
1877
  /** Short chip labels used in tight UI spots (filter modal, card pills). */
1768
1878
  export const DietaryOptionShortLabel: DietaryOptionToStringType = {
1769
- [DietaryOption.GlutenFreeOptions]: "Gluten-Free",
1770
- [DietaryOption.VeganOptions]: "Vegan",
1771
- [DietaryOption.VegetarianOptions]: "Vegetarian",
1772
- [DietaryOption.DairyFreeOptions]: "Dairy-Free",
1773
- [DietaryOption.NutFreeOptions]: "Nut-Free",
1774
- [DietaryOption.HalalOptions]: "Halal",
1775
- [DietaryOption.KosherOptions]: "Kosher",
1776
- [DietaryOption.SugarFreeOptions]: "Sugar-Free",
1777
- [DietaryOption.KetoOptions]: "Keto",
1778
- [DietaryOption.PaleoOptions]: "Paleo",
1779
- [DietaryOption.AlcoholFreeOptions]: "Non-Alcoholic",
1879
+ GlutenFreeOptions: "Gluten-Free",
1880
+ VeganOptions: "Vegan",
1881
+ VegetarianOptions: "Vegetarian",
1882
+ DairyFreeOptions: "Dairy-Free",
1883
+ NutFreeOptions: "Nut-Free",
1884
+ HalalOptions: "Halal",
1885
+ KosherOptions: "Kosher",
1886
+ SugarFreeOptions: "Sugar-Free",
1887
+ KetoOptions: "Keto",
1888
+ PaleoOptions: "Paleo",
1889
+ AlcoholFreeOptions: "Non-Alcoholic",
1780
1890
  };
1781
1891
 
1782
1892
  /** All `DietaryOption` enum values in display order (for rendering checkboxes/tags). */
1783
1893
  export const DIETARY_OPTION_VALUES: ReadonlyArray<DietaryOption> = [
1784
- DietaryOption.GlutenFreeOptions,
1785
- DietaryOption.VeganOptions,
1786
- DietaryOption.VegetarianOptions,
1787
- DietaryOption.DairyFreeOptions,
1788
- DietaryOption.NutFreeOptions,
1789
- DietaryOption.HalalOptions,
1790
- DietaryOption.KosherOptions,
1791
- DietaryOption.SugarFreeOptions,
1792
- DietaryOption.KetoOptions,
1793
- DietaryOption.PaleoOptions,
1794
- DietaryOption.AlcoholFreeOptions,
1894
+ "GlutenFreeOptions",
1895
+ "VeganOptions",
1896
+ "VegetarianOptions",
1897
+ "DairyFreeOptions",
1898
+ "NutFreeOptions",
1899
+ "HalalOptions",
1900
+ "KosherOptions",
1901
+ "SugarFreeOptions",
1902
+ "KetoOptions",
1903
+ "PaleoOptions",
1904
+ "AlcoholFreeOptions",
1795
1905
  ];
1796
1906
 
1797
1907
  /** Display labels for `IncomeRange` (wizard primary/secondary chips). */
@@ -1799,29 +1909,28 @@ export type IncomeRangeToStringType = {
1799
1909
  [key in IncomeRange]: string;
1800
1910
  };
1801
1911
  export const IncomeRangeLabel: IncomeRangeToStringType = {
1802
- [IncomeRange.NoPreference]: "Anyone",
1803
- [IncomeRange.Under40k]: "Under $40k",
1804
- [IncomeRange.From40to80k]: "$40k – $80k",
1805
- [IncomeRange.From80to150k]: "$80k – $150k",
1806
- [IncomeRange.From150to200k]: "$150k – $200k",
1807
- [IncomeRange.From200to500k]:
1808
- "$200k – $500k (common accredited-income band)",
1809
- [IncomeRange.From500kTo1M]: "$500k – $1M",
1810
- [IncomeRange.Over1M]: "$1M+",
1811
- [IncomeRange.PreferNotToSay]: "Prefer not to say",
1912
+ NoPreference: "Anyone",
1913
+ Under40k: "Under $40k",
1914
+ From40to80k: "$40k – $80k",
1915
+ From80to150k: "$80k – $150k",
1916
+ From150to200k: "$150k – $200k",
1917
+ From200to500k: "$200k – $500k (common accredited-income band)",
1918
+ From500kTo1M: "$500k – $1M",
1919
+ Over1M: "$1M+",
1920
+ PreferNotToSay: "Prefer not to say",
1812
1921
  };
1813
1922
 
1814
1923
  /** Short labels for filter modal / tight chips. */
1815
1924
  export const IncomeRangeShortLabel: IncomeRangeToStringType = {
1816
- [IncomeRange.NoPreference]: "Any income",
1817
- [IncomeRange.Under40k]: "<$40k",
1818
- [IncomeRange.From40to80k]: "$40–80k",
1819
- [IncomeRange.From80to150k]: "$80–150k",
1820
- [IncomeRange.From150to200k]: "$150–200k",
1821
- [IncomeRange.From200to500k]: "$200–500k",
1822
- [IncomeRange.From500kTo1M]: "$500k–$1M",
1823
- [IncomeRange.Over1M]: "$1M+",
1824
- [IncomeRange.PreferNotToSay]: "No answer",
1925
+ NoPreference: "Any income",
1926
+ Under40k: "<$40k",
1927
+ From40to80k: "$40–80k",
1928
+ From80to150k: "$80–150k",
1929
+ From150to200k: "$150–200k",
1930
+ From200to500k: "$200–500k",
1931
+ From500kTo1M: "$500k–$1M",
1932
+ Over1M: "$1M+",
1933
+ PreferNotToSay: "No answer",
1825
1934
  };
1826
1935
 
1827
1936
  /**
@@ -1829,15 +1938,15 @@ export const IncomeRangeShortLabel: IncomeRangeToStringType = {
1829
1938
  * Audience-targeting only — never use to gate ticket purchase or entry.
1830
1939
  */
1831
1940
  export const INCOME_RANGE_VALUES: ReadonlyArray<IncomeRange> = [
1832
- IncomeRange.Under40k,
1833
- IncomeRange.From40to80k,
1834
- IncomeRange.From80to150k,
1835
- IncomeRange.From150to200k,
1836
- IncomeRange.From200to500k,
1837
- IncomeRange.From500kTo1M,
1838
- IncomeRange.Over1M,
1839
- IncomeRange.PreferNotToSay,
1840
- IncomeRange.NoPreference,
1941
+ "Under40k",
1942
+ "From40to80k",
1943
+ "From80to150k",
1944
+ "From150to200k",
1945
+ "From200to500k",
1946
+ "From500kTo1M",
1947
+ "Over1M",
1948
+ "PreferNotToSay",
1949
+ "NoPreference",
1841
1950
  ];
1842
1951
 
1843
1952
  /** Homepage / discovery section labels for synthetic rows (not `BashEventType` enum values). */
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Per-event staff role templates. Booleans on AssociatedBash stay the live
3
+ * write-through so existing canUserEditBashEvent / userCanAccessDoor callers
4
+ * keep working during the transition.
5
+ */
6
+
7
+ export const EVENT_STAFF_ROLES = [
8
+ "Admin",
9
+ "Organizer",
10
+ "Promoter",
11
+ "DoorStaff",
12
+ "ServiceProvider",
13
+ "Volunteer",
14
+ ] as const;
15
+
16
+ export type EventStaffRole = (typeof EVENT_STAFF_ROLES)[number];
17
+
18
+ export type EventStaffPermissionKey =
19
+ | "canEditEvent"
20
+ | "canEditTickets"
21
+ | "canManageAttendees"
22
+ | "canIssueComps"
23
+ | "canAccessDoor"
24
+ | "canViewFinances"
25
+ | "canRefund"
26
+ | "canMessage"
27
+ | "canManageTrackingLinks";
28
+
29
+ export type EventStaffPermissionFlags = Record<EventStaffPermissionKey, boolean>;
30
+
31
+ export type EventStaffBooleanWriteThrough = {
32
+ isOrganizer: boolean;
33
+ isDoorStaff: boolean;
34
+ isCoHost: boolean;
35
+ };
36
+
37
+ export const EVENT_STAFF_TEMPLATE_DEFAULTS: Record<EventStaffRole, EventStaffPermissionFlags> = {
38
+ Admin: {
39
+ canEditEvent: true,
40
+ canEditTickets: true,
41
+ canManageAttendees: true,
42
+ canIssueComps: true,
43
+ canAccessDoor: true,
44
+ canViewFinances: true,
45
+ canRefund: true,
46
+ canMessage: true,
47
+ canManageTrackingLinks: true,
48
+ },
49
+ Organizer: {
50
+ canEditEvent: true,
51
+ canEditTickets: true,
52
+ canManageAttendees: true,
53
+ canIssueComps: true,
54
+ canAccessDoor: true,
55
+ canViewFinances: false,
56
+ canRefund: false,
57
+ canMessage: true,
58
+ canManageTrackingLinks: true,
59
+ },
60
+ Promoter: {
61
+ canEditEvent: false,
62
+ canEditTickets: false,
63
+ canManageAttendees: false,
64
+ canIssueComps: true,
65
+ canAccessDoor: false,
66
+ canViewFinances: false,
67
+ canRefund: false,
68
+ canMessage: false,
69
+ canManageTrackingLinks: true,
70
+ },
71
+ DoorStaff: {
72
+ canEditEvent: false,
73
+ canEditTickets: false,
74
+ canManageAttendees: true,
75
+ canIssueComps: true,
76
+ canAccessDoor: true,
77
+ canViewFinances: false,
78
+ canRefund: false,
79
+ canMessage: false,
80
+ canManageTrackingLinks: false,
81
+ },
82
+ ServiceProvider: {
83
+ canEditEvent: false,
84
+ canEditTickets: false,
85
+ canManageAttendees: false,
86
+ canIssueComps: false,
87
+ canAccessDoor: false,
88
+ canViewFinances: false,
89
+ canRefund: false,
90
+ canMessage: false,
91
+ canManageTrackingLinks: false,
92
+ },
93
+ Volunteer: {
94
+ canEditEvent: false,
95
+ canEditTickets: false,
96
+ canManageAttendees: false,
97
+ canIssueComps: false,
98
+ canAccessDoor: false,
99
+ canViewFinances: false,
100
+ canRefund: false,
101
+ canMessage: false,
102
+ canManageTrackingLinks: false,
103
+ },
104
+ };
105
+
106
+ export function isEventStaffRole(value: string | null | undefined): value is EventStaffRole {
107
+ return !!value && (EVENT_STAFF_ROLES as readonly string[]).includes(value);
108
+ }
109
+
110
+ /** Setting a role also writes the legacy booleans so existing callers stay valid. */
111
+ export function writeThroughAssociatedBashFlags(
112
+ role: EventStaffRole,
113
+ isCoHost = false
114
+ ): EventStaffBooleanWriteThrough {
115
+ if (role === "Admin" || role === "Organizer") {
116
+ return { isOrganizer: true, isDoorStaff: false, isCoHost };
117
+ }
118
+ if (role === "DoorStaff") {
119
+ return { isOrganizer: false, isDoorStaff: true, isCoHost: false };
120
+ }
121
+ return { isOrganizer: false, isDoorStaff: false, isCoHost: false };
122
+ }
123
+
124
+ export function resolveEventStaffPermission(
125
+ role: EventStaffRole | null | undefined,
126
+ key: EventStaffPermissionKey,
127
+ override: boolean | null | undefined
128
+ ): boolean {
129
+ if (typeof override === "boolean") return override;
130
+ if (!isEventStaffRole(role)) return false;
131
+ return EVENT_STAFF_TEMPLATE_DEFAULTS[role][key];
132
+ }
133
+
134
+ /**
135
+ * Edit access: honor role+overrides when eventRole is set; otherwise the
136
+ * existing isOrganizer boolean. Never drop the boolean check for legacy rows.
137
+ */
138
+ export function associatedBashCanEditEvent(row: {
139
+ eventRole?: string | null;
140
+ isOrganizer?: boolean | null;
141
+ canEditEvent?: boolean | null;
142
+ }): boolean {
143
+ if (isEventStaffRole(row.eventRole)) {
144
+ return resolveEventStaffPermission(row.eventRole, "canEditEvent", row.canEditEvent);
145
+ }
146
+ return !!row.isOrganizer;
147
+ }
148
+
149
+ export function associatedBashCanAccessDoor(row: {
150
+ eventRole?: string | null;
151
+ isOrganizer?: boolean | null;
152
+ isDoorStaff?: boolean | null;
153
+ canAccessDoor?: boolean | null;
154
+ }): boolean {
155
+ if (isEventStaffRole(row.eventRole)) {
156
+ return resolveEventStaffPermission(row.eventRole, "canAccessDoor", row.canAccessDoor);
157
+ }
158
+ return Boolean(row.isOrganizer || row.isDoorStaff);
159
+ }
160
+
161
+ export function roleFromLegacyFlags(row: {
162
+ isOrganizer?: boolean | null;
163
+ isDoorStaff?: boolean | null;
164
+ }): EventStaffRole | null {
165
+ if (row.isDoorStaff && !row.isOrganizer) return "DoorStaff";
166
+ if (row.isOrganizer) return "Organizer";
167
+ return null;
168
+ }