@bash-app/bash-common 30.333.0 → 30.335.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 (53) hide show
  1. package/dist/__tests__/causeTags.test.d.ts +2 -0
  2. package/dist/__tests__/causeTags.test.d.ts.map +1 -0
  3. package/dist/__tests__/causeTags.test.js +94 -0
  4. package/dist/__tests__/causeTags.test.js.map +1 -0
  5. package/dist/__tests__/hostCrmGrowthPack.test.js +12 -2
  6. package/dist/__tests__/hostCrmGrowthPack.test.js.map +1 -1
  7. package/dist/bashFeedTypes.d.ts +4 -0
  8. package/dist/bashFeedTypes.d.ts.map +1 -1
  9. package/dist/causeTags.d.ts +42 -0
  10. package/dist/causeTags.d.ts.map +1 -0
  11. package/dist/causeTags.js +173 -0
  12. package/dist/causeTags.js.map +1 -0
  13. package/dist/definitions.d.ts +23 -0
  14. package/dist/definitions.d.ts.map +1 -1
  15. package/dist/definitions.js +28 -0
  16. package/dist/definitions.js.map +1 -1
  17. package/dist/hostCrmAutomation.d.ts +12 -2
  18. package/dist/hostCrmAutomation.d.ts.map +1 -1
  19. package/dist/hostCrmAutomation.js +16 -2
  20. package/dist/hostCrmAutomation.js.map +1 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/sms/smsTemplates.d.ts +6 -1
  26. package/dist/sms/smsTemplates.d.ts.map +1 -1
  27. package/dist/sms/smsTemplates.js +1 -0
  28. package/dist/sms/smsTemplates.js.map +1 -1
  29. package/dist/utils/__tests__/coatCheckUtils.test.d.ts +2 -0
  30. package/dist/utils/__tests__/coatCheckUtils.test.d.ts.map +1 -0
  31. package/dist/utils/__tests__/coatCheckUtils.test.js +76 -0
  32. package/dist/utils/__tests__/coatCheckUtils.test.js.map +1 -0
  33. package/dist/utils/coatCheckUtils.d.ts +30 -0
  34. package/dist/utils/coatCheckUtils.d.ts.map +1 -0
  35. package/dist/utils/coatCheckUtils.js +85 -0
  36. package/dist/utils/coatCheckUtils.js.map +1 -0
  37. package/dist/utils/slugUtils.d.ts +3 -4
  38. package/dist/utils/slugUtils.d.ts.map +1 -1
  39. package/dist/utils/slugUtils.js +3 -4
  40. package/dist/utils/slugUtils.js.map +1 -1
  41. package/package.json +1 -1
  42. package/prisma/schema.prisma +429 -3
  43. package/src/__tests__/causeTags.test.ts +143 -0
  44. package/src/__tests__/hostCrmGrowthPack.test.ts +14 -1
  45. package/src/bashFeedTypes.ts +4 -0
  46. package/src/causeTags.ts +243 -0
  47. package/src/definitions.ts +46 -0
  48. package/src/hostCrmAutomation.ts +25 -2
  49. package/src/index.ts +2 -0
  50. package/src/sms/smsTemplates.ts +6 -0
  51. package/src/utils/__tests__/coatCheckUtils.test.ts +110 -0
  52. package/src/utils/coatCheckUtils.ts +134 -0
  53. package/src/utils/slugUtils.ts +3 -4
