@bash-app/bash-common 30.260.0 → 30.262.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 (82) hide show
  1. package/dist/__tests__/ticketBnplPaymentMethods.cashapp.test.d.ts +2 -0
  2. package/dist/__tests__/ticketBnplPaymentMethods.cashapp.test.d.ts.map +1 -0
  3. package/dist/__tests__/ticketBnplPaymentMethods.cashapp.test.js +33 -0
  4. package/dist/__tests__/ticketBnplPaymentMethods.cashapp.test.js.map +1 -0
  5. package/dist/__tests__/ticketBnplPaymentMethods.test.d.ts +2 -0
  6. package/dist/__tests__/ticketBnplPaymentMethods.test.d.ts.map +1 -0
  7. package/dist/__tests__/ticketBnplPaymentMethods.test.js +46 -0
  8. package/dist/__tests__/ticketBnplPaymentMethods.test.js.map +1 -0
  9. package/dist/definitions.d.ts +6 -1
  10. package/dist/definitions.d.ts.map +1 -1
  11. package/dist/definitions.js.map +1 -1
  12. package/dist/index.d.ts +6 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +6 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/ticketBnplPaymentMethods.d.ts +32 -0
  17. package/dist/ticketBnplPaymentMethods.d.ts.map +1 -0
  18. package/dist/ticketBnplPaymentMethods.js +42 -0
  19. package/dist/ticketBnplPaymentMethods.js.map +1 -0
  20. package/dist/utils/__tests__/featuredDiscoveryGeo.test.d.ts +2 -0
  21. package/dist/utils/__tests__/featuredDiscoveryGeo.test.d.ts.map +1 -0
  22. package/dist/utils/__tests__/featuredDiscoveryGeo.test.js +98 -0
  23. package/dist/utils/__tests__/featuredDiscoveryGeo.test.js.map +1 -0
  24. package/dist/utils/__tests__/flyerUtils.test.js +7 -1
  25. package/dist/utils/__tests__/flyerUtils.test.js.map +1 -1
  26. package/dist/utils/__tests__/guestSurface.test.d.ts +2 -0
  27. package/dist/utils/__tests__/guestSurface.test.d.ts.map +1 -0
  28. package/dist/utils/__tests__/guestSurface.test.js +94 -0
  29. package/dist/utils/__tests__/guestSurface.test.js.map +1 -0
  30. package/dist/utils/__tests__/ticketTierSchedule.test.d.ts +2 -0
  31. package/dist/utils/__tests__/ticketTierSchedule.test.d.ts.map +1 -0
  32. package/dist/utils/__tests__/ticketTierSchedule.test.js +84 -0
  33. package/dist/utils/__tests__/ticketTierSchedule.test.js.map +1 -0
  34. package/dist/utils/featuredDiscoveryGeo.d.ts +30 -0
  35. package/dist/utils/featuredDiscoveryGeo.d.ts.map +1 -0
  36. package/dist/utils/featuredDiscoveryGeo.js +47 -0
  37. package/dist/utils/featuredDiscoveryGeo.js.map +1 -0
  38. package/dist/utils/flyerUtils.d.ts +5 -0
  39. package/dist/utils/flyerUtils.d.ts.map +1 -1
  40. package/dist/utils/flyerUtils.js +24 -0
  41. package/dist/utils/flyerUtils.js.map +1 -1
  42. package/dist/utils/guestSurface.d.ts +114 -0
  43. package/dist/utils/guestSurface.d.ts.map +1 -0
  44. package/dist/utils/guestSurface.js +164 -0
  45. package/dist/utils/guestSurface.js.map +1 -0
  46. package/dist/utils/promotionBlastUtils.d.ts +38 -0
  47. package/dist/utils/promotionBlastUtils.d.ts.map +1 -0
  48. package/dist/utils/promotionBlastUtils.js +88 -0
  49. package/dist/utils/promotionBlastUtils.js.map +1 -0
  50. package/dist/utils/service/__tests__/defaultOfferAmountUtils.test.d.ts +2 -0
  51. package/dist/utils/service/__tests__/defaultOfferAmountUtils.test.d.ts.map +1 -0
  52. package/dist/utils/service/__tests__/defaultOfferAmountUtils.test.js +65 -0
  53. package/dist/utils/service/__tests__/defaultOfferAmountUtils.test.js.map +1 -0
  54. package/dist/utils/service/defaultOfferAmountUtils.d.ts +32 -0
  55. package/dist/utils/service/defaultOfferAmountUtils.d.ts.map +1 -0
  56. package/dist/utils/service/defaultOfferAmountUtils.js +115 -0
  57. package/dist/utils/service/defaultOfferAmountUtils.js.map +1 -0
  58. package/dist/utils/service/serviceBookingExposureUtils.js +1 -1
  59. package/dist/utils/service/serviceBookingExposureUtils.js.map +1 -1
  60. package/dist/utils/ticketTierSchedule.d.ts +58 -0
  61. package/dist/utils/ticketTierSchedule.d.ts.map +1 -0
  62. package/dist/utils/ticketTierSchedule.js +279 -0
  63. package/dist/utils/ticketTierSchedule.js.map +1 -0
  64. package/package.json +5 -2
  65. package/prisma/schema.prisma +332 -1
  66. package/src/__tests__/ticketBnplPaymentMethods.cashapp.test.ts +45 -0
  67. package/src/__tests__/ticketBnplPaymentMethods.test.ts +66 -0
  68. package/src/definitions.ts +6 -1
  69. package/src/index.ts +6 -0
  70. package/src/ticketBnplPaymentMethods.ts +79 -0
  71. package/src/utils/__tests__/featuredDiscoveryGeo.test.ts +120 -0
  72. package/src/utils/__tests__/flyerUtils.test.ts +10 -0
  73. package/src/utils/__tests__/guestSurface.test.ts +128 -0
  74. package/src/utils/__tests__/ticketTierSchedule.test.ts +131 -0
  75. package/src/utils/featuredDiscoveryGeo.ts +75 -0
  76. package/src/utils/flyerUtils.ts +33 -0
  77. package/src/utils/guestSurface.ts +315 -0
  78. package/src/utils/promotionBlastUtils.ts +127 -0
  79. package/src/utils/service/__tests__/defaultOfferAmountUtils.test.ts +102 -0
  80. package/src/utils/service/defaultOfferAmountUtils.ts +170 -0
  81. package/src/utils/service/serviceBookingExposureUtils.ts +1 -1
  82. package/src/utils/ticketTierSchedule.ts +387 -0
