@bash-app/bash-common 30.391.0 → 30.396.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 (101) hide show
  1. package/dist/__tests__/bashSectionTitleForBashType.test.js +5 -0
  2. package/dist/__tests__/bashSectionTitleForBashType.test.js.map +1 -1
  3. package/dist/__tests__/budgetApproval.test.d.ts +2 -0
  4. package/dist/__tests__/budgetApproval.test.d.ts.map +1 -0
  5. package/dist/__tests__/budgetApproval.test.js +109 -0
  6. package/dist/__tests__/budgetApproval.test.js.map +1 -0
  7. package/dist/__tests__/crewSocialCopy.test.d.ts +2 -0
  8. package/dist/__tests__/crewSocialCopy.test.d.ts.map +1 -0
  9. package/dist/__tests__/crewSocialCopy.test.js +21 -0
  10. package/dist/__tests__/crewSocialCopy.test.js.map +1 -0
  11. package/dist/__tests__/discoveryScope.test.d.ts +2 -0
  12. package/dist/__tests__/discoveryScope.test.d.ts.map +1 -0
  13. package/dist/__tests__/discoveryScope.test.js +40 -0
  14. package/dist/__tests__/discoveryScope.test.js.map +1 -0
  15. package/dist/__tests__/suggestedNight.test.d.ts +2 -0
  16. package/dist/__tests__/suggestedNight.test.d.ts.map +1 -0
  17. package/dist/__tests__/suggestedNight.test.js +10 -0
  18. package/dist/__tests__/suggestedNight.test.js.map +1 -0
  19. package/dist/__tests__/suggestionScore.test.d.ts +2 -0
  20. package/dist/__tests__/suggestionScore.test.d.ts.map +1 -0
  21. package/dist/__tests__/suggestionScore.test.js +68 -0
  22. package/dist/__tests__/suggestionScore.test.js.map +1 -0
  23. package/dist/__tests__/userTaste.test.d.ts +2 -0
  24. package/dist/__tests__/userTaste.test.d.ts.map +1 -0
  25. package/dist/__tests__/userTaste.test.js +21 -0
  26. package/dist/__tests__/userTaste.test.js.map +1 -0
  27. package/dist/budgetApproval.d.ts +126 -0
  28. package/dist/budgetApproval.d.ts.map +1 -0
  29. package/dist/budgetApproval.js +161 -0
  30. package/dist/budgetApproval.js.map +1 -0
  31. package/dist/crewSocialCopy.d.ts +16 -0
  32. package/dist/crewSocialCopy.d.ts.map +1 -0
  33. package/dist/crewSocialCopy.js +23 -0
  34. package/dist/crewSocialCopy.js.map +1 -0
  35. package/dist/definitions.d.ts +1 -0
  36. package/dist/definitions.d.ts.map +1 -1
  37. package/dist/definitions.js +6 -0
  38. package/dist/definitions.js.map +1 -1
  39. package/dist/discoveryScope.d.ts +48 -0
  40. package/dist/discoveryScope.d.ts.map +1 -0
  41. package/dist/discoveryScope.js +114 -0
  42. package/dist/discoveryScope.js.map +1 -0
  43. package/dist/home.d.ts +6 -1
  44. package/dist/home.d.ts.map +1 -1
  45. package/dist/home.js +5 -1
  46. package/dist/home.js.map +1 -1
  47. package/dist/hostCrmCrowdPulse.d.ts +2 -0
  48. package/dist/hostCrmCrowdPulse.d.ts.map +1 -1
  49. package/dist/hostCrmCrowdPulse.js.map +1 -1
  50. package/dist/hostCrmSegments.d.ts +6 -0
  51. package/dist/hostCrmSegments.d.ts.map +1 -1
  52. package/dist/hostCrmSegments.js.map +1 -1
  53. package/dist/index.d.ts +7 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +13 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/sms/smsTemplates.d.ts +6 -1
  58. package/dist/sms/smsTemplates.d.ts.map +1 -1
  59. package/dist/sms/smsTemplates.js +1 -0
  60. package/dist/sms/smsTemplates.js.map +1 -1
  61. package/dist/suggestedNight.d.ts +12 -0
  62. package/dist/suggestedNight.d.ts.map +1 -0
  63. package/dist/suggestedNight.js +12 -0
  64. package/dist/suggestedNight.js.map +1 -0
  65. package/dist/suggestionScore.d.ts +42 -0
  66. package/dist/suggestionScore.d.ts.map +1 -0
  67. package/dist/suggestionScore.js +100 -0
  68. package/dist/suggestionScore.js.map +1 -0
  69. package/dist/userTaste.d.ts +40 -0
  70. package/dist/userTaste.d.ts.map +1 -0
  71. package/dist/userTaste.js +103 -0
  72. package/dist/userTaste.js.map +1 -0
  73. package/dist/utils/__tests__/featuredDiscoveryGeo.test.js +9 -1
  74. package/dist/utils/__tests__/featuredDiscoveryGeo.test.js.map +1 -1
  75. package/dist/utils/featuredDiscoveryGeo.d.ts +7 -0
  76. package/dist/utils/featuredDiscoveryGeo.d.ts.map +1 -1
  77. package/dist/utils/featuredDiscoveryGeo.js +11 -0
  78. package/dist/utils/featuredDiscoveryGeo.js.map +1 -1
  79. package/package.json +1 -1
  80. package/prisma/schema.prisma +152 -50
  81. package/src/__tests__/bashSectionTitleForBashType.test.ts +10 -0
  82. package/src/__tests__/budgetApproval.test.ts +153 -0
  83. package/src/__tests__/crewSocialCopy.test.ts +25 -0
  84. package/src/__tests__/discoveryScope.test.ts +65 -0
  85. package/src/__tests__/suggestedNight.test.ts +11 -0
  86. package/src/__tests__/suggestionScore.test.ts +81 -0
  87. package/src/__tests__/userTaste.test.ts +33 -0
  88. package/src/budgetApproval.ts +303 -0
  89. package/src/crewSocialCopy.ts +37 -0
  90. package/src/definitions.ts +6 -0
  91. package/src/discoveryScope.ts +170 -0
  92. package/src/home.ts +15 -0
  93. package/src/hostCrmCrowdPulse.ts +2 -0
  94. package/src/hostCrmSegments.ts +6 -0
  95. package/src/index.ts +12 -3
  96. package/src/sms/smsTemplates.ts +6 -0
  97. package/src/suggestedNight.ts +20 -0
  98. package/src/suggestionScore.ts +140 -0
  99. package/src/userTaste.ts +152 -0
  100. package/src/utils/__tests__/featuredDiscoveryGeo.test.ts +13 -0
  101. package/src/utils/featuredDiscoveryGeo.ts +15 -0
