@bash-app/bash-common 30.400.0 → 30.406.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 (47) hide show
  1. package/dist/__tests__/audienceImport.test.d.ts +2 -0
  2. package/dist/__tests__/audienceImport.test.d.ts.map +1 -0
  3. package/dist/__tests__/audienceImport.test.js +39 -0
  4. package/dist/__tests__/audienceImport.test.js.map +1 -0
  5. package/dist/__tests__/definitionsHelpers.test.js +34 -1
  6. package/dist/__tests__/definitionsHelpers.test.js.map +1 -1
  7. package/dist/__tests__/eventStaffRoles.test.d.ts +2 -0
  8. package/dist/__tests__/eventStaffRoles.test.d.ts.map +1 -0
  9. package/dist/__tests__/eventStaffRoles.test.js +37 -0
  10. package/dist/__tests__/eventStaffRoles.test.js.map +1 -0
  11. package/dist/audienceImport.d.ts +71 -0
  12. package/dist/audienceImport.d.ts.map +1 -0
  13. package/dist/audienceImport.js +115 -0
  14. package/dist/audienceImport.js.map +1 -0
  15. package/dist/definitions.d.ts +51 -2
  16. package/dist/definitions.d.ts.map +1 -1
  17. package/dist/definitions.js +223 -150
  18. package/dist/definitions.js.map +1 -1
  19. package/dist/eventStaffRoles.d.ts +39 -0
  20. package/dist/eventStaffRoles.d.ts.map +1 -0
  21. package/dist/eventStaffRoles.js +125 -0
  22. package/dist/eventStaffRoles.js.map +1 -0
  23. package/dist/extendedSchemas.d.ts +22 -1
  24. package/dist/extendedSchemas.d.ts.map +1 -1
  25. package/dist/extendedSchemas.js.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/utils/__tests__/locationVisibility.test.js +27 -1
  31. package/dist/utils/__tests__/locationVisibility.test.js.map +1 -1
  32. package/dist/utils/locationVisibility.d.ts +9 -0
  33. package/dist/utils/locationVisibility.d.ts.map +1 -1
  34. package/dist/utils/locationVisibility.js +13 -0
  35. package/dist/utils/locationVisibility.js.map +1 -1
  36. package/package.json +1 -1
  37. package/prisma/schema.prisma +154 -3
  38. package/src/__tests__/audienceImport.test.ts +58 -0
  39. package/src/__tests__/definitionsHelpers.test.ts +50 -0
  40. package/src/__tests__/eventStaffRoles.test.ts +50 -0
  41. package/src/audienceImport.ts +181 -0
  42. package/src/definitions.ts +260 -151
  43. package/src/eventStaffRoles.ts +168 -0
  44. package/src/extendedSchemas.ts +22 -1
  45. package/src/index.ts +2 -0
  46. package/src/utils/__tests__/locationVisibility.test.ts +38 -0
  47. package/src/utils/locationVisibility.ts +16 -0
