@bash-app/bash-common 30.236.0 → 30.238.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/dist/__tests__/statusEnums.test.js +88 -7
  2. package/dist/__tests__/statusEnums.test.js.map +1 -1
  3. package/dist/definitions.d.ts +39 -8
  4. package/dist/definitions.d.ts.map +1 -1
  5. package/dist/definitions.js +31 -10
  6. package/dist/definitions.js.map +1 -1
  7. package/dist/extendedSchemas.d.ts +6 -0
  8. package/dist/extendedSchemas.d.ts.map +1 -1
  9. package/dist/extendedSchemas.js.map +1 -1
  10. package/dist/index.d.ts +13 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +16 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/mirroredPrismaEnums.d.ts +145 -0
  15. package/dist/mirroredPrismaEnums.d.ts.map +1 -0
  16. package/dist/mirroredPrismaEnums.js +136 -0
  17. package/dist/mirroredPrismaEnums.js.map +1 -0
  18. package/dist/utils/__tests__/nonprofitUtils.test.d.ts +2 -0
  19. package/dist/utils/__tests__/nonprofitUtils.test.d.ts.map +1 -0
  20. package/dist/utils/__tests__/nonprofitUtils.test.js +84 -0
  21. package/dist/utils/__tests__/nonprofitUtils.test.js.map +1 -0
  22. package/dist/utils/__tests__/paymentUtils.test.js +87 -0
  23. package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
  24. package/dist/utils/nonprofitUtils.d.ts +21 -0
  25. package/dist/utils/nonprofitUtils.d.ts.map +1 -0
  26. package/dist/utils/nonprofitUtils.js +29 -0
  27. package/dist/utils/nonprofitUtils.js.map +1 -0
  28. package/dist/utils/paymentUtils.d.ts +23 -0
  29. package/dist/utils/paymentUtils.d.ts.map +1 -1
  30. package/dist/utils/paymentUtils.js +24 -7
  31. package/dist/utils/paymentUtils.js.map +1 -1
  32. package/dist/utils/service/__tests__/billingCapUtils.test.d.ts +2 -0
  33. package/dist/utils/service/__tests__/billingCapUtils.test.d.ts.map +1 -0
  34. package/dist/utils/service/__tests__/billingCapUtils.test.js +120 -0
  35. package/dist/utils/service/__tests__/billingCapUtils.test.js.map +1 -0
  36. package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.js +1 -1
  37. package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.js.map +1 -1
  38. package/dist/utils/service/__tests__/eventPromoterPhase3.test.d.ts +2 -0
  39. package/dist/utils/service/__tests__/eventPromoterPhase3.test.d.ts.map +1 -0
  40. package/dist/utils/service/__tests__/eventPromoterPhase3.test.js +243 -0
  41. package/dist/utils/service/__tests__/eventPromoterPhase3.test.js.map +1 -0
  42. package/dist/utils/service/__tests__/eventPromoterUtils.test.d.ts +2 -0
  43. package/dist/utils/service/__tests__/eventPromoterUtils.test.d.ts.map +1 -0
  44. package/dist/utils/service/__tests__/eventPromoterUtils.test.js +333 -0
  45. package/dist/utils/service/__tests__/eventPromoterUtils.test.js.map +1 -0
  46. package/dist/utils/service/__tests__/offerAllowListUtils.test.d.ts +2 -0
  47. package/dist/utils/service/__tests__/offerAllowListUtils.test.d.ts.map +1 -0
  48. package/dist/utils/service/__tests__/offerAllowListUtils.test.js +52 -0
  49. package/dist/utils/service/__tests__/offerAllowListUtils.test.js.map +1 -0
  50. package/dist/utils/service/__tests__/offerComparisonUtils.test.d.ts +2 -0
  51. package/dist/utils/service/__tests__/offerComparisonUtils.test.d.ts.map +1 -0
  52. package/dist/utils/service/__tests__/offerComparisonUtils.test.js +111 -0
  53. package/dist/utils/service/__tests__/offerComparisonUtils.test.js.map +1 -0
  54. package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.d.ts +2 -0
  55. package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.d.ts.map +1 -0
  56. package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.js +155 -0
  57. package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.js.map +1 -0
  58. package/dist/utils/service/__tests__/unifiedPricingResolver.test.d.ts +2 -0
  59. package/dist/utils/service/__tests__/unifiedPricingResolver.test.d.ts.map +1 -0
  60. package/dist/utils/service/__tests__/unifiedPricingResolver.test.js +77 -0
  61. package/dist/utils/service/__tests__/unifiedPricingResolver.test.js.map +1 -0
  62. package/dist/utils/service/__tests__/venueFBSettlementUtils.test.d.ts +2 -0
  63. package/dist/utils/service/__tests__/venueFBSettlementUtils.test.d.ts.map +1 -0
  64. package/dist/utils/service/__tests__/venueFBSettlementUtils.test.js +157 -0
  65. package/dist/utils/service/__tests__/venueFBSettlementUtils.test.js.map +1 -0
  66. package/dist/utils/service/__tests__/venueSettlementUtils.test.d.ts +2 -0
  67. package/dist/utils/service/__tests__/venueSettlementUtils.test.d.ts.map +1 -0
  68. package/dist/utils/service/__tests__/venueSettlementUtils.test.js +175 -0
  69. package/dist/utils/service/__tests__/venueSettlementUtils.test.js.map +1 -0
  70. package/dist/utils/service/__tests__/volunteerCompensationUtils.test.d.ts +2 -0
  71. package/dist/utils/service/__tests__/volunteerCompensationUtils.test.d.ts.map +1 -0
  72. package/dist/utils/service/__tests__/volunteerCompensationUtils.test.js +118 -0
  73. package/dist/utils/service/__tests__/volunteerCompensationUtils.test.js.map +1 -0
  74. package/dist/utils/service/apiServiceBookingApiUtils.d.ts.map +1 -1
  75. package/dist/utils/service/apiServiceBookingApiUtils.js +4 -0
  76. package/dist/utils/service/apiServiceBookingApiUtils.js.map +1 -1
  77. package/dist/utils/service/billingCapUtils.d.ts +86 -0
  78. package/dist/utils/service/billingCapUtils.d.ts.map +1 -0
  79. package/dist/utils/service/billingCapUtils.js +96 -0
  80. package/dist/utils/service/billingCapUtils.js.map +1 -0
  81. package/dist/utils/service/cancellationPolicyRefundResolver.d.ts +1 -1
  82. package/dist/utils/service/cancellationPolicyRefundResolver.d.ts.map +1 -1
  83. package/dist/utils/service/cancellationPolicyRefundResolver.js +1 -1
  84. package/dist/utils/service/cancellationPolicyRefundResolver.js.map +1 -1
  85. package/dist/utils/service/eventPromoterUtils.d.ts +319 -0
  86. package/dist/utils/service/eventPromoterUtils.d.ts.map +1 -0
  87. package/dist/utils/service/eventPromoterUtils.js +372 -0
  88. package/dist/utils/service/eventPromoterUtils.js.map +1 -0
  89. package/dist/utils/service/offerAllowListUtils.d.ts +42 -0
  90. package/dist/utils/service/offerAllowListUtils.d.ts.map +1 -0
  91. package/dist/utils/service/offerAllowListUtils.js +49 -0
  92. package/dist/utils/service/offerAllowListUtils.js.map +1 -0
  93. package/dist/utils/service/offerComparisonUtils.d.ts +69 -0
  94. package/dist/utils/service/offerComparisonUtils.d.ts.map +1 -0
  95. package/dist/utils/service/offerComparisonUtils.js +110 -0
  96. package/dist/utils/service/offerComparisonUtils.js.map +1 -0
  97. package/dist/utils/service/serviceBookingExposureUtils.d.ts +111 -0
  98. package/dist/utils/service/serviceBookingExposureUtils.d.ts.map +1 -0
  99. package/dist/utils/service/serviceBookingExposureUtils.js +133 -0
  100. package/dist/utils/service/serviceBookingExposureUtils.js.map +1 -0
  101. package/dist/utils/service/serviceBookingTypes.d.ts +1 -1
  102. package/dist/utils/service/serviceBookingTypes.d.ts.map +1 -1
  103. package/dist/utils/service/serviceUtils.d.ts +2 -1
  104. package/dist/utils/service/serviceUtils.d.ts.map +1 -1
  105. package/dist/utils/service/serviceUtils.js +17 -16
  106. package/dist/utils/service/serviceUtils.js.map +1 -1
  107. package/dist/utils/service/unifiedPricingResolver.d.ts +67 -0
  108. package/dist/utils/service/unifiedPricingResolver.d.ts.map +1 -0
  109. package/dist/utils/service/unifiedPricingResolver.js +75 -0
  110. package/dist/utils/service/unifiedPricingResolver.js.map +1 -0
  111. package/dist/utils/service/venueFBSettlementUtils.d.ts +122 -0
  112. package/dist/utils/service/venueFBSettlementUtils.d.ts.map +1 -0
  113. package/dist/utils/service/venueFBSettlementUtils.js +106 -0
  114. package/dist/utils/service/venueFBSettlementUtils.js.map +1 -0
  115. package/dist/utils/service/venueSettlementUtils.d.ts +107 -0
  116. package/dist/utils/service/venueSettlementUtils.d.ts.map +1 -0
  117. package/dist/utils/service/venueSettlementUtils.js +117 -0
  118. package/dist/utils/service/venueSettlementUtils.js.map +1 -0
  119. package/dist/utils/service/volunteerCompensationUtils.d.ts +75 -0
  120. package/dist/utils/service/volunteerCompensationUtils.d.ts.map +1 -0
  121. package/dist/utils/service/volunteerCompensationUtils.js +88 -0
  122. package/dist/utils/service/volunteerCompensationUtils.js.map +1 -0
  123. package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.d.ts +2 -0
  124. package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.d.ts.map +1 -0
  125. package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.js +67 -0
  126. package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.js.map +1 -0
  127. package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.d.ts +2 -0
  128. package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.d.ts.map +1 -0
  129. package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.js +71 -0
  130. package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.js.map +1 -0
  131. package/dist/utils/venueLoyalty/eligibilityUtils.d.ts +33 -0
  132. package/dist/utils/venueLoyalty/eligibilityUtils.d.ts.map +1 -0
  133. package/dist/utils/venueLoyalty/eligibilityUtils.js +49 -0
  134. package/dist/utils/venueLoyalty/eligibilityUtils.js.map +1 -0
  135. package/dist/utils/venueLoyalty/redemptionCodeUtils.d.ts +35 -0
  136. package/dist/utils/venueLoyalty/redemptionCodeUtils.d.ts.map +1 -0
  137. package/dist/utils/venueLoyalty/redemptionCodeUtils.js +74 -0
  138. package/dist/utils/venueLoyalty/redemptionCodeUtils.js.map +1 -0
  139. package/dist/venueLoyaltyRedemption.d.ts +146 -1
  140. package/dist/venueLoyaltyRedemption.d.ts.map +1 -1
  141. package/dist/venueLoyaltyRedemption.js +18 -2
  142. package/dist/venueLoyaltyRedemption.js.map +1 -1
  143. package/package.json +1 -1
  144. package/prisma/schema.prisma +609 -214
  145. package/src/__tests__/statusEnums.test.ts +119 -11
  146. package/src/definitions.ts +57 -14
  147. package/src/extendedSchemas.ts +6 -0
  148. package/src/index.ts +16 -6
  149. package/src/mirroredPrismaEnums.ts +181 -0
  150. package/src/utils/__tests__/nonprofitUtils.test.ts +117 -0
  151. package/src/utils/__tests__/paymentUtils.test.ts +93 -0
  152. package/src/utils/nonprofitUtils.ts +44 -0
  153. package/src/utils/paymentUtils.ts +33 -7
  154. package/src/utils/service/__tests__/billingCapUtils.test.ts +175 -0
  155. package/src/utils/service/__tests__/cancellationPolicyRefundResolver.test.ts +1 -1
  156. package/src/utils/service/__tests__/eventPromoterPhase3.test.ts +296 -0
  157. package/src/utils/service/__tests__/eventPromoterUtils.test.ts +396 -0
  158. package/src/utils/service/__tests__/offerAllowListUtils.test.ts +76 -0
  159. package/src/utils/service/__tests__/offerComparisonUtils.test.ts +124 -0
  160. package/src/utils/service/__tests__/serviceBookingExposureUtils.test.ts +171 -0
  161. package/src/utils/service/__tests__/unifiedPricingResolver.test.ts +83 -0
  162. package/src/utils/service/__tests__/venueSettlementUtils.test.ts +219 -0
  163. package/src/utils/service/__tests__/volunteerCompensationUtils.test.ts +172 -0
  164. package/src/utils/service/apiServiceBookingApiUtils.ts +4 -0
  165. package/src/utils/service/billingCapUtils.ts +145 -0
  166. package/src/utils/service/cancellationPolicyRefundResolver.ts +1 -1
  167. package/src/utils/service/eventPromoterUtils.ts +644 -0
  168. package/src/utils/service/offerAllowListUtils.ts +81 -0
  169. package/src/utils/service/offerComparisonUtils.ts +197 -0
  170. package/src/utils/service/serviceBookingExposureUtils.ts +218 -0
  171. package/src/utils/service/serviceBookingTypes.ts +4 -0
  172. package/src/utils/service/serviceUtils.ts +18 -21
  173. package/src/utils/service/unifiedPricingResolver.ts +149 -0
  174. package/src/utils/service/venueSettlementUtils.ts +198 -0
  175. package/src/utils/service/volunteerCompensationUtils.ts +145 -0
  176. package/src/utils/venueLoyalty/__tests__/eligibilityUtils.test.ts +103 -0
  177. package/src/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.ts +92 -0
  178. package/src/utils/venueLoyalty/eligibilityUtils.ts +84 -0
  179. package/src/utils/venueLoyalty/redemptionCodeUtils.ts +94 -0
  180. package/src/venueLoyaltyRedemption.ts +177 -2
