@bash-app/bash-common 30.3.0 → 30.5.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/package.json +1 -1
- package/prisma/schema.prisma +140 -87
- package/scripts/symlinks.sh +2 -0
- package/src/definitions.ts +37 -43
- package/src/extendedSchemas.ts +122 -104
- package/src/index.ts +4 -3
- package/src/utils/arrayUtils.ts +8 -0
- package/src/utils/luxonUtils.ts +78 -15
- package/src/utils/service/apiServiceBookingApiUtils.ts +222 -0
- package/src/utils/service/frontendServiceBookingUtils.ts +310 -0
- package/src/utils/service/serviceBookingStatusUtils.ts +96 -27
- package/src/utils/service/serviceBookingTypes.ts +56 -0
- package/src/utils/service/serviceDBUtils.ts +35 -35
- package/src/utils/service/serviceRateDBUtils.ts +179 -179
- package/src/utils/service/serviceRateTypes.ts +18 -0
- package/src/utils/service/serviceRateUtils.ts +38 -64
- package/src/utils/service/serviceUtils.ts +35 -3
- package/src/utils/stringUtils.ts +1 -1
- package/src/utils/service/serviceBookingApiUtils.ts +0 -259
- package/src/utils/service/serviceBookingUtils.ts +0 -391
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ServiceBookingApiParamsLuxon,
|
|
3
|
-
ServiceBookingApiParams,
|
|
4
|
-
ServiceBookedDayApiParamsLuxon,
|
|
5
|
-
ServiceAddonApiParams,
|
|
6
|
-
ServiceBookedDayApiParams,
|
|
7
|
-
ServicePriceToBookApiResult,
|
|
8
|
-
} from "../../definitions";
|
|
9
|
-
import {
|
|
10
|
-
ServiceAddonExt,
|
|
11
|
-
ServiceBookingAddOnExt,
|
|
12
|
-
ServiceBookingDayExt,
|
|
13
|
-
ServiceBookingExt,
|
|
14
|
-
ServiceExt,
|
|
15
|
-
} from "../../extendedSchemas";
|
|
16
|
-
import {
|
|
17
|
-
dateTimeFromDate,
|
|
18
|
-
dateTimeFromString,
|
|
19
|
-
dateTimeRangeFromDates,
|
|
20
|
-
} from "../luxonUtils";
|
|
21
|
-
import { convertDollarsToCents } from "../paymentUtils";
|
|
22
|
-
import {
|
|
23
|
-
ServiceAttendeeOption,
|
|
24
|
-
serviceAttendeeOptions,
|
|
25
|
-
} from "./attendeeOptionUtils";
|
|
26
|
-
import {
|
|
27
|
-
ServiceGetPriceToBookFeesParams,
|
|
28
|
-
ServiceBookingDayInfoParams,
|
|
29
|
-
ServiceBookingFee,
|
|
30
|
-
ServiceAddonInput,
|
|
31
|
-
ServiceGetPriceToBookResult,
|
|
32
|
-
serviceGetPriceToBookFees,
|
|
33
|
-
serviceGetPriceToBookExt,
|
|
34
|
-
ServiceGetPriceToBookExtDay,
|
|
35
|
-
} from "./serviceBookingUtils";
|
|
36
|
-
|
|
37
|
-
export type ServiceCantBookReason = {
|
|
38
|
-
type: string;
|
|
39
|
-
msg: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const ServiceCantBookReasons = {
|
|
43
|
-
notInBusinessHours: {
|
|
44
|
-
type: "notInBusinessHours",
|
|
45
|
-
msg: "Time not within normal Business Hours",
|
|
46
|
-
} as ServiceCantBookReason,
|
|
47
|
-
minimumTimeBlock: {
|
|
48
|
-
type: "minimumTimeBlock",
|
|
49
|
-
msg: "Minimum time block not satisfied",
|
|
50
|
-
} as ServiceCantBookReason,
|
|
51
|
-
dateBlocked: {
|
|
52
|
-
type: "dateBlocked",
|
|
53
|
-
msg: "The selected date is blocked.",
|
|
54
|
-
} as ServiceCantBookReason,
|
|
55
|
-
alreadyBooked: {
|
|
56
|
-
type: "alreadyBooked",
|
|
57
|
-
msg: "This service has already been booked.",
|
|
58
|
-
} as ServiceCantBookReason,
|
|
59
|
-
requestDeclined: {
|
|
60
|
-
type: "requestDeclined",
|
|
61
|
-
msg: "The booking request has been declined.",
|
|
62
|
-
} as ServiceCantBookReason,
|
|
63
|
-
} as const;
|
|
64
|
-
|
|
65
|
-
export type ServiceCantBookReasons = keyof typeof ServiceCantBookReasons;
|
|
66
|
-
|
|
67
|
-
export function apiTransformAddOnsToAddonInputs<
|
|
68
|
-
AddonParams_t extends ServiceAddonApiParams
|
|
69
|
-
>(
|
|
70
|
-
addOnsApi: AddonParams_t[],
|
|
71
|
-
serviceAddons: ServiceAddonExt[]
|
|
72
|
-
): ServiceAddonInput[] {
|
|
73
|
-
// Create a Map for faster lookup by addonId
|
|
74
|
-
const addonMap = new Map<string, ServiceAddonExt>(
|
|
75
|
-
serviceAddons.map((addon) => [addon.id, addon])
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
return addOnsApi.map((addOnApi) => {
|
|
79
|
-
const matchedAddon = addonMap.get(addOnApi.addonId);
|
|
80
|
-
if (!matchedAddon) {
|
|
81
|
-
throw new Error(`Failed to match addon with id: ${addOnApi.addonId}`);
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
...matchedAddon,
|
|
85
|
-
...addOnApi,
|
|
86
|
-
} as ServiceAddonInput;
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// export function serviceAddonInputAddOnsToAddonInputs<
|
|
91
|
-
// AddonParams_t extends ServiceAddonInput
|
|
92
|
-
// >(
|
|
93
|
-
// addOnsApi: AddonParams_t[],
|
|
94
|
-
// serviceAddons: ServiceAddonExt[]
|
|
95
|
-
// ): ServiceAddonInput[] {
|
|
96
|
-
// // Create a Map for faster lookup by addonId
|
|
97
|
-
// const addonMap = new Map<string, ServiceAddonExt>(
|
|
98
|
-
// serviceAddons.map((addon) => [addon.id, addon])
|
|
99
|
-
// );
|
|
100
|
-
|
|
101
|
-
// return addOnsApi.map((addOnApi) => {
|
|
102
|
-
// const matchedAddon = addonMap.get(addOnApi.id);
|
|
103
|
-
// if (!matchedAddon) {
|
|
104
|
-
// throw new Error(`Failed to match addon with id: ${addOnApi.id}`);
|
|
105
|
-
// }
|
|
106
|
-
// return {
|
|
107
|
-
// ...matchedAddon,
|
|
108
|
-
// ...addOnApi,
|
|
109
|
-
// } as ServiceAddonInput;
|
|
110
|
-
// });
|
|
111
|
-
// }
|
|
112
|
-
|
|
113
|
-
// export function apiTransformBookedDays(
|
|
114
|
-
// bookedDays: ServiceBookedDayApiParams[],
|
|
115
|
-
// bookingId: string,
|
|
116
|
-
// serviceAddons: ServiceAddonExt[]
|
|
117
|
-
// ): ServiceBookingDayExt[] {
|
|
118
|
-
// return bookedDays.map((day) => {
|
|
119
|
-
// // Transform the addOns using the helper function and then map to the expected output
|
|
120
|
-
// const addOns = apiTransformAddOnsToAddonInputs(
|
|
121
|
-
// day.addOns,
|
|
122
|
-
// serviceAddons
|
|
123
|
-
// ).map(
|
|
124
|
-
// (addOn) =>
|
|
125
|
-
// ({
|
|
126
|
-
// addOnId: addOn.id,
|
|
127
|
-
// chosenQuantity: addOn.chosenQuantity,
|
|
128
|
-
// serviceBookingDayId: bookingId,
|
|
129
|
-
// costATBCents: convertDollarsToCents(addOn.price),
|
|
130
|
-
// } as ServiceBookingAddOnExt)
|
|
131
|
-
// );
|
|
132
|
-
|
|
133
|
-
// return {
|
|
134
|
-
// id: "",
|
|
135
|
-
// startDate: dateTimeFromString(day.startDate).toJSDate(),
|
|
136
|
-
// endDate: dateTimeFromString(day.endDate).toJSDate(),
|
|
137
|
-
// serviceBookingRequestId: bookingId,
|
|
138
|
-
// addOns: addOns,
|
|
139
|
-
// packages: [],
|
|
140
|
-
// } as ServiceBookingDayExt;
|
|
141
|
-
// });
|
|
142
|
-
// }
|
|
143
|
-
|
|
144
|
-
export function apiTransformBookedDays(
|
|
145
|
-
priceToBook: ServicePriceToBookApiResult
|
|
146
|
-
): Partial<ServiceBookingDayExt>[] {
|
|
147
|
-
return priceToBook.daysToBook.map((day): Partial<ServiceBookingDayExt> => {
|
|
148
|
-
// Transform addOns using the helper function, then map to the expected output.
|
|
149
|
-
const addOns = day.addOns.map(
|
|
150
|
-
(addOn): Partial<ServiceBookingAddOnExt> => ({
|
|
151
|
-
addOnId: addOn.id,
|
|
152
|
-
chosenQuantity: addOn.chosenQuantity ?? 0,
|
|
153
|
-
addOn: addOn,
|
|
154
|
-
// costATBCents: convertDollarsToCents(addOn.price),
|
|
155
|
-
})
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
startDate: day.dateTimeRange.start.toJSDate(),
|
|
160
|
-
endDate: day.dateTimeRange.end.toJSDate(),
|
|
161
|
-
addOns: addOns,
|
|
162
|
-
packages: [],
|
|
163
|
-
// costATBCents: convertDollarsToCents(day.totalBeforeTaxes),
|
|
164
|
-
} as Partial<ServiceBookingDayExt>;
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function serviceGetPriceToBookFromBooking(
|
|
169
|
-
service: ServiceExt,
|
|
170
|
-
booking: ServiceBookingExt
|
|
171
|
-
): ServiceGetPriceToBookResult {
|
|
172
|
-
const daysToBook = booking.bookedDays.map((day) => {
|
|
173
|
-
// Transform addOns back to the original format
|
|
174
|
-
const addOns =
|
|
175
|
-
day.addOns?.map(
|
|
176
|
-
(addOn): ServiceAddonInput => ({
|
|
177
|
-
...addOn.addOn,
|
|
178
|
-
id: addOn.addOnId!,
|
|
179
|
-
chosenQuantity: addOn.chosenQuantity,
|
|
180
|
-
// Reconstruct other addOn properties from the stored addOn object
|
|
181
|
-
})
|
|
182
|
-
) ?? [];
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
dateRange: dateTimeRangeFromDates(
|
|
186
|
-
day.startDate,
|
|
187
|
-
day.endDate,
|
|
188
|
-
"UTC",
|
|
189
|
-
booking.timezone
|
|
190
|
-
),
|
|
191
|
-
addOns: addOns,
|
|
192
|
-
// totalBeforeTaxes: day.costATBCents ? convertCentsToDollars(day.costATBCents) : 0,
|
|
193
|
-
} as ServiceGetPriceToBookExtDay;
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return serviceGetPriceToBookExt(service.serviceRatesAssociation, {
|
|
197
|
-
bookedDays: daysToBook,
|
|
198
|
-
selectedAttendeeOption: serviceAttendeeOptions[0],
|
|
199
|
-
timezone: booking.timezone,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Main function that transforms service booking parameters to price-to-book parameters
|
|
204
|
-
export function serviceBookingParamsToPriceToBookParams(
|
|
205
|
-
params: ServiceBookingApiParamsLuxon,
|
|
206
|
-
serviceAddons: ServiceAddonExt[]
|
|
207
|
-
): ServiceGetPriceToBookFeesParams {
|
|
208
|
-
// Map each booked day to the required parameters
|
|
209
|
-
const daysToBookParams: ServiceBookingDayInfoParams[] = params.bookedDays.map(
|
|
210
|
-
(day) => {
|
|
211
|
-
// Use the helper function to transform the day's addOns
|
|
212
|
-
const addOnsWithDetails = apiTransformAddOnsToAddonInputs(
|
|
213
|
-
day.addOns,
|
|
214
|
-
serviceAddons
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
dateTimeRange: day.dateTimeRange,
|
|
219
|
-
fees: addOnsWithDetails.map(
|
|
220
|
-
(addon): ServiceBookingFee => ({
|
|
221
|
-
name: `${addon.name} Fee`,
|
|
222
|
-
type: "addOnFees",
|
|
223
|
-
fee: (addon.chosenQuantity || 0) * (addon.price || 0),
|
|
224
|
-
})
|
|
225
|
-
),
|
|
226
|
-
} as ServiceBookingDayInfoParams;
|
|
227
|
-
}
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
daysToBookParams: daysToBookParams,
|
|
232
|
-
selectedAttendeeOption: {} as ServiceAttendeeOption, // Placeholder: update with actual business logic
|
|
233
|
-
timezone: params.timezone,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function serviceBookingParamsToLuxon(
|
|
238
|
-
params: ServiceBookingApiParams
|
|
239
|
-
): ServiceBookingApiParamsLuxon {
|
|
240
|
-
const bookedDays = params.bookedDays.reduce(
|
|
241
|
-
(sofar, day): ServiceBookedDayApiParamsLuxon[] => {
|
|
242
|
-
sofar.push({
|
|
243
|
-
...day,
|
|
244
|
-
dateTimeRange: {
|
|
245
|
-
start: dateTimeFromString(day.startDate),
|
|
246
|
-
end: dateTimeFromString(day.endDate),
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
return sofar;
|
|
251
|
-
},
|
|
252
|
-
[] as ServiceBookedDayApiParamsLuxon[]
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
return {
|
|
256
|
-
...params,
|
|
257
|
-
bookedDays,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
import { ServiceAddon } from "@prisma/client";
|
|
2
|
-
import { ServiceRatesAssociationExt } from "../../extendedSchemas";
|
|
3
|
-
import { ServiceAttendeeOption } from "./attendeeOptionUtils";
|
|
4
|
-
import {
|
|
5
|
-
getServiceRatePricingInfo,
|
|
6
|
-
serviceGetFilteredRates,
|
|
7
|
-
ServiceRatePricingInfo,
|
|
8
|
-
serviceRatesFilter,
|
|
9
|
-
serviceRatesHasRates,
|
|
10
|
-
ServiceRatesLuxon,
|
|
11
|
-
} from "./serviceRateUtils";
|
|
12
|
-
import {
|
|
13
|
-
dateTimeDiffHours,
|
|
14
|
-
dateTimeRangeHours,
|
|
15
|
-
LuxonDateRange,
|
|
16
|
-
} from "../luxonUtils";
|
|
17
|
-
|
|
18
|
-
export const SERVICE_BOOKING_PROCESSING_FEE_PERCENT = 0.15;
|
|
19
|
-
|
|
20
|
-
export interface ServiceAddonInput extends ServiceAddon {
|
|
21
|
-
chosenQuantity?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type ServiceBookingFeeType =
|
|
25
|
-
| "addOnFees"
|
|
26
|
-
| "cleaningFee"
|
|
27
|
-
| "processingFee";
|
|
28
|
-
|
|
29
|
-
export const ServiceBookingFeeType = {
|
|
30
|
-
addonFees: "addOnFees",
|
|
31
|
-
cleaningFee: "cleaningFee",
|
|
32
|
-
processingFee: "processingFee",
|
|
33
|
-
} as const;
|
|
34
|
-
|
|
35
|
-
export interface ServiceBookingFee {
|
|
36
|
-
name: string;
|
|
37
|
-
type: ServiceBookingFeeType;
|
|
38
|
-
fee: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ServiceBookingDayInfoParams {
|
|
42
|
-
dateTimeRange: LuxonDateRange;
|
|
43
|
-
fees: ServiceBookingFee[];
|
|
44
|
-
addOns: ServiceAddonInput[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface ServiceBookingDayInfo {
|
|
48
|
-
priceBreakdown: ServiceRatePricingInfo[];
|
|
49
|
-
discounts: ServiceRatePricingInfo[];
|
|
50
|
-
|
|
51
|
-
dateTimeRange: LuxonDateRange;
|
|
52
|
-
|
|
53
|
-
baseCostUndiscounted: number; //rate purely based on generalHourly
|
|
54
|
-
baseCostDiscounted: number;
|
|
55
|
-
baseCostDiscount: number;
|
|
56
|
-
|
|
57
|
-
fees: ServiceBookingFee[];
|
|
58
|
-
totalBeforeTaxes: number;
|
|
59
|
-
|
|
60
|
-
addOns: ServiceAddonInput[];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface ServiceGetPriceToBookResult {
|
|
64
|
-
serviceId: string;
|
|
65
|
-
|
|
66
|
-
daysToBook: ServiceBookingDayInfo[];
|
|
67
|
-
|
|
68
|
-
baseCostUndiscounted: number; //rate purely based on generalHourly
|
|
69
|
-
baseCostDiscounted: number;
|
|
70
|
-
baseCostDiscount: number;
|
|
71
|
-
|
|
72
|
-
additionalFees: ServiceBookingFee[];
|
|
73
|
-
totalBeforeTaxes: number;
|
|
74
|
-
|
|
75
|
-
minimumTimeBlockHours?: number; //only valid on generalRate
|
|
76
|
-
totalDurationHours: number;
|
|
77
|
-
durationHoursPerDay: number;
|
|
78
|
-
|
|
79
|
-
baseHourlyRate: number;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface ServiceGetBookingDayInfoParams {
|
|
83
|
-
filteredRates: ServiceRatesLuxon;
|
|
84
|
-
// overlappingBusinessHours: LuxonDateRange[] | null;
|
|
85
|
-
|
|
86
|
-
timezone: string | null | undefined;
|
|
87
|
-
daysToBookParams: ServiceBookingDayInfoParams[];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function serviceGetBookingDayInfo({
|
|
91
|
-
filteredRates,
|
|
92
|
-
daysToBookParams,
|
|
93
|
-
// overlappingBusinessHours,
|
|
94
|
-
timezone,
|
|
95
|
-
}: ServiceGetBookingDayInfoParams): ServiceBookingDayInfo[] {
|
|
96
|
-
const bookingDayResults: ServiceBookingDayInfo[] = daysToBookParams.map(
|
|
97
|
-
(day) => {
|
|
98
|
-
const rates = serviceRatesFilter(filteredRates, [day.dateTimeRange]);
|
|
99
|
-
const { generalRate, specialRates, dailyRates } = rates;
|
|
100
|
-
|
|
101
|
-
let priceBreakdown: ServiceRatePricingInfo[] = [];
|
|
102
|
-
const durationHours = dateTimeDiffHours(
|
|
103
|
-
day.dateTimeRange.start,
|
|
104
|
-
day.dateTimeRange.end
|
|
105
|
-
);
|
|
106
|
-
// console.log(
|
|
107
|
-
// `serviceGetBookingDayInfo: durationHours: ${JSON.stringify(
|
|
108
|
-
// durationHours
|
|
109
|
-
// )}, dtRange: ${JSON.stringify(day.dateTimeRange)}`
|
|
110
|
-
// );
|
|
111
|
-
let hoursRemaining = durationHours;
|
|
112
|
-
let currentRange = { ...day.dateTimeRange };
|
|
113
|
-
let baseCostDiscounted = 0;
|
|
114
|
-
while (serviceRatesHasRates(rates) && hoursRemaining > 0) {
|
|
115
|
-
const pricingInfo = getServiceRatePricingInfo(
|
|
116
|
-
currentRange,
|
|
117
|
-
generalRate,
|
|
118
|
-
dailyRates,
|
|
119
|
-
specialRates,
|
|
120
|
-
// overlappingBusinessHours,
|
|
121
|
-
timezone,
|
|
122
|
-
hoursRemaining
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
if (pricingInfo.hours == 0 || pricingInfo.unitCount == 0) {
|
|
126
|
-
throw new Error(`Invalid pricingInfo`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
priceBreakdown.push(pricingInfo);
|
|
130
|
-
|
|
131
|
-
// currentRange.start = currentRange.start.plus({
|
|
132
|
-
// hours: pricingInfo.hours,
|
|
133
|
-
// });
|
|
134
|
-
currentRange.start = pricingInfo.dateTimeRange.end;
|
|
135
|
-
hoursRemaining -= pricingInfo.hours;
|
|
136
|
-
baseCostDiscounted += pricingInfo.piTotal;
|
|
137
|
-
|
|
138
|
-
if (pricingInfo.rateType == "Special") {
|
|
139
|
-
specialRates.pop();
|
|
140
|
-
} else if (pricingInfo.rateType == "Weekday") {
|
|
141
|
-
dailyRates.pop();
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const baseHourlyRate = generalRate?.hourlyRate ?? 0;
|
|
146
|
-
|
|
147
|
-
const discounts = priceBreakdown
|
|
148
|
-
.filter(
|
|
149
|
-
(pi) => !(pi.pricingType == "Hourly" && pi.rateType == "General")
|
|
150
|
-
)
|
|
151
|
-
.map((pi) => {
|
|
152
|
-
return {
|
|
153
|
-
...pi,
|
|
154
|
-
piTotal: pi.hours * baseHourlyRate - pi.piTotal,
|
|
155
|
-
};
|
|
156
|
-
})
|
|
157
|
-
.filter((pi) => pi.piTotal > 0);
|
|
158
|
-
|
|
159
|
-
const baseCostUndiscounted = baseHourlyRate * durationHours;
|
|
160
|
-
const baseCostDiscount =
|
|
161
|
-
baseCostUndiscounted == 0
|
|
162
|
-
? 0
|
|
163
|
-
: 1 - baseCostDiscounted / baseCostUndiscounted;
|
|
164
|
-
|
|
165
|
-
const totalBeforeTaxes =
|
|
166
|
-
baseCostDiscounted +
|
|
167
|
-
day.fees.reduce((sofar, fee) => sofar + fee.fee, 0);
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
baseCostDiscount: baseCostDiscount,
|
|
171
|
-
baseCostDiscounted: baseCostDiscounted,
|
|
172
|
-
baseCostUndiscounted: baseCostUndiscounted,
|
|
173
|
-
discounts: discounts,
|
|
174
|
-
priceBreakdown: priceBreakdown,
|
|
175
|
-
dateTimeRange: day.dateTimeRange,
|
|
176
|
-
fees: day.fees,
|
|
177
|
-
addOns: day.addOns,
|
|
178
|
-
totalBeforeTaxes: totalBeforeTaxes,
|
|
179
|
-
} as ServiceBookingDayInfo;
|
|
180
|
-
}
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
return bookingDayResults;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export interface ServiceGetPriceToBookFeesParams {
|
|
187
|
-
daysToBookParams: ServiceBookingDayInfoParams[];
|
|
188
|
-
|
|
189
|
-
selectedAttendeeOption: ServiceAttendeeOption;
|
|
190
|
-
|
|
191
|
-
// overlappingBusinessHours: LuxonDateRange[] | null;
|
|
192
|
-
timezone: string | null | undefined;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function serviceGetPriceToBookFees(
|
|
196
|
-
serviceRatesAssociation: ServiceRatesAssociationExt | null | undefined,
|
|
197
|
-
{
|
|
198
|
-
daysToBookParams,
|
|
199
|
-
selectedAttendeeOption,
|
|
200
|
-
// overlappingBusinessHours,
|
|
201
|
-
timezone,
|
|
202
|
-
}: ServiceGetPriceToBookFeesParams
|
|
203
|
-
): ServiceGetPriceToBookResult {
|
|
204
|
-
const bookingDateTimeRanges = daysToBookParams.map(
|
|
205
|
-
(daysToBookParams) => daysToBookParams.dateTimeRange
|
|
206
|
-
);
|
|
207
|
-
const filteredRates = serviceGetFilteredRates(
|
|
208
|
-
serviceRatesAssociation,
|
|
209
|
-
bookingDateTimeRanges,
|
|
210
|
-
timezone
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
const bookingDayResults: ServiceBookingDayInfo[] = serviceGetBookingDayInfo({
|
|
214
|
-
filteredRates: filteredRates,
|
|
215
|
-
daysToBookParams: daysToBookParams,
|
|
216
|
-
// overlappingBusinessHours: overlappingBusinessHours,
|
|
217
|
-
timezone: timezone,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const durationHours = bookingDateTimeRanges.reduce((sofar, curr) => {
|
|
221
|
-
return sofar + dateTimeRangeHours(curr);
|
|
222
|
-
}, 0);
|
|
223
|
-
const bookingsTotalBeforeTaxes = bookingDayResults.reduce((sofar, curr) => {
|
|
224
|
-
return sofar + curr.totalBeforeTaxes;
|
|
225
|
-
}, 0);
|
|
226
|
-
const baseHourlyRate =
|
|
227
|
-
serviceRatesAssociation?.serviceGeneralRates?.hourlyRate ?? 0;
|
|
228
|
-
|
|
229
|
-
const baseCostUndiscounted = baseHourlyRate * durationHours;
|
|
230
|
-
const baseCostDiscount =
|
|
231
|
-
baseCostUndiscounted == 0
|
|
232
|
-
? 0
|
|
233
|
-
: 1 - bookingsTotalBeforeTaxes / baseCostUndiscounted;
|
|
234
|
-
|
|
235
|
-
const minimumTimeBlockHours =
|
|
236
|
-
serviceRatesAssociation?.serviceGeneralRates?.minimumTimeBlockHours;
|
|
237
|
-
|
|
238
|
-
const attendeeRate = selectedAttendeeOption.rate * durationHours;
|
|
239
|
-
|
|
240
|
-
const processingFee =
|
|
241
|
-
SERVICE_BOOKING_PROCESSING_FEE_PERCENT * bookingsTotalBeforeTaxes;
|
|
242
|
-
const additionalFees = (
|
|
243
|
-
[
|
|
244
|
-
{
|
|
245
|
-
name: "Processing Fee",
|
|
246
|
-
type: "processingFee",
|
|
247
|
-
fee: processingFee,
|
|
248
|
-
},
|
|
249
|
-
] as ServiceBookingFee[]
|
|
250
|
-
).filter((fee) => fee.fee > 0);
|
|
251
|
-
|
|
252
|
-
const totalBeforeTaxes =
|
|
253
|
-
bookingsTotalBeforeTaxes +
|
|
254
|
-
additionalFees.reduce((sofar, fee) => sofar + fee.fee, 0);
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
serviceId: serviceRatesAssociation?.serviceId,
|
|
258
|
-
daysToBook: bookingDayResults,
|
|
259
|
-
baseCostUndiscounted: baseCostUndiscounted,
|
|
260
|
-
baseCostDiscounted: bookingsTotalBeforeTaxes,
|
|
261
|
-
baseCostDiscount: baseCostDiscount,
|
|
262
|
-
totalBeforeTaxes: totalBeforeTaxes,
|
|
263
|
-
additionalFees: additionalFees,
|
|
264
|
-
baseHourlyRate: baseHourlyRate,
|
|
265
|
-
totalDurationHours: durationHours,
|
|
266
|
-
minimumTimeBlockHours: minimumTimeBlockHours,
|
|
267
|
-
durationHoursPerDay: dateTimeRangeHours(bookingDateTimeRanges[0]),
|
|
268
|
-
} as ServiceGetPriceToBookResult;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export interface ServiceGetPriceToBookParams {
|
|
272
|
-
addOns: ServiceAddonInput[];
|
|
273
|
-
|
|
274
|
-
selectedAttendeeOption: ServiceAttendeeOption;
|
|
275
|
-
|
|
276
|
-
bookingDateTimeRanges: LuxonDateRange[];
|
|
277
|
-
|
|
278
|
-
// overlappingBusinessHours: LuxonDateRange[] | null;
|
|
279
|
-
timezone: string | null | undefined;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function serviceGetPriceToBook(
|
|
283
|
-
serviceRatesAssociation: ServiceRatesAssociationExt | null | undefined,
|
|
284
|
-
{
|
|
285
|
-
addOns,
|
|
286
|
-
selectedAttendeeOption,
|
|
287
|
-
bookingDateTimeRanges, //list of dateTime ranges to book
|
|
288
|
-
// overlappingBusinessHours,
|
|
289
|
-
timezone,
|
|
290
|
-
}: ServiceGetPriceToBookParams
|
|
291
|
-
): ServiceGetPriceToBookResult {
|
|
292
|
-
const cleaningFee =
|
|
293
|
-
serviceRatesAssociation?.serviceGeneralRates?.cleaningFeePerBooking ?? 0;
|
|
294
|
-
|
|
295
|
-
const addOnTotal = addOns.reduce((sum, addOn) => {
|
|
296
|
-
return sum + (addOn.chosenQuantity || 0) * addOn.price;
|
|
297
|
-
}, 0);
|
|
298
|
-
|
|
299
|
-
const feesPerBooking = (
|
|
300
|
-
[
|
|
301
|
-
{
|
|
302
|
-
name: "AddOn Total",
|
|
303
|
-
type: "addOnFees",
|
|
304
|
-
fee: addOnTotal,
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
name: "Cleaning Fee",
|
|
308
|
-
type: "cleaningFee",
|
|
309
|
-
fee: cleaningFee,
|
|
310
|
-
},
|
|
311
|
-
] as ServiceBookingFee[]
|
|
312
|
-
).filter((fee) => fee.fee > 0);
|
|
313
|
-
|
|
314
|
-
const daysToBookParams = bookingDateTimeRanges.map(
|
|
315
|
-
(dtRange): ServiceBookingDayInfoParams => {
|
|
316
|
-
return {
|
|
317
|
-
dateTimeRange: dtRange,
|
|
318
|
-
fees: feesPerBooking,
|
|
319
|
-
addOns: addOns,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
return serviceGetPriceToBookFees(serviceRatesAssociation, {
|
|
325
|
-
daysToBookParams: daysToBookParams,
|
|
326
|
-
selectedAttendeeOption: selectedAttendeeOption,
|
|
327
|
-
timezone: timezone,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export interface ServiceGetPriceToBookExtDay {
|
|
332
|
-
dateRange: LuxonDateRange;
|
|
333
|
-
addOns: ServiceAddonInput[];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export interface ServiceGetPriceToBookExtParams {
|
|
337
|
-
selectedAttendeeOption: ServiceAttendeeOption;
|
|
338
|
-
|
|
339
|
-
bookedDays: ServiceGetPriceToBookExtDay[];
|
|
340
|
-
|
|
341
|
-
// overlappingBusinessHours: LuxonDateRange[] | null;
|
|
342
|
-
timezone: string | null | undefined;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export function serviceGetPriceToBookExt(
|
|
346
|
-
serviceRatesAssociation: ServiceRatesAssociationExt | null | undefined,
|
|
347
|
-
{
|
|
348
|
-
selectedAttendeeOption,
|
|
349
|
-
bookedDays, //list of dateTime ranges to book
|
|
350
|
-
// overlappingBusinessHours,
|
|
351
|
-
timezone,
|
|
352
|
-
}: ServiceGetPriceToBookExtParams
|
|
353
|
-
): ServiceGetPriceToBookResult {
|
|
354
|
-
const cleaningFee =
|
|
355
|
-
serviceRatesAssociation?.serviceGeneralRates?.cleaningFeePerBooking ?? 0;
|
|
356
|
-
|
|
357
|
-
const daysToBookParams = bookedDays.map(
|
|
358
|
-
(day): ServiceBookingDayInfoParams => {
|
|
359
|
-
const addOnTotal = day.addOns.reduce((sum, addOn) => {
|
|
360
|
-
return sum + (addOn.chosenQuantity || 0) * addOn.price;
|
|
361
|
-
}, 0);
|
|
362
|
-
|
|
363
|
-
const feesPerBooking = (
|
|
364
|
-
[
|
|
365
|
-
{
|
|
366
|
-
name: "AddOn Total",
|
|
367
|
-
type: "addOnFees",
|
|
368
|
-
fee: addOnTotal,
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
name: "Cleaning Fee",
|
|
372
|
-
type: "cleaningFee",
|
|
373
|
-
fee: cleaningFee,
|
|
374
|
-
},
|
|
375
|
-
] as ServiceBookingFee[]
|
|
376
|
-
).filter((fee) => fee.fee > 0);
|
|
377
|
-
|
|
378
|
-
return {
|
|
379
|
-
dateTimeRange: day.dateRange,
|
|
380
|
-
fees: feesPerBooking,
|
|
381
|
-
addOns: day.addOns,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
return serviceGetPriceToBookFees(serviceRatesAssociation, {
|
|
387
|
-
daysToBookParams: daysToBookParams,
|
|
388
|
-
selectedAttendeeOption: selectedAttendeeOption,
|
|
389
|
-
timezone: timezone,
|
|
390
|
-
});
|
|
391
|
-
}
|