@bash-app/bash-common 30.385.0 → 30.386.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/definitions.d.ts +14 -13
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +13 -12
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +15 -0
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js +9 -0
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/utils/__tests__/paymentUtils.test.js +28 -1
- package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
- package/dist/utils/__tests__/serviceAvailabilityDisplay.test.js +56 -1
- package/dist/utils/__tests__/serviceAvailabilityDisplay.test.js.map +1 -1
- package/dist/utils/__tests__/slugUtils.test.js +5 -3
- package/dist/utils/__tests__/slugUtils.test.js.map +1 -1
- package/dist/utils/paymentUtils.d.ts +18 -1
- package/dist/utils/paymentUtils.d.ts.map +1 -1
- package/dist/utils/paymentUtils.js +29 -2
- package/dist/utils/paymentUtils.js.map +1 -1
- package/dist/utils/serviceAvailabilityDisplay.d.ts +28 -2
- package/dist/utils/serviceAvailabilityDisplay.d.ts.map +1 -1
- package/dist/utils/serviceAvailabilityDisplay.js +39 -0
- package/dist/utils/serviceAvailabilityDisplay.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +50 -4
- package/src/definitions.ts +13 -12
- package/src/extendedSchemas.ts +15 -0
- package/src/utils/__tests__/paymentUtils.test.ts +36 -0
- package/src/utils/__tests__/serviceAvailabilityDisplay.test.ts +73 -0
- package/src/utils/__tests__/slugUtils.test.ts +5 -3
- package/src/utils/paymentUtils.ts +47 -2
- package/src/utils/serviceAvailabilityDisplay.ts +82 -2
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyAvailabilityPreset,
|
|
3
|
+
blockedRangesFromSpecialRates,
|
|
3
4
|
computeEarliestOpenDate,
|
|
5
|
+
countServicesAvailableOnDate,
|
|
4
6
|
detectAvailabilityPreset,
|
|
7
|
+
filterServicesAvailableOnDate,
|
|
5
8
|
formatAvailabilityPresetLabel,
|
|
9
|
+
isServiceAvailableOnDate,
|
|
6
10
|
parseAvailableHours,
|
|
11
|
+
type ServiceDayAvailabilityInput,
|
|
7
12
|
} from "../serviceAvailabilityDisplay.js";
|
|
8
13
|
|
|
9
14
|
describe("applyAvailabilityPreset", () => {
|
|
@@ -150,3 +155,71 @@ describe("computeEarliestOpenDate", () => {
|
|
|
150
155
|
expect(result).toBeNull();
|
|
151
156
|
});
|
|
152
157
|
});
|
|
158
|
+
|
|
159
|
+
describe("isServiceAvailableOnDate", () => {
|
|
160
|
+
const friday = new Date(2026, 8, 11); // Friday Sep 11 2026
|
|
161
|
+
const saturday = new Date(2026, 8, 12);
|
|
162
|
+
|
|
163
|
+
it("treats a listing with no hours or blocks as available", () => {
|
|
164
|
+
expect(isServiceAvailableOnDate({}, friday)).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("closes weekdays when the service only works weekends", () => {
|
|
168
|
+
const service = {
|
|
169
|
+
availableHours: applyAvailabilityPreset("weekends"),
|
|
170
|
+
};
|
|
171
|
+
expect(isServiceAvailableOnDate(service, friday)).toBe(false);
|
|
172
|
+
expect(isServiceAvailableOnDate(service, saturday)).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("treats owner-blocked special rates as unavailable", () => {
|
|
176
|
+
const service = {
|
|
177
|
+
serviceRatesAssociation: {
|
|
178
|
+
serviceSpecialRates: [
|
|
179
|
+
{
|
|
180
|
+
isAvailable: false,
|
|
181
|
+
startDate: friday,
|
|
182
|
+
endDate: friday,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
expect(isServiceAvailableOnDate(service, friday)).toBe(false);
|
|
188
|
+
expect(isServiceAvailableOnDate(service, saturday)).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("treats a confirmed booking window as occupying the day", () => {
|
|
192
|
+
const service = {
|
|
193
|
+
bookedRanges: [{ start: friday, end: friday }],
|
|
194
|
+
};
|
|
195
|
+
expect(isServiceAvailableOnDate(service, friday)).toBe(false);
|
|
196
|
+
expect(isServiceAvailableOnDate(service, saturday)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("counts and filters a mixed set of listings", () => {
|
|
200
|
+
type ListedService = ServiceDayAvailabilityInput & { id: string };
|
|
201
|
+
const open: ListedService = { id: "open" };
|
|
202
|
+
const booked: ListedService = {
|
|
203
|
+
id: "booked",
|
|
204
|
+
bookedRanges: [{ start: friday, end: friday }],
|
|
205
|
+
};
|
|
206
|
+
const blocked: ListedService = {
|
|
207
|
+
id: "blocked",
|
|
208
|
+
serviceRatesAssociation: {
|
|
209
|
+
serviceSpecialRates: [
|
|
210
|
+
{ isAvailable: false, startDate: friday, endDate: friday },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
const all: ListedService[] = [open, booked, blocked];
|
|
215
|
+
expect(countServicesAvailableOnDate(all, friday)).toBe(1);
|
|
216
|
+
expect(filterServicesAvailableOnDate(all, friday).map((s) => s.id)).toEqual(
|
|
217
|
+
["open"]
|
|
218
|
+
);
|
|
219
|
+
expect(
|
|
220
|
+
blockedRangesFromSpecialRates(
|
|
221
|
+
blocked.serviceRatesAssociation?.serviceSpecialRates
|
|
222
|
+
)
|
|
223
|
+
).toHaveLength(1);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -117,9 +117,11 @@ describe("getBashEventWizardPayoutStepPath", () => {
|
|
|
117
117
|
test("How substep indices match host-flow order", () => {
|
|
118
118
|
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.PRICE).toBe(0);
|
|
119
119
|
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.APPLICATION).toBe(1);
|
|
120
|
-
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.
|
|
121
|
-
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.
|
|
122
|
-
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.
|
|
120
|
+
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.PROMOTER_CHALLENGE).toBe(2);
|
|
121
|
+
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.PROMO_CODES).toBe(3);
|
|
122
|
+
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.GIVEAWAYS).toBe(7);
|
|
123
|
+
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.TASKS).toBe(9);
|
|
124
|
+
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP.PAYOUT).toBe(10);
|
|
123
125
|
expect(BASH_EVENT_WIZARD_HOW_SUBSTEP_PAYOUT).toBe(
|
|
124
126
|
BASH_EVENT_WIZARD_HOW_SUBSTEP.PAYOUT
|
|
125
127
|
);
|
|
@@ -430,6 +430,32 @@ export function lowestGuestAllInPriceCents(
|
|
|
430
430
|
return min;
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
+
/** Max share of attributed ticket subtotal a host can assign to a performer (50%). */
|
|
434
|
+
export const MAX_TICKET_COMMISSION_BPS = 5000;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Floor cents owed to a promo-code performer from the discounted ticket subtotal.
|
|
438
|
+
* Guest price is unchanged; this amount is withheld from the host via application_fee.
|
|
439
|
+
*/
|
|
440
|
+
export function ticketCommissionCents(
|
|
441
|
+
basisCents: number,
|
|
442
|
+
commissionBps: number | null | undefined
|
|
443
|
+
): number {
|
|
444
|
+
if (basisCents <= 0) return 0;
|
|
445
|
+
const bps = Math.floor(Number(commissionBps) || 0);
|
|
446
|
+
if (bps <= 0) return 0;
|
|
447
|
+
const clamped = Math.min(MAX_TICKET_COMMISSION_BPS, bps);
|
|
448
|
+
return Math.floor((basisCents * clamped) / 10_000);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function clampTicketCommissionBps(
|
|
452
|
+
commissionBps: number | null | undefined
|
|
453
|
+
): number {
|
|
454
|
+
const bps = Math.floor(Number(commissionBps) || 0);
|
|
455
|
+
if (bps <= 0) return 0;
|
|
456
|
+
return Math.min(MAX_TICKET_COMMISSION_BPS, bps);
|
|
457
|
+
}
|
|
458
|
+
|
|
433
459
|
/**
|
|
434
460
|
* Compute the Stripe Connect `application_fee_amount` for a ticket-style charge,
|
|
435
461
|
* along with its constituent parts.
|
|
@@ -441,8 +467,10 @@ export function lowestGuestAllInPriceCents(
|
|
|
441
467
|
* - `bashPlatformTotalCents` and `guestPlatformLineCents` are both zero
|
|
442
468
|
* - `totalChargedCents` collapses to the discounted subtotal (no guest-line fee added)
|
|
443
469
|
* - `applicationFeeAmountCents` equals `stripeProcessingCents`
|
|
470
|
+
* plus optional `performerCommissionCents`
|
|
444
471
|
*
|
|
445
|
-
* Default (`false`) preserves existing math bit-exact for every current caller
|
|
472
|
+
* Default (`false`) preserves existing math bit-exact for every current caller
|
|
473
|
+
* when `performerCommissionCents` is omitted or 0.
|
|
446
474
|
*/
|
|
447
475
|
export function computeTicketApplicationFeeAmountCents(args: {
|
|
448
476
|
discountedSubtotalCents: number;
|
|
@@ -456,6 +484,11 @@ export function computeTicketApplicationFeeAmountCents(args: {
|
|
|
456
484
|
* NEVER from `BashEvent.nonProfit`, which is user-entered.
|
|
457
485
|
*/
|
|
458
486
|
isVerifiedNonprofitHost?: boolean;
|
|
487
|
+
/**
|
|
488
|
+
* Performer / street-team cut withheld from the host (not added to the guest).
|
|
489
|
+
* Must be less than remaining charge after Stripe + Bash platform fees.
|
|
490
|
+
*/
|
|
491
|
+
performerCommissionCents?: number;
|
|
459
492
|
}): {
|
|
460
493
|
totalChargedCents: number;
|
|
461
494
|
/** Stripe's 2.9% + $0.30 pass-through; unchanged by the nonprofit waiver. */
|
|
@@ -467,15 +500,27 @@ export function computeTicketApplicationFeeAmountCents(args: {
|
|
|
467
500
|
guestPlatformLineCents: number;
|
|
468
501
|
/** Guest-visible Stripe processing line (0 when host absorbs). */
|
|
469
502
|
guestProcessingLineCents: number;
|
|
503
|
+
/** Host-withheld performer cut included in application_fee. */
|
|
504
|
+
performerCommissionCents: number;
|
|
470
505
|
} {
|
|
471
506
|
const breakdown = computeGuestTicketCheckoutBreakdown(args);
|
|
507
|
+
const performerCommissionCents = Math.max(
|
|
508
|
+
0,
|
|
509
|
+
Math.floor(args.performerCommissionCents ?? 0)
|
|
510
|
+
);
|
|
511
|
+
const maxWithhold = Math.max(
|
|
512
|
+
0,
|
|
513
|
+
breakdown.totalDueCents - breakdown.applicationFeeAmountCents - 1
|
|
514
|
+
);
|
|
515
|
+
const withheld = Math.min(performerCommissionCents, maxWithhold);
|
|
472
516
|
return {
|
|
473
517
|
totalChargedCents: breakdown.totalDueCents,
|
|
474
518
|
stripeProcessingCents: breakdown.stripeProcessingCents,
|
|
475
519
|
bashPlatformTotalCents: breakdown.bashPlatformTotalCents,
|
|
476
|
-
applicationFeeAmountCents: breakdown.applicationFeeAmountCents,
|
|
520
|
+
applicationFeeAmountCents: breakdown.applicationFeeAmountCents + withheld,
|
|
477
521
|
guestPlatformLineCents: breakdown.guestPlatformLineCents,
|
|
478
522
|
guestProcessingLineCents: breakdown.guestProcessingLineCents,
|
|
523
|
+
performerCommissionCents: withheld,
|
|
479
524
|
};
|
|
480
525
|
}
|
|
481
526
|
|
|
@@ -30,6 +30,23 @@ export interface AvailabilityBlockedRange {
|
|
|
30
30
|
end: Date | string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Minimum shape needed to decide whether a service can take a bash on a
|
|
35
|
+
* calendar day. One confirmed/approved booking occupies the whole day.
|
|
36
|
+
*/
|
|
37
|
+
export interface ServiceDayAvailabilityInput {
|
|
38
|
+
availableHours?: unknown;
|
|
39
|
+
serviceRatesAssociation?: {
|
|
40
|
+
serviceSpecialRates?: Array<{
|
|
41
|
+
isAvailable?: boolean | null;
|
|
42
|
+
startDate?: Date | string | null;
|
|
43
|
+
endDate?: Date | string | null;
|
|
44
|
+
}> | null;
|
|
45
|
+
} | null;
|
|
46
|
+
/** Occupied ranges from Confirmed/Approved bookings (public list). */
|
|
47
|
+
bookedRanges?: readonly AvailabilityBlockedRange[] | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
const WEEKDAY_NAMES = [
|
|
34
51
|
"Monday",
|
|
35
52
|
"Tuesday",
|
|
@@ -156,9 +173,72 @@ export function isAvailabilityDayOpen(
|
|
|
156
173
|
return openDays[name] !== false;
|
|
157
174
|
}
|
|
158
175
|
|
|
176
|
+
/** Owner-blocked ranges from `ServiceSpecialRates` where `isAvailable` is false. */
|
|
177
|
+
export function blockedRangesFromSpecialRates(
|
|
178
|
+
specialRates:
|
|
179
|
+
| NonNullable<
|
|
180
|
+
NonNullable<
|
|
181
|
+
ServiceDayAvailabilityInput["serviceRatesAssociation"]
|
|
182
|
+
>["serviceSpecialRates"]
|
|
183
|
+
>
|
|
184
|
+
| null
|
|
185
|
+
| undefined
|
|
186
|
+
): AvailabilityBlockedRange[] {
|
|
187
|
+
if (!specialRates) return [];
|
|
188
|
+
const ranges: AvailabilityBlockedRange[] = [];
|
|
189
|
+
for (const rate of specialRates) {
|
|
190
|
+
if (rate?.isAvailable === false && rate.startDate && rate.endDate) {
|
|
191
|
+
ranges.push({ start: rate.startDate, end: rate.endDate });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return ranges;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* True when the service can take a bash on `date`: weekly hours are open,
|
|
199
|
+
* the owner has not blocked the day, and no confirmed/approved booking
|
|
200
|
+
* already occupies it. Missing hours mean every weekday is treated as open.
|
|
201
|
+
*/
|
|
202
|
+
export function isServiceAvailableOnDate(
|
|
203
|
+
service: ServiceDayAvailabilityInput,
|
|
204
|
+
date: Date
|
|
205
|
+
): boolean {
|
|
206
|
+
const config = parseAvailableHours(service.availableHours);
|
|
207
|
+
if (!isAvailabilityDayOpen(config?.openDays, date)) return false;
|
|
208
|
+
if (
|
|
209
|
+
isDateBlocked(
|
|
210
|
+
blockedRangesFromSpecialRates(
|
|
211
|
+
service.serviceRatesAssociation?.serviceSpecialRates
|
|
212
|
+
),
|
|
213
|
+
date
|
|
214
|
+
)
|
|
215
|
+
) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (isDateBlocked(service.bookedRanges ?? [], date)) return false;
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function filterServicesAvailableOnDate<T extends ServiceDayAvailabilityInput>(
|
|
223
|
+
services: readonly T[],
|
|
224
|
+
date: Date
|
|
225
|
+
): T[] {
|
|
226
|
+
return services.filter((service) => isServiceAvailableOnDate(service, date));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function countServicesAvailableOnDate<
|
|
230
|
+
T extends ServiceDayAvailabilityInput,
|
|
231
|
+
>(services: readonly T[], date: Date): number {
|
|
232
|
+
let count = 0;
|
|
233
|
+
for (const service of services) {
|
|
234
|
+
if (isServiceAvailableOnDate(service, date)) count += 1;
|
|
235
|
+
}
|
|
236
|
+
return count;
|
|
237
|
+
}
|
|
238
|
+
|
|
159
239
|
/** Whether a date falls within any owner-blocked (unavailable) range. */
|
|
160
240
|
export function isDateBlocked(
|
|
161
|
-
blockedRanges: AvailabilityBlockedRange[],
|
|
241
|
+
blockedRanges: readonly AvailabilityBlockedRange[],
|
|
162
242
|
date: Date
|
|
163
243
|
): boolean {
|
|
164
244
|
const day = startOfLocalDay(date).getTime();
|
|
@@ -177,7 +257,7 @@ export function isDateBlocked(
|
|
|
177
257
|
*/
|
|
178
258
|
export function computeEarliestOpenDate(params: {
|
|
179
259
|
openDays?: Record<string, boolean> | null;
|
|
180
|
-
blockedRanges?: AvailabilityBlockedRange[] | null;
|
|
260
|
+
blockedRanges?: readonly AvailabilityBlockedRange[] | null;
|
|
181
261
|
fromDate?: Date;
|
|
182
262
|
maxLookaheadDays?: number;
|
|
183
263
|
}): Date | null {
|