@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,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
+ });
@@ -0,0 +1,396 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import {
3
+ EVENT_PROMOTER_LATE_CANCEL_HOURS,
4
+ EVENT_PROMOTER_LATE_CANCEL_PENALTY_FRACTION,
5
+ EVENT_PROMOTER_LOCKED_PRICING_MODEL,
6
+ buildIssuePromoCodeDeepLink,
7
+ computeEventPromoterDepositCents,
8
+ computeEventPromoterSettlement,
9
+ decideEventPromoterCancellation,
10
+ isPerAttendeeLockedPromotionSubType,
11
+ validateEventPromoterPricing,
12
+ } from "../eventPromoterUtils.js";
13
+
14
+ describe("isPerAttendeeLockedPromotionSubType", () => {
15
+ it("returns true for Promoter subtype", () => {
16
+ expect(isPerAttendeeLockedPromotionSubType("Promoter")).toBe(true);
17
+ });
18
+
19
+ it("returns false for other Promotion subtypes", () => {
20
+ expect(isPerAttendeeLockedPromotionSubType("Influencer")).toBe(false);
21
+ expect(isPerAttendeeLockedPromotionSubType("ContentCreator")).toBe(false);
22
+ expect(isPerAttendeeLockedPromotionSubType("CelebrityAppearance")).toBe(
23
+ false
24
+ );
25
+ });
26
+
27
+ it("returns false for null / undefined / unrelated strings", () => {
28
+ expect(isPerAttendeeLockedPromotionSubType(null)).toBe(false);
29
+ expect(isPerAttendeeLockedPromotionSubType(undefined)).toBe(false);
30
+ expect(isPerAttendeeLockedPromotionSubType("AnythingElse")).toBe(false);
31
+ });
32
+ });
33
+
34
+ describe("validateEventPromoterPricing", () => {
35
+ it("passes when subtype is locked and pricing is PER_ATTENDEE", () => {
36
+ expect(
37
+ validateEventPromoterPricing({
38
+ eventServiceSubType: "Promoter",
39
+ pricingModel: EVENT_PROMOTER_LOCKED_PRICING_MODEL,
40
+ })
41
+ ).toEqual({ ok: true });
42
+ });
43
+
44
+ it("rejects PROMOTER + non-PER_ATTENDEE pricing model", () => {
45
+ const result = validateEventPromoterPricing({
46
+ eventServiceSubType: "Promoter",
47
+ pricingModel: "HOURLY",
48
+ });
49
+ expect(result.ok).toBe(false);
50
+ if (!result.ok) {
51
+ expect(result.expected).toEqual([
52
+ "PER_ATTENDEE",
53
+ "PER_PAID_ATTENDEE",
54
+ "PER_BASHPOINTS_ATTENDEE",
55
+ ]);
56
+ expect(result.reason).toContain("PER_ATTENDEE");
57
+ }
58
+ });
59
+
60
+ it("rejects PROMOTER + null pricing model", () => {
61
+ const result = validateEventPromoterPricing({
62
+ eventServiceSubType: "Promoter",
63
+ pricingModel: null,
64
+ });
65
+ expect(result.ok).toBe(false);
66
+ });
67
+
68
+ it("passes any pricing model for non-Promoter subtypes", () => {
69
+ expect(
70
+ validateEventPromoterPricing({
71
+ eventServiceSubType: "Influencer",
72
+ pricingModel: "HOURLY",
73
+ })
74
+ ).toEqual({ ok: true });
75
+ expect(
76
+ validateEventPromoterPricing({
77
+ eventServiceSubType: "ContentCreator",
78
+ pricingModel: "FLAT_FEE",
79
+ })
80
+ ).toEqual({ ok: true });
81
+ });
82
+ });
83
+
84
+ describe("computeEventPromoterDepositCents", () => {
85
+ it("returns expected attendees * baseRate when minimum is below expected", () => {
86
+ expect(
87
+ computeEventPromoterDepositCents({
88
+ bashExpectedAttendees: 200,
89
+ baseRateCents: 500,
90
+ minimumQuantity: 50,
91
+ })
92
+ ).toBe(100_000);
93
+ });
94
+
95
+ it("uses minimumQuantity floor when expected is below it", () => {
96
+ expect(
97
+ computeEventPromoterDepositCents({
98
+ bashExpectedAttendees: 30,
99
+ baseRateCents: 500,
100
+ minimumQuantity: 100,
101
+ })
102
+ ).toBe(50_000);
103
+ });
104
+
105
+ it("returns null when baseRateCents is missing or 0", () => {
106
+ expect(
107
+ computeEventPromoterDepositCents({
108
+ bashExpectedAttendees: 200,
109
+ baseRateCents: null,
110
+ minimumQuantity: 100,
111
+ })
112
+ ).toBeNull();
113
+ expect(
114
+ computeEventPromoterDepositCents({
115
+ bashExpectedAttendees: 200,
116
+ baseRateCents: 0,
117
+ minimumQuantity: 100,
118
+ })
119
+ ).toBeNull();
120
+ });
121
+
122
+ it("treats negative expected attendees as 0", () => {
123
+ expect(
124
+ computeEventPromoterDepositCents({
125
+ bashExpectedAttendees: -10,
126
+ baseRateCents: 500,
127
+ minimumQuantity: null,
128
+ })
129
+ ).toBe(0);
130
+ });
131
+ });
132
+
133
+ describe("computeEventPromoterSettlement", () => {
134
+ const baseInput = {
135
+ baseRateCents: 500,
136
+ expectedAttendeesAtBooking: 200,
137
+ };
138
+
139
+ it("returns zero delta when checked-in matches expected exactly", () => {
140
+ const result = computeEventPromoterSettlement({
141
+ ...baseInput,
142
+ totalCheckedInAttendees: 200,
143
+ });
144
+ expect(result.finalCount).toBe(200);
145
+ expect(result.settlementDeltaCents).toBe(0);
146
+ expect(result.totalCents).toBe(100_000);
147
+ expect(result.ceilingHit).toBe(false);
148
+ });
149
+
150
+ it("returns positive delta when more attendees show up than expected", () => {
151
+ const result = computeEventPromoterSettlement({
152
+ ...baseInput,
153
+ totalCheckedInAttendees: 250,
154
+ });
155
+ expect(result.finalCount).toBe(250);
156
+ expect(result.settlementDeltaCents).toBe(25_000);
157
+ expect(result.totalCents).toBe(125_000);
158
+ expect(result.ceilingHit).toBe(false);
159
+ });
160
+
161
+ it("returns negative delta (refund) when fewer attendees show up", () => {
162
+ const result = computeEventPromoterSettlement({
163
+ ...baseInput,
164
+ totalCheckedInAttendees: 150,
165
+ });
166
+ expect(result.finalCount).toBe(150);
167
+ expect(result.settlementDeltaCents).toBe(-25_000);
168
+ expect(result.totalCents).toBe(75_000);
169
+ });
170
+
171
+ it("caps finalCount at maxAttendeesAttributable and sets ceilingHit", () => {
172
+ const result = computeEventPromoterSettlement({
173
+ ...baseInput,
174
+ totalCheckedInAttendees: 500,
175
+ maxAttendeesAttributable: 250,
176
+ });
177
+ expect(result.finalCount).toBe(250);
178
+ expect(result.ceilingHit).toBe(true);
179
+ expect(result.settlementDeltaCents).toBe(25_000);
180
+ });
181
+
182
+ it("does not set ceilingHit when checked-in is exactly the ceiling", () => {
183
+ const result = computeEventPromoterSettlement({
184
+ ...baseInput,
185
+ totalCheckedInAttendees: 250,
186
+ maxAttendeesAttributable: 250,
187
+ });
188
+ expect(result.finalCount).toBe(250);
189
+ expect(result.ceilingHit).toBe(false);
190
+ });
191
+
192
+ it("applies minimumQuantity floor when checked-in is below it", () => {
193
+ const result = computeEventPromoterSettlement({
194
+ ...baseInput,
195
+ totalCheckedInAttendees: 50,
196
+ minimumQuantity: 100,
197
+ });
198
+ expect(result.finalCount).toBe(100);
199
+ expect(result.settlementDeltaCents).toBe(-50_000);
200
+ });
201
+
202
+ it("worked example A: 200 expected / 250 ceiling / 220 checked-in", () => {
203
+ const result = computeEventPromoterSettlement({
204
+ baseRateCents: 500,
205
+ expectedAttendeesAtBooking: 200,
206
+ totalCheckedInAttendees: 220,
207
+ maxAttendeesAttributable: 250,
208
+ });
209
+ expect(result.finalCount).toBe(220);
210
+ expect(result.settlementDeltaCents).toBe(10_000);
211
+ expect(result.ceilingHit).toBe(false);
212
+ });
213
+
214
+ it("worked example B: 100 expected / 150 ceiling / 220 checked-in (capped)", () => {
215
+ const result = computeEventPromoterSettlement({
216
+ baseRateCents: 500,
217
+ expectedAttendeesAtBooking: 100,
218
+ totalCheckedInAttendees: 220,
219
+ maxAttendeesAttributable: 150,
220
+ });
221
+ expect(result.finalCount).toBe(150);
222
+ expect(result.settlementDeltaCents).toBe(25_000);
223
+ expect(result.ceilingHit).toBe(true);
224
+ });
225
+
226
+ it("treats negative checked-in as 0", () => {
227
+ const result = computeEventPromoterSettlement({
228
+ ...baseInput,
229
+ totalCheckedInAttendees: -10,
230
+ });
231
+ expect(result.finalCount).toBe(0);
232
+ expect(result.settlementDeltaCents).toBe(-100_000);
233
+ });
234
+
235
+ it("treats null ceiling as no cap", () => {
236
+ const result = computeEventPromoterSettlement({
237
+ ...baseInput,
238
+ totalCheckedInAttendees: 999,
239
+ maxAttendeesAttributable: null,
240
+ });
241
+ expect(result.finalCount).toBe(999);
242
+ expect(result.ceilingHit).toBe(false);
243
+ });
244
+ });
245
+
246
+ describe("buildIssuePromoCodeDeepLink", () => {
247
+ it("includes bashEventId, promoterUserId, and source param", () => {
248
+ const url = buildIssuePromoCodeDeepLink({
249
+ bashEventId: "bash-1",
250
+ promoterUserId: "user-9",
251
+ });
252
+ expect(url).toContain("/bashes/bash-1/edit");
253
+ expect(url).toContain("step=how");
254
+ expect(url).toContain("open=promo-codes");
255
+ expect(url).toContain("promoterUserId=user-9");
256
+ expect(url).toContain("source=event-promoter-booking");
257
+ expect(url).not.toContain("ticketTierId=");
258
+ });
259
+
260
+ it("adds ticketTierId when provided", () => {
261
+ const url = buildIssuePromoCodeDeepLink({
262
+ bashEventId: "bash-1",
263
+ promoterUserId: "user-9",
264
+ ticketTierId: "tier-42",
265
+ });
266
+ expect(url).toContain("ticketTierId=tier-42");
267
+ });
268
+
269
+ it("omits ticketTierId when null/undefined", () => {
270
+ const urlNull = buildIssuePromoCodeDeepLink({
271
+ bashEventId: "bash-1",
272
+ promoterUserId: "user-9",
273
+ ticketTierId: null,
274
+ });
275
+ const urlUndef = buildIssuePromoCodeDeepLink({
276
+ bashEventId: "bash-1",
277
+ promoterUserId: "user-9",
278
+ });
279
+ expect(urlNull).not.toContain("ticketTierId=");
280
+ expect(urlUndef).not.toContain("ticketTierId=");
281
+ });
282
+ });
283
+
284
+ describe("decideEventPromoterCancellation", () => {
285
+ const HOUR = 60 * 60 * 1000;
286
+ const now = Date.parse("2026-05-12T12:00:00Z");
287
+
288
+ it("blocks any cancellation once the booking is settled", () => {
289
+ const result = decideEventPromoterCancellation({
290
+ who: "host",
291
+ nowMs: now,
292
+ bashStartMs: now + 100 * HOUR,
293
+ bashEndMs: now + 105 * HOUR,
294
+ depositCents: 100_000,
295
+ alreadySettled: true,
296
+ });
297
+ expect(result.allow).toBe(false);
298
+ expect(result.outcome).toBe("Blocked");
299
+ expect(result.refundCents).toBe(0);
300
+ });
301
+
302
+ it("host: full refund when bash is far in the future", () => {
303
+ const result = decideEventPromoterCancellation({
304
+ who: "host",
305
+ nowMs: now,
306
+ bashStartMs: now + 100 * HOUR,
307
+ bashEndMs: now + 105 * HOUR,
308
+ depositCents: 100_000,
309
+ alreadySettled: false,
310
+ });
311
+ expect(result.allow).toBe(true);
312
+ expect(result.outcome).toBe("FullRefund");
313
+ expect(result.refundCents).toBe(100_000);
314
+ expect(result.penaltyFraction).toBe(0);
315
+ });
316
+
317
+ it("host: partial refund inside the late-cancel window", () => {
318
+ const result = decideEventPromoterCancellation({
319
+ who: "host",
320
+ nowMs: now,
321
+ bashStartMs: now + (EVENT_PROMOTER_LATE_CANCEL_HOURS - 6) * HOUR,
322
+ bashEndMs: now + (EVENT_PROMOTER_LATE_CANCEL_HOURS - 1) * HOUR,
323
+ depositCents: 100_000,
324
+ alreadySettled: false,
325
+ });
326
+ expect(result.outcome).toBe("PartialRefund");
327
+ expect(result.penaltyFraction).toBe(
328
+ EVENT_PROMOTER_LATE_CANCEL_PENALTY_FRACTION
329
+ );
330
+ expect(result.refundCents).toBe(75_000);
331
+ });
332
+
333
+ it("host: NoRefund once the bash has started — settlement handles money", () => {
334
+ const result = decideEventPromoterCancellation({
335
+ who: "host",
336
+ nowMs: now,
337
+ bashStartMs: now - 1 * HOUR,
338
+ bashEndMs: now + 4 * HOUR,
339
+ depositCents: 100_000,
340
+ alreadySettled: false,
341
+ });
342
+ expect(result.allow).toBe(false);
343
+ expect(result.outcome).toBe("NoRefund");
344
+ });
345
+
346
+ it("promoter: full refund any time before the bash starts", () => {
347
+ const result = decideEventPromoterCancellation({
348
+ who: "promoter",
349
+ nowMs: now,
350
+ bashStartMs: now + 3 * HOUR,
351
+ bashEndMs: now + 8 * HOUR,
352
+ depositCents: 100_000,
353
+ alreadySettled: false,
354
+ });
355
+ expect(result.allow).toBe(true);
356
+ expect(result.outcome).toBe("FullRefund");
357
+ expect(result.refundCents).toBe(100_000);
358
+ });
359
+
360
+ it("promoter: blocked once the bash has started", () => {
361
+ const result = decideEventPromoterCancellation({
362
+ who: "promoter",
363
+ nowMs: now,
364
+ bashStartMs: now - 1 * HOUR,
365
+ bashEndMs: now + 4 * HOUR,
366
+ depositCents: 100_000,
367
+ alreadySettled: false,
368
+ });
369
+ expect(result.allow).toBe(false);
370
+ expect(result.outcome).toBe("Blocked");
371
+ });
372
+
373
+ it("admin: always allowed, full refund before bash, NoRefund after", () => {
374
+ const before = decideEventPromoterCancellation({
375
+ who: "admin",
376
+ nowMs: now,
377
+ bashStartMs: now + 100 * HOUR,
378
+ bashEndMs: now + 105 * HOUR,
379
+ depositCents: 100_000,
380
+ alreadySettled: false,
381
+ });
382
+ expect(before.allow).toBe(true);
383
+ expect(before.outcome).toBe("FullRefund");
384
+
385
+ const after = decideEventPromoterCancellation({
386
+ who: "admin",
387
+ nowMs: now,
388
+ bashStartMs: now - 5 * HOUR,
389
+ bashEndMs: now - 1 * HOUR,
390
+ depositCents: 100_000,
391
+ alreadySettled: false,
392
+ });
393
+ expect(after.allow).toBe(true);
394
+ expect(after.outcome).toBe("NoRefund");
395
+ });
396
+ });