@bash-app/bash-common 30.320.0 → 30.322.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__/bashEventCancellationPolicy.test.d.ts +2 -0
- package/dist/__tests__/bashEventCancellationPolicy.test.d.ts.map +1 -0
- package/dist/__tests__/bashEventCancellationPolicy.test.js +91 -0
- package/dist/__tests__/bashEventCancellationPolicy.test.js.map +1 -0
- package/dist/__tests__/sideQuestIdeas.test.js +5 -1
- package/dist/__tests__/sideQuestIdeas.test.js.map +1 -1
- package/dist/__tests__/sideQuestTypes.test.js +33 -1
- package/dist/__tests__/sideQuestTypes.test.js.map +1 -1
- package/dist/bashEventCancellationPolicy.d.ts +58 -0
- package/dist/bashEventCancellationPolicy.d.ts.map +1 -0
- package/dist/bashEventCancellationPolicy.js +150 -0
- package/dist/bashEventCancellationPolicy.js.map +1 -0
- package/dist/definitions.d.ts +5 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/sideQuestIdeas.d.ts +2 -2
- package/dist/sideQuestIdeas.d.ts.map +1 -1
- package/dist/sideQuestIdeas.js +18 -2
- package/dist/sideQuestIdeas.js.map +1 -1
- package/dist/sideQuestTypes.d.ts +76 -0
- package/dist/sideQuestTypes.d.ts.map +1 -1
- package/dist/sideQuestTypes.js +35 -0
- package/dist/sideQuestTypes.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +80 -1
- package/src/__tests__/bashEventCancellationPolicy.test.ts +148 -0
- package/src/__tests__/sideQuestIdeas.test.ts +5 -1
- package/src/__tests__/sideQuestTypes.test.ts +41 -0
- package/src/bashEventCancellationPolicy.ts +231 -0
- package/src/definitions.ts +5 -0
- package/src/index.ts +1 -0
- package/src/sideQuestIdeas.ts +22 -3
- package/src/sideQuestTypes.ts +111 -0
package/prisma/schema.prisma
CHANGED
|
@@ -2043,7 +2043,7 @@ enum SideQuestCategory {
|
|
|
2043
2043
|
GiveBack
|
|
2044
2044
|
}
|
|
2045
2045
|
|
|
2046
|
-
/// How the guest proves completion. Phase 1
|
|
2046
|
+
/// How the guest proves completion. Phase 1+: Qr, Code, Vote, Circuit, Connect. Others reserved.
|
|
2047
2047
|
enum SideQuestType {
|
|
2048
2048
|
Qr
|
|
2049
2049
|
Code
|
|
@@ -2055,6 +2055,10 @@ enum SideQuestType {
|
|
|
2055
2055
|
Sponsor
|
|
2056
2056
|
Social
|
|
2057
2057
|
Trivia
|
|
2058
|
+
/// Multi-stop booth tour — guest scans N checkpoint QRs
|
|
2059
|
+
Circuit
|
|
2060
|
+
/// Meet N people — scan their ticket QR after the icebreaker (requiredTicks)
|
|
2061
|
+
Connect
|
|
2058
2062
|
}
|
|
2059
2063
|
|
|
2060
2064
|
enum EventBadgeAwardSource {
|
|
@@ -2089,6 +2093,12 @@ model SideQuest {
|
|
|
2089
2093
|
sponsorServiceId String?
|
|
2090
2094
|
/// When questType is Vote: the competition casting a vote completes this quest
|
|
2091
2095
|
competitionId String?
|
|
2096
|
+
/// Ticks needed for this guest to complete (1 = single-shot; Circuit/Connect use N)
|
|
2097
|
+
requiredTicks Int @default(1)
|
|
2098
|
+
/// Quest-local icebreaker when bash icebreaker is off (Connect quests)
|
|
2099
|
+
icebreakerPrompt String?
|
|
2100
|
+
/// Set when timed-window-open push has been sent (cron)
|
|
2101
|
+
windowOpenedNotifiedAt DateTime?
|
|
2092
2102
|
createdAt DateTime @default(now())
|
|
2093
2103
|
updatedAt DateTime @updatedAt
|
|
2094
2104
|
|
|
@@ -2097,11 +2107,70 @@ model SideQuest {
|
|
|
2097
2107
|
competition Competition? @relation(fields: [competitionId], references: [id], onDelete: SetNull)
|
|
2098
2108
|
completions SideQuestCompletion[]
|
|
2099
2109
|
milestonesUnlocking SideQuestMilestone[] @relation("MilestoneUnlocksQuest")
|
|
2110
|
+
checkpoints SideQuestCheckpoint[]
|
|
2111
|
+
progressHits SideQuestProgressHit[]
|
|
2100
2112
|
|
|
2101
2113
|
@@index([bashEventId])
|
|
2102
2114
|
@@index([bashEventId, sortOrder])
|
|
2103
2115
|
@@index([eventBadgeId])
|
|
2104
2116
|
@@index([competitionId])
|
|
2117
|
+
@@index([startsAt, windowOpenedNotifiedAt])
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
model SideQuestCheckpoint {
|
|
2121
|
+
id String @id @default(cuid())
|
|
2122
|
+
sideQuestId String
|
|
2123
|
+
label String
|
|
2124
|
+
sortOrder Int @default(0)
|
|
2125
|
+
code String
|
|
2126
|
+
codeHash String
|
|
2127
|
+
/// Platform hire (Vendors / Exhibitors) — Pending, Approved, or Confirmed.
|
|
2128
|
+
serviceBookingId String?
|
|
2129
|
+
serviceId String?
|
|
2130
|
+
/// Partner request flows (vendor or exhibitor) before/at confirm.
|
|
2131
|
+
vendorBookingRequestId String?
|
|
2132
|
+
exhibitorBookingRequestId String?
|
|
2133
|
+
/// Off-platform vendor — QR emailed here; optional display name.
|
|
2134
|
+
contactEmail String?
|
|
2135
|
+
contactName String?
|
|
2136
|
+
createdAt DateTime @default(now())
|
|
2137
|
+
updatedAt DateTime @updatedAt
|
|
2138
|
+
|
|
2139
|
+
sideQuest SideQuest @relation(fields: [sideQuestId], references: [id], onDelete: Cascade)
|
|
2140
|
+
hits SideQuestProgressHit[]
|
|
2141
|
+
serviceBooking ServiceBooking? @relation(fields: [serviceBookingId], references: [id], onDelete: SetNull)
|
|
2142
|
+
service Service? @relation(fields: [serviceId], references: [id], onDelete: SetNull)
|
|
2143
|
+
vendorBookingRequest VendorBookingRequest? @relation(fields: [vendorBookingRequestId], references: [id], onDelete: SetNull)
|
|
2144
|
+
exhibitorBookingRequest ExhibitorBookingRequest? @relation(fields: [exhibitorBookingRequestId], references: [id], onDelete: SetNull)
|
|
2145
|
+
|
|
2146
|
+
@@index([sideQuestId, sortOrder])
|
|
2147
|
+
@@index([sideQuestId, codeHash])
|
|
2148
|
+
@@index([serviceBookingId])
|
|
2149
|
+
@@index([vendorBookingRequestId])
|
|
2150
|
+
@@index([exhibitorBookingRequestId])
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/// Per-guest progress toward requiredTicks (checkpoint scan or Connect meet).
|
|
2154
|
+
model SideQuestProgressHit {
|
|
2155
|
+
id String @id @default(cuid())
|
|
2156
|
+
sideQuestId String
|
|
2157
|
+
userId String
|
|
2158
|
+
/// Stable key: "cp:{checkpointId}" | "user:{meetUserId}" | "name:{normalized}"
|
|
2159
|
+
dedupeKey String
|
|
2160
|
+
checkpointId String?
|
|
2161
|
+
meetDisplayName String?
|
|
2162
|
+
meetUserId String?
|
|
2163
|
+
createdAt DateTime @default(now())
|
|
2164
|
+
|
|
2165
|
+
sideQuest SideQuest @relation(fields: [sideQuestId], references: [id], onDelete: Cascade)
|
|
2166
|
+
user User @relation("SideQuestProgressHits", fields: [userId], references: [id], onDelete: Cascade)
|
|
2167
|
+
checkpoint SideQuestCheckpoint? @relation(fields: [checkpointId], references: [id], onDelete: Cascade)
|
|
2168
|
+
meetUser User? @relation("SideQuestMeetTargets", fields: [meetUserId], references: [id], onDelete: SetNull)
|
|
2169
|
+
|
|
2170
|
+
@@unique([sideQuestId, userId, dedupeKey])
|
|
2171
|
+
@@index([sideQuestId, userId])
|
|
2172
|
+
@@index([userId])
|
|
2173
|
+
@@index([checkpointId])
|
|
2105
2174
|
}
|
|
2106
2175
|
|
|
2107
2176
|
model SideQuestCompletion {
|
|
@@ -2217,6 +2286,8 @@ model CommunityQuestGoal {
|
|
|
2217
2286
|
eventBadgeId String?
|
|
2218
2287
|
bashPointsReward Int?
|
|
2219
2288
|
unlockedAt DateTime?
|
|
2289
|
+
/// Set when ~80% near-goal push has been sent (once per event)
|
|
2290
|
+
nearGoalNotifiedAt DateTime?
|
|
2220
2291
|
createdAt DateTime @default(now())
|
|
2221
2292
|
updatedAt DateTime @updatedAt
|
|
2222
2293
|
|
|
@@ -3815,6 +3886,8 @@ model User {
|
|
|
3815
3886
|
eventBadgeAwards EventBadgeAward[] @relation("EventBadgeAwards")
|
|
3816
3887
|
sideQuestMilestoneClaims SideQuestMilestoneClaim[] @relation("SideQuestMilestoneClaims")
|
|
3817
3888
|
sideQuestPrizeEntries SideQuestPrizeEntry[] @relation("SideQuestPrizeEntries")
|
|
3889
|
+
sideQuestProgressHits SideQuestProgressHit[] @relation("SideQuestProgressHits")
|
|
3890
|
+
sideQuestMeetTargets SideQuestProgressHit[] @relation("SideQuestMeetTargets")
|
|
3818
3891
|
contacts Contact[]
|
|
3819
3892
|
contactLists ContactList[] @relation("UserContactLists")
|
|
3820
3893
|
contactlist ContactList[]
|
|
@@ -4638,6 +4711,7 @@ model Service {
|
|
|
4638
4711
|
googleReviews GoogleReview[]
|
|
4639
4712
|
serviceReviews ServiceReview[]
|
|
4640
4713
|
providerHostReviews ProviderHostReview[]
|
|
4714
|
+
sideQuestCheckpoints SideQuestCheckpoint[]
|
|
4641
4715
|
|
|
4642
4716
|
@@index([serviceListingStripeSubscriptionId])
|
|
4643
4717
|
@@index([paymentAccountId])
|
|
@@ -6383,6 +6457,7 @@ model ServiceBooking {
|
|
|
6383
6457
|
representedByOrganization Organization? @relation(fields: [representedByOrganizationId], references: [id], onDelete: SetNull)
|
|
6384
6458
|
representation OrganizationArtistRepresentation? @relation("BookingRepresentation", fields: [representationId], references: [id], onDelete: SetNull)
|
|
6385
6459
|
agencySettlement AgencyBookingSettlement?
|
|
6460
|
+
sideQuestCheckpoints SideQuestCheckpoint[]
|
|
6386
6461
|
|
|
6387
6462
|
@@index([status])
|
|
6388
6463
|
@@index([creatorId])
|
|
@@ -6452,6 +6527,7 @@ model VendorBookingRequest {
|
|
|
6452
6527
|
vendorService Service @relation("VendorBookingService", fields: [vendorServiceId], references: [id], onDelete: Cascade)
|
|
6453
6528
|
offers ServiceBookingOffer[]
|
|
6454
6529
|
categoryExclusivities VendorCategoryExclusivity[]
|
|
6530
|
+
sideQuestCheckpoints SideQuestCheckpoint[]
|
|
6455
6531
|
|
|
6456
6532
|
@@index([hostUserId])
|
|
6457
6533
|
@@index([vendorServiceId])
|
|
@@ -6575,6 +6651,7 @@ model ExhibitorBookingRequest {
|
|
|
6575
6651
|
exhibitorService Service @relation("ExhibitorBookingService", fields: [exhibitorServiceId], references: [id], onDelete: Cascade)
|
|
6576
6652
|
hostUser User @relation("ExhibitorBookingHost", fields: [hostUserId], references: [id], onDelete: Cascade)
|
|
6577
6653
|
serviceMessages ServiceBookingMessage[] @relation("ExhibitorServiceMessages")
|
|
6654
|
+
sideQuestCheckpoints SideQuestCheckpoint[]
|
|
6578
6655
|
|
|
6579
6656
|
@@index([hostUserId])
|
|
6580
6657
|
@@index([exhibitorServiceId])
|
|
@@ -7080,6 +7157,8 @@ enum NotificationType {
|
|
|
7080
7157
|
PartnerPromotionBlast // Attendee: partner on-site promo deal at an event
|
|
7081
7158
|
SideQuestRevealed // Attendee: host revealed a secret Side Quest
|
|
7082
7159
|
CommunityQuestUnlocked // Attendee: community quest goal unlocked
|
|
7160
|
+
CommunityQuestNearGoal // Attendee: community goal ~80% of the way
|
|
7161
|
+
SideQuestWindowOpened // Attendee: a timed Side Quest just went live
|
|
7083
7162
|
}
|
|
7084
7163
|
|
|
7085
7164
|
/// Opt-in reminder for a scheduled tier release (v1.5).
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveBashEventCancellationPresetId,
|
|
3
|
+
bashEventCancellationPresetDisplayText,
|
|
4
|
+
bashEventCancellationPolicyDetailLabel,
|
|
5
|
+
defaultBashEventCancellationPolicyDisplayText,
|
|
6
|
+
resolveBashEventGuestRefundEligibility,
|
|
7
|
+
BASH_EVENT_CANCELLATION_PRESETS,
|
|
8
|
+
DEFAULT_BASH_EVENT_CANCELLATION_PRESET_ID,
|
|
9
|
+
} from "../bashEventCancellationPolicy";
|
|
10
|
+
|
|
11
|
+
describe("bashEventCancellationPolicy", () => {
|
|
12
|
+
it("defaults to Standard — 7 days", () => {
|
|
13
|
+
expect(DEFAULT_BASH_EVENT_CANCELLATION_PRESET_ID).toBe("flexible_7_day");
|
|
14
|
+
expect(defaultBashEventCancellationPolicyDisplayText()).toBe(
|
|
15
|
+
bashEventCancellationPresetDisplayText("flexible_7_day")
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("lists Anytime first as the most guest-friendly preset", () => {
|
|
20
|
+
expect(BASH_EVENT_CANCELLATION_PRESETS[0].id).toBe("anytime_until_start");
|
|
21
|
+
expect(BASH_EVENT_CANCELLATION_PRESETS[0].name).toBe("Anytime");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("maps stored policy text to a short detail-page label", () => {
|
|
25
|
+
expect(bashEventCancellationPolicyDetailLabel(null)).toBeNull();
|
|
26
|
+
expect(
|
|
27
|
+
bashEventCancellationPolicyDetailLabel(
|
|
28
|
+
bashEventCancellationPresetDisplayText("anytime_until_start")
|
|
29
|
+
)
|
|
30
|
+
).toBe("Anytime");
|
|
31
|
+
expect(
|
|
32
|
+
bashEventCancellationPolicyDetailLabel(
|
|
33
|
+
bashEventCancellationPresetDisplayText("flexible_7_day")
|
|
34
|
+
)
|
|
35
|
+
).toBe("Standard — 7 days");
|
|
36
|
+
expect(
|
|
37
|
+
bashEventCancellationPolicyDetailLabel("Call the host for a refund.")
|
|
38
|
+
).toBe("Call the host for a refund.");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("resolves empty to null", () => {
|
|
42
|
+
expect(resolveBashEventCancellationPresetId(null)).toBeNull();
|
|
43
|
+
expect(resolveBashEventCancellationPresetId("")).toBeNull();
|
|
44
|
+
expect(resolveBashEventCancellationPresetId(" ")).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("resolves each preset displayText to its id", () => {
|
|
48
|
+
for (const preset of BASH_EVENT_CANCELLATION_PRESETS) {
|
|
49
|
+
expect(resolveBashEventCancellationPresetId(preset.displayText)).toBe(
|
|
50
|
+
preset.id
|
|
51
|
+
);
|
|
52
|
+
expect(bashEventCancellationPresetDisplayText(preset.id)).toBe(
|
|
53
|
+
preset.displayText
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("treats unknown free text as custom", () => {
|
|
59
|
+
expect(
|
|
60
|
+
resolveBashEventCancellationPresetId("Contact the host for refund.")
|
|
61
|
+
).toBe("custom");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("resolveBashEventGuestRefundEligibility", () => {
|
|
65
|
+
const start = new Date("2026-08-01T20:00:00.000Z");
|
|
66
|
+
|
|
67
|
+
it("auto-refunds Standard 7-day when more than 7 days out", () => {
|
|
68
|
+
const now = new Date("2026-07-20T20:00:00.000Z"); // 12 days before
|
|
69
|
+
const result = resolveBashEventGuestRefundEligibility(
|
|
70
|
+
bashEventCancellationPresetDisplayText("flexible_7_day"),
|
|
71
|
+
{ eventStart: start, now }
|
|
72
|
+
);
|
|
73
|
+
expect(result).toEqual({
|
|
74
|
+
mode: "auto",
|
|
75
|
+
eligibleNow: true,
|
|
76
|
+
presetId: "flexible_7_day",
|
|
77
|
+
fullRefundHoursBeforeStart: 168,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("blocks Standard 7-day when inside the 7-day window", () => {
|
|
82
|
+
const now = new Date("2026-07-28T20:00:00.000Z"); // 4 days before
|
|
83
|
+
const result = resolveBashEventGuestRefundEligibility(
|
|
84
|
+
bashEventCancellationPresetDisplayText("flexible_7_day"),
|
|
85
|
+
{ eventStart: start, now }
|
|
86
|
+
);
|
|
87
|
+
expect(result.mode).toBe("auto");
|
|
88
|
+
expect(result.eligibleNow).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("allows Anytime until the event starts", () => {
|
|
92
|
+
const now = new Date("2026-08-01T19:00:00.000Z");
|
|
93
|
+
const result = resolveBashEventGuestRefundEligibility(
|
|
94
|
+
bashEventCancellationPresetDisplayText("anytime_until_start"),
|
|
95
|
+
{ eventStart: start, now }
|
|
96
|
+
);
|
|
97
|
+
expect(result.mode).toBe("auto");
|
|
98
|
+
expect(result.eligibleNow).toBe(true);
|
|
99
|
+
expect(result.fullRefundHoursBeforeStart).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("blocks Anytime after the event starts", () => {
|
|
103
|
+
const now = new Date("2026-08-01T21:00:00.000Z");
|
|
104
|
+
const result = resolveBashEventGuestRefundEligibility(
|
|
105
|
+
bashEventCancellationPresetDisplayText("anytime_until_start"),
|
|
106
|
+
{ eventStart: start, now }
|
|
107
|
+
);
|
|
108
|
+
expect(result.eligibleNow).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns none for no_refunds", () => {
|
|
112
|
+
const result = resolveBashEventGuestRefundEligibility(
|
|
113
|
+
bashEventCancellationPresetDisplayText("no_refunds"),
|
|
114
|
+
{ eventStart: start, now: new Date("2026-06-01T00:00:00.000Z") }
|
|
115
|
+
);
|
|
116
|
+
expect(result).toEqual({
|
|
117
|
+
mode: "none",
|
|
118
|
+
eligibleNow: false,
|
|
119
|
+
presetId: "no_refunds",
|
|
120
|
+
fullRefundHoursBeforeStart: null,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns host_review for custom free text", () => {
|
|
125
|
+
const result = resolveBashEventGuestRefundEligibility(
|
|
126
|
+
"Email the host for a partial refund.",
|
|
127
|
+
{ eventStart: start, now: new Date("2026-06-01T00:00:00.000Z") }
|
|
128
|
+
);
|
|
129
|
+
expect(result).toEqual({
|
|
130
|
+
mode: "host_review",
|
|
131
|
+
eligibleNow: true,
|
|
132
|
+
presetId: "custom",
|
|
133
|
+
fullRefundHoursBeforeStart: null,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("uses Standard 7-day when policy is empty", () => {
|
|
138
|
+
const now = new Date("2026-07-20T20:00:00.000Z");
|
|
139
|
+
const result = resolveBashEventGuestRefundEligibility(null, {
|
|
140
|
+
eventStart: start,
|
|
141
|
+
now,
|
|
142
|
+
});
|
|
143
|
+
expect(result.presetId).toBe("flexible_7_day");
|
|
144
|
+
expect(result.mode).toBe("auto");
|
|
145
|
+
expect(result.eligibleNow).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -19,8 +19,12 @@ describe("sideQuestIdeas", () => {
|
|
|
19
19
|
expect(hits.every((h) => JSON.stringify(h).toLowerCase().includes("transit") || h.category === "Transit")).toBe(true);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
it("includes Vote in phase 1 types", () => {
|
|
22
|
+
it("includes Vote, Circuit, and Connect in phase 1 types", () => {
|
|
23
23
|
expect(SIDE_QUEST_PHASE1_TYPES).toContain("Vote");
|
|
24
|
+
expect(SIDE_QUEST_PHASE1_TYPES).toContain("Circuit");
|
|
25
|
+
expect(SIDE_QUEST_PHASE1_TYPES).toContain("Connect");
|
|
24
26
|
expect(SIDE_QUEST_TYPE_META.Vote.phase1).toBe(true);
|
|
27
|
+
expect(SIDE_QUEST_TYPE_META.Circuit.phase1).toBe(true);
|
|
28
|
+
expect(SIDE_QUEST_TYPE_META.Connect.phase1).toBe(true);
|
|
25
29
|
});
|
|
26
30
|
});
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
buildSideQuestDeepLink,
|
|
3
3
|
buildSideQuestLegacyPayload,
|
|
4
4
|
parseSideQuestScanPayload,
|
|
5
|
+
parseTicketDoorQrPayload,
|
|
5
6
|
} from "../sideQuestTypes.js";
|
|
6
7
|
|
|
7
8
|
describe("sideQuestTypes deep links", () => {
|
|
@@ -38,7 +39,47 @@ describe("sideQuestTypes deep links", () => {
|
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
|
|
42
|
+
it("builds and parses checkpoint deep links", () => {
|
|
43
|
+
const url = buildSideQuestDeepLink({
|
|
44
|
+
bashEventId: "evt1",
|
|
45
|
+
questId: "q1",
|
|
46
|
+
code: "ab12",
|
|
47
|
+
checkpointId: "cp9",
|
|
48
|
+
frontendOrigin: "https://bash.community",
|
|
49
|
+
});
|
|
50
|
+
expect(url).toContain("checkpoint=cp9");
|
|
51
|
+
expect(parseSideQuestScanPayload(url)).toEqual({
|
|
52
|
+
bashEventId: "evt1",
|
|
53
|
+
questId: "q1",
|
|
54
|
+
checkpointId: "cp9",
|
|
55
|
+
code: "AB12",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
41
59
|
it("parses plain codes", () => {
|
|
42
60
|
expect(parseSideQuestScanPayload("ab12")).toEqual({ code: "AB12" });
|
|
43
61
|
});
|
|
62
|
+
|
|
63
|
+
it("parses ticket door QR JSON for Connect meets", () => {
|
|
64
|
+
expect(
|
|
65
|
+
parseTicketDoorQrPayload(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
eventId: "evt-1",
|
|
68
|
+
userId: "user-2",
|
|
69
|
+
ticketId: "tix-9",
|
|
70
|
+
qrToken: "tok",
|
|
71
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
).toEqual({
|
|
75
|
+
eventId: "evt-1",
|
|
76
|
+
userId: "user-2",
|
|
77
|
+
ticketId: "tix-9",
|
|
78
|
+
qrToken: "tok",
|
|
79
|
+
});
|
|
80
|
+
expect(parseTicketDoorQrPayload("not-json")).toBeNull();
|
|
81
|
+
expect(
|
|
82
|
+
parseTicketDoorQrPayload(JSON.stringify({ eventId: "e", userId: "u" }))
|
|
83
|
+
).toBeNull();
|
|
84
|
+
});
|
|
44
85
|
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guest-facing cancellation policy presets for ticketed bash events
|
|
3
|
+
* (Eventbrite-style windows). Stored as the human-readable `displayText` in
|
|
4
|
+
* BashEvent.cancellationPolicy; refund eligibility is resolved from the matching
|
|
5
|
+
* named preset (or host-review for custom free text).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { hoursUntilEventStart } from "./utils/service/cancellationPolicyRefundResolver.js";
|
|
9
|
+
|
|
10
|
+
export const BASH_EVENT_CANCELLATION_PRESET_IDS = [
|
|
11
|
+
"anytime_until_start",
|
|
12
|
+
"flexible_1_day",
|
|
13
|
+
"flexible_7_day",
|
|
14
|
+
"flexible_30_day",
|
|
15
|
+
"no_refunds",
|
|
16
|
+
"custom",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export type BashEventCancellationPresetId =
|
|
20
|
+
(typeof BASH_EVENT_CANCELLATION_PRESET_IDS)[number];
|
|
21
|
+
|
|
22
|
+
export type BashEventCancellationNamedPresetId = Exclude<
|
|
23
|
+
BashEventCancellationPresetId,
|
|
24
|
+
"custom"
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
/** Default for new bashes and empty policy fields. */
|
|
28
|
+
export const DEFAULT_BASH_EVENT_CANCELLATION_PRESET_ID: BashEventCancellationNamedPresetId =
|
|
29
|
+
"flexible_7_day";
|
|
30
|
+
|
|
31
|
+
export type BashEventCancellationPreset = {
|
|
32
|
+
id: BashEventCancellationNamedPresetId;
|
|
33
|
+
name: string;
|
|
34
|
+
/** Short line under the name in the host picker. */
|
|
35
|
+
summary: string;
|
|
36
|
+
/** Exact string persisted to BashEvent.cancellationPolicy and shown to guests. */
|
|
37
|
+
displayText: string;
|
|
38
|
+
/**
|
|
39
|
+
* Hours before start required for a full auto guest refund.
|
|
40
|
+
* - `0` = anytime until the event starts (`hoursUntil > 0`)
|
|
41
|
+
* - `null` = no guest auto-refunds (`no_refunds`)
|
|
42
|
+
*/
|
|
43
|
+
fullRefundHoursBeforeStart: number | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Ordered most guest-friendly → strictest (default is still Standard). */
|
|
47
|
+
export const BASH_EVENT_CANCELLATION_PRESETS: readonly BashEventCancellationPreset[] =
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
id: "anytime_until_start",
|
|
51
|
+
name: "Anytime",
|
|
52
|
+
summary: "Full refund until the event starts.",
|
|
53
|
+
displayText:
|
|
54
|
+
"Full refund if you cancel anytime before the event starts. No refunds after it begins.",
|
|
55
|
+
fullRefundHoursBeforeStart: 0,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "flexible_1_day",
|
|
59
|
+
name: "Flexible — 24 hours",
|
|
60
|
+
summary: "Full refund until 24 hours before start.",
|
|
61
|
+
displayText:
|
|
62
|
+
"Full refund if you cancel at least 24 hours before the event starts. No refunds after that.",
|
|
63
|
+
fullRefundHoursBeforeStart: 24,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "flexible_7_day",
|
|
67
|
+
name: "Standard — 7 days",
|
|
68
|
+
summary: "Full refund until 7 days before start.",
|
|
69
|
+
displayText:
|
|
70
|
+
"Full refund if you cancel at least 7 days before the event starts. No refunds after that.",
|
|
71
|
+
fullRefundHoursBeforeStart: 168,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "flexible_30_day",
|
|
75
|
+
name: "Strict — 30 days",
|
|
76
|
+
summary: "Full refund until 30 days before start.",
|
|
77
|
+
displayText:
|
|
78
|
+
"Full refund if you cancel at least 30 days before the event starts. No refunds after that.",
|
|
79
|
+
fullRefundHoursBeforeStart: 720,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "no_refunds",
|
|
83
|
+
name: "No guest refunds",
|
|
84
|
+
summary: "Tickets are final sale for guest cancellations.",
|
|
85
|
+
displayText:
|
|
86
|
+
"No refunds for guest cancellations. If the host cancels the event, refunds follow the host’s guarantee settings.",
|
|
87
|
+
fullRefundHoursBeforeStart: null,
|
|
88
|
+
},
|
|
89
|
+
] as const;
|
|
90
|
+
|
|
91
|
+
const PRESET_BY_DISPLAY = new Map(
|
|
92
|
+
BASH_EVENT_CANCELLATION_PRESETS.map((p) => [p.displayText, p.id] as const)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const PRESET_BY_ID = new Map(
|
|
96
|
+
BASH_EVENT_CANCELLATION_PRESETS.map((p) => [p.id, p] as const)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
/** Map a stored policy string to a preset id (or custom / null if empty). */
|
|
100
|
+
export function resolveBashEventCancellationPresetId(
|
|
101
|
+
policy: string | null | undefined
|
|
102
|
+
): BashEventCancellationPresetId | null {
|
|
103
|
+
const trimmed = policy?.trim() ?? "";
|
|
104
|
+
if (!trimmed) return null;
|
|
105
|
+
return PRESET_BY_DISPLAY.get(trimmed) ?? "custom";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getBashEventCancellationPreset(
|
|
109
|
+
id: BashEventCancellationNamedPresetId
|
|
110
|
+
): BashEventCancellationPreset {
|
|
111
|
+
const preset = PRESET_BY_ID.get(id);
|
|
112
|
+
if (!preset) {
|
|
113
|
+
throw new Error(`Unknown bash cancellation preset: ${id}`);
|
|
114
|
+
}
|
|
115
|
+
return preset;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function bashEventCancellationPresetDisplayText(
|
|
119
|
+
id: BashEventCancellationNamedPresetId
|
|
120
|
+
): string {
|
|
121
|
+
return getBashEventCancellationPreset(id).displayText;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function defaultBashEventCancellationPolicyDisplayText(): string {
|
|
125
|
+
return bashEventCancellationPresetDisplayText(
|
|
126
|
+
DEFAULT_BASH_EVENT_CANCELLATION_PRESET_ID
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Short label for bash detail / meta UI. Named presets use the host-picker name
|
|
132
|
+
* (e.g. "Anytime"); custom/legacy free text returns the stored string.
|
|
133
|
+
*/
|
|
134
|
+
export function bashEventCancellationPolicyDetailLabel(
|
|
135
|
+
policy: string | null | undefined
|
|
136
|
+
): string | null {
|
|
137
|
+
const trimmed = policy?.trim() ?? "";
|
|
138
|
+
if (!trimmed) return null;
|
|
139
|
+
const id = resolveBashEventCancellationPresetId(trimmed);
|
|
140
|
+
if (id && id !== "custom") {
|
|
141
|
+
return getBashEventCancellationPreset(id).name;
|
|
142
|
+
}
|
|
143
|
+
return trimmed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type BashEventGuestRefundMode = "auto" | "host_review" | "none";
|
|
147
|
+
|
|
148
|
+
export type BashEventGuestRefundEligibility = {
|
|
149
|
+
mode: BashEventGuestRefundMode;
|
|
150
|
+
/** True when the guest may take the mode's money path right now. */
|
|
151
|
+
eligibleNow: boolean;
|
|
152
|
+
presetId: BashEventCancellationPresetId | null;
|
|
153
|
+
fullRefundHoursBeforeStart: number | null;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export type ResolveBashEventGuestRefundOptions = {
|
|
157
|
+
eventStart: Date | string | null | undefined;
|
|
158
|
+
now?: Date;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function parseEventStart(
|
|
162
|
+
eventStart: Date | string | null | undefined
|
|
163
|
+
): Date | null {
|
|
164
|
+
if (eventStart == null) return null;
|
|
165
|
+
const d = eventStart instanceof Date ? eventStart : new Date(eventStart);
|
|
166
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
167
|
+
return d;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolves guest self-serve refund path from stored cancellation policy text.
|
|
172
|
+
* - Named presets (except no_refunds): `auto` when still inside the window
|
|
173
|
+
* - `no_refunds`: `none`
|
|
174
|
+
* - Custom / unknown free text: `host_review` (host must approve)
|
|
175
|
+
* - Empty / missing policy: treated as default Standard 7-day preset
|
|
176
|
+
*/
|
|
177
|
+
export function resolveBashEventGuestRefundEligibility(
|
|
178
|
+
policy: string | null | undefined,
|
|
179
|
+
options: ResolveBashEventGuestRefundOptions
|
|
180
|
+
): BashEventGuestRefundEligibility {
|
|
181
|
+
const now = options.now ?? new Date();
|
|
182
|
+
const eventStart = parseEventStart(options.eventStart);
|
|
183
|
+
const hoursUntil =
|
|
184
|
+
eventStart != null ? hoursUntilEventStart(eventStart, now) : 0;
|
|
185
|
+
|
|
186
|
+
const trimmed = policy?.trim() ?? "";
|
|
187
|
+
const resolvedId = trimmed
|
|
188
|
+
? resolveBashEventCancellationPresetId(trimmed)
|
|
189
|
+
: DEFAULT_BASH_EVENT_CANCELLATION_PRESET_ID;
|
|
190
|
+
|
|
191
|
+
if (resolvedId === "custom") {
|
|
192
|
+
return {
|
|
193
|
+
mode: "host_review",
|
|
194
|
+
eligibleNow: true,
|
|
195
|
+
presetId: "custom",
|
|
196
|
+
fullRefundHoursBeforeStart: null,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (resolvedId == null) {
|
|
201
|
+
return {
|
|
202
|
+
mode: "none",
|
|
203
|
+
eligibleNow: false,
|
|
204
|
+
presetId: null,
|
|
205
|
+
fullRefundHoursBeforeStart: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const preset = getBashEventCancellationPreset(resolvedId);
|
|
210
|
+
const threshold = preset.fullRefundHoursBeforeStart;
|
|
211
|
+
|
|
212
|
+
if (threshold == null) {
|
|
213
|
+
return {
|
|
214
|
+
mode: "none",
|
|
215
|
+
eligibleNow: false,
|
|
216
|
+
presetId: resolvedId,
|
|
217
|
+
fullRefundHoursBeforeStart: null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// threshold 0 = anytime until start; otherwise need hoursUntil >= threshold
|
|
222
|
+
const eligibleNow =
|
|
223
|
+
hoursUntil > 0 && (threshold === 0 || hoursUntil >= threshold);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
mode: "auto",
|
|
227
|
+
eligibleNow,
|
|
228
|
+
presetId: resolvedId,
|
|
229
|
+
fullRefundHoursBeforeStart: threshold,
|
|
230
|
+
};
|
|
231
|
+
}
|
package/src/definitions.ts
CHANGED
|
@@ -836,6 +836,11 @@ export interface ResetPassword {
|
|
|
836
836
|
|
|
837
837
|
export interface ApiResult<T, P extends ErrorDataType = ErrorDataType> {
|
|
838
838
|
data?: T;
|
|
839
|
+
/**
|
|
840
|
+
* Endpoint-specific extras that are not the primary `data` payload
|
|
841
|
+
* (e.g. guest roster `depth`, ticket `refundMode`).
|
|
842
|
+
*/
|
|
843
|
+
meta?: Record<string, unknown>;
|
|
839
844
|
/** PATCH group member / some RSVP endpoints — true when BashPoints RSVP bonus was newly awarded */
|
|
840
845
|
rsvpBonusAwarded?: boolean;
|
|
841
846
|
/** POST event task assign — true when volunteer guestlist invite was created */
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from "./hostCrmInsights.js";
|
|
|
35
35
|
export * from "./hostCrmAutomation.js";
|
|
36
36
|
export * from "./relationshipHealth.js";
|
|
37
37
|
export * from "./hostCrmConstants.js";
|
|
38
|
+
export * from "./bashEventCancellationPolicy.js";
|
|
38
39
|
export * from "./userReportTypes.js";
|
|
39
40
|
export * from "./mirroredPrismaEnums.js";
|
|
40
41
|
export * from "./utils/featuredDiscoveryGeo.js";
|
package/src/sideQuestIdeas.ts
CHANGED
|
@@ -23,7 +23,9 @@ export type SideQuestTypeId =
|
|
|
23
23
|
| "Vote"
|
|
24
24
|
| "Sponsor"
|
|
25
25
|
| "Social"
|
|
26
|
-
| "Trivia"
|
|
26
|
+
| "Trivia"
|
|
27
|
+
| "Circuit"
|
|
28
|
+
| "Connect";
|
|
27
29
|
|
|
28
30
|
export const SIDE_QUEST_CATEGORY_META: Record<
|
|
29
31
|
SideQuestCategoryId,
|
|
@@ -39,8 +41,14 @@ export const SIDE_QUEST_CATEGORY_META: Record<
|
|
|
39
41
|
GiveBack: { label: "Give Back", emoji: "❤️" },
|
|
40
42
|
};
|
|
41
43
|
|
|
42
|
-
/**
|
|
43
|
-
export const SIDE_QUEST_PHASE1_TYPES: SideQuestTypeId[] = [
|
|
44
|
+
/** Host-selectable quest types (single-shot + Circuit/Connect) */
|
|
45
|
+
export const SIDE_QUEST_PHASE1_TYPES: SideQuestTypeId[] = [
|
|
46
|
+
"Qr",
|
|
47
|
+
"Code",
|
|
48
|
+
"Vote",
|
|
49
|
+
"Circuit",
|
|
50
|
+
"Connect",
|
|
51
|
+
];
|
|
44
52
|
|
|
45
53
|
export const SIDE_QUEST_TYPE_META: Record<
|
|
46
54
|
SideQuestTypeId,
|
|
@@ -96,6 +104,17 @@ export const SIDE_QUEST_TYPE_META: Record<
|
|
|
96
104
|
description: "Answer a question correctly.",
|
|
97
105
|
phase1: false,
|
|
98
106
|
},
|
|
107
|
+
Circuit: {
|
|
108
|
+
label: "Vendor circuit",
|
|
109
|
+
description: "Guest scans QRs at N booths — progress ticks toward done.",
|
|
110
|
+
phase1: true,
|
|
111
|
+
},
|
|
112
|
+
Connect: {
|
|
113
|
+
label: "Icebreaker Connect",
|
|
114
|
+
description:
|
|
115
|
+
"Meet N people by scanning their ticket QR after the icebreaker.",
|
|
116
|
+
phase1: true,
|
|
117
|
+
},
|
|
99
118
|
};
|
|
100
119
|
|
|
101
120
|
export interface SideQuestIdea {
|