@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,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
+ });
@@ -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
+ });