@bash-app/bash-common 30.391.0 → 30.393.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.
- package/dist/__tests__/bashSectionTitleForBashType.test.js +5 -0
- package/dist/__tests__/bashSectionTitleForBashType.test.js.map +1 -1
- package/dist/__tests__/budgetApproval.test.d.ts +2 -0
- package/dist/__tests__/budgetApproval.test.d.ts.map +1 -0
- package/dist/__tests__/budgetApproval.test.js +109 -0
- package/dist/__tests__/budgetApproval.test.js.map +1 -0
- package/dist/__tests__/crewSocialCopy.test.d.ts +2 -0
- package/dist/__tests__/crewSocialCopy.test.d.ts.map +1 -0
- package/dist/__tests__/crewSocialCopy.test.js +21 -0
- package/dist/__tests__/crewSocialCopy.test.js.map +1 -0
- package/dist/__tests__/discoveryScope.test.d.ts +2 -0
- package/dist/__tests__/discoveryScope.test.d.ts.map +1 -0
- package/dist/__tests__/discoveryScope.test.js +40 -0
- package/dist/__tests__/discoveryScope.test.js.map +1 -0
- package/dist/__tests__/suggestedNight.test.d.ts +2 -0
- package/dist/__tests__/suggestedNight.test.d.ts.map +1 -0
- package/dist/__tests__/suggestedNight.test.js +10 -0
- package/dist/__tests__/suggestedNight.test.js.map +1 -0
- package/dist/__tests__/suggestionScore.test.d.ts +2 -0
- package/dist/__tests__/suggestionScore.test.d.ts.map +1 -0
- package/dist/__tests__/suggestionScore.test.js +68 -0
- package/dist/__tests__/suggestionScore.test.js.map +1 -0
- package/dist/__tests__/userTaste.test.d.ts +2 -0
- package/dist/__tests__/userTaste.test.d.ts.map +1 -0
- package/dist/__tests__/userTaste.test.js +21 -0
- package/dist/__tests__/userTaste.test.js.map +1 -0
- package/dist/budgetApproval.d.ts +126 -0
- package/dist/budgetApproval.d.ts.map +1 -0
- package/dist/budgetApproval.js +161 -0
- package/dist/budgetApproval.js.map +1 -0
- package/dist/crewSocialCopy.d.ts +16 -0
- package/dist/crewSocialCopy.d.ts.map +1 -0
- package/dist/crewSocialCopy.js +23 -0
- package/dist/crewSocialCopy.js.map +1 -0
- package/dist/definitions.d.ts +1 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +6 -0
- package/dist/definitions.js.map +1 -1
- package/dist/discoveryScope.d.ts +48 -0
- package/dist/discoveryScope.d.ts.map +1 -0
- package/dist/discoveryScope.js +114 -0
- package/dist/discoveryScope.js.map +1 -0
- package/dist/home.d.ts +6 -1
- package/dist/home.d.ts.map +1 -1
- package/dist/home.js +5 -1
- package/dist/home.js.map +1 -1
- package/dist/hostCrmCrowdPulse.d.ts +2 -0
- package/dist/hostCrmCrowdPulse.d.ts.map +1 -1
- package/dist/hostCrmCrowdPulse.js.map +1 -1
- package/dist/hostCrmSegments.d.ts +6 -0
- package/dist/hostCrmSegments.d.ts.map +1 -1
- package/dist/hostCrmSegments.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/sms/smsTemplates.d.ts +6 -1
- package/dist/sms/smsTemplates.d.ts.map +1 -1
- package/dist/sms/smsTemplates.js +1 -0
- package/dist/sms/smsTemplates.js.map +1 -1
- package/dist/suggestedNight.d.ts +12 -0
- package/dist/suggestedNight.d.ts.map +1 -0
- package/dist/suggestedNight.js +12 -0
- package/dist/suggestedNight.js.map +1 -0
- package/dist/suggestionScore.d.ts +42 -0
- package/dist/suggestionScore.d.ts.map +1 -0
- package/dist/suggestionScore.js +100 -0
- package/dist/suggestionScore.js.map +1 -0
- package/dist/userTaste.d.ts +40 -0
- package/dist/userTaste.d.ts.map +1 -0
- package/dist/userTaste.js +103 -0
- package/dist/userTaste.js.map +1 -0
- package/dist/utils/__tests__/featuredDiscoveryGeo.test.js +9 -1
- package/dist/utils/__tests__/featuredDiscoveryGeo.test.js.map +1 -1
- package/dist/utils/featuredDiscoveryGeo.d.ts +7 -0
- package/dist/utils/featuredDiscoveryGeo.d.ts.map +1 -1
- package/dist/utils/featuredDiscoveryGeo.js +11 -0
- package/dist/utils/featuredDiscoveryGeo.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +140 -53
- package/src/__tests__/bashSectionTitleForBashType.test.ts +10 -0
- package/src/__tests__/budgetApproval.test.ts +153 -0
- package/src/__tests__/crewSocialCopy.test.ts +25 -0
- package/src/__tests__/discoveryScope.test.ts +65 -0
- package/src/__tests__/suggestedNight.test.ts +11 -0
- package/src/__tests__/suggestionScore.test.ts +81 -0
- package/src/__tests__/userTaste.test.ts +33 -0
- package/src/budgetApproval.ts +303 -0
- package/src/crewSocialCopy.ts +37 -0
- package/src/definitions.ts +6 -0
- package/src/discoveryScope.ts +170 -0
- package/src/home.ts +15 -0
- package/src/hostCrmCrowdPulse.ts +2 -0
- package/src/hostCrmSegments.ts +6 -0
- package/src/index.ts +12 -3
- package/src/sms/smsTemplates.ts +6 -0
- package/src/suggestedNight.ts +20 -0
- package/src/suggestionScore.ts +140 -0
- package/src/userTaste.ts +152 -0
- package/src/utils/__tests__/featuredDiscoveryGeo.test.ts +13 -0
- package/src/utils/featuredDiscoveryGeo.ts +15 -0
package/prisma/schema.prisma
CHANGED
|
@@ -9,38 +9,6 @@ datasource db {
|
|
|
9
9
|
directUrl = env("POSTGRES_URL_NON_POOLING")
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
model Club {
|
|
13
|
-
id String @id @default(cuid())
|
|
14
|
-
name String
|
|
15
|
-
street String
|
|
16
|
-
userId String
|
|
17
|
-
price Int?
|
|
18
|
-
events BashEvent[]
|
|
19
|
-
admin ClubAdmin[]
|
|
20
|
-
members ClubMember[]
|
|
21
|
-
budgets Budget[] @relation("ClubBudgets")
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
model ClubAdmin {
|
|
25
|
-
id String @id @default(cuid())
|
|
26
|
-
userId String
|
|
27
|
-
clubId String
|
|
28
|
-
role ClubAdminRole
|
|
29
|
-
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
|
|
30
|
-
admin User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
model ClubMember {
|
|
34
|
-
id String @id @default(cuid())
|
|
35
|
-
userId String
|
|
36
|
-
clubId String
|
|
37
|
-
status ClubMemberStatus
|
|
38
|
-
statusHistory Json
|
|
39
|
-
price Int?
|
|
40
|
-
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
|
|
41
|
-
member User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
12
|
model BashComment {
|
|
45
13
|
id String @id @default(cuid())
|
|
46
14
|
creatorId String
|
|
@@ -937,6 +905,9 @@ model BashEvent {
|
|
|
937
905
|
/// Curated cause IDs from `CAUSE_TAGS` (bash-common) for discovery/search later.
|
|
938
906
|
causeTags String[] @default([])
|
|
939
907
|
eventType String @default("Other")
|
|
908
|
+
/// How far the draw goes — orthogonal to eventType. Hosts may set Local/State only.
|
|
909
|
+
discoveryScope DiscoveryScope @default(Unspecified)
|
|
910
|
+
discoveryScopeLabel String?
|
|
940
911
|
timezone String?
|
|
941
912
|
startDateTime DateTime?
|
|
942
913
|
endDateTime DateTime?
|
|
@@ -994,7 +965,6 @@ model BashEvent {
|
|
|
994
965
|
status BashStatus @default(Draft)
|
|
995
966
|
tags String[]
|
|
996
967
|
coverPhoto String?
|
|
997
|
-
clubId String?
|
|
998
968
|
dateTimePublished DateTime?
|
|
999
969
|
includedItems String[]
|
|
1000
970
|
videoLink String?
|
|
@@ -1276,7 +1246,6 @@ model BashEvent {
|
|
|
1276
1246
|
associatedBashesReferencingMe AssociatedBash[]
|
|
1277
1247
|
comments BashComment[]
|
|
1278
1248
|
amountOfGuests AmountOfGuests? @relation(fields: [amountOfGuestsId], references: [id], onDelete: Cascade)
|
|
1279
|
-
club Club? @relation(fields: [clubId], references: [id])
|
|
1280
1249
|
creator User @relation("CreatedEvent", fields: [creatorId], references: [id], onDelete: Cascade)
|
|
1281
1250
|
targetAudience TargetAudience? @relation(fields: [targetAudienceId], references: [id], onDelete: Cascade)
|
|
1282
1251
|
transferredFrom User? @relation("TransferredFrom", fields: [transferredFromId], references: [id])
|
|
@@ -1390,6 +1359,7 @@ model BashEvent {
|
|
|
1390
1359
|
|
|
1391
1360
|
// Organization Relations
|
|
1392
1361
|
budgets Budget[] @relation("EventBudgets")
|
|
1362
|
+
budgetApprovalRequests BudgetApprovalRequest[]
|
|
1393
1363
|
rsvps EventRSVP[] @relation("EventRSVPs")
|
|
1394
1364
|
|
|
1395
1365
|
// Groups & page views (Phases 0–4)
|
|
@@ -1435,6 +1405,7 @@ model BashEvent {
|
|
|
1435
1405
|
locationShareSessions LocationShareSession[]
|
|
1436
1406
|
/// Private Memories vault captures for this bash
|
|
1437
1407
|
memoryCaptures BashMemoryCapture[]
|
|
1408
|
+
suggestedNights SuggestedNight[]
|
|
1438
1409
|
|
|
1439
1410
|
@@index([templateId])
|
|
1440
1411
|
@@index([parentEventId])
|
|
@@ -1442,6 +1413,7 @@ model BashEvent {
|
|
|
1442
1413
|
@@index([hostedByServiceId])
|
|
1443
1414
|
@@index([eventSeriesId])
|
|
1444
1415
|
@@index([status, aiApprovalVerdict])
|
|
1416
|
+
@@index([discoveryScope, startDateTime])
|
|
1445
1417
|
}
|
|
1446
1418
|
|
|
1447
1419
|
/// Lineup / attribution roster for an event (optional act list for checkout tagging).
|
|
@@ -4634,8 +4606,6 @@ model User {
|
|
|
4634
4606
|
blogPosts BlogPost[] @relation("BlogAuthor")
|
|
4635
4607
|
checkouts Checkout[]
|
|
4636
4608
|
waiverAcceptances WaiverAcceptance[]
|
|
4637
|
-
clubAdmins ClubAdmin[]
|
|
4638
|
-
clubMembers ClubMember[]
|
|
4639
4609
|
competitions Competition[]
|
|
4640
4610
|
competitionSponsorships CompetitionSponsor[]
|
|
4641
4611
|
competitionPrizeOffersSubmitted CompetitionPrizeOffer[] @relation("CompetitionPrizeOffersSubmitted")
|
|
@@ -4855,6 +4825,8 @@ model User {
|
|
|
4855
4825
|
approvedBudgets Budget[] @relation("BudgetApprover")
|
|
4856
4826
|
submittedBudgets Budget[] @relation("BudgetSubmitter")
|
|
4857
4827
|
ownedBudgets Budget[] @relation("UserBudgets")
|
|
4828
|
+
budgetApprovalsRequested BudgetApprovalRequest[] @relation("BudgetApprovalRequester")
|
|
4829
|
+
budgetApprovalsAsApprover BudgetApprovalRequest[] @relation("BudgetApprovalApprover")
|
|
4858
4830
|
departmentsHeaded Department[] @relation("DepartmentHead")
|
|
4859
4831
|
/// Enterprise platform (Phase 0-4): internal team members assigned as a named
|
|
4860
4832
|
/// account manager, and org members who submit/approve procurement requests
|
|
@@ -4895,6 +4867,8 @@ model User {
|
|
|
4895
4867
|
// Activity Intent / Experience Discovery
|
|
4896
4868
|
userDiscoveryPreferences UserDiscoveryPreferences?
|
|
4897
4869
|
activityIntents ActivityIntent[]
|
|
4870
|
+
tastePreferences UserTastePreference[]
|
|
4871
|
+
suggestedNights SuggestedNight[]
|
|
4898
4872
|
experienceInvitations ExperienceInvitation[]
|
|
4899
4873
|
experienceOpportunitiesCreated ExperienceOpportunity[] @relation("ExperienceOpportunityCreatedBy")
|
|
4900
4874
|
|
|
@@ -4944,6 +4918,12 @@ model UserPreferences {
|
|
|
4944
4918
|
organizerUpdatesNotify Boolean @default(true)
|
|
4945
4919
|
eventSuggestions Boolean @default(true)
|
|
4946
4920
|
eventReminderNotify Boolean @default(true)
|
|
4921
|
+
/// Opt-in: engine may propose nights (Build my nights strip).
|
|
4922
|
+
suggestedNightsEnabled Boolean @default(false)
|
|
4923
|
+
/// Favorite-nudge email/push. Migrated to match eventReminderNotify.
|
|
4924
|
+
suggestedNightReminderNotify Boolean @default(true)
|
|
4925
|
+
/// Hosts in the guest's CRM may filter blasts by taste chips.
|
|
4926
|
+
hostsMayTargetTaste Boolean @default(true)
|
|
4947
4927
|
newMessageNotify Boolean @default(true)
|
|
4948
4928
|
invitationNotify Boolean @default(true)
|
|
4949
4929
|
commentNotify Boolean @default(true)
|
|
@@ -5053,6 +5033,38 @@ model UserPreferences {
|
|
|
5053
5033
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
5054
5034
|
}
|
|
5055
5035
|
|
|
5036
|
+
model UserTastePreference {
|
|
5037
|
+
id String @id @default(cuid())
|
|
5038
|
+
userId String
|
|
5039
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
5040
|
+
kind UserTasteKind
|
|
5041
|
+
value String
|
|
5042
|
+
weight Int @default(1)
|
|
5043
|
+
source UserTasteSource @default(Explicit)
|
|
5044
|
+
updatedAt DateTime @updatedAt
|
|
5045
|
+
|
|
5046
|
+
@@unique([userId, kind, value])
|
|
5047
|
+
@@index([userId, kind])
|
|
5048
|
+
}
|
|
5049
|
+
|
|
5050
|
+
model SuggestedNight {
|
|
5051
|
+
id String @id @default(cuid())
|
|
5052
|
+
userId String
|
|
5053
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
5054
|
+
bashEventId String
|
|
5055
|
+
bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
|
|
5056
|
+
status SuggestedNightStatus @default(Proposed)
|
|
5057
|
+
reason String[] @default([])
|
|
5058
|
+
proposedAt DateTime @default(now())
|
|
5059
|
+
acceptedAt DateTime?
|
|
5060
|
+
dismissedAt DateTime?
|
|
5061
|
+
remindedAt DateTime?
|
|
5062
|
+
|
|
5063
|
+
@@unique([userId, bashEventId])
|
|
5064
|
+
@@index([userId, status])
|
|
5065
|
+
@@index([bashEventId, status])
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5056
5068
|
model UserFollowing {
|
|
5057
5069
|
id String @id @default(cuid())
|
|
5058
5070
|
userId String
|
|
@@ -7263,7 +7275,6 @@ model Budget {
|
|
|
7263
7275
|
// Flexible ownership - at least ONE should be set
|
|
7264
7276
|
organizationId String?
|
|
7265
7277
|
bashEventId String?
|
|
7266
|
-
clubId String?
|
|
7267
7278
|
departmentId String?
|
|
7268
7279
|
userId String? // For personal event budgets
|
|
7269
7280
|
/// Optional: budgets tied to a service profile (vendor/exhibitor/sponsor owner costs).
|
|
@@ -7301,7 +7312,6 @@ model Budget {
|
|
|
7301
7312
|
// Relations
|
|
7302
7313
|
organization Organization? @relation("OrganizationBudgets", fields: [organizationId], references: [id], onDelete: Cascade)
|
|
7303
7314
|
bashEvent BashEvent? @relation("EventBudgets", fields: [bashEventId], references: [id], onDelete: Cascade)
|
|
7304
|
-
club Club? @relation("ClubBudgets", fields: [clubId], references: [id], onDelete: Cascade)
|
|
7305
7315
|
department Department? @relation("DepartmentBudgets", fields: [departmentId], references: [id], onDelete: Cascade)
|
|
7306
7316
|
user User? @relation("UserBudgets", fields: [userId], references: [id], onDelete: Cascade)
|
|
7307
7317
|
service Service? @relation("ServiceBudgets", fields: [serviceId], references: [id], onDelete: Cascade)
|
|
@@ -7312,7 +7322,6 @@ model Budget {
|
|
|
7312
7322
|
|
|
7313
7323
|
@@index([organizationId])
|
|
7314
7324
|
@@index([bashEventId])
|
|
7315
|
-
@@index([clubId])
|
|
7316
7325
|
@@index([departmentId])
|
|
7317
7326
|
@@index([userId])
|
|
7318
7327
|
@@index([serviceId])
|
|
@@ -7320,6 +7329,66 @@ model Budget {
|
|
|
7320
7329
|
@@index([fiscalYear])
|
|
7321
7330
|
}
|
|
7322
7331
|
|
|
7332
|
+
enum BudgetApprovalKind {
|
|
7333
|
+
CapAsk
|
|
7334
|
+
PlanSignOff
|
|
7335
|
+
}
|
|
7336
|
+
|
|
7337
|
+
enum BudgetApprovalStatus {
|
|
7338
|
+
Pending
|
|
7339
|
+
Approved
|
|
7340
|
+
ChangesRequested
|
|
7341
|
+
Declined
|
|
7342
|
+
Cancelled
|
|
7343
|
+
Superseded
|
|
7344
|
+
}
|
|
7345
|
+
|
|
7346
|
+
/// Event-level budget sign-off or pre-plan cap ask. Not per-line Budget.status.
|
|
7347
|
+
model BudgetApprovalRequest {
|
|
7348
|
+
id String @id @default(cuid())
|
|
7349
|
+
bashEventId String
|
|
7350
|
+
kind BudgetApprovalKind
|
|
7351
|
+
status BudgetApprovalStatus @default(Pending)
|
|
7352
|
+
requesterUserId String
|
|
7353
|
+
hostNote String?
|
|
7354
|
+
approverEmail String
|
|
7355
|
+
approverName String?
|
|
7356
|
+
approverUserId String?
|
|
7357
|
+
token String @unique @default(cuid())
|
|
7358
|
+
expiresAt DateTime
|
|
7359
|
+
snapshotJson Json
|
|
7360
|
+
snapshotTotalCents Int
|
|
7361
|
+
authorizedCents Int?
|
|
7362
|
+
decisionNote String?
|
|
7363
|
+
decidedAt DateTime?
|
|
7364
|
+
staleAt DateTime?
|
|
7365
|
+
createdAt DateTime @default(now())
|
|
7366
|
+
updatedAt DateTime @updatedAt
|
|
7367
|
+
|
|
7368
|
+
bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
|
|
7369
|
+
requester User @relation("BudgetApprovalRequester", fields: [requesterUserId], references: [id], onDelete: Cascade)
|
|
7370
|
+
approverUser User? @relation("BudgetApprovalApprover", fields: [approverUserId], references: [id], onDelete: SetNull)
|
|
7371
|
+
lineNotes BudgetApprovalLineNote[]
|
|
7372
|
+
|
|
7373
|
+
@@index([bashEventId, status])
|
|
7374
|
+
@@index([approverEmail])
|
|
7375
|
+
}
|
|
7376
|
+
|
|
7377
|
+
model BudgetApprovalLineNote {
|
|
7378
|
+
id String @id @default(cuid())
|
|
7379
|
+
requestId String
|
|
7380
|
+
budgetId String?
|
|
7381
|
+
nameSnapshot String
|
|
7382
|
+
categorySnapshot String?
|
|
7383
|
+
allocatedCentsSnapshot Int
|
|
7384
|
+
note String
|
|
7385
|
+
createdAt DateTime @default(now())
|
|
7386
|
+
|
|
7387
|
+
request BudgetApprovalRequest @relation(fields: [requestId], references: [id], onDelete: Cascade)
|
|
7388
|
+
|
|
7389
|
+
@@index([requestId])
|
|
7390
|
+
}
|
|
7391
|
+
|
|
7323
7392
|
enum BudgetContributionKind {
|
|
7324
7393
|
Cash
|
|
7325
7394
|
InKind
|
|
@@ -8510,20 +8579,6 @@ model BashEventMessageRead {
|
|
|
8510
8579
|
@@index([userId])
|
|
8511
8580
|
}
|
|
8512
8581
|
|
|
8513
|
-
enum ClubAdminRole {
|
|
8514
|
-
Owner
|
|
8515
|
-
Admin
|
|
8516
|
-
Staff
|
|
8517
|
-
}
|
|
8518
|
-
|
|
8519
|
-
enum ClubMemberStatus {
|
|
8520
|
-
Active
|
|
8521
|
-
Inactive
|
|
8522
|
-
Canceled
|
|
8523
|
-
Paused
|
|
8524
|
-
Removed
|
|
8525
|
-
}
|
|
8526
|
-
|
|
8527
8582
|
enum TaskStatus {
|
|
8528
8583
|
Open
|
|
8529
8584
|
Accepted
|
|
@@ -8624,6 +8679,10 @@ enum NotificationType {
|
|
|
8624
8679
|
SideQuestWindowOpened // Attendee: a timed Side Quest just went live
|
|
8625
8680
|
/// Attendee/ticket holder: host changed significant bash details (time, place, title, …)
|
|
8626
8681
|
EventDetailsUpdated
|
|
8682
|
+
/// Approver already on Bash: host sent a budget sign-off or cap ask
|
|
8683
|
+
BudgetApprovalRequested
|
|
8684
|
+
/// Host: approver approved, requested changes, or declined a budget request
|
|
8685
|
+
BudgetApprovalDecided
|
|
8627
8686
|
}
|
|
8628
8687
|
|
|
8629
8688
|
/// Opt-in reminder for a scheduled tier release (v1.5).
|
|
@@ -8674,6 +8733,34 @@ enum BashEventSource {
|
|
|
8674
8733
|
Partner // Generic: use sourcePartnerId for partner key (e.g. 'mixlog', custom integrations)
|
|
8675
8734
|
}
|
|
8676
8735
|
|
|
8736
|
+
/// How far an event's draw goes. Orthogonal to BashEventType.
|
|
8737
|
+
enum DiscoveryScope {
|
|
8738
|
+
Local
|
|
8739
|
+
State
|
|
8740
|
+
National
|
|
8741
|
+
International
|
|
8742
|
+
Unspecified
|
|
8743
|
+
}
|
|
8744
|
+
|
|
8745
|
+
enum UserTasteKind {
|
|
8746
|
+
EventType
|
|
8747
|
+
CauseTag
|
|
8748
|
+
MusicGenre
|
|
8749
|
+
DiscoveryScope
|
|
8750
|
+
}
|
|
8751
|
+
|
|
8752
|
+
enum UserTasteSource {
|
|
8753
|
+
Explicit
|
|
8754
|
+
Inferred
|
|
8755
|
+
}
|
|
8756
|
+
|
|
8757
|
+
enum SuggestedNightStatus {
|
|
8758
|
+
Proposed
|
|
8759
|
+
Accepted
|
|
8760
|
+
Dismissed
|
|
8761
|
+
Expired
|
|
8762
|
+
}
|
|
8763
|
+
|
|
8677
8764
|
/// Channel used to create a BashEvent. See BashEvent.creationChannel doc comment.
|
|
8678
8765
|
enum CreationChannel {
|
|
8679
8766
|
Web
|
|
@@ -52,6 +52,16 @@ describe("bashSectionTitleForBashType", () => {
|
|
|
52
52
|
it("'Ideas' → 'Ideas'", () => {
|
|
53
53
|
expect(bashSectionTitleForBashType("Ideas")).toBe("Ideas");
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
it("scope rows use destination copy", () => {
|
|
57
|
+
expect(bashSectionTitleForBashType("National")).toBe(
|
|
58
|
+
"Across the country"
|
|
59
|
+
);
|
|
60
|
+
expect(bashSectionTitleForBashType("International")).toBe(
|
|
61
|
+
"Worth the flight"
|
|
62
|
+
);
|
|
63
|
+
expect(bashSectionTitleForBashType("CrewGoing")).toBe("The crew's going");
|
|
64
|
+
});
|
|
55
65
|
});
|
|
56
66
|
|
|
57
67
|
describe("BashEventType enum values get pluralized", () => {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildBudgetApprovalSnapshot,
|
|
3
|
+
budgetApprovalSnapshotsMatch,
|
|
4
|
+
eventBudgetApprovalWizardPath,
|
|
5
|
+
formatBudgetCents,
|
|
6
|
+
isBudgetApprovalExpired,
|
|
7
|
+
isValidApproverEmail,
|
|
8
|
+
normalizeApproverEmail,
|
|
9
|
+
parseBudgetApprovalSnapshot,
|
|
10
|
+
resolveApproverDisplayName,
|
|
11
|
+
validateBudgetApprovalDecide,
|
|
12
|
+
} from "../budgetApproval.js";
|
|
13
|
+
|
|
14
|
+
describe("budgetApproval helpers", () => {
|
|
15
|
+
it("normalizes and validates approver emails", () => {
|
|
16
|
+
expect(normalizeApproverEmail(" Pat@Acme.com ")).toBe("pat@acme.com");
|
|
17
|
+
expect(isValidApproverEmail("pat@acme.com")).toBe(true);
|
|
18
|
+
expect(isValidApproverEmail("not-an-email")).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("hashes snapshots in a stable, order-independent way", () => {
|
|
22
|
+
const a = buildBudgetApprovalSnapshot([
|
|
23
|
+
{ id: "b", name: "DJ", category: null, totalAllocatedCents: 20000 },
|
|
24
|
+
{ id: "a", name: "Venue", category: "Rentals", totalAllocatedCents: 10000 },
|
|
25
|
+
]);
|
|
26
|
+
const b = buildBudgetApprovalSnapshot([
|
|
27
|
+
{ id: "a", name: "Venue", category: "Rentals", totalAllocatedCents: 10000 },
|
|
28
|
+
{ id: "b", name: "DJ", category: null, totalAllocatedCents: 20000 },
|
|
29
|
+
]);
|
|
30
|
+
expect(a.totalCents).toBe(30000);
|
|
31
|
+
expect(a.hash).toBe(b.hash);
|
|
32
|
+
expect(
|
|
33
|
+
budgetApprovalSnapshotsMatch(a.lines, parseBudgetApprovalSnapshot(b.lines))
|
|
34
|
+
).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("detects drift when an amount changes", () => {
|
|
38
|
+
const original = buildBudgetApprovalSnapshot([
|
|
39
|
+
{ id: "a", name: "Venue", category: null, totalAllocatedCents: 10000 },
|
|
40
|
+
]);
|
|
41
|
+
const changed = buildBudgetApprovalSnapshot([
|
|
42
|
+
{ id: "a", name: "Venue", category: null, totalAllocatedCents: 12000 },
|
|
43
|
+
]);
|
|
44
|
+
expect(budgetApprovalSnapshotsMatch(original.lines, changed.lines)).toBe(
|
|
45
|
+
false
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("requires cents on cap-ask approve and a note on request-changes", () => {
|
|
50
|
+
expect(
|
|
51
|
+
validateBudgetApprovalDecide({
|
|
52
|
+
action: "approve",
|
|
53
|
+
kind: "CapAsk",
|
|
54
|
+
authorizedCents: 0,
|
|
55
|
+
})
|
|
56
|
+
).toEqual({
|
|
57
|
+
ok: false,
|
|
58
|
+
error: "Enter an authorized amount greater than zero.",
|
|
59
|
+
});
|
|
60
|
+
expect(
|
|
61
|
+
validateBudgetApprovalDecide({
|
|
62
|
+
action: "approve",
|
|
63
|
+
kind: "CapAsk",
|
|
64
|
+
authorizedCents: 50000,
|
|
65
|
+
}).ok
|
|
66
|
+
).toBe(true);
|
|
67
|
+
expect(
|
|
68
|
+
validateBudgetApprovalDecide({
|
|
69
|
+
action: "changes",
|
|
70
|
+
kind: "PlanSignOff",
|
|
71
|
+
})
|
|
72
|
+
).toEqual({
|
|
73
|
+
ok: false,
|
|
74
|
+
error: "Add an overall note or a note on at least one line.",
|
|
75
|
+
});
|
|
76
|
+
expect(
|
|
77
|
+
validateBudgetApprovalDecide({
|
|
78
|
+
action: "changes",
|
|
79
|
+
kind: "PlanSignOff",
|
|
80
|
+
lineNotes: [
|
|
81
|
+
{
|
|
82
|
+
nameSnapshot: "Venue",
|
|
83
|
+
allocatedCentsSnapshot: 10000,
|
|
84
|
+
note: "Too high",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}).ok
|
|
88
|
+
).toBe(true);
|
|
89
|
+
expect(
|
|
90
|
+
validateBudgetApprovalDecide({
|
|
91
|
+
action: "changes",
|
|
92
|
+
kind: "CapAsk",
|
|
93
|
+
lineNotes: [
|
|
94
|
+
{
|
|
95
|
+
nameSnapshot: "Venue",
|
|
96
|
+
allocatedCentsSnapshot: 10000,
|
|
97
|
+
note: "n/a",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
})
|
|
101
|
+
).toEqual({
|
|
102
|
+
ok: false,
|
|
103
|
+
error: "A cap ask has no budget lines to annotate.",
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("resolves display name decided > hint > email", () => {
|
|
108
|
+
expect(
|
|
109
|
+
resolveApproverDisplayName({
|
|
110
|
+
decidedName: "Pat Chen",
|
|
111
|
+
hostHintName: "Boss",
|
|
112
|
+
email: "pat@acme.com",
|
|
113
|
+
})
|
|
114
|
+
).toBe("Pat Chen");
|
|
115
|
+
expect(
|
|
116
|
+
resolveApproverDisplayName({
|
|
117
|
+
decidedName: " ",
|
|
118
|
+
hostHintName: "Boss",
|
|
119
|
+
email: "pat@acme.com",
|
|
120
|
+
})
|
|
121
|
+
).toBe("Boss");
|
|
122
|
+
expect(
|
|
123
|
+
resolveApproverDisplayName({
|
|
124
|
+
decidedName: null,
|
|
125
|
+
hostHintName: null,
|
|
126
|
+
email: "pat@acme.com",
|
|
127
|
+
})
|
|
128
|
+
).toBe("pat@acme.com");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("computes expiry from expiresAt", () => {
|
|
132
|
+
expect(
|
|
133
|
+
isBudgetApprovalExpired("2020-01-01T00:00:00.000Z", new Date("2026-01-01"))
|
|
134
|
+
).toBe(true);
|
|
135
|
+
expect(
|
|
136
|
+
isBudgetApprovalExpired("2030-01-01T00:00:00.000Z", new Date("2026-01-01"))
|
|
137
|
+
).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("formats money and the wizard deep link", () => {
|
|
141
|
+
expect(formatBudgetCents(420000)).toBe("$4,200");
|
|
142
|
+
expect(eventBudgetApprovalWizardPath("evt-1")).toContain(
|
|
143
|
+
"/bash-event-wizard/evt-1/"
|
|
144
|
+
);
|
|
145
|
+
expect(eventBudgetApprovalWizardPath("evt-1")).toContain("budgetTab=lines");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("parses invalid snapshot json as empty", () => {
|
|
149
|
+
expect(parseBudgetApprovalSnapshot(null)).toEqual([]);
|
|
150
|
+
expect(parseBudgetApprovalSnapshot("nope")).toEqual([]);
|
|
151
|
+
expect(parseBudgetApprovalSnapshot([{ id: 1 }])).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { crewSocialProofCopy } from "../crewSocialCopy.js";
|
|
2
|
+
|
|
3
|
+
describe("crewSocialProofCopy", () => {
|
|
4
|
+
it("names visible crew", () => {
|
|
5
|
+
expect(
|
|
6
|
+
crewSocialProofCopy([
|
|
7
|
+
{ displayName: "Nico", attendanceVisible: true },
|
|
8
|
+
{ displayName: "Petra", attendanceVisible: true },
|
|
9
|
+
]).line
|
|
10
|
+
).toBe("Nico and Petra are going");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("does not use a count when names are hidden", () => {
|
|
14
|
+
const copy = crewSocialProofCopy([
|
|
15
|
+
{ displayName: "Sam", attendanceVisible: false },
|
|
16
|
+
{ displayName: "Alex", attendanceVisible: false },
|
|
17
|
+
]);
|
|
18
|
+
expect(copy.line).toBe("Your crew's on this one");
|
|
19
|
+
expect(copy.named).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("omits the line when nobody is going", () => {
|
|
23
|
+
expect(crewSocialProofCopy([]).line).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DiscoveryScope,
|
|
3
|
+
inferDiscoveryScopeFromText,
|
|
4
|
+
isHostSettableDiscoveryScope,
|
|
5
|
+
resolveDiscoveryScopeForWrite,
|
|
6
|
+
} from "../discoveryScope.js";
|
|
7
|
+
|
|
8
|
+
describe("discoveryScope", () => {
|
|
9
|
+
it("lets hosts set Local and State only", () => {
|
|
10
|
+
expect(isHostSettableDiscoveryScope(DiscoveryScope.Local)).toBe(true);
|
|
11
|
+
expect(isHostSettableDiscoveryScope(DiscoveryScope.State)).toBe(true);
|
|
12
|
+
expect(isHostSettableDiscoveryScope(DiscoveryScope.National)).toBe(false);
|
|
13
|
+
expect(isHostSettableDiscoveryScope(DiscoveryScope.International)).toBe(
|
|
14
|
+
false
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("infers State from state fair copy", () => {
|
|
19
|
+
expect(inferDiscoveryScopeFromText("Utah State Fair")).toBe(
|
|
20
|
+
DiscoveryScope.State
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("infers National from destination brands", () => {
|
|
25
|
+
expect(inferDiscoveryScopeFromText("EDC Las Vegas")).toBe(
|
|
26
|
+
DiscoveryScope.National
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("infers International from world-tour language", () => {
|
|
31
|
+
expect(inferDiscoveryScopeFromText("Tomorrowland world tour")).toBe(
|
|
32
|
+
DiscoveryScope.International
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("clamps host National without a hard signal", () => {
|
|
37
|
+
expect(
|
|
38
|
+
resolveDiscoveryScopeForWrite({
|
|
39
|
+
requested: DiscoveryScope.National,
|
|
40
|
+
role: "host",
|
|
41
|
+
title: "House party in Sugar House",
|
|
42
|
+
})
|
|
43
|
+
).toBe(DiscoveryScope.Unspecified);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("allows host National when inference already matches", () => {
|
|
47
|
+
expect(
|
|
48
|
+
resolveDiscoveryScopeForWrite({
|
|
49
|
+
requested: DiscoveryScope.National,
|
|
50
|
+
role: "host",
|
|
51
|
+
title: "Coachella Weekend 1",
|
|
52
|
+
})
|
|
53
|
+
).toBe(DiscoveryScope.National);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("lets scrapers set National", () => {
|
|
57
|
+
expect(
|
|
58
|
+
resolveDiscoveryScopeForWrite({
|
|
59
|
+
requested: DiscoveryScope.National,
|
|
60
|
+
role: "scraper",
|
|
61
|
+
title: "Random fest",
|
|
62
|
+
})
|
|
63
|
+
).toBe(DiscoveryScope.National);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { unfavoriteSuggestedNightAction } from "../suggestedNight.js";
|
|
2
|
+
|
|
3
|
+
describe("unfavoriteSuggestedNightAction", () => {
|
|
4
|
+
it("deletes when never reminded", () => {
|
|
5
|
+
expect(unfavoriteSuggestedNightAction(null)).toBe("delete");
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("dismisses after a nudge", () => {
|
|
9
|
+
expect(unfavoriteSuggestedNightAction(new Date())).toBe("dismiss");
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { DiscoveryScope } from "../discoveryScope.js";
|
|
2
|
+
import { scoreSuggestion, SUGGESTION_WEIGHTS } from "../suggestionScore.js";
|
|
3
|
+
import { UserTasteKind, UserTasteSource } from "../userTaste.js";
|
|
4
|
+
|
|
5
|
+
function base() {
|
|
6
|
+
return {
|
|
7
|
+
connectionsGoingVisible: 0,
|
|
8
|
+
connectionsGoingHidden: 0,
|
|
9
|
+
followedHost: false,
|
|
10
|
+
attendedHost: false,
|
|
11
|
+
eventType: "Festival",
|
|
12
|
+
causeTags: [] as string[],
|
|
13
|
+
musicGenre: null,
|
|
14
|
+
discoveryScope: DiscoveryScope.Local,
|
|
15
|
+
artistsToSee: [] as string[],
|
|
16
|
+
eventTitle: "Block party",
|
|
17
|
+
distanceMiles: 5,
|
|
18
|
+
radiusMiles: 50,
|
|
19
|
+
isFeatured: false,
|
|
20
|
+
trendingScore: 0,
|
|
21
|
+
paidFeatureTasteOverlap: false,
|
|
22
|
+
tastes: [] as Array<{
|
|
23
|
+
kind: string;
|
|
24
|
+
value: string;
|
|
25
|
+
weight: number;
|
|
26
|
+
source: string;
|
|
27
|
+
}>,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("scoreSuggestion", () => {
|
|
32
|
+
it("ranks crew going above paid taste overlap", () => {
|
|
33
|
+
const crew = scoreSuggestion({
|
|
34
|
+
...base(),
|
|
35
|
+
connectionsGoingVisible: 1,
|
|
36
|
+
});
|
|
37
|
+
const paid = scoreSuggestion({
|
|
38
|
+
...base(),
|
|
39
|
+
paidFeatureTasteOverlap: true,
|
|
40
|
+
tastes: [
|
|
41
|
+
{
|
|
42
|
+
kind: UserTasteKind.EventType,
|
|
43
|
+
value: "Festival",
|
|
44
|
+
weight: 1,
|
|
45
|
+
source: UserTasteSource.Explicit,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
expect(crew).toBeGreaterThan(SUGGESTION_WEIGHTS.paidFeatureTaste);
|
|
50
|
+
expect(crew).toBeGreaterThan(
|
|
51
|
+
paid - SUGGESTION_WEIGHTS.explicitTaste
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("lets travel taste break radius for National events only", () => {
|
|
56
|
+
const tastes = [
|
|
57
|
+
{
|
|
58
|
+
kind: UserTasteKind.DiscoveryScope,
|
|
59
|
+
value: DiscoveryScope.National,
|
|
60
|
+
weight: 1,
|
|
61
|
+
source: UserTasteSource.Explicit,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
expect(
|
|
65
|
+
scoreSuggestion({
|
|
66
|
+
...base(),
|
|
67
|
+
discoveryScope: DiscoveryScope.National,
|
|
68
|
+
distanceMiles: 900,
|
|
69
|
+
tastes,
|
|
70
|
+
})
|
|
71
|
+
).toBeGreaterThan(0);
|
|
72
|
+
expect(
|
|
73
|
+
scoreSuggestion({
|
|
74
|
+
...base(),
|
|
75
|
+
discoveryScope: DiscoveryScope.Local,
|
|
76
|
+
distanceMiles: 900,
|
|
77
|
+
tastes,
|
|
78
|
+
})
|
|
79
|
+
).toBe(Number.NEGATIVE_INFINITY);
|
|
80
|
+
});
|
|
81
|
+
});
|