@bash-app/bash-common 30.309.0 → 30.314.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 (70) 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__/statusEnums.test.js +7 -0
  12. package/dist/__tests__/statusEnums.test.js.map +1 -1
  13. package/dist/competitionValidation.d.ts +11 -2
  14. package/dist/competitionValidation.d.ts.map +1 -1
  15. package/dist/competitionValidation.js +40 -2
  16. package/dist/competitionValidation.js.map +1 -1
  17. package/dist/definitions.d.ts +18 -1
  18. package/dist/definitions.d.ts.map +1 -1
  19. package/dist/definitions.js +28 -1
  20. package/dist/definitions.js.map +1 -1
  21. package/dist/extendedSchemas.d.ts +259 -46
  22. package/dist/extendedSchemas.d.ts.map +1 -1
  23. package/dist/extendedSchemas.js +76 -22
  24. package/dist/extendedSchemas.js.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/membershipDefinitions.d.ts +2 -0
  30. package/dist/membershipDefinitions.d.ts.map +1 -1
  31. package/dist/membershipDefinitions.js +3 -0
  32. package/dist/membershipDefinitions.js.map +1 -1
  33. package/dist/mirroredPrismaEnums.d.ts +10 -0
  34. package/dist/mirroredPrismaEnums.d.ts.map +1 -1
  35. package/dist/mirroredPrismaEnums.js +16 -0
  36. package/dist/mirroredPrismaEnums.js.map +1 -1
  37. package/dist/serviceTypeLabels.d.ts +15 -0
  38. package/dist/serviceTypeLabels.d.ts.map +1 -0
  39. package/dist/serviceTypeLabels.js +24 -0
  40. package/dist/serviceTypeLabels.js.map +1 -0
  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 +1 -0
  45. package/dist/sideQuestTypes.d.ts.map +1 -1
  46. package/dist/sideQuestTypes.js.map +1 -1
  47. package/dist/targetAudienceLabels.d.ts +12 -0
  48. package/dist/targetAudienceLabels.d.ts.map +1 -0
  49. package/dist/targetAudienceLabels.js +50 -0
  50. package/dist/targetAudienceLabels.js.map +1 -0
  51. package/dist/utils/service/serviceBookingTypes.d.ts +1 -1
  52. package/dist/utils/service/serviceBookingTypes.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/prisma/schema.prisma +114 -1
  55. package/src/__tests__/competitionValidation.test.ts +43 -0
  56. package/src/__tests__/definitionsHelpers.test.ts +16 -0
  57. package/src/__tests__/membershipDefinitions.test.ts +18 -0
  58. package/src/__tests__/sideQuestIdeas.test.ts +11 -1
  59. package/src/__tests__/statusEnums.test.ts +7 -0
  60. package/src/competitionValidation.ts +56 -2
  61. package/src/definitions.ts +40 -1
  62. package/src/extendedSchemas.ts +216 -63
  63. package/src/index.ts +2 -0
  64. package/src/membershipDefinitions.ts +3 -0
  65. package/src/mirroredPrismaEnums.ts +20 -0
  66. package/src/serviceTypeLabels.ts +24 -0
  67. package/src/sideQuestIdeas.ts +2 -2
  68. package/src/sideQuestTypes.ts +1 -0
  69. package/src/targetAudienceLabels.ts +54 -0
  70. package/src/utils/service/serviceBookingTypes.ts +1 -1
@@ -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])
@@ -4740,6 +4813,9 @@ model VendorSocialMediaExpectation {
4740
4813
  @@index([preferencesId])
4741
4814
  }
4742
4815
 
