@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,81 @@
1
+ /**
2
+ * Phase 4 of the unified-pricing roadmap — host-side pricing-model allow-list
3
+ * validation.
4
+ *
5
+ * Each vertical request entity (`VendorBookingRequest`,
6
+ * `ExhibitorBookingRequest`, `SponsorBookingRequest`) carries an optional
7
+ * `pricingModelAllowList String[]`. When empty, the host hasn't restricted
8
+ * anything and providers may bid in any of their supported models. When
9
+ * non-empty, providers can only bid in models that appear in the list.
10
+ *
11
+ * Pure function so the wizard, api validators, and offer-suggestion AI all
12
+ * share one rule.
13
+ */
14
+ import type { PricingModel } from "@prisma/client";
15
+
16
+ export type AllowListValidation =
17
+ | { ok: true }
18
+ | {
19
+ ok: false;
20
+ reason: string;
21
+ offered: PricingModel | string;
22
+ allowList: ReadonlyArray<string>;
23
+ };
24
+
25
+ /**
26
+ * Returns `{ ok: true }` if either:
27
+ * - the allow-list is empty / null (host has no restriction), or
28
+ * - the offered model appears in the allow-list.
29
+ *
30
+ * Otherwise returns `{ ok: false }` with a friendly reason suitable for
31
+ * surfacing in the SendOfferSheet error region.
32
+ */
33
+ export function validateOfferAgainstAllowList(args: {
34
+ offered: PricingModel | string | null | undefined;
35
+ allowList: ReadonlyArray<string> | null | undefined;
36
+ }): AllowListValidation {
37
+ if (!args.offered) {
38
+ return {
39
+ ok: false,
40
+ reason: "Offer is missing a pricing model.",
41
+ offered: "",
42
+ allowList: args.allowList ?? [],
43
+ };
44
+ }
45
+ const list = args.allowList ?? [];
46
+ if (list.length === 0) {
47
+ return { ok: true };
48
+ }
49
+ if (list.includes(args.offered)) {
50
+ return { ok: true };
51
+ }
52
+ return {
53
+ ok: false,
54
+ reason: `Host accepts bids priced as ${list.join(
55
+ ", "
56
+ )}. ${args.offered} is not on the list.`,
57
+ offered: args.offered,
58
+ allowList: list,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Per-vertical sensible defaults for what a wizard should pre-check in the
64
+ * "I'll consider bids priced as…" group when the host hasn't customized.
65
+ * Used by Phase 4 wizard sections; safe to share with the AI service so its
66
+ * suggestions match the wizard's defaults.
67
+ */
68
+ export const DEFAULT_PRICING_MODEL_ALLOW_LIST_BY_SERVICE_TYPE: Record<
69
+ string,
70
+ ReadonlyArray<PricingModel>
71
+ > = {
72
+ // Strings match the Prisma `ServiceTypes` enum values (see schema.prisma).
73
+ Vendors: ["FLAT_FEE", "PRODUCT_SALES_PERCENTAGE"],
74
+ Exhibitors: ["FLAT_FEE"],
75
+ Sponsors: ["TIERED", "FLAT_FEE"],
76
+ Venues: ["FLAT_FEE", "TICKET_REVENUE_PERCENTAGE"],
77
+ EntertainmentServices: ["HOURLY", "FLAT_FEE"],
78
+ EventServices: ["FLAT_FEE", "PER_ATTENDEE"],
79
+ Volunteers: ["FLAT_FEE"],
80
+ Organizations: ["FLAT_FEE"],
81
+ };
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Phase 5 of the unified-pricing roadmap — pure projection of a `ServiceBookingOffer`
3
+ * candidate into expected / worst-case / best-case totals (in cents).
4
+ *
5
+ * Used by:
6
+ * - the offer-suggestion AI to return one suggestion per allowed model with
7
+ * bands the host can compare side-by-side,
8
+ * - the SendOfferSheet preview row that renders the same bands,
9
+ * - the api validator that double-checks an accepted offer against the
10
+ * host's expected attendees and any host-side caps.
11
+ *
12
+ * Heuristics for `worstCaseCents` / `bestCaseCents`:
13
+ * - `FLAT_FEE` / `HOURLY` / `PER_DAY` / `CUSTOM` — all three bands equal the
14
+ * expected total (no variability).
15
+ * - `PER_ATTENDEE` / `PER_PAID_ATTENDEE` — worst case = floor (minimumQuantity
16
+ * * baseRateCents, capped by minimumChargeCents); best case = capped to
17
+ * `maxBillableCents` when present, else `expectedAttendees * 1.5`.
18
+ * - `PER_BASHPOINTS_ATTENDEE` — same shape but in BashPoints, so we surface
19
+ * `expectedTotalBp` and the USD bands return null.
20
+ * - `TICKET_REVENUE_PERCENTAGE` / `PRODUCT_SALES_PERCENTAGE` — caller passes
21
+ * an `expectedBasisCents` (usually the bash's projected ticket revenue or
22
+ * vendor gross sales). Worst case = 0 if pre-sale, best case = basis * pct.
23
+ * - `TIERED` — worst case = lowest tier price, best case = highest tier
24
+ * price; expected resolves via the supplied `tieredExpectedTierCents`.
25
+ */
26
+ import type { PricingModel } from "@prisma/client";
27
+
28
+ export interface OfferComparisonInput {
29
+ pricingModel: PricingModel | string;
30
+ baseRateCents?: number | null;
31
+ baseRateBashPoints?: number | null;
32
+ minimumChargeCents?: number | null;
33
+ minimumQuantity?: number | null;
34
+ maxBillableCents?: number | null;
35
+ /** Bash's expectedAttendees at offer time. */
36
+ expectedAttendees?: number | null;
37
+ /** Optional expected basis for revenue-share models. */
38
+ expectedBasisCents?: number | null;
39
+ /** Percentage (0..100) for revenue-share models. */
40
+ percentage?: number | null;
41
+ /** Pre-resolved expected total for TIERED (already chosen tier). */
42
+ tieredExpectedTierCents?: number | null;
43
+ tieredLowestTierCents?: number | null;
44
+ tieredHighestTierCents?: number | null;
45
+ /** Optional flat amount the caller already computed (e.g. from priceToBook). */
46
+ flatRateCents?: number | null;
47
+ }
48
+
49
+ export interface OfferComparisonResult {
50
+ /** Most-likely total in USD cents. `null` for BashPoints-only models. */
51
+ expectedTotalCents: number | null;
52
+ /** Worst case in USD cents the host could end up owing (or null for BP). */
53
+ worstCaseCents: number | null;
54
+ /** Best case for the host (cheapest plausible total). */
55
+ bestCaseCents: number | null;
56
+ /** Same triple in BashPoints when the model settles in BP. */
57
+ expectedTotalBp: number | null;
58
+ worstCaseBp: number | null;
59
+ bestCaseBp: number | null;
60
+ /**
61
+ * Plain-English tradeoff sentence to surface next to the bands. AI may
62
+ * override with its own copy; this is a deterministic fallback.
63
+ */
64
+ reasoning: string;
65
+ }
66
+
67
+ function nn(v: number | null | undefined): number {
68
+ return v == null || !Number.isFinite(v) || v <= 0 ? 0 : Math.round(v);
69
+ }
70
+
71
+ function fmt(cents: number): string {
72
+ return `$${(cents / 100).toFixed(0)}`;
73
+ }
74
+
75
+ /**
76
+ * Project an offer candidate into expected / worst / best totals.
77
+ * Returns sensible-but-defensive defaults if the input model is unknown.
78
+ */
79
+ export function computeOfferComparisonBands(
80
+ input: OfferComparisonInput
81
+ ): OfferComparisonResult {
82
+ const expected = nn(input.expectedAttendees);
83
+ const minQty = nn(input.minimumQuantity);
84
+
85
+ switch (input.pricingModel) {
86
+ case "FLAT_FEE":
87
+ case "HOURLY":
88
+ case "PER_DAY":
89
+ case "CUSTOM": {
90
+ const total =
91
+ nn(input.flatRateCents) || nn(input.baseRateCents) || 0;
92
+ return {
93
+ expectedTotalCents: total,
94
+ worstCaseCents: total,
95
+ bestCaseCents: total,
96
+ expectedTotalBp: null,
97
+ worstCaseBp: null,
98
+ bestCaseBp: null,
99
+ reasoning:
100
+ total > 0
101
+ ? `Fixed price: you pay ${fmt(total)} regardless of turnout.`
102
+ : "Fixed price — provider has not set a rate.",
103
+ };
104
+ }
105
+
106
+ case "PER_ATTENDEE":
107
+ case "PER_PAID_ATTENDEE": {
108
+ const rate = nn(input.baseRateCents);
109
+ const expectedTotal = expected * rate;
110
+ const floor = Math.max(
111
+ nn(input.minimumChargeCents),
112
+ minQty * rate
113
+ );
114
+ const worst = Math.max(expectedTotal, floor);
115
+ const ceiling =
116
+ nn(input.maxBillableCents) ||
117
+ Math.max(expectedTotal, Math.round(expected * rate * 1.5));
118
+ const best = Math.max(floor, 0);
119
+ return {
120
+ expectedTotalCents: expectedTotal,
121
+ worstCaseCents: ceiling,
122
+ bestCaseCents: best,
123
+ expectedTotalBp: null,
124
+ worstCaseBp: null,
125
+ bestCaseBp: null,
126
+ reasoning: `Per-${input.pricingModel === "PER_PAID_ATTENDEE" ? "paid-" : ""}attendee: expected ${fmt(
127
+ expectedTotal
128
+ )} at ${expected} guests, with a floor of ${fmt(floor)} and a host cap of ${fmt(ceiling)}.`,
129
+ };
130
+ }
131
+
132
+ case "PER_BASHPOINTS_ATTENDEE": {
133
+ const rate = nn(input.baseRateBashPoints);
134
+ const expectedTotalBp = expected * rate;
135
+ const floorBp = minQty * rate;
136
+ const worstBp = Math.max(expectedTotalBp, floorBp);
137
+ const bestBp = Math.max(floorBp, 0);
138
+ return {
139
+ expectedTotalCents: null,
140
+ worstCaseCents: null,
141
+ bestCaseCents: null,
142
+ expectedTotalBp,
143
+ worstCaseBp: worstBp,
144
+ bestCaseBp: bestBp,
145
+ reasoning: `Per-attendee BashPoints: expected ${expectedTotalBp} BP at ${expected} guests, floor ${floorBp} BP.`,
146
+ };
147
+ }
148
+
149
+ case "TICKET_REVENUE_PERCENTAGE":
150
+ case "PRODUCT_SALES_PERCENTAGE": {
151
+ const basis = nn(input.expectedBasisCents);
152
+ const pct = Math.max(0, Math.min(100, input.percentage ?? 0));
153
+ const expectedTotal = Math.round((basis * pct) / 100);
154
+ return {
155
+ expectedTotalCents: expectedTotal,
156
+ worstCaseCents: 0,
157
+ bestCaseCents: expectedTotal,
158
+ expectedTotalBp: null,
159
+ worstCaseBp: null,
160
+ bestCaseBp: null,
161
+ reasoning: `Revenue share: provider takes ${pct}% of ${fmt(
162
+ basis
163
+ )} projected. Worst case for you (slow night, no revenue): $0.`,
164
+ };
165
+ }
166
+
167
+ case "TIERED": {
168
+ const exp = nn(input.tieredExpectedTierCents);
169
+ const low = nn(input.tieredLowestTierCents);
170
+ const high = nn(input.tieredHighestTierCents);
171
+ return {
172
+ expectedTotalCents: exp || low || high || 0,
173
+ worstCaseCents: high || exp || low,
174
+ bestCaseCents: low || exp || high,
175
+ expectedTotalBp: null,
176
+ worstCaseBp: null,
177
+ bestCaseBp: null,
178
+ reasoning: `Tiered: from ${fmt(low)} to ${fmt(
179
+ high
180
+ )} depending on the tier you pick.`,
181
+ };
182
+ }
183
+
184
+ default: {
185
+ const fallback = nn(input.flatRateCents) || nn(input.baseRateCents);
186
+ return {
187
+ expectedTotalCents: fallback,
188
+ worstCaseCents: fallback,
189
+ bestCaseCents: fallback,
190
+ expectedTotalBp: null,
191
+ worstCaseBp: null,
192
+ bestCaseBp: null,
193
+ reasoning: `Unrecognized pricing model "${input.pricingModel}" — treated as flat.`,
194
+ };
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Phase 0 adoption guardrails — aggregate cost exposure for a bash's service
3
+ * bookings. Pure functions so the api endpoint (Prisma query, then this util)
4
+ * and bash-app (display) share one source of truth.
5
+ *
6
+ * Each booking resolves to:
7
+ * - `expectedCents` — the most-likely total at expected attendance.
8
+ * - `worstCaseCents` — the dollar ceiling if the host's caps trip. For flat
9
+ * models this equals `expectedCents` (no variance). For variable models it
10
+ * equals `maxBillableCents` if set; otherwise `1.5x expected` (matches the
11
+ * `defaultMaxBillableCents` heuristic in `billingCapUtils.ts`).
12
+ *
13
+ * Forward-looking: the input shape uses the unified pricing block fields
14
+ * (`pricingModel`, `baseRateCents`, etc.). Phase 1 adds them to every subtype;
15
+ * Phase 0 callers passing in `pricingModel: null` for legacy bookings still get
16
+ * a sensible flat-fee fallback.
17
+ */
18
+
19
+ import {
20
+ DEFAULT_MAX_BILLABLE_MULTIPLIER,
21
+ isFlatPricingModel,
22
+ } from "./billingCapUtils";
23
+
24
+ /**
25
+ * Pricing-model strings recognized by the exposure util. Stays a string
26
+ * literal (not the Prisma enum) so this util compiles for the browser bundle.
27
+ */
28
+ export const KNOWN_VARIABLE_PRICING_MODELS = [
29
+ "PER_ATTENDEE",
30
+ "PER_PAID_ATTENDEE",
31
+ "PER_BASHPOINTS_ATTENDEE",
32
+ "TICKET_REVENUE_PERCENTAGE",
33
+ "PRODUCT_SALES_PERCENTAGE",
34
+ "TIERED",
35
+ "CUSTOM",
36
+ ] as const;
37
+
38
+ export interface ServiceBookingExposureInput {
39
+ bookingId: string;
40
+ /** Display label (e.g. service name). Echoed back for UI. */
41
+ label: string;
42
+ /**
43
+ * The pricing model from the booked service's subtype, if known. Phase 0
44
+ * bookings that predate the unified block pass `null` — exposure falls
45
+ * back to flat-rate math.
46
+ */
47
+ pricingModel?: string | null;
48
+ /**
49
+ * Per-unit rate the booking is priced at. Cents-per-attendee for
50
+ * PER_ATTENDEE; cents-per-hour for HOURLY; etc. Optional fallback.
51
+ */
52
+ baseRateCents?: number | null;
53
+ /**
54
+ * Expected attendance at booking time (snapshot from
55
+ * `bashEvent.expectedAttendees`).
56
+ */
57
+ expectedAttendeesAtBooking?: number | null;
58
+ /** Host-set ceiling on billable check-ins for PER_ATTENDEE bookings. */
59
+ maxAttendeesAttributable?: number | null;
60
+ /**
61
+ * Phase 0 soft cap: host-set per-booking dollar ceiling. Overrides the
62
+ * 1.5x worst-case heuristic when set.
63
+ */
64
+ maxBillableCents?: number | null;
65
+ /**
66
+ * The booking's grand total (cents) when it was confirmed — used as the
67
+ * fallback "expected" amount for non-variable bookings whose pricing model
68
+ * isn't yet tracked. Mirrors `ServiceBooking.totalATBCents`.
69
+ */
70
+ totalATBCents?: number | null;
71
+ /**
72
+ * Optional flat-rate snapshot (`ServiceBooking.flatRateCents`). Used as a
73
+ * narrow fallback when both `pricingModel` and `totalATBCents` are missing.
74
+ */
75
+ flatRateCents?: number | null;
76
+ }
77
+
78
+ export interface ServiceBookingExposure {
79
+ bookingId: string;
80
+ label: string;
81
+ pricingModel: string | null;
82
+ isVariable: boolean;
83
+ /** Most likely total (cents). */
84
+ expectedCents: number;
85
+ /** Worst-case total (cents) the host might owe if the cap trips. */
86
+ worstCaseCents: number;
87
+ }
88
+
89
+ export interface AggregateExposureSummary {
90
+ /** Sum of `expectedCents` across all bookings (cents). */
91
+ projectedTotalCents: number;
92
+ /** Sum of `worstCaseCents` across all bookings (cents). */
93
+ worstCaseTotalCents: number;
94
+ /**
95
+ * Per-booking breakdown — preserves input order so callers can render rows
96
+ * alongside the booking list without re-sorting.
97
+ */
98
+ byBooking: ServiceBookingExposure[];
99
+ }
100
+
101
+ /**
102
+ * Returns `true` for any pricing model whose total depends on event-day
103
+ * variables (attendance, revenue, etc.).
104
+ */
105
+ export function isVariablePricingModel(
106
+ model: string | null | undefined
107
+ ): boolean {
108
+ if (!model) return false;
109
+ if (isFlatPricingModel(model)) return false;
110
+ return (KNOWN_VARIABLE_PRICING_MODELS as readonly string[]).includes(model);
111
+ }
112
+
113
+ /**
114
+ * Resolve one booking's exposure (expected + worst case). Pure.
115
+ *
116
+ * Decision matrix:
117
+ * - PER_ATTENDEE / PER_PAID_ATTENDEE:
118
+ * expected = expectedAttendees * baseRateCents
119
+ * worst = maxBillableCents ?? maxAttendeesAttributable * baseRateCents
120
+ * ?? expected * 1.5
121
+ * - PER_BASHPOINTS_ATTENDEE: same shape but reported in cents-of-BP-equivalent.
122
+ * Caller should treat this booking's contribution as informational only;
123
+ * `expectedCents`/`worstCaseCents` reflect BP units, not dollars.
124
+ * - TICKET_REVENUE_PERCENTAGE / PRODUCT_SALES_PERCENTAGE / TIERED / CUSTOM:
125
+ * expected = totalATBCents ?? 0
126
+ * worst = maxBillableCents ?? expected * 1.5 (variance unknown without
127
+ * settlement basis math — see Phase 2 for venue rev-share).
128
+ * - FLAT_FEE / HOURLY / PER_DAY (or null pricingModel): flat → expected = worst.
129
+ */
130
+ export function computeServiceBookingExposure(
131
+ input: ServiceBookingExposureInput
132
+ ): ServiceBookingExposure {
133
+ const flatFallback =
134
+ input.totalATBCents ?? input.flatRateCents ?? 0;
135
+
136
+ const variable = isVariablePricingModel(input.pricingModel);
137
+
138
+ if (!variable) {
139
+ return {
140
+ bookingId: input.bookingId,
141
+ label: input.label,
142
+ pricingModel: input.pricingModel ?? null,
143
+ isVariable: false,
144
+ expectedCents: Math.max(0, flatFallback),
145
+ worstCaseCents: Math.max(0, flatFallback),
146
+ };
147
+ }
148
+
149
+ const expectedAttendees = Math.max(
150
+ 0,
151
+ Math.floor(input.expectedAttendeesAtBooking ?? 0)
152
+ );
153
+ const baseRate = Math.max(0, Math.floor(input.baseRateCents ?? 0));
154
+
155
+ // For attendee-driven models, expected = N * rate.
156
+ const isAttendeeDriven =
157
+ input.pricingModel === "PER_ATTENDEE" ||
158
+ input.pricingModel === "PER_PAID_ATTENDEE" ||
159
+ input.pricingModel === "PER_BASHPOINTS_ATTENDEE";
160
+
161
+ let expected: number;
162
+ if (isAttendeeDriven) {
163
+ expected = expectedAttendees * baseRate;
164
+ } else {
165
+ // Revenue-share / tiered / custom — totalATBCents is the most we know.
166
+ expected = Math.max(0, flatFallback);
167
+ }
168
+
169
+ // Worst case prefers the explicit `maxBillableCents` snapshot, then the
170
+ // attendee ceiling math, then a 1.5x fallback.
171
+ let worst: number;
172
+ if (input.maxBillableCents != null && input.maxBillableCents > 0) {
173
+ worst = input.maxBillableCents;
174
+ } else if (
175
+ isAttendeeDriven &&
176
+ input.maxAttendeesAttributable != null &&
177
+ input.maxAttendeesAttributable > 0
178
+ ) {
179
+ worst = input.maxAttendeesAttributable * baseRate;
180
+ } else if (expected > 0) {
181
+ worst = Math.ceil(expected * DEFAULT_MAX_BILLABLE_MULTIPLIER);
182
+ } else {
183
+ worst = 0;
184
+ }
185
+
186
+ // Sanity: worst should never be below expected.
187
+ worst = Math.max(worst, expected);
188
+
189
+ return {
190
+ bookingId: input.bookingId,
191
+ label: input.label,
192
+ pricingModel: input.pricingModel ?? null,
193
+ isVariable: true,
194
+ expectedCents: expected,
195
+ worstCaseCents: worst,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Sum a list of booking exposures into the host-facing summary used by
201
+ * the budget step's projected-cost and worst-case lines.
202
+ */
203
+ export function summarizeServiceBookingExposures(
204
+ inputs: ServiceBookingExposureInput[]
205
+ ): AggregateExposureSummary {
206
+ const byBooking = inputs.map(computeServiceBookingExposure);
207
+ let projected = 0;
208
+ let worstCase = 0;
209
+ for (const row of byBooking) {
210
+ projected += row.expectedCents;
211
+ worstCase += row.worstCaseCents;
212
+ }
213
+ return {
214
+ projectedTotalCents: projected,
215
+ worstCaseTotalCents: worstCase,
216
+ byBooking,
217
+ };
218
+ }
@@ -63,6 +63,10 @@ export type ServiceBookingRequiredData = MakeNullablePropsOptional<
63
63
  | "creatorId"
64
64
  | "creator"
65
65
  | "checkout"
66
+ // Event Promoter settlement fields — DB applies defaults at creation,
67
+ // so callers building a new booking shape may omit them.
68
+ | "settlementStatus"
69
+ | "settlementAttempts"
66
70
  >
67
71
  >;
68
72
 
@@ -1,9 +1,6 @@
1
- import {
2
- Service,
3
- ServiceCancellationPolicy as ServiceCancellationPolicyOption,
4
- ServiceSubscriptionStatus,
5
- ServiceTypes,
6
- } from "@prisma/client";
1
+ import { Service, ServiceSubscriptionStatus, ServiceTypes } from "@prisma/client";
2
+ import type { ServiceCancellationPolicy as ServiceCancellationPolicyOption } from "@prisma/client";
3
+ import { ServiceCancellationPolicy } from "../../mirroredPrismaEnums.js";
7
4
  import {
8
5
  ServiceExt,
9
6
  ServiceExtNoIds,
@@ -27,12 +24,12 @@ export type ServiceCancellationPolicyMap = {
27
24
  };
28
25
 
29
26
  export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
30
- [ServiceCancellationPolicyOption.None]: {
27
+ [ServiceCancellationPolicy.None]: {
31
28
  name: "None",
32
29
  refundPolicy: [],
33
30
  description: "No cancellation policy.",
34
31
  },
35
- [ServiceCancellationPolicyOption.VeryFlexible]: {
32
+ [ServiceCancellationPolicy.VeryFlexible]: {
36
33
  name: "Very Flexible",
37
34
  refundPolicy: [
38
35
  {
@@ -41,7 +38,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
41
38
  },
42
39
  ],
43
40
  },
44
- [ServiceCancellationPolicyOption.Flexible]: {
41
+ [ServiceCancellationPolicy.Flexible]: {
45
42
  name: "Flexible",
46
43
  refundPolicy: [
47
44
  {
@@ -54,7 +51,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
54
51
  },
55
52
  ],
56
53
  },
57
- [ServiceCancellationPolicyOption.Standard30Day]: {
54
+ [ServiceCancellationPolicy.Standard30Day]: {
58
55
  name: "Standard 30 Day",
59
56
  refundPolicy: [
60
57
  {
@@ -67,7 +64,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
67
64
  },
68
65
  ],
69
66
  },
70
- [ServiceCancellationPolicyOption.Standard90Day]: {
67
+ [ServiceCancellationPolicy.Standard90Day]: {
71
68
  name: "Standard 90 Day",
72
69
  refundPolicy: [
73
70
  {
@@ -81,7 +78,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
81
78
  ],
82
79
  },
83
80
  // Vendor-specific policies (vendor pays host, different language needed)
84
- [ServiceCancellationPolicyOption.VendorFlexible]: {
81
+ [ServiceCancellationPolicy.VendorFlexible]: {
85
82
  name: "Vendor Flexible",
86
83
  refundPolicy: [
87
84
  {
@@ -95,7 +92,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
95
92
  ],
96
93
  description: "Flexible policy for vendor bookings",
97
94
  },
98
- [ServiceCancellationPolicyOption.VendorStandard]: {
95
+ [ServiceCancellationPolicy.VendorStandard]: {
99
96
  name: "Vendor Standard",
100
97
  refundPolicy: [
101
98
  {
@@ -109,7 +106,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
109
106
  ],
110
107
  description: "Standard policy for vendor bookings",
111
108
  },
112
- [ServiceCancellationPolicyOption.VendorStrict]: {
109
+ [ServiceCancellationPolicy.VendorStrict]: {
113
110
  name: "Vendor Strict",
114
111
  refundPolicy: [
115
112
  {
@@ -124,7 +121,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
124
121
  description: "Strict policy for vendor bookings with longer notice periods",
125
122
  },
126
123
  // Exhibitor / Sponsor: same tier math as vendor; separate enum + copy for role clarity
127
- [ServiceCancellationPolicyOption.ExhibitorFlexible]: {
124
+ [ServiceCancellationPolicy.ExhibitorFlexible]: {
128
125
  name: "Exhibitor Flexible",
129
126
  refundPolicy: [
130
127
  {
@@ -138,7 +135,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
138
135
  ],
139
136
  description: "Flexible policy for exhibitor bookings",
140
137
  },
141
- [ServiceCancellationPolicyOption.ExhibitorStandard]: {
138
+ [ServiceCancellationPolicy.ExhibitorStandard]: {
142
139
  name: "Exhibitor Standard",
143
140
  refundPolicy: [
144
141
  {
@@ -152,7 +149,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
152
149
  ],
153
150
  description: "Standard policy for exhibitor bookings",
154
151
  },
155
- [ServiceCancellationPolicyOption.ExhibitorStrict]: {
152
+ [ServiceCancellationPolicy.ExhibitorStrict]: {
156
153
  name: "Exhibitor Strict",
157
154
  refundPolicy: [
158
155
  {
@@ -166,7 +163,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
166
163
  ],
167
164
  description: "Strict policy for exhibitor bookings",
168
165
  },
169
- [ServiceCancellationPolicyOption.SponsorFlexible]: {
166
+ [ServiceCancellationPolicy.SponsorFlexible]: {
170
167
  name: "Sponsor Flexible",
171
168
  refundPolicy: [
172
169
  {
@@ -180,7 +177,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
180
177
  ],
181
178
  description: "Flexible policy for sponsorship bookings",
182
179
  },
183
- [ServiceCancellationPolicyOption.SponsorStandard]: {
180
+ [ServiceCancellationPolicy.SponsorStandard]: {
184
181
  name: "Sponsor Standard",
185
182
  refundPolicy: [
186
183
  {
@@ -194,7 +191,7 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
194
191
  ],
195
192
  description: "Standard policy for sponsorship bookings",
196
193
  },
197
- [ServiceCancellationPolicyOption.SponsorStrict]: {
194
+ [ServiceCancellationPolicy.SponsorStrict]: {
198
195
  name: "Sponsor Strict",
199
196
  refundPolicy: [
200
197
  {
@@ -376,7 +373,7 @@ Object.keys(SERVICE_CANCELLATION_POLICY_DATA)
376
373
  .filter(
377
374
  (policyKey) =>
378
375
  (policyKey as ServiceCancellationPolicyOption) !=
379
- ServiceCancellationPolicyOption.None
376
+ ServiceCancellationPolicy.None
380
377
  )
381
378
  .forEach((policyKey) => {
382
379
  const policy =