@bash-app/bash-common 30.306.0 → 30.308.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 (36) hide show
  1. package/dist/__tests__/guestPartySize.test.d.ts +2 -0
  2. package/dist/__tests__/guestPartySize.test.d.ts.map +1 -0
  3. package/dist/__tests__/guestPartySize.test.js +22 -0
  4. package/dist/__tests__/guestPartySize.test.js.map +1 -0
  5. package/dist/__tests__/sideQuestIdeas.test.d.ts +2 -0
  6. package/dist/__tests__/sideQuestIdeas.test.d.ts.map +1 -0
  7. package/dist/__tests__/sideQuestIdeas.test.js +13 -0
  8. package/dist/__tests__/sideQuestIdeas.test.js.map +1 -0
  9. package/dist/extendedSchemas.d.ts +4 -0
  10. package/dist/extendedSchemas.d.ts.map +1 -1
  11. package/dist/extendedSchemas.js.map +1 -1
  12. package/dist/guestPartySize.d.ts +18 -0
  13. package/dist/guestPartySize.d.ts.map +1 -0
  14. package/dist/guestPartySize.js +72 -0
  15. package/dist/guestPartySize.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/mirroredPrismaEnums.d.ts +3 -0
  21. package/dist/mirroredPrismaEnums.d.ts.map +1 -1
  22. package/dist/mirroredPrismaEnums.js +3 -0
  23. package/dist/mirroredPrismaEnums.js.map +1 -1
  24. package/dist/sideQuestIdeas.d.ts +51 -0
  25. package/dist/sideQuestIdeas.d.ts.map +1 -0
  26. package/dist/sideQuestIdeas.js +215 -0
  27. package/dist/sideQuestIdeas.js.map +1 -0
  28. package/package.json +1 -1
  29. package/prisma/schema.prisma +204 -0
  30. package/src/__tests__/guestPartySize.test.ts +31 -0
  31. package/src/__tests__/sideQuestIdeas.test.ts +16 -0
  32. package/src/extendedSchemas.ts +4 -0
  33. package/src/guestPartySize.ts +83 -0
  34. package/src/index.ts +2 -0
  35. package/src/mirroredPrismaEnums.ts +3 -0
  36. package/src/sideQuestIdeas.ts +260 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Side Quest starter templates and category display metadata.