4816
+ /// @deprecated Not written or read by any live API route — vendor-initiated offers now flow through
4817
+ /// the unified ServiceBookingOffer model (see ServiceBooking.isProviderInitiatedOffer). Kept only so
4818
+ /// existing rows aren't dropped; do not add new call sites. Slated for removal in a future migration.
4743
4819
  model VendorBid {
4744
4820
  id String @id @default(cuid())
4745
4821
  vendorId String
@@ -6034,7 +6110,8 @@ model ServiceBooking {
6034
6110
  totalATBCents Int
6035
6111
  subtotalATBCents Int @default(0)
6036
6112
  bashEventId String?
6037
- isVendorBid Boolean @default(false)
6113
+ // Renamed from isVendorBid: set whenever a ProviderToHost offer (vendor, exhibitor, OR sponsor) is accepted — not vendor-specific.
6114
+ isProviderInitiatedOffer Boolean @default(false) @map("isVendorBid")
6038
6115
  vendorBidAmountCents Int?
6039
6116
  vendorEventDetails String?
6040
6117
  bashPointsApplied Int? // BashPoints credits applied to this booking
@@ -6204,12 +6281,22 @@ model VendorBookingRequest {
6204
6281
  paidAt DateTime?
6205
6282
  vendorResponse String?
6206
6283
  respondedAt DateTime?
6284
+ /// Informational counter-offer to the vendor's stated guarantee terms — not a payment obligation.
6285
+ proposedGuaranteeCents Int?
6286
+ proposedGuaranteeNotes String? @db.Text
6287
+ /// Host recruiting incentive offered at request-creation time — see VendorCategoryExclusivity.
6288
+ /// Requires bashEventId (exclusivity is scoped to one event) and a Pro+ membership on the host.
6289
+ exclusiveCategoryOffered Boolean? @default(false)
6290
+ /// Vended product/service categories (VendedProductType/VendorServiceType values) the host
6291
+ /// chose to offer exclusivity for — not necessarily every category the vendor lists.
6292
+ exclusiveCategories String[] @default([])
6207
6293
  serviceMessages ServiceBookingMessage[] @relation("ServiceBookingMessages")
6208
6294
  messages VendorBookingMessage[]
6209
6295
  bashEvent BashEvent? @relation("VendorBookingEvent", fields: [bashEventId], references: [id])
6210
6296
  hostUser User @relation("VendorBookingHost", fields: [hostUserId], references: [id], onDelete: Cascade)
6211
6297
  vendorService Service @relation("VendorBookingService", fields: [vendorServiceId], references: [id], onDelete: Cascade)
6212
6298
  offers ServiceBookingOffer[]
6299
+ categoryExclusivities VendorCategoryExclusivity[]
6213
6300
 
6214
6301
  @@index([hostUserId])
6215
6302
  @@index([vendorServiceId])
@@ -6217,6 +6304,25 @@ model VendorBookingRequest {
6217
6304
  @@index([createdAt])
6218
6305
  }
6219
6306
 
6307
+ /// Records a category lock granted when a host's VendorBookingRequest with
6308
+ /// exclusiveCategoryOffered=true is accepted. One row per (event, category) the
6309
+ /// host chose — a multi-category vendor does not lock every category it lists,
6310
+ /// only the ones the host explicitly offered exclusivity for. Released (deleted)
6311
+ /// if the underlying accepted request is later cancelled.
6312
+ model VendorCategoryExclusivity {
6313
+ id String @id @default(cuid())
6314
+ bashEventId String
6315
+ /// A VendedProductType or VendorServiceType value.
6316
+ category String
6317
+ vendorBookingRequestId String
6318
+ createdAt DateTime @default(now())
6319
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
6320
+ vendorBookingRequest VendorBookingRequest @relation(fields: [vendorBookingRequestId], references: [id], onDelete: Cascade)
6321
+
6322
+ @@unique([vendorBookingRequestId, category])
6323
+ @@index([bashEventId, category])
6324
+ }
6325
+
6220
6326
  model VendorBookingMessage {
6221
6327
  id String @id @default(cuid())
6222
6328
  createdAt DateTime @default(now())
@@ -6775,6 +6881,9 @@ enum NotificationType {
6775
6881
  CompetitionParticipantRequested // Someone requested to join your competition
6776
6882
  CompetitionParticipantApproved // Host approved your request to compete
6777
6883
  CompetitionParticipantRejected // Host rejected your request
6884
+ CompetitionPrizeOfferReceived // Host: someone offered a prize contribution
6885
+ CompetitionPrizeOfferAccepted // Offeror: host accepted your prize offer
6886
+ CompetitionPrizeOfferDeclined // Offeror: host declined your prize offer
6778
6887
  BashRating
6779
6888
  BashRatingMilestone
6780
6889
  BashFeedPost
@@ -9256,6 +9365,10 @@ enum CreditSourceType {
9256
9365
  SideQuestCompletion // Completing a Side Quest on a bash
9257
9366
  SideQuestMilestone // Hitting a Side Quest milestone (e.g. complete N quests)
9258
9367
  CommunityQuestUnlock // Digital reward when a Community Quest goal unlocks
9368
+ CompetitionVote // Completing a Vote-type Side Quest by casting a competition vote
9369
+ CompetitionParticipation // Adventures reward for becoming a competition participant
9370
+ CompetitionFinalist // Adventures reward for reaching a finalist placement
9371
+ CompetitionWin // Adventures reward for winning a competition prize
9259
9372
  }
9260
9373
 
9261
9374
  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
  });
@@ -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,35 @@ 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
+
60
+ /**
61
+ * Human-readable label for a ServiceTypes value — the raw enum ("EventServices")
62
+ * is not fit for display copy. Only covers the values COMPLEMENTARY_SERVICE_TYPE
63
+ * can produce; extend as more callers need display labels for other types.
64
+ */
65
+ export const SERVICE_TYPE_DISPLAY_LABEL: Partial<Record<ServiceTypes, string>> =
66
+ {
67
+ [ServiceTypes.Vendors]: "Vendor",
68
+ [ServiceTypes.EventServices]: "Event Service",
69
+ };
70
+
71
+ /** Picks "a"/"an" for the given word, e.g. for "a Vendor" vs "an Event Service". */
72
+ export function articleFor(word: string): "a" | "an" {
73
+ return /^[aeiou]/i.test(word) ? "an" : "a";
74
+ }
75
+
47
76
  export const ServiceBookingOfferDirection = {
48
77
  HostToProvider: "HostToProvider",
49
78
  ProviderToHost: "ProviderToHost",
@@ -1161,7 +1190,17 @@ export const YearsOfExperienceToString: { [key in YearsOfExperienceEnum]: string
1161
1190
  [YearsOfExperience.FivePlusYears]: "More than five years",
1162
1191
  };
1163
1192
 
1164
- export { BracketType } from "./mirroredPrismaEnums.js";
1193
+ export { BracketType, ServiceBookingSettlementStatusLabel } from "./mirroredPrismaEnums.js";
1194
+
1195
+ // Re-exported for backward compatibility during migration — see serviceTypeLabels.ts/targetAudienceLabels.ts
1196
+ // for the canonical, consolidated source (following the BracketType precedent above).
1197
+ export { SERVICE_TYPE_LABELS } from "./serviceTypeLabels.js";
1198
+ export {
1199
+ AgeRangeLabel,
1200
+ EducationLabel,
1201
+ GenderLabel,
1202
+ OccupationLabel,
1203
+ } from "./targetAudienceLabels.js";
1165
1204
 
1166
1205
  // Export Prisma enums for use in frontend
1167
1206
  export {