@@ -0,0 +1,58 @@
1
+ import {
2
+ compareBashVsPartifulTicketFees,
3
+ importGuestDedupeKey,
4
+ isAudienceImportDestination,
5
+ isAudienceImportSource,
6
+ mapCsvHeaderToImportField,
7
+ normalizeImportEmail,
8
+ normalizeImportPhone,
9
+ stripeProcessingFeeCents,
10
+ } from "../audienceImport.js";
11
+
12
+ describe("audienceImport", () => {
13
+ it("maps competitor CSV headers", () => {
14
+ expect(mapCsvHeaderToImportField("Attendee Email")).toBe("email");
15
+ expect(mapCsvHeaderToImportField("Ticket Type")).toBe("tierLabel");
16
+ expect(mapCsvHeaderToImportField("unknown col")).toBeUndefined();
17
+ expect(mapCsvHeaderToImportField("source")).toBeUndefined();
18
+ });
19
+
20
+ it("normalizes email and phone", () => {
21
+ expect(normalizeImportEmail(" A@B.com ")).toBe("a@b.com");
22
+ expect(normalizeImportEmail("not-an-email")).toBeUndefined();
23
+ expect(normalizeImportPhone("(555) 123-4567")).toBe("5551234567");
24
+ expect(normalizeImportPhone("12")).toBeUndefined();
25
+ });
26
+
27
+ it("dedupes by email then phone then external id", () => {
28
+ expect(importGuestDedupeKey({ source: "Eventbrite", email: "A@b.com" })).toBe(
29
+ "email:a@b.com"
30
+ );
31
+ expect(importGuestDedupeKey({ source: "Luma", phone: "5551234567" })).toBe(
32
+ "phone:5551234567"
33
+ );
34
+ expect(
35
+ importGuestDedupeKey({ source: "Posh", externalId: "ord-1" })
36
+ ).toBe("ext:Posh:ord-1");
37
+ expect(importGuestDedupeKey({ source: "GenericCsv" })).toBeUndefined();
38
+ });
39
+
40
+ it("compares Bash vs Partiful with guest-pays primary", () => {
41
+ const result = compareBashVsPartifulTicketFees({
42
+ ticketPriceCents: 10000,
43
+ quantity: 1,
44
+ });
45
+ expect(result.bash.guestPaysCents).toBe(10000);
46
+ expect(result.bash.hostNetsCents).toBe(10000 - stripeProcessingFeeCents(10000));
47
+ expect(result.partiful.guestPaysCents).toBe(11200);
48
+ expect(result.partiful.hostNetsCents).toBe(10000);
49
+ expect(result.citation).toMatch(/June 2026/);
50
+ });
51
+
52
+ it("guards source and destination unions", () => {
53
+ expect(isAudienceImportSource("Eventbrite")).toBe(true);
54
+ expect(isAudienceImportSource("Dice")).toBe(false);
55
+ expect(isAudienceImportDestination("crm")).toBe(true);
56
+ expect(isAudienceImportDestination("sms")).toBe(false);
57
+ });
58
+ });
@@ -15,6 +15,16 @@ import {
15
15
  COMPLEMENTARY_SERVICE_TYPE,
16
16
  complementaryServiceTypeFor,
17
17
  ServiceTypes,
18
+ isIntroIntentId,
19
+ INTRO_INTENT_OPTIONS,
20
+ isLocusOfControlId,
21
+ LOCUS_OF_CONTROL_OPTIONS,
22
+ hasSensingIntuitionOverlap,
23
+ personalityOverlapHint,
24
+ normalizePersonalityType,
25
+ isInterestExpressionMode,
26
+ sharedIntroIntentHint,
27
+ locusOfControlShortLabel,
18
28
  } from "../definitions.js";
19
29
 
20
30
  describe("definitions helpers", () => {
@@ -183,4 +193,44 @@ describe("definitions helpers", () => {
183
193
  ).toBeUndefined();
184
194
  });
185
195
  });
