@bash-app/bash-common 30.236.0 → 30.238.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 (180) hide show
  1. package/dist/__tests__/statusEnums.test.js +88 -7
  2. package/dist/__tests__/statusEnums.test.js.map +1 -1
  3. package/dist/definitions.d.ts +39 -8
  4. package/dist/definitions.d.ts.map +1 -1
  5. package/dist/definitions.js +31 -10
  6. package/dist/definitions.js.map +1 -1
  7. package/dist/extendedSchemas.d.ts +6 -0
  8. package/dist/extendedSchemas.d.ts.map +1 -1
  9. package/dist/extendedSchemas.js.map +1 -1
  10. package/dist/index.d.ts +13 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +16 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/mirroredPrismaEnums.d.ts +145 -0
  15. package/dist/mirroredPrismaEnums.d.ts.map +1 -0
  16. package/dist/mirroredPrismaEnums.js +136 -0
  17. package/dist/mirroredPrismaEnums.js.map +1 -0
  18. package/dist/utils/__tests__/nonprofitUtils.test.d.ts +2 -0
  19. package/dist/utils/__tests__/nonprofitUtils.test.d.ts.map +1 -0
  20. package/dist/utils/__tests__/nonprofitUtils.test.js +84 -0
  21. package/dist/utils/__tests__/nonprofitUtils.test.js.map +1 -0
  22. package/dist/utils/__tests__/paymentUtils.test.js +87 -0
  23. package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
  24. package/dist/utils/nonprofitUtils.d.ts +21 -0
  25. package/dist/utils/nonprofitUtils.d.ts.map +1 -0
  26. package/dist/utils/nonprofitUtils.js +29 -0
  27. package/dist/utils/nonprofitUtils.js.map +1 -0
  28. package/dist/utils/paymentUtils.d.ts +23 -0
  29. package/dist/utils/paymentUtils.d.ts.map +1 -1
  30. package/dist/utils/paymentUtils.js +24 -7
  31. package/dist/utils/paymentUtils.js.map +1 -1
  32. package/dist/utils/service/__tests__/billingCapUtils.test.d.ts +2 -0
  33. package/dist/utils/service/__tests__/billingCapUtils.test.d.ts.map +1 -0
  34. package/dist/utils/service/__tests__/billingCapUtils.test.js +120 -0
  35. package/dist/utils/service/__tests__/billingCapUtils.test.js.map +1 -0
  36. package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.js +1 -1
  37. package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.js.map +1 -1
  38. package/dist/utils/service/__tests__/eventPromoterPhase3.test.d.ts +2 -0
  39. package/dist/utils/service/__tests__/eventPromoterPhase3.test.d.ts.map +1 -0
  40. package/dist/utils/service/__tests__/eventPromoterPhase3.test.js +243 -0
  41. package/dist/utils/service/__tests__/eventPromoterPhase3.test.js.map +1 -0
  42. package/dist/utils/service/__tests__/eventPromoterUtils.test.d.ts +2 -0
  43. package/dist/utils/service/__tests__/eventPromoterUtils.test.d.ts.map +1 -0
  44. package/dist/utils/service/__tests__/eventPromoterUtils.test.js +333 -0
  45. package/dist/utils/service/__tests__/eventPromoterUtils.test.js.map +1 -0
  46. package/dist/utils/service/__tests__/offerAllowListUtils.test.d.ts +2 -0
  47. package/dist/utils/service/__tests__/offerAllowListUtils.test.d.ts.map +1 -0
  48. package/dist/utils/service/__tests__/offerAllowListUtils.test.js +52 -0
  49. package/dist/utils/service/__tests__/offerAllowListUtils.test.js.map +1 -0
  50. package/dist/utils/service/__tests__/offerComparisonUtils.test.d.ts +2 -0
  51. package/dist/utils/service/__tests__/offerComparisonUtils.test.d.ts.map +1 -0
  52. package/dist/utils/service/__tests__/offerComparisonUtils.test.js +111 -0
  53. package/dist/utils/service/__tests__/offerComparisonUtils.test.js.map +1 -0
  54. package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.d.ts +2 -0
  55. package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.d.ts.map +1 -0
  56. package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.js +155 -0
  57. package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.js.map +1 -0
  58. package/dist/utils/service/__tests__/unifiedPricingResolver.test.d.ts +2 -0
  59. package/dist/utils/service/__tests__/unifiedPricingResolver.test.d.ts.map +1 -0
  60. package/dist/utils/service/__tests__/unifiedPricingResolver.test.js +77 -0
  61. package/dist/utils/service/__tests__/unifiedPricingResolver.test.js.map +1 -0
  62. package/dist/utils/service/__tests__/venueFBSettlementUtils.test.d.ts +2 -0
  63. package/dist/utils/service/__tests__/venueFBSettlementUtils.test.d.ts.map +1 -0
  64. package/dist/utils/service/__tests__/venueFBSettlementUtils.test.js +157 -0
  65. package/dist/utils/service/__tests__/venueFBSettlementUtils.test.js.map +1 -0
  66. package/dist/utils/service/__tests__/venueSettlementUtils.test.d.ts +2 -0
  67. package/dist/utils/service/__tests__/venueSettlementUtils.test.d.ts.map +1 -0
  68. package/dist/utils/service/__tests__/venueSettlementUtils.test.js +175 -0
  69. package/dist/utils/service/__tests__/venueSettlementUtils.test.js.map +1 -0
  70. package/dist/utils/service/__tests__/volunteerCompensationUtils.test.d.ts +2 -0
  71. package/dist/utils/service/__tests__/volunteerCompensationUtils.test.d.ts.map +1 -0
  72. package/dist/utils/service/__tests__/volunteerCompensationUtils.test.js +118 -0
  73. package/dist/utils/service/__tests__/volunteerCompensationUtils.test.js.map +1 -0
  74. package/dist/utils/service/apiServiceBookingApiUtils.d.ts.map +1 -1
  75. package/dist/utils/service/apiServiceBookingApiUtils.js +4 -0
  76. package/dist/utils/service/apiServiceBookingApiUtils.js.map +1 -1
  77. package/dist/utils/service/billingCapUtils.d.ts +86 -0
  78. package/dist/utils/service/billingCapUtils.d.ts.map +1 -0
  79. package/dist/utils/service/billingCapUtils.js +96 -0
  80. package/dist/utils/service/billingCapUtils.js.map +1 -0
  81. package/dist/utils/service/cancellationPolicyRefundResolver.d.ts +1 -1
  82. package/dist/utils/service/cancellationPolicyRefundResolver.d.ts.map +1 -1
  83. package/dist/utils/service/cancellationPolicyRefundResolver.js +1 -1
  84. package/dist/utils/service/cancellationPolicyRefundResolver.js.map +1 -1
  85. package/dist/utils/service/eventPromoterUtils.d.ts +319 -0
  86. package/dist/utils/service/eventPromoterUtils.d.ts.map +1 -0
  87. package/dist/utils/service/eventPromoterUtils.js +372 -0
  88. package/dist/utils/service/eventPromoterUtils.js.map +1 -0
  89. package/dist/utils/service/offerAllowListUtils.d.ts +42 -0
  90. package/dist/utils/service/offerAllowListUtils.d.ts.map +1 -0
  91. package/dist/utils/service/offerAllowListUtils.js +49 -0
  92. package/dist/utils/service/offerAllowListUtils.js.map +1 -0
  93. package/dist/utils/service/offerComparisonUtils.d.ts +69 -0
  94. package/dist/utils/service/offerComparisonUtils.d.ts.map +1 -0
  95. package/dist/utils/service/offerComparisonUtils.js +110 -0
  96. package/dist/utils/service/offerComparisonUtils.js.map +1 -0
  97. package/dist/utils/service/serviceBookingExposureUtils.d.ts +111 -0
  98. package/dist/utils/service/serviceBookingExposureUtils.d.ts.map +1 -0
  99. package/dist/utils/service/serviceBookingExposureUtils.js +133 -0
  100. package/dist/utils/service/serviceBookingExposureUtils.js.map +1 -0
  101. package/dist/utils/service/serviceBookingTypes.d.ts +1 -1
  102. package/dist/utils/service/serviceBookingTypes.d.ts.map +1 -1
  103. package/dist/utils/service/serviceUtils.d.ts +2 -1
  104. package/dist/utils/service/serviceUtils.d.ts.map +1 -1
  105. package/dist/utils/service/serviceUtils.js +17 -16
  106. package/dist/utils/service/serviceUtils.js.map +1 -1
  107. package/dist/utils/service/unifiedPricingResolver.d.ts +67 -0
  108. package/dist/utils/service/unifiedPricingResolver.d.ts.map +1 -0
  109. package/dist/utils/service/unifiedPricingResolver.js +75 -0
  110. package/dist/utils/service/unifiedPricingResolver.js.map +1 -0
  111. package/dist/utils/service/venueFBSettlementUtils.d.ts +122 -0
  112. package/dist/utils/service/venueFBSettlementUtils.d.ts.map +1 -0
  113. package/dist/utils/service/venueFBSettlementUtils.js +106 -0
  114. package/dist/utils/service/venueFBSettlementUtils.js.map +1 -0
  115. package/dist/utils/service/venueSettlementUtils.d.ts +107 -0
  116. package/dist/utils/service/venueSettlementUtils.d.ts.map +1 -0
  117. package/dist/utils/service/venueSettlementUtils.js +117 -0
  118. package/dist/utils/service/venueSettlementUtils.js.map +1 -0
  119. package/dist/utils/service/volunteerCompensationUtils.d.ts +75 -0
  120. package/dist/utils/service/volunteerCompensationUtils.d.ts.map +1 -0
  121. package/dist/utils/service/volunteerCompensationUtils.js +88 -0
  122. package/dist/utils/service/volunteerCompensationUtils.js.map +1 -0
  123. package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.d.ts +2 -0
  124. package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.d.ts.map +1 -0
  125. package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.js +67 -0
  126. package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.js.map +1 -0
  127. package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.d.ts +2 -0
  128. package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.d.ts.map +1 -0
  129. package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.js +71 -0
  130. package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.js.map +1 -0
  131. package/dist/utils/venueLoyalty/eligibilityUtils.d.ts +33 -0
  132. package/dist/utils/venueLoyalty/eligibilityUtils.d.ts.map +1 -0
  133. package/dist/utils/venueLoyalty/eligibilityUtils.js +49 -0
  134. package/dist/utils/venueLoyalty/eligibilityUtils.js.map +1 -0
  135. package/dist/utils/venueLoyalty/redemptionCodeUtils.d.ts +35 -0
  136. package/dist/utils/venueLoyalty/redemptionCodeUtils.d.ts.map +1 -0
  137. package/dist/utils/venueLoyalty/redemptionCodeUtils.js +74 -0
  138. package/dist/utils/venueLoyalty/redemptionCodeUtils.js.map +1 -0
  139. package/dist/venueLoyaltyRedemption.d.ts +146 -1
  140. package/dist/venueLoyaltyRedemption.d.ts.map +1 -1
  141. package/dist/venueLoyaltyRedemption.js +18 -2
  142. package/dist/venueLoyaltyRedemption.js.map +1 -1
  143. package/package.json +1 -1
  144. package/prisma/schema.prisma +609 -214
  145. package/src/__tests__/statusEnums.test.ts +119 -11
  146. package/src/definitions.ts +57 -14
  147. package/src/extendedSchemas.ts +6 -0
  148. package/src/index.ts +16 -6
  149. package/src/mirroredPrismaEnums.ts +181 -0
  150. package/src/utils/__tests__/nonprofitUtils.test.ts +117 -0
  151. package/src/utils/__tests__/paymentUtils.test.ts +93 -0
  152. package/src/utils/nonprofitUtils.ts +44 -0
  153. package/src/utils/paymentUtils.ts +33 -7
  154. package/src/utils/service/__tests__/billingCapUtils.test.ts +175 -0
  155. package/src/utils/service/__tests__/cancellationPolicyRefundResolver.test.ts +1 -1
  156. package/src/utils/service/__tests__/eventPromoterPhase3.test.ts +296 -0
  157. package/src/utils/service/__tests__/eventPromoterUtils.test.ts +396 -0
  158. package/src/utils/service/__tests__/offerAllowListUtils.test.ts +76 -0
  159. package/src/utils/service/__tests__/offerComparisonUtils.test.ts +124 -0
  160. package/src/utils/service/__tests__/serviceBookingExposureUtils.test.ts +171 -0
  161. package/src/utils/service/__tests__/unifiedPricingResolver.test.ts +83 -0
  162. package/src/utils/service/__tests__/venueSettlementUtils.test.ts +219 -0
  163. package/src/utils/service/__tests__/volunteerCompensationUtils.test.ts +172 -0
  164. package/src/utils/service/apiServiceBookingApiUtils.ts +4 -0
  165. package/src/utils/service/billingCapUtils.ts +145 -0
  166. package/src/utils/service/cancellationPolicyRefundResolver.ts +1 -1
  167. package/src/utils/service/eventPromoterUtils.ts +644 -0
  168. package/src/utils/service/offerAllowListUtils.ts +81 -0
  169. package/src/utils/service/offerComparisonUtils.ts +197 -0
  170. package/src/utils/service/serviceBookingExposureUtils.ts +218 -0
  171. package/src/utils/service/serviceBookingTypes.ts +4 -0
  172. package/src/utils/service/serviceUtils.ts +18 -21
  173. package/src/utils/service/unifiedPricingResolver.ts +149 -0
  174. package/src/utils/service/venueSettlementUtils.ts +198 -0
  175. package/src/utils/service/volunteerCompensationUtils.ts +145 -0
  176. package/src/utils/venueLoyalty/__tests__/eligibilityUtils.test.ts +103 -0
  177. package/src/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.ts +92 -0
  178. package/src/utils/venueLoyalty/eligibilityUtils.ts +84 -0
  179. package/src/utils/venueLoyalty/redemptionCodeUtils.ts +94 -0
  180. package/src/venueLoyaltyRedemption.ts +177 -2
