@bash-app/bash-common 30.400.0 → 30.403.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__/audienceImport.test.d.ts +2 -0
- package/dist/__tests__/audienceImport.test.d.ts.map +1 -0
- package/dist/__tests__/audienceImport.test.js +39 -0
- package/dist/__tests__/audienceImport.test.js.map +1 -0
- package/dist/__tests__/eventStaffRoles.test.d.ts +2 -0
- package/dist/__tests__/eventStaffRoles.test.d.ts.map +1 -0
- package/dist/__tests__/eventStaffRoles.test.js +37 -0
- package/dist/__tests__/eventStaffRoles.test.js.map +1 -0
- package/dist/audienceImport.d.ts +71 -0
- package/dist/audienceImport.d.ts.map +1 -0
- package/dist/audienceImport.js +115 -0
- package/dist/audienceImport.js.map +1 -0
- package/dist/eventStaffRoles.d.ts +39 -0
- package/dist/eventStaffRoles.d.ts.map +1 -0
- package/dist/eventStaffRoles.js +125 -0
- package/dist/eventStaffRoles.js.map +1 -0
- package/dist/extendedSchemas.d.ts +2 -0
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/__tests__/locationVisibility.test.js +27 -1
- package/dist/utils/__tests__/locationVisibility.test.js.map +1 -1
- package/dist/utils/locationVisibility.d.ts +9 -0
- package/dist/utils/locationVisibility.d.ts.map +1 -1
- package/dist/utils/locationVisibility.js +13 -0
- package/dist/utils/locationVisibility.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +102 -0
- package/src/__tests__/audienceImport.test.ts +58 -0
- package/src/__tests__/eventStaffRoles.test.ts +50 -0
- package/src/audienceImport.ts +181 -0
- package/src/eventStaffRoles.ts +168 -0
- package/src/extendedSchemas.ts +2 -0
- package/src/index.ts +2 -0
- package/src/utils/__tests__/locationVisibility.test.ts +38 -0
- package/src/utils/locationVisibility.ts +16 -0
package/prisma/schema.prisma
CHANGED
|
@@ -1202,6 +1202,8 @@ model BashEvent {
|
|
|
1202
1202
|
lastBroadcastAt DateTime?
|
|
1203
1203
|
/// Optional host note appended to ticket purchase confirmation emails.
|
|
1204
1204
|
confirmationEmailNote String? @db.Text
|
|
1205
|
+
/// Guest-facing "meet here" label; pin uses existing coordinates.
|
|
1206
|
+
meetingPointLabel String?
|
|
1205
1207
|
/// When true, notify opted-in CRM audience on publish (optional tag in crmPublishNotifyTag).
|
|
1206
1208
|
crmPublishNotifyEnabled Boolean @default(false)
|
|
1207
1209
|
crmPublishNotifyTag String?
|
|
@@ -1244,6 +1246,7 @@ model BashEvent {
|
|
|
1244
1246
|
requireExpenseApproval Boolean @default(false)
|
|
1245
1247
|
|
|
1246
1248
|
associatedBashesReferencingMe AssociatedBash[]
|
|
1249
|
+
eventStaffAudits EventStaffAudit[]
|
|
1247
1250
|
comments BashComment[]
|
|
1248
1251
|
amountOfGuests AmountOfGuests? @relation(fields: [amountOfGuestsId], references: [id], onDelete: Cascade)
|
|
1249
1252
|
creator User @relation("CreatedEvent", fields: [creatorId], references: [id], onDelete: Cascade)
|
|
@@ -2900,6 +2903,80 @@ model HostAttendeeProfile {
|
|
|
2900
2903
|
@@index([hostUserId])
|
|
2901
2904
|
}
|
|
2902
2905
|
|
|
2906
|
+
enum EventStaffRole {
|
|
2907
|
+
Admin
|
|
2908
|
+
Organizer
|
|
2909
|
+
Promoter
|
|
2910
|
+
DoorStaff
|
|
2911
|
+
ServiceProvider
|
|
2912
|
+
Volunteer
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
enum AudienceImportSource {
|
|
2916
|
+
Eventbrite
|
|
2917
|
+
Luma
|
|
2918
|
+
Partiful
|
|
2919
|
+
Posh
|
|
2920
|
+
GenericCsv
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
enum HostImportConnectionProvider {
|
|
2924
|
+
Eventbrite
|
|
2925
|
+
Luma
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/// Host-wide standing guestlist (free-entry contacts reused across bashes).
|
|
2929
|
+
model HostStandingGuestlistEntry {
|
|
2930
|
+
id String @id @default(cuid())
|
|
2931
|
+
hostUserId String
|
|
2932
|
+
email String?
|
|
2933
|
+
phone String?
|
|
2934
|
+
fullName String?
|
|
2935
|
+
createdAt DateTime @default(now())
|
|
2936
|
+
updatedAt DateTime @updatedAt
|
|
2937
|
+
|
|
2938
|
+
host User @relation("HostStandingGuestlistOwned", fields: [hostUserId], references: [id], onDelete: Cascade)
|
|
2939
|
+
|
|
2940
|
+
@@index([hostUserId])
|
|
2941
|
+
@@index([hostUserId, email])
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
/// Encrypted Eventbrite OAuth / Luma API-key connection for audience import.
|
|
2945
|
+
model HostImportConnection {
|
|
2946
|
+
id String @id @default(cuid())
|
|
2947
|
+
hostUserId String
|
|
2948
|
+
provider HostImportConnectionProvider
|
|
2949
|
+
encryptedAccessToken String @db.Text
|
|
2950
|
+
encryptedRefreshToken String? @db.Text
|
|
2951
|
+
tokenExpiry DateTime?
|
|
2952
|
+
createdAt DateTime @default(now())
|
|
2953
|
+
updatedAt DateTime @updatedAt
|
|
2954
|
+
|
|
2955
|
+
host User @relation("HostImportConnectionsOwned", fields: [hostUserId], references: [id], onDelete: Cascade)
|
|
2956
|
+
|
|
2957
|
+
@@unique([hostUserId, provider])
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
/// Append-only staff role grant/change/revoke log.
|
|
2961
|
+
model EventStaffAudit {
|
|
2962
|
+
id String @id @default(cuid())
|
|
2963
|
+
bashEventId String
|
|
2964
|
+
hostUserId String
|
|
2965
|
+
targetEmail String?
|
|
2966
|
+
targetUserId String?
|
|
2967
|
+
action String
|
|
2968
|
+
fromRole EventStaffRole?
|
|
2969
|
+
toRole EventStaffRole?
|
|
2970
|
+
changedByUserId String
|
|
2971
|
+
createdAt DateTime @default(now())
|
|
2972
|
+
|
|
2973
|
+
bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
|
|
2974
|
+
host User @relation("EventStaffAuditHost", fields: [hostUserId], references: [id], onDelete: Cascade)
|
|
2975
|
+
|
|
2976
|
+
@@index([bashEventId, createdAt])
|
|
2977
|
+
@@index([hostUserId])
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2903
2980
|
enum HostAttendeeActivityType {
|
|
2904
2981
|
NoteAdded
|
|
2905
2982
|
NoteUpdated
|
|
@@ -3614,6 +3691,9 @@ model Ticket {
|
|
|
3614
3691
|
status TicketStatus @default(Pending)
|
|
3615
3692
|
isFreeGuest Boolean?
|
|
3616
3693
|
isNamedGuest Boolean @default(false)
|
|
3694
|
+
/// Competitor import: Eventbrite / Luma / Partiful / Posh / GenericCsv
|
|
3695
|
+
importSource String?
|
|
3696
|
+
importedExternalPaid Boolean @default(false)
|
|
3617
3697
|
geoFenceCheckInUnnecessary Boolean?
|
|
3618
3698
|
checkedInAt DateTime?
|
|
3619
3699
|
checkedOutAt DateTime?
|
|
@@ -4756,6 +4836,9 @@ model User {
|
|
|
4756
4836
|
eventDiscussionComments EventDiscussionComment[] @relation("EventDiscussionComments")
|
|
4757
4837
|
hostAttendeeProfilesOwned HostAttendeeProfile[] @relation("HostAttendeeProfilesOwned")
|
|
4758
4838
|
hostAttendeeProfilesTarget HostAttendeeProfile[] @relation("HostAttendeeProfilesTarget")
|
|
4839
|
+
hostStandingGuestlistEntries HostStandingGuestlistEntry[] @relation("HostStandingGuestlistOwned")
|
|
4840
|
+
hostImportConnections HostImportConnection[] @relation("HostImportConnectionsOwned")
|
|
4841
|
+
eventStaffAuditsAsHost EventStaffAudit[] @relation("EventStaffAuditHost")
|
|
4759
4842
|
hostCrmSavedSegments HostCrmSavedSegment[] @relation("HostCrmSavedSegments")
|
|
4760
4843
|
hostCrmEmailSendLogsAsHost HostCrmEmailSendLog[] @relation("HostCrmEmailSendLogsHost")
|
|
4761
4844
|
hostCrmEmailSendLogsAsUser HostCrmEmailSendLog[] @relation("HostCrmEmailSendLogsUser")
|
|
@@ -5375,8 +5458,22 @@ model AssociatedBash {
|
|
|
5375
5458
|
ownerUserId String?
|
|
5376
5459
|
isOrganizer Boolean?
|
|
5377
5460
|
isCoHost Boolean? @default(false)
|
|
5461
|
+
/// @deprecated — write-through for legacy callers. New rows set eventRole = DoorStaff
|
|
5462
|
+
/// (+ canAccessDoor override when needed). Drop after all read sites use
|
|
5463
|
+
/// associatedBashCanAccessDoor() instead of this boolean directly.
|
|
5378
5464
|
isDoorStaff Boolean? @default(false)
|
|
5379
5465
|
organizerTitle String?
|
|
5466
|
+
/// Null = legacy row; honor isOrganizer / isDoorStaff. Non-null uses role + overrides.
|
|
5467
|
+
eventRole EventStaffRole?
|
|
5468
|
+
canEditEvent Boolean?
|
|
5469
|
+
canEditTickets Boolean?
|
|
5470
|
+
canManageAttendees Boolean?
|
|
5471
|
+
canIssueComps Boolean?
|
|
5472
|
+
canAccessDoor Boolean?
|
|
5473
|
+
canViewFinances Boolean?
|
|
5474
|
+
canRefund Boolean?
|
|
5475
|
+
canMessage Boolean?
|
|
5476
|
+
canManageTrackingLinks Boolean?
|
|
5380
5477
|
bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
|
|
5381
5478
|
invitation Invitation? @relation(fields: [invitationId], references: [id])
|
|
5382
5479
|
owner User? @relation(fields: [ownerUserId], references: [id], onDelete: Cascade)
|
|
@@ -7551,6 +7648,11 @@ model Expense {
|
|
|
7551
7648
|
/// Tracks which invoice due reminders were sent: { dueSoon, dueDay, pastDue1, pastDue15 }.
|
|
7552
7649
|
invoiceDueRemindersSent Json?
|
|
7553
7650
|
|
|
7651
|
+
/// Host-entered email for an off-platform vendor (no Bash account required).
|
|
7652
|
+
externalVendorEmail String?
|
|
7653
|
+
/// True when the host intentionally added an external vendor (vs OCR-only vendorName).
|
|
7654
|
+
externalVendorIsExternal Boolean @default(false)
|
|
7655
|
+
|
|
7554
7656
|
budget Budget @relation(fields: [budgetId], references: [id], onDelete: Cascade)
|
|
7555
7657
|
submitter User @relation("ExpenseSubmitter", fields: [submittedBy], references: [id])
|
|
7556
7658
|
approver User? @relation("ExpenseApprover", fields: [approvedBy], references: [id])
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-event staff role templates. Booleans on AssociatedBash stay the live
|
|
3
|
+
* write-through so existing canUserEditBashEvent / userCanAccessDoor callers
|
|
4
|
+
* keep working during the transition.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const EVENT_STAFF_ROLES = [
|
|
8
|
+
"Admin",
|
|
9
|
+
"Organizer",
|
|
10
|
+
"Promoter",
|
|
11
|
+
"DoorStaff",
|
|
12
|
+
"ServiceProvider",
|
|
13
|
+
"Volunteer",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type EventStaffRole = (typeof EVENT_STAFF_ROLES)[number];
|
|
17
|
+
|
|
18
|
+
export type EventStaffPermissionKey =
|
|
19
|
+
| "canEditEvent"
|
|
20
|
+
| "canEditTickets"
|
|
21
|
+
| "canManageAttendees"
|
|
22
|
+
| "canIssueComps"
|
|
23
|
+
| "canAccessDoor"
|
|
24
|
+
| "canViewFinances"
|
|
25
|
+
| "canRefund"
|
|
26
|
+
| "canMessage"
|
|
27
|
+
| "canManageTrackingLinks";
|
|
28
|
+
|
|
29
|
+
export type EventStaffPermissionFlags = Record<EventStaffPermissionKey, boolean>;
|
|
30
|
+
|
|
31
|
+
export type EventStaffBooleanWriteThrough = {
|
|
32
|
+
isOrganizer: boolean;
|
|
33
|
+
isDoorStaff: boolean;
|
|
34
|
+
isCoHost: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const EVENT_STAFF_TEMPLATE_DEFAULTS: Record<EventStaffRole, EventStaffPermissionFlags> = {
|
|
38
|
+
Admin: {
|
|
39
|
+
canEditEvent: true,
|
|
40
|
+
canEditTickets: true,
|
|
41
|
+
canManageAttendees: true,
|
|
42
|
+
canIssueComps: true,
|
|
43
|
+
canAccessDoor: true,
|
|
44
|
+
canViewFinances: true,
|
|
45
|
+
canRefund: true,
|
|
46
|
+
canMessage: true,
|
|
47
|
+
canManageTrackingLinks: true,
|
|
48
|
+
},
|
|
49
|
+
Organizer: {
|
|
50
|
+
canEditEvent: true,
|
|
51
|
+
canEditTickets: true,
|
|
52
|
+
canManageAttendees: true,
|
|
53
|
+
canIssueComps: true,
|
|
54
|
+
canAccessDoor: true,
|
|
55
|
+
canViewFinances: false,
|
|
56
|
+
canRefund: false,
|
|
57
|
+
canMessage: true,
|
|
58
|
+
canManageTrackingLinks: true,
|
|
59
|
+
},
|
|
60
|
+
Promoter: {
|
|
61
|
+
canEditEvent: false,
|
|
62
|
+
canEditTickets: false,
|
|
63
|
+
canManageAttendees: false,
|
|
64
|
+
canIssueComps: true,
|
|
65
|
+
canAccessDoor: false,
|
|
66
|
+
canViewFinances: false,
|
|
67
|
+
canRefund: false,
|
|
68
|
+
canMessage: false,
|
|
69
|
+
canManageTrackingLinks: true,
|
|
70
|
+
},
|
|
71
|
+
DoorStaff: {
|
|
72
|
+
canEditEvent: false,
|
|
73
|
+
canEditTickets: false,
|
|
74
|
+
canManageAttendees: true,
|
|
75
|
+
canIssueComps: true,
|
|
76
|
+
canAccessDoor: true,
|
|
77
|
+
canViewFinances: false,
|
|
78
|
+
canRefund: false,
|
|
79
|
+
canMessage: false,
|
|
80
|
+
canManageTrackingLinks: false,
|
|
81
|
+
},
|
|
82
|
+
ServiceProvider: {
|
|
83
|
+
canEditEvent: false,
|
|
84
|
+
canEditTickets: false,
|
|
85
|
+
canManageAttendees: false,
|
|
86
|
+
canIssueComps: false,
|
|
87
|
+
canAccessDoor: false,
|
|
88
|
+
canViewFinances: false,
|
|
89
|
+
canRefund: false,
|
|
90
|
+
canMessage: false,
|
|
91
|
+
canManageTrackingLinks: false,
|
|
92
|
+
},
|
|
93
|
+
Volunteer: {
|
|
94
|
+
canEditEvent: false,
|
|
95
|
+
canEditTickets: false,
|
|
96
|
+
canManageAttendees: false,
|
|
97
|
+
canIssueComps: false,
|
|
98
|
+
canAccessDoor: false,
|
|
99
|
+
canViewFinances: false,
|
|
100
|
+
canRefund: false,
|
|
101
|
+
canMessage: false,
|
|
102
|
+
canManageTrackingLinks: false,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function isEventStaffRole(value: string | null | undefined): value is EventStaffRole {
|
|
107
|
+
return !!value && (EVENT_STAFF_ROLES as readonly string[]).includes(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Setting a role also writes the legacy booleans so existing callers stay valid. */
|
|
111
|
+
export function writeThroughAssociatedBashFlags(
|
|
112
|
+
role: EventStaffRole,
|
|
113
|
+
isCoHost = false
|
|
114
|
+
): EventStaffBooleanWriteThrough {
|
|
115
|
+
if (role === "Admin" || role === "Organizer") {
|
|
116
|
+
return { isOrganizer: true, isDoorStaff: false, isCoHost };
|
|
117
|
+
}
|
|
118
|
+
if (role === "DoorStaff") {
|
|
119
|
+
return { isOrganizer: false, isDoorStaff: true, isCoHost: false };
|
|
120
|
+
}
|
|
121
|
+
return { isOrganizer: false, isDoorStaff: false, isCoHost: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function resolveEventStaffPermission(
|
|
125
|
+
role: EventStaffRole | null | undefined,
|
|
126
|
+
key: EventStaffPermissionKey,
|
|
127
|
+
override: boolean | null | undefined
|
|
128
|
+
): boolean {
|
|
129
|
+
if (typeof override === "boolean") return override;
|
|
130
|
+
if (!isEventStaffRole(role)) return false;
|
|
131
|
+
return EVENT_STAFF_TEMPLATE_DEFAULTS[role][key];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Edit access: honor role+overrides when eventRole is set; otherwise the
|
|
136
|
+
* existing isOrganizer boolean. Never drop the boolean check for legacy rows.
|
|
137
|
+
*/
|
|
138
|
+
export function associatedBashCanEditEvent(row: {
|
|
139
|
+
eventRole?: string | null;
|
|
140
|
+
isOrganizer?: boolean | null;
|
|
141
|
+
canEditEvent?: boolean | null;
|
|
142
|
+
}): boolean {
|
|
143
|
+
if (isEventStaffRole(row.eventRole)) {
|
|
144
|
+
return resolveEventStaffPermission(row.eventRole, "canEditEvent", row.canEditEvent);
|
|
145
|
+
}
|
|
146
|
+
return !!row.isOrganizer;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function associatedBashCanAccessDoor(row: {
|
|
150
|
+
eventRole?: string | null;
|
|
151
|
+
isOrganizer?: boolean | null;
|
|
152
|
+
isDoorStaff?: boolean | null;
|
|
153
|
+
canAccessDoor?: boolean | null;
|
|
154
|
+
}): boolean {
|
|
155
|
+
if (isEventStaffRole(row.eventRole)) {
|
|
156
|
+
return resolveEventStaffPermission(row.eventRole, "canAccessDoor", row.canAccessDoor);
|
|
157
|
+
}
|
|
158
|
+
return Boolean(row.isOrganizer || row.isDoorStaff);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function roleFromLegacyFlags(row: {
|
|
162
|
+
isOrganizer?: boolean | null;
|
|
163
|
+
isDoorStaff?: boolean | null;
|
|
164
|
+
}): EventStaffRole | null {
|
|
165
|
+
if (row.isDoorStaff && !row.isOrganizer) return "DoorStaff";
|
|
166
|
+
if (row.isOrganizer) return "Organizer";
|
|
167
|
+
return null;
|
|
168
|
+
}
|
package/src/extendedSchemas.ts
CHANGED
|
@@ -1794,6 +1794,8 @@ export interface InvitationExtraData extends Invitation {
|
|
|
1794
1794
|
isFreeGuest: boolean;
|
|
1795
1795
|
isOrganizer?: boolean;
|
|
1796
1796
|
isCoHost?: boolean;
|
|
1797
|
+
isDoorStaff?: boolean;
|
|
1798
|
+
eventRole?: string;
|
|
1797
1799
|
organizerTitle?: string;
|
|
1798
1800
|
isOwnershipTransfer?: boolean;
|
|
1799
1801
|
includeQRCode?: boolean; // Controls whether QR code should be included in the invitation email
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,8 @@ export * from "./hostCrmAutomation.js";
|
|
|
49
49
|
export * from "./relationshipHealth.js";
|
|
50
50
|
export * from "./hostCrmConstants.js";
|
|
51
51
|
export * from "./hostPromoter.js";
|
|
52
|
+
export * from "./audienceImport.js";
|
|
53
|
+
export * from "./eventStaffRoles.js";
|
|
52
54
|
export * from "./bashEventCancellationPolicy.js";
|
|
53
55
|
export * from "./userReportTypes.js";
|
|
54
56
|
export * from "./mirroredPrismaEnums.js";
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
defaultLocationRevealMode,
|
|
6
6
|
geocodeQueryTextForPublicBashMap,
|
|
7
7
|
isLocationGloballyPublic,
|
|
8
|
+
resolveMeetingPointLabel,
|
|
8
9
|
redactBashEventLocation,
|
|
9
10
|
shouldAutoRevealLocationToPublicNow,
|
|
10
11
|
viewerCanSeeFullBashLocation,
|
|
@@ -310,3 +311,40 @@ describe("geocodeQueryTextForPublicBashMap", () => {
|
|
|
310
311
|
).toBe("Venue|123 St");
|
|
311
312
|
});
|
|
312
313
|
});
|
|
314
|
+
|
|
315
|
+
describe("resolveMeetingPointLabel", () => {
|
|
316
|
+
it("returns the trimmed label when an exact address is present", () => {
|
|
317
|
+
expect(
|
|
318
|
+
resolveMeetingPointLabel({
|
|
319
|
+
meetingPointLabel: " Side door on 9th ",
|
|
320
|
+
location: "123 Main St",
|
|
321
|
+
})
|
|
322
|
+
).toBe("Side door on 9th");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("treats street as an exact address when location is empty", () => {
|
|
326
|
+
expect(
|
|
327
|
+
resolveMeetingPointLabel({
|
|
328
|
+
meetingPointLabel: "Loading dock",
|
|
329
|
+
street: "88 Pier Ave",
|
|
330
|
+
})
|
|
331
|
+
).toBe("Loading dock");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("returns null when the label is blank or the address is still hidden", () => {
|
|
335
|
+
expect(
|
|
336
|
+
resolveMeetingPointLabel({
|
|
337
|
+
meetingPointLabel: " ",
|
|
338
|
+
location: "123 Main St",
|
|
339
|
+
})
|
|
340
|
+
).toBeNull();
|
|
341
|
+
expect(
|
|
342
|
+
resolveMeetingPointLabel({
|
|
343
|
+
meetingPointLabel: "Side door",
|
|
344
|
+
location: null,
|
|
345
|
+
street: " ",
|
|
346
|
+
})
|
|
347
|
+
).toBeNull();
|
|
348
|
+
expect(resolveMeetingPointLabel({})).toBeNull();
|
|
349
|
+
});
|
|
350
|
+
});
|