@bash-app/bash-common 29.67.0 → 29.69.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 +3 -1
- package/prisma/schema.prisma +242 -80
- package/src/definitions.ts +228 -123
- package/src/extendedSchemas.ts +176 -105
- package/src/index.ts +11 -1
- package/src/utils/generalDateTimeUtils.ts +43 -0
- package/src/utils/luxonUtils.ts +906 -0
- package/src/utils/mathUtils.ts +3 -0
- package/src/utils/paymentUtils.ts +25 -10
- package/src/utils/service/attendeeOptionUtils.ts +19 -0
- package/src/utils/service/serviceBookingApiUtils.ts +259 -0
- package/src/utils/service/serviceBookingStatusUtils.ts +106 -0
- package/src/utils/service/serviceBookingUtils.ts +391 -0
- package/src/utils/service/serviceDBUtils.ts +51 -0
- package/src/utils/service/serviceRateDBUtils.ts +179 -0
- package/src/utils/service/serviceRateUtils.ts +350 -0
- package/src/utils/service/serviceUtils.ts +35 -0
- package/src/utils/stringUtils.ts +8 -0
- package/src/utils/typeUtils.ts +16 -0
- package/src/utils/urlUtils.ts +64 -5
- package/tsconfig.json +2 -2
|
@@ -1,33 +1,44 @@
|
|
|
1
|
-
import {BashEventPromoCode, TicketTier} from "@prisma/client";
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { BashEventPromoCode, TicketTier } from "@prisma/client";
|
|
2
|
+
import {
|
|
3
|
+
BASH_FEE_PERCENTAGE,
|
|
4
|
+
NumberOfTicketsForDate,
|
|
5
|
+
PRICE_DOLLARS_AND_CENTS_RATIO,
|
|
6
|
+
} from "../definitions";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Returns the amount of discount in dollars
|
|
7
10
|
* @param totalInDollars
|
|
8
11
|
* @param promoCode
|
|
9
12
|
*/
|
|
10
|
-
export function calculateDiscountFromPromoCode(
|
|
13
|
+
export function calculateDiscountFromPromoCode(
|
|
14
|
+
totalInDollars: number,
|
|
15
|
+
promoCode: BashEventPromoCode | undefined
|
|
16
|
+
): number {
|
|
11
17
|
if (promoCode) {
|
|
12
18
|
if (promoCode.discountAmountInCents) {
|
|
13
|
-
const discountInDollars = convertCentsToDollars(
|
|
19
|
+
const discountInDollars = convertCentsToDollars(
|
|
20
|
+
promoCode.discountAmountInCents
|
|
21
|
+
);
|
|
14
22
|
return Math.max(0, discountInDollars); // Ensure we don't discount more than 100%
|
|
15
23
|
}
|
|
16
24
|
if (promoCode.discountAmountPercentage) {
|
|
17
|
-
const discountInDollars =
|
|
25
|
+
const discountInDollars =
|
|
26
|
+
totalInDollars * (promoCode.discountAmountPercentage / 100);
|
|
18
27
|
return Math.min(totalInDollars, discountInDollars); // Ensure we don't discount more than 100%
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
return 0;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
|
|
25
33
|
/**
|
|
26
34
|
* Returns the total price based on a map where the keys are the ticketTierIds
|
|
27
35
|
* @param ticketTiers
|
|
28
36
|
* @param ticketList
|
|
29
37
|
*/
|
|
30
|
-
export function calculateTotalPriceWithoutTax(
|
|
38
|
+
export function calculateTotalPriceWithoutTax(
|
|
39
|
+
ticketTiers: TicketTier[],
|
|
40
|
+
ticketList: Map<string, NumberOfTicketsForDate[]>
|
|
41
|
+
): number {
|
|
31
42
|
let total = 0;
|
|
32
43
|
ticketTiers.forEach((tier: TicketTier) => {
|
|
33
44
|
const ticketListNumAndDate = ticketList.get(tier.id);
|
|
@@ -38,13 +49,17 @@ export function calculateTotalPriceWithoutTax(ticketTiers: TicketTier[], ticketL
|
|
|
38
49
|
return total;
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
export function convertCentsToDollars(
|
|
52
|
+
export function convertCentsToDollars(
|
|
53
|
+
price: number | undefined | null
|
|
54
|
+
): number {
|
|
42
55
|
if (!price) {
|
|
43
56
|
return 0;
|
|
44
57
|
}
|
|
45
58
|
return price / PRICE_DOLLARS_AND_CENTS_RATIO;
|
|
46
59
|
}
|
|
47
|
-
export function convertDollarsToCents(
|
|
60
|
+
export function convertDollarsToCents(
|
|
61
|
+
price: number | undefined | null
|
|
62
|
+
): number {
|
|
48
63
|
if (!price) {
|
|
49
64
|
return 0;
|
|
50
65
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ServiceAttendeeOptionRange {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ServiceAttendeeOption {
|
|
7
|
+
label: string;
|
|
8
|
+
rate: number;
|
|
9
|
+
personRange: ServiceAttendeeOptionRange;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const serviceAttendeeOptions = [
|
|
13
|
+
{ label: "1 - 5 people", personRange: { start: 1, end: 5 }, rate: 0 },
|
|
14
|
+
{ label: "6 - 15 people", personRange: { start: 6, end: 15 }, rate: 50 },
|
|
15
|
+
{ label: "16 - 30 people", personRange: { start: 16, end: 30 }, rate: 100 },
|
|
16
|
+
{ label: "31 - 45 people", personRange: { start: 31, end: 45 }, rate: 200 },
|
|
17
|
+
{ label: "46 - 60 people", personRange: { start: 46, end: 60 }, rate: 400 },
|
|
18
|
+
{ label: "61 - 75 people", personRange: { start: 61, end: 75 }, rate: 500 },
|
|
19
|
+
] as ServiceAttendeeOption[];
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ServiceBookingExt, ServiceExt } from "../../extendedSchemas";
|
|
2
|
+
|
|
3
|
+
export interface ValidationResult {
|
|
4
|
+
valid: boolean;
|
|
5
|
+
errorMessage?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function serviceBookingIsValid(
|
|
9
|
+
service: ServiceExt,
|
|
10
|
+
booking: ServiceBookingExt | null | undefined
|
|
11
|
+
) {
|
|
12
|
+
if (booking == null) {
|
|
13
|
+
return {
|
|
14
|
+
valid: false,
|
|
15
|
+
errorMessage: "This booking has already been canceled.",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function serviceBookingCanBePaid(
|
|
21
|
+
service: ServiceExt,
|
|
22
|
+
booking: ServiceBookingExt
|
|
23
|
+
): ValidationResult {
|
|
24
|
+
if (booking.status === "Canceled") {
|
|
25
|
+
return {
|
|
26
|
+
valid: false,
|
|
27
|
+
errorMessage: "This booking has already been canceled.",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (booking.status === "Rejected") {
|
|
31
|
+
return {
|
|
32
|
+
valid: false,
|
|
33
|
+
errorMessage: "You cannot pay for a rejected booking request.",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (booking.status === "Pending" && booking.bookingType === "Request") {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
errorMessage: "You cannot pay for a pending booking request.",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { valid: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function serviceBookingCanHaveApprovalDecision(
|
|
46
|
+
service: ServiceExt,
|
|
47
|
+
booking: ServiceBookingExt
|
|
48
|
+
): ValidationResult {
|
|
49
|
+
if (booking.bookingType !== "Request") {
|
|
50
|
+
return {
|
|
51
|
+
valid: false,
|
|
52
|
+
errorMessage: "Approval decisions can only be made on booking requests.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (booking.status !== "Pending") {
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
errorMessage:
|
|
59
|
+
"Approval decisions can only be made for pending booking requests.",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { valid: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function serviceBookingHasDecision(
|
|
67
|
+
service: ServiceExt,
|
|
68
|
+
booking: ServiceBookingExt
|
|
69
|
+
): ValidationResult {
|
|
70
|
+
if (booking.bookingType !== "Request") {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
errorMessage: "Only booking requests can have a decision.",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (booking.status !== "Approved" && booking.status !== "Rejected") {
|
|
77
|
+
return {
|
|
78
|
+
valid: false,
|
|
79
|
+
errorMessage: "The booking request does not have a decision yet.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { valid: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function serviceBookingCanBeCanceled(
|
|
86
|
+
service: ServiceExt,
|
|
87
|
+
booking: ServiceBookingExt
|
|
88
|
+
): ValidationResult {
|
|
89
|
+
if (booking.status !== "Confirmed") {
|
|
90
|
+
return {
|
|
91
|
+
valid: false,
|
|
92
|
+
errorMessage: "Only confirmed bookings can be canceled.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { valid: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function serviceBookingCanBeConfirmed(
|
|
99
|
+
service: ServiceExt,
|
|
100
|
+
booking: ServiceBookingExt
|
|
101
|
+
): ValidationResult {
|
|
102
|
+
if (booking.status !== "Canceled") {
|
|
103
|
+
return { valid: false, errorMessage: "The booking is not canceled." };
|
|
104
|
+
}
|
|
105
|
+
return { valid: true };
|
|
106
|
+
}
|