@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.
- package/dist/__tests__/statusEnums.test.js +88 -7
- package/dist/__tests__/statusEnums.test.js.map +1 -1
- package/dist/definitions.d.ts +45 -14
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +38 -17
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +8 -0
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/mirroredPrismaEnums.d.ts +145 -0
- package/dist/mirroredPrismaEnums.d.ts.map +1 -0
- package/dist/mirroredPrismaEnums.js +136 -0
- package/dist/mirroredPrismaEnums.js.map +1 -0
- package/dist/utils/__tests__/nonprofitUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/nonprofitUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/nonprofitUtils.test.js +84 -0
- package/dist/utils/__tests__/nonprofitUtils.test.js.map +1 -0
- package/dist/utils/__tests__/paymentUtils.test.js +87 -0
- package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
- package/dist/utils/nonprofitUtils.d.ts +21 -0
- package/dist/utils/nonprofitUtils.d.ts.map +1 -0
- package/dist/utils/nonprofitUtils.js +29 -0
- package/dist/utils/nonprofitUtils.js.map +1 -0
- package/dist/utils/paymentUtils.d.ts +23 -0
- package/dist/utils/paymentUtils.d.ts.map +1 -1
- package/dist/utils/paymentUtils.js +24 -7
- package/dist/utils/paymentUtils.js.map +1 -1
- package/dist/utils/service/__tests__/billingCapUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/billingCapUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/billingCapUtils.test.js +120 -0
- package/dist/utils/service/__tests__/billingCapUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.js +1 -1
- package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.js.map +1 -1
- package/dist/utils/service/__tests__/eventPromoterPhase3.test.d.ts +2 -0
- package/dist/utils/service/__tests__/eventPromoterPhase3.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/eventPromoterPhase3.test.js +243 -0
- package/dist/utils/service/__tests__/eventPromoterPhase3.test.js.map +1 -0
- package/dist/utils/service/__tests__/eventPromoterUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/eventPromoterUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/eventPromoterUtils.test.js +333 -0
- package/dist/utils/service/__tests__/eventPromoterUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/offerAllowListUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/offerAllowListUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/offerAllowListUtils.test.js +52 -0
- package/dist/utils/service/__tests__/offerAllowListUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/offerComparisonUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/offerComparisonUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/offerComparisonUtils.test.js +111 -0
- package/dist/utils/service/__tests__/offerComparisonUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.js +155 -0
- package/dist/utils/service/__tests__/serviceBookingExposureUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/unifiedPricingResolver.test.d.ts +2 -0
- package/dist/utils/service/__tests__/unifiedPricingResolver.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/unifiedPricingResolver.test.js +77 -0
- package/dist/utils/service/__tests__/unifiedPricingResolver.test.js.map +1 -0
- package/dist/utils/service/__tests__/venueFBSettlementUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/venueFBSettlementUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/venueFBSettlementUtils.test.js +157 -0
- package/dist/utils/service/__tests__/venueFBSettlementUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/venueSettlementUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/venueSettlementUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/venueSettlementUtils.test.js +175 -0
- package/dist/utils/service/__tests__/venueSettlementUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/volunteerCompensationUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/volunteerCompensationUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/volunteerCompensationUtils.test.js +118 -0
- package/dist/utils/service/__tests__/volunteerCompensationUtils.test.js.map +1 -0
- package/dist/utils/service/apiServiceBookingApiUtils.d.ts.map +1 -1
- package/dist/utils/service/apiServiceBookingApiUtils.js +4 -0
- package/dist/utils/service/apiServiceBookingApiUtils.js.map +1 -1
- package/dist/utils/service/billingCapUtils.d.ts +86 -0
- package/dist/utils/service/billingCapUtils.d.ts.map +1 -0
- package/dist/utils/service/billingCapUtils.js +96 -0
- package/dist/utils/service/billingCapUtils.js.map +1 -0
- package/dist/utils/service/cancellationPolicyRefundResolver.d.ts +1 -1
- package/dist/utils/service/cancellationPolicyRefundResolver.d.ts.map +1 -1
- package/dist/utils/service/cancellationPolicyRefundResolver.js +1 -1
- package/dist/utils/service/cancellationPolicyRefundResolver.js.map +1 -1
- package/dist/utils/service/eventPromoterUtils.d.ts +319 -0
- package/dist/utils/service/eventPromoterUtils.d.ts.map +1 -0
- package/dist/utils/service/eventPromoterUtils.js +372 -0
- package/dist/utils/service/eventPromoterUtils.js.map +1 -0
- package/dist/utils/service/offerAllowListUtils.d.ts +42 -0
- package/dist/utils/service/offerAllowListUtils.d.ts.map +1 -0
- package/dist/utils/service/offerAllowListUtils.js +49 -0
- package/dist/utils/service/offerAllowListUtils.js.map +1 -0
- package/dist/utils/service/offerComparisonUtils.d.ts +69 -0
- package/dist/utils/service/offerComparisonUtils.d.ts.map +1 -0
- package/dist/utils/service/offerComparisonUtils.js +110 -0
- package/dist/utils/service/offerComparisonUtils.js.map +1 -0
- package/dist/utils/service/serviceBookingExposureUtils.d.ts +111 -0
- package/dist/utils/service/serviceBookingExposureUtils.d.ts.map +1 -0
- package/dist/utils/service/serviceBookingExposureUtils.js +133 -0
- package/dist/utils/service/serviceBookingExposureUtils.js.map +1 -0
- package/dist/utils/service/serviceBookingTypes.d.ts +1 -1
- package/dist/utils/service/serviceBookingTypes.d.ts.map +1 -1
- package/dist/utils/service/serviceUtils.d.ts +2 -1
- package/dist/utils/service/serviceUtils.d.ts.map +1 -1
- package/dist/utils/service/serviceUtils.js +17 -16
- package/dist/utils/service/serviceUtils.js.map +1 -1
- package/dist/utils/service/unifiedPricingResolver.d.ts +67 -0
- package/dist/utils/service/unifiedPricingResolver.d.ts.map +1 -0
- package/dist/utils/service/unifiedPricingResolver.js +75 -0
- package/dist/utils/service/unifiedPricingResolver.js.map +1 -0
- package/dist/utils/service/venueFBSettlementUtils.d.ts +122 -0
- package/dist/utils/service/venueFBSettlementUtils.d.ts.map +1 -0
- package/dist/utils/service/venueFBSettlementUtils.js +106 -0
- package/dist/utils/service/venueFBSettlementUtils.js.map +1 -0
- package/dist/utils/service/venueSettlementUtils.d.ts +107 -0
- package/dist/utils/service/venueSettlementUtils.d.ts.map +1 -0
- package/dist/utils/service/venueSettlementUtils.js +117 -0
- package/dist/utils/service/venueSettlementUtils.js.map +1 -0
- package/dist/utils/service/volunteerCompensationUtils.d.ts +75 -0
- package/dist/utils/service/volunteerCompensationUtils.d.ts.map +1 -0
- package/dist/utils/service/volunteerCompensationUtils.js +88 -0
- package/dist/utils/service/volunteerCompensationUtils.js.map +1 -0
- package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.d.ts +2 -0
- package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.d.ts.map +1 -0
- package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.js +67 -0
- package/dist/utils/venueLoyalty/__tests__/eligibilityUtils.test.js.map +1 -0
- package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.d.ts +2 -0
- package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.d.ts.map +1 -0
- package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.js +71 -0
- package/dist/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.js.map +1 -0
- package/dist/utils/venueLoyalty/eligibilityUtils.d.ts +33 -0
- package/dist/utils/venueLoyalty/eligibilityUtils.d.ts.map +1 -0
- package/dist/utils/venueLoyalty/eligibilityUtils.js +49 -0
- package/dist/utils/venueLoyalty/eligibilityUtils.js.map +1 -0
- package/dist/utils/venueLoyalty/redemptionCodeUtils.d.ts +35 -0
- package/dist/utils/venueLoyalty/redemptionCodeUtils.d.ts.map +1 -0
- package/dist/utils/venueLoyalty/redemptionCodeUtils.js +74 -0
- package/dist/utils/venueLoyalty/redemptionCodeUtils.js.map +1 -0
- package/dist/venueLoyaltyRedemption.d.ts +146 -1
- package/dist/venueLoyaltyRedemption.d.ts.map +1 -1
- package/dist/venueLoyaltyRedemption.js +18 -2
- package/dist/venueLoyaltyRedemption.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +638 -214
- package/src/__tests__/statusEnums.test.ts +119 -11
- package/src/definitions.ts +65 -22
- package/src/extendedSchemas.ts +8 -0
- package/src/index.ts +16 -6
- package/src/mirroredPrismaEnums.ts +181 -0
- package/src/utils/__tests__/nonprofitUtils.test.ts +117 -0
- package/src/utils/__tests__/paymentUtils.test.ts +93 -0
- package/src/utils/nonprofitUtils.ts +44 -0
- package/src/utils/paymentUtils.ts +33 -7
- package/src/utils/service/__tests__/billingCapUtils.test.ts +175 -0
- package/src/utils/service/__tests__/cancellationPolicyRefundResolver.test.ts +1 -1
- package/src/utils/service/__tests__/eventPromoterPhase3.test.ts +296 -0
- package/src/utils/service/__tests__/eventPromoterUtils.test.ts +396 -0
- package/src/utils/service/__tests__/offerAllowListUtils.test.ts +76 -0
- package/src/utils/service/__tests__/offerComparisonUtils.test.ts +124 -0
- package/src/utils/service/__tests__/serviceBookingExposureUtils.test.ts +171 -0
- package/src/utils/service/__tests__/unifiedPricingResolver.test.ts +83 -0
- package/src/utils/service/__tests__/venueSettlementUtils.test.ts +219 -0
- package/src/utils/service/__tests__/volunteerCompensationUtils.test.ts +172 -0
- package/src/utils/service/apiServiceBookingApiUtils.ts +4 -0
- package/src/utils/service/billingCapUtils.ts +145 -0
- package/src/utils/service/cancellationPolicyRefundResolver.ts +1 -1
- package/src/utils/service/eventPromoterUtils.ts +644 -0
- package/src/utils/service/offerAllowListUtils.ts +81 -0
- package/src/utils/service/offerComparisonUtils.ts +197 -0
- package/src/utils/service/serviceBookingExposureUtils.ts +218 -0
- package/src/utils/service/serviceBookingTypes.ts +4 -0
- package/src/utils/service/serviceUtils.ts +18 -21
- package/src/utils/service/unifiedPricingResolver.ts +149 -0
- package/src/utils/service/venueSettlementUtils.ts +198 -0
- package/src/utils/service/volunteerCompensationUtils.ts +145 -0
- package/src/utils/venueLoyalty/__tests__/eligibilityUtils.test.ts +103 -0
- package/src/utils/venueLoyalty/__tests__/redemptionCodeUtils.test.ts +92 -0
- package/src/utils/venueLoyalty/eligibilityUtils.ts +84 -0
- package/src/utils/venueLoyalty/redemptionCodeUtils.ts +94 -0
- package/src/venueLoyaltyRedemption.ts +177 -2
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PricingModel,
|
|
3
|
+
PromotionAndMarketingSubType,
|
|
4
|
+
} from "@prisma/client";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Event Promoter is a productized hire-a-promoter service profile under
|
|
8
|
+
* `EventService` → `PromotionAndMarketing` → `Promoter`. Its pricing model
|
|
9
|
+
* is intentionally locked to PER_ATTENDEE so:
|
|
10
|
+
*
|
|
11
|
+
* 1. Bookings can compute a deposit at booking time from
|
|
12
|
+
* `bashEvent.expectedAttendees * baseRateCents`.
|
|
13
|
+
* 2. The bash-close settlement job can run a delta charge/refund based on
|
|
14
|
+
* final checked-in attendee count (see
|
|
15
|
+
* `apiEventPromoterSettlementMath` below).
|
|
16
|
+
*
|
|
17
|
+
* Keep the disambiguation: this is the **Event Promoter** service, not the
|
|
18
|
+
* **Promo-code Promoter** referral program (`model Promoter` +
|
|
19
|
+
* `BashEventPromoCode`), which earns BashPoints per ticket purchased.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns true when the given `PromotionAndMarketing` subtype is locked to a
|
|
24
|
+
* specific `PricingModel`. Today only `Promoter` is locked (PER_ATTENDEE).
|
|
25
|
+
*
|
|
26
|
+
* Compared with the **string literal** rather than the Prisma enum object
|
|
27
|
+
* because the browser bundle (`index-browser`) does not always re-export
|
|
28
|
+
* Prisma enums as runtime values — see `bash-common/src/mirroredPrismaEnums.ts`
|
|
29
|
+
* for the same pattern.
|
|
30
|
+
*/
|
|
31
|
+
export function isPerAttendeeLockedPromotionSubType(
|
|
32
|
+
subType: PromotionAndMarketingSubType | string | null | undefined
|
|
33
|
+
): boolean {
|
|
34
|
+
return subType === "Promoter";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the URL the host clicks from an Event Promoter booking thread to
|
|
39
|
+
* issue/edit a `BashEventPromoCode` with the promoter pre-selected. The
|
|
40
|
+
* landing page reads `promoterUserId` from the query string and opens the
|
|
41
|
+
* promo-code create modal already focused on that user.
|
|
42
|
+
*
|
|
43
|
+
* Keeping this in `bash-common` so api emails + bash-app threads + push
|
|
44
|
+
* notifications all generate the same deep link.
|
|
45
|
+
*/
|
|
46
|
+
export function buildIssuePromoCodeDeepLink(args: {
|
|
47
|
+
bashEventId: string;
|
|
48
|
+
promoterUserId: string;
|
|
49
|
+
/** Optional preselected ticket tier (skip the chooser screen when set). */
|
|
50
|
+
ticketTierId?: string | null;
|
|
51
|
+
}): string {
|
|
52
|
+
const params = new URLSearchParams({
|
|
53
|
+
promoterUserId: args.promoterUserId,
|
|
54
|
+
source: "event-promoter-booking",
|
|
55
|
+
});
|
|
56
|
+
if (args.ticketTierId) {
|
|
57
|
+
params.set("ticketTierId", args.ticketTierId);
|
|
58
|
+
}
|
|
59
|
+
return `/bashes/${args.bashEventId}/edit?step=how&open=promo-codes&${params.toString()}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Hours-before-bash threshold under which a host cancellation is "late". */
|
|
63
|
+
export const EVENT_PROMOTER_LATE_CANCEL_HOURS = 48;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Late-cancellation penalty as a fraction of the deposit (0..1). 0.25 means
|
|
67
|
+
* the host forfeits 25% of the deposit when they cancel inside the
|
|
68
|
+
* `EVENT_PROMOTER_LATE_CANCEL_HOURS` window — covers the promoter's
|
|
69
|
+
* sunk planning time.
|
|
70
|
+
*/
|
|
71
|
+
export const EVENT_PROMOTER_LATE_CANCEL_PENALTY_FRACTION = 0.25;
|
|
72
|
+
|
|
73
|
+
/** Who is cancelling the Event Promoter booking. Maps to refund policy. */
|
|
74
|
+
export type EventPromoterCanceller = "host" | "promoter" | "admin";
|
|
75
|
+
|
|
76
|
+
export interface EventPromoterCancellationInput {
|
|
77
|
+
who: EventPromoterCanceller;
|
|
78
|
+
/** ISO timestamp of "now" — injected so callers can test edge cases. */
|
|
79
|
+
nowMs: number;
|
|
80
|
+
/** Bash start time. */
|
|
81
|
+
bashStartMs: number | null;
|
|
82
|
+
/** Bash end time — once we're past this, settlement takes over instead. */
|
|
83
|
+
bashEndMs: number | null;
|
|
84
|
+
/** Deposit cents already charged at booking. */
|
|
85
|
+
depositCents: number;
|
|
86
|
+
/** Has settlement already run? If so, cancellation is a no-op. */
|
|
87
|
+
alreadySettled: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface EventPromoterCancellationDecision {
|
|
91
|
+
/** Whether the cancellation can run automatically right now. */
|
|
92
|
+
allow: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Outcome category for telemetry + thread messaging:
|
|
95
|
+
* - `FullRefund` — full deposit returned (host cancel outside window, or
|
|
96
|
+
* promoter cancel any time before the bash).
|
|
97
|
+
* - `PartialRefund` — host cancels inside the late-cancel window; promoter
|
|
98
|
+
* keeps `EVENT_PROMOTER_LATE_CANCEL_PENALTY_FRACTION` of the deposit.
|
|
99
|
+
* - `NoRefund` — bash already happened; settlement handles it instead.
|
|
100
|
+
* - `Blocked` — booking already settled, or impossible state; admin only.
|
|
101
|
+
*/
|
|
102
|
+
outcome: "FullRefund" | "PartialRefund" | "NoRefund" | "Blocked";
|
|
103
|
+
/** Refund amount in cents (0 when outcome is NoRefund/Blocked). */
|
|
104
|
+
refundCents: number;
|
|
105
|
+
/** Penalty fraction applied (0 when not late-cancelled by host). */
|
|
106
|
+
penaltyFraction: number;
|
|
107
|
+
/** Human-readable reason, suitable for booking-thread system messages. */
|
|
108
|
+
reason: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Pure decision function that returns what should happen when someone tries
|
|
113
|
+
* to cancel an Event Promoter booking. Mirrors the matrix called out in the
|
|
114
|
+
* plan:
|
|
115
|
+
*
|
|
116
|
+
* - Host, bash hasn't started, > 48h out → FullRefund.
|
|
117
|
+
* - Host, bash hasn't started, ≤ 48h out → PartialRefund (25% penalty).
|
|
118
|
+
* - Host, bash already running/ended → NoRefund (settlement takes over).
|
|
119
|
+
* - Promoter, bash not started → FullRefund + promoter notified
|
|
120
|
+
* (Bash never holds a promoter to a job they can't do).
|
|
121
|
+
* - Promoter, bash already running/ended → Blocked (use the booking thread
|
|
122
|
+
* to coordinate; nothing automatic).
|
|
123
|
+
* - Admin always allowed; refund amount decided by caller.
|
|
124
|
+
* - Already settled → Blocked (no automatic cancel).
|
|
125
|
+
*
|
|
126
|
+
* The actual Stripe refund + DB mutation lives in the api-side counterpart;
|
|
127
|
+
* this function only encodes policy so the same matrix surfaces in tests,
|
|
128
|
+
* the booking thread copy, and the audit log.
|
|
129
|
+
*/
|
|
130
|
+
export function decideEventPromoterCancellation(
|
|
131
|
+
input: EventPromoterCancellationInput
|
|
132
|
+
): EventPromoterCancellationDecision {
|
|
133
|
+
if (input.alreadySettled) {
|
|
134
|
+
return {
|
|
135
|
+
allow: false,
|
|
136
|
+
outcome: "Blocked",
|
|
137
|
+
refundCents: 0,
|
|
138
|
+
penaltyFraction: 0,
|
|
139
|
+
reason: "Booking already settled — cancellation no longer applies.",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const bashHasStarted =
|
|
144
|
+
input.bashStartMs != null && input.nowMs >= input.bashStartMs;
|
|
145
|
+
const bashHasEnded =
|
|
146
|
+
input.bashEndMs != null && input.nowMs >= input.bashEndMs;
|
|
147
|
+
const bashInProgressOrPast = bashHasStarted || bashHasEnded;
|
|
148
|
+
const hoursUntilStart =
|
|
149
|
+
input.bashStartMs != null
|
|
150
|
+
? (input.bashStartMs - input.nowMs) / (60 * 60 * 1000)
|
|
151
|
+
: Number.POSITIVE_INFINITY;
|
|
152
|
+
|
|
153
|
+
if (input.who === "admin") {
|
|
154
|
+
if (bashInProgressOrPast) {
|
|
155
|
+
return {
|
|
156
|
+
allow: true,
|
|
157
|
+
outcome: "NoRefund",
|
|
158
|
+
refundCents: 0,
|
|
159
|
+
penaltyFraction: 0,
|
|
160
|
+
reason: "Admin cancel after bash started — settlement handles money.",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
allow: true,
|
|
165
|
+
outcome: "FullRefund",
|
|
166
|
+
refundCents: input.depositCents,
|
|
167
|
+
penaltyFraction: 0,
|
|
168
|
+
reason: "Admin cancel — full refund of deposit.",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (input.who === "promoter") {
|
|
173
|
+
if (bashInProgressOrPast) {
|
|
174
|
+
return {
|
|
175
|
+
allow: false,
|
|
176
|
+
outcome: "Blocked",
|
|
177
|
+
refundCents: 0,
|
|
178
|
+
penaltyFraction: 0,
|
|
179
|
+
reason:
|
|
180
|
+
"Bash already started — promoter must coordinate with host in the thread, not auto-cancel.",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
allow: true,
|
|
185
|
+
outcome: "FullRefund",
|
|
186
|
+
refundCents: input.depositCents,
|
|
187
|
+
penaltyFraction: 0,
|
|
188
|
+
reason:
|
|
189
|
+
"Promoter cancelled — full refund of host deposit. Bash notifies the host and re-opens the slot.",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// who === "host"
|
|
194
|
+
if (bashInProgressOrPast) {
|
|
195
|
+
return {
|
|
196
|
+
allow: false,
|
|
197
|
+
outcome: "NoRefund",
|
|
198
|
+
refundCents: 0,
|
|
199
|
+
penaltyFraction: 0,
|
|
200
|
+
reason:
|
|
201
|
+
"Bash already in progress or over — the post-event settlement will calculate any refund automatically.",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (hoursUntilStart >= EVENT_PROMOTER_LATE_CANCEL_HOURS) {
|
|
206
|
+
return {
|
|
207
|
+
allow: true,
|
|
208
|
+
outcome: "FullRefund",
|
|
209
|
+
refundCents: input.depositCents,
|
|
210
|
+
penaltyFraction: 0,
|
|
211
|
+
reason: `Host cancelled with more than ${EVENT_PROMOTER_LATE_CANCEL_HOURS}h notice — full refund of deposit.`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const penaltyFraction = EVENT_PROMOTER_LATE_CANCEL_PENALTY_FRACTION;
|
|
216
|
+
const refundCents = Math.max(
|
|
217
|
+
0,
|
|
218
|
+
Math.round(input.depositCents * (1 - penaltyFraction))
|
|
219
|
+
);
|
|
220
|
+
return {
|
|
221
|
+
allow: true,
|
|
222
|
+
outcome: "PartialRefund",
|
|
223
|
+
refundCents,
|
|
224
|
+
penaltyFraction,
|
|
225
|
+
reason: `Host cancelled inside the ${EVENT_PROMOTER_LATE_CANCEL_HOURS}h window — promoter keeps ${Math.round(
|
|
226
|
+
penaltyFraction * 100
|
|
227
|
+
)}% of the deposit to cover planning time.`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The single allowed pricing model for the locked promotion subtype. Exposed
|
|
233
|
+
* as a constant so wizard + api validators share the same source of truth.
|
|
234
|
+
*
|
|
235
|
+
* Retained for backwards compatibility; new call sites should prefer
|
|
236
|
+
* {@link EVENT_PROMOTER_LOCKED_PRICING_MODELS} to allow Phase 3's
|
|
237
|
+
* `PER_PAID_ATTENDEE` and `PER_BASHPOINTS_ATTENDEE`.
|
|
238
|
+
*/
|
|
239
|
+
export const EVENT_PROMOTER_LOCKED_PRICING_MODEL: PricingModel =
|
|
240
|
+
"PER_ATTENDEE";
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Phase 3 — set of pricing models the locked Event Promoter subtype may use.
|
|
244
|
+
* Any of these guarantees a per-head settlement at bash close, which is the
|
|
245
|
+
* actual constraint the worker cares about. Wizard offers them as a choice;
|
|
246
|
+
* api validator accepts any.
|
|
247
|
+
*/
|
|
248
|
+
export const EVENT_PROMOTER_LOCKED_PRICING_MODELS: ReadonlyArray<PricingModel> = [
|
|
249
|
+
"PER_ATTENDEE",
|
|
250
|
+
"PER_PAID_ATTENDEE",
|
|
251
|
+
"PER_BASHPOINTS_ATTENDEE",
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
export type EventPromoterPricingValidation =
|
|
255
|
+
| { ok: true }
|
|
256
|
+
| { ok: false; reason: string; expected: ReadonlyArray<PricingModel> };
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Validates that an `EventService` save with `subType === "Promoter"` uses
|
|
260
|
+
* one of the {@link EVENT_PROMOTER_LOCKED_PRICING_MODELS}. Intended for both:
|
|
261
|
+
*
|
|
262
|
+
* - the Event Services wizard (block save with a friendly message), and
|
|
263
|
+
* - the api save path (reject the request even if the wizard is bypassed).
|
|
264
|
+
*
|
|
265
|
+
* Returns `{ ok: true }` for any non-locked subtype.
|
|
266
|
+
*/
|
|
267
|
+
export function validateEventPromoterPricing(args: {
|
|
268
|
+
eventServiceSubType: PromotionAndMarketingSubType | string | null | undefined;
|
|
269
|
+
pricingModel: PricingModel | null | undefined;
|
|
270
|
+
}): EventPromoterPricingValidation {
|
|
271
|
+
if (!isPerAttendeeLockedPromotionSubType(args.eventServiceSubType)) {
|
|
272
|
+
return { ok: true };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
args.pricingModel != null &&
|
|
277
|
+
EVENT_PROMOTER_LOCKED_PRICING_MODELS.includes(args.pricingModel)
|
|
278
|
+
) {
|
|
279
|
+
return { ok: true };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
expected: EVENT_PROMOTER_LOCKED_PRICING_MODELS,
|
|
285
|
+
reason: `Event Promoter services must use one of: ${EVENT_PROMOTER_LOCKED_PRICING_MODELS.join(
|
|
286
|
+
", "
|
|
287
|
+
)}. Got: ${args.pricingModel ?? "null"}.`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Computes the deposit cents to charge a host at Event Promoter booking time.
|
|
293
|
+
*
|
|
294
|
+
* Logic:
|
|
295
|
+
* - Baseline = max(`bashExpectedAttendees`, `minimumQuantity ?? 0`).
|
|
296
|
+
* - Deposit = baseline * `baseRateCents`.
|
|
297
|
+
*
|
|
298
|
+
* Returns `null` if `baseRateCents` is not set (caller should surface a
|
|
299
|
+
* validation error rather than silently charge $0).
|
|
300
|
+
*/
|
|
301
|
+
export function computeEventPromoterDepositCents(args: {
|
|
302
|
+
bashExpectedAttendees: number;
|
|
303
|
+
baseRateCents: number | null | undefined;
|
|
304
|
+
minimumQuantity: number | null | undefined;
|
|
305
|
+
}): number | null {
|
|
306
|
+
if (args.baseRateCents == null || args.baseRateCents <= 0) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const baseline = Math.max(
|
|
310
|
+
Math.max(0, args.bashExpectedAttendees),
|
|
311
|
+
Math.max(0, args.minimumQuantity ?? 0)
|
|
312
|
+
);
|
|
313
|
+
return baseline * args.baseRateCents;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface EventPromoterSettlementInput {
|
|
317
|
+
baseRateCents: number;
|
|
318
|
+
expectedAttendeesAtBooking: number;
|
|
319
|
+
totalCheckedInAttendees: number;
|
|
320
|
+
/** Per-booking host-set ceiling on attributable attendees; `null` = no cap. */
|
|
321
|
+
maxAttendeesAttributable?: number | null;
|
|
322
|
+
/** Optional floor — usually `EventService.minimumQuantity` at booking time. */
|
|
323
|
+
minimumQuantity?: number | null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface EventPromoterSettlementResult {
|
|
327
|
+
/**
|
|
328
|
+
* Attendee count actually used to bill, after applying the ceiling and any
|
|
329
|
+
* minimum-quantity floor. Always ≥ 0.
|
|
330
|
+
*/
|
|
331
|
+
finalCount: number;
|
|
332
|
+
/**
|
|
333
|
+
* True iff `maxAttendeesAttributable` capped `finalCount`. Useful for
|
|
334
|
+
* analytics — high `ceilingHit` rates signal v2 (per-ticket attribution).
|
|
335
|
+
*/
|
|
336
|
+
ceilingHit: boolean;
|
|
337
|
+
/**
|
|
338
|
+
* Signed delta in cents:
|
|
339
|
+
* - positive → charge the host this additional amount.
|
|
340
|
+
* - negative → refund the host this amount (refund magnitude = |delta|).
|
|
341
|
+
* - 0 → no Stripe action needed; mark `settledAt`.
|
|
342
|
+
*/
|
|
343
|
+
settlementDeltaCents: number;
|
|
344
|
+
/**
|
|
345
|
+
* Total cents the host owes for the booking after settlement
|
|
346
|
+
* (`finalCount * baseRateCents`). Useful for receipts / analytics.
|
|
347
|
+
*/
|
|
348
|
+
totalCents: number;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Pure function: compute the per-attendee settlement delta for an Event
|
|
353
|
+
* Promoter `ServiceBooking`. Caller is responsible for translating the result
|
|
354
|
+
* into Stripe API calls (charge if positive delta, refund if negative).
|
|
355
|
+
*
|
|
356
|
+
* Worked example (host hires two promoters for the same bash @ $5/attendee):
|
|
357
|
+
*
|
|
358
|
+
* Promoter A: expected=200, ceiling=250, checked-in=220
|
|
359
|
+
* → finalCount=220, delta = (220-200) * 500 = +$100
|
|
360
|
+
*
|
|
361
|
+
* Promoter B: expected=100, ceiling=150, same 220 checked-in
|
|
362
|
+
* → finalCount=min(220,150)=150, delta = (150-100) * 500 = +$250
|
|
363
|
+
*
|
|
364
|
+
* @see plan: docs/design/CONTENT-PATTERNS.md, .cursor/plans/ambashadors_page_tiers_ux_*.plan.md
|
|
365
|
+
*/
|
|
366
|
+
export function computeEventPromoterSettlement(
|
|
367
|
+
input: EventPromoterSettlementInput
|
|
368
|
+
): EventPromoterSettlementResult {
|
|
369
|
+
const checkedIn = Math.max(0, input.totalCheckedInAttendees);
|
|
370
|
+
const ceiling =
|
|
371
|
+
input.maxAttendeesAttributable != null && input.maxAttendeesAttributable >= 0
|
|
372
|
+
? input.maxAttendeesAttributable
|
|
373
|
+
: Number.POSITIVE_INFINITY;
|
|
374
|
+
const minQty = Math.max(0, input.minimumQuantity ?? 0);
|
|
375
|
+
|
|
376
|
+
// Apply ceiling first, then enforce minimum-quantity floor for the
|
|
377
|
+
// billing-floor case (host committed to paying for at least `minQty`
|
|
378
|
+
// attendees regardless of turnout).
|
|
379
|
+
const cappedByCeiling = Math.min(checkedIn, ceiling);
|
|
380
|
+
const finalCount = Math.max(cappedByCeiling, minQty);
|
|
381
|
+
const ceilingHit = checkedIn > ceiling;
|
|
382
|
+
|
|
383
|
+
const totalCents = finalCount * input.baseRateCents;
|
|
384
|
+
const expectedCents = input.expectedAttendeesAtBooking * input.baseRateCents;
|
|
385
|
+
const settlementDeltaCents = totalCents - expectedCents;
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
finalCount,
|
|
389
|
+
ceilingHit,
|
|
390
|
+
settlementDeltaCents,
|
|
391
|
+
totalCents,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// --------------------------------------------------------------------------
|
|
396
|
+
// Phase 3 — PER_PAID_ATTENDEE + PER_BASHPOINTS_ATTENDEE settlement helpers.
|
|
397
|
+
// --------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Which checked-in attendees count toward the settlement headcount?
|
|
401
|
+
*
|
|
402
|
+
* - `AllCheckedIn` — every checked-in, non-comp guest (legacy PER_ATTENDEE).
|
|
403
|
+
* - `PaidUsdOnly` — only guests on USD-priced tiers (PER_PAID_ATTENDEE without
|
|
404
|
+
* `countsBashPointsTickets`).
|
|
405
|
+
* - `PaidBpOnly` — only guests on BashPoints-priced tiers
|
|
406
|
+
* (PER_BASHPOINTS_ATTENDEE).
|
|
407
|
+
* - `PaidAny` — paid guests on either currency
|
|
408
|
+
* (PER_PAID_ATTENDEE with `countsBashPointsTickets = true`).
|
|
409
|
+
*/
|
|
410
|
+
export type EventPromoterCountingBasis =
|
|
411
|
+
| "AllCheckedIn"
|
|
412
|
+
| "PaidUsdOnly"
|
|
413
|
+
| "PaidBpOnly"
|
|
414
|
+
| "PaidAny";
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Choose the correct counting basis given the pricing model and the
|
|
418
|
+
* per-booking `countsBashPointsTickets` knob. Pure function so wizard +
|
|
419
|
+
* worker share one mapping.
|
|
420
|
+
*/
|
|
421
|
+
export function eventPromoterCountingBasisFor(args: {
|
|
422
|
+
pricingModel: PricingModel | string | null | undefined;
|
|
423
|
+
countsBashPointsTickets?: boolean | null;
|
|
424
|
+
}): EventPromoterCountingBasis {
|
|
425
|
+
switch (args.pricingModel) {
|
|
426
|
+
case "PER_PAID_ATTENDEE":
|
|
427
|
+
return args.countsBashPointsTickets ? "PaidAny" : "PaidUsdOnly";
|
|
428
|
+
case "PER_BASHPOINTS_ATTENDEE":
|
|
429
|
+
return "PaidBpOnly";
|
|
430
|
+
case "PER_ATTENDEE":
|
|
431
|
+
default:
|
|
432
|
+
return "AllCheckedIn";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Pick the matching checked-in count from the snapshots a worker has gathered
|
|
438
|
+
* at settlement time. Returns the **paying** head count regardless of
|
|
439
|
+
* currency, narrowed by basis.
|
|
440
|
+
*/
|
|
441
|
+
export interface EventPromoterAttendeeSnapshots {
|
|
442
|
+
/** All checked-in attendees (`AllCheckedIn` basis). */
|
|
443
|
+
totalCheckedInAttendees: number;
|
|
444
|
+
/** Checked-in attendees on USD-priced tiers (`PaidUsdOnly` / `PaidAny`). */
|
|
445
|
+
paidAttendeesAtSettle?: number | null;
|
|
446
|
+
/** Checked-in attendees on BP-priced tiers (`PaidBpOnly` / `PaidAny`). */
|
|
447
|
+
paidInBashPointsAttendeesAtSettle?: number | null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function pickCheckedInForBasis(
|
|
451
|
+
basis: EventPromoterCountingBasis,
|
|
452
|
+
s: EventPromoterAttendeeSnapshots
|
|
453
|
+
): number {
|
|
454
|
+
switch (basis) {
|
|
455
|
+
case "AllCheckedIn":
|
|
456
|
+
return Math.max(0, s.totalCheckedInAttendees);
|
|
457
|
+
case "PaidUsdOnly":
|
|
458
|
+
return Math.max(0, s.paidAttendeesAtSettle ?? 0);
|
|
459
|
+
case "PaidBpOnly":
|
|
460
|
+
return Math.max(0, s.paidInBashPointsAttendeesAtSettle ?? 0);
|
|
461
|
+
case "PaidAny":
|
|
462
|
+
return (
|
|
463
|
+
Math.max(0, s.paidAttendeesAtSettle ?? 0) +
|
|
464
|
+
Math.max(0, s.paidInBashPointsAttendeesAtSettle ?? 0)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export interface EventPromoterMixedSettlementInput {
|
|
470
|
+
/** Per-attendee USD rate (cents). 0/null = no USD owed. */
|
|
471
|
+
baseRateCents?: number | null;
|
|
472
|
+
/** Per-attendee BP rate. 0/null = no BP owed. */
|
|
473
|
+
baseRateBashPoints?: number | null;
|
|
474
|
+
/** Expected USD-paying head count at booking. */
|
|
475
|
+
expectedPaidAttendeesAtBooking?: number | null;
|
|
476
|
+
/** Expected BP-paying head count at booking. */
|
|
477
|
+
expectedBpAttendeesAtBooking?: number | null;
|
|
478
|
+
/** Snapshots from the bash checkout (see `EventPromoterAttendeeSnapshots`). */
|
|
479
|
+
snapshots: EventPromoterAttendeeSnapshots;
|
|
480
|
+
/** Optional ceilings — separate caps for USD and BP. */
|
|
481
|
+
maxUsdAttendeesAttributable?: number | null;
|
|
482
|
+
maxBpAttendeesAttributable?: number | null;
|
|
483
|
+
/** Optional dollar cap on USD side (from `maxBillableCents`). */
|
|
484
|
+
maxBillableCents?: number | null;
|
|
485
|
+
/** Optional BP cap on BP side (from `maxBillableBashPoints`). */
|
|
486
|
+
maxBillableBashPoints?: number | null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export interface EventPromoterMixedSettlementResult {
|
|
490
|
+
/** USD owed at settlement. */
|
|
491
|
+
usdTotalCents: number;
|
|
492
|
+
/** USD delta vs the deposit pre-paid at booking. */
|
|
493
|
+
usdDeltaCents: number;
|
|
494
|
+
/** BashPoints owed at settlement. */
|
|
495
|
+
bpTotal: number;
|
|
496
|
+
/** BP delta vs the deposit pre-paid at booking. */
|
|
497
|
+
bpDelta: number;
|
|
498
|
+
/** True if either cap kicked in. */
|
|
499
|
+
cappedUsd: boolean;
|
|
500
|
+
cappedBp: boolean;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Pure function for mixed PER_PAID_ATTENDEE / PER_BASHPOINTS_ATTENDEE
|
|
505
|
+
* settlement. Either side can be unset (the worker only computes the side
|
|
506
|
+
* that applies). Caller is responsible for the actual Stripe + BP ledger
|
|
507
|
+
* postings.
|
|
508
|
+
*/
|
|
509
|
+
export function computeEventPromoterMixedSettlement(
|
|
510
|
+
input: EventPromoterMixedSettlementInput
|
|
511
|
+
): EventPromoterMixedSettlementResult {
|
|
512
|
+
// --- USD side -----------------------------------------------------------
|
|
513
|
+
const usdRate = Math.max(0, input.baseRateCents ?? 0);
|
|
514
|
+
let usdCount = pickCheckedInForBasis("PaidUsdOnly", input.snapshots);
|
|
515
|
+
const usdCeiling =
|
|
516
|
+
input.maxUsdAttendeesAttributable != null &&
|
|
517
|
+
input.maxUsdAttendeesAttributable >= 0
|
|
518
|
+
? input.maxUsdAttendeesAttributable
|
|
519
|
+
: Number.POSITIVE_INFINITY;
|
|
520
|
+
usdCount = Math.min(usdCount, usdCeiling);
|
|
521
|
+
|
|
522
|
+
let usdTotalCents = usdRate * usdCount;
|
|
523
|
+
let cappedUsd = false;
|
|
524
|
+
if (
|
|
525
|
+
input.maxBillableCents != null &&
|
|
526
|
+
input.maxBillableCents >= 0 &&
|
|
527
|
+
usdTotalCents > input.maxBillableCents
|
|
528
|
+
) {
|
|
529
|
+
usdTotalCents = input.maxBillableCents;
|
|
530
|
+
cappedUsd = true;
|
|
531
|
+
}
|
|
532
|
+
const usdExpected =
|
|
533
|
+
Math.max(0, input.expectedPaidAttendeesAtBooking ?? 0) * usdRate;
|
|
534
|
+
const usdDeltaCents = usdTotalCents - usdExpected;
|
|
535
|
+
|
|
536
|
+
// --- BP side ------------------------------------------------------------
|
|
537
|
+
const bpRate = Math.max(0, input.baseRateBashPoints ?? 0);
|
|
538
|
+
let bpCount = pickCheckedInForBasis("PaidBpOnly", input.snapshots);
|
|
539
|
+
const bpCeiling =
|
|
540
|
+
input.maxBpAttendeesAttributable != null &&
|
|
541
|
+
input.maxBpAttendeesAttributable >= 0
|
|
542
|
+
? input.maxBpAttendeesAttributable
|
|
543
|
+
: Number.POSITIVE_INFINITY;
|
|
544
|
+
bpCount = Math.min(bpCount, bpCeiling);
|
|
545
|
+
|
|
546
|
+
let bpTotal = bpRate * bpCount;
|
|
547
|
+
let cappedBp = false;
|
|
548
|
+
if (
|
|
549
|
+
input.maxBillableBashPoints != null &&
|
|
550
|
+
input.maxBillableBashPoints >= 0 &&
|
|
551
|
+
bpTotal > input.maxBillableBashPoints
|
|
552
|
+
) {
|
|
553
|
+
bpTotal = input.maxBillableBashPoints;
|
|
554
|
+
cappedBp = true;
|
|
555
|
+
}
|
|
556
|
+
const bpExpected =
|
|
557
|
+
Math.max(0, input.expectedBpAttendeesAtBooking ?? 0) * bpRate;
|
|
558
|
+
const bpDelta = bpTotal - bpExpected;
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
usdTotalCents,
|
|
562
|
+
usdDeltaCents,
|
|
563
|
+
bpTotal,
|
|
564
|
+
bpDelta,
|
|
565
|
+
cappedUsd,
|
|
566
|
+
cappedBp,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// --------------------------------------------------------------------------
|
|
571
|
+
// Phase 3 — BashPoints payout source resolver (pool-then-wallet).
|
|
572
|
+
// --------------------------------------------------------------------------
|
|
573
|
+
|
|
574
|
+
export type BashPointsPayoutSourceValue = "HostWallet" | "BashBpPool";
|
|
575
|
+
|
|
576
|
+
export interface BashPointsPayoutResolverInput {
|
|
577
|
+
/** From `BashEvent.bashPointsPayoutSource`. */
|
|
578
|
+
source: BashPointsPayoutSourceValue;
|
|
579
|
+
/** BP the worker owes the provider for this settlement. */
|
|
580
|
+
payoutBp: number;
|
|
581
|
+
/** BP the host has in their wallet at settlement time. */
|
|
582
|
+
hostWalletBp: number;
|
|
583
|
+
/**
|
|
584
|
+
* For `BashBpPool` source: live remaining BP in the bash's BP pool. This is
|
|
585
|
+
* `sum(BP from BP-ticket purchases) − sum(BP already paid out) −
|
|
586
|
+
* sum(outstanding BP-refund obligations)`. Caller computes; resolver only
|
|
587
|
+
* drains.
|
|
588
|
+
*/
|
|
589
|
+
bashPoolBp?: number;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export interface BashPointsPayoutResolverResult {
|
|
593
|
+
/** BP drained from the bash's BashPoints pool. */
|
|
594
|
+
fromPoolBp: number;
|
|
595
|
+
/** BP debited from the host's wallet. */
|
|
596
|
+
fromWalletBp: number;
|
|
597
|
+
/** BP that could NOT be sourced (resulting failure → dunning). */
|
|
598
|
+
shortfallBp: number;
|
|
599
|
+
/** True when fromWalletBp > 0 — used to surface UI / push notifications. */
|
|
600
|
+
walletDebited: boolean;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Pure function that decides which BashPoints buckets to drain to satisfy a
|
|
605
|
+
* provider payout. Mirrors the worker's behavior:
|
|
606
|
+
*
|
|
607
|
+
* - `HostWallet`: take everything from the host's wallet; any shortfall is
|
|
608
|
+
* a payment failure (`SettlementPaymentFailed`).
|
|
609
|
+
* - `BashBpPool`: drain the pool first; pay the remainder from the host's
|
|
610
|
+
* wallet; if still short, mark `SettlementPaymentFailed`.
|
|
611
|
+
*
|
|
612
|
+
* Negative inputs are clamped to zero so the resolver never returns a
|
|
613
|
+
* negative bucket.
|
|
614
|
+
*/
|
|
615
|
+
export function resolveBashPointsPayoutSources(
|
|
616
|
+
input: BashPointsPayoutResolverInput
|
|
617
|
+
): BashPointsPayoutResolverResult {
|
|
618
|
+
const need = Math.max(0, Math.floor(input.payoutBp));
|
|
619
|
+
const wallet = Math.max(0, Math.floor(input.hostWalletBp));
|
|
620
|
+
const pool = Math.max(0, Math.floor(input.bashPoolBp ?? 0));
|
|
621
|
+
|
|
622
|
+
if (input.source === "HostWallet") {
|
|
623
|
+
const fromWalletBp = Math.min(need, wallet);
|
|
624
|
+
const shortfallBp = need - fromWalletBp;
|
|
625
|
+
return {
|
|
626
|
+
fromPoolBp: 0,
|
|
627
|
+
fromWalletBp,
|
|
628
|
+
shortfallBp,
|
|
629
|
+
walletDebited: fromWalletBp > 0,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// BashBpPool
|
|
634
|
+
const fromPoolBp = Math.min(need, pool);
|
|
635
|
+
const remainAfterPool = need - fromPoolBp;
|
|
636
|
+
const fromWalletBp = Math.min(remainAfterPool, wallet);
|
|
637
|
+
const shortfallBp = remainAfterPool - fromWalletBp;
|
|
638
|
+
return {
|
|
639
|
+
fromPoolBp,
|
|
640
|
+
fromWalletBp,
|
|
641
|
+
shortfallBp,
|
|
642
|
+
walletDebited: fromWalletBp > 0,
|
|
643
|
+
};
|
|
644
|
+
}
|