@@ -0,0 +1,140 @@
1
+ import { DiscoveryScope, isNationalOrInternational } from "./discoveryScope.js";
2
+ import { UserTasteKind, UserTasteSource } from "./userTaste.js";
3
+
4
+ export type SuggestionTasteRow = {
5
+ kind: string;
6
+ value: string;
7
+ weight: number;
8
+ source: string;
9
+ };
10
+
11
+ export type SuggestionSignals = {
12
+ connectionsGoingVisible: number;
13
+ connectionsGoingHidden: number;
14
+ followedHost: boolean;
15
+ attendedHost: boolean;
16
+ eventType: string;
17
+ causeTags: readonly string[];
18
+ musicGenre?: string | null;
19
+ discoveryScope: string;
20
+ artistsToSee: readonly string[];
21
+ eventTitle: string;
22
+ distanceMiles: number | null;
23
+ radiusMiles: number;
24
+ isFeatured: boolean;
25
+ trendingScore: number;
26
+ paidFeatureTasteOverlap: boolean;
27
+ tastes: readonly SuggestionTasteRow[];
28
+ };
29
+
30
+ export const SUGGESTION_WEIGHTS = {
31
+ connectionsGoing: 40,
32
+ followedHost: 24,
33
+ attendedHost: 20,
34
+ explicitTaste: 16,
35
+ explicitTasteLove: 32,
36
+ inferredTaste: 8,
37
+ travelScope: 10,
38
+ featured: 3,
39
+ trending: 2,
40
+ paidFeatureTaste: 1,
41
+ artistsToSee: 4,
42
+ } as const;
43
+
44
+ export const PAID_FEATURE_TASTE_CAP = SUGGESTION_WEIGHTS.paidFeatureTaste;
45
+
46
+ export function scoreSuggestion(signals: SuggestionSignals): number {
47
+ let score = 0;
48
+ if (signals.connectionsGoingVisible + signals.connectionsGoingHidden > 0) {
49
+ score += SUGGESTION_WEIGHTS.connectionsGoing;
50
+ }
51
+ if (signals.followedHost) score += SUGGESTION_WEIGHTS.followedHost;
52
+ if (signals.attendedHost) score += SUGGESTION_WEIGHTS.attendedHost;
53
+
54
+ for (const taste of signals.tastes) {
55
+ if (taste.weight < 0) {
56
+ if (tasteMatchesEvent(taste, signals)) return Number.NEGATIVE_INFINITY;
57
+ continue;
58
+ }
59
+ if (!tasteMatchesEvent(taste, signals)) continue;
60
+ if (taste.kind === UserTasteKind.DiscoveryScope) {
61
+ score += SUGGESTION_WEIGHTS.travelScope;
62
+ continue;
63
+ }
64
+ if (taste.source === UserTasteSource.Explicit) {
65
+ score +=
66
+ taste.weight === 2
67
+ ? SUGGESTION_WEIGHTS.explicitTasteLove
68
+ : SUGGESTION_WEIGHTS.explicitTaste;
69
+ } else {
70
+ score += SUGGESTION_WEIGHTS.inferredTaste;
71
+ }
72
+ }
73
+
74
+ if (signals.isFeatured) score += SUGGESTION_WEIGHTS.featured;
75
+ if (signals.trendingScore > 0) score += SUGGESTION_WEIGHTS.trending;
76
+ if (signals.paidFeatureTasteOverlap) {
77
+ score += PAID_FEATURE_TASTE_CAP;
78
+ }
79
+ if (
80
+ signals.artistsToSee.some((a) =>
81
+ signals.eventTitle.toLowerCase().includes(a.toLowerCase())
82
+ )
83
+ ) {
84
+ score += SUGGESTION_WEIGHTS.artistsToSee;
85
+ }
86
+
87
+ const withinRadius = isWithinDiscoveryRadius(signals);
88
+ if (!withinRadius) return Number.NEGATIVE_INFINITY;
89
+
90
+ if (signals.distanceMiles != null && signals.radiusMiles > 0) {
91
+ const decay = Math.min(1, signals.distanceMiles / signals.radiusMiles);
92
+ score -= decay * 4;
93
+ }
94
+
95
+ return score;
96
+ }
97
+
98
+ export function isWithinDiscoveryRadius(signals: SuggestionSignals): boolean {
99
+ const travel = signals.tastes.some(
100
+ (t) =>
101
+ t.kind === UserTasteKind.DiscoveryScope &&
102
+ isNationalOrInternational(t.value as DiscoveryScope) &&
103
+ t.weight > 0
104
+ );
105
+ if (
106
+ travel &&
107
+ isNationalOrInternational(signals.discoveryScope as DiscoveryScope)
108
+ ) {
109
+ return true;
110
+ }
111
+ if (signals.distanceMiles == null) return true;
112
+ return signals.distanceMiles <= signals.radiusMiles;
113
+ }
114
+
115
+ function tasteMatchesEvent(
116
+ taste: SuggestionTasteRow,
117
+ signals: SuggestionSignals
118
+ ): boolean {
119
+ switch (taste.kind) {
120
+ case UserTasteKind.EventType:
121
+ return signals.eventType === taste.value;
122
+ case UserTasteKind.CauseTag:
123
+ return signals.causeTags.includes(taste.value);
124
+ case UserTasteKind.MusicGenre:
125
+ return (signals.musicGenre ?? "") === taste.value;
126
+ case UserTasteKind.DiscoveryScope:
127
+ return signals.discoveryScope === taste.value;
128
+ default:
129
+ return false;
130
+ }
131
+ }
132
+
133
+ export function crewOutranksPaid(signals: SuggestionSignals): boolean {
134
+ const crew =
135
+ signals.connectionsGoingVisible +
136
+ signals.connectionsGoingHidden +
137
+ (signals.followedHost ? 1 : 0) +
138
+ (signals.attendedHost ? 1 : 0);
139
+ return crew > 0;
140
+ }
@@ -0,0 +1,152 @@
1
+ import { BashEventType } from "@prisma/client";
2
+
3
+ import { isCauseTagId } from "./causeTags.js";
4
+ import {
5
+ DiscoveryScope,
6
+ DISCOVERY_SCOPE_VALUES,
7
+ isDiscoveryScope,
8
+ } from "./discoveryScope.js";
9
+
10
+ export const UserTasteKind = {
11
+ EventType: "EventType",
12
+ CauseTag: "CauseTag",
13
+ MusicGenre: "MusicGenre",
14
+ DiscoveryScope: "DiscoveryScope",
15
+ } as const;
16
+
17
+ export type UserTasteKind =
18
+ (typeof UserTasteKind)[keyof typeof UserTasteKind];
19
+
20
+ export const UserTasteSource = {
21
+ Explicit: "Explicit",
22
+ Inferred: "Inferred",
23
+ } as const;
24
+
25
+ export type UserTasteSource =
26
+ (typeof UserTasteSource)[keyof typeof UserTasteSource];
27
+
28
+ const BASH_EVENT_TYPE_VALUES = new Set(
29
+ Object.values(BashEventType) as string[]
30
+ );
31
+
32
+ /** Mirrors Prisma MusicGenreType — kept local so validation works without extra imports. */
33
+ export const MUSIC_GENRE_TASTE_VALUES = [
34
+ "EDM",
35
+ "Electronic",
36
+ "House",
37
+ "Techno",
38
+ "Dubstep",
39
+ "Trance",
40
+ "Pop",
41
+ "Rock",
42
+ "HipHop",
43
+ "Country",
44
+ "RnB",
45
+ "Jazz",
46
+ "Blues",
47
+ "Indie",
48
+ "Alternative",
49
+ "Folk",
50
+ "Acoustic",
51
+ "Classical",
52
+ "Opera",
53
+ "World",
54
+ "Latin",
55
+ "Experimental",
56
+ "Fusion",
57
+ "Other",
58
+ ] as const;
59
+
60
+ export type MusicGenreTasteValue = (typeof MUSIC_GENRE_TASTE_VALUES)[number];
61
+
62
+ const MUSIC_GENRE_SET = new Set<string>(MUSIC_GENRE_TASTE_VALUES);
63
+
64
+ const CIVIC_EVENT_TYPES = new Set(["ChurchEvent", "SchoolEvent"]);
65
+
66
+ export function isUserTasteKind(value: unknown): value is UserTasteKind {
67
+ return (
68
+ value === UserTasteKind.EventType ||
69
+ value === UserTasteKind.CauseTag ||
70
+ value === UserTasteKind.MusicGenre ||
71
+ value === UserTasteKind.DiscoveryScope
72
+ );
73
+ }
74
+
75
+ export function isValidTasteValue(kind: UserTasteKind, value: string): boolean {
76
+ if (!value.trim()) return false;
77
+ switch (kind) {
78
+ case UserTasteKind.EventType:
79
+ return BASH_EVENT_TYPE_VALUES.has(value);
80
+ case UserTasteKind.CauseTag:
81
+ return isCauseTagId(value);
82
+ case UserTasteKind.MusicGenre:
83
+ return MUSIC_GENRE_SET.has(value);
84
+ case UserTasteKind.DiscoveryScope:
85
+ return (
86
+ isDiscoveryScope(value) &&
87
+ value !== DiscoveryScope.Unspecified &&
88
+ (DISCOVERY_SCOPE_VALUES as readonly string[]).includes(value)
89
+ );
90
+ default:
91
+ return false;
92
+ }
93
+ }
94
+
95
+ export function isCivicEventType(eventType: string): boolean {
96
+ return CIVIC_EVENT_TYPES.has(eventType);
97
+ }
98
+
99
+ export type SceneTasteChip = {
100
+ id: string;
101
+ label: string;
102
+ kind: UserTasteKind;
103
+ value: string;
104
+ };
105
+
106
+ /** First-screen chips — no Networking. */
107
+ export const SCENE_TASTE_CHIPS: readonly SceneTasteChip[] = [
108
+ { id: "festival", label: "Festivals", kind: UserTasteKind.EventType, value: "Festival" },
109
+ { id: "concert", label: "Concerts", kind: UserTasteKind.EventType, value: "Concert" },
110
+ { id: "comedy", label: "Comedy", kind: UserTasteKind.EventType, value: "ComedyShow" },
111
+ { id: "rave", label: "Raves / nightlife", kind: UserTasteKind.EventType, value: "Rave" },
112
+ { id: "fair", label: "Fairs", kind: UserTasteKind.EventType, value: "Fair" },
113
+ { id: "sports", label: "Sports", kind: UserTasteKind.EventType, value: "SportsWatchParty" },
114
+ { id: "pride", label: "Drag / Pride", kind: UserTasteKind.CauseTag, value: "lgbtq" },
115
+ { id: "arts", label: "Arts & culture", kind: UserTasteKind.CauseTag, value: "arts_culture" },
116
+ { id: "food", label: "Food", kind: UserTasteKind.EventType, value: "FestivalFood" },
117
+ { id: "outdoor", label: "Outdoor", kind: UserTasteKind.EventType, value: "OutdoorActivity" },
118
+ ];
119
+
120
+ export const TRAVEL_TASTE_CHIP: SceneTasteChip = {
121
+ id: "travel",
122
+ label: "I'll travel for the right night",
123
+ kind: UserTasteKind.DiscoveryScope,
124
+ value: DiscoveryScope.National,
125
+ };
126
+
127
+ export const TRAVEL_TASTE_VALUES: readonly string[] = [
128
+ DiscoveryScope.National,
129
+ DiscoveryScope.International,
130
+ ];
131
+
132
+ export type TastePreferenceInput = {
133
+ kind: UserTasteKind;
134
+ value: string;
135
+ weight?: number;
136
+ source?: UserTasteSource;
137
+ };
138
+
139
+ export function normalizeTasteWeight(weight: number | undefined): number {
140
+ if (weight === 2 || weight === -1 || weight === 1) return weight;
141
+ return 1;
142
+ }
143
+
144
+ export function userHasTravelTaste(
145
+ rows: ReadonlyArray<{ kind: string; value: string }>
146
+ ): boolean {
147
+ return rows.some(
148
+ (r) =>
149
+ r.kind === UserTasteKind.DiscoveryScope &&
150
+ TRAVEL_TASTE_VALUES.includes(r.value)
151
+ );
152
+ }
@@ -1,4 +1,7 @@
1
+ import { SUGGESTION_WEIGHTS } from "../../suggestionScore.js";
1
2
  import {
3
+ applyFeaturedTasteMultiplier,
4
+ FEATURED_TASTE_MATCH_SCORE,
2
5
  haversineMiles,
3
6
  effectiveFeaturedForGeoScope,
4
7
  sortPublicEventsForFeaturedDiscovery,
@@ -118,3 +121,13 @@ describe("sortPublicEventsForFeaturedDiscovery", () => {
118
121
  ]);
119
122
  });
