@bash-app/bash-common 30.308.0 → 30.310.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 (63) hide show
  1. package/dist/__tests__/competitionValidation.test.d.ts +2 -0
  2. package/dist/__tests__/competitionValidation.test.d.ts.map +1 -0
  3. package/dist/__tests__/competitionValidation.test.js +28 -0
  4. package/dist/__tests__/competitionValidation.test.js.map +1 -0
  5. package/dist/__tests__/definitionsHelpers.test.js +8 -1
  6. package/dist/__tests__/definitionsHelpers.test.js.map +1 -1
  7. package/dist/__tests__/membershipDefinitions.test.js +5 -0
  8. package/dist/__tests__/membershipDefinitions.test.js.map +1 -1
  9. package/dist/__tests__/sideQuestIdeas.test.js +5 -1
  10. package/dist/__tests__/sideQuestIdeas.test.js.map +1 -1
  11. package/dist/__tests__/sideQuestTypes.test.d.ts +2 -0
  12. package/dist/__tests__/sideQuestTypes.test.d.ts.map +1 -0
  13. package/dist/__tests__/sideQuestTypes.test.js +29 -0
  14. package/dist/__tests__/sideQuestTypes.test.js.map +1 -0
  15. package/dist/__tests__/statusEnums.test.js +7 -0
  16. package/dist/__tests__/statusEnums.test.js.map +1 -1
  17. package/dist/competitionValidation.d.ts +11 -2
  18. package/dist/competitionValidation.d.ts.map +1 -1
  19. package/dist/competitionValidation.js +40 -2
  20. package/dist/competitionValidation.js.map +1 -1
  21. package/dist/definitions.d.ts +7 -0
  22. package/dist/definitions.d.ts.map +1 -1
  23. package/dist/definitions.js +10 -0
  24. package/dist/definitions.js.map +1 -1
  25. package/dist/extendedSchemas.d.ts +51 -11
  26. package/dist/extendedSchemas.d.ts.map +1 -1
  27. package/dist/extendedSchemas.js +5 -0
  28. package/dist/extendedSchemas.js.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/membershipDefinitions.d.ts +2 -0
  34. package/dist/membershipDefinitions.d.ts.map +1 -1
  35. package/dist/membershipDefinitions.js +3 -0
  36. package/dist/membershipDefinitions.js.map +1 -1
  37. package/dist/mirroredPrismaEnums.d.ts +4 -0
  38. package/dist/mirroredPrismaEnums.d.ts.map +1 -1
  39. package/dist/mirroredPrismaEnums.js +4 -0
  40. package/dist/mirroredPrismaEnums.js.map +1 -1
  41. package/dist/sideQuestIdeas.d.ts.map +1 -1
  42. package/dist/sideQuestIdeas.js +2 -2
  43. package/dist/sideQuestIdeas.js.map +1 -1
  44. package/dist/sideQuestTypes.d.ts +116 -0
  45. package/dist/sideQuestTypes.d.ts.map +1 -0
  46. package/dist/sideQuestTypes.js +67 -0
  47. package/dist/sideQuestTypes.js.map +1 -0
  48. package/package.json +1 -1
  49. package/prisma/schema.prisma +111 -0
  50. package/src/__tests__/competitionValidation.test.ts +43 -0
  51. package/src/__tests__/definitionsHelpers.test.ts +16 -0
  52. package/src/__tests__/membershipDefinitions.test.ts +18 -0
  53. package/src/__tests__/sideQuestIdeas.test.ts +11 -1
  54. package/src/__tests__/sideQuestTypes.test.ts +44 -0
  55. package/src/__tests__/statusEnums.test.ts +7 -0
  56. package/src/competitionValidation.ts +56 -2
  57. package/src/definitions.ts +13 -0
  58. package/src/extendedSchemas.ts +23 -11
  59. package/src/index.ts +1 -0
  60. package/src/membershipDefinitions.ts +3 -0
  61. package/src/mirroredPrismaEnums.ts +4 -0
  62. package/src/sideQuestIdeas.ts +2 -2
  63. package/src/sideQuestTypes.ts +190 -0
