@bash-app/bash-common 30.236.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 +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
@@ -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
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import {
3
+ DEFAULT_PRICING_MODEL_ALLOW_LIST_BY_SERVICE_TYPE,
4
+ validateOfferAgainstAllowList,
5
+ } from "../offerAllowListUtils.js";
6
+
7
+ describe("validateOfferAgainstAllowList", () => {
8
+ it("passes when allow-list is empty (no host restriction)", () => {
9
+ expect(
10
+ validateOfferAgainstAllowList({
11
+ offered: "PER_ATTENDEE",
12
+ allowList: [],
13
+ }).ok
14
+ ).toBe(true);
15
+ expect(
16
+ validateOfferAgainstAllowList({
17
+ offered: "FLAT_FEE",
18
+ allowList: null,
19
+ }).ok
20
+ ).toBe(true);
21
+ });
22
+
23
+ it("passes when offered model is on the allow-list", () => {
24
+ expect(
25
+ validateOfferAgainstAllowList({
26
+ offered: "PER_PAID_ATTENDEE",
27
+ allowList: ["FLAT_FEE", "PER_PAID_ATTENDEE"],
28
+ }).ok
29
+ ).toBe(true);
30
+ });
31
+
32
+ it("rejects when offered model is NOT on the allow-list", () => {
33
+ const r = validateOfferAgainstAllowList({
34
+ offered: "PER_BASHPOINTS_ATTENDEE",
35
+ allowList: ["FLAT_FEE", "PER_PAID_ATTENDEE"],
36
+ });
37
+ expect(r.ok).toBe(false);
38
+ if (!r.ok) {
39
+ expect(r.reason).toContain("FLAT_FEE");
40
+ expect(r.reason).toContain("PER_BASHPOINTS_ATTENDEE");
41
+ }
42
+ });
43
+
44
+ it("rejects when offered is missing", () => {
45
+ const r = validateOfferAgainstAllowList({
46
+ offered: null,
47
+ allowList: ["FLAT_FEE"],
48
+ });
49
+ expect(r.ok).toBe(false);
50
+ if (!r.ok) expect(r.reason).toMatch(/missing/);
51
+ });
52
+ });
53
+
54
+ describe("DEFAULT_PRICING_MODEL_ALLOW_LIST_BY_SERVICE_TYPE", () => {
55
+ it("has a default for every service vertical the wizard ships", () => {
56
+ expect(
57
+ DEFAULT_PRICING_MODEL_ALLOW_LIST_BY_SERVICE_TYPE.Vendors.length
58
+ ).toBeGreaterThan(0);
59
+ expect(
60
+ DEFAULT_PRICING_MODEL_ALLOW_LIST_BY_SERVICE_TYPE.Exhibitors.length
61
+ ).toBeGreaterThan(0);
62
+ expect(
63
+ DEFAULT_PRICING_MODEL_ALLOW_LIST_BY_SERVICE_TYPE.Sponsors.length
64
+ ).toBeGreaterThan(0);
65
+ expect(
66
+ DEFAULT_PRICING_MODEL_ALLOW_LIST_BY_SERVICE_TYPE.Venues.length
67
+ ).toBeGreaterThan(0);
68
+ expect(
69
+ DEFAULT_PRICING_MODEL_ALLOW_LIST_BY_SERVICE_TYPE.EntertainmentServices
70
+ .length
71
+ ).toBeGreaterThan(0);
72
+ expect(
73
+ DEFAULT_PRICING_MODEL_ALLOW_LIST_BY_SERVICE_TYPE.EventServices.length
74
+ ).toBeGreaterThan(0);
75
+ });
76
+ });
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import { computeOfferComparisonBands } from "../offerComparisonUtils.js";
3
+
4
+ describe("computeOfferComparisonBands FLAT family", () => {
5
+ it("returns identical bands for FLAT_FEE", () => {
6
+ const r = computeOfferComparisonBands({
7
+ pricingModel: "FLAT_FEE",
8
+ flatRateCents: 50_000,
9
+ });
10
+ expect(r.expectedTotalCents).toBe(50_000);
11
+ expect(r.worstCaseCents).toBe(50_000);
12
+ expect(r.bestCaseCents).toBe(50_000);
13
+ expect(r.reasoning).toContain("Fixed price");
14
+ });
15
+
16
+ it("HOURLY and PER_DAY treated as flat", () => {
17
+ expect(
18
+ computeOfferComparisonBands({
19
+ pricingModel: "HOURLY",
20
+ baseRateCents: 15_000,
21
+ }).expectedTotalCents
22
+ ).toBe(15_000);
23
+ expect(
24
+ computeOfferComparisonBands({
25
+ pricingModel: "PER_DAY",
26
+ baseRateCents: 40_000,
27
+ }).expectedTotalCents
28
+ ).toBe(40_000);
29
+ });
30
+ });
31
+
32
+ describe("computeOfferComparisonBands PER_ATTENDEE family", () => {
33
+ it("expects rate * expected, applies minimum floor", () => {
34
+ const r = computeOfferComparisonBands({
35
+ pricingModel: "PER_ATTENDEE",
36
+ baseRateCents: 500,
37
+ expectedAttendees: 200,
38
+ minimumQuantity: 100,
39
+ minimumChargeCents: 70_000,
40
+ });
41
+ expect(r.expectedTotalCents).toBe(100_000);
42
+ expect(r.bestCaseCents).toBeGreaterThanOrEqual(70_000);
43
+ });
44
+
45
+ it("worst case respects maxBillableCents when present", () => {
46
+ const r = computeOfferComparisonBands({
47
+ pricingModel: "PER_PAID_ATTENDEE",
48
+ baseRateCents: 500,
49
+ expectedAttendees: 200,
50
+ maxBillableCents: 200_000,
51
+ });
52
+ expect(r.worstCaseCents).toBe(200_000);
53
+ });
54
+
55
+ it("worst case defaults to 1.5x when no cap", () => {
56
+ const r = computeOfferComparisonBands({
57
+ pricingModel: "PER_ATTENDEE",
58
+ baseRateCents: 500,
59
+ expectedAttendees: 200,
60
+ });
61
+ expect(r.worstCaseCents).toBe(Math.round(200 * 500 * 1.5));
62
+ });
63
+ });
64
+
65
+ describe("computeOfferComparisonBands PER_BASHPOINTS_ATTENDEE", () => {
66
+ it("returns BP bands only, USD bands null", () => {
67
+ const r = computeOfferComparisonBands({
68
+ pricingModel: "PER_BASHPOINTS_ATTENDEE",
69
+ baseRateBashPoints: 10,
70
+ expectedAttendees: 200,
71
+ minimumQuantity: 100,
72
+ });
73
+ expect(r.expectedTotalCents).toBeNull();
74
+ expect(r.expectedTotalBp).toBe(2000);
75
+ expect(r.bestCaseBp).toBe(1000);
76
+ });
77
+ });
78
+
79
+ describe("computeOfferComparisonBands revenue-share", () => {
80
+ it("expected = basis * pct; worst case zero", () => {
81
+ const r = computeOfferComparisonBands({
82
+ pricingModel: "TICKET_REVENUE_PERCENTAGE",
83
+ expectedBasisCents: 1_000_000,
84
+ percentage: 30,
85
+ });
86
+ expect(r.expectedTotalCents).toBe(300_000);
87
+ expect(r.bestCaseCents).toBe(300_000);
88
+ expect(r.worstCaseCents).toBe(0);
89
+ });
90
+
91
+ it("clamps percentage", () => {
92
+ const r = computeOfferComparisonBands({
93
+ pricingModel: "PRODUCT_SALES_PERCENTAGE",
94
+ expectedBasisCents: 1_000_000,
95
+ percentage: 200,
96
+ });
97
+ expect(r.expectedTotalCents).toBe(1_000_000);
98
+ });
99
+ });
100
+
101
+ describe("computeOfferComparisonBands TIERED", () => {
102
+ it("uses chosen tier as expected, low/high for bands", () => {
103
+ const r = computeOfferComparisonBands({
104
+ pricingModel: "TIERED",
105
+ tieredExpectedTierCents: 200_000,
106
+ tieredLowestTierCents: 100_000,
107
+ tieredHighestTierCents: 500_000,
108
+ });
109
+ expect(r.expectedTotalCents).toBe(200_000);
110
+ expect(r.bestCaseCents).toBe(100_000);
111
+ expect(r.worstCaseCents).toBe(500_000);
112
+ });
113
+ });
114
+
115
+ describe("computeOfferComparisonBands fallback", () => {
116
+ it("treats unknown model as flat", () => {
117
+ const r = computeOfferComparisonBands({
118
+ pricingModel: "MYSTERY_MODEL",
119
+ baseRateCents: 30_000,
120
+ });
121
+ expect(r.expectedTotalCents).toBe(30_000);
122
+ expect(r.reasoning).toContain("Unrecognized");
123
+ });
124
+ });