@@ -727,6 +727,8 @@ model BashEvent {
727
727
  createdAt DateTime? @default(now())
728
728
  isApproved Boolean? @default(false)
729
729
  description String?
730
+ /// Curated cause IDs from `CAUSE_TAGS` (bash-common) for discovery/search later.
731
+ causeTags String[] @default([])
730
732
  eventType String @default("Other")
731
733
  timezone String?
732
734
  startDateTime DateTime?
@@ -801,6 +803,14 @@ model BashEvent {
801
803
  allowDonations Boolean? @default(false)
802
804
  suggestedDonationAmount Int?
803
805
  donationDetails String?
806
+ /// Optional public fundraising goal (donations only, cents).
807
+ donationGoalCents Int?
808
+ /// Host-declared share of donations going to a cause (0–100). Transparency only — not automated routing.
809
+ donationCausePercent Int?
810
+ /// Curated CauseTagId when the cause is selected from the picker.
811
+ donationCauseTagId String?
812
+ /// Free-text cause name when not using a curated tag (or display override).
813
+ donationCauseLabel String?
804
814
  absorbDonationFees Boolean @default(true)
805
815
  absorbTicketFees Boolean @default(false)
806
816
  /// Phase 1 of unified-pricing roadmap: governs where PER_BASHPOINTS_ATTENDEE
@@ -829,6 +839,22 @@ model BashEvent {
829
839
  platformFeeRate Float @default(0.03)
830
840
  /// When true, door staff can use pay-at-door QR and terminal flows. When false, door pay CTAs and endpoints return 403.
831
841
  allowDoorQrPayments Boolean @default(true)
842
+ /// Optional coat/bag check door service (not a ticket tier).
843
+ coatCheckEnabled Boolean @default(false)
844
+ /// Unit price in cents when coat check is enabled (0 = complimentary).
845
+ coatCheckPriceCents Int?
846
+ /// Max claim slots for the event; null = unlimited.
847
+ coatCheckSlotCapacity Int?
848
+ /// Max items a guest may buy in one purchase.
849
+ coatCheckMaxItemsPerPurchase Int @default(1)
850
+ /// Guest-facing label (default Coat/bag check).
851
+ coatCheckTitle String @default("Coat/bag check")
852
+ /// Optional reclaim instructions shown on claim QR / My Tickets.
853
+ coatCheckInstructions String?
854
+ /// Extra cents per oversized bag/coat when guest opts in (null = no oversized option).
855
+ coatCheckOversizedSurchargeCents Int?
856
+ /// When true, door/checkout may collect an optional tip for coat check staff.
857
+ coatCheckTipsEnabled Boolean @default(false)
832
858
  /// Host uses a Stripe card reader (Terminal) at the door; door app can run card on reader.
833
859
  hasStripeTerminal Boolean @default(false)
834
860
  /// Host uses Tap to Pay on a supported device (no separate reader). Door flow when enabled.
@@ -876,6 +902,8 @@ model BashEvent {
876
902
  bankTransferEnabled Boolean @default(false)
877
903
  /// Send confirmation SMS for this event when user opted in and env allows.
878
904
  smsConfirmationsEnabled Boolean @default(false)
905
+ /// Host opt-in: auto-remind pending guest invitees at T-24h and T-7h before start.
906
+ guestInviteAutoRemindEnabled Boolean @default(true)
879
907
  /// Collect named holder per seat at checkout.
880
908
  namedTicketsEnabled Boolean @default(false)
881
909
  /// Max companions a guest may bring (0 = alone, 1 = +1, 2 = +2, null = unlimited).
@@ -1116,6 +1144,8 @@ model BashEvent {
1116
1144
  guestSessionRecoveries GuestSessionRecovery[]
1117
1145
  /// Paid host-event merch purchases (Stripe checkout; inventory still tracked in merchandiseItems JSON)
1118
1146
  merchOrders BashEventMerchOrder[]
1147
+ coatCheckEntitlements CoatCheckEntitlement[]
1148
+ experienceOpportunities ExperienceOpportunity[]
1119
1149
 
1120
1150
  /// Latest AI moderator verdict
1121
1151
  /// Null = never reviewed by AI. Recomputed each time the bash transitions to Pending
@@ -1138,6 +1168,8 @@ model BashEvent {
1138
1168
 
1139
1169
  /// Crew location sharing (host + organizers/co-hosts opt-in near-live pins)
1140
1170
  locationShareSessions LocationShareSession[]
1171
+ /// Private Memories vault captures for this bash
1172
+ memoryCaptures BashMemoryCapture[]
1141
1173
 
1142
1174
  @@index([templateId])
1143
1175
  @@index([parentEventId])
@@ -2497,6 +2529,29 @@ model BashMemoriesTag {
2497
2529
  @@index([taggedUserId])
2498
2530
  }
2499
2531
 
2532
+ /// Private per-user media vault for a bash — photos/videos the attendee
2533
+ /// saved from the event window. Staging only: post publicly to BashFeed or
2534
+ /// contribute privately to the collaborative album from here.
2535
+ model BashMemoryCapture {
2536
+ id String @id @default(cuid())
2537
+ bashEventId String
2538
+ userId String
2539
+ mediaId String
2540
+ mediaUrl String
2541
+ mediaType MediaType @default(Image)
2542
+ source BashMemoryCaptureSource @default(ManualUpload)
2543
+ capturedAt DateTime?
2544
+ createdAt DateTime @default(now())
2545
+
2546
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2547
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
2548
+ media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade)
2549
+
2550
+ @@unique([bashEventId, userId, mediaId])
2551
+ @@index([bashEventId, userId])
2552
+ @@index([userId])
2553
+ }
2554
+
2500
2555
  /// Post-event networking opt-in: attendees who agreed to be discoverable to others who went.
2501
2556
  model EventNetworkingOptIn {
2502
2557
  id String @id @default(cuid())
@@ -2538,6 +2593,11 @@ enum LocationShareStatus {
2538
2593
  Expired
2539
2594
  }
2540
2595
 
2596
+ enum BashMemoryCaptureSource {
2597
+ CameraRoll
2598
+ ManualUpload
2599
+ }
2600
+
2541
2601
  /// Host-scoped CRM row for a past guest (VIP tags, notes)
2542
2602
  model HostAttendeeProfile {
2543
2603
  id String @id @default(cuid())
@@ -3227,6 +3287,7 @@ model Ticket {
3227
3287
  appliedDiscounts AppliedDiscount[] // NEW - Discounts applied to this ticket
3228
3288
  eventReferral EventReferral? // When this ticket was purchased via a referral link
3229
3289
  flyerConversions FlyerConversion[]
3290
+ coatCheckEntitlements CoatCheckEntitlement[]
3230
3291
 
3231
3292
  @@index([bashEventId])
3232
3293
  @@index([waitlistUserId])
@@ -3539,6 +3600,7 @@ model Media {
3539
3600
  serviceReferencingMe Service[] @relation("MediaToService")
3540
3601
  sponsoredEventsReferencingMe SponsoredEvent[] @relation("MediaToSponsoredEvent")
3541
3602
  organizationsNonprofitDeterminationDoc Organization[] @relation("OrganizationNonprofitDeterminationDoc")
3603
+ memoryCaptures BashMemoryCapture[]
3542
3604
  }
3543
3605
 
3544
3606
  model Prize {
@@ -4000,6 +4062,7 @@ model User {
4000
4062
  associatedBashes AssociatedBash[]
4001
4063
  associatedServices AssociatedService[]
4002
4064
  locationShareSessions LocationShareSession[]
4065
+ memoryCaptures BashMemoryCapture[]
4003
4066
  comment BashComment[]
4004
4067
  referredTransactions BashCreditTransaction[] @relation("ReferredUser")
4005
4068
  creditTransactions BashCreditTransaction[]
@@ -4201,6 +4264,10 @@ model User {
4201
4264
  reviewedFeedReports BashFeedReport[] @relation("ReviewedReports")
4202
4265
  storeOrders StoreOrder[]
4203
4266
  bashEventMerchOrders BashEventMerchOrder[]
4267
+ coatCheckEntitlementsOwned CoatCheckEntitlement[] @relation("CoatCheckOwner")
4268
+ coatCheckEntitlementsCheckedIn CoatCheckEntitlement[] @relation("CoatCheckCheckedInBy")
4269
+ coatCheckEntitlementsClaimed CoatCheckEntitlement[] @relation("CoatCheckClaimedBy")
4270
+ bashBusWaitlistSignup BashBusWaitlistSignup?
4204
4271
  collectibleRedemptions CollectibleRedemption[]
4205
4272
  bashFeedSaves BashFeedSave[]
4206
4273
  bashFeedReposts BashFeedRepost[]
@@ -4239,6 +4306,12 @@ model User {
4239
4306
  // Availability
4240
4307
  bashAvailability BashAvailability? @relation("UserAvailability")
4241
4308
 
4309
+ // Activity Intent / Experience Discovery
4310
+ userDiscoveryPreferences UserDiscoveryPreferences?
4311
+ activityIntents ActivityIntent[]
4312
+ experienceInvitations ExperienceInvitation[]
4313
+ experienceOpportunitiesCreated ExperienceOpportunity[] @relation("ExperienceOpportunityCreatedBy")
4314
+
4242
4315
  // Groups
4243
4316
  ownedGroups EventGroup[] @relation("OwnedGroups")
4244
4317
  groupMembers GroupMember[] @relation("GroupMemberships")
@@ -4278,9 +4351,7 @@ model UserPreferences {
4278
4351
  newFollowerNotifications Boolean @default(true)
4279
4352
  trendingBashesNotify Boolean @default(true)
4280
4353
  organizerUpdatesNotify Boolean @default(true)
4281
- recurringReminders Boolean @default(true)
4282
4354
  eventSuggestions Boolean @default(true)
4283
- serviceSuggestions Boolean @default(true)
4284
4355
  eventReminderNotify Boolean @default(true)
4285
4356
  newMessageNotify Boolean @default(true)
4286
4357
  invitationNotify Boolean @default(true)
@@ -4353,7 +4424,8 @@ model UserPreferences {
4353
4424
  createdAt DateTime @default(now())
4354
4425
  updatedAt DateTime @updatedAt
4355
4426
  smsNotifications Boolean @default(false)
4356
- friendRequestNotify Boolean @default(true)
4427
+ /// In-app / email when someone sends a connection request (not follow).
4428
+ connectionRequestNotify Boolean @default(true)
4357
4429
  allowTagging Boolean @default(true)
4358
4430
  showActivityStatus Boolean @default(true)
4359
4431
  hiddenBashIds String[] @default([])
@@ -4739,6 +4811,8 @@ model Service {
4739
4811
  backgroundImage String? // Custom or curated background image URL for the service/org profile page
4740
4812
  mission String?
4741
4813
  communityInvolvement String?
4814
+ /// Curated cause IDs from `CAUSE_TAGS` (bash-common) for profile + future search.
4815
+ causeTags String[] @default([])
4742
4816
  emergencyContact String?
4743
4817
  contactDetails String?
4744
4818
  cancellationPolicy ServiceCancellationPolicy? @default(None)
@@ -7635,6 +7709,95 @@ enum BashAvailabilityWindow {
7635
7709
  Custom
7636
7710
  }
7637
7711
 
7712
+ // ─── Activity Intent / Experience Discovery (MVP) ────────────────────────────
7713
+ // Durable “I’m available for an experience” model. Bash Bus is one fulfillment
7714
+ // path; Looking-for windows dual-write into ActivityIntent.
7715
+
7716
+ enum ActivityIntentStatus {
7717
+ ACTIVE
7718
+ MATCHED
7719
+ INVITED
7720
+ ACCEPTED
7721
+ FULFILLED
7722
+ EXPIRED
7723
+ CANCELED
7724
+ }
7725
+
7726
+ enum ActivityIntentSource {
7727
+ USER
7728
+ ORGANIZER
7729
+ CAMPAIGN
7730
+ SYSTEM
7731
+ }
7732
+
7733
+ enum ExperienceCategory {
7734
+ LIVE_MUSIC
7735
+ FOOD
7736
+ NIGHTLIFE
7737
+ DANCING
7738
+ COMEDY
7739
+ SPORTS
7740
+ OUTDOORS
7741
+ GAMES
7742
+ ARTS
7743
+ FAMILY
7744
+ NETWORKING
7745
+ WELLNESS
7746
+ VOLUNTEERING
7747
+ CHILL
7748
+ SURPRISE
7749
+ }
7750
+
7751
+ enum ExperienceOpportunityType {
7752
+ EVENT_RECOMMENDATION
7753
+ POP_UP_BASH
7754
+ BASH_BUS_ADVENTURE
7755
+ SPONSORED_ACTIVATION
7756
+ CAMPUS_ACTIVATION
7757
+ SECRET_SHOW
7758
+ GROUP_MEETUP
7759
+ }
7760
+
7761
+ enum ExperienceOpportunityStatus {
7762
+ DRAFT
7763
+ MEASURING_INTEREST
7764
+ CONFIRMED
7765
+ BOARDING
7766
+ IN_PROGRESS
7767
+ COMPLETED
7768
+ CANCELED
7769
+ }
7770
+
7771
+ enum ExperienceVisibility {
7772
+ PUBLIC
7773
+ UNLISTED
7774
+ INVITE_ONLY
7775
+ }
7776
+
7777
+ enum TransportationMode {
7778
+ NONE
7779
+ WALK
7780
+ USER_TRANSPORT
7781
+ SHUTTLE
7782
+ BASH_BUS
7783
+ THIRD_PARTY_CHARTER
7784
+ }
7785
+
7786
+ enum InvitationStatus {
7787
+ PENDING
7788
+ SENT
7789
+ VIEWED
7790
+ ACCEPTED
7791
+ DECLINED
7792
+ WAITLISTED
7793
+ EXPIRED
7794
+ CANCELED
7795
+ CHECKED_IN
7796
+ BOARDED
7797
+ COMPLETED
7798
+ NO_SHOW
7799
+ }
7800
+
7638
7801
  enum BashStatus {
7639
7802
  Idea // Pre-event idea gauging interest
7640
7803
  Draft
@@ -11464,6 +11627,269 @@ model BashEventMerchOrder {
11464
11627
  @@index([bashEventId, createdAt])
11465
11628
  }
11466
11629
 
11630
+ enum CoatCheckEntitlementStatus {
11631
+ Paid
11632
+ CheckedIn
11633
+ Claimed
11634
+ Refunded
11635
+ Cancelled
11636
+ }
11637
+
11638
+ enum CoatCheckPurchaseChannel {
11639
+ Online
11640
+ Door
11641
+ }
11642
+
11643
+ /// One claimable coat/bag check unit (not admission). Paid → CheckedIn → Claimed.
11644
+ model CoatCheckEntitlement {
11645
+ id String @id @default(cuid())
11646
+ bashEventId String
11647
+ userId String?
11648
+ buyerEmail String?
11649
+ buyerName String?
11650
+ ticketId String?
11651
+ status CoatCheckEntitlementStatus @default(Paid)
11652
+ qrToken String
11653
+ hangerNumber String?
11654
+ itemNotes String?
11655
+ /// Guest marked item as oversized (adds BashEvent.coatCheckOversizedSurchargeCents).
11656
+ isOversized Boolean @default(false)
11657
+ /// Oversized surcharge amount charged for this unit (cents).
11658
+ surchargeCents Int @default(0)
11659
+ /// Optional tip for coat check staff (cents).
11660
+ tipCents Int @default(0)
11661
+ /// Walk-up phone for lost-QR / SMS claim delivery.
11662
+ buyerPhone String?
11663
+ purchaseChannel CoatCheckPurchaseChannel
11664
+ /// Align with Ticket.purchaseType: USD, Cash, Comp, etc.
11665
+ purchaseType String @default("USD")
11666
+ unitPriceCents Int @default(0)
11667
+ paidOn DateTime?
11668
+ checkedInAt DateTime?
11669
+ claimedAt DateTime?
11670
+ checkedInById String?
11671
+ claimedById String?
11672
+ stripeCheckoutSessionId String?
11673
+ stripePaymentIntentId String?
11674
+ squarePaymentId String?
11675
+ squareOrderId String?
11676
+ createdAt DateTime @default(now())
11677
+ updatedAt DateTime @updatedAt
11678
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
11679
+ user User? @relation("CoatCheckOwner", fields: [userId], references: [id], onDelete: SetNull)
11680
+ ticket Ticket? @relation(fields: [ticketId], references: [id], onDelete: SetNull)
11681
+ checkedInBy User? @relation("CoatCheckCheckedInBy", fields: [checkedInById], references: [id], onDelete: SetNull)
11682
+ claimedBy User? @relation("CoatCheckClaimedBy", fields: [claimedById], references: [id], onDelete: SetNull)
11683
+
11684
+ @@index([bashEventId, status])
11685
+ @@index([userId])
11686
+ @@index([ticketId])
11687
+ @@index([buyerEmail])
11688
+ @@index([buyerPhone])
11689
+ @@index([qrToken])
11690
+ @@index([hangerNumber])
11691
+ }
11692
+
11693
+ /// Pre-launch interest list for Bash Bus Adventures (maiden voyage waitlist).
11694
+ /// Prefer UserDiscoveryPreferences.bashBusEnabled for ongoing notify preference;
11695
+ /// this table remains the address/DOB-confirmed lead snapshot for the maiden voyage.
11696
+ model BashBusWaitlistSignup {
11697
+ id String @id @default(cuid())
11698
+ userId String @unique
11699
+ email String
11700
+ phone String?
11701
+ givenName String?
11702
+ familyName String?
11703
+ street String
11704
+ city String
11705
+ state String
11706
+ zipCode String
11707
+ country String @default("US")
11708
+ /// When the guest confirmed their birthday in the join modal.
11709
+ dobConfirmedAt DateTime
11710
+ createdAt DateTime @default(now())
11711
+ updatedAt DateTime @updatedAt
11712
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
11713
+
11714
+ @@index([email])
11715
+ @@index([createdAt])
11716
+ @@index([city, state])
11717
+ }
11718
+
11719
+ /// Persistent discovery prefs (Bash Bus / pop-ups / notify channels).
11720
+ model UserDiscoveryPreferences {
11721
+ id String @id @default(cuid())
11722
+ userId String @unique
11723
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
11724
+
11725
+ popUpExperiencesEnabled Boolean @default(false)
11726
+ bashBusEnabled Boolean @default(false)
11727
+
11728
+ maxTravelMinutes Int?
11729
+ preferredRadiusKm Float?
11730
+
11731
+ allowPushNotifications Boolean @default(true)
11732
+ allowSmsNotifications Boolean @default(false)
11733
+
11734
+ createdAt DateTime @default(now())
11735
+ updatedAt DateTime @updatedAt
11736
+ }
11737
+
11738
+ /// Central “I’m available for an experience during a window” record.
11739
+ model ActivityIntent {
11740
+ id String @id @default(cuid())
11741
+ userId String
11742
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
11743
+
11744
+ status ActivityIntentStatus @default(ACTIVE)
11745
+ source ActivityIntentSource @default(USER)
11746
+
11747
+ /// Mirrors Looking-for preset when created from that UI (optional).
11748
+ window BashAvailabilityWindow?
11749
+
11750
+ startsAt DateTime
11751
+ endsAt DateTime
11752
+ timezone String
11753
+
11754
+ latitude Float?
11755
+ longitude Float?
11756
+
11757
+ /// H3 (or similar) cell for demand aggregation without exposing exact coords long-term.
11758
+ locationCell String?
11759
+ city String?
11760
+ region String?
11761
+ countryCode String?
11762
+
11763
+ radiusKm Float?
11764
+ maxTravelMinutes Int?
11765
+
11766
+ partySize Int @default(1)
11767
+
11768
+ surpriseMeEnabled Boolean @default(false)
11769
+ popUpEnabled Boolean @default(false)
11770
+ bashBusEnabled Boolean @default(false)
11771
+
11772
+ notes String? @db.VarChar(500)
11773
+
11774
+ expiresAt DateTime
11775
+ canceledAt DateTime?
11776
+ fulfilledAt DateTime?
11777
+
11778
+ createdAt DateTime @default(now())
11779
+ updatedAt DateTime @updatedAt
11780
+
11781
+ preferences ActivityIntentPreference[]
11782
+ invitations ExperienceInvitation[]
11783
+
11784
+ @@index([status, startsAt, endsAt])
11785
+ @@index([locationCell, status, startsAt])
11786
+ @@index([userId, status])
11787
+ @@index([expiresAt])
11788
+ }
11789
+
11790
+ model ActivityIntentPreference {
11791
+ id String @id @default(cuid())
11792
+ intentId String
11793
+ intent ActivityIntent @relation(fields: [intentId], references: [id], onDelete: Cascade)
11794
+
11795
+ category ExperienceCategory
11796
+ /// 1 = interested, 2 = strongly, -1 = avoid (MVP: treat as selected/not).
11797
+ weight Int @default(1)
11798
+
11799
+ @@unique([intentId, category])
11800
+ @@index([category])
11801
+ }
11802
+
11803
+ /// Lightweight experience Bash can create/recommend before (or without) a full BashEvent.
11804
+ model ExperienceOpportunity {
11805
+ id String @id @default(cuid())
11806
+
11807
+ title String
11808
+ description String?
11809
+
11810
+ type ExperienceOpportunityType
11811
+ status ExperienceOpportunityStatus @default(DRAFT)
11812
+ visibility ExperienceVisibility @default(INVITE_ONLY)
11813
+
11814
+ startsAt DateTime
11815
+ endsAt DateTime?
11816
+
11817
+ latitude Float?
11818
+ longitude Float?
11819
+
11820
+ locationCell String?
11821
+ city String?
11822
+ timezone String
11823
+
11824
+ capacity Int?
11825
+ minimumRiders Int?
11826
+ minimumAttendees Int?
11827
+
11828
+ destinationHidden Boolean @default(false)
11829
+ destinationRevealAt DateTime?
11830
+
11831
+ transportationMode TransportationMode @default(NONE)
11832
+
11833
+ /// Optional graduation into a normal public bash.
11834
+ bashEventId String?
11835
+ bashEvent BashEvent? @relation(fields: [bashEventId], references: [id], onDelete: SetNull)
11836
+
11837
+ /// Optional vehicle / sponsor FKs deferred until pilot tables ship.
11838
+ vehicleId String?
11839
+ sponsorCampaignId String?
11840
+
11841
+ createdById String?
11842
+ createdBy User? @relation("ExperienceOpportunityCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
11843
+
11844
+ invitations ExperienceInvitation[]
11845
+
11846
+ createdAt DateTime @default(now())
11847
+ updatedAt DateTime @updatedAt
11848
+
11849
+ @@index([status, startsAt])
11850
+ @@index([locationCell, status])
11851
+ @@index([bashEventId])
11852
+ }
11853
+
11854
+ model ExperienceInvitation {
11855
+ id String @id @default(cuid())
11856
+
11857
+ opportunityId String
11858
+ opportunity ExperienceOpportunity @relation(fields: [opportunityId], references: [id], onDelete: Cascade)
11859
+
11860
+ intentId String?
11861
+ intent ActivityIntent? @relation(fields: [intentId], references: [id], onDelete: SetNull)
11862
+
11863
+ userId String
11864
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
11865
+
11866
+ status InvitationStatus @default(PENDING)
11867
+
11868
+ matchScore Float?
11869
+ matchReasons Json?
11870
+
11871
+ /// Pickup stop FK deferred until ExperienceStop ships.
11872
+ pickupStopId String?
11873
+
11874
+ sentAt DateTime?
11875
+ viewedAt DateTime?
11876
+ respondedAt DateTime?
11877
+ expiresAt DateTime?
11878
+
11879
+ checkedInAt DateTime?
11880
+ boardedAt DateTime?
11881
+ completedAt DateTime?
11882
+ noShowAt DateTime?
11883
+
11884
+ createdAt DateTime @default(now())
11885
+ updatedAt DateTime @updatedAt
11886
+
11887
+ @@unique([opportunityId, userId])
11888
+ @@index([userId, status])
11889
+ @@index([opportunityId, status])
11890
+ @@index([intentId])
11891
+ }
11892
+
11467
11893
  /// Official Bash merch sold on /store (merchant-of-record via Stripe).
11468
11894
  model StoreProduct {
11469
11895
  id String @id @default(cuid())
@@ -0,0 +1,143 @@
1
+ import {
2
+ causeTagIdsMatchingSearch,
3
+ causeTagLabels,
4
+ causeTagsMatchSearch,
5
+ normalizeCauseTags,
6
+ resolveDonationCauseLabel,
7
+ validateBashDonationCauseFields,
8
+ } from "../causeTags";
9
+
10
+ describe("causeTags", () => {
11
+ describe("normalizeCauseTags", () => {
12
+ it("drops unknown ids and dedupes", () => {
13
+ expect(
14
+ normalizeCauseTags(["lgbtq", "nope", "lgbtq", "mutual_aid"])
15
+ ).toEqual(["lgbtq", "mutual_aid"]);
16
+ });
17
+
18
+ it("handles empty input", () => {
19
+ expect(normalizeCauseTags(undefined)).toEqual([]);
20
+ expect(normalizeCauseTags(null)).toEqual([]);
21
+ expect(normalizeCauseTags([])).toEqual([]);
22
+ });
23
+ });
24
+
25
+ describe("causeTagLabels", () => {
26
+ it("maps known ids to labels", () => {
27
+ expect(causeTagLabels(["lgbtq", "fundraiser"])).toEqual([
28
+ "LGBTQ+",
29
+ "Fundraiser",
30
+ ]);
31
+ });
32
+ });
33
+
34
+ describe("causeTagIdsMatchingSearch", () => {
35
+ it("returns empty for blank query", () => {
36
+ expect(causeTagIdsMatchingSearch("")).toEqual([]);
37
+ expect(causeTagIdsMatchingSearch(" ")).toEqual([]);
38
+ });
39
+
40
+ it("matches by label substring", () => {
41
+ expect(causeTagIdsMatchingSearch("mutual")).toContain("mutual_aid");
42
+ expect(causeTagIdsMatchingSearch("LGBTQ")).toContain("lgbtq");
43
+ expect(causeTagIdsMatchingSearch("music")).toContain("music_scene");
44
+ });
45
+
46
+ it("matches by id / spaced id", () => {
47
+ expect(causeTagIdsMatchingSearch("mental_health")).toContain(
48
+ "mental_health"
49
+ );
50
+ expect(causeTagIdsMatchingSearch("mental health")).toContain(
51
+ "mental_health"
52
+ );
53
+ });
54
+ });
55
+
56
+ describe("causeTagsMatchSearch", () => {
57
+ it("is false when entity has no matching tags", () => {
58
+ expect(causeTagsMatchSearch(["neighborhood"], "lgbtq")).toBe(false);
59
+ expect(causeTagsMatchSearch([], "mutual")).toBe(false);
60
+ expect(causeTagsMatchSearch(undefined, "mutual")).toBe(false);
61
+ });
62
+
63
+ it("is true when a selected tag matches the query", () => {
64
+ expect(causeTagsMatchSearch(["lgbtq", "arts_culture"], "queer")).toBe(
65
+ false
66
+ );
67
+ expect(causeTagsMatchSearch(["lgbtq", "arts_culture"], "lgbtq")).toBe(
68
+ true
69
+ );
70
+ expect(causeTagsMatchSearch(["mutual_aid"], "mutual aid")).toBe(true);
71
+ });
72
+ });
73
+
74
+ describe("resolveDonationCauseLabel", () => {
75
+ it("prefers curated tag label over free text", () => {
76
+ expect(
77
+ resolveDonationCauseLabel({
78
+ donationCauseTagId: "mutual_aid",
79
+ donationCauseLabel: "Something else",
80
+ })
81
+ ).toBe("Mutual aid");
82
+ });
83
+
84
+ it("falls back to free-text label", () => {
85
+ expect(
86
+ resolveDonationCauseLabel({
87
+ donationCauseTagId: null,
88
+ donationCauseLabel: " Local food bank ",
89
+ })
90
+ ).toBe("Local food bank");
91
+ });
92
+ });
93
+
94
+ describe("validateBashDonationCauseFields", () => {
95
+ it("accepts empty / unset fields", () => {
96
+ expect(validateBashDonationCauseFields({})).toBeNull();
97
+ expect(
98
+ validateBashDonationCauseFields({
99
+ donationGoalCents: null,
100
+ donationCausePercent: null,
101
+ })
102
+ ).toBeNull();
103
+ });
104
+
105
+ it("rejects non-positive goals", () => {
106
+ expect(validateBashDonationCauseFields({ donationGoalCents: 0 })).toBe(
107
+ "invalid_goal"
108
+ );
109
+ expect(validateBashDonationCauseFields({ donationGoalCents: -1 })).toBe(
110
+ "invalid_goal"
111
+ );
112
+ });
113
+
114
+ it("rejects percent without a cause", () => {
115
+ expect(
116
+ validateBashDonationCauseFields({ donationCausePercent: 80 })
117
+ ).toBe("percent_requires_cause");
118
+ });
119
+
120
+ it("accepts percent with tag or label", () => {
121
+ expect(
122
+ validateBashDonationCauseFields({
123
+ donationCausePercent: 80,
124
+ donationCauseTagId: "mutual_aid",
125
+ })
126
+ ).toBeNull();
127
+ expect(
128
+ validateBashDonationCauseFields({
129
+ donationCausePercent: 100,
130
+ donationCauseLabel: "Food bank",
131
+ })
132
+ ).toBeNull();
133
+ });
134
+
135
+ it("rejects invalid tag ids", () => {
136
+ expect(
137
+ validateBashDonationCauseFields({
138
+ donationCauseTagId: "not_a_real_tag",
139
+ })
140
+ ).toBe("invalid_cause_tag");
141
+ });
142
+ });
143
+ });