@@ -266,6 +266,16 @@ model Competition {
266
266
  /// How 1st/2nd/3rd place icons appear on leaderboards and profiles
267
267
  placementAwardStyle PlacementAwardStyle @default(Trophy)
268
268
 
269
+ /// Optional Adventures rewards when someone becomes an ACCEPTED participant
270
+ participationBashPoints Int?
271
+ participationEventBadgeId String?
272
+ /// Optional Adventures rewards when a participant reaches a finalist placement (placement <= 3)
273
+ finalistBashPoints Int?
274
+ finalistEventBadgeId String?
275
+ /// Optional Adventures rewards when assigned as a prize winner
276
+ winBashPoints Int?
277
+ winEventBadgeId String?
278
+
269
279
  createdAt DateTime @default(now())
270
280
  updatedAt DateTime @updatedAt
271
281
  bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
@@ -279,11 +289,18 @@ model Competition {
279
289
  judgeAssignments CompetitionJudgeAssignment[]
280
290
  judgeScores CompetitionJudgeScore[]
281
291
  notifications Notification[]
292
+ sideQuests SideQuest[]
293
+ participationEventBadge EventBadge? @relation("CompetitionParticipationBadge", fields: [participationEventBadgeId], references: [id], onDelete: SetNull)
294
+ finalistEventBadge EventBadge? @relation("CompetitionFinalistBadge", fields: [finalistEventBadgeId], references: [id], onDelete: SetNull)
295
+ winEventBadge EventBadge? @relation("CompetitionWinBadge", fields: [winEventBadgeId], references: [id], onDelete: SetNull)
282
296
 
283
297
  @@index([bashEventId])
284
298
  @@index([competitionType])
285
299
  @@index([startTime])
286
300
  @@index([endTime])
301
+ @@index([participationEventBadgeId])
302
+ @@index([finalistEventBadgeId])
303
+ @@index([winEventBadgeId])
287
304
  }
288
305
 
289
306
  model CompetitionJudgeAssignment {
@@ -373,6 +390,8 @@ model CompetitionParticipant {
373
390
  name String // Display name
374
391
  description String? @db.Text // Why they should win
375
392
  imageUrl String? // Photo/avatar
393
+ /// Optional link when the entrant is a Service listing (e.g. Best Food Truck)
394
+ sourceServiceId String?
376
395
  order Int @default(0) // Display order
377
396
  voteCount Int @default(0) // Denormalized for performance
378
397
  isEliminated Boolean @default(false) // For tournament brackets
@@ -385,6 +404,7 @@ model CompetitionParticipant {
385
404
  createdAt DateTime @default(now())
386
405
  competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
387
406
  user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
407
+ sourceService Service? @relation("CompetitionParticipantSourceService", fields: [sourceServiceId], references: [id], onDelete: SetNull)
388
408
  votes CompetitionVote[]
389
409
 
390
410
  // Tournament bracket relations
@@ -397,6 +417,7 @@ model CompetitionParticipant {
397
417
  @@index([email])
398
418
  @@index([voteCount])
399
419
  @@index([status])
420
+ @@index([sourceServiceId])
400
421
  }
401
422
 
402
423
  // Tournament bracket matches (1v1, heats, or battle royale via entrants join table)
@@ -994,6 +1015,7 @@ model BashEvent {
994
1015
  userPromoCodeRedemption UserPromoCodeRedemption[]
995
1016
  userReport UserReport[]
996
1017
  vendorBookingRequests VendorBookingRequest[] @relation("VendorBookingEvent")
1018
+ vendorCategoryExclusivities VendorCategoryExclusivity[]
997
1019
  vendorBids VendorBid[]
998
1020
  media Media[] @relation("BashEventToMedia")
999
1021
  associatedServicesReferencingMe Service[] @relation("BashEventToService")
@@ -2023,6 +2045,7 @@ enum EventBadgeAwardSource {
2023
2045
  Milestone
2024
2046
  Manual
2025
2047
  Community
2048
+ Competition
2026
2049
  }
2027
2050
 
2028
2051
  model SideQuest {
@@ -2047,17 +2070,21 @@ model SideQuest {
2047
2070
  unlockedAt DateTime?
2048
2071
  sponsorLabel String?
2049
2072
  sponsorServiceId String?
2073
+ /// When questType is Vote: the competition casting a vote completes this quest
2074
+ competitionId String?
2050
2075
  createdAt DateTime @default(now())
2051
2076
  updatedAt DateTime @updatedAt
2052
2077
 
2053
2078
  bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2054
2079
  eventBadge EventBadge? @relation(fields: [eventBadgeId], references: [id], onDelete: SetNull)
2080
+ competition Competition? @relation(fields: [competitionId], references: [id], onDelete: SetNull)
2055
2081
  completions SideQuestCompletion[]
2056
2082
  milestonesUnlocking SideQuestMilestone[] @relation("MilestoneUnlocksQuest")
2057
2083
 
2058
2084
  @@index([bashEventId])
2059
2085
  @@index([bashEventId, sortOrder])
2060
2086
  @@index([eventBadgeId])
2087
+ @@index([competitionId])
2061
2088
  }
2062
2089
 
2063
2090
  model SideQuestCompletion {
@@ -2091,6 +2118,9 @@ model EventBadge {
2091
2118
  sideQuests SideQuest[]
2092
2119
  milestones SideQuestMilestone[]
2093
2120
  communityGoals CommunityQuestGoal[]
2121
+ competitionsParticipation Competition[] @relation("CompetitionParticipationBadge")
2122
+ competitionsFinalist Competition[] @relation("CompetitionFinalistBadge")
2123
+ competitionsWin Competition[] @relation("CompetitionWinBadge")
2094
2124
 
2095
2125
  @@index([bashEventId])
2096
2126
  @@index([createdByUserId])
@@ -2208,6 +2238,7 @@ model EventDiscussionComment {
2208
2238
  parent EventDiscussionComment? @relation("EventDiscussionReplies", fields: [parentId], references: [id], onDelete: Cascade)
2209
2239
  replies EventDiscussionComment[] @relation("EventDiscussionReplies")
2210
2240
  likes EventDiscussionCommentLike[]
2241
+ reports EventDiscussionCommentReport[]
2211
2242
 
2212
2243
  @@index([bashEventId])
2213
2244
  @@index([authorId])
@@ -2228,6 +2259,27 @@ model EventDiscussionCommentLike {
2228
2259
  @@index([commentId])
2229
2260
  }
2230
2261
 
2262
+ model EventDiscussionCommentReport {
2263
+ id String @id @default(cuid())
2264
+ userId String
2265
+ commentId String
2266
+ reason BashFeedReportReason
2267
+ description String? @db.Text
2268
+ status ReportStatus @default(Pending)
2269
+ reviewedBy String?
2270
+ reviewedAt DateTime?
2271
+ createdAt DateTime @default(now())
2272
+
2273
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
2274
+ comment EventDiscussionComment @relation(fields: [commentId], references: [id], onDelete: Cascade)
2275
+ reviewer User? @relation("ReviewedDiscussionReports", fields: [reviewedBy], references: [id])
2276
+
2277
+ @@unique([userId, commentId])
2278
+ @@index([commentId])
2279
+ @@index([status])
2280
+ @@index([userId])
2281
+ }
2282
+
2231
2283
  // === Phase 3: event experience depth ========================================
2232
2284
  // Scalar-FK models (no Prisma relations to the large BashEvent/User models);
2233
2285
  // DB foreign keys are added in the migration for integrity.
@@ -3766,6 +3818,8 @@ model User {
3766
3818
  bashFeedComments BashFeedComment[]
3767
3819
  bashFeedCommentLikes BashFeedCommentLike[]
3768
3820
  eventDiscussionCommentLikes EventDiscussionCommentLike[]
3821
+ discussionCommentReports EventDiscussionCommentReport[]
3822
+ reviewedDiscussionCommentReports EventDiscussionCommentReport[] @relation("ReviewedDiscussionReports")
3769
3823
  bashFeedRatings BashFeedRating[]
3770
3824
  bashFeedReports BashFeedReport[]
3771
3825
  reviewedFeedReports BashFeedReport[] @relation("ReviewedReports")
@@ -4347,6 +4401,11 @@ model Service {
4347
4401
  sponsorId String? @unique
4348
4402
  venueId String? @unique
4349
4403
  organizationId String? @unique
4404
+ /// Set when this Service was created via "Also list as [complementary type]"
4405
+ /// (e.g. Vendors -> EventServices) as a one-time identity-field prefill copy —
4406
+ /// not an ongoing synced link. Used to avoid creating duplicate complementary
4407
+ /// drafts from repeated clicks; the two profiles are otherwise independent.
4408
+ complementaryOfServiceId String?
4350
4409
  isFreeFirstListing Boolean @default(false)
4351
4410
  monthlyPrice Decimal @default(0)
4352
4411
  serviceListingStripeSubscriptionId String?
@@ -4377,6 +4436,8 @@ model Service {
4377
4436
  exhibitorBookingRequests ExhibitorBookingRequest[] @relation("ExhibitorBookingService")
4378
4437
  notification Notification[]
4379
4438
  creator User? @relation("CreatedService", fields: [creatorId], references: [id])
4439
+ complementaryOf Service? @relation("ComplementaryService", fields: [complementaryOfServiceId], references: [id])
4440
+ complementaryProfiles Service[] @relation("ComplementaryService")
4380
4441
  entertainmentService EntertainmentService? @relation(fields: [entertainmentServiceId], references: [id], onDelete: Cascade)
4381
4442
  eventService EventService? @relation(fields: [eventServiceId], references: [id], onDelete: Cascade)
4382
4443
  exhibitor Exhibitor? @relation(fields: [exhibitorId], references: [id], onDelete: Cascade)
@@ -4416,6 +4477,7 @@ model Service {
4416
4477
  competitionPrizesSourced Prize[] @relation("PrizeSourceService")
4417
4478
  competitionSponsorCredits CompetitionSponsor[] @relation("CompetitionSponsorSourceService")
4418
4479
  competitionPrizeOffersSourced CompetitionPrizeOffer[] @relation("CompetitionPrizeOfferSourceService")
4480
+ competitionParticipantsSourced CompetitionParticipant[] @relation("CompetitionParticipantSourceService")
4419
4481
  venueMatchCandidates VenueMatchCandidate[]
4420
4482
  supplyLeadConversions VenueSupplyLead[] @relation("SupplyLeadConvertedService")
4421
4483
  googleReviews GoogleReview[]
@@ -4429,6 +4491,7 @@ model Service {
4429
4491
  @@index([isLicensed])
4430
4492
  @@index([isFeaturedService, featuredReason, featuredAt])
4431
4493
  @@index([latitude, longitude])
4494
+ @@index([complementaryOfServiceId])
4432
4495
  }
4433
4496
 
4434
4497
  model StripeAccount {
@@ -4666,6 +4729,16 @@ model Vendor {
4666
4729
  tierPricing Json? // For tiered pricing: [{name: "Bronze", priceCents: 100000, description: "..."}, ...]
4667
4730
  pricingNotes String? // Additional pricing details or terms
4668
4731
 
4732
+ // Guarantee & deal-terms communication (informational only — Bash does not
4733
+ // calculate or process any guarantee shortfall payment). Preferred/minimum
4734
+ // attendance for this context reuses crowdSize (crowdSizeId/AmountOfGuests)
4735
+ // above rather than a separate field.
4736
+ requiresGuarantee Boolean? @default(false)
4737
+ minimumRevenueGuaranteeCents Int?
4738
+ averageSalesGoalCents Int?
4739
+ openToGuaranteeNegotiation Boolean? @default(true)
4740
+ guaranteeNotes String? @db.Text
4741
+
4669
4742
  service Service?
4670
4743
  crowdSize AmountOfGuests? @relation(fields: [crowdSizeId], references: [id], onDelete: Cascade)
4671
4744
  serviceRange ServiceRange? @relation(fields: [serviceRangeId], references: [id])
@@ -6204,12 +6277,22 @@ model VendorBookingRequest {
6204
6277
  paidAt DateTime?
6205
6278
  vendorResponse String?
6206
6279
  respondedAt DateTime?
6280
+ /// Informational counter-offer to the vendor's stated guarantee terms — not a payment obligation.
6281
+ proposedGuaranteeCents Int?
6282
+ proposedGuaranteeNotes String? @db.Text
6283
+ /// Host recruiting incentive offered at request-creation time — see VendorCategoryExclusivity.
6284
+ /// Requires bashEventId (exclusivity is scoped to one event) and a Pro+ membership on the host.
6285
+ exclusiveCategoryOffered Boolean? @default(false)
6286
+ /// Vended product/service categories (VendedProductType/VendorServiceType values) the host
6287
+ /// chose to offer exclusivity for — not necessarily every category the vendor lists.
6288
+ exclusiveCategories String[] @default([])
6207
6289
  serviceMessages ServiceBookingMessage[] @relation("ServiceBookingMessages")
6208
6290
  messages VendorBookingMessage[]
6209
6291
  bashEvent BashEvent? @relation("VendorBookingEvent", fields: [bashEventId], references: [id])
6210
6292
  hostUser User @relation("VendorBookingHost", fields: [hostUserId], references: [id], onDelete: Cascade)
6211
6293
  vendorService Service @relation("VendorBookingService", fields: [vendorServiceId], references: [id], onDelete: Cascade)
6212
6294
  offers ServiceBookingOffer[]
6295
+ categoryExclusivities VendorCategoryExclusivity[]
6213
6296
 
6214
6297
  @@index([hostUserId])
6215
6298
  @@index([vendorServiceId])
@@ -6217,6 +6300,25 @@ model VendorBookingRequest {
6217
6300
  @@index([createdAt])
6218
6301
  }
6219
6302
 
6303
+ /// Records a category lock granted when a host's VendorBookingRequest with
6304
+ /// exclusiveCategoryOffered=true is accepted. One row per (event, category) the
6305
+ /// host chose — a multi-category vendor does not lock every category it lists,
6306
+ /// only the ones the host explicitly offered exclusivity for. Released (deleted)
6307
+ /// if the underlying accepted request is later cancelled.
6308
+ model VendorCategoryExclusivity {
6309
+ id String @id @default(cuid())
6310
+ bashEventId String
6311
+ /// A VendedProductType or VendorServiceType value.
6312
+ category String
6313
+ vendorBookingRequestId String
6314
+ createdAt DateTime @default(now())
6315
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
6316
+ vendorBookingRequest VendorBookingRequest @relation(fields: [vendorBookingRequestId], references: [id], onDelete: Cascade)
6317
+
6318
+ @@unique([vendorBookingRequestId, category])
6319
+ @@index([bashEventId, category])
6320
+ }
6321
+
6220
6322
  model VendorBookingMessage {
6221
6323
  id String @id @default(cuid())
6222
6324
  createdAt DateTime @default(now())
@@ -6775,6 +6877,9 @@ enum NotificationType {
6775
6877
  CompetitionParticipantRequested // Someone requested to join your competition
6776
6878
  CompetitionParticipantApproved // Host approved your request to compete
6777
6879
  CompetitionParticipantRejected // Host rejected your request
6880
+ CompetitionPrizeOfferReceived // Host: someone offered a prize contribution
6881
+ CompetitionPrizeOfferAccepted // Offeror: host accepted your prize offer
6882
+ CompetitionPrizeOfferDeclined // Offeror: host declined your prize offer
6778
6883
  BashRating
6779
6884
  BashRatingMilestone
6780
6885
  BashFeedPost
@@ -6814,6 +6919,8 @@ enum NotificationType {
6814
6919
  FeaturedGuestInviteAccepted // Host: guest accepted featured guest invite
6815
6920
  FeaturedGuestInviteDeclined // Host: guest declined featured guest invite
6816
6921
  PartnerPromotionBlast // Attendee: partner on-site promo deal at an event
6922
+ SideQuestRevealed // Attendee: host revealed a secret Side Quest
6923
+ CommunityQuestUnlocked // Attendee: community quest goal unlocked
6817
6924
  }
6818
6925
 
6819
6926
  /// Opt-in reminder for a scheduled tier release (v1.5).
@@ -9254,6 +9361,10 @@ enum CreditSourceType {
9254
9361
  SideQuestCompletion // Completing a Side Quest on a bash
9255
9362
  SideQuestMilestone // Hitting a Side Quest milestone (e.g. complete N quests)
9256
9363
  CommunityQuestUnlock // Digital reward when a Community Quest goal unlocks
9364
+ CompetitionVote // Completing a Vote-type Side Quest by casting a competition vote
9365
+ CompetitionParticipation // Adventures reward for becoming a competition participant
9366
+ CompetitionFinalist // Adventures reward for reaching a finalist placement
9367
+ CompetitionWin // Adventures reward for winning a competition prize
9257
9368
  }
9258
9369
 
9259
9370
  enum ReferralTier {
@@ -0,0 +1,43 @@
1
+ import {
2
+ assertAllowedJudgingTypeForNew,
3
+ DEFAULT_COMPETITION_JUDGING_TYPE,
4
+ DISALLOWED_JUDGING_TYPE_MESSAGE,
5
+ getJudgingTypeDescription,
6
+ getJudgingTypeLabel,
7
+ isDisallowedJudgingTypeForNew,
8
+ } from "../competitionValidation";
9
+ import { JudgingType } from "../definitions";
10
+
11
+ describe("competitionValidation", () => {
12
+ it("defaults new competitions to HOST_CHOOSES", () => {
13
+ expect(DEFAULT_COMPETITION_JUDGING_TYPE).toBe(JudgingType.HOST_CHOOSES);
14
+ });
15
+
16
+ it("disallows METRICS and FIRST_COME for new competitions", () => {
17
+ expect(isDisallowedJudgingTypeForNew(JudgingType.METRICS)).toBe(true);
18
+ expect(isDisallowedJudgingTypeForNew(JudgingType.FIRST_COME)).toBe(true);
19
+ expect(isDisallowedJudgingTypeForNew(JudgingType.RANDOM_DRAW)).toBe(true);
20
+ expect(isDisallowedJudgingTypeForNew(JudgingType.HIGHEST_BID)).toBe(true);
21
+ expect(isDisallowedJudgingTypeForNew(JudgingType.HOST_CHOOSES)).toBe(false);
22
+ });
23
+
24
+ it("throws with the updated disallowed judging message", () => {
25
+ expect(() =>
26
+ assertAllowedJudgingTypeForNew(JudgingType.FIRST_COME)
27
+ ).toThrow(DISALLOWED_JUDGING_TYPE_MESSAGE);
28
+ expect(DISALLOWED_JUDGING_TYPE_MESSAGE).toMatch(/metrics/i);
29
+ });
30
+
31
+ it("getJudgingTypeLabel returns TBD for null", () => {
32
+ expect(getJudgingTypeLabel(null)).toBe("TBD");
33
+ expect(getJudgingTypeLabel(undefined)).toBe("TBD");
34
+ });
35
+
36
+ it("getJudgingTypeDescription covers AUDIENCE_VOTE and legacy types", () => {
37
+ expect(getJudgingTypeDescription(JudgingType.AUDIENCE_VOTE).length).toBeGreaterThan(
38
+ 0
39
+ );
40
+ expect(getJudgingTypeDescription(JudgingType.METRICS).length).toBeGreaterThan(0);
41
+ expect(getJudgingTypeLabel(JudgingType.FIRST_COME)).toMatch(/first come/i);
42
+ });
43
+ });
@@ -10,6 +10,8 @@ import {
10
10
  isUnpaidPayAtDoorTicket,
11
11
  TICKET_PURCHASE_TYPE_COMP,
12
12
  TICKET_PURCHASE_TYPE_PAY_AT_DOOR,
13
+ COMPLEMENTARY_SERVICE_TYPE,
14
+ ServiceTypes,
13
15
  } from "../definitions.js";
14
16
 
15
17
  describe("definitions helpers", () => {
@@ -133,4 +135,18 @@ describe("definitions helpers", () => {
133
135
  ).toBe(false);
134
136
  });
135
137
  });
138
+
139
+ describe("COMPLEMENTARY_SERVICE_TYPE", () => {
140
+ test("pairs Vendors and EventServices both ways", () => {
141
+ expect(COMPLEMENTARY_SERVICE_TYPE[ServiceTypes.Vendors]).toBe(
142
+ ServiceTypes.EventServices
143
+ );
144
+ expect(COMPLEMENTARY_SERVICE_TYPE[ServiceTypes.EventServices]).toBe(
145
+ ServiceTypes.Vendors
146
+ );
147
+ expect(
148
+ COMPLEMENTARY_SERVICE_TYPE[ServiceTypes.EntertainmentServices]
149
+ ).toBeUndefined();
150
+ });
151
+ });
136
152
  });
@@ -65,6 +65,24 @@ describe("membershipDefinitions helpers", () => {
65
65
  );
66
66
  });
67
67
 
68
+ test("Pro unlocks vendor category exclusivity", () => {
69
+ expect(
70
+ hasFeatureAccess(
71
+ MembershipTier.Pro,
72
+ MEMBERSHIP_FEATURES.VENDOR_CATEGORY_EXCLUSIVITY
73
+ )
74
+ ).toBe(true);
75
+ expect(
76
+ hasFeatureAccess(
77
+ MembershipTier.Premium,
78
+ MEMBERSHIP_FEATURES.VENDOR_CATEGORY_EXCLUSIVITY
79
+ )
80
+ ).toBe(false);
81
+ expect(
82
+ getRequiredTierForFeature(MEMBERSHIP_FEATURES.VENDOR_CATEGORY_EXCLUSIVITY)
83
+ ).toBe(MembershipTier.Pro);
84
+ });
85
+
68
86
  test("unknown feature returns false", () => {
69
87
  expect(hasFeatureAccess(MembershipTier.Legend, "not_a_feature")).toBe(false);
70
88
  });
@@ -1,4 +1,9 @@
1
- import { filterSideQuestIdeas, SIDE_QUEST_IDEAS } from "../sideQuestIdeas";
1
+ import {
2
+ filterSideQuestIdeas,
3
+ SIDE_QUEST_IDEAS,
4
+ SIDE_QUEST_PHASE1_TYPES,
5
+ SIDE_QUEST_TYPE_META,
6
+ } from "../sideQuestIdeas";
2
7
 
3
8
  describe("sideQuestIdeas", () => {
4
9
  it("includes festival and market templates", () => {
@@ -13,4 +18,9 @@ describe("sideQuestIdeas", () => {
13
18
  expect(hits.length).toBeGreaterThan(0);
14
19
  expect(hits.every((h) => JSON.stringify(h).toLowerCase().includes("transit") || h.category === "Transit")).toBe(true);
15
20
  });
21
+
22
+ it("includes Vote in phase 1 types", () => {
23
+ expect(SIDE_QUEST_PHASE1_TYPES).toContain("Vote");
24
+ expect(SIDE_QUEST_TYPE_META.Vote.phase1).toBe(true);
25
+ });
16
26
  });
@@ -0,0 +1,44 @@
1
+ import {
2
+ buildSideQuestDeepLink,
3
+ buildSideQuestLegacyPayload,
4
+ parseSideQuestScanPayload,
5
+ } from "../sideQuestTypes.js";
6
+
7
+ describe("sideQuestTypes deep links", () => {
8
+ it("builds HTTPS ticket-details URLs", () => {
9
+ expect(
10
+ buildSideQuestDeepLink({
11
+ bashEventId: "evt1",
12
+ questId: "q1",
13
+ code: "ab12",
14
+ frontendOrigin: "https://bash.community",
15
+ })
16
+ ).toBe("https://bash.community/ticket-details/evt1?quest=q1&code=AB12");
17
+ });
18
+
19
+ it("parses HTTPS deep links", () => {
20
+ expect(
21
+ parseSideQuestScanPayload(
22
+ "https://bash.community/ticket-details/evt1?quest=q1&code=XY99"
23
+ )
24
+ ).toEqual({
25
+ bashEventId: "evt1",
26
+ questId: "q1",
27
+ code: "XY99",
28
+ });
29
+ });
30
+
31
+ it("parses legacy bash-side-quest payloads", () => {
32
+ expect(
33
+ parseSideQuestScanPayload(buildSideQuestLegacyPayload("evt1", "q1", "ab12"))
34
+ ).toEqual({
35
+ bashEventId: "evt1",
36
+ questId: "q1",
37
+ code: "AB12",
38
+ });
39
+ });
40
+
41
+ it("parses plain codes", () => {
42
+ expect(parseSideQuestScanPayload("ab12")).toEqual({ code: "AB12" });
43
+ });
44
+ });
@@ -139,6 +139,13 @@ describe("status enum value sets", () => {
139
139
  "ReferralCodeUsed",
140
140
  "VenueLoyaltyRedemption",
141
141
  "PartnerPromoRedemption",
142
+ "SideQuestCompletion",
143
+ "SideQuestMilestone",
144
+ "CommunityQuestUnlock",
145
+ "CompetitionVote",
146
+ "CompetitionParticipation",
147
+ "CompetitionFinalist",
148
+ "CompetitionWin",
142
149
  ].sort(),
143
150
  );
144
151
  });
@@ -17,14 +17,20 @@ export function parseJudgingType(
17
17
  return value as JudgingType;
18
18
  }
19
19
 
20
- /** Judging modes disallowed for new competitions (lottery / auction-adjacent). */
20
+ /**
21
+ * Judging modes disallowed for new competitions: lottery / auction-adjacent
22
+ * (RANDOM_DRAW, HIGHEST_BID), plus METRICS and FIRST_COME which have no
23
+ * winner-resolution implementation yet (dead ends for hosts).
24
+ */
21
25
  export const DISALLOWED_JUDGING_TYPES_FOR_NEW: ReadonlySet<JudgingType> = new Set([
22
26
  JudgingType.RANDOM_DRAW,
23
27
  JudgingType.HIGHEST_BID,
28
+ JudgingType.METRICS,
29
+ JudgingType.FIRST_COME,
24
30
  ]);
25
31
 
26
32
  export const DISALLOWED_JUDGING_TYPE_MESSAGE =
27
- "Random draw and highest-bid judging are not supported on Bash. Use host choice, skill judging, audience vote, metrics, first come, or tournament bracket.";
33
+ "Random draw, highest-bid, metrics, and first-come judging are not supported on Bash yet. Use host choice, skill judging, audience vote, or tournament bracket.";
28
34
 
29
35
  export function isDisallowedJudgingTypeForNew(
30
36
  judgingType: JudgingType | null | undefined
@@ -45,6 +51,54 @@ export function assertAllowedJudgingTypeForNew(
45
51
  export const DEFAULT_COMPETITION_JUDGING_TYPE: JudgingType =
46
52
  JudgingType.HOST_CHOOSES;
47
53
 
54
+ /** Single source of truth for judging-type copy — shared by the host wizard and guest-facing display. */
55
+ export const JUDGING_TYPE_LABELS: Record<JudgingType, string> = {
56
+ [JudgingType.RANDOM_DRAW]: "Drawing (legacy)",
57
+ [JudgingType.HIGHEST_BID]: "Highest Bid (legacy)",
58
+ [JudgingType.HOST_CHOOSES]: "Host Selection",
59
+ [JudgingType.JUDGES]: "Panel of Judges",
60
+ [JudgingType.AUDIENCE_VOTE]: "Audience Vote",
61
+ [JudgingType.METRICS]: "Performance Metrics",
62
+ [JudgingType.FIRST_COME]: "First Come First Serve",
63
+ [JudgingType.BRACKET_TOURNAMENT]: "Bracket Tournament",
64
+ };
65
+
66
+ export const JUDGING_TYPE_DESCRIPTIONS: Record<JudgingType, string> = {
67
+ [JudgingType.RANDOM_DRAW]:
68
+ "This competition uses a legacy random drawing configuration. New competitions on Bash use host choice, skill judging, audience vote, or tournament formats.",
69
+ [JudgingType.HIGHEST_BID]:
70
+ "Legacy auction-style configuration. New competitions on Bash do not support highest-bid judging.",
71
+ [JudgingType.HOST_CHOOSES]:
72
+ "The event host will personally select the winner(s) based on their criteria.",
73
+ [JudgingType.JUDGES]:
74
+ "A panel of judges will evaluate participants based on specific criteria and select the winner(s).",
75
+ [JudgingType.AUDIENCE_VOTE]:
76
+ "Attendees will vote to determine the winner(s). The participant(s) with the most votes win!",
77
+ [JudgingType.METRICS]:
78
+ "Winners are determined by measurable performance metrics and objective criteria.",
79
+ [JudgingType.FIRST_COME]:
80
+ "Winners are determined by order of participation - first to sign up or complete wins!",
81
+ [JudgingType.BRACKET_TOURNAMENT]:
82
+ "Participants compete head-to-head in a tournament bracket. Winners advance through rounds until a champion is determined.",
83
+ };
84
+
85
+ export function getJudgingTypeLabel(
86
+ judgingType: JudgingType | null | undefined
87
+ ): string {
88
+ if (!judgingType) return "TBD";
89
+ return JUDGING_TYPE_LABELS[judgingType] ?? "TBD";
90
+ }
91
+
92
+ export function getJudgingTypeDescription(
93
+ judgingType: JudgingType | null | undefined
94
+ ): string {
95
+ if (!judgingType) return "The judging method has not been determined yet.";
96
+ return (
97
+ JUDGING_TYPE_DESCRIPTIONS[judgingType] ??
98
+ "The judging method has not been determined yet."
99
+ );
100
+ }
101
+
48
102
  export function normalizeJudgingTypeForSave(
49
103
  judgingType: JudgingType | null | undefined,
50
104
  isExistingLegacy: boolean
@@ -44,6 +44,19 @@ import type { UtmFields } from "./utmAttribution.js";
44
44
  * values — importing them from `@prisma/client` yields `undefined` and crashes on first
45
45
  * property access. These mirrors match the Prisma schema / full client enum strings.
46
46
  */
47
+ /**
48
+ * "Also list as [complementary type]" — a one-time identity-field prefill copy
49
+ * (see Service.complementaryOfServiceId), not an ongoing synced link. Scoped to
50
+ * the Vendors <-> EventServices pairing only (e.g. a food truck that vends
51
+ * publicly at festivals and is also paid a flat fee at private weddings).
52
+ */
53
+ export const COMPLEMENTARY_SERVICE_TYPE: Partial<
54
+ Record<ServiceTypes, ServiceTypes>
55
+ > = {
56
+ [ServiceTypes.Vendors]: ServiceTypes.EventServices,
57
+ [ServiceTypes.EventServices]: ServiceTypes.Vendors,
58
+ };
59
+
47
60
  export const ServiceBookingOfferDirection = {
48
61
  HostToProvider: "HostToProvider",
49
62
  ProviderToHost: "ProviderToHost",
@@ -270,14 +270,17 @@ export const PRIVATE_USER_ACCOUNT_TO_SELECT = {
270
270
 
271
271
  export interface SponsoredEventExt extends SponsoredEvent {}
272
272
 
273
+ /** Minimal service identity used wherever a competition prize/offer just needs to point at a listing. */
274
+ export interface ServiceRefLite {
275
+ id: string;
276
+ serviceName: string | null;
277
+ coverPhoto: string | null;
278
+ }
279
+
273
280
  export interface PrizeExt extends Prize {
274
281
  winner?: PublicUser | null;
275
282
  contributor?: PublicUser | null;
276
- sourceService?: {
277
- id: string;
278
- serviceName: string | null;
279
- coverPhoto: string | null;
280
- } | null;
283
+ sourceService?: ServiceRefLite | null;
281
284
  }
282
285
 
283
286
  export interface CompetitionPrizeOfferExt {
@@ -296,11 +299,7 @@ export interface CompetitionPrizeOfferExt {
296
299
  createdAt: Date | string;
297
300
  respondedAt?: Date | string | null;
298
301
  offeredBy?: PublicUser;
299
- sourceService?: {
300
- id: string;
301
- serviceName: string | null;
302
- coverPhoto: string | null;
303
- } | null;
302
+ sourceService?: ServiceRefLite | null;
304
303
  targetPrize?: {
305
304
  id: string;
306
305
  name: string;
@@ -309,6 +308,11 @@ export interface CompetitionPrizeOfferExt {
309
308
  } | null;
310
309
  }
311
310
 
311
+ /** One competition participant row + resolved user; shared by CompetitionExt.participants and the host roster endpoint. */
312
+ export interface CompetitionParticipantExt extends CompetitionParticipant {
313
+ user?: Pick<PublicUser, "uploadedImage" | "image"> | null;
314
+ }
315
+
312
316
  /** Competition row + relations; Override keeps new columns visible before consumers refresh Prisma. */
313
317
  export type CompetitionExt = Override<
314
318
  Competition,
@@ -320,7 +324,7 @@ export type CompetitionExt = Override<
320
324
  prizes: PrizeExt[];
321
325
  }
322
326
  > & {
323
- participants?: CompetitionParticipant[];
327
+ participants?: CompetitionParticipantExt[];
324
328
  votes?: CompetitionVote[];
325
329
  entryTicketTier?: TicketTier | null;
326
330
  sponsor?: CompetitionSponsor[];
@@ -848,6 +852,11 @@ export const SERVICE_DATA_TO_INCLUDE = {
848
852
  sportsProfile: true,
849
853
  }
850
854
  }, // Include organization data with member counts
855
+ // Lightweight — just enough for the UI to know a complementary Vendor <-> EventServices
856
+ // profile already exists (see Part B "also list as" flow) without a second fetch.
857
+ complementaryProfiles: {
858
+ select: { id: true, serviceType: true },
859
+ },
851
860
  // bookings: {
852
861
  // include: SERVICE_BOOKING_PRIVATE_DATA_TO_INCLUDE, //make sure only to include owned bookedDays
853
862
  // },
@@ -1101,6 +1110,9 @@ export interface ServiceExt extends Service {
1101
1110
  associatedBashesReferencingMe?: AssociatedBashExt[];
1102
1111
  associatedServicesReferencingMe?: AssociatedServiceExt[];
1103
1112
 
1113
+ /** Reverse side of `complementaryOfServiceId` — the "also list as" profile(s) this service already spawned. */
1114
+ complementaryProfiles?: Pick<Service, "id" | "serviceType">[];
1115
+
1104
1116
  // googleReviews: GoogleReview[];
1105
1117
 
1106
1118
  // For availability filtering - matches Prisma relation name "bookings"
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from "./stripeListingSubscriptionMessages.js";
8
8
  export * from "./extendedSchemas.js";
9
9
  export * from "./competitionIdeas.js";
10
10
  export * from "./sideQuestIdeas.js";
11
+ export * from "./sideQuestTypes.js";
11
12
  export * from "./icebreakerPrompts.js";
12
13
  export * from "./competitionValidation.js";
13
14
  export * from "./groupTicketUtils.js";
@@ -813,6 +813,8 @@ export const MEMBERSHIP_FEATURES = {
813
813
  SEARCH_PRIORITY: 'search_priority',
814
814
  BASIC_ANALYTICS: 'basic_analytics',
815
815
  VENDOR_DIRECTORY: 'vendor_directory',
816
+ /** Host-side perk: offer a vendor exclusive-category status for an event when sending a booking request. */
817
+ VENDOR_CATEGORY_EXCLUSIVITY: 'vendor_category_exclusivity',
816
818
  TIER_BADGE: 'tier_badge',
817
819
 
818
820
  // Elite features
@@ -854,6 +856,7 @@ export function hasFeatureAccess(userTier: MembershipTier, feature: string): boo
854
856
  case MEMBERSHIP_FEATURES.SEARCH_PRIORITY:
855
857
  case MEMBERSHIP_FEATURES.BASIC_ANALYTICS:
856
858
  case MEMBERSHIP_FEATURES.VENDOR_DIRECTORY:
859
+ case MEMBERSHIP_FEATURES.VENDOR_CATEGORY_EXCLUSIVITY:
857
860
  case MEMBERSHIP_FEATURES.TIER_BADGE:
858
861
  return tierIndex >= 2; // Pro+
859
862
 
@@ -180,6 +180,10 @@ export const CreditSourceType = {
180
180
  SideQuestCompletion: "SideQuestCompletion",
181
181
  SideQuestMilestone: "SideQuestMilestone",
182
182
  CommunityQuestUnlock: "CommunityQuestUnlock",
183
+ CompetitionVote: "CompetitionVote",
184
+ CompetitionParticipation: "CompetitionParticipation",
185
+ CompetitionFinalist: "CompetitionFinalist",
186
+ CompetitionWin: "CompetitionWin",
183
187
  } as const satisfies Record<CreditSourceTypeEnum, CreditSourceTypeEnum>;
184
188
 
185
189
  export type CreditSourceType = CreditSourceTypeEnum;