@@ -0,0 +1,171 @@
1
+ import {
2
+ computeServiceBookingExposure,
3
+ isVariablePricingModel,
4
+ summarizeServiceBookingExposures,
5
+ } from "../serviceBookingExposureUtils";
6
+
7
+ describe("isVariablePricingModel", () => {
8
+ it("returns true for attendee- and revenue-driven models", () => {
9
+ expect(isVariablePricingModel("PER_ATTENDEE")).toBe(true);
10
+ expect(isVariablePricingModel("PER_PAID_ATTENDEE")).toBe(true);
11
+ expect(isVariablePricingModel("PER_BASHPOINTS_ATTENDEE")).toBe(true);
12
+ expect(isVariablePricingModel("TICKET_REVENUE_PERCENTAGE")).toBe(true);
13
+ expect(isVariablePricingModel("PRODUCT_SALES_PERCENTAGE")).toBe(true);
14
+ expect(isVariablePricingModel("TIERED")).toBe(true);
15
+ expect(isVariablePricingModel("CUSTOM")).toBe(true);
16
+ });
17
+
18
+ it("returns false for flat models", () => {
19
+ expect(isVariablePricingModel("FLAT_FEE")).toBe(false);
20
+ expect(isVariablePricingModel("HOURLY")).toBe(false);
21
+ expect(isVariablePricingModel("PER_DAY")).toBe(false);
22
+ });
23
+
24
+ it("returns false for unknown / null inputs", () => {
25
+ expect(isVariablePricingModel(null)).toBe(false);
26
+ expect(isVariablePricingModel(undefined)).toBe(false);
27
+ expect(isVariablePricingModel("NOT_A_MODEL")).toBe(false);
28
+ });
29
+ });
30
+
31
+ describe("computeServiceBookingExposure", () => {
32
+ it("treats null pricingModel as flat — expected equals worst from totalATBCents", () => {
33
+ const out = computeServiceBookingExposure({
34
+ bookingId: "b1",
35
+ label: "Legacy booking",
36
+ pricingModel: null,
37
+ totalATBCents: 150_000,
38
+ });
39
+ expect(out.isVariable).toBe(false);
40
+ expect(out.expectedCents).toBe(150_000);
41
+ expect(out.worstCaseCents).toBe(150_000);
42
+ });
43
+
44
+ it("uses flatRateCents when totalATBCents missing", () => {
45
+ const out = computeServiceBookingExposure({
46
+ bookingId: "b1",
47
+ label: "Photo Booth",
48
+ pricingModel: "FLAT_FEE",
49
+ flatRateCents: 50_000,
50
+ });
51
+ expect(out.expectedCents).toBe(50_000);
52
+ expect(out.worstCaseCents).toBe(50_000);
53
+ });
54
+
55
+ it("computes PER_ATTENDEE expected and worst case from attendee fields", () => {
56
+ const out = computeServiceBookingExposure({
57
+ bookingId: "b1",
58
+ label: "DJ",
59
+ pricingModel: "PER_ATTENDEE",
60
+ baseRateCents: 500,
61
+ expectedAttendeesAtBooking: 200,
62
+ maxAttendeesAttributable: 250,
63
+ });
64
+ expect(out.isVariable).toBe(true);
65
+ expect(out.expectedCents).toBe(100_000); // 200 * 500
66
+ expect(out.worstCaseCents).toBe(125_000); // 250 * 500
67
+ });
68
+
69
+ it("prefers maxBillableCents over the attendee ceiling for worst case", () => {
70
+ const out = computeServiceBookingExposure({
71
+ bookingId: "b1",
72
+ label: "DJ",
73
+ pricingModel: "PER_ATTENDEE",
74
+ baseRateCents: 500,
75
+ expectedAttendeesAtBooking: 200,
76
+ maxAttendeesAttributable: 250,
77
+ maxBillableCents: 200_000,
78
+ });
79
+ expect(out.worstCaseCents).toBe(200_000);
80
+ });
81
+
82
+ it("falls back to 1.5x expected when no caps set on variable bookings", () => {
83
+ const out = computeServiceBookingExposure({
84
+ bookingId: "b1",
85
+ label: "DJ",
86
+ pricingModel: "PER_ATTENDEE",
87
+ baseRateCents: 500,
88
+ expectedAttendeesAtBooking: 200,
89
+ });
90
+ expect(out.expectedCents).toBe(100_000);
91
+ expect(out.worstCaseCents).toBe(150_000); // 100_000 * 1.5
92
+ });
93
+
94
+ it("clamps worst case never below expected", () => {
95
+ // Pathological: maxBillableCents below expected.
96
+ const out = computeServiceBookingExposure({
97
+ bookingId: "b1",
98
+ label: "DJ",
99
+ pricingModel: "PER_ATTENDEE",
100
+ baseRateCents: 500,
101
+ expectedAttendeesAtBooking: 200,
102
+ maxBillableCents: 50_000, // lower than expected 100_000
103
+ });
104
+ expect(out.expectedCents).toBe(100_000);
105
+ expect(out.worstCaseCents).toBe(100_000);
106
+ });
107
+
108
+ it("handles revenue-share by using totalATB as expected, 1.5x as worst", () => {
109
+ const out = computeServiceBookingExposure({
110
+ bookingId: "b1",
111
+ label: "Venue (rev-share)",
112
+ pricingModel: "TICKET_REVENUE_PERCENTAGE",
113
+ totalATBCents: 80_000,
114
+ });
115
+ expect(out.isVariable).toBe(true);
116
+ expect(out.expectedCents).toBe(80_000);
117
+ expect(out.worstCaseCents).toBe(120_000); // 80_000 * 1.5
118
+ });
119
+
120
+ it("returns zeros when nothing usable is provided", () => {
121
+ const out = computeServiceBookingExposure({
122
+ bookingId: "b1",
123
+ label: "Mystery",
124
+ pricingModel: "CUSTOM",
125
+ });
126
+ expect(out.expectedCents).toBe(0);
127
+ expect(out.worstCaseCents).toBe(0);
128
+ });
129
+ });
130
+
131
+ describe("summarizeServiceBookingExposures", () => {
132
+ it("sums expected + worst-case across mixed bookings", () => {
133
+ const out = summarizeServiceBookingExposures([
134
+ {
135
+ bookingId: "a",
136
+ label: "DJ",
137
+ pricingModel: "PER_ATTENDEE",
138
+ baseRateCents: 500,
139
+ expectedAttendeesAtBooking: 200,
140
+ maxAttendeesAttributable: 250,
141
+ },
142
+ {
143
+ bookingId: "b",
144
+ label: "Photo Booth",
145
+ pricingModel: "FLAT_FEE",
146
+ totalATBCents: 50_000,
147
+ },
148
+ {
149
+ bookingId: "c",
150
+ label: "Caterer (legacy)",
151
+ pricingModel: null,
152
+ totalATBCents: 200_000,
153
+ },
154
+ ]);
155
+ // Expected: 100_000 + 50_000 + 200_000 = 350_000
156
+ expect(out.projectedTotalCents).toBe(350_000);
157
+ // Worst: 125_000 + 50_000 + 200_000 = 375_000
158
+ expect(out.worstCaseTotalCents).toBe(375_000);
159
+ expect(out.byBooking).toHaveLength(3);
160
+ expect(out.byBooking[0].isVariable).toBe(true);
161
+ expect(out.byBooking[1].isVariable).toBe(false);
162
+ expect(out.byBooking[2].isVariable).toBe(false);
163
+ });
164
+
165
+ it("returns zeros for an empty list", () => {
166
+ const out = summarizeServiceBookingExposures([]);
167
+ expect(out.projectedTotalCents).toBe(0);
168
+ expect(out.worstCaseTotalCents).toBe(0);
169
+ expect(out.byBooking).toEqual([]);
170
+ });
171
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import { resolveUnifiedPricingBlock } from "../unifiedPricingResolver.js";
3
+
4
+ describe("resolveUnifiedPricingBlock", () => {
5
+ it("returns the empty block when nothing is configured", () => {
6
+ const r = resolveUnifiedPricingBlock({});
7
+ expect(r.fromUnifiedBlock).toBe(false);
8
+ expect(r.noRateConfigured).toBe(true);
9
+ expect(r.baseRateCents).toBeNull();
10
+ });
11
+
12
+ it("prefers the unified block from the first subtype that has one", () => {
13
+ const r = resolveUnifiedPricingBlock({
14
+ vendor: {
15
+ pricingModel: "FLAT_FEE",
16
+ baseRateCents: 50_000,
17
+ rateUnit: "event",
18
+ },
19
+ legacyRate: { ratePerUnitCents: 99_999 },
20
+ });
21
+ expect(r.fromUnifiedBlock).toBe(true);
22
+ expect(r.baseRateCents).toBe(50_000);
23
+ expect(r.rateUnit).toBe("event");
24
+ });
25
+
26
+ it("walks all candidate subtypes when earlier ones are empty", () => {
27
+ const r = resolveUnifiedPricingBlock({
28
+ eventService: null,
29
+ vendor: null,
30
+ exhibitor: null,
31
+ sponsor: {
32
+ pricingModel: "TIERED",
33
+ baseRateCents: 100_000,
34
+ },
35
+ });
36
+ expect(r.fromUnifiedBlock).toBe(true);
37
+ expect(r.pricingModel).toBe("TIERED");
38
+ expect(r.baseRateCents).toBe(100_000);
39
+ });
40
+
41
+ it("recognizes a unified signal even when only baseRateBashPoints is set", () => {
42
+ const r = resolveUnifiedPricingBlock({
43
+ eventService: {
44
+ pricingModel: null,
45
+ baseRateCents: null,
46
+ baseRateBashPoints: 25,
47
+ },
48
+ });
49
+ expect(r.fromUnifiedBlock).toBe(true);
50
+ expect(r.baseRateBashPoints).toBe(25);
51
+ expect(r.noRateConfigured).toBe(false);
52
+ });
53
+
54
+ it("falls back to legacy ServiceRate when no unified block exists", () => {
55
+ const r = resolveUnifiedPricingBlock({
56
+ legacyRate: {
57
+ ratePerUnitCents: 12_345,
58
+ rateUnit: "hour",
59
+ minimumQuantity: 4,
60
+ },
61
+ });
62
+ expect(r.fromUnifiedBlock).toBe(false);
63
+ expect(r.pricingModel).toBe("FLAT_FEE");
64
+ expect(r.baseRateCents).toBe(12_345);
65
+ expect(r.minimumQuantity).toBe(4);
66
+ });
67
+
68
+ it("falls through to empty when both unified and legacy are unset", () => {
69
+ const r = resolveUnifiedPricingBlock({
70
+ vendor: { pricingModel: null, baseRateCents: null },
71
+ legacyRate: null,
72
+ });
73
+ expect(r.noRateConfigured).toBe(true);
74
+ });
75
+
76
+ it("noRateConfigured stays true when unified has minimumQuantity but no rate", () => {
77
+ const r = resolveUnifiedPricingBlock({
78
+ vendor: { minimumQuantity: 10 },
79
+ });
80
+ expect(r.fromUnifiedBlock).toBe(true);
81
+ expect(r.noRateConfigured).toBe(true);
82
+ });
83
+ });
@@ -0,0 +1,219 @@
1
+ import {
2
+ DEFAULT_FB_CARRYOVER_EXPIRY_DAYS,
3
+ REVENUE_SHARE_CANCELLATION_COPY,
4
+ FB_MINIMUM_CANCELLATION_COPY,
5
+ computeFBMinimumShortfallCents,
6
+ computeRevenueShareCents,
7
+ defaultFBCarryoverExpiresAt,
8
+ resolveFBMinimumForDate,
9
+ } from "../venueSettlementUtils.js";
10
+
11
+ describe("computeFBMinimumShortfallCents", () => {
12
+ it("computes a clean shortfall when subtotal < minimum", () => {
13
+ const out = computeFBMinimumShortfallCents({
14
+ fbMinimumCents: 1_000_000, // $10k
15
+ fbActualSpendCents: 750_000, // $7,500 subtotal
16
+ serviceChargeCents: 0,
17
+ taxCents: 0,
18
+ serviceChargeCountsTowardMinimum: true,
19
+ includesTax: false,
20
+ });
21
+ expect(out.coveredSpendCents).toBe(750_000);
22
+ expect(out.shortfallCents).toBe(250_000);
23
+ expect(out.excessCents).toBe(0);
24
+ expect(out.metExactly).toBe(false);
25
+ });
26
+
27
+ it("counts service charge toward minimum when the flag is on", () => {
28
+ const out = computeFBMinimumShortfallCents({
29
+ fbMinimumCents: 1_000_000,
30
+ fbActualSpendCents: 800_000,
31
+ serviceChargeCents: 160_000, // 20% svc
32
+ taxCents: 80_000,
33
+ serviceChargeCountsTowardMinimum: true,
34
+ includesTax: false,
35
+ });
36
+ expect(out.coveredSpendCents).toBe(960_000);
37
+ expect(out.shortfallCents).toBe(40_000);
38
+ });
39
+
40
+ it("excludes service charge when the flag is off", () => {
41
+ const out = computeFBMinimumShortfallCents({
42
+ fbMinimumCents: 1_000_000,
43
+ fbActualSpendCents: 800_000,
44
+ serviceChargeCents: 160_000,
45
+ taxCents: 80_000,
46
+ serviceChargeCountsTowardMinimum: false,
47
+ includesTax: false,
48
+ });
49
+ expect(out.coveredSpendCents).toBe(800_000);
50
+ expect(out.shortfallCents).toBe(200_000);
51
+ });
52
+
53
+ it("includes tax when the includesTax flag is set", () => {
54
+ const out = computeFBMinimumShortfallCents({
55
+ fbMinimumCents: 1_000_000,
56
+ fbActualSpendCents: 800_000,
57
+ serviceChargeCents: 0,
58
+ taxCents: 120_000,
59
+ serviceChargeCountsTowardMinimum: true,
60
+ includesTax: true,
61
+ });
62
+ expect(out.coveredSpendCents).toBe(920_000);
63
+ expect(out.shortfallCents).toBe(80_000);
64
+ });
65
+
66
+ it("returns excess (carryover-eligible) when covered exceeds minimum", () => {
67
+ const out = computeFBMinimumShortfallCents({
68
+ fbMinimumCents: 1_000_000,
69
+ fbActualSpendCents: 1_250_000,
70
+ serviceChargeCents: 0,
71
+ taxCents: 0,
72
+ serviceChargeCountsTowardMinimum: true,
73
+ includesTax: false,
74
+ });
75
+ expect(out.shortfallCents).toBe(0);
76
+ expect(out.excessCents).toBe(250_000);
77
+ });
78
+
79
+ it("marks metExactly when covered === minimum", () => {
80
+ const out = computeFBMinimumShortfallCents({
81
+ fbMinimumCents: 1_000_000,
82
+ fbActualSpendCents: 1_000_000,
83
+ serviceChargeCents: 0,
84
+ taxCents: 0,
85
+ serviceChargeCountsTowardMinimum: true,
86
+ includesTax: false,
87
+ });
88
+ expect(out.metExactly).toBe(true);
89
+ expect(out.shortfallCents).toBe(0);
90
+ expect(out.excessCents).toBe(0);
91
+ });
92
+
93
+ it("treats negative inputs as zero", () => {
94
+ const out = computeFBMinimumShortfallCents({
95
+ fbMinimumCents: 1_000_000,
96
+ fbActualSpendCents: -50_000,
97
+ serviceChargeCents: -10_000,
98
+ taxCents: -10_000,
99
+ serviceChargeCountsTowardMinimum: true,
100
+ includesTax: true,
101
+ });
102
+ expect(out.coveredSpendCents).toBe(0);
103
+ expect(out.shortfallCents).toBe(1_000_000);
104
+ });
105
+ });
106
+
107
+ describe("computeRevenueShareCents", () => {
108
+ it("splits 70/30 cleanly", () => {
109
+ expect(
110
+ computeRevenueShareCents({ basisCents: 1_000_000, hostPct: 70 })
111
+ ).toEqual({ hostCents: 700_000, venueCents: 300_000 });
112
+ });
113
+
114
+ it("clamps host pct to [0, 100]", () => {
115
+ expect(
116
+ computeRevenueShareCents({ basisCents: 1_000_000, hostPct: 250 })
117
+ ).toEqual({ hostCents: 1_000_000, venueCents: 0 });
118
+ expect(
119
+ computeRevenueShareCents({ basisCents: 1_000_000, hostPct: -50 })
120
+ ).toEqual({ hostCents: 0, venueCents: 1_000_000 });
121
+ });
122
+
123
+ it("returns zero for a zero basis", () => {
124
+ expect(
125
+ computeRevenueShareCents({ basisCents: 0, hostPct: 70 })
126
+ ).toEqual({ hostCents: 0, venueCents: 0 });
127
+ });
128
+ });
129
+
130
+ describe("resolveFBMinimumForDate", () => {
131
+ const TUE = new Date("2026-05-12T20:00:00Z"); // Tuesday
132
+ const SAT = new Date("2026-05-16T22:00:00Z"); // Saturday
133
+
134
+ it("picks the day-of-week minimum from the schedule", () => {
135
+ expect(
136
+ resolveFBMinimumForDate({
137
+ schedule: { Mon: 800_000, Tue: 900_000, Sat: 4_000_000 },
138
+ eventStartDateTime: TUE,
139
+ })
140
+ ).toBe(900_000);
141
+ expect(
142
+ resolveFBMinimumForDate({
143
+ schedule: { Mon: 800_000, Tue: 900_000, Sat: 4_000_000 },
144
+ eventStartDateTime: SAT,
145
+ })
146
+ ).toBe(4_000_000);
147
+ });
148
+
149
+ it("returns null when the schedule has no entry for the day", () => {
150
+ expect(
151
+ resolveFBMinimumForDate({
152
+ schedule: { Mon: 800_000 },
153
+ eventStartDateTime: TUE,
154
+ })
155
+ ).toBeNull();
156
+ });
157
+
158
+ it("returns null when the schedule is missing entirely", () => {
159
+ expect(
160
+ resolveFBMinimumForDate({
161
+ schedule: null,
162
+ eventStartDateTime: TUE,
163
+ })
164
+ ).toBeNull();
165
+ });
166
+
167
+ it("prefers an override window over the schedule when the date matches", () => {
168
+ const out = resolveFBMinimumForDate({
169
+ schedule: { Tue: 900_000 },
170
+ eventStartDateTime: new Date("2026-12-31T20:00:00Z"),
171
+ overrides: [
172
+ {
173
+ startDate: new Date("2026-12-30T00:00:00Z"),
174
+ endDate: new Date("2027-01-01T23:59:59Z"),
175
+ minimumCents: 10_000_000,
176
+ },
177
+ ],
178
+ });
179
+ expect(out).toBe(10_000_000);
180
+ });
181
+
182
+ it("ignores overrides whose window does not contain the bash date", () => {
183
+ const out = resolveFBMinimumForDate({
184
+ schedule: { Tue: 900_000 },
185
+ eventStartDateTime: TUE,
186
+ overrides: [
187
+ {
188
+ startDate: new Date("2026-12-30T00:00:00Z"),
189
+ endDate: new Date("2027-01-01T23:59:59Z"),
190
+ minimumCents: 10_000_000,
191
+ },
192
+ ],
193
+ });
194
+ expect(out).toBe(900_000);
195
+ });
196
+ });
197
+
198
+ describe("defaultFBCarryoverExpiresAt", () => {
199
+ it("defaults to 365 days from issuance", () => {
200
+ const issued = new Date("2026-01-01T00:00:00Z");
201
+ const expires = defaultFBCarryoverExpiresAt(issued);
202
+ expect(expires.getTime() - issued.getTime()).toBe(
203
+ DEFAULT_FB_CARRYOVER_EXPIRY_DAYS * 86_400_000
204
+ );
205
+ });
206
+
207
+ it("respects an explicit days override", () => {
208
+ const issued = new Date("2026-01-01T00:00:00Z");
209
+ const expires = defaultFBCarryoverExpiresAt(issued, 30);
210
+ expect(expires.getTime() - issued.getTime()).toBe(30 * 86_400_000);
211
+ });
212
+ });
213
+
214
+ describe("cancellation copy", () => {
215
+ it("exposes F&B + revenue-share cancellation snippets for the UI", () => {
216
+ expect(FB_MINIMUM_CANCELLATION_COPY).toMatch(/F&B minimum/);
217
+ expect(REVENUE_SHARE_CANCELLATION_COPY).toMatch(/revenue-share/i);
218
+ });
219
+ });
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import {
3
+ DEFAULT_VOLUNTEER_STIPEND_BP_PER_HOUR,
4
+ DEFAULT_VOLUNTEER_STIPEND_BP_PER_SHIFT,
5
+ VOLUNTEER_COMPENSATION_DISCLAIMER,
6
+ VOLUNTEER_RATE_UNITS,
7
+ compensationTypeIncludesPerks,
8
+ compensationTypePaysBashPoints,
9
+ defaultStipendBpFor,
10
+ validateVolunteerCompensation,
11
+ volunteerCompensationSummary,
12
+ } from "../volunteerCompensationUtils.js";
13
+
14
+ describe("defaultStipendBpFor", () => {
15
+ it("defaults to per-shift starter BP when rateUnit is shift / null / unknown", () => {
16
+ expect(defaultStipendBpFor({ rateUnit: "shift" })).toBe(
17
+ DEFAULT_VOLUNTEER_STIPEND_BP_PER_SHIFT
18
+ );
19
+ expect(defaultStipendBpFor({ rateUnit: null })).toBe(
20
+ DEFAULT_VOLUNTEER_STIPEND_BP_PER_SHIFT
21
+ );
22
+ expect(defaultStipendBpFor({ rateUnit: "garbage" })).toBe(
23
+ DEFAULT_VOLUNTEER_STIPEND_BP_PER_SHIFT
24
+ );
25
+ });
26
+
27
+ it("returns the per-hour starter BP when rateUnit is hour", () => {
28
+ expect(defaultStipendBpFor({ rateUnit: "hour" })).toBe(
29
+ DEFAULT_VOLUNTEER_STIPEND_BP_PER_HOUR
30
+ );
31
+ });
32
+ });
33
+
34
+ describe("VOLUNTEER_RATE_UNITS", () => {
35
+ it("locks the allowed unit set to shift and hour", () => {
36
+ expect([...VOLUNTEER_RATE_UNITS]).toEqual(["shift", "hour"]);
37
+ });
38
+ });
39
+
40
+ describe("compensationTypePaysBashPoints / IncludesPerks", () => {
41
+ it("BashPointsStipend pays BP, no perks", () => {
42
+ expect(compensationTypePaysBashPoints("BashPointsStipend")).toBe(true);
43
+ expect(compensationTypeIncludesPerks("BashPointsStipend")).toBe(false);
44
+ });
45
+
46
+ it("PerksOnly has perks, no BP", () => {
47
+ expect(compensationTypePaysBashPoints("PerksOnly")).toBe(false);
48
+ expect(compensationTypeIncludesPerks("PerksOnly")).toBe(true);
49
+ });
50
+
51
+ it("BashPointsAndPerks has both", () => {
52
+ expect(compensationTypePaysBashPoints("BashPointsAndPerks")).toBe(true);
53
+ expect(compensationTypeIncludesPerks("BashPointsAndPerks")).toBe(true);
54
+ });
55
+
56
+ it("None has neither", () => {
57
+ expect(compensationTypePaysBashPoints("None")).toBe(false);
58
+ expect(compensationTypeIncludesPerks("None")).toBe(false);
59
+ });
60
+ });
61
+
62
+ describe("validateVolunteerCompensation", () => {
63
+ it("ok for pure-volunteer", () => {
64
+ expect(
65
+ validateVolunteerCompensation({ compensationType: "None" }).ok
66
+ ).toBe(true);
67
+ });
68
+
69
+ it("requires a stipend amount + rateUnit when paying BashPoints", () => {
70
+ expect(
71
+ validateVolunteerCompensation({
72
+ compensationType: "BashPointsStipend",
73
+ bashPointsStipend: null,
74
+ rateUnit: "shift",
75
+ }).ok
76
+ ).toBe(false);
77
+ expect(
78
+ validateVolunteerCompensation({
79
+ compensationType: "BashPointsStipend",
80
+ bashPointsStipend: 100,
81
+ rateUnit: null,
82
+ }).ok
83
+ ).toBe(false);
84
+ expect(
85
+ validateVolunteerCompensation({
86
+ compensationType: "BashPointsStipend",
87
+ bashPointsStipend: 100,
88
+ rateUnit: "shift",
89
+ }).ok
90
+ ).toBe(true);
91
+ });
92
+
93
+ it("rejects a leftover stipend on a non-BP comp type", () => {
94
+ const r = validateVolunteerCompensation({
95
+ compensationType: "PerksOnly",
96
+ bashPointsStipend: 50,
97
+ perksDescription: "Comp ticket",
98
+ });
99
+ expect(r.ok).toBe(false);
100
+ });
101
+
102
+ it("requires perksDescription when perks are included", () => {
103
+ expect(
104
+ validateVolunteerCompensation({
105
+ compensationType: "PerksOnly",
106
+ perksDescription: null,
107
+ }).ok
108
+ ).toBe(false);
109
+ expect(
110
+ validateVolunteerCompensation({
111
+ compensationType: "PerksOnly",
112
+ perksDescription: "Comp ticket + drink wristband",
113
+ }).ok
114
+ ).toBe(true);
115
+ });
116
+
117
+ it("ok for BashPointsAndPerks with everything filled in", () => {
118
+ expect(
119
+ validateVolunteerCompensation({
120
+ compensationType: "BashPointsAndPerks",
121
+ bashPointsStipend: 150,
122
+ rateUnit: "shift",
123
+ perksDescription: "Comp ticket",
124
+ }).ok
125
+ ).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe("volunteerCompensationSummary", () => {
130
+ it("None → pure volunteer copy", () => {
131
+ expect(
132
+ volunteerCompensationSummary({ compensationType: "None" })
133
+ ).toMatch(/Pure volunteer/);
134
+ });
135
+
136
+ it("BashPointsStipend includes amount + unit", () => {
137
+ expect(
138
+ volunteerCompensationSummary({
139
+ compensationType: "BashPointsStipend",
140
+ bashPointsStipend: 100,
141
+ rateUnit: "shift",
142
+ })
143
+ ).toBe("100 BashPoints per shift.");
144
+ });
145
+
146
+ it("PerksOnly echoes the perks description", () => {
147
+ expect(
148
+ volunteerCompensationSummary({
149
+ compensationType: "PerksOnly",
150
+ perksDescription: "Comp ticket + drink wristband",
151
+ })
152
+ ).toContain("Comp ticket");
153
+ });
154
+
155
+ it("BashPointsAndPerks shows both", () => {
156
+ const out = volunteerCompensationSummary({
157
+ compensationType: "BashPointsAndPerks",
158
+ bashPointsStipend: 150,
159
+ rateUnit: "shift",
160
+ perksDescription: "Comp ticket",
161
+ });
162
+ expect(out).toContain("150 BashPoints");
163
+ expect(out).toContain("Comp ticket");
164
+ });
165
+ });
166
+
167
+ describe("disclaimer copy", () => {
168
+ it("makes clear volunteers are not employees or contractors and BP isn't wages", () => {
169
+ expect(VOLUNTEER_COMPENSATION_DISCLAIMER).toMatch(/not employees/i);
170
+ expect(VOLUNTEER_COMPENSATION_DISCLAIMER).toMatch(/not wages/i);
171
+ });
172
+ });
@@ -276,5 +276,9 @@ export function servicePriceToBookToApiBookingBase({
276
276
  packages: [],
277
277
  allowPromiseToPay: allowPromiseToPay,
278
278
  isFreeGuest: isFreeGuest,
279
+ // Phase 3 — providers opt in per-booking when they want BP-paid tickets
280
+ // to count toward PER_PAID_ATTENDEE settlement. Defaults to false so
281
+ // legacy bookings keep their original counting semantics.
282
+ countsBashPointsTickets: false,
279
283
  };
280
284
  }