@bash-app/bash-common 30.307.0 → 30.309.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 (42) hide show
  1. package/dist/__tests__/guestPartySize.test.d.ts +2 -0
  2. package/dist/__tests__/guestPartySize.test.d.ts.map +1 -0
  3. package/dist/__tests__/guestPartySize.test.js +22 -0
  4. package/dist/__tests__/guestPartySize.test.js.map +1 -0
  5. package/dist/__tests__/sideQuestIdeas.test.d.ts +2 -0
  6. package/dist/__tests__/sideQuestIdeas.test.d.ts.map +1 -0
  7. package/dist/__tests__/sideQuestIdeas.test.js +13 -0
  8. package/dist/__tests__/sideQuestIdeas.test.js.map +1 -0
  9. package/dist/__tests__/sideQuestTypes.test.d.ts +2 -0
  10. package/dist/__tests__/sideQuestTypes.test.d.ts.map +1 -0
  11. package/dist/__tests__/sideQuestTypes.test.js +29 -0
  12. package/dist/__tests__/sideQuestTypes.test.js.map +1 -0
  13. package/dist/guestPartySize.d.ts +18 -0
  14. package/dist/guestPartySize.d.ts.map +1 -0
  15. package/dist/guestPartySize.js +72 -0
  16. package/dist/guestPartySize.js.map +1 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/mirroredPrismaEnums.d.ts +3 -0
  22. package/dist/mirroredPrismaEnums.d.ts.map +1 -1
  23. package/dist/mirroredPrismaEnums.js +3 -0
  24. package/dist/mirroredPrismaEnums.js.map +1 -1
  25. package/dist/sideQuestIdeas.d.ts +51 -0
  26. package/dist/sideQuestIdeas.d.ts.map +1 -0
  27. package/dist/sideQuestIdeas.js +215 -0
  28. package/dist/sideQuestIdeas.js.map +1 -0
  29. package/dist/sideQuestTypes.d.ts +115 -0
  30. package/dist/sideQuestTypes.d.ts.map +1 -0
  31. package/dist/sideQuestTypes.js +67 -0
  32. package/dist/sideQuestTypes.js.map +1 -0
  33. package/package.json +1 -1
  34. package/prisma/schema.prisma +206 -0
  35. package/src/__tests__/guestPartySize.test.ts +31 -0
  36. package/src/__tests__/sideQuestIdeas.test.ts +16 -0
  37. package/src/__tests__/sideQuestTypes.test.ts +44 -0
  38. package/src/guestPartySize.ts +83 -0
  39. package/src/index.ts +3 -0
  40. package/src/mirroredPrismaEnums.ts +3 -0
  41. package/src/sideQuestIdeas.ts +260 -0
  42. package/src/sideQuestTypes.ts +189 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Shared Side Quest / Adventures API shapes and QR deep-link helpers.