120
123
  });
124
+
125
+ describe("applyFeaturedTasteMultiplier", () => {
126
+ it("stays smaller than crew-going weights", () => {
127
+ expect(applyFeaturedTasteMultiplier(10, false)).toBe(10);
128
+ expect(applyFeaturedTasteMultiplier(10, true)).toBe(10.5);
129
+ expect(FEATURED_TASTE_MATCH_SCORE).toBeLessThan(
130
+ SUGGESTION_WEIGHTS.connectionsGoing
131
+ );
132
+ });
133
+ });
@@ -73,3 +73,18 @@ export function sortPublicEventsForFeaturedDiscovery<
73
73
  });
74
74
  return withKeys.map((x) => x.e);
75
75
  }
76
+
77
+ /**
78
+ * Paid featuring × taste overlap. Must stay smaller than any crew-going
79
+ * boost (`SUGGESTION_WEIGHTS.connectionsGoing` / followed / attended host).
80
+ */
81
+ export const FEATURED_TASTE_MATCH_MULTIPLIER = 1.05;
82
+ export const FEATURED_TASTE_MATCH_SCORE = 1;
83
+
84
+ export function applyFeaturedTasteMultiplier(
85
+ featuredScore: number,
86
+ tasteOverlaps: boolean
87
+ ): number {
88
+ if (!tasteOverlaps) return featuredScore;
89
+ return featuredScore * FEATURED_TASTE_MATCH_MULTIPLIER;
90
+ }