@@ -19,7 +19,7 @@
19
19
  import {
20
20
  DEFAULT_MAX_BILLABLE_MULTIPLIER,
21
21
  isFlatPricingModel,
22
- } from "./billingCapUtils";
22
+ } from "./billingCapUtils.js";
23
23
 
24
24
  /**
25
25
  * Pricing-model strings recognized by the exposure util. Stays a string
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Ticket tier release / expiry / sold-out schedule resolver — shared by API + bash-app.
3
+ */
4
+
5
+ import type { TicketTierAvailability } from "../definitions.js";
6
+
7
+ export type PreReleaseDisplay = "Tease" | "Hidden";
8
+
9
+ export type PresaleAudience = "Public" | "Ambashador" | "BashInsider" | "Promoters";
10
+
11
+ export type TicketTierSchedulePhase =
12
+ | "HostHidden"
13
+ | "HiddenPreRelease"
14
+ | "TeasePreRelease"
15
+ | "Live"
16
+ | "Expired"
17
+ | "SoldOut";
18
+
19
+ export type TicketTierScheduleInput = {
20
+ availabilityType?: string | null;
21
+ availableAt?: Date | string | null;
22
+ expiresAt?: Date | string | null;
23
+ preReleaseDisplay?: string | null;
24
+ hidden?: boolean | null;
25
+ isManuallySoldOut?: boolean | null;
26
+ isWaitlistTier?: boolean | null;
27
+ availabilityTypeUnlock?: string | null;
28
+ availableWhenInterestReached?: number | null;
29
+ unlockWhenTicketsSold?: number | null;
30
+ presaleAudience?: string | null;
31
+ publicAvailableAt?: Date | string | null;
32
+ };
33
+
34
+ export type TicketTierScheduleContext = {
35
+ now?: Date;
36
+ availableInventory?: number;
37
+ ideaInterestCount?: number;
38
+ ticketsSoldCount?: number;
39
+ isViewerEngaged?: boolean;
40
+ isViewerAmbashador?: boolean;
41
+ isViewerBashInsider?: boolean;
42
+ isViewerPromoterForEvent?: boolean;
43
+ };
44
+
45
+ export type TicketTierScheduleState = {
46
+ phase: TicketTierSchedulePhase;
47
+ availableAt: string | null;
48
+ expiresAt: string | null;
49
+ canPurchase: boolean;
50
+ soldOut: boolean;
51
+ showWaitlistCta: boolean;
52
+ guestVisible: boolean;
53
+ availability: TicketTierAvailability;
54
+ hostPreviewLabel: string;
55
+ };
56
+
57
+ export type ScheduleValidationIssue = {
58
+ field: string;
59
+ message: string;
60
+ severity: "error" | "warn";
61
+ };
62
+
63
+ function toDate(value: Date | string | null | undefined): Date | null {
64
+ if (value == null) return null;
65
+ const d = value instanceof Date ? value : new Date(value);
66
+ return Number.isNaN(d.getTime()) ? null : d;
67
+ }
68
+
69
+ function toIso(value: Date | string | null | undefined): string | null {
70
+ const d = toDate(value);
71
+ return d ? d.toISOString() : null;
72
+ }
73
+
74
+ function normalizePreReleaseDisplay(value: string | null | undefined): PreReleaseDisplay {
75
+ return value === "Hidden" ? "Hidden" : "Tease";
76
+ }
77
+
78
+ function isOnDate(type: string | null | undefined): boolean {
79
+ return type === "OnDate";
80
+ }
81
+
82
+ function isPresaleAudienceEligible(
83
+ audience: string | null | undefined,
84
+ ctx: TicketTierScheduleContext
85
+ ): boolean {
86
+ switch (audience) {
87
+ case "Ambashador":
88
+ return ctx.isViewerAmbashador === true;
89
+ case "BashInsider":
90
+ return ctx.isViewerBashInsider === true;
91
+ case "Promoters":
92
+ return ctx.isViewerPromoterForEvent === true;
93
+ default:
94
+ return true;
95
+ }
96
+ }
97
+
98
+ function resolveUnlockAvailability(
99
+ tier: TicketTierScheduleInput,
100
+ ctx: TicketTierScheduleContext
101
+ ): TicketTierAvailability | null {
102
+ const type = tier.availabilityType ?? "Immediate";
103
+ if (type === "UnlockWhenInterestReached") {
104
+ const threshold = tier.availableWhenInterestReached ?? 0;
105
+ const count = ctx.ideaInterestCount ?? 0;
106
+ const available = count >= threshold;
107
+ return {
108
+ available,
109
+ reason: "UnlockWhenInterestReached",
110
+ interestCount: count,
111
+ interestThreshold: threshold,
112
+ };
113
+ }
114
+ if (type === "UnlockWhenTicketsSold") {
115
+ const threshold = tier.unlockWhenTicketsSold ?? 0;
116
+ const count = ctx.ticketsSoldCount ?? 0;
117
+ const available = count >= threshold;
118
+ return {
119
+ available,
120
+ reason: "UnlockWhenTicketsSold",
121
+ ticketsSoldCount: count,
122
+ ticketsSoldThreshold: threshold,
123
+ };
124
+ }
125
+ return null;
126
+ }
127
+
128
+ /**
129
+ * Resolve guest-visible phase and purchase eligibility for a ticket tier.
130
+ */
131
+ export function resolveTicketTierScheduleState(
132
+ tier: TicketTierScheduleInput,
133
+ ctx: TicketTierScheduleContext = {}
134
+ ): TicketTierScheduleState {
135
+ const now = ctx.now ?? new Date();
136
+ const availableAt = toDate(tier.availableAt);
137
+ const expiresAt = toDate(tier.expiresAt);
138
+ const publicAvailableAt = toDate(tier.publicAvailableAt);
139
+ const inventory = ctx.availableInventory;
140
+ const manualSoldOut = tier.isManuallySoldOut === true;
141
+ /** Only treat as inventory sold-out when callers supply a count — unknown ≠ zero. */
142
+ const soldOut =
143
+ manualSoldOut || (inventory !== undefined && inventory <= 0);
144
+ const preDisplay = normalizePreReleaseDisplay(tier.preReleaseDisplay);
145
+ const type = tier.availabilityType ?? "Immediate";
146
+
147
+ if (tier.hidden) {
148
+ return buildState({
149
+ phase: "HostHidden",
150
+ availableAt,
151
+ expiresAt,
152
+ canPurchase: false,
153
+ soldOut,
154
+ isWaitlistTier: tier.isWaitlistTier,
155
+ availability: { available: false },
156
+ hostPreviewLabel: "Hidden by host",
157
+ });
158
+ }
159
+
160
+ if (expiresAt && now >= expiresAt) {
161
+ return buildState({
162
+ phase: "Expired",
163
+ availableAt,
164
+ expiresAt,
165
+ canPurchase: false,
166
+ soldOut,
167
+ isWaitlistTier: tier.isWaitlistTier,
168
+ availability: { available: false },
169
+ hostPreviewLabel: "Expired",
170
+ });
171
+ }
172
+
173
+ const unlockAvail = resolveUnlockAvailability(tier, ctx);
174
+ if (unlockAvail && !unlockAvail.available) {
175
+ return buildState({
176
+ phase: type === "OnDate" && availableAt && now < availableAt
177
+ ? preDisplay === "Hidden" ? "HiddenPreRelease" : "TeasePreRelease"
178
+ : "TeasePreRelease",
179
+ availableAt,
180
+ expiresAt,
181
+ canPurchase: false,
182
+ soldOut,
183
+ isWaitlistTier: tier.isWaitlistTier,
184
+ availability: unlockAvail,
185
+ hostPreviewLabel: "Unlock pending",
186
+ });
187
+ }
188
+
189
+ if (isOnDate(type) && availableAt && now < availableAt) {
190
+ const phase: TicketTierSchedulePhase =
191
+ preDisplay === "Hidden" ? "HiddenPreRelease" : "TeasePreRelease";
192
+ return buildState({
193
+ phase,
194
+ availableAt,
195
+ expiresAt,
196
+ canPurchase: false,
197
+ soldOut,
198
+ isWaitlistTier: tier.isWaitlistTier,
199
+ availability: {
200
+ available: false,
201
+ reason: "OnDate",
202
+ availableAt: availableAt.toISOString(),
203
+ },
204
+ hostPreviewLabel: phase === "HiddenPreRelease" ? "Hidden until release" : "Tease + countdown",
205
+ });
206
+ }
207
+
208
+ const presaleAudience = tier.presaleAudience ?? "Public";
209
+ if (presaleAudience !== "Public") {
210
+ const publicOpensAt = publicAvailableAt ?? availableAt;
211
+ const presaleOpen = !availableAt || now >= availableAt;
212
+ const publicOpen = !publicOpensAt || now >= publicOpensAt;
213
+ const eligible = isPresaleAudienceEligible(presaleAudience, ctx);
214
+ if (!publicOpen && (!presaleOpen || !eligible)) {
215
+ return buildState({
216
+ phase: presaleOpen && !publicOpen ? "TeasePreRelease" : "HiddenPreRelease",
217
+ availableAt: publicOpensAt ?? availableAt,
218
+ expiresAt,
219
+ canPurchase: false,
220
+ soldOut,
221
+ isWaitlistTier: tier.isWaitlistTier,
222
+ availability: {
223
+ available: false,
224
+ reason: "OnDate",
225
+ availableAt: (publicOpensAt ?? availableAt)?.toISOString(),
226
+ },
227
+ hostPreviewLabel: `${presaleAudience} presale`,
228
+ });
229
+ }
230
+ if (presaleOpen && !publicOpen && !eligible) {
231
+ return buildState({
232
+ phase: "TeasePreRelease",
233
+ availableAt: publicOpensAt,
234
+ expiresAt,
235
+ canPurchase: false,
236
+ soldOut,
237
+ isWaitlistTier: tier.isWaitlistTier,
238
+ availability: {
239
+ available: false,
240
+ reason: "OnDate",
241
+ availableAt: publicOpensAt?.toISOString(),
242
+ },
243
+ hostPreviewLabel: "Public sale scheduled",
244
+ });
245
+ }
246
+ }
247
+
248
+ if (type === "EngagedUsersOnly" && ctx.isViewerEngaged === false) {
249
+ return buildState({
250
+ phase: soldOut ? "SoldOut" : "Live",
251
+ availableAt,
252
+ expiresAt,
253
+ canPurchase: false,
254
+ soldOut,
255
+ isWaitlistTier: tier.isWaitlistTier,
256
+ availability: {
257
+ available: false,
258
+ reason: "EngagedUsersOnly",
259
+ isViewerEngaged: false,
260
+ },
261
+ hostPreviewLabel: soldOut ? "Sold out" : "RSVP required",
262
+ });
263
+ }
264
+
265
+ if (soldOut) {
266
+ return buildState({
267
+ phase: "SoldOut",
268
+ availableAt,
269
+ expiresAt,
270
+ canPurchase: false,
271
+ soldOut: true,
272
+ isWaitlistTier: tier.isWaitlistTier,
273
+ availability: { available: false },
274
+ hostPreviewLabel: manualSoldOut ? "Sold out (manual)" : "Sold out",
275
+ });
276
+ }
277
+
278
+ const availability: TicketTierAvailability = unlockAvail ?? { available: true };
279
+ if (type === "EngagedUsersOnly") {
280
+ availability.reason = "EngagedUsersOnly";
281
+ availability.isViewerEngaged = ctx.isViewerEngaged;
282
+ }
283
+
284
+ return buildState({
285
+ phase: "Live",
286
+ availableAt,
287
+ expiresAt,
288
+ canPurchase: true,
289
+ soldOut: false,
290
+ isWaitlistTier: tier.isWaitlistTier,
291
+ availability,
292
+ hostPreviewLabel: "Live",
293
+ });
294
+ }
295
+
296
+ function buildState(args: {
297
+ phase: TicketTierSchedulePhase;
298
+ availableAt: Date | null;
299
+ expiresAt: Date | null;
300
+ canPurchase: boolean;
301
+ soldOut: boolean;
302
+ isWaitlistTier?: boolean | null;
303
+ availability: TicketTierAvailability;
304
+ hostPreviewLabel: string;
305
+ }): TicketTierScheduleState {
306
+ const guestVisible =
307
+ args.phase !== "HostHidden" &&
308
+ args.phase !== "HiddenPreRelease" &&
309
+ args.phase !== "Expired";
310
+ return {
311
+ phase: args.phase,
312
+ availableAt: toIso(args.availableAt),
313
+ expiresAt: toIso(args.expiresAt),
314
+ canPurchase: args.canPurchase,
315
+ soldOut: args.soldOut,
316
+ showWaitlistCta: args.soldOut && args.isWaitlistTier === true,
317
+ guestVisible,
318
+ availability: args.availability,
319
+ hostPreviewLabel: args.hostPreviewLabel,
320
+ };
321
+ }
322
+
323
+ export function validateTicketTierScheduleInput(
324
+ tier: TicketTierScheduleInput,
325
+ bashEvent?: { startDateTime?: Date | string | null }
326
+ ): ScheduleValidationIssue[] {
327
+ const issues: ScheduleValidationIssue[] = [];
328
+ const type = tier.availabilityType ?? "Immediate";
329
+ const availableAt = toDate(tier.availableAt);
330
+ const expiresAt = toDate(tier.expiresAt);
331
+ const start = bashEvent ? toDate(bashEvent.startDateTime) : null;
332
+
333
+ if (isOnDate(type) && !availableAt) {
334
+ issues.push({
335
+ field: "availableAt",
336
+ message: "Release date and time are required when scheduling a tier.",
337
+ severity: "error",
338
+ });
339
+ }
340
+
341
+ if (availableAt && expiresAt && expiresAt <= availableAt) {
342
+ issues.push({
343
+ field: "expiresAt",
344
+ message: "Expires must be after the release time.",
345
+ severity: "error",
346
+ });
347
+ }
348
+
349
+ if (expiresAt && expiresAt <= new Date()) {
350
+ issues.push({
351
+ field: "expiresAt",
352
+ message: "Expires must be in the future.",
353
+ severity: "error",
354
+ });
355
+ }
356
+
357
+ if (start && availableAt && availableAt > start) {
358
+ issues.push({
359
+ field: "availableAt",
360
+ message: "Release is after the event start — double-check the time.",
361
+ severity: "warn",
362
+ });
363
+ }
364
+
365
+ if (start && expiresAt && expiresAt > start) {
366
+ issues.push({
367
+ field: "expiresAt",
368
+ message: "Tier expires after the event starts — unusual but allowed.",
369
+ severity: "warn",
370
+ });
371
+ }
372
+
373
+ return issues;
374
+ }
375
+
376
+ export function scheduleValidationHasErrors(issues: ScheduleValidationIssue[]): boolean {
377
+ return issues.some((i) => i.severity === "error");
378
+ }
379
+
380
+ /** Whether tier should appear on guest detail ticket list (incl. tease). */
381
+ export function isTicketTierGuestListVisible(state: TicketTierScheduleState): boolean {
382
+ return (
383
+ state.guestVisible ||
384
+ state.phase === "TeasePreRelease" ||
385
+ state.phase === "SoldOut"
386
+ );
387
+ }