3
+ */
4
+ import type { SideQuestCategoryId, SideQuestTypeId } from "./sideQuestIdeas.js";
5
+ export interface EventBadgeExt {
6
+ id: string;
7
+ bashEventId: string;
8
+ name: string;
9
+ description: string | null;
10
+ imageUrl: string;
11
+ createdByUserId?: string;
12
+ }
13
+ export interface SideQuestExt {
14
+ id: string;
15
+ title: string;
16
+ description: string | null;
17
+ sortOrder: number;
18
+ category: SideQuestCategoryId | string;
19
+ questType: SideQuestTypeId | string;
20
+ pointsReward: number;
21
+ raffleEntriesOnComplete: number;
22
+ eventBadgeId: string | null;
23
+ eventBadge?: Pick<EventBadgeExt, "id" | "name" | "imageUrl" | "description"> | null;
24
+ startsAt: string | Date | null;
25
+ endsAt: string | Date | null;
26
+ maxCompletions: number | null;
27
+ isHidden: boolean;
28
+ isLocked?: boolean;
29
+ unlockedAt: string | Date | null;
30
+ sponsorLabel: string | null;
31
+ completed?: boolean;
32
+ code?: string;
33
+ completionCount?: number;
34
+ }
35
+ export interface SideQuestMilestoneExt {
36
+ id: string;
37
+ requiredCompletions: number;
38
+ bashPointsReward: number | null;
39
+ eventBadgeId: string | null;
40
+ raffleEntries: number | null;
41
+ unlockSideQuestId: string | null;
42
+ sortOrder: number;
43
+ }
44
+ export interface CommunityQuestGoalExt {
45
+ id?: string;
46
+ targetCompletions: number;
47
+ currentCompletions?: number;
48
+ rewardLabel: string;
49
+ eventBadgeId?: string | null;
50
+ bashPointsReward?: number | null;
51
+ unlockedAt?: string | Date | null;
52
+ }
53
+ export interface SideQuestLogExt {
54
+ quests: SideQuestExt[];
55
+ availableCount: number;
56
+ completedCount: number;
57
+ hiddenCount: number;
58
+ badgesEarned: EventBadgeExt[];
59
+ eventBadges: EventBadgeExt[];
60
+ raffleEntries: number;
61
+ pointsFromQuests: number;
62
+ community: CommunityQuestGoalExt | null;
63
+ }
64
+ export interface SideQuestHostBundleExt {
65
+ quests: SideQuestExt[];
66
+ milestones: SideQuestMilestoneExt[];
67
+ community: CommunityQuestGoalExt | null;
68
+ badges: EventBadgeExt[];
69
+ }
70
+ export interface SideQuestAnalyticsExt {
71
+ uniqueCompleters: number;
72
+ totalCompletions: number;
73
+ raffleEntriesTotal: number;
74
+ community: CommunityQuestGoalExt | null;
75
+ perQuest: Array<{
76
+ questId: string;
77
+ title: string;
78
+ sponsorLabel: string | null;
79
+ completionCount: number;
80
+ }>;
81
+ sponsorRollup: Array<{
82
+ sponsorLabel: string;
83
+ completionCount: number;
84
+ }>;
85
+ recentCompletions: Array<{
86
+ completionId: string;
87
+ questId: string;
88
+ questTitle: string;
89
+ userId: string;
90
+ displayName: string;
91
+ completedAt: string | Date;
92
+ }>;
93
+ }
94
+ /** Absolute or path-only HTTPS deep link for booth QR codes. */
95
+ export declare function buildSideQuestDeepLink(params: {
96
+ bashEventId: string;
97
+ questId: string;
98
+ code: string;
99
+ /** Origin without trailing slash, e.g. https://bash.community */
100
+ frontendOrigin?: string;
101
+ slug?: string | null;
102
+ }): string;
103
+ /** Legacy opaque payload (still accepted when pasted). */
104
+ export declare function buildSideQuestLegacyPayload(bashEventId: string, questId: string, code: string): string;
105
+ export type ParsedSideQuestScan = {
106
+ bashEventId?: string;
107
+ questId?: string;
108
+ code: string;
109
+ };
110
+ /**
111
+ * Parse a scanned/pasted value into quest id + code.
112
+ * Supports HTTPS ticket-details URLs and legacy `bash-side-quest:` payloads.
113
+ */
114
+ export declare function parseSideQuestScanPayload(raw: string): ParsedSideQuestScan | null;
115
+ //# sourceMappingURL=sideQuestTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sideQuestTypes.d.ts","sourceRoot":"","sources":["../src/sideQuestTypes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shared Side Quest / Adventures API shapes and QR deep-link helpers.
3
+ */
4
+ import { TICKET_DETAILS } from "./definitions.js";
5
+ const LEGACY_PREFIX = "bash-side-quest:";
6
+ /** Absolute or path-only HTTPS deep link for booth QR codes. */
7
+ export function buildSideQuestDeepLink(params) {
8
+ const code = params.code.trim().toUpperCase().replace(/\s+/g, "");
9
+ const qs = new URLSearchParams({
10
+ quest: params.questId,
11
+ code,
12
+ });
13
+ const path = `${TICKET_DETAILS}/${params.bashEventId}?${qs.toString()}`;
14
+ const origin = (params.frontendOrigin ?? "").replace(/\/$/, "");
15
+ return origin ? `${origin}${path}` : path;
16
+ }
17
+ /** Legacy opaque payload (still accepted when pasted). */
18
+ export function buildSideQuestLegacyPayload(bashEventId, questId, code) {
19
+ const normalized = code.trim().toUpperCase().replace(/\s+/g, "");
20
+ return `${LEGACY_PREFIX}${bashEventId}:${questId}:${normalized}`;
21
+ }
22
+ /**
23
+ * Parse a scanned/pasted value into quest id + code.
24
+ * Supports HTTPS ticket-details URLs and legacy `bash-side-quest:` payloads.
25
+ */
26
+ export function parseSideQuestScanPayload(raw) {
27
+ const trimmed = raw.trim();
28
+ if (!trimmed)
29
+ return null;
30
+ if (trimmed.startsWith(LEGACY_PREFIX)) {
31
+ const rest = trimmed.slice(LEGACY_PREFIX.length);
32
+ const parts = rest.split(":");
33
+ if (parts.length >= 3) {
34
+ const [bashEventId, questId, ...codeParts] = parts;
35
+ const code = codeParts.join(":").trim().toUpperCase();
36
+ if (code)
37
+ return { bashEventId, questId, code };
38
+ }
39
+ return null;
40
+ }
41
+ try {
42
+ const url = trimmed.includes("://")
43
+ ? new URL(trimmed)
44
+ : new URL(trimmed, "https://bash.community");
45
+ const questId = url.searchParams.get("quest") ?? undefined;
46
+ const codeParam = url.searchParams.get("code");
47
+ if (codeParam) {
48
+ return {
49
+ questId: questId || undefined,
50
+ code: codeParam.trim().toUpperCase().replace(/\s+/g, ""),
51
+ bashEventId: url.pathname.includes("/ticket-details/")
52
+ ? url.pathname.split("/ticket-details/")[1]?.split(/[/?#]/)[0]
53
+ : undefined,
54
+ };
55
+ }
56
+ }
57
+ catch {
58
+ // not a URL — treat as raw code
59
+ }
60
+ // Plain code (4–6 chars)
61
+ const plain = trimmed.toUpperCase().replace(/\s+/g, "");
62
+ if (/^[A-Z0-9]{4,8}$/.test(plain)) {
63
+ return { code: plain };
64
+ }
65
+ return null;
66
+ }
67
+ //# sourceMappingURL=sideQuestTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sideQuestTypes.js","sourceRoot":"","sources":["../src/sideQuestTypes.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash-app/bash-common",
3
- "version": "30.307.0",
3
+ "version": "30.309.0",
4
4
  "description": "Common data and scripts to use on the frontend and backend",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -842,6 +842,9 @@ model BashEvent {
842
842
  smsConfirmationsEnabled Boolean @default(false)
843
843
  /// Collect named holder per seat at checkout.
844
844
  namedTicketsEnabled Boolean @default(false)
845
+ /// Max companions a guest may bring (0 = alone, 1 = +1, 2 = +2, null = unlimited).
846
+ /// Applies to RSVP plus-ones and ticketed seats-per-buyer. Null preserves open defaults.
847
+ maxAdditionalGuests Int?
845
848
  attendeeDisplayMode String @default("All") // "All" | "CountOnly" | "NetworkOnly" | "Hidden"
846
849
  /// Optional headline above featured guests on bash detail (curated guest visibility).
847
850
  featuredGuestsHeadline String?
@@ -1038,6 +1041,13 @@ model BashEvent {
1038
1041
  discussionComments EventDiscussionComment[]
1039
1042
  bashEventDonations BashEventDonation[]
1040
1043
 
1044
+ // Adventures / Side Quests
1045
+ sideQuests SideQuest[]
1046
+ eventBadges EventBadge[]
1047
+ sideQuestMilestones SideQuestMilestone[]
1048
+ sideQuestPrizeEntries SideQuestPrizeEntry[]
1049
+ communityQuestGoal CommunityQuestGoal?
1050
+
1041
1051
  // User night itineraries (events in user's "My Night" plan)
1042
1052
  userItineraryItems UserItineraryItem[]
1043
1053
 
@@ -1981,6 +1991,192 @@ model EventReferralReward {
1981
1991
  @@index([bashEventId])
1982
1992
  }
1983
1993
 
1994
+ // --- Adventures / Side Quests (behavior design engine) ---
1995
+
1996
+ enum SideQuestCategory {
1997
+ Explore
1998
+ Connect
1999
+ Discover
2000
+ SupportLocal
2001
+ Compete
2002
+ Transit
2003
+ Create
2004
+ GiveBack
2005
+ }
2006
+
2007
+ /// How the guest proves completion. Phase 1: Qr + Code. Others reserved.
2008
+ enum SideQuestType {
2009
+ Qr
2010
+ Code
2011
+ Gps
2012
+ Photo
2013
+ CheckIn
2014
+ Purchase
2015
+ Vote
2016
+ Sponsor
2017
+ Social
2018
+ Trivia
2019
+ }
2020
+
2021
+ enum EventBadgeAwardSource {
2022
+ SideQuest
2023
+ Milestone
2024
+ Manual
2025
+ Community
2026
+ }
2027
+
2028
+ model SideQuest {
2029
+ id String @id @default(cuid())
2030
+ bashEventId String
2031
+ title String
2032
+ description String?
2033
+ sortOrder Int @default(0)
2034
+ category SideQuestCategory @default(Explore)
2035
+ questType SideQuestType @default(Code)
2036
+ /// Short display code for hosts/guests (e.g. 4-digit). Guests submit this; never treat as the only secret.
2037
+ code String
2038
+ /// SHA-256 hex of normalized code — server verification
2039
+ codeHash String
2040
+ pointsReward Int @default(0)
2041
+ raffleEntriesOnComplete Int @default(0)
2042
+ eventBadgeId String?
2043
+ startsAt DateTime?
2044
+ endsAt DateTime?
2045
+ maxCompletions Int?
2046
+ isHidden Boolean @default(false)
2047
+ unlockedAt DateTime?
2048
+ sponsorLabel String?
2049
+ sponsorServiceId String?
2050
+ createdAt DateTime @default(now())
2051
+ updatedAt DateTime @updatedAt
2052
+
2053
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2054
+ eventBadge EventBadge? @relation(fields: [eventBadgeId], references: [id], onDelete: SetNull)
2055
+ completions SideQuestCompletion[]
2056
+ milestonesUnlocking SideQuestMilestone[] @relation("MilestoneUnlocksQuest")
2057
+
2058
+ @@index([bashEventId])
2059
+ @@index([bashEventId, sortOrder])
2060
+ @@index([eventBadgeId])
2061
+ }
2062
+
2063
+ model SideQuestCompletion {
2064
+ id String @id @default(cuid())
2065
+ sideQuestId String
2066
+ userId String
2067
+ completedAt DateTime @default(now())
2068
+ questType SideQuestType
2069
+
2070
+ sideQuest SideQuest @relation(fields: [sideQuestId], references: [id], onDelete: Cascade)
2071
+ user User @relation("SideQuestCompletions", fields: [userId], references: [id], onDelete: Cascade)
2072
+
2073
+ @@unique([sideQuestId, userId])
2074
+ @@index([userId])
2075
+ @@index([sideQuestId])
2076
+ }
2077
+
2078
+ model EventBadge {
2079
+ id String @id @default(cuid())
2080
+ bashEventId String
2081
+ name String
2082
+ description String?
2083
+ imageUrl String
2084
+ createdByUserId String
2085
+ createdAt DateTime @default(now())
2086
+ updatedAt DateTime @updatedAt
2087
+
2088
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2089
+ createdBy User @relation("EventBadgesCreated", fields: [createdByUserId], references: [id], onDelete: Cascade)
2090
+ awards EventBadgeAward[]
2091
+ sideQuests SideQuest[]
2092
+ milestones SideQuestMilestone[]
2093
+ communityGoals CommunityQuestGoal[]
2094
+
2095
+ @@index([bashEventId])
2096
+ @@index([createdByUserId])
2097
+ }
2098
+
2099
+ model EventBadgeAward {
2100
+ id String @id @default(cuid())
2101
+ eventBadgeId String
2102
+ userId String
2103
+ awardedAt DateTime @default(now())
2104
+ sourceType EventBadgeAwardSource
2105
+ sourceId String?
2106
+
2107
+ eventBadge EventBadge @relation(fields: [eventBadgeId], references: [id], onDelete: Cascade)
2108
+ user User @relation("EventBadgeAwards", fields: [userId], references: [id], onDelete: Cascade)
2109
+
2110
+ @@unique([eventBadgeId, userId])
2111
+ @@index([userId])
2112
+ }
2113
+
2114
+ model SideQuestMilestone {
2115
+ id String @id @default(cuid())
2116
+ bashEventId String
2117
+ requiredCompletions Int
2118
+ bashPointsReward Int?
2119
+ eventBadgeId String?
2120
+ raffleEntries Int?
2121
+ unlockSideQuestId String?
2122
+ sortOrder Int @default(0)
2123
+ createdAt DateTime @default(now())
2124
+ updatedAt DateTime @updatedAt
2125
+
2126
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2127
+ eventBadge EventBadge? @relation(fields: [eventBadgeId], references: [id], onDelete: SetNull)
2128
+ unlockSideQuest SideQuest? @relation("MilestoneUnlocksQuest", fields: [unlockSideQuestId], references: [id], onDelete: SetNull)
2129
+ claims SideQuestMilestoneClaim[]
2130
+
2131
+ @@index([bashEventId])
2132
+ }
2133
+
2134
+ model SideQuestMilestoneClaim {
2135
+ id String @id @default(cuid())
2136
+ milestoneId String
2137
+ userId String
2138
+ claimedAt DateTime @default(now())
2139
+
2140
+ milestone SideQuestMilestone @relation(fields: [milestoneId], references: [id], onDelete: Cascade)
2141
+ user User @relation("SideQuestMilestoneClaims", fields: [userId], references: [id], onDelete: Cascade)
2142
+
2143
+ @@unique([milestoneId, userId])
2144
+ @@index([userId])
2145
+ }
2146
+
2147
+ model SideQuestPrizeEntry {
2148
+ id String @id @default(cuid())
2149
+ bashEventId String
2150
+ userId String
2151
+ entries Int @default(1)
2152
+ sourceCompletionId String?
2153
+ sourceMilestoneId String?
2154
+ sourceType String @default("SideQuest")
2155
+ createdAt DateTime @default(now())
2156
+
2157
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2158
+ user User @relation("SideQuestPrizeEntries", fields: [userId], references: [id], onDelete: Cascade)
2159
+
2160
+ @@index([bashEventId, userId])
2161
+ @@index([userId])
2162
+ }
2163
+
2164
+ model CommunityQuestGoal {
2165
+ id String @id @default(cuid())
2166
+ bashEventId String @unique
2167
+ targetCompletions Int
2168
+ currentCompletions Int @default(0)
2169
+ rewardLabel String
2170
+ eventBadgeId String?
2171
+ bashPointsReward Int?
2172
+ unlockedAt DateTime?
2173
+ createdAt DateTime @default(now())
2174
+ updatedAt DateTime @updatedAt
2175
+
2176
+ bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
2177
+ eventBadge EventBadge? @relation(fields: [eventBadgeId], references: [id], onDelete: SetNull)
2178
+ }
2179
+
1984
2180
  enum BackgroundEffect {
1985
2181
  None
1986
2182
  Confetti
@@ -3414,6 +3610,11 @@ model User {
3414
3610
  competitionVotes CompetitionVote[]
3415
3611
  competitionJudgeAssignments CompetitionJudgeAssignment[] @relation("CompetitionJudgeAssignments")
3416
3612
  competitionJudgeScoresAuthored CompetitionJudgeScore[] @relation("CompetitionJudgeScoresAuthored")
3613
+ sideQuestCompletions SideQuestCompletion[] @relation("SideQuestCompletions")
3614
+ eventBadgesCreated EventBadge[] @relation("EventBadgesCreated")
3615
+ eventBadgeAwards EventBadgeAward[] @relation("EventBadgeAwards")
3616
+ sideQuestMilestoneClaims SideQuestMilestoneClaim[] @relation("SideQuestMilestoneClaims")
3617
+ sideQuestPrizeEntries SideQuestPrizeEntry[] @relation("SideQuestPrizeEntries")
3417
3618
  contacts Contact[]
3418
3619
  contactLists ContactList[] @relation("UserContactLists")
3419
3620
  contactlist ContactList[]
@@ -6613,6 +6814,8 @@ enum NotificationType {
6613
6814
  FeaturedGuestInviteAccepted // Host: guest accepted featured guest invite
6614
6815
  FeaturedGuestInviteDeclined // Host: guest declined featured guest invite
6615
6816
  PartnerPromotionBlast // Attendee: partner on-site promo deal at an event
6817
+ SideQuestRevealed // Attendee: host revealed a secret Side Quest
6818
+ CommunityQuestUnlocked // Attendee: community quest goal unlocked
6616
6819
  }
6617
6820
 
6618
6821
  /// Opt-in reminder for a scheduled tier release (v1.5).
@@ -9050,6 +9253,9 @@ enum CreditSourceType {
9050
9253
  ReferralCodeUsed // Flat bonus for referrer when code drives a ticket/signup
9051
9254
  VenueLoyaltyRedemption // BashPoints moved through a VenueLoyaltyRedemption claim/redeem/refund
9052
9255
  PartnerPromoRedemption // BashPoints spent to unlock a local partner promo code
9256
+ SideQuestCompletion // Completing a Side Quest on a bash
9257
+ SideQuestMilestone // Hitting a Side Quest milestone (e.g. complete N quests)
9258
+ CommunityQuestUnlock // Digital reward when a Community Quest goal unlocks
9053
9259
  }
9054
9260
 
9055
9261
  enum ReferralTier {
@@ -0,0 +1,31 @@
1
+ import {
2
+ resolveMaxPlusOnes,
3
+ resolveMaxSeatsPerBuyer,
4
+ rsvpPartyPolicyGuestCopy,
5
+ seatsForTicketQuantity,
6
+ ticketPartyPolicyGuestCopy,
7
+ } from "../guestPartySize.js";
8
+
9
+ describe("guestPartySize", () => {
10
+ it("resolveMaxSeatsPerBuyer", () => {
11
+ expect(resolveMaxSeatsPerBuyer(0)).toBe(1);
12
+ expect(resolveMaxSeatsPerBuyer(1)).toBe(2);
13
+ expect(resolveMaxSeatsPerBuyer(null)).toBeNull();
14
+ expect(resolveMaxSeatsPerBuyer(2, 1)).toBe(1);
15
+ });
16
+
17
+ it("resolveMaxPlusOnes soft-caps unlimited", () => {
18
+ expect(resolveMaxPlusOnes(null)).toBe(10);
19
+ expect(resolveMaxPlusOnes(0)).toBe(0);
20
+ });
21
+
22
+ it("seatsForTicketQuantity expands group packages", () => {
23
+ expect(seatsForTicketQuantity(1, 6, true)).toBe(6);
24
+ });
25
+
26
+ it("guest copy helpers", () => {
27
+ expect(rsvpPartyPolicyGuestCopy(0)).toMatch(/no plus-ones/i);
28
+ expect(ticketPartyPolicyGuestCopy(1)).toBe("Up to 2 tickets per person");
29
+ expect(ticketPartyPolicyGuestCopy(null)).toBeNull();
30
+ });
31
+ });
@@ -0,0 +1,16 @@
1
+ import { filterSideQuestIdeas, SIDE_QUEST_IDEAS } from "../sideQuestIdeas";
2
+
3
+ describe("sideQuestIdeas", () => {
4
+ it("includes festival and market templates", () => {
5
+ expect(SIDE_QUEST_IDEAS.some((i) => i.tags.includes("festival"))).toBe(
6
+ true
7
+ );
8
+ expect(SIDE_QUEST_IDEAS.some((i) => i.tags.includes("market"))).toBe(true);
9
+ });
10
+
11
+ it("filters by search", () => {
12
+ const hits = filterSideQuestIdeas("transit");
13
+ expect(hits.length).toBeGreaterThan(0);
14
+ expect(hits.every((h) => JSON.stringify(h).toLowerCase().includes("transit") || h.category === "Transit")).toBe(true);
15
+ });
16
+ });
@@ -0,0 +1,44 @@
1
+ import {
2
+ buildSideQuestDeepLink,
3
+ buildSideQuestLegacyPayload,
4
+ parseSideQuestScanPayload,
5
+ } from "../sideQuestTypes.js";
6
+
7
+ describe("sideQuestTypes deep links", () => {
8
+ it("builds HTTPS ticket-details URLs", () => {
9
+ expect(
10
+ buildSideQuestDeepLink({
11
+ bashEventId: "evt1",
12
+ questId: "q1",
13
+ code: "ab12",
14
+ frontendOrigin: "https://bash.community",
15
+ })
16
+ ).toBe("https://bash.community/ticket-details/evt1?quest=q1&code=AB12");
17
+ });
18
+
19
+ it("parses HTTPS deep links", () => {
20
+ expect(
21
+ parseSideQuestScanPayload(
22
+ "https://bash.community/ticket-details/evt1?quest=q1&code=XY99"
23
+ )
24
+ ).toEqual({
25
+ bashEventId: "evt1",
26
+ questId: "q1",
27
+ code: "XY99",
28
+ });
29
+ });
30
+
31
+ it("parses legacy bash-side-quest payloads", () => {
32
+ expect(
33
+ parseSideQuestScanPayload(buildSideQuestLegacyPayload("evt1", "q1", "ab12"))
34
+ ).toEqual({
35
+ bashEventId: "evt1",
36
+ questId: "q1",
37
+ code: "AB12",
38
+ });
39
+ });
40
+
41
+ it("parses plain codes", () => {
42
+ expect(parseSideQuestScanPayload("ab12")).toEqual({ code: "AB12" });
43
+ });
44
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Event-level guest party size (plus-ones / seats-per-buyer).
3
+ * maxAdditionalGuests: 0 = alone, 1 = +1, 2 = +2, null = unlimited.
4
+ */
5
+
6
+ export const RSVP_PLUS_ONES_UI_SOFT_CAP = 10;
7
+ export const RSVP_PLUS_ONES_API_HARD_CAP = 20;
8
+
9
+ /** Max seats one buyer may hold (owned + cart). null = unlimited. */
10
+ export function resolveMaxSeatsPerBuyer(
11
+ maxAdditionalGuests: number | null | undefined,
12
+ tierMaxTicketsPerUser?: number | null
13
+ ): number | null {
14
+ let eventCap: number | null = null;
15
+ if (maxAdditionalGuests != null && Number.isFinite(maxAdditionalGuests)) {
16
+ eventCap = Math.max(0, Math.floor(maxAdditionalGuests)) + 1;
17
+ }
18
+ const tierCap =
19
+ tierMaxTicketsPerUser != null && Number.isFinite(tierMaxTicketsPerUser)
20
+ ? Math.max(0, Math.floor(tierMaxTicketsPerUser))
21
+ : null;
22
+
23
+ if (eventCap == null && tierCap == null) return null;
24
+ if (eventCap == null) return tierCap;
25
+ if (tierCap == null) return eventCap;
26
+ return Math.min(eventCap, tierCap);
27
+ }
28
+
29
+ /** Max plus-ones for RSVP UI/API. */
30
+ export function resolveMaxPlusOnes(
31
+ maxAdditionalGuests: number | null | undefined,
32
+ softCap = RSVP_PLUS_ONES_UI_SOFT_CAP
33
+ ): number {
34
+ if (maxAdditionalGuests == null) return softCap;
35
+ return Math.max(0, Math.min(softCap, Math.floor(maxAdditionalGuests)));
36
+ }
37
+
38
+ export function seatsForTicketQuantity(
39
+ quantity: number,
40
+ groupTicketSeatCount?: number | null,
41
+ isGroupTicket?: boolean | null
42
+ ): number {
43
+ const qty = Math.max(0, Math.floor(quantity));
44
+ if (isGroupTicket && groupTicketSeatCount && groupTicketSeatCount > 0) {
45
+ return qty * groupTicketSeatCount;
46
+ }
47
+ return qty;
48
+ }
49
+
50
+ export function guestPartyPolicyLabel(
51
+ maxAdditionalGuests: number | null | undefined
52
+ ): string {
53
+ if (maxAdditionalGuests === 0) return "No plus-ones";
54
+ if (maxAdditionalGuests === 1) return "Plus one";
55
+ if (maxAdditionalGuests === 2) return "Plus two";
56
+ return "Unlimited";
57
+ }
58
+
59
+ /** Guest-facing RSVP copy. */
60
+ export function rsvpPartyPolicyGuestCopy(
61
+ maxAdditionalGuests: number | null | undefined
62
+ ): string {
63
+ if (maxAdditionalGuests === 0) return "Just you — no plus-ones";
64
+ if (maxAdditionalGuests === 1) return "You can bring up to 1 guest";
65
+ if (maxAdditionalGuests === 2) return "You can bring up to 2 guests";
66
+ return `You can bring up to ${RSVP_PLUS_ONES_UI_SOFT_CAP} guests`;
67
+ }
68
+
69
+ /** Guest-facing ticketed copy. */
70
+ export function ticketPartyPolicyGuestCopy(
71
+ maxAdditionalGuests: number | null | undefined
72
+ ): string | null {
73
+ if (maxAdditionalGuests == null) return null;
74
+ const seats = maxAdditionalGuests + 1;
75
+ if (seats === 1) return "1 ticket per person";
76
+ return `Up to ${seats} tickets per person`;
77
+ }
78
+
79
+ export function nearCapacityPartyError(spotsLeft: number): string {
80
+ if (spotsLeft <= 0) return "This bash is at capacity.";
81
+ if (spotsLeft === 1) return "Only 1 spot left — you can’t bring that many guests.";
82
+ return `Only ${spotsLeft} spots left — you can’t bring that many guests.`;
83
+ }
package/src/index.ts CHANGED
@@ -7,9 +7,12 @@ export * from "./venueLoyaltyRedemption.js";
7
7
  export * from "./stripeListingSubscriptionMessages.js";
8
8
  export * from "./extendedSchemas.js";
9
9
  export * from "./competitionIdeas.js";
10
+ export * from "./sideQuestIdeas.js";
11
+ export * from "./sideQuestTypes.js";
10
12
  export * from "./icebreakerPrompts.js";
11
13
  export * from "./competitionValidation.js";
12
14
  export * from "./groupTicketUtils.js";
15
+ export * from "./guestPartySize.js";
13
16
  export * from "./utils/groupTicketCartUtils.js";
14
17
  export * from "./partnerStoreTypes.js";
15
18
  export * from "./storeTypes.js";
@@ -177,6 +177,9 @@ export const CreditSourceType = {
177
177
  ReferralCodeUsed: "ReferralCodeUsed",
178
178
  VenueLoyaltyRedemption: "VenueLoyaltyRedemption",
179
179
  PartnerPromoRedemption: "PartnerPromoRedemption",
180
+ SideQuestCompletion: "SideQuestCompletion",
181
+ SideQuestMilestone: "SideQuestMilestone",
182
+ CommunityQuestUnlock: "CommunityQuestUnlock",
180
183
  } as const satisfies Record<CreditSourceTypeEnum, CreditSourceTypeEnum>;
181
184
 
182
185
  export type CreditSourceType = CreditSourceTypeEnum;