196
+
197
+ describe("Intros intents", () => {
198
+ test("accepts the three platonic/professional intent ids", () => {
199
+ expect(INTRO_INTENT_OPTIONS.map((option) => option.id)).toEqual([
200
+ "friends",
201
+ "professional",
202
+ "mentorship",
203
+ ]);
204
+ expect(isIntroIntentId("friends")).toBe(true);
205
+ expect(isIntroIntentId("dating")).toBe(false);
206
+ });
207
+
208
+ test("normalizes personality type and detects S/N overlap", () => {
209
+ expect(normalizePersonalityType("enfp!")).toBe("ENFP");
210
+ expect(normalizePersonalityType(" ")).toBeNull();
211
+ expect(hasSensingIntuitionOverlap("ENFP", "INTJ")).toBe(true);
212
+ expect(hasSensingIntuitionOverlap("ESFP", "INTJ")).toBe(false);
213
+ expect(personalityOverlapHint("ENFP", "INTJ")).toBe(
214
+ "You both picked N — good icebreaker."
215
+ );
216
+ expect(isLocusOfControlId("internal")).toBe(true);
217
+ expect(isLocusOfControlId("random")).toBe(false);
218
+ expect(LOCUS_OF_CONTROL_OPTIONS.map((option) => option.id)).toEqual([
219
+ "internal",
220
+ "external",
221
+ "mixed",
222
+ ]);
223
+ expect(isInterestExpressionMode("named")).toBe(true);
224
+ expect(isInterestExpressionMode("crush")).toBe(false);
225
+ });
226
+
227
+ test("shared intro intent and locus labels stay conversational", () => {
228
+ expect(
229
+ sharedIntroIntentHint(["friends", "mentorship"], ["professional", "friends"])
230
+ ).toBe("You're both here for making friends.");
231
+ expect(sharedIntroIntentHint(["friends"], ["professional"])).toBeNull();
232
+ expect(locusOfControlShortLabel("internal")).toBe("Internal");
233
+ expect(locusOfControlShortLabel("nope")).toBeNull();
234
+ });
235
+ });
186
236
  });
