@bash-app/bash-common 30.235.0 → 30.237.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 +45 -14
  4. package/dist/definitions.d.ts.map +1 -1
  5. package/dist/definitions.js +38 -17
  6. package/dist/definitions.js.map +1 -1
  7. package/dist/extendedSchemas.d.ts +8 -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 +638 -214
  145. package/src/__tests__/statusEnums.test.ts +119 -11
  146. package/src/definitions.ts +65 -22
  147. package/src/extendedSchemas.ts +8 -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
@@ -0,0 +1,117 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ isVerifiedNonprofitOrganization,
4
+ type NonprofitVerificationFields,
5
+ } from "../nonprofitUtils.js";
6
+
7
+ const fullyVerified: NonprofitVerificationFields = {
8
+ nonprofitCountry: "US",
9
+ nonprofitRegistrationNumber: "12-3456789",
10
+ nonprofitVerifiedAt: new Date("2025-01-15T00:00:00Z"),
11
+ stripeNonprofitPricingStatus: "NotApplied",
12
+ };
13
+
14
+ describe("isVerifiedNonprofitOrganization", () => {
15
+ test("null returns false", () => {
16
+ expect(isVerifiedNonprofitOrganization(null)).toBe(false);
17
+ });
18
+
19
+ test("undefined returns false", () => {
20
+ expect(isVerifiedNonprofitOrganization(undefined)).toBe(false);
21
+ });
22
+
23
+ test("missing nonprofitVerifiedAt returns false even with country + EIN", () => {
24
+ expect(
25
+ isVerifiedNonprofitOrganization({
26
+ ...fullyVerified,
27
+ nonprofitVerifiedAt: null,
28
+ })
29
+ ).toBe(false);
30
+ });
31
+
32
+ test("missing nonprofitRegistrationNumber returns false", () => {
33
+ expect(
34
+ isVerifiedNonprofitOrganization({
35
+ ...fullyVerified,
36
+ nonprofitRegistrationNumber: null,
37
+ })
38
+ ).toBe(false);
39
+ });
40
+
41
+ test("blank/whitespace nonprofitRegistrationNumber returns false", () => {
42
+ expect(
43
+ isVerifiedNonprofitOrganization({
44
+ ...fullyVerified,
45
+ nonprofitRegistrationNumber: " ",
46
+ })
47
+ ).toBe(false);
48
+ });
49
+
50
+ test("missing nonprofitCountry returns false", () => {
51
+ expect(
52
+ isVerifiedNonprofitOrganization({
53
+ ...fullyVerified,
54
+ nonprofitCountry: null,
55
+ })
56
+ ).toBe(false);
57
+ });
58
+
59
+ test("blank/whitespace nonprofitCountry returns false", () => {
60
+ expect(
61
+ isVerifiedNonprofitOrganization({
62
+ ...fullyVerified,
63
+ nonprofitCountry: " ",
64
+ })
65
+ ).toBe(false);
66
+ });
67
+
68
+ test("all required fields set returns true", () => {
69
+ expect(isVerifiedNonprofitOrganization(fullyVerified)).toBe(true);
70
+ });
71
+
72
+ test("non-US verified charity (e.g. UK) returns true", () => {
73
+ expect(
74
+ isVerifiedNonprofitOrganization({
75
+ ...fullyVerified,
76
+ nonprofitCountry: "GB",
77
+ nonprofitRegistrationNumber: "1234567",
78
+ })
79
+ ).toBe(true);
80
+ });
81
+
82
+ test("Stripe nonprofit-pricing status does NOT affect verification truthiness", () => {
83
+ const statuses: NonprofitVerificationFields["stripeNonprofitPricingStatus"][] = [
84
+ "NotApplied",
85
+ "Pending",
86
+ "Approved",
87
+ "Declined",
88
+ ];
89
+ for (const status of statuses) {
90
+ expect(
91
+ isVerifiedNonprofitOrganization({
92
+ ...fullyVerified,
93
+ stripeNonprofitPricingStatus: status,
94
+ })
95
+ ).toBe(true);
96
+ }
97
+ });
98
+
99
+ test("Declined Stripe status with no Bash verification stamp is still NOT verified", () => {
100
+ expect(
101
+ isVerifiedNonprofitOrganization({
102
+ ...fullyVerified,
103
+ nonprofitVerifiedAt: null,
104
+ stripeNonprofitPricingStatus: "Declined",
105
+ })
106
+ ).toBe(false);
107
+ });
108
+
109
+ test("future nonprofitVerifiedAt is NOT yet verified", () => {
110
+ expect(
111
+ isVerifiedNonprofitOrganization({
112
+ ...fullyVerified,
113
+ nonprofitVerifiedAt: new Date(Date.now() + 60_000),
114
+ })
115
+ ).toBe(false);
116
+ });
117
+ });
@@ -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,