@bash-app/bash-common 30.275.0 → 30.276.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 (57) hide show
  1. package/dist/__tests__/groupTicketUtils.test.d.ts +2 -0
  2. package/dist/__tests__/groupTicketUtils.test.d.ts.map +1 -0
  3. package/dist/__tests__/groupTicketUtils.test.js +38 -0
  4. package/dist/__tests__/groupTicketUtils.test.js.map +1 -0
  5. package/dist/competitionIdeas.d.ts +19 -0
  6. package/dist/competitionIdeas.d.ts.map +1 -0
  7. package/dist/competitionIdeas.js +405 -0
  8. package/dist/competitionIdeas.js.map +1 -0
  9. package/dist/competitionValidation.d.ts +10 -0
  10. package/dist/competitionValidation.d.ts.map +1 -0
  11. package/dist/competitionValidation.js +29 -0
  12. package/dist/competitionValidation.js.map +1 -0
  13. package/dist/definitions.d.ts +4 -0
  14. package/dist/definitions.d.ts.map +1 -1
  15. package/dist/definitions.js.map +1 -1
  16. package/dist/extendedSchemas.d.ts +351 -1
  17. package/dist/extendedSchemas.d.ts.map +1 -1
  18. package/dist/extendedSchemas.js +15 -0
  19. package/dist/extendedSchemas.js.map +1 -1
  20. package/dist/groupTicketUtils.d.ts +19 -0
  21. package/dist/groupTicketUtils.d.ts.map +1 -0
  22. package/dist/groupTicketUtils.js +74 -0
  23. package/dist/groupTicketUtils.js.map +1 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +4 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/utils/__tests__/paymentUtils.test.js +18 -0
  29. package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
  30. package/dist/utils/__tests__/ticketListUtils.test.js +10 -0
  31. package/dist/utils/__tests__/ticketListUtils.test.js.map +1 -1
  32. package/dist/utils/groupTicketCartUtils.d.ts +12 -0
  33. package/dist/utils/groupTicketCartUtils.d.ts.map +1 -0
  34. package/dist/utils/groupTicketCartUtils.js +30 -0
  35. package/dist/utils/groupTicketCartUtils.js.map +1 -0
  36. package/dist/utils/paymentUtils.d.ts +10 -0
  37. package/dist/utils/paymentUtils.d.ts.map +1 -1
  38. package/dist/utils/paymentUtils.js +37 -1
  39. package/dist/utils/paymentUtils.js.map +1 -1
  40. package/dist/utils/ticketListUtils.d.ts.map +1 -1
  41. package/dist/utils/ticketListUtils.js +15 -3
  42. package/dist/utils/ticketListUtils.js.map +1 -1
  43. package/package.json +1 -1
  44. package/prisma/migrations/competition-prize-catalog-entry-tier.sql +39 -0
  45. package/prisma/schema.prisma +90 -7
  46. package/src/__tests__/groupTicketUtils.test.ts +53 -0
  47. package/src/competitionIdeas.ts +429 -0
  48. package/src/competitionValidation.ts +42 -0
  49. package/src/definitions.ts +5 -0
  50. package/src/extendedSchemas.ts +47 -0
  51. package/src/groupTicketUtils.ts +101 -0
  52. package/src/index.ts +4 -0
  53. package/src/utils/__tests__/paymentUtils.test.ts +19 -0
  54. package/src/utils/__tests__/ticketListUtils.test.ts +12 -0
  55. package/src/utils/groupTicketCartUtils.ts +36 -0
  56. package/src/utils/paymentUtils.ts +42 -1
  57. package/src/utils/ticketListUtils.ts +19 -5