@@ -147,6 +147,99 @@ describe("PaymentUtils - Core Functions", () => {
147
147
  expect(r.applicationFeeAmountCents).toBe(r.stripeProcessingCents + 300);
148
148
  });
149
149
 
150
+ test("computeTicketApplicationFeeAmountCents — explicit isVerifiedNonprofitHost: false matches default", () => {
151
+ const baseArgs = {
152
+ discountedSubtotalCents: 10000,
153
+ platformFeeEnabled: true,
154
+ platformFeeRate: 0.03,
155
+ feeHandling: "HostAbsorbs" as const,
156
+ bashPassWaivesGuestFee: false,
157
+ };
158
+ const defaulted = computeTicketApplicationFeeAmountCents(baseArgs);
159
+ const explicitFalse = computeTicketApplicationFeeAmountCents({
160
+ ...baseArgs,
161
+ isVerifiedNonprofitHost: false,
162
+ });
163
+ expect(explicitFalse).toEqual(defaulted);
164
+ });
165
+
166
+ test("computeTicketApplicationFeeAmountCents — verified nonprofit host: zero Bash platform, Stripe untouched", () => {
167
+ const r = computeTicketApplicationFeeAmountCents({
168
+ discountedSubtotalCents: 10000,
169
+ platformFeeEnabled: true,
170
+ platformFeeRate: 0.03,
171
+ feeHandling: "GuestPays",
172
+ bashPassWaivesGuestFee: false,
173
+ isVerifiedNonprofitHost: true,
174
+ });
175
+ expect(r.bashPlatformTotalCents).toBe(0);
176
+ expect(r.guestPlatformLineCents).toBe(0);
177
+ expect(r.totalChargedCents).toBe(10000);
178
+ expect(r.stripeProcessingCents).toBe(calculateStripeProcessingFeeCents(10000));
179
+ expect(r.applicationFeeAmountCents).toBe(r.stripeProcessingCents);
180
+ });
181
+
182
+ test("computeTicketApplicationFeeAmountCents — verified nonprofit waiver overrides feeHandling=GuestPays", () => {
183
+ const nonNonprofit = computeTicketApplicationFeeAmountCents({
184
+ discountedSubtotalCents: 5000,
185
+ platformFeeEnabled: true,
186
+ platformFeeRate: 0.05,
187
+ feeHandling: "GuestPays",
188
+ bashPassWaivesGuestFee: false,
189
+ });
190
+ expect(nonNonprofit.bashPlatformTotalCents).toBe(250);
191
+ expect(nonNonprofit.guestPlatformLineCents).toBe(250);
192
+ expect(nonNonprofit.totalChargedCents).toBe(5250);
193
+
194
+ const nonprofit = computeTicketApplicationFeeAmountCents({
195
+ discountedSubtotalCents: 5000,
196
+ platformFeeEnabled: true,
197
+ platformFeeRate: 0.05,
198
+ feeHandling: "GuestPays",
199
+ bashPassWaivesGuestFee: false,
200
+ isVerifiedNonprofitHost: true,
201
+ });
202
+ expect(nonprofit.bashPlatformTotalCents).toBe(0);
203
+ expect(nonprofit.guestPlatformLineCents).toBe(0);
204
+ expect(nonprofit.totalChargedCents).toBe(5000);
205
+ expect(nonprofit.applicationFeeAmountCents).toBe(nonprofit.stripeProcessingCents);
206
+ });
207
+
208
+ test("computeTicketApplicationFeeAmountCents — verified nonprofit waiver beats any membership-tier math", () => {
209
+ const tierCases = [
210
+ { feeHandling: "HostAbsorbs" as const, bashPassWaivesGuestFee: false },
211
+ { feeHandling: "Split" as const, bashPassWaivesGuestFee: false },
212
+ { feeHandling: "GuestPays" as const, bashPassWaivesGuestFee: true },
213
+ ];
214
+ for (const tier of tierCases) {
215
+ const r = computeTicketApplicationFeeAmountCents({
216
+ discountedSubtotalCents: 12345,
217
+ platformFeeEnabled: true,
218
+ platformFeeRate: 0.04,
219
+ feeHandling: tier.feeHandling,
220
+ bashPassWaivesGuestFee: tier.bashPassWaivesGuestFee,
221
+ isVerifiedNonprofitHost: true,
222
+ });
223
+ expect(r.bashPlatformTotalCents).toBe(0);
224
+ expect(r.guestPlatformLineCents).toBe(0);
225
+ expect(r.totalChargedCents).toBe(12345);
226
+ expect(r.applicationFeeAmountCents).toBe(r.stripeProcessingCents);
227
+ }
228
+ });
229
+
230
+ test("computeTicketApplicationFeeAmountCents — verified nonprofit + platformFeeEnabled=false still zeroes the take", () => {
231
+ const r = computeTicketApplicationFeeAmountCents({
232
+ discountedSubtotalCents: 2500,
233
+ platformFeeEnabled: false,
234
+ platformFeeRate: 0.03,
235
+ feeHandling: "HostAbsorbs",
236
+ bashPassWaivesGuestFee: false,
237
+ isVerifiedNonprofitHost: true,
238
+ });
239
+ expect(r.bashPlatformTotalCents).toBe(0);
240
+ expect(r.applicationFeeAmountCents).toBe(r.stripeProcessingCents);
241
+ });
242
+
150
243
  test("userHasActiveBashPass", () => {
151
244
  expect(userHasActiveBashPass({ bashPassStripeSubscriptionId: null, bashPassCurrentPeriodEnd: null })).toBe(false);
152
245
  expect(userHasActiveBashPass({ bashPassStripeSubscriptionId: "sub_1", bashPassCurrentPeriodEnd: null })).toBe(true);
@@ -0,0 +1,44 @@
1
+ import type { Organization } from "@prisma/client";
2
+
3
+ /**
4
+ * Subset of `Organization` fields the verified-nonprofit helpers need. Callers
5
+ * may pass any prisma payload that selects (or includes) at least these
6
+ * columns — keeps consumers free to use narrow `select` shapes.
7
+ */
8
+ export type NonprofitVerificationFields = Pick<
9
+ Organization,
10
+ | "nonprofitCountry"
11
+ | "nonprofitRegistrationNumber"
12
+ | "nonprofitVerifiedAt"
13
+ | "stripeNonprofitPricingStatus"
14
+ >;
15
+
16
+ /**
17
+ * Verified-nonprofit status is determined solely by an admin stamp:
18
+ * `nonprofitVerifiedAt` is set (in the past or now — future timestamps do
19
+ * NOT count) AND we captured `nonprofitCountry` + `nonprofitRegistrationNumber`.
20
+ * The Stripe nonprofit-pricing status is informational only and DOES NOT gate
21
+ * verification (a charity can be verified by Bash even before applying to
22
+ * Stripe).
23
+ *
24
+ * IMPORTANT: `BashEvent.nonProfit` is a user-entered flag and MUST NOT be
25
+ * passed in here. Fee-waiver code reads only the Organization's verification
26
+ * fields — see Phase 3 of the donation-followups-leak-fix plan.
27
+ */
28
+ export function isVerifiedNonprofitOrganization(
29
+ org: NonprofitVerificationFields | null | undefined
30
+ ): boolean {
31
+ if (!org) return false;
32
+ if (!org.nonprofitVerifiedAt) return false;
33
+ if (org.nonprofitVerifiedAt.getTime() > Date.now()) return false;
34
+ if (!org.nonprofitCountry || org.nonprofitCountry.trim().length === 0) {
35
+ return false;
36
+ }
37
+ if (
38
+ !org.nonprofitRegistrationNumber ||
39
+ org.nonprofitRegistrationNumber.trim().length === 0
40
+ ) {
41
+ return false;
42
+ }
43
+ return true;
44
+ }
@@ -275,26 +275,52 @@ export function splitBashPlatformFeeForTicketCheckout(args: {
275
275
  return { totalPlatformCents: total, guestPlatformLineCents: guestLine };
276
276
  }
277
277
 
278
+ /**
279
+ * Compute the Stripe Connect `application_fee_amount` for a ticket-style charge,
280
+ * along with its constituent parts.
281
+ *
282
+ * `isVerifiedNonprofitHost` waives ONLY Bash's platform take
283
+ * (`bashPlatformTotalCents`) — Stripe's pass-through (2.9% + $0.30) is untouched
284
+ * because that's not ours to waive. The host applies for Stripe nonprofit
285
+ * pricing directly. When `true`:
286
+ * - `bashPlatformTotalCents` and `guestPlatformLineCents` are both zero
287
+ * - `totalChargedCents` collapses to the discounted subtotal (no guest-line fee added)
288
+ * - `applicationFeeAmountCents` equals `stripeProcessingCents`
289
+ *
290
+ * Default (`false`) preserves existing math bit-exact for every current caller.
291
+ */
278
292
  export function computeTicketApplicationFeeAmountCents(args: {
279
293
  discountedSubtotalCents: number;
280
294
  platformFeeEnabled: boolean;
281
295
  platformFeeRate: number;
282
296
  feeHandling: string | null | undefined;
283
297
  bashPassWaivesGuestFee: boolean;
298
+ /**
299
+ * Verified-nonprofit hosts pay no Bash platform fee. Sourced from
300
+ * `isVerifiedNonprofitOrganization()` over the host's Organization row —
301
+ * NEVER from `BashEvent.nonProfit`, which is user-entered.
302
+ */
303
+ isVerifiedNonprofitHost?: boolean;
284
304
  }): {
285
305
  totalChargedCents: number;
306
+ /** Stripe's 2.9% + $0.30 pass-through; unchanged by the nonprofit waiver. */
286
307
  stripeProcessingCents: number;
308
+ /** Bash's platform take. Zero when `isVerifiedNonprofitHost` is true. */
287
309
  bashPlatformTotalCents: number;
288
310
  applicationFeeAmountCents: number;
311
+ /** Guest-visible portion of the platform line. Zero when `isVerifiedNonprofitHost` is true. */
289
312
  guestPlatformLineCents: number;
290
313
  } {
291
- const split = splitBashPlatformFeeForTicketCheckout({
292
- grossCents: args.discountedSubtotalCents,
293
- platformFeeEnabled: args.platformFeeEnabled,
294
- platformFeeRate: args.platformFeeRate,
295
- feeHandling: args.feeHandling,
296
- bashPassWaivesGuestFee: args.bashPassWaivesGuestFee,
297
- });
314
+ const isVerifiedNonprofitHost = args.isVerifiedNonprofitHost === true;
315
+ const split = isVerifiedNonprofitHost
316
+ ? { totalPlatformCents: 0, guestPlatformLineCents: 0 }
317
+ : splitBashPlatformFeeForTicketCheckout({
318
+ grossCents: args.discountedSubtotalCents,
319
+ platformFeeEnabled: args.platformFeeEnabled,
320
+ platformFeeRate: args.platformFeeRate,
321
+ feeHandling: args.feeHandling,
322
+ bashPassWaivesGuestFee: args.bashPassWaivesGuestFee,
323
+ });
298
324
  const guestPlatformLineCents = split.guestPlatformLineCents;
299
325
  const bashPlatformTotalCents = split.totalPlatformCents;
300
326
  const totalChargedCents = args.discountedSubtotalCents + guestPlatformLineCents;
@@ -0,0 +1,175 @@
1
+ import {
2
+ DEFAULT_MAX_BILLABLE_MULTIPLIER,
3
+ DEFAULT_MAX_BILLABLE_ROUNDING_CENTS,
4
+ DEFAULT_VARIABLE_PAYOUT_TIMING_NOTE,
5
+ defaultMaxBillableBashPoints,
6
+ defaultMaxBillableCents,
7
+ isFlatPricingModel,
8
+ } from "../billingCapUtils";
9
+
10
+ describe("defaultMaxBillableCents", () => {
11
+ it("computes 1.5x of expectedAttendees * baseRateCents, rounded up to $10", () => {
12
+ // 200 * $5 * 1.5 = $1500 — already a $10 multiple.
13
+ expect(
14
+ defaultMaxBillableCents({ expectedAttendees: 200, baseRateCents: 500 })
15
+ ).toBe(150_000);
16
+ });
17
+
18
+ it("rounds the cap UP to the nearest $10", () => {
19
+ // 217 * 500 * 1.5 = 162,750 cents = $1,627.50 → round up to $1,630.
20
+ expect(
21
+ defaultMaxBillableCents({ expectedAttendees: 217, baseRateCents: 500 })
22
+ ).toBe(163_000);
23
+ });
24
+
25
+ it("returns 0 when expectedAttendees is zero", () => {
26
+ expect(
27
+ defaultMaxBillableCents({ expectedAttendees: 0, baseRateCents: 500 })
28
+ ).toBe(0);
29
+ });
30
+
31
+ it("returns 0 when expectedAttendees is null or undefined", () => {
32
+ expect(
33
+ defaultMaxBillableCents({ expectedAttendees: null, baseRateCents: 500 })
34
+ ).toBe(0);
35
+ expect(
36
+ defaultMaxBillableCents({
37
+ expectedAttendees: undefined,
38
+ baseRateCents: 500,
39
+ })
40
+ ).toBe(0);
41
+ });
42
+
43
+ it("returns null when baseRateCents is missing or zero", () => {
44
+ expect(
45
+ defaultMaxBillableCents({ expectedAttendees: 100, baseRateCents: null })
46
+ ).toBeNull();
47
+ expect(
48
+ defaultMaxBillableCents({ expectedAttendees: 100, baseRateCents: 0 })
49
+ ).toBeNull();
50
+ expect(
51
+ defaultMaxBillableCents({
52
+ expectedAttendees: 100,
53
+ baseRateCents: undefined,
54
+ })
55
+ ).toBeNull();
56
+ });
57
+
58
+ it("clamps negative expectedAttendees to zero", () => {
59
+ expect(
60
+ defaultMaxBillableCents({ expectedAttendees: -50, baseRateCents: 500 })
61
+ ).toBe(0);
62
+ });
63
+
64
+ it("respects an explicit multiplier override", () => {
65
+ // 100 * 500 * 2.0 = 100,000 → $1,000.
66
+ expect(
67
+ defaultMaxBillableCents({
68
+ expectedAttendees: 100,
69
+ baseRateCents: 500,
70
+ multiplier: 2,
71
+ })
72
+ ).toBe(100_000);
73
+ });
74
+
75
+ it("falls back to default multiplier when override is invalid", () => {
76
+ const out = defaultMaxBillableCents({
77
+ expectedAttendees: 100,
78
+ baseRateCents: 500,
79
+ multiplier: 0,
80
+ });
81
+ expect(out).toBe(100 * 500 * DEFAULT_MAX_BILLABLE_MULTIPLIER);
82
+ });
83
+
84
+ it("respects a custom rounding granularity", () => {
85
+ // 200 * 500 * 1.5 = 150,000 cents = $1,500 — already a $100 multiple.
86
+ expect(
87
+ defaultMaxBillableCents({
88
+ expectedAttendees: 200,
89
+ baseRateCents: 500,
90
+ roundingCents: 10_000,
91
+ })
92
+ ).toBe(150_000);
93
+ // 101 * 500 * 1.5 = 75,750 cents = $757.50 → round up to nearest $100 = $800 = 80_000 cents.
94
+ expect(
95
+ defaultMaxBillableCents({
96
+ expectedAttendees: 101,
97
+ baseRateCents: 500,
98
+ roundingCents: 10_000,
99
+ })
100
+ ).toBe(80_000);
101
+ });
102
+
103
+ it("uses the platform default rounding when override is invalid", () => {
104
+ const out = defaultMaxBillableCents({
105
+ expectedAttendees: 217,
106
+ baseRateCents: 500,
107
+ roundingCents: 0,
108
+ });
109
+ // Falls back to DEFAULT rounding ($10) → same as the rounded case above.
110
+ expect(out).toBe(
111
+ Math.ceil((217 * 500 * 1.5) / DEFAULT_MAX_BILLABLE_ROUNDING_CENTS) *
112
+ DEFAULT_MAX_BILLABLE_ROUNDING_CENTS
113
+ );
114
+ });
115
+ });
116
+
117
+ describe("defaultMaxBillableBashPoints", () => {
118
+ it("computes the BP parallel of the USD cap, rounded up to nearest 100 BP", () => {
119
+ // 100 * 50 * 1.5 = 7,500 — already a 100-multiple.
120
+ expect(
121
+ defaultMaxBillableBashPoints({
122
+ expectedAttendees: 100,
123
+ baseRateBashPoints: 50,
124
+ })
125
+ ).toBe(7_500);
126
+ });
127
+
128
+ it("rounds up to the nearest 100 BashPoints", () => {
129
+ // 33 * 50 * 1.5 = 2,475 → round up to 2,500.
130
+ expect(
131
+ defaultMaxBillableBashPoints({
132
+ expectedAttendees: 33,
133
+ baseRateBashPoints: 50,
134
+ })
135
+ ).toBe(2_500);
136
+ });
137
+
138
+ it("returns null when baseRateBashPoints is missing", () => {
139
+ expect(
140
+ defaultMaxBillableBashPoints({
141
+ expectedAttendees: 100,
142
+ baseRateBashPoints: null,
143
+ })
144
+ ).toBeNull();
145
+ });
146
+ });
147
+
148
+ describe("isFlatPricingModel", () => {
149
+ it("recognizes the three flat models", () => {
150
+ expect(isFlatPricingModel("FLAT_FEE")).toBe(true);
151
+ expect(isFlatPricingModel("HOURLY")).toBe(true);
152
+ expect(isFlatPricingModel("PER_DAY")).toBe(true);
153
+ });
154
+
155
+ it("returns false for variable models", () => {
156
+ expect(isFlatPricingModel("PER_ATTENDEE")).toBe(false);
157
+ expect(isFlatPricingModel("PER_PAID_ATTENDEE")).toBe(false);
158
+ expect(isFlatPricingModel("PER_BASHPOINTS_ATTENDEE")).toBe(false);
159
+ expect(isFlatPricingModel("TICKET_REVENUE_PERCENTAGE")).toBe(false);
160
+ expect(isFlatPricingModel("TIERED")).toBe(false);
161
+ expect(isFlatPricingModel("CUSTOM")).toBe(false);
162
+ });
163
+
164
+ it("returns false for null / undefined / empty", () => {
165
+ expect(isFlatPricingModel(null)).toBe(false);
166
+ expect(isFlatPricingModel(undefined)).toBe(false);
167
+ expect(isFlatPricingModel("")).toBe(false);
168
+ });
169
+ });
170
+
171
+ describe("DEFAULT_VARIABLE_PAYOUT_TIMING_NOTE", () => {
172
+ it("mentions the ~3 business day window", () => {
173
+ expect(DEFAULT_VARIABLE_PAYOUT_TIMING_NOTE).toMatch(/3 business days/i);
174
+ });
175
+ });
@@ -1,4 +1,4 @@
1
- import { ServiceCancellationPolicy } from "@prisma/client";
1
+ import { ServiceCancellationPolicy } from "../../../mirroredPrismaEnums.js";
2
2
  import {
3
3
  hoursUntilEventStart,
4
4
  cancellationRuleThresholdHours,
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Phase 3 of the unified-pricing roadmap — tests for the
3
+ * - counting basis chooser,
4
+ * - locked-models set / validator update,
5
+ * - mixed USD + BashPoints settlement math,
6
+ * - BashPoints payout source resolver (pool-then-wallet).
7
+ *
8
+ * Lives next to `eventPromoterUtils.test.ts` so it picks up the same Jest
9
+ * config; pure-function-only, no Prisma.
10
+ */
11
+ import { describe, expect, it } from "@jest/globals";
12
+ import {
13
+ EVENT_PROMOTER_LOCKED_PRICING_MODELS,
14
+ computeEventPromoterMixedSettlement,
15
+ eventPromoterCountingBasisFor,
16
+ pickCheckedInForBasis,
17
+ resolveBashPointsPayoutSources,
18
+ validateEventPromoterPricing,
19
+ } from "../eventPromoterUtils.js";
20
+
21
+ describe("EVENT_PROMOTER_LOCKED_PRICING_MODELS", () => {
22
+ it("contains PER_ATTENDEE, PER_PAID_ATTENDEE, PER_BASHPOINTS_ATTENDEE in that order", () => {
23
+ expect([...EVENT_PROMOTER_LOCKED_PRICING_MODELS]).toEqual([
24
+ "PER_ATTENDEE",
25
+ "PER_PAID_ATTENDEE",
26
+ "PER_BASHPOINTS_ATTENDEE",
27
+ ]);
28
+ });
29
+ });
30
+
31
+ describe("validateEventPromoterPricing (Phase 3 model set)", () => {
32
+ it("accepts PER_ATTENDEE for Promoter subtype", () => {
33
+ expect(
34
+ validateEventPromoterPricing({
35
+ eventServiceSubType: "Promoter",
36
+ pricingModel: "PER_ATTENDEE",
37
+ }).ok
38
+ ).toBe(true);
39
+ });
40
+ it("accepts PER_PAID_ATTENDEE for Promoter subtype", () => {
41
+ expect(
42
+ validateEventPromoterPricing({
43
+ eventServiceSubType: "Promoter",
44
+ pricingModel: "PER_PAID_ATTENDEE",
45
+ }).ok
46
+ ).toBe(true);
47
+ });
48
+ it("accepts PER_BASHPOINTS_ATTENDEE for Promoter subtype", () => {
49
+ expect(
50
+ validateEventPromoterPricing({
51
+ eventServiceSubType: "Promoter",
52
+ pricingModel: "PER_BASHPOINTS_ATTENDEE",
53
+ }).ok
54
+ ).toBe(true);
55
+ });
56
+ it("rejects FLAT_FEE for Promoter subtype", () => {
57
+ const r = validateEventPromoterPricing({
58
+ eventServiceSubType: "Promoter",
59
+ pricingModel: "FLAT_FEE",
60
+ });
61
+ expect(r.ok).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe("eventPromoterCountingBasisFor", () => {
66
+ it("PER_ATTENDEE → AllCheckedIn", () => {
67
+ expect(
68
+ eventPromoterCountingBasisFor({ pricingModel: "PER_ATTENDEE" })
69
+ ).toBe("AllCheckedIn");
70
+ });
71
+ it("PER_PAID_ATTENDEE without BP knob → PaidUsdOnly", () => {
72
+ expect(
73
+ eventPromoterCountingBasisFor({
74
+ pricingModel: "PER_PAID_ATTENDEE",
75
+ countsBashPointsTickets: false,
76
+ })
77
+ ).toBe("PaidUsdOnly");
78
+ });
79
+ it("PER_PAID_ATTENDEE with countsBashPointsTickets → PaidAny", () => {
80
+ expect(
81
+ eventPromoterCountingBasisFor({
82
+ pricingModel: "PER_PAID_ATTENDEE",
83
+ countsBashPointsTickets: true,
84
+ })
85
+ ).toBe("PaidAny");
86
+ });
87
+ it("PER_BASHPOINTS_ATTENDEE → PaidBpOnly regardless of knob", () => {
88
+ expect(
89
+ eventPromoterCountingBasisFor({
90
+ pricingModel: "PER_BASHPOINTS_ATTENDEE",
91
+ countsBashPointsTickets: true,
92
+ })
93
+ ).toBe("PaidBpOnly");
94
+ });
95
+ it("unknown / null pricing → AllCheckedIn (legacy fallback)", () => {
96
+ expect(eventPromoterCountingBasisFor({ pricingModel: null })).toBe(
97
+ "AllCheckedIn"
98
+ );
99
+ });
100
+ });
101
+
102
+ describe("pickCheckedInForBasis", () => {
103
+ const SNAPS = {
104
+ totalCheckedInAttendees: 220,
105
+ paidAttendeesAtSettle: 180,
106
+ paidInBashPointsAttendeesAtSettle: 30,
107
+ };
108
+ it("AllCheckedIn → total", () => {
109
+ expect(pickCheckedInForBasis("AllCheckedIn", SNAPS)).toBe(220);
110
+ });
111
+ it("PaidUsdOnly → paid USD", () => {
112
+ expect(pickCheckedInForBasis("PaidUsdOnly", SNAPS)).toBe(180);
113
+ });
114
+ it("PaidBpOnly → paid BP", () => {
115
+ expect(pickCheckedInForBasis("PaidBpOnly", SNAPS)).toBe(30);
116
+ });
117
+ it("PaidAny → USD + BP", () => {
118
+ expect(pickCheckedInForBasis("PaidAny", SNAPS)).toBe(210);
119
+ });
120
+ it("treats missing snapshots as zero", () => {
121
+ expect(
122
+ pickCheckedInForBasis("PaidUsdOnly", {
123
+ totalCheckedInAttendees: 100,
124
+ })
125
+ ).toBe(0);
126
+ });
127
+ });
128
+
129
+ describe("computeEventPromoterMixedSettlement", () => {
130
+ const baseSnaps = {
131
+ totalCheckedInAttendees: 250,
132
+ paidAttendeesAtSettle: 200,
133
+ paidInBashPointsAttendeesAtSettle: 50,
134
+ };
135
+
136
+ it("computes a positive USD delta when more USD attendees show up than expected", () => {
137
+ const r = computeEventPromoterMixedSettlement({
138
+ baseRateCents: 500,
139
+ expectedPaidAttendeesAtBooking: 150,
140
+ snapshots: baseSnaps,
141
+ });
142
+ expect(r.usdTotalCents).toBe(200 * 500);
143
+ expect(r.usdDeltaCents).toBe((200 - 150) * 500);
144
+ expect(r.cappedUsd).toBe(false);
145
+ });
146
+
147
+ it("respects the USD ceiling (maxUsdAttendeesAttributable)", () => {
148
+ const r = computeEventPromoterMixedSettlement({
149
+ baseRateCents: 500,
150
+ expectedPaidAttendeesAtBooking: 150,
151
+ maxUsdAttendeesAttributable: 175,
152
+ snapshots: baseSnaps,
153
+ });
154
+ expect(r.usdTotalCents).toBe(175 * 500);
155
+ expect(r.cappedUsd).toBe(false); // ceiling, not dollar cap
156
+ });
157
+
158
+ it("flags cappedUsd when maxBillableCents bites", () => {
159
+ const r = computeEventPromoterMixedSettlement({
160
+ baseRateCents: 500,
161
+ expectedPaidAttendeesAtBooking: 150,
162
+ maxBillableCents: 80_000, // < 200 * 500 = 100_000
163
+ snapshots: baseSnaps,
164
+ });
165
+ expect(r.usdTotalCents).toBe(80_000);
166
+ expect(r.cappedUsd).toBe(true);
167
+ });
168
+
169
+ it("computes the BP side independently from USD", () => {
170
+ const r = computeEventPromoterMixedSettlement({
171
+ baseRateBashPoints: 10,
172
+ expectedBpAttendeesAtBooking: 30,
173
+ snapshots: baseSnaps,
174
+ });
175
+ expect(r.usdTotalCents).toBe(0);
176
+ expect(r.bpTotal).toBe(50 * 10);
177
+ expect(r.bpDelta).toBe(20 * 10);
178
+ });
179
+
180
+ it("caps BP via maxBillableBashPoints", () => {
181
+ const r = computeEventPromoterMixedSettlement({
182
+ baseRateBashPoints: 10,
183
+ expectedBpAttendeesAtBooking: 30,
184
+ maxBillableBashPoints: 400,
185
+ snapshots: baseSnaps,
186
+ });
187
+ expect(r.bpTotal).toBe(400);
188
+ expect(r.cappedBp).toBe(true);
189
+ });
190
+
191
+ it("returns all zeros when neither side has a rate", () => {
192
+ const r = computeEventPromoterMixedSettlement({
193
+ snapshots: baseSnaps,
194
+ });
195
+ expect(r.usdTotalCents).toBe(0);
196
+ expect(r.usdDeltaCents).toBe(0);
197
+ expect(r.bpTotal).toBe(0);
198
+ expect(r.bpDelta).toBe(0);
199
+ });
200
+ });
201
+
202
+ describe("resolveBashPointsPayoutSources", () => {
203
+ it("HostWallet path drains wallet only", () => {
204
+ expect(
205
+ resolveBashPointsPayoutSources({
206
+ source: "HostWallet",
207
+ payoutBp: 100,
208
+ hostWalletBp: 500,
209
+ bashPoolBp: 9999, // ignored
210
+ })
211
+ ).toEqual({
212
+ fromPoolBp: 0,
213
+ fromWalletBp: 100,
214
+ shortfallBp: 0,
215
+ walletDebited: true,
216
+ });
217
+ });
218
+
219
+ it("HostWallet path with insufficient wallet returns shortfall", () => {
220
+ expect(
221
+ resolveBashPointsPayoutSources({
222
+ source: "HostWallet",
223
+ payoutBp: 100,
224
+ hostWalletBp: 60,
225
+ })
226
+ ).toEqual({
227
+ fromPoolBp: 0,
228
+ fromWalletBp: 60,
229
+ shortfallBp: 40,
230
+ walletDebited: true,
231
+ });
232
+ });
233
+
234
+ it("BashBpPool drains pool first, wallet covers the rest", () => {
235
+ expect(
236
+ resolveBashPointsPayoutSources({
237
+ source: "BashBpPool",
238
+ payoutBp: 100,
239
+ hostWalletBp: 500,
240
+ bashPoolBp: 70,
241
+ })
242
+ ).toEqual({
243
+ fromPoolBp: 70,
244
+ fromWalletBp: 30,
245
+ shortfallBp: 0,
246
+ walletDebited: true,
247
+ });
248
+ });
249
+
250
+ it("BashBpPool fully covered by pool leaves wallet untouched", () => {
251
+ expect(
252
+ resolveBashPointsPayoutSources({
253
+ source: "BashBpPool",
254
+ payoutBp: 50,
255
+ hostWalletBp: 500,
256
+ bashPoolBp: 100,
257
+ })
258
+ ).toEqual({
259
+ fromPoolBp: 50,
260
+ fromWalletBp: 0,
261
+ shortfallBp: 0,
262
+ walletDebited: false,
263
+ });
264
+ });
265
+
266
+ it("BashBpPool with empty pool falls through to wallet", () => {
267
+ expect(
268
+ resolveBashPointsPayoutSources({
269
+ source: "BashBpPool",
270
+ payoutBp: 100,
271
+ hostWalletBp: 50,
272
+ bashPoolBp: 0,
273
+ })
274
+ ).toEqual({
275
+ fromPoolBp: 0,
276
+ fromWalletBp: 50,
277
+ shortfallBp: 50,
278
+ walletDebited: true,
279
+ });
280
+ });
281
+
282
+ it("clamps negative inputs to zero", () => {
283
+ expect(
284
+ resolveBashPointsPayoutSources({
285
+ source: "HostWallet",
286
+ payoutBp: -10,
287
+ hostWalletBp: -5,
288
+ })
289
+ ).toEqual({
290
+ fromPoolBp: 0,
291
+ fromWalletBp: 0,
292
+ shortfallBp: 0,
293
+ walletDebited: false,
294
+ });
295
+ });
296
+ });