@@ -0,0 +1,50 @@
1
+ import {
2
+ associatedBashCanAccessDoor,
3
+ associatedBashCanEditEvent,
4
+ isEventStaffRole,
5
+ roleFromLegacyFlags,
6
+ writeThroughAssociatedBashFlags,
7
+ } from "../eventStaffRoles.js";
8
+
9
+ describe("eventStaffRoles", () => {
10
+ it("write-through keeps organizer and door flags in sync", () => {
11
+ expect(writeThroughAssociatedBashFlags("Organizer", true)).toEqual({
12
+ isOrganizer: true,
13
+ isDoorStaff: false,
14
+ isCoHost: true,
15
+ });
16
+ expect(writeThroughAssociatedBashFlags("DoorStaff")).toEqual({
17
+ isOrganizer: false,
18
+ isDoorStaff: true,
19
+ isCoHost: false,
20
+ });
21
+ expect(writeThroughAssociatedBashFlags("Promoter").isOrganizer).toBe(false);
22
+ });
23
+
24
+ it("edit access honors role when set, else isOrganizer", () => {
25
+ expect(associatedBashCanEditEvent({ eventRole: "Organizer" })).toBe(true);
26
+ expect(associatedBashCanEditEvent({ eventRole: "DoorStaff" })).toBe(false);
27
+ expect(associatedBashCanEditEvent({ eventRole: "Admin", canEditEvent: false })).toBe(
28
+ false
29
+ );
30
+ expect(associatedBashCanEditEvent({ isOrganizer: true })).toBe(true);
31
+ expect(associatedBashCanEditEvent({ isOrganizer: false })).toBe(false);
32
+ });
33
+
34
+ it("door access honors role when set, else legacy flags", () => {
35
+ expect(associatedBashCanAccessDoor({ eventRole: "DoorStaff" })).toBe(true);
36
+ expect(associatedBashCanAccessDoor({ eventRole: "Volunteer" })).toBe(false);
37
+ expect(associatedBashCanAccessDoor({ isDoorStaff: true })).toBe(true);
38
+ expect(associatedBashCanAccessDoor({ isOrganizer: true })).toBe(true);
39
+ });
40
+
41
+ it("maps legacy flags and validates roles", () => {
42
+ expect(roleFromLegacyFlags({ isDoorStaff: true, isOrganizer: false })).toBe(
43
+ "DoorStaff"
44
+ );
45
+ expect(roleFromLegacyFlags({ isOrganizer: true })).toBe("Organizer");
46
+ expect(roleFromLegacyFlags({})).toBeNull();
47
+ expect(isEventStaffRole("Admin")).toBe(true);
48
+ expect(isEventStaffRole("Owner")).toBe(false);
49
+ });
50
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Competitor audience import — shared types, CSV header maps, and fee comparison math.
3
+ * Adapters normalize Eventbrite / Luma / Partiful / Posh / generic CSVs into one row shape.
4
+ */
5
+
6
+ import { calculateStripeProcessingFeeCents } from "./utils/paymentUtils.js";
7
+
8
+ export const AUDIENCE_IMPORT_SOURCES = [
9
+ "Eventbrite",
10
+ "Luma",
11
+ "Partiful",
12
+ "Posh",
13
+ "GenericCsv",
14
+ ] as const;
15
+
16
+ export type AudienceImportSource = (typeof AUDIENCE_IMPORT_SOURCES)[number];
17
+
18
+ export const AUDIENCE_IMPORT_DESTINATIONS = [
19
+ "admission",
20
+ "crm",
21
+ "standingGuestlist",
22
+ ] as const;
23
+
24
+ export type AudienceImportDestination = (typeof AUDIENCE_IMPORT_DESTINATIONS)[number];
25
+
26
+ export type NormalizedImportGuest = {
27
+ source: AudienceImportSource;
28
+ externalId?: string;
29
+ fullName?: string;
30
+ email?: string;
31
+ phone?: string;
32
+ rsvpOrOrderStatus?: string;
33
+ tierLabel?: string;
34
+ checkedInAt?: string;
35
+ isComp?: boolean;
36
+ plusOnes?: number;
37
+ importedExternalPaid?: boolean;
38
+ };
39
+
40
+ export type AudienceImportPreviewRow = NormalizedImportGuest & {
41
+ willImport: boolean;
42
+ skipReason?: string;
43
+ };
44
+
45
+ export type AudienceImportCommitResult = {
46
+ added: number;
47
+ skipped: number;
48
+ failed: number;
49
+ failures: Array<{ email?: string; fullName?: string; reason: string }>;
50
+ };
51
+
52
+ export const MAX_IMPORT_ROWS = 5000;
53
+ export const IMPORT_BATCH_SIZE = 500;
54
+
55
+ /** Partiful's published June 2026 example: guest pays $112 for a $100 organizer price. */
56
+ export const PARTIFUL_PUBLISHED_GUEST_MULTIPLIER = 112 / 100;
57
+ export const PARTIFUL_FEE_EXAMPLE_CITATION =
58
+ "Based on Partiful's published June 2026 example — their fee varies by event.";
59
+
60
+ export type TicketFeeCompareInput = {
61
+ ticketPriceCents: number;
62
+ quantity: number;
63
+ };
64
+
65
+ export type TicketFeeCompareSide = {
66
+ guestPaysCents: number;
67
+ hostNetsCents: number;
68
+ };
69
+
70
+ export type TicketFeeCompareResult = {
71
+ bash: TicketFeeCompareSide;
72
+ partiful: TicketFeeCompareSide;
73
+ citation: string;
74
+ };
75
+
76
+ export function stripeProcessingFeeCents(amountCents: number): number {
77
+ return calculateStripeProcessingFeeCents(amountCents);
78
+ }
79
+
80
+ /**
81
+ * Guest-pays is the primary comparison. Host-nets is secondary.
82
+ * Bash HostAbsorbs: guest pays listed price; host nets listed minus Stripe.
83
+ * Partiful fee-inclusive: guest pays listed × published multiplier; host receives listed.
84
+ */
85
+ export function compareBashVsPartifulTicketFees(
86
+ input: TicketFeeCompareInput
87
+ ): TicketFeeCompareResult {
88
+ const qty = Math.max(1, Math.floor(input.quantity) || 1);
89
+ const listed = Math.max(0, Math.floor(input.ticketPriceCents)) * qty;
90
+ const bashStripe = stripeProcessingFeeCents(listed);
91
+ const partifulGuest = Math.round(listed * PARTIFUL_PUBLISHED_GUEST_MULTIPLIER);
92
+ return {
93
+ bash: {
94
+ guestPaysCents: listed,
95
+ hostNetsCents: Math.max(0, listed - bashStripe),
96
+ },
97
+ partiful: {
98
+ guestPaysCents: partifulGuest,
99
+ hostNetsCents: listed,
100
+ },
101
+ citation: PARTIFUL_FEE_EXAMPLE_CITATION,
102
+ };
103
+ }
104
+
105
+ export function normalizeImportEmail(value: string | undefined | null): string | undefined {
106
+ const trimmed = value?.trim().toLowerCase();
107
+ return trimmed && trimmed.includes("@") ? trimmed : undefined;
108
+ }
109
+
110
+ export function normalizeImportPhone(value: string | undefined | null): string | undefined {
111
+ const digits = (value ?? "").replace(/\D/g, "");
112
+ if (digits.length < 7) return undefined;
113
+ return digits;
114
+ }
115
+
116
+ export function importGuestDedupeKey(guest: NormalizedImportGuest): string | undefined {
117
+ const email = normalizeImportEmail(guest.email);
118
+ if (email) return `email:${email}`;
119
+ const phone = normalizeImportPhone(guest.phone);
120
+ if (phone) return `phone:${phone}`;
121
+ if (guest.externalId?.trim()) return `ext:${guest.source}:${guest.externalId.trim()}`;
122
+ return undefined;
123
+ }
124
+
125
+ /** CSV columns never overwrite `source` (set by the importer) or `importedExternalPaid`. */
126
+ export type AudienceImportCsvMappedField = Exclude<
127
+ keyof NormalizedImportGuest,
128
+ "source" | "importedExternalPaid"
129
+ >;
130
+
131
+ /** Competitor + generic CSV headers → NormalizedImportGuest fields. */
132
+ export const AUDIENCE_IMPORT_CSV_HEADERS: Record<string, AudienceImportCsvMappedField> = {
133
+ "full name": "fullName",
134
+ name: "fullName",
135
+ "attendee name": "fullName",
136
+ "ticket buyer name": "fullName",
137
+ "first name last name": "fullName",
138
+ guest: "fullName",
139
+ "guest name": "fullName",
140
+ "first name": "fullName",
141
+ email: "email",
142
+ "e-mail": "email",
143
+ "email address": "email",
144
+ "attendee email": "email",
145
+ "ticket holder email": "email",
146
+ "buyer email": "email",
147
+ phone: "phone",
148
+ "phone number": "phone",
149
+ mobile: "phone",
150
+ cell: "phone",
151
+ status: "rsvpOrOrderStatus",
152
+ "rsvp status": "rsvpOrOrderStatus",
153
+ "order status": "rsvpOrOrderStatus",
154
+ "approval status": "rsvpOrOrderStatus",
155
+ "ticket type": "tierLabel",
156
+ "ticket class": "tierLabel",
157
+ tier: "tierLabel",
158
+ "order id": "externalId",
159
+ "attendee id": "externalId",
160
+ "guest id": "externalId",
161
+ "checked in": "checkedInAt",
162
+ "checked_in_at": "checkedInAt",
163
+ complimentary: "isComp",
164
+ comp: "isComp",
165
+ "plus ones": "plusOnes",
166
+ "plus ones count": "plusOnes",
167
+ };
168
+
169
+ export function mapCsvHeaderToImportField(
170
+ header: string
171
+ ): AudienceImportCsvMappedField | undefined {
172
+ return AUDIENCE_IMPORT_CSV_HEADERS[header.trim().toLowerCase()];
173
+ }
174
+
175
+ export function isAudienceImportSource(value: string): value is AudienceImportSource {
176
+ return (AUDIENCE_IMPORT_SOURCES as readonly string[]).includes(value);
177
+ }
178
+
179
+ export function isAudienceImportDestination(value: string): value is AudienceImportDestination {
180
+ return (AUDIENCE_IMPORT_DESTINATIONS as readonly string[]).includes(value);
181
+ }