@@ -0,0 +1,429 @@
1
+ import { CompetitionType, JudgingType } from "@prisma/client";
2
+
3
+ export interface CompetitionIdea {
4
+ id: string;
5
+ title: string;
6
+ description: string;
7
+ competitionType: CompetitionType;
8
+ subtype: string;
9
+ judgingType: JudgingType;
10
+ tags?: string[];
11
+ suggestsParticipationTier?: boolean;
12
+ rulesSnippet?: string;
13
+ }
14
+
15
+ export const COMPETITION_IDEAS: CompetitionIdea[] = [
16
+ {
17
+ id: "art-contest",
18
+ title: "Art contest",
19
+ description: "Live or submitted artwork judged on creativity and skill.",
20
+ competitionType: CompetitionType.SKILL,
21
+ subtype: "Art contests (speed painting, live art)",
22
+ judgingType: JudgingType.JUDGES,
23
+ tags: ["Skill", "Creative"],
24
+ },
25
+ {
26
+ id: "audience-favorite-dish",
27
+ title: "Audience favorite dish",
28
+ description: "Potluck or tasting event — guests vote for their favorite plate.",
29
+ competitionType: CompetitionType.AUDIENCE_VOTING,
30
+ subtype: "Best dish (potlucks)",
31
+ judgingType: JudgingType.AUDIENCE_VOTE,
32
+ tags: ["Food", "Vote"],
33
+ },
34
+ {
35
+ id: "barista-latte-art",
36
+ title: "Barista latte art contest",
37
+ description: "Baristas pour their best latte art for judges or crowd applause.",
38
+ competitionType: CompetitionType.FOOD_DRINK,
39
+ subtype: "Barista/latte art contest",
40
+ judgingType: JudgingType.JUDGES,
41
+ tags: ["Food", "Skill"],
42
+ },
43
+ {
44
+ id: "best-booth-decor",
45
+ title: "Best booth decor",
46
+ description: "Vendors compete for the best-looking booth at the fair.",
47
+ competitionType: CompetitionType.VENDOR_BOOTH,
48
+ subtype: "Best booth decor",
49
+ judgingType: JudgingType.AUDIENCE_VOTE,
50
+ tags: ["Vendor", "Vote"],
51
+ },
52
+ {
53
+ id: "best-costume",
54
+ title: "Best costume contest",
55
+ description: "Show off creative costumes — judges or audience pick winners.",
56
+ competitionType: CompetitionType.SKILL,
57
+ subtype: "Costume contests",
58
+ judgingType: JudgingType.JUDGES,
59
+ tags: ["Seasonal", "Fun"],
60
+ },
61
+ {
62
+ id: "best-dressed",
63
+ title: "Best dressed",
64
+ description: "Formal or themed event — who nailed the look?",
65
+ competitionType: CompetitionType.AUDIENCE_VOTING,
66
+ subtype: "Best dressed",
67
+ judgingType: JudgingType.AUDIENCE_VOTE,
68
+ tags: ["Vote", "Social"],
69
+ },
70
+ {
71
+ id: "bingo-night",
72
+ title: "Bingo night",
73
+ description: "Classic bingo rounds — host calls numbers and tracks winners.",
74
+ competitionType: CompetitionType.INTERACTIVE,
75
+ subtype: "Bingo",
76
+ judgingType: JudgingType.HOST_CHOOSES,
77
+ tags: ["Interactive", "All ages"],
78
+ },
79
+ {
80
+ id: "case-study-competition",
81
+ title: "Case study competition",
82
+ description: "Teams present solutions; panel scores on rubric.",
83
+ competitionType: CompetitionType.BUSINESS_PROFESSIONAL,
84
+ subtype: "Case study competitions",
85
+ judgingType: JudgingType.JUDGES,
86
+ tags: ["Professional", "Teams"],
87
+ },
88
+ {
89
+ id: "check-in-contest",
90
+ title: "Check-in contest",
91
+ description: "First attendees to check in win a sponsor gift (host confirms).",
92
+ competitionType: CompetitionType.SOCIAL_MEDIA,
93
+ subtype: "Check-in contests (first X people get a prize)",
94
+ judgingType: JudgingType.FIRST_COME,
95
+ tags: ["Door", "Sponsor"],
96
+ },
97
+ {
98
+ id: "chili-cook-off",
99
+ title: "Chili cook-off",
100
+ description: "Cooks bring their best chili; judges score taste and presentation.",
101
+ competitionType: CompetitionType.FOOD_DRINK,
102
+ subtype: "Chili cook-off / BBQ contest",
103
+ judgingType: JudgingType.JUDGES,
104
+ tags: ["Food", "Fundraiser"],
105
+ },
106
+ {
107
+ id: "coloring-contest",
108
+ title: "Coloring contest",
109
+ description: "Kids submit coloring pages; host picks winners by age group.",
110
+ competitionType: CompetitionType.KIDS_FAMILY,
111
+ subtype: "Coloring contests",
112
+ judgingType: JudgingType.HOST_CHOOSES,
113
+ tags: ["Kids", "Family"],
114
+ },
115
+ {
116
+ id: "cornhole-tournament",
117
+ title: "Cornhole tournament",
118
+ description: "Single-elimination bracket on the lawn.",
119
+ competitionType: CompetitionType.PHYSICAL,
120
+ subtype: "Cornhole tournaments",
121
+ judgingType: JudgingType.BRACKET_TOURNAMENT,
122
+ tags: ["Outdoor", "Bracket"],
123
+ },
124
+ {
125
+ id: "cupcake-decorating",
126
+ title: "Cupcake decorating contest",
127
+ description: "Decorate cupcakes on the spot; judges pick the most creative.",
128
+ competitionType: CompetitionType.FOOD_DRINK,
129
+ subtype: "Cupcake decorating",
130
+ judgingType: JudgingType.JUDGES,
131
+ tags: ["Food", "Kids"],
132
+ },
133
+ {
134
+ id: "dance-off",
135
+ title: "Dance-off",
136
+ description: "Dancers battle in rounds; audience or judges advance winners.",
137
+ competitionType: CompetitionType.SKILL,
138
+ subtype: "Dance-offs",
139
+ judgingType: JudgingType.AUDIENCE_VOTE,
140
+ tags: ["Skill", "Performance"],
141
+ },
142
+ {
143
+ id: "design-sprint",
144
+ title: "Design sprint showcase",
145
+ description: "Teams demo prototypes; judges score on impact and polish.",
146
+ competitionType: CompetitionType.BUSINESS_PROFESSIONAL,
147
+ subtype: "Design sprints",
148
+ judgingType: JudgingType.JUDGES,
149
+ tags: ["Professional", "Teams"],
150
+ },
151
+ {
152
+ id: "eating-contest",
153
+ title: "Eating contest",
154
+ description: "Timed eating challenge — fastest or most completed wins.",
155
+ competitionType: CompetitionType.FOOD_DRINK,
156
+ subtype: "Eating competitions (hot wings, donuts, pie, etc.)",
157
+ judgingType: JudgingType.METRICS,
158
+ tags: ["Food", "Fun"],
159
+ },
160
+ {
161
+ id: "fan-favorite-vendor",
162
+ title: "Fan favorite vendor",
163
+ description: "Attendees vote for their favorite vendor booth.",
164
+ competitionType: CompetitionType.VENDOR_BOOTH,
165
+ subtype: "Fan favorite vendor",
166
+ judgingType: JudgingType.AUDIENCE_VOTE,
167
+ tags: ["Vendor", "Vote"],
168
+ },
169
+ {
170
+ id: "gingerbread-houses",
171
+ title: "Gingerbread house competition",
172
+ description: "Teams build gingerbread houses; judges score creativity.",
173
+ competitionType: CompetitionType.SEASONAL_HOLIDAY,
174
+ subtype: "Gingerbread house competition",
175
+ judgingType: JudgingType.JUDGES,
176
+ tags: ["Seasonal", "Family"],
177
+ },
178
+ {
179
+ id: "hackathon",
180
+ title: "Hackathon showcase",
181
+ description: "Teams demo builds; judges score on technical merit and UX.",
182
+ competitionType: CompetitionType.BUSINESS_PROFESSIONAL,
183
+ subtype: "Hackathons",
184
+ judgingType: JudgingType.JUDGES,
185
+ tags: ["Professional", "Tech"],
186
+ },
187
+ {
188
+ id: "hashtag-photo-contest",
189
+ title: "Hashtag photo contest",
190
+ description: "Best photo posted with the event hashtag — host or crowd picks.",
191
+ competitionType: CompetitionType.SOCIAL_MEDIA,
192
+ subtype: "Best photo posted with event hashtag",
193
+ judgingType: JudgingType.AUDIENCE_VOTE,
194
+ tags: ["Social", "Marketing"],
195
+ },
196
+ {
197
+ id: "lego-build-off",
198
+ title: "LEGO build-off",
199
+ description: "Build teams compete on a theme within a time limit.",
200
+ competitionType: CompetitionType.KIDS_FAMILY,
201
+ subtype: "LEGO build-offs",
202
+ judgingType: JudgingType.JUDGES,
203
+ tags: ["Kids", "Creative"],
204
+ },
205
+ {
206
+ id: "lip-sync-battle",
207
+ title: "Lip sync battle",
208
+ description: "Performers lip sync to tracks; audience votes for the best show.",
209
+ competitionType: CompetitionType.INTERACTIVE,
210
+ subtype: "Lip sync battles",
211
+ judgingType: JudgingType.AUDIENCE_VOTE,
212
+ tags: ["Performance", "Fun"],
213
+ },
214
+ {
215
+ id: "minute-to-win-it",
216
+ title: "Minute to Win It games",
217
+ description: "Quick challenge stations — host tracks fastest times.",
218
+ competitionType: CompetitionType.INTERACTIVE,
219
+ subtype: "Minute-to-Win-It style games",
220
+ judgingType: JudgingType.METRICS,
221
+ tags: ["Interactive", "All ages"],
222
+ },
223
+ {
224
+ id: "mixology-challenge",
225
+ title: "Mixology challenge",
226
+ description: "Bartenders craft signature drinks; judges score taste and presentation.",
227
+ competitionType: CompetitionType.FOOD_DRINK,
228
+ subtype: "Mixology or mocktail challenge",
229
+ judgingType: JudgingType.JUDGES,
230
+ tags: ["Food", "21+"],
231
+ },
232
+ {
233
+ id: "most-interactive-booth",
234
+ title: "Most interactive booth",
235
+ description: "Which vendor booth engaged attendees the most?",
236
+ competitionType: CompetitionType.VENDOR_BOOTH,
237
+ subtype: "Most interactive booth",
238
+ judgingType: JudgingType.AUDIENCE_VOTE,
239
+ tags: ["Vendor", "Vote"],
240
+ },
241
+ {
242
+ id: "obstacle-course",
243
+ title: "Obstacle course race",
244
+ description: "Timed runs through obstacles — fastest time wins.",
245
+ competitionType: CompetitionType.PHYSICAL,
246
+ subtype: "Obstacle courses",
247
+ judgingType: JudgingType.METRICS,
248
+ tags: ["Outdoor", "Athletic"],
249
+ },
250
+ {
251
+ id: "photography-contest",
252
+ title: "Photography contest",
253
+ description: "Submit or display photos; judges score composition and story.",
254
+ competitionType: CompetitionType.SKILL,
255
+ subtype: "Photography contests",
256
+ judgingType: JudgingType.JUDGES,
257
+ tags: ["Creative", "Skill"],
258
+ },
259
+ {
260
+ id: "pitch-competition",
261
+ title: "Pitch competition",
262
+ description: "Founders pitch ideas; panel scores and picks a winner.",
263
+ competitionType: CompetitionType.BUSINESS_PROFESSIONAL,
264
+ subtype: "Pitch competitions",
265
+ judgingType: JudgingType.JUDGES,
266
+ tags: ["Professional", "Startup"],
267
+ },
268
+ {
269
+ id: "pumpkin-carving",
270
+ title: "Pumpkin carving contest",
271
+ description: "Carve on site; host or judges pick the best jack-o'-lantern.",
272
+ competitionType: CompetitionType.KIDS_FAMILY,
273
+ subtype: "Pumpkin carving",
274
+ judgingType: JudgingType.HOST_CHOOSES,
275
+ tags: ["Seasonal", "Family"],
276
+ },
277
+ {
278
+ id: "relay-race",
279
+ title: "Relay race",
280
+ description: "Team relay on the field — first team across wins.",
281
+ competitionType: CompetitionType.PHYSICAL,
282
+ subtype: "Relay races",
283
+ judgingType: JudgingType.METRICS,
284
+ tags: ["Outdoor", "Teams"],
285
+ },
286
+ {
287
+ id: "scavenger-hunt",
288
+ title: "Scavenger hunt",
289
+ description: "Teams find clues around the venue — first to finish wins.",
290
+ competitionType: CompetitionType.PHYSICAL,
291
+ subtype: "Scavenger hunts",
292
+ judgingType: JudgingType.METRICS,
293
+ tags: ["Outdoor", "Teams"],
294
+ },
295
+ {
296
+ id: "science-fair",
297
+ title: "Science fair expo",
298
+ description: "Projects on display; judges score on hypothesis and presentation.",
299
+ competitionType: CompetitionType.BUSINESS_PROFESSIONAL,
300
+ subtype: "Science fairs/expos",
301
+ judgingType: JudgingType.JUDGES,
302
+ tags: ["Education", "Kids"],
303
+ },
304
+ {
305
+ id: "singing-battle",
306
+ title: "Singing or karaoke battle",
307
+ description: "Performers sing live; audience or judges pick favorites.",
308
+ competitionType: CompetitionType.SKILL,
309
+ subtype: "Singing or karaoke battles",
310
+ judgingType: JudgingType.AUDIENCE_VOTE,
311
+ tags: ["Performance", "Skill"],
312
+ },
313
+ {
314
+ id: "startup-demo-day",
315
+ title: "Startup demo day voting",
316
+ description: "Founders demo on stage; audience votes for fan favorite.",
317
+ competitionType: CompetitionType.BUSINESS_PROFESSIONAL,
318
+ subtype: "Startup demo day voting",
319
+ judgingType: JudgingType.AUDIENCE_VOTE,
320
+ tags: ["Professional", "Vote"],
321
+ },
322
+ {
323
+ id: "talent-show",
324
+ title: "Talent show",
325
+ description: "Open mic performances; judges or audience advance acts.",
326
+ competitionType: CompetitionType.SKILL,
327
+ subtype: "Talent shows",
328
+ judgingType: JudgingType.JUDGES,
329
+ tags: ["Performance", "All ages"],
330
+ },
331
+ {
332
+ id: "treasure-hunt",
333
+ title: "Treasure hunt",
334
+ description: "Follow clues to a hidden prize — host verifies first find.",
335
+ competitionType: CompetitionType.RANDOMIZED,
336
+ subtype: "Treasure hunts",
337
+ judgingType: JudgingType.METRICS,
338
+ tags: ["Interactive", "Outdoor"],
339
+ },
340
+ {
341
+ id: "trivia-night",
342
+ title: "Trivia night",
343
+ description: "Teams answer rounds of questions; highest score wins.",
344
+ competitionType: CompetitionType.SKILL,
345
+ subtype: "Trivia competitions",
346
+ judgingType: JudgingType.METRICS,
347
+ tags: ["Indoor", "Teams"],
348
+ },
349
+ {
350
+ id: "tug-of-war",
351
+ title: "Tug of war",
352
+ description: "Team bracket on the rope — last team standing wins.",
353
+ competitionType: CompetitionType.PHYSICAL,
354
+ subtype: "Tug of war",
355
+ judgingType: JudgingType.BRACKET_TOURNAMENT,
356
+ tags: ["Outdoor", "Teams"],
357
+ },
358
+ {
359
+ id: "ugly-sweater",
360
+ title: "Ugly sweater contest",
361
+ description: "Holiday sweaters judged for creativity and commitment.",
362
+ competitionType: CompetitionType.SEASONAL_HOLIDAY,
363
+ subtype: "Ugly sweater contest",
364
+ judgingType: JudgingType.AUDIENCE_VOTE,
365
+ tags: ["Seasonal", "Fun"],
366
+ },
367
+ {
368
+ id: "white-elephant",
369
+ title: "White elephant gift exchange",
370
+ description: "Participants bring wrapped gifts; steal-and-swap game run by the host.",
371
+ competitionType: CompetitionType.INTERACTIVE,
372
+ subtype: "White elephant / gift exchange",
373
+ judgingType: JudgingType.HOST_CHOOSES,
374
+ tags: ["Fundraiser", "Interactive"],
375
+ suggestsParticipationTier: true,
376
+ rulesSnippet:
377
+ "- Bring one wrapped gift\n- Must be present to play\n- Host runs steal/swap rounds\n- Prizes listed are sponsor donations, not cash values",
378
+ },
379
+ ];
380
+
381
+ export function competitionIdeasSorted(): CompetitionIdea[] {
382
+ return [...COMPETITION_IDEAS].sort((a, b) =>
383
+ a.title.localeCompare(b.title, undefined, { sensitivity: "base" })
384
+ );
385
+ }
386
+
387
+ export function competitionIdeasByLetter(): Map<string, CompetitionIdea[]> {
388
+ const sorted = competitionIdeasSorted();
389
+ const map = new Map<string, CompetitionIdea[]>();
390
+ for (const idea of sorted) {
391
+ const first = idea.title.trim().charAt(0).toUpperCase();
392
+ const letter = /[A-Z]/.test(first) ? first : "#";
393
+ const list = map.get(letter) ?? [];
394
+ list.push(idea);
395
+ map.set(letter, list);
396
+ }
397
+ return map;
398
+ }
399
+
400
+ /** Unique subtype labels per competition type (for host subtype dropdown). */
401
+ export function competitionSubtypesByType(): Record<
402
+ CompetitionType,
403
+ string[]
404
+ > {
405
+ const out = {} as Record<CompetitionType, string[]>;
406
+ for (const idea of COMPETITION_IDEAS) {
407
+ const existing = out[idea.competitionType] ?? [];
408
+ if (!existing.includes(idea.subtype)) {
409
+ existing.push(idea.subtype);
410
+ }
411
+ out[idea.competitionType] = existing;
412
+ }
413
+ for (const key of Object.keys(out) as CompetitionType[]) {
414
+ out[key].sort((a, b) => a.localeCompare(b));
415
+ }
416
+ return out;
417
+ }
418
+
419
+ export function filterCompetitionIdeas(query: string): CompetitionIdea[] {
420
+ const q = query.trim().toLowerCase();
421
+ if (!q) return competitionIdeasSorted();
422
+ return competitionIdeasSorted().filter(
423
+ (idea) =>
424
+ idea.title.toLowerCase().includes(q) ||
425
+ idea.description.toLowerCase().includes(q) ||
426
+ idea.subtype.toLowerCase().includes(q) ||
427
+ (idea.tags?.some((t) => t.toLowerCase().includes(q)) ?? false)
428
+ );
429
+ }
@@ -0,0 +1,42 @@
1
+ import { JudgingType } from "@prisma/client";
2
+
3
+ /** Judging modes disallowed for new competitions (lottery / auction-adjacent). */
4
+ export const DISALLOWED_JUDGING_TYPES_FOR_NEW: ReadonlySet<JudgingType> = new Set([
5
+ JudgingType.RANDOM_DRAW,
6
+ JudgingType.HIGHEST_BID,
7
+ ]);
8
+
9
+ export const DISALLOWED_JUDGING_TYPE_MESSAGE =
10
+ "Random draw and highest-bid judging are not supported on Bash. Use host choice, skill judging, audience vote, metrics, first come, or tournament bracket.";
11
+
12
+ export function isDisallowedJudgingTypeForNew(
13
+ judgingType: JudgingType | null | undefined
14
+ ): boolean {
15
+ if (!judgingType) return false;
16
+ return DISALLOWED_JUDGING_TYPES_FOR_NEW.has(judgingType);
17
+ }
18
+
19
+ export function assertAllowedJudgingTypeForNew(
20
+ judgingType: JudgingType | null | undefined
21
+ ): void {
22
+ if (isDisallowedJudgingTypeForNew(judgingType)) {
23
+ throw new Error(DISALLOWED_JUDGING_TYPE_MESSAGE);
24
+ }
25
+ }
26
+
27
+ /** Default judging type for new competitions. */
28
+ export const DEFAULT_COMPETITION_JUDGING_TYPE: JudgingType =
29
+ JudgingType.HOST_CHOOSES;
30
+
31
+ export function normalizeJudgingTypeForSave(
32
+ judgingType: JudgingType | null | undefined,
33
+ isExistingLegacy: boolean
34
+ ): JudgingType {
35
+ if (!judgingType) {
36
+ return isExistingLegacy ? JudgingType.RANDOM_DRAW : DEFAULT_COMPETITION_JUDGING_TYPE;
37
+ }
38
+ if (!isExistingLegacy && isDisallowedJudgingTypeForNew(judgingType)) {
39
+ throw new Error(DISALLOWED_JUDGING_TYPE_MESSAGE);
40
+ }
41
+ return judgingType;
42
+ }
@@ -749,6 +749,8 @@ export interface LinkedInAuthData {
749
749
  export interface NumberOfTicketsForDate {
750
750
  numberOfTickets: number;
751
751
  ticketDateTime: DateTimeArgType;
752
+ /** Pay-what-you-can tier: buyer-chosen unit price in cents (min = tier.price). */
753
+ unitPriceCents?: number;
752
754
  }
753
755
 
754
756
  export interface TicketTierAvailability {
@@ -942,6 +944,9 @@ export interface StripeCreateBashEventTicketsCheckoutSessionArgs extends UtmFiel
942
944
 
943
945
  /** Required when the event has `waiverRequired` — buyer attested to host waiver/terms */
944
946
  waiverAccepted?: boolean;
947
+
948
+ /** EventGroup id when purchasing a group package for the crew */
949
+ groupId?: string;
945
950
  }
946
951
 
947
952
  export interface StripeCreateBashEventDonationCheckoutSessionArgs extends UtmFields {
@@ -14,6 +14,7 @@ import {
14
14
  Checkout,
15
15
  Competition,
16
16
  CompetitionParticipant,
17
+ CompetitionSponsor,
17
18
  CompetitionVote,
18
19
  Contact,
19
20
  Coordinates,
@@ -249,12 +250,43 @@ export interface SponsoredEventExt extends SponsoredEvent {}
249
250
 
250
251
  export interface PrizeExt extends Prize {
251
252
  winner?: PublicUser | null;
253
+ contributor?: PublicUser | null;
254
+ sourceService?: {
255
+ id: string;
256
+ serviceName: string | null;
257
+ coverPhoto: string | null;
258
+ } | null;
259
+ }
260
+
261
+ export interface CompetitionPrizeOfferExt {
262
+ id: string;
263
+ competitionId: string;
264
+ offeredByUserId: string;
265
+ status: string;
266
+ name: string;
267
+ description: string;
268
+ imageUrl?: string | null;
269
+ sponsorLabel?: string | null;
270
+ sourceServiceId?: string | null;
271
+ sourceSku?: string | null;
272
+ hostNote?: string | null;
273
+ createdAt: Date | string;
274
+ respondedAt?: Date | string | null;
275
+ offeredBy?: PublicUser;
276
+ sourceService?: {
277
+ id: string;
278
+ serviceName: string | null;
279
+ coverPhoto: string | null;
280
+ } | null;
252
281
  }
253
282
 
254
283
  export interface CompetitionExt extends Competition {
255
284
  prizes: PrizeExt[];
256
285
  participants?: CompetitionParticipant[];
257
286
  votes?: CompetitionVote[];
287
+ entryTicketTier?: TicketTier | null;
288
+ sponsor?: CompetitionSponsor[];
289
+ prizeOffers?: CompetitionPrizeOfferExt[];
258
290
  }
259
291
 
260
292
  /** One row in BashEvent.merchandiseItems (host wizard; informational catalog) */
@@ -456,11 +488,26 @@ export const BASH_EVENT_DATA_TO_INCLUDE = {
456
488
  invitations: true,
457
489
  competitions: {
458
490
  include: {
491
+ entryTicketTier: {
492
+ select: {
493
+ id: true,
494
+ title: true,
495
+ price: true,
496
+ isDonationTier: true,
497
+ },
498
+ },
499
+ sponsor: true,
459
500
  prizes: {
460
501
  include: {
461
502
  winner: {
462
503
  select: FRONT_END_USER_DATA_TO_SELECT,
463
504
  },
505
+ contributor: {
506
+ select: FRONT_END_USER_DATA_TO_SELECT,
507
+ },
508
+ sourceService: {
509
+ select: { id: true, serviceName: true, coverPhoto: true },
510
+ },
464
511
  },
465
512
  },
466
513
  participants: {
@@ -0,0 +1,101 @@
1
+ import type { TicketTier } from "@prisma/client";
2
+
3
+ /** Minimum seats per group package tier. */
4
+ export const MIN_GROUP_TICKET_SEAT_COUNT = 2;
5
+
6
+ /** Maximum seats per group package tier (v1). */
7
+ export const MAX_GROUP_TICKET_SEAT_COUNT = 20;
8
+
9
+ export type GroupTicketTierFields = Pick<
10
+ TicketTier,
11
+ "isGroupTicket" | "groupTicketSeatCount" | "pricingType" | "isDonationTier" | "isWaitlistTier"
12
+ >;
13
+
14
+ export function isGroupTicketTier(
15
+ tier: Pick<TicketTier, "isGroupTicket" | "groupTicketSeatCount">
16
+ ): boolean {
17
+ return (
18
+ tier.isGroupTicket === true &&
19
+ tier.groupTicketSeatCount != null &&
20
+ tier.groupTicketSeatCount >= MIN_GROUP_TICKET_SEAT_COUNT
21
+ );
22
+ }
23
+
24
+ export function validateGroupTicketTierInput(
25
+ tier: GroupTicketTierFields
26
+ ): { ok: true } | { ok: false; message: string } {
27
+ if (!tier.isGroupTicket) {
28
+ return { ok: true };
29
+ }
30
+ const seats = tier.groupTicketSeatCount;
31
+ if (seats == null || Number.isNaN(seats)) {
32
+ return {
33
+ ok: false,
34
+ message: "Group package tiers need a seat count (how many people are included).",
35
+ };
36
+ }
37
+ if (seats < MIN_GROUP_TICKET_SEAT_COUNT) {
38
+ return {
39
+ ok: false,
40
+ message: `Group packages need at least ${MIN_GROUP_TICKET_SEAT_COUNT} people.`,
41
+ };
42
+ }
43
+ if (seats > MAX_GROUP_TICKET_SEAT_COUNT) {
44
+ return {
45
+ ok: false,
46
+ message: `Group packages can include up to ${MAX_GROUP_TICKET_SEAT_COUNT} people.`,
47
+ };
48
+ }
49
+ if (tier.isDonationTier === true) {
50
+ return {
51
+ ok: false,
52
+ message: "Donation tiers cannot be group packages.",
53
+ };
54
+ }
55
+ if (tier.isWaitlistTier === true) {
56
+ return {
57
+ ok: false,
58
+ message: "Waitlist tiers cannot be group packages.",
59
+ };
60
+ }
61
+ if (tier.pricingType === "BASHPOINTS") {
62
+ return {
63
+ ok: false,
64
+ message: "BashPoints tiers cannot be group packages yet.",
65
+ };
66
+ }
67
+ return { ok: true };
68
+ }
69
+
70
+ /** Sold ticket rows → packages consumed (for inventory caps). */
71
+ export function packagesSoldFromTicketCount(
72
+ ticketRowCount: number,
73
+ seatCount: number
74
+ ): number {
75
+ if (seatCount < 1) {
76
+ return ticketRowCount;
77
+ }
78
+ return Math.ceil(ticketRowCount / seatCount);
79
+ }
80
+
81
+ /** Per-person share in cents for Venmo hints (integer division). */
82
+ export function groupPackageShareCents(
83
+ packagePriceCents: number,
84
+ seatCount: number
85
+ ): number {
86
+ if (seatCount < 1) {
87
+ return packagePriceCents;
88
+ }
89
+ return Math.floor(packagePriceCents / seatCount);
90
+ }
91
+
92
+ export function formatGroupPackagePerPersonUsd(
93
+ packagePriceCents: number,
94
+ seatCount: number
95
+ ): string {
96
+ const dollars = groupPackageShareCents(packagePriceCents, seatCount) / 100;
97
+ return new Intl.NumberFormat("en-US", {
98
+ style: "currency",
99
+ currency: "USD",
100
+ }).format(dollars);
101
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,10 @@ export * from "./utmAttribution.js";
6
6
  export * from "./venueLoyaltyRedemption.js";
7
7
  export * from "./stripeListingSubscriptionMessages.js";
8
8
  export * from "./extendedSchemas.js";
9
+ export * from "./competitionIdeas.js";
10
+ export * from "./competitionValidation.js";
11
+ export * from "./groupTicketUtils.js";
12
+ export * from "./utils/groupTicketCartUtils.js";
9
13
  export * from "./partnerStoreTypes.js";
10
14
  export * from "./bashFeedTypes.js";
11
15
  export * from "./membershipDefinitions.js";