3
+ * Behavior design: Action → Prove (quest type) → Reward.
4
+ */
5
+ export const SIDE_QUEST_CATEGORY_META = {
6
+ Explore: { label: "Explore", emoji: "🧭" },
7
+ Connect: { label: "Connect", emoji: "🤝" },
8
+ Discover: { label: "Discover", emoji: "🎵" },
9
+ SupportLocal: { label: "Support Local", emoji: "🛍️" },
10
+ Compete: { label: "Compete", emoji: "🏆" },
11
+ Transit: { label: "Transit", emoji: "🚆" },
12
+ Create: { label: "Create", emoji: "📸" },
13
+ GiveBack: { label: "Give Back", emoji: "❤️" },
14
+ };
15
+ /** Phase 1 host-selectable quest types */
16
+ export const SIDE_QUEST_PHASE1_TYPES = ["Qr", "Code"];
17
+ export const SIDE_QUEST_TYPE_META = {
18
+ Qr: {
19
+ label: "QR Quest",
20
+ description: "Guest scans a printed QR at the location.",
21
+ phase1: true,
22
+ },
23
+ Code: {
24
+ label: "Code Quest",
25
+ description: "Guest enters a short code shown on-site.",
26
+ phase1: true,
27
+ },
28
+ Gps: {
29
+ label: "GPS Quest",
30
+ description: "Guest checks in near a location.",
31
+ phase1: false,
32
+ },
33
+ Photo: {
34
+ label: "Photo Quest",
35
+ description: "Guest uploads a photo as proof.",
36
+ phase1: false,
37
+ },
38
+ CheckIn: {
39
+ label: "Check-in Quest",
40
+ description: "Tied to event ticket check-in.",
41
+ phase1: false,
42
+ },
43
+ Purchase: {
44
+ label: "Purchase Quest",
45
+ description: "Completes after a qualifying purchase.",
46
+ phase1: false,
47
+ },
48
+ Vote: {
49
+ label: "Vote Quest",
50
+ description: "Completes after voting in a competition.",
51
+ phase1: false,
52
+ },
53
+ Sponsor: {
54
+ label: "Sponsor Quest",
55
+ description: "Visit or engage with a sponsor booth.",
56
+ phase1: false,
57
+ },
58
+ Social: {
59
+ label: "Social Quest",
60
+ description: "Share or tag on social.",
61
+ phase1: false,
62
+ },
63
+ Trivia: {
64
+ label: "Trivia Quest",
65
+ description: "Answer a question correctly.",
66
+ phase1: false,
67
+ },
68
+ };
69
+ export const SIDE_QUEST_IDEAS = [
70
+ {
71
+ id: "festival-transit",
72
+ title: "Ride transit to the festival",
73
+ description: "Take FrontRunner, TRAX, or the bus — scan the code at the station booth.",
74
+ category: "Transit",
75
+ questType: "Qr",
76
+ pointsReward: 100,
77
+ tags: ["festival", "transit"],
78
+ },
79
+ {
80
+ id: "festival-vendor",
81
+ title: "Try food from a local vendor",
82
+ description: "Support a food vendor on site and grab their quest code.",
83
+ category: "SupportLocal",
84
+ questType: "Code",
85
+ pointsReward: 50,
86
+ tags: ["festival", "food"],
87
+ },
88
+ {
89
+ id: "festival-artists",
90
+ title: "Watch 3 different artists",
91
+ description: "Catch three sets — collect the stage code after the third.",
92
+ category: "Discover",
93
+ questType: "Code",
94
+ pointsReward: 75,
95
+ tags: ["festival", "music"],
96
+ },
97
+ {
98
+ id: "festival-photo",
99
+ title: "Take a photo at the landmark",
100
+ description: "Find the landmark and scan the QR nearby.",
101
+ category: "Create",
102
+ questType: "Qr",
103
+ pointsReward: 25,
104
+ tags: ["festival", "photo"],
105
+ },
106
+ {
107
+ id: "market-three-vendors",
108
+ title: "Buy from 3 vendors",
109
+ description: "Support three market stalls — get the code from the info booth when done.",
110
+ category: "SupportLocal",
111
+ questType: "Code",
112
+ pointsReward: 75,
113
+ tags: ["market"],
114
+ },
115
+ {
116
+ id: "market-meet-farmer",
117
+ title: "Meet a farmer",
118
+ description: "Say hello to a grower and scan their booth QR.",
119
+ category: "Connect",
120
+ questType: "Qr",
121
+ pointsReward: 50,
122
+ tags: ["market"],
123
+ },
124
+ {
125
+ id: "conference-keynote",
126
+ title: "Attend a keynote",
127
+ description: "Sit in on a keynote — scan the room QR on the way out.",
128
+ category: "Discover",
129
+ questType: "Qr",
130
+ pointsReward: 50,
131
+ tags: ["conference"],
132
+ },
133
+ {
134
+ id: "conference-meet-five",
135
+ title: "Meet five new people",
136
+ description: "Network intentionally — enter the lounge code when you've made five intros.",
137
+ category: "Connect",
138
+ questType: "Code",
139
+ pointsReward: 75,
140
+ tags: ["conference"],
141
+ },
142
+ {
143
+ id: "campus-club",
144
+ title: "Join a club fair booth",
145
+ description: "Stop by a student org table and scan their QR.",
146
+ category: "Connect",
147
+ questType: "Qr",
148
+ pointsReward: 40,
149
+ tags: ["campus"],
150
+ },
151
+ {
152
+ id: "downtown-mural",
153
+ title: "Find a hidden mural",
154
+ description: "Hunt the mural and scan the nearby QR.",
155
+ category: "Explore",
156
+ questType: "Qr",
157
+ pointsReward: 60,
158
+ tags: ["downtown"],
159
+ },
160
+ {
161
+ id: "give-back-volunteer",
162
+ title: "Complete a volunteer shift",
163
+ description: "Help out — staff will share the completion code.",
164
+ category: "GiveBack",
165
+ questType: "Code",
166
+ pointsReward: 100,
167
+ tags: ["volunteer"],
168
+ },
169
+ {
170
+ id: "compete-vote",
171
+ title: "Vote for your favorite",
172
+ description: "Cast your vote — use the code shown after you submit.",
173
+ category: "Compete",
174
+ questType: "Code",
175
+ pointsReward: 25,
176
+ tags: ["festival", "vote"],
177
+ },
178
+ ];
179
+ export function filterSideQuestIdeas(search) {
180
+ const q = search.trim().toLowerCase();
181
+ if (!q)
182
+ return SIDE_QUEST_IDEAS;
183
+ return SIDE_QUEST_IDEAS.filter((idea) => idea.title.toLowerCase().includes(q) ||
184
+ idea.description.toLowerCase().includes(q) ||
185
+ idea.tags.some((t) => t.toLowerCase().includes(q)) ||
186
+ idea.category.toLowerCase().includes(q));
187
+ }
188
+ /** Lifetime achievement catalog (Phase 2 awarding; definitions ship now). */
189
+ export const ADVENTURE_ACHIEVEMENT_CATALOG = [
190
+ {
191
+ id: "explorer",
192
+ name: "Explorer",
193
+ description: "Completed 100 Side Quests",
194
+ threshold: 100,
195
+ },
196
+ {
197
+ id: "foodie",
198
+ name: "Foodie",
199
+ description: "Visited 50 vendors via Support Local quests",
200
+ threshold: 50,
201
+ },
202
+ {
203
+ id: "festival-veteran",
204
+ name: "Festival Veteran",
205
+ description: "Attended 25 festivals with Side Quests",
206
+ threshold: 25,
207
+ },
208
+ {
209
+ id: "transit-hero",
210
+ name: "Transit Hero",
211
+ description: "Rode transit to 15 events",
212
+ threshold: 15,
213
+ },
214
+ ];
215
+ //# sourceMappingURL=sideQuestIdeas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sideQuestIdeas.js","sourceRoot":"","sources":["../src/sideQuestIdeas.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash-app/bash-common",
3
- "version": "30.306.0",
3
+ "version": "30.308.0",
4
4
  "description": "Common data and scripts to use on the frontend and backend",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -842,6 +842,9 @@ model BashEvent {
842
842
  smsConfirmationsEnabled Boolean @default(false)
843
843
  /// Collect named holder per seat at checkout.
844
844
  namedTicketsEnabled Boolean @default(false)
845
+ /// Max companions a guest may bring (0 = alone, 1 = +1, 2 = +2, null = unlimited).
846
+ /// Applies to RSVP plus-ones and ticketed seats-per-buyer. Null preserves open defaults.
847
+ maxAdditionalGuests Int?
845
848
  attendeeDisplayMode String @default("All") // "All" | "CountOnly" | "NetworkOnly" | "Hidden"
846
849
  /// Optional headline above featured guests on bash detail (curated guest visibility).
847
850
  featuredGuestsHeadline String?
@@ -1038,6 +1041,13 @@ model BashEvent {
1038
1041
  discussionComments EventDiscussionComment[]
1039
1042
  bashEventDonations BashEventDonation[]
1040
1043
 
1044
+ // Adventures / Side Quests
1045
+ sideQuests SideQuest[]
1046
+ eventBadges EventBadge[]
1047
+ sideQuestMilestones SideQuestMilestone[]
1048
+ sideQuestPrizeEntries SideQuestPrizeEntry[]
1049
+ communityQuestGoal CommunityQuestGoal?
1050
+
1041
1051
  // User night itineraries (events in user's "My Night" plan)
1042
1052
  userItineraryItems UserItineraryItem[]
1043
1053
 
@@ -1981,6 +1991,192 @@ model EventReferralReward {
1981
1991
  @@index([bashEventId])
1982
1992
  }
1983
1993
 
1994
+ // --- Adventures / Side Quests (behavior design engine) ---
1995
+
1996
+ enum SideQuestCategory {
1997
+ Explore
1998
+ Connect
1999
+ Discover
2000
+ SupportLocal
2001
+ Compete
2002
+ Transit
2003
+ Create
2004
+ GiveBack
2005
+ }
2006
+
2007
+ /// How the guest proves completion. Phase 1: Qr + Code. Others reserved.
2008
+ enum SideQuestType {
2009
+ Qr
2010
+ Code
2011
+ Gps
2012
+ Photo
2013
+ CheckIn
2014
+ Purchase
2015
+ Vote
2016
+ Sponsor
2017
+ Social
2018
+ Trivia
2019
+ }
2020
+
2021
+ enum EventBadgeAwardSource {
2022
+ SideQuest
2023
+ Milestone
2024
+ Manual
2025
+ Community
2026
+ }
2027
+
2028
+ model SideQuest {
2029
+ id String @id @default(cuid())
2030
+ bashEventId String
2031
+ title String
2032
+ description String?
2033
+ sortOrder Int @default(0)
2034
+ category SideQuestCategory @default(Explore)
2035
+ questType SideQuestType @default(Code)
2036
+ /// Short display code for hosts/guests (e.g. 4-digit). Guests submit this; never treat as the only secret.
2037
+ code String
2038
+ /// SHA-256 hex of normalized code — server verification
2039
+ codeHash String
2040
+ pointsReward Int @default(0)
2041
+ raffleEntriesOnComplete Int @default(0)
2042
+ eventBadgeId String?
2043
+ startsAt DateTime?
2044
+ endsAt DateTime?
2045
+ maxCompletions Int?
2046
+ isHidden Boolean @default(false)
2047
+ unlockedAt DateTime?
2048
+ sponsorLabel String?
2049
+ sponsorServiceId String?
2050
+ createdAt DateTime @default(now())
2051
+ updatedAt DateTime @updatedAt
2052
+
2053
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2054
+ eventBadge EventBadge? @relation(fields: [eventBadgeId], references: [id], onDelete: SetNull)
2055
+ completions SideQuestCompletion[]
2056
+ milestonesUnlocking SideQuestMilestone[] @relation("MilestoneUnlocksQuest")
2057
+
2058
+ @@index([bashEventId])
2059
+ @@index([bashEventId, sortOrder])
2060
+ @@index([eventBadgeId])
2061
+ }
2062
+
2063
+ model SideQuestCompletion {
2064
+ id String @id @default(cuid())
2065
+ sideQuestId String
2066
+ userId String
2067
+ completedAt DateTime @default(now())
2068
+ questType SideQuestType
2069
+
2070
+ sideQuest SideQuest @relation(fields: [sideQuestId], references: [id], onDelete: Cascade)
2071
+ user User @relation("SideQuestCompletions", fields: [userId], references: [id], onDelete: Cascade)
2072
+
2073
+ @@unique([sideQuestId, userId])
2074
+ @@index([userId])
2075
+ @@index([sideQuestId])
2076
+ }
2077
+
2078
+ model EventBadge {
2079
+ id String @id @default(cuid())
2080
+ bashEventId String
2081
+ name String
2082
+ description String?
2083
+ imageUrl String
2084
+ createdByUserId String
2085
+ createdAt DateTime @default(now())
2086
+ updatedAt DateTime @updatedAt
2087
+
2088
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2089
+ createdBy User @relation("EventBadgesCreated", fields: [createdByUserId], references: [id], onDelete: Cascade)
2090
+ awards EventBadgeAward[]
2091
+ sideQuests SideQuest[]
2092
+ milestones SideQuestMilestone[]
2093
+ communityGoals CommunityQuestGoal[]
2094
+
2095
+ @@index([bashEventId])
2096
+ @@index([createdByUserId])
2097
+ }
2098
+
2099
+ model EventBadgeAward {
2100
+ id String @id @default(cuid())
2101
+ eventBadgeId String
2102
+ userId String
2103
+ awardedAt DateTime @default(now())
2104
+ sourceType EventBadgeAwardSource
2105
+ sourceId String?
2106
+
2107
+ eventBadge EventBadge @relation(fields: [eventBadgeId], references: [id], onDelete: Cascade)
2108
+ user User @relation("EventBadgeAwards", fields: [userId], references: [id], onDelete: Cascade)
2109
+
2110
+ @@unique([eventBadgeId, userId])
2111
+ @@index([userId])
2112
+ }
2113
+
2114
+ model SideQuestMilestone {
2115
+ id String @id @default(cuid())
2116
+ bashEventId String
2117
+ requiredCompletions Int
2118
+ bashPointsReward Int?
2119
+ eventBadgeId String?
2120
+ raffleEntries Int?
2121
+ unlockSideQuestId String?
2122
+ sortOrder Int @default(0)
2123
+ createdAt DateTime @default(now())
2124
+ updatedAt DateTime @updatedAt
2125
+
2126
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2127
+ eventBadge EventBadge? @relation(fields: [eventBadgeId], references: [id], onDelete: SetNull)
2128
+ unlockSideQuest SideQuest? @relation("MilestoneUnlocksQuest", fields: [unlockSideQuestId], references: [id], onDelete: SetNull)
2129
+ claims SideQuestMilestoneClaim[]
2130
+
2131
+ @@index([bashEventId])
2132
+ }
2133
+
2134
+ model SideQuestMilestoneClaim {
2135
+ id String @id @default(cuid())
2136
+ milestoneId String
2137
+ userId String
2138
+ claimedAt DateTime @default(now())
2139
+
2140
+ milestone SideQuestMilestone @relation(fields: [milestoneId], references: [id], onDelete: Cascade)
2141
+ user User @relation("SideQuestMilestoneClaims", fields: [userId], references: [id], onDelete: Cascade)
2142
+
2143
+ @@unique([milestoneId, userId])
2144
+ @@index([userId])
2145
+ }
2146
+
2147
+ model SideQuestPrizeEntry {
2148
+ id String @id @default(cuid())
2149
+ bashEventId String
2150
+ userId String
2151
+ entries Int @default(1)
2152
+ sourceCompletionId String?
2153
+ sourceMilestoneId String?
2154
+ sourceType String @default("SideQuest")
2155
+ createdAt DateTime @default(now())
2156
+
2157
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2158
+ user User @relation("SideQuestPrizeEntries", fields: [userId], references: [id], onDelete: Cascade)
2159
+
2160
+ @@index([bashEventId, userId])
2161
+ @@index([userId])
2162
+ }
2163
+
2164
+ model CommunityQuestGoal {
2165
+ id String @id @default(cuid())
2166
+ bashEventId String @unique
2167
+ targetCompletions Int
2168
+ currentCompletions Int @default(0)
2169
+ rewardLabel String
2170
+ eventBadgeId String?
2171
+ bashPointsReward Int?
2172
+ unlockedAt DateTime?
2173
+ createdAt DateTime @default(now())
2174
+ updatedAt DateTime @updatedAt
2175
+
2176
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2177
+ eventBadge EventBadge? @relation(fields: [eventBadgeId], references: [id], onDelete: SetNull)
2178
+ }
2179
+
1984
2180
  enum BackgroundEffect {
1985
2181
  None
1986
2182
  Confetti
@@ -3414,6 +3610,11 @@ model User {
3414
3610
  competitionVotes CompetitionVote[]
3415
3611
  competitionJudgeAssignments CompetitionJudgeAssignment[] @relation("CompetitionJudgeAssignments")
3416
3612
  competitionJudgeScoresAuthored CompetitionJudgeScore[] @relation("CompetitionJudgeScoresAuthored")
3613
+ sideQuestCompletions SideQuestCompletion[] @relation("SideQuestCompletions")
3614
+ eventBadgesCreated EventBadge[] @relation("EventBadgesCreated")
3615
+ eventBadgeAwards EventBadgeAward[] @relation("EventBadgeAwards")
3616
+ sideQuestMilestoneClaims SideQuestMilestoneClaim[] @relation("SideQuestMilestoneClaims")
3617
+ sideQuestPrizeEntries SideQuestPrizeEntry[] @relation("SideQuestPrizeEntries")
3417
3618
  contacts Contact[]
3418
3619
  contactLists ContactList[] @relation("UserContactLists")
3419
3620
  contactlist ContactList[]
@@ -9050,6 +9251,9 @@ enum CreditSourceType {
9050
9251
  ReferralCodeUsed // Flat bonus for referrer when code drives a ticket/signup
9051
9252
  VenueLoyaltyRedemption // BashPoints moved through a VenueLoyaltyRedemption claim/redeem/refund
9052
9253
  PartnerPromoRedemption // BashPoints spent to unlock a local partner promo code
9254
+ SideQuestCompletion // Completing a Side Quest on a bash
9255
+ SideQuestMilestone // Hitting a Side Quest milestone (e.g. complete N quests)
9256
+ CommunityQuestUnlock // Digital reward when a Community Quest goal unlocks
9053
9257
  }
9054
9258
 
9055
9259
  enum ReferralTier {
@@ -0,0 +1,31 @@
1
+ import {
2
+ resolveMaxPlusOnes,
3
+ resolveMaxSeatsPerBuyer,
4
+ rsvpPartyPolicyGuestCopy,
5
+ seatsForTicketQuantity,
6
+ ticketPartyPolicyGuestCopy,
7
+ } from "../guestPartySize.js";
8
+
9
+ describe("guestPartySize", () => {
10
+ it("resolveMaxSeatsPerBuyer", () => {
11
+ expect(resolveMaxSeatsPerBuyer(0)).toBe(1);
12
+ expect(resolveMaxSeatsPerBuyer(1)).toBe(2);
13
+ expect(resolveMaxSeatsPerBuyer(null)).toBeNull();
14
+ expect(resolveMaxSeatsPerBuyer(2, 1)).toBe(1);
15
+ });
16
+
17
+ it("resolveMaxPlusOnes soft-caps unlimited", () => {
18
+ expect(resolveMaxPlusOnes(null)).toBe(10);
19
+ expect(resolveMaxPlusOnes(0)).toBe(0);
20
+ });
21
+
22
+ it("seatsForTicketQuantity expands group packages", () => {
23
+ expect(seatsForTicketQuantity(1, 6, true)).toBe(6);
24
+ });
25
+
26
+ it("guest copy helpers", () => {
27
+ expect(rsvpPartyPolicyGuestCopy(0)).toMatch(/no plus-ones/i);
28
+ expect(ticketPartyPolicyGuestCopy(1)).toBe("Up to 2 tickets per person");
29
+ expect(ticketPartyPolicyGuestCopy(null)).toBeNull();
30
+ });
31
+ });
@@ -0,0 +1,16 @@
1
+ import { filterSideQuestIdeas, SIDE_QUEST_IDEAS } from "../sideQuestIdeas";
2
+
3
+ describe("sideQuestIdeas", () => {
4
+ it("includes festival and market templates", () => {
5
+ expect(SIDE_QUEST_IDEAS.some((i) => i.tags.includes("festival"))).toBe(
6
+ true
7
+ );
8
+ expect(SIDE_QUEST_IDEAS.some((i) => i.tags.includes("market"))).toBe(true);
9
+ });
10
+
11
+ it("filters by search", () => {
12
+ const hits = filterSideQuestIdeas("transit");
13
+ expect(hits.length).toBeGreaterThan(0);
14
+ expect(hits.every((h) => JSON.stringify(h).toLowerCase().includes("transit") || h.category === "Transit")).toBe(true);
15
+ });
16
+ });
@@ -478,6 +478,10 @@ export interface BashEventExt extends Override<
478
478
  * (host absorbs processing, no external checkout). Drives "No fees for attendees" badge.
479
479
  */
480
480
  guestCheckoutHasNoAddedFees?: boolean;
481
+ /**
482
+ * Partner-scraped listing still open for an organizer to claim (detail page CTA).
483
+ */
484
+ listingClaimable?: boolean;
481
485
  /** When eventType is a CustomBashEventType id, resolved display name for filters/UI */
482
486
  customType?: { displayName?: string | null } | null;
483
487
  /** Wizard context when bash is loaded via AssociatedBash (organizer invitation flow) */
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Event-level guest party size (plus-ones / seats-per-buyer).
3
+ * maxAdditionalGuests: 0 = alone, 1 = +1, 2 = +2, null = unlimited.
4
+ */
5
+
6
+ export const RSVP_PLUS_ONES_UI_SOFT_CAP = 10;
7
+ export const RSVP_PLUS_ONES_API_HARD_CAP = 20;
8
+
9
+ /** Max seats one buyer may hold (owned + cart). null = unlimited. */
10
+ export function resolveMaxSeatsPerBuyer(
11
+ maxAdditionalGuests: number | null | undefined,
12
+ tierMaxTicketsPerUser?: number | null
13
+ ): number | null {
14
+ let eventCap: number | null = null;
15
+ if (maxAdditionalGuests != null && Number.isFinite(maxAdditionalGuests)) {
16
+ eventCap = Math.max(0, Math.floor(maxAdditionalGuests)) + 1;
17
+ }
18
+ const tierCap =
19
+ tierMaxTicketsPerUser != null && Number.isFinite(tierMaxTicketsPerUser)
20
+ ? Math.max(0, Math.floor(tierMaxTicketsPerUser))
21
+ : null;
22
+
23
+ if (eventCap == null && tierCap == null) return null;
24
+ if (eventCap == null) return tierCap;
25
+ if (tierCap == null) return eventCap;
26
+ return Math.min(eventCap, tierCap);
27
+ }
28
+
29
+ /** Max plus-ones for RSVP UI/API. */
30
+ export function resolveMaxPlusOnes(
31
+ maxAdditionalGuests: number | null | undefined,
32
+ softCap = RSVP_PLUS_ONES_UI_SOFT_CAP
33
+ ): number {
34
+ if (maxAdditionalGuests == null) return softCap;
35
+ return Math.max(0, Math.min(softCap, Math.floor(maxAdditionalGuests)));
36
+ }
37
+
38
+ export function seatsForTicketQuantity(
39
+ quantity: number,
40
+ groupTicketSeatCount?: number | null,
41
+ isGroupTicket?: boolean | null
42
+ ): number {
43
+ const qty = Math.max(0, Math.floor(quantity));
44
+ if (isGroupTicket && groupTicketSeatCount && groupTicketSeatCount > 0) {
45
+ return qty * groupTicketSeatCount;
46
+ }
47
+ return qty;
48
+ }
49
+
50
+ export function guestPartyPolicyLabel(
51
+ maxAdditionalGuests: number | null | undefined
52
+ ): string {
53
+ if (maxAdditionalGuests === 0) return "No plus-ones";
54
+ if (maxAdditionalGuests === 1) return "Plus one";
55
+ if (maxAdditionalGuests === 2) return "Plus two";
56
+ return "Unlimited";
57
+ }
58
+
59
+ /** Guest-facing RSVP copy. */
60
+ export function rsvpPartyPolicyGuestCopy(
61
+ maxAdditionalGuests: number | null | undefined
62
+ ): string {
63
+ if (maxAdditionalGuests === 0) return "Just you — no plus-ones";
64
+ if (maxAdditionalGuests === 1) return "You can bring up to 1 guest";
65
+ if (maxAdditionalGuests === 2) return "You can bring up to 2 guests";
66
+ return `You can bring up to ${RSVP_PLUS_ONES_UI_SOFT_CAP} guests`;
67
+ }
68
+
69
+ /** Guest-facing ticketed copy. */
70
+ export function ticketPartyPolicyGuestCopy(
71
+ maxAdditionalGuests: number | null | undefined
72
+ ): string | null {
73
+ if (maxAdditionalGuests == null) return null;
74
+ const seats = maxAdditionalGuests + 1;
75
+ if (seats === 1) return "1 ticket per person";
76
+ return `Up to ${seats} tickets per person`;
77
+ }
78
+
79
+ export function nearCapacityPartyError(spotsLeft: number): string {
80
+ if (spotsLeft <= 0) return "This bash is at capacity.";
81
+ if (spotsLeft === 1) return "Only 1 spot left — you can’t bring that many guests.";
82
+ return `Only ${spotsLeft} spots left — you can’t bring that many guests.`;
83
+ }
package/src/index.ts CHANGED
@@ -7,9 +7,11 @@ export * from "./venueLoyaltyRedemption.js";
7
7
  export * from "./stripeListingSubscriptionMessages.js";
8
8
  export * from "./extendedSchemas.js";
9
9
  export * from "./competitionIdeas.js";
10
+ export * from "./sideQuestIdeas.js";
10
11
  export * from "./icebreakerPrompts.js";
11
12
  export * from "./competitionValidation.js";
12
13
  export * from "./groupTicketUtils.js";
14
+ export * from "./guestPartySize.js";
13
15
  export * from "./utils/groupTicketCartUtils.js";
14
16
  export * from "./partnerStoreTypes.js";
15
17
  export * from "./storeTypes.js";
@@ -177,6 +177,9 @@ export const CreditSourceType = {
177
177
  ReferralCodeUsed: "ReferralCodeUsed",
178
178
  VenueLoyaltyRedemption: "VenueLoyaltyRedemption",
179
179
  PartnerPromoRedemption: "PartnerPromoRedemption",
180
+ SideQuestCompletion: "SideQuestCompletion",
181
+ SideQuestMilestone: "SideQuestMilestone",
182
+ CommunityQuestUnlock: "CommunityQuestUnlock",
180
183
  } as const satisfies Record<CreditSourceTypeEnum, CreditSourceTypeEnum>;
181
184
 
182
185
  export type CreditSourceType = CreditSourceTypeEnum;