@bash-app/bash-common 30.274.0 → 30.276.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 (65) hide show
  1. package/dist/__tests__/groupTicketUtils.test.d.ts +2 -0
  2. package/dist/__tests__/groupTicketUtils.test.d.ts.map +1 -0
  3. package/dist/__tests__/groupTicketUtils.test.js +38 -0
  4. package/dist/__tests__/groupTicketUtils.test.js.map +1 -0
  5. package/dist/competitionIdeas.d.ts +19 -0
  6. package/dist/competitionIdeas.d.ts.map +1 -0
  7. package/dist/competitionIdeas.js +405 -0
  8. package/dist/competitionIdeas.js.map +1 -0
  9. package/dist/competitionValidation.d.ts +10 -0
  10. package/dist/competitionValidation.d.ts.map +1 -0
  11. package/dist/competitionValidation.js +29 -0
  12. package/dist/competitionValidation.js.map +1 -0
  13. package/dist/definitions.d.ts +4 -0
  14. package/dist/definitions.d.ts.map +1 -1
  15. package/dist/definitions.js.map +1 -1
  16. package/dist/extendedSchemas.d.ts +351 -1
  17. package/dist/extendedSchemas.d.ts.map +1 -1
  18. package/dist/extendedSchemas.js +15 -0
  19. package/dist/extendedSchemas.js.map +1 -1
  20. package/dist/groupTicketUtils.d.ts +19 -0
  21. package/dist/groupTicketUtils.d.ts.map +1 -0
  22. package/dist/groupTicketUtils.js +74 -0
  23. package/dist/groupTicketUtils.js.map +1 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +4 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/utils/__tests__/guestSurface.test.js +13 -1
  29. package/dist/utils/__tests__/guestSurface.test.js.map +1 -1
  30. package/dist/utils/__tests__/paymentUtils.test.js +18 -0
  31. package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
  32. package/dist/utils/__tests__/ticketListUtils.test.js +10 -0
  33. package/dist/utils/__tests__/ticketListUtils.test.js.map +1 -1
  34. package/dist/utils/groupTicketCartUtils.d.ts +12 -0
  35. package/dist/utils/groupTicketCartUtils.d.ts.map +1 -0
  36. package/dist/utils/groupTicketCartUtils.js +30 -0
  37. package/dist/utils/groupTicketCartUtils.js.map +1 -0
  38. package/dist/utils/guestSurface.d.ts +9 -0
  39. package/dist/utils/guestSurface.d.ts.map +1 -1
  40. package/dist/utils/guestSurface.js +8 -0
  41. package/dist/utils/guestSurface.js.map +1 -1
  42. package/dist/utils/paymentUtils.d.ts +10 -0
  43. package/dist/utils/paymentUtils.d.ts.map +1 -1
  44. package/dist/utils/paymentUtils.js +37 -1
  45. package/dist/utils/paymentUtils.js.map +1 -1
  46. package/dist/utils/ticketListUtils.d.ts.map +1 -1
  47. package/dist/utils/ticketListUtils.js +15 -3
  48. package/dist/utils/ticketListUtils.js.map +1 -1
  49. package/package.json +1 -1
  50. package/prisma/migrations/competition-prize-catalog-entry-tier.sql +39 -0
  51. package/prisma/schema.prisma +92 -7
  52. package/src/__tests__/groupTicketUtils.test.ts +53 -0
  53. package/src/competitionIdeas.ts +429 -0
  54. package/src/competitionValidation.ts +42 -0
  55. package/src/definitions.ts +5 -0
  56. package/src/extendedSchemas.ts +47 -0
  57. package/src/groupTicketUtils.ts +101 -0
  58. package/src/index.ts +4 -0
  59. package/src/utils/__tests__/guestSurface.test.ts +21 -0
  60. package/src/utils/__tests__/paymentUtils.test.ts +19 -0
  61. package/src/utils/__tests__/ticketListUtils.test.ts +12 -0
  62. package/src/utils/groupTicketCartUtils.ts +36 -0
  63. package/src/utils/guestSurface.ts +17 -0
  64. package/src/utils/paymentUtils.ts +42 -1
  65. package/src/utils/ticketListUtils.ts +19 -5
@@ -2,12 +2,33 @@ import {
2
2
  capRosterDepthForEventMode,
3
3
  filterHiddenVisibilityUserIds,
4
4
  orderPreviewAttendeeIds,
5
+ resolveAttendeeListAccess,
5
6
  resolveEffectiveEventMode,
6
7
  resolveGuestSurfacePolicy,
7
8
  viewerMayFetchFullRoster,
8
9
  maxGuestListDepth,
9
10
  } from "../guestSurface.js";
10
11
 
12
+ describe("resolveAttendeeListAccess", () => {
13
+ it("prefers attendeeListAccess enum", () => {
14
+ expect(
15
+ resolveAttendeeListAccess({
16
+ attendeeListAccess: "TicketHolders",
17
+ attendeeListPublic: true,
18
+ })
19
+ ).toBe("TicketHolders");
20
+ });
21
+
22
+ it("falls back to legacy attendeeListPublic boolean", () => {
23
+ expect(resolveAttendeeListAccess({ attendeeListPublic: true })).toBe(
24
+ "Public"
25
+ );
26
+ expect(resolveAttendeeListAccess({ attendeeListPublic: false })).toBe(
27
+ "Preview"
28
+ );
29
+ });
30
+ });
31
+
11
32
  describe("resolveEffectiveEventMode", () => {
12
33
  it("maps legacy showAttendees false to Hidden", () => {
13
34
  expect(resolveEffectiveEventMode({ showAttendees: false })).toBe("Hidden");
@@ -289,6 +289,25 @@ describe("PaymentUtils - Core Functions", () => {
289
289
  test("calculateTotalPriceWithoutTax returns 0 for empty list", () => {
290
290
  expect(calculateTotalPriceWithoutTax([], new Map())).toBe(0);
291
291
  });
292
+
293
+ test("calculateTotalPriceWithoutTax uses unitPriceCents for pay-what-you-can tiers", () => {
294
+ const tiers = [
295
+ { id: "t1", price: 2000, pricingType: "USD", isDonationTier: true },
296
+ ] as any;
297
+ const ticketList = new Map([
298
+ [
299
+ "t1",
300
+ [
301
+ {
302
+ numberOfTickets: 2,
303
+ ticketDateTime: new Date(),
304
+ unitPriceCents: 5000,
305
+ },
306
+ ],
307
+ ],
308
+ ]);
309
+ expect(calculateTotalPriceWithoutTax(tiers, ticketList)).toBe(10000);
310
+ });
292
311
  });
293
312
 
294
313
  describe("Ticket total BashPoints", () => {
@@ -29,6 +29,18 @@ describe("ticketListUtils", () => {
29
29
  expect(decoded.get("tier-xyz")![0].numberOfTickets).toBe(3);
30
30
  });
31
31
 
32
+ test("round-trips unitPriceCents for pay-what-you-can tiers", () => {
33
+ const ticketList = new Map<string, NumberOfTicketsForDate[]>([
34
+ [
35
+ "tier-pwyc",
36
+ [{ numberOfTickets: 1, ticketDateTime: isoDate, unitPriceCents: 7500 }],
37
+ ],
38
+ ]);
39
+
40
+ const decoded = ticketListStrToTicketList(ticketListToString(ticketList));
41
+ expect(decoded.get("tier-pwyc")![0].unitPriceCents).toBe(7500);
42
+ });
43
+
32
44
  test("omits tiers with zero tickets", () => {
33
45
  const ticketList = new Map<string, NumberOfTicketsForDate[]>([
34
46
  ["tier-empty", [{ numberOfTickets: 0, ticketDateTime: isoDate }]],
@@ -0,0 +1,36 @@
1
+ import type { TicketTier } from "@prisma/client";
2
+
3
+ import type { NumberOfTicketsForDate } from "../definitions.js";
4
+ import { isGroupTicketTier } from "../groupTicketUtils.js";
5
+
6
+ /**
7
+ * Validates cart quantities for group package tiers: qty = packages, not seats.
8
+ */
9
+ export function validateGroupTicketCartForTiers(
10
+ ticketTiers: TicketTier[],
11
+ ticketList: Map<string, NumberOfTicketsForDate[]>
12
+ ): { ok: true } | { ok: false; message: string } {
13
+ for (const tier of ticketTiers) {
14
+ if (!isGroupTicketTier(tier)) {
15
+ continue;
16
+ }
17
+ const rows = ticketList.get(tier.id);
18
+ if (!rows?.length) {
19
+ continue;
20
+ }
21
+ const packageQty = rows.reduce((s, r) => s + (r.numberOfTickets ?? 0), 0);
22
+ if (packageQty < 1) {
23
+ return {
24
+ ok: false,
25
+ message: `${tier.title} is a group package — select one package to buy.`,
26
+ };
27
+ }
28
+ if (packageQty > 1) {
29
+ return {
30
+ ok: false,
31
+ message: `${tier.title} is one package per checkout. Buy one table/package at a time.`,
32
+ };
33
+ }
34
+ }
35
+ return { ok: true };
36
+ }
@@ -19,6 +19,9 @@ export type AttendeeVisibility =
19
19
 
20
20
  export type GuestListDepth = "None" | "Partial" | "Full";
21
21
 
22
+ /** Who may see the full attendee roster on the bash detail page (Layer 1). */
23
+ export type AttendeeListAccess = "Preview" | "Public" | "TicketHolders";
24
+
22
25
  export type { PublicTeaserUser };
23
26
 
24
27
  export type NetworkOverlapPayload = {
@@ -60,6 +63,8 @@ export type GuestSurfacePayload = {
60
63
  attendeeVisibilityByUserId?: Record<string, AttendeeVisibility>;
61
64
  /** Event has public guest list enabled on bash detail */
62
65
  attendeeListPublic?: boolean;
66
+ /** Preview | Public | TicketHolders — supersedes attendeeListPublic when set */
67
+ attendeeListAccess?: string | null;
63
68
  };
64
69
 
65
70
  export type AttendeeRosterMeta = {
@@ -98,6 +103,18 @@ const DEPTH_RANK: Record<GuestListDepth, number> = {
98
103
  Full: 2,
99
104
  };
100
105
 
106
+ /** Legacy boolean + new enum; prefer attendeeListAccess when present. */
107
+ export function resolveAttendeeListAccess(event: {
108
+ attendeeListAccess?: string | null;
109
+ attendeeListPublic?: boolean | null;
110
+ }): AttendeeListAccess {
111
+ const raw = event.attendeeListAccess;
112
+ if (raw === "Public" || raw === "TicketHolders" || raw === "Preview") {
113
+ return raw;
114
+ }
115
+ return event.attendeeListPublic ? "Public" : "Preview";
116
+ }
117
+
101
118
  /** Legacy showAttendees=false → Hidden; otherwise honour attendeeDisplayMode string. */
102
119
  export function resolveEffectiveEventMode(
103
120
  event: EventModeInput
@@ -44,6 +44,46 @@ export function calculateDiscountFromPromoCode(
44
44
  return 0;
45
45
  }
46
46
 
47
+ /**
48
+ * Unit price in cents for checkout (supports pay-what-you-can tiers).
49
+ */
50
+ export function effectiveTicketUnitPriceCents(
51
+ tier: TicketTier,
52
+ row: NumberOfTicketsForDate
53
+ ): number {
54
+ if (tier.isDonationTier === true && row.unitPriceCents != null) {
55
+ return Math.max(tier.price, row.unitPriceCents);
56
+ }
57
+ return tier.price;
58
+ }
59
+
60
+ export function validatePayWhatYouCanTicketList(
61
+ ticketTiers: TicketTier[],
62
+ ticketList: Map<string, NumberOfTicketsForDate[]>
63
+ ): { ok: true } | { ok: false; message: string } {
64
+ for (const tier of ticketTiers) {
65
+ if (tier.isDonationTier !== true) continue;
66
+ const rows = ticketList.get(tier.id);
67
+ if (!rows?.length) continue;
68
+ for (const row of rows) {
69
+ if ((row.numberOfTickets ?? 0) <= 0) continue;
70
+ if (row.unitPriceCents == null) {
71
+ return {
72
+ ok: false,
73
+ message: `Enter an amount for ${tier.title} (minimum $${convertCentsToDollars(tier.price).toFixed(2)}).`,
74
+ };
75
+ }
76
+ if (row.unitPriceCents < tier.price) {
77
+ return {
78
+ ok: false,
79
+ message: `${tier.title} requires at least $${convertCentsToDollars(tier.price).toFixed(2)} per ticket.`,
80
+ };
81
+ }
82
+ }
83
+ }
84
+ return { ok: true };
85
+ }
86
+
47
87
  /**
48
88
  * Returns the total price in USD cents based on a map where the keys are the ticketTierIds
49
89
  * Only counts USD-priced tickets. BashPoints tickets are excluded (they have price=0).
@@ -60,7 +100,8 @@ export function calculateTotalPriceWithoutTax(
60
100
  if (tier.pricingType !== "BASHPOINTS") {
61
101
  const ticketListNumAndDate = ticketList.get(tier.id);
62
102
  ticketListNumAndDate?.forEach((numAndDate: NumberOfTicketsForDate) => {
63
- total += tier.price * (numAndDate.numberOfTickets ?? 0);
103
+ const unitCents = effectiveTicketUnitPriceCents(tier, numAndDate);
104
+ total += unitCents * (numAndDate.numberOfTickets ?? 0);
64
105
  });
65
106
  }
66
107
  });
@@ -80,7 +80,12 @@ export function ticketListToString(
80
80
  }
81
81
 
82
82
  ticketArgs.push(
83
- `${ticketNumAndDates.numberOfTickets}${URL_PARAMS_TICKETS_DATE_DELIM}${dateTimeStr}`
83
+ `${ticketNumAndDates.numberOfTickets}${URL_PARAMS_TICKETS_DATE_DELIM}${dateTimeStr}${
84
+ ticketNumAndDates.unitPriceCents != null &&
85
+ ticketNumAndDates.unitPriceCents > 0
86
+ ? `${URL_PARAMS_TICKETS_DATE_DELIM}${ticketNumAndDates.unitPriceCents}`
87
+ : ""
88
+ }`
84
89
  );
85
90
  }
86
91
  }
@@ -147,10 +152,11 @@ export function ticketListStrToTicketList(
147
152
  .filter((ticketNumAndDateStr): boolean => !!ticketNumAndDateStr);
148
153
 
149
154
  for (const ticketNumAndDateStr of ticketNumAndDates) {
150
- // [numberOfTickets];;[date]
151
- const [numberOfTickets, ticketDateTimeStr] = ticketNumAndDateStr.split(
152
- URL_PARAMS_TICKETS_DATE_DELIM
153
- );
155
+ // [numberOfTickets];;[date];;[unitPriceCents?]
156
+ const segments = ticketNumAndDateStr.split(URL_PARAMS_TICKETS_DATE_DELIM);
157
+ const numberOfTickets = segments[0];
158
+ const ticketDateTimeStr = segments[1];
159
+ const unitPriceCentsRaw = segments[2];
154
160
  const ticketDateTime = DateTime.fromISO(ticketDateTimeStr);
155
161
  let ticketDateTimeFormattedStr: string | undefined = undefined;
156
162
 
@@ -158,9 +164,17 @@ export function ticketListStrToTicketList(
158
164
  ticketDateTimeFormattedStr = ticketDateTime.toISO() ?? undefined;
159
165
  }
160
166
 
167
+ const unitPriceCents =
168
+ unitPriceCentsRaw != null && unitPriceCentsRaw !== ""
169
+ ? parseInt(unitPriceCentsRaw, 10)
170
+ : undefined;
171
+
161
172
  ticketNumAndDatesArr.push({
162
173
  numberOfTickets: parseInt(numberOfTickets),
163
174
  ticketDateTime: ticketDateTimeFormattedStr,
175
+ ...(unitPriceCents != null && !Number.isNaN(unitPriceCents)
176
+ ? { unitPriceCents }
177
+ : {}),
164
178
  });
165
179
  }
166
180
  ticketList.set(ticketTierId, ticketNumAndDatesArr);