@bash-app/bash-common 29.68.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 +1 -1
- package/prisma/schema.prisma +45 -25
- package/src/definitions.ts +100 -52
- package/src/extendedSchemas.ts +17 -1
- package/src/index.ts +2 -0
- package/src/utils/luxonUtils.ts +21 -0
- package/src/utils/paymentUtils.ts +25 -10
- package/src/utils/service/serviceBookingApiUtils.ts +168 -25
- package/src/utils/service/serviceBookingStatusUtils.ts +106 -0
- package/src/utils/service/serviceBookingUtils.ts +86 -16
- package/src/utils/stringUtils.ts +8 -0
- package/src/utils/urlUtils.ts +64 -5
package/package.json
CHANGED
package/prisma/schema.prisma
CHANGED
|
@@ -140,25 +140,30 @@ model BashEventPromoCode {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
model BashNotification {
|
|
143
|
-
id
|
|
144
|
-
creatorId
|
|
145
|
-
creator
|
|
146
|
-
email
|
|
147
|
-
createdDateTime
|
|
148
|
-
message
|
|
149
|
-
image
|
|
150
|
-
readDateTime
|
|
151
|
-
taskId
|
|
152
|
-
eventTask
|
|
153
|
-
invitationId
|
|
154
|
-
invitation
|
|
155
|
-
bashEventId
|
|
156
|
-
bashEvent
|
|
157
|
-
|
|
158
|
-
|
|
143
|
+
id String @id @default(cuid())
|
|
144
|
+
creatorId String
|
|
145
|
+
creator User @relation("NotificationsCreatedByMe", fields: [creatorId], references: [id], onDelete: Cascade)
|
|
146
|
+
email String
|
|
147
|
+
createdDateTime DateTime @default(now())
|
|
148
|
+
message String
|
|
149
|
+
image String?
|
|
150
|
+
readDateTime DateTime?
|
|
151
|
+
taskId String?
|
|
152
|
+
eventTask EventTask? @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
|
153
|
+
invitationId String?
|
|
154
|
+
invitation Invitation? @relation(fields: [invitationId], references: [id], onDelete: Cascade)
|
|
155
|
+
bashEventId String?
|
|
156
|
+
bashEvent BashEvent? @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
|
|
157
|
+
serviceId String?
|
|
158
|
+
service Service? @relation(fields: [serviceId], references: [id], onDelete: Cascade)
|
|
159
|
+
serviceBookingId String?
|
|
160
|
+
serviceBooking ServiceBooking? @relation(fields: [serviceBookingId], references: [id], onDelete: Cascade)
|
|
161
|
+
reminders Reminder[]
|
|
162
|
+
redirectUrl String?
|
|
159
163
|
|
|
160
164
|
@@unique([creatorId, email, bashEventId])
|
|
161
165
|
@@unique([creatorId, email, invitationId])
|
|
166
|
+
@@unique([creatorId, email, serviceBookingId])
|
|
162
167
|
@@index([creatorId])
|
|
163
168
|
}
|
|
164
169
|
|
|
@@ -628,6 +633,7 @@ enum BashStatus {
|
|
|
628
633
|
PreSale
|
|
629
634
|
Published
|
|
630
635
|
Finished
|
|
636
|
+
// should we add Approved, Rejected, Confirmed, and Canceled like services has?
|
|
631
637
|
}
|
|
632
638
|
|
|
633
639
|
enum ServiceStatus {
|
|
@@ -1158,6 +1164,7 @@ model Service {
|
|
|
1158
1164
|
bashEvent BashEvent[] // because a service needs to be associated with a bash to post to the bashfeed
|
|
1159
1165
|
userPromoCodeRedemption UserPromoCodeRedemption[]
|
|
1160
1166
|
bookings ServiceBooking[]
|
|
1167
|
+
bashNotification BashNotification[]
|
|
1161
1168
|
}
|
|
1162
1169
|
|
|
1163
1170
|
model StripeAccount {
|
|
@@ -1632,6 +1639,8 @@ model ServiceBookingAddOn {
|
|
|
1632
1639
|
addOnId String
|
|
1633
1640
|
addOn ServiceAddon @relation(fields: [addOnId], references: [id])
|
|
1634
1641
|
chosenQuantity Int
|
|
1642
|
+
|
|
1643
|
+
// costATBCents Int
|
|
1635
1644
|
}
|
|
1636
1645
|
|
|
1637
1646
|
model ServiceBookingPackage {
|
|
@@ -1643,6 +1652,8 @@ model ServiceBookingPackage {
|
|
|
1643
1652
|
packageId String
|
|
1644
1653
|
package ServicePackage @relation(fields: [packageId], references: [id])
|
|
1645
1654
|
chosenQuantity Int
|
|
1655
|
+
|
|
1656
|
+
// costATBCents Int
|
|
1646
1657
|
}
|
|
1647
1658
|
|
|
1648
1659
|
model ServiceBookingDay {
|
|
@@ -1656,6 +1667,8 @@ model ServiceBookingDay {
|
|
|
1656
1667
|
|
|
1657
1668
|
addOns ServiceBookingAddOn[]
|
|
1658
1669
|
packages ServiceBookingPackage[]
|
|
1670
|
+
|
|
1671
|
+
// costATBCents Int
|
|
1659
1672
|
}
|
|
1660
1673
|
|
|
1661
1674
|
model ServiceBooking {
|
|
@@ -1675,17 +1688,20 @@ model ServiceBooking {
|
|
|
1675
1688
|
status ServiceBookingStatus @default(Pending)
|
|
1676
1689
|
bookingType ServiceBookingType @default(Request)
|
|
1677
1690
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1691
|
+
timezone String //stored here instead of service incase different service types operate in multiple timezones (requesters timezone would matter)
|
|
1692
|
+
|
|
1693
|
+
requestedOn DateTime?
|
|
1694
|
+
requestDecisionOn DateTime?
|
|
1695
|
+
bookedOn DateTime?
|
|
1696
|
+
canceledOn DateTime?
|
|
1682
1697
|
|
|
1683
1698
|
isFreeGuest Boolean @default(false)
|
|
1684
1699
|
allowPromiseToPay Boolean @default(false)
|
|
1685
1700
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1701
|
+
checkout ServiceBookingCheckout?
|
|
1702
|
+
bashNotification BashNotification[]
|
|
1703
|
+
|
|
1704
|
+
// costATBCents Int
|
|
1689
1705
|
|
|
1690
1706
|
@@index([status])
|
|
1691
1707
|
@@index([creatorId])
|
|
@@ -1703,9 +1719,13 @@ model ServiceBookingCheckout {
|
|
|
1703
1719
|
|
|
1704
1720
|
checkoutDateTime DateTime? @default(now())
|
|
1705
1721
|
stripeCheckoutSessionId String? @unique
|
|
1722
|
+
stripePaymentIntentId String? @unique
|
|
1723
|
+
|
|
1724
|
+
paidOn DateTime?
|
|
1725
|
+
refundedOn DateTime?
|
|
1706
1726
|
|
|
1707
|
-
totalAmount Int //stored in cents
|
|
1708
|
-
depositAmount Int
|
|
1727
|
+
totalAmount Int? //stored in cents
|
|
1728
|
+
depositAmount Int?
|
|
1709
1729
|
|
|
1710
1730
|
@@index(creatorId)
|
|
1711
1731
|
}
|
package/src/definitions.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
PublicUser,
|
|
22
22
|
VolunteerServiceExt,
|
|
23
23
|
BashEventExt,
|
|
24
|
+
ServiceBookingExt,
|
|
24
25
|
} from "./extendedSchemas";
|
|
25
26
|
import { ServiceSubscriptionTier } from "./utils/userSubscriptionUtils";
|
|
26
27
|
import {
|
|
@@ -29,59 +30,13 @@ import {
|
|
|
29
30
|
} from "./utils/service/serviceBookingUtils";
|
|
30
31
|
import { LuxonDateRange } from "./utils/luxonUtils";
|
|
31
32
|
import { ServiceCantBookReason } from "./utils/service/serviceBookingApiUtils";
|
|
33
|
+
import { urlAppendQueryParam } from "./utils/urlUtils";
|
|
34
|
+
import { ServiceAttendeeOption } from "./utils/service/attendeeOptionUtils";
|
|
32
35
|
|
|
33
36
|
export const PASSWORD_MIN_LENGTH = 8 as const;
|
|
34
37
|
export const PASSWORD_REQUIREMENTS_REGEX = new RegExp(
|
|
35
38
|
String.raw`^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&^#])[A-Za-z\d@$!%*?&^#]{${PASSWORD_MIN_LENGTH},}$`
|
|
36
39
|
);
|
|
37
|
-
export const BASH_FEE_PERCENTAGE = 0.1;
|
|
38
|
-
export const GOOGLE_CALLBACK_URL = "/auth/google/callback" as const;
|
|
39
|
-
export const CHECKOUT_RETURN_SUCCESS_URL =
|
|
40
|
-
`/checkout-return/success/{CHECKOUT_SESSION_ID}` as const;
|
|
41
|
-
export const CHECKOUT_RETURN_CANCEL_URL =
|
|
42
|
-
`/checkout-return/cancel/{CHECKOUT_SESSION_ID}` as const;
|
|
43
|
-
export const CHECKOUT_RETURN_SUCCESS_URL_PAGE =
|
|
44
|
-
"/checkout-return/success/$checkoutSessionId" as const;
|
|
45
|
-
export const CHECKOUT_RETURN_CANCEL_URL_PAGE =
|
|
46
|
-
"/checkout-return/cancel/$checkoutSessionId" as const;
|
|
47
|
-
export const DONATION_CHECKOUT_RETURN_SUCCESS_URL =
|
|
48
|
-
`/donation-checkout-return/success/{CHECKOUT_SESSION_ID}` as const;
|
|
49
|
-
export const DONATION_CHECKOUT_RETURN_CANCEL_URL =
|
|
50
|
-
`/donation-checkout-return/cancel/{CHECKOUT_SESSION_ID}` as const;
|
|
51
|
-
export const DONATION_CHECKOUT_RETURN_SUCCESS_URL_PAGE =
|
|
52
|
-
"/donation-checkout-return/success/$checkoutSessionId" as const;
|
|
53
|
-
export const DONATION_CHECKOUT_RETURN_CANCEL_URL_PAGE =
|
|
54
|
-
"/donation-checkout-return/cancel/$checkoutSessionId" as const;
|
|
55
|
-
export const VERIFICATION_RETURN_URL = `/sign-up` as const;
|
|
56
|
-
export const MY_SERVICES_URL = "/my-services" as const;
|
|
57
|
-
export const BASH_DETAIL_URL = `/bash-detail` as const;
|
|
58
|
-
export const SERVICE_PAGE_URL = `/service-page` as const;
|
|
59
|
-
export const LOGIN_URL = `/login` as const;
|
|
60
|
-
export const TICKET_DETAILS = `/ticket-details` as const;
|
|
61
|
-
|
|
62
|
-
export const SWR_KEY_AUTH_TOKEN = "auth-token" as const;
|
|
63
|
-
|
|
64
|
-
export const PRICE_DOLLARS_AND_CENTS_RATIO = 100 as const;
|
|
65
|
-
|
|
66
|
-
export const MIN_AMOUNT_OF_TICKETS_FOR_PUBLIC_EVENT_TO_SHOW = 0 as const;
|
|
67
|
-
export const DEFAULT_MAX_NUMBER_OF_TICKETS = 35 as const;
|
|
68
|
-
export const MIN_NUMBER_OF_TICKETS = 0 as const;
|
|
69
|
-
|
|
70
|
-
export const MAX_NUMBER_OF_FREE_TICKETS_PER_USER_FOR_A_BASH_EVENT =
|
|
71
|
-
100 as const;
|
|
72
|
-
export const MAX_NUMBER_OF_TICKETS_PER_REQUEST_FOR_A_BASH_EVENT = 50 as const;
|
|
73
|
-
|
|
74
|
-
export const MONTHS_PREVIOUS_THAT_STRIPE_ACCOUNTS_WILL_BE_SEARCHED_BY_EMAIL =
|
|
75
|
-
1 as const;
|
|
76
|
-
|
|
77
|
-
export const HTTP_CODE_OK = 200 as const;
|
|
78
|
-
export const HTTP_CODE_TEMPORARY_REDIRECT = 307 as const;
|
|
79
|
-
export const HTTP_CODE_INTERNAL_SERVER_ERR = 500 as const;
|
|
80
|
-
export const HTTP_CODE_BAD_REQUEST = 400 as const;
|
|
81
|
-
export const HTTP_CODE_UNAUTHORIZED = 401 as const;
|
|
82
|
-
export const HTTP_CODE_NOT_FOUND = 404 as const;
|
|
83
|
-
export const ERR_UNAUTHORIZED_REQUEST =
|
|
84
|
-
"Unauthorized to perform requested action. Have you logged in?" as const;
|
|
85
40
|
|
|
86
41
|
export const URL_PARAMS_BASH_EVENT_ID = "bashEventId" as const;
|
|
87
42
|
export const URL_PARAMS_BASH_EVENT_TITLE = "bashEventTitle" as const;
|
|
@@ -138,6 +93,85 @@ export const URL_PARAMS_TICKETS_DATE_DELIM = ";;" as const;
|
|
|
138
93
|
export const URL_INCLUDE_QUERY_PARAM_DELIM = "," as const;
|
|
139
94
|
export const URL_INCLUDE_PRISMA_DATA_KEYS_DELIM = "." as const;
|
|
140
95
|
|
|
96
|
+
export const BASH_FEE_PERCENTAGE = 0.1;
|
|
97
|
+
export const GOOGLE_CALLBACK_URL = "/auth/google/callback" as const;
|
|
98
|
+
export const CHECKOUT_RETURN_SUCCESS_URL =
|
|
99
|
+
`/checkout-return/success/{CHECKOUT_SESSION_ID}` as const;
|
|
100
|
+
export const CHECKOUT_RETURN_CANCEL_URL =
|
|
101
|
+
`/checkout-return/cancel/{CHECKOUT_SESSION_ID}` as const;
|
|
102
|
+
export const CHECKOUT_RETURN_SUCCESS_URL_PAGE =
|
|
103
|
+
"/checkout-return/success/$checkoutSessionId" as const;
|
|
104
|
+
export const CHECKOUT_RETURN_CANCEL_URL_PAGE =
|
|
105
|
+
"/checkout-return/cancel/$checkoutSessionId" as const;
|
|
106
|
+
export const DONATION_CHECKOUT_RETURN_SUCCESS_URL =
|
|
107
|
+
`/donation-checkout-return/success/{CHECKOUT_SESSION_ID}` as const;
|
|
108
|
+
export const DONATION_CHECKOUT_RETURN_CANCEL_URL =
|
|
109
|
+
`/donation-checkout-return/cancel/{CHECKOUT_SESSION_ID}` as const;
|
|
110
|
+
export const DONATION_CHECKOUT_RETURN_SUCCESS_URL_PAGE =
|
|
111
|
+
"/donation-checkout-return/success/$checkoutSessionId" as const;
|
|
112
|
+
export const DONATION_CHECKOUT_RETURN_CANCEL_URL_PAGE =
|
|
113
|
+
"/donation-checkout-return/cancel/$checkoutSessionId" as const;
|
|
114
|
+
|
|
115
|
+
// export const SERVICE_BOOKING_CHECKOUT_RETURN_SUCCESS_URL = urlAppendQueryParam(
|
|
116
|
+
// `/service/{SERVICE_ID}/booking/{BOOKING_ID}/checkout-return/{CHECKOUT_SESSION_ID}`,
|
|
117
|
+
// [
|
|
118
|
+
// {
|
|
119
|
+
// key: URL_PARAMS_STRIPE_CHECKOUT,
|
|
120
|
+
// value: URL_PARAMS_STRIPE_CHECKOUT_OPTIONS.complete,
|
|
121
|
+
// },
|
|
122
|
+
// ]
|
|
123
|
+
// );
|
|
124
|
+
// export const SERVICE_BOOKING_CHECKOUT_RETURN_CANCEL_URL = urlAppendQueryParam(
|
|
125
|
+
// `/service/{SERVICE_ID}/booking/{BOOKING_ID}/checkout-return/{CHECKOUT_SESSION_ID}`,
|
|
126
|
+
// [
|
|
127
|
+
// {
|
|
128
|
+
// key: URL_PARAMS_STRIPE_CHECKOUT,
|
|
129
|
+
// value: URL_PARAMS_STRIPE_CHECKOUT_OPTIONS.incomplete,
|
|
130
|
+
// },
|
|
131
|
+
// ]
|
|
132
|
+
// );
|
|
133
|
+
|
|
134
|
+
export const SERVICE_BOOKING_CHECKOUT_RETURN_SUCCESS_URL =
|
|
135
|
+
"/service/${SERVICE_ID}/booking/${BOOKING_ID}/checkout-return/success/{CHECKOUT_SESSION_ID}" as const; //CHECKOUT_SESSION_ID filled by stripe
|
|
136
|
+
export const SERVICE_BOOKING_CHECKOUT_RETURN_CANCEL_URL =
|
|
137
|
+
"/service/${SERVICE_ID}/booking/${BOOKING_ID}/checkout-return/cancel/{CHECKOUT_SESSION_ID}" as const;
|
|
138
|
+
|
|
139
|
+
export const SERVICE_BOOKING_CHECKOUT_RETURN_SUCCESS_URL_PAGE =
|
|
140
|
+
"/service/:serviceId/booking/:bookingId/checkout-return/success/:checkoutSessionId" as const;
|
|
141
|
+
export const SERVICE_BOOKING_CHECKOUT_RETURN_CANCEL_URL_PAGE =
|
|
142
|
+
"/service/:serviceId/booking/:bookingId/checkout-return/cancel/:checkoutSessionId" as const;
|
|
143
|
+
|
|
144
|
+
export const VERIFICATION_RETURN_URL = `/sign-up` as const;
|
|
145
|
+
export const MY_SERVICES_URL = "/my-services" as const;
|
|
146
|
+
export const BASH_DETAIL_URL = `/bash-detail` as const;
|
|
147
|
+
export const SERVICE_PAGE_URL = `/service-page` as const;
|
|
148
|
+
export const LOGIN_URL = `/login` as const;
|
|
149
|
+
export const TICKET_DETAILS = `/ticket-details` as const;
|
|
150
|
+
|
|
151
|
+
export const SWR_KEY_AUTH_TOKEN = "auth-token" as const;
|
|
152
|
+
|
|
153
|
+
export const PRICE_DOLLARS_AND_CENTS_RATIO = 100 as const;
|
|
154
|
+
|
|
155
|
+
export const MIN_AMOUNT_OF_TICKETS_FOR_PUBLIC_EVENT_TO_SHOW = 0 as const;
|
|
156
|
+
export const DEFAULT_MAX_NUMBER_OF_TICKETS = 35 as const;
|
|
157
|
+
export const MIN_NUMBER_OF_TICKETS = 0 as const;
|
|
158
|
+
|
|
159
|
+
export const MAX_NUMBER_OF_FREE_TICKETS_PER_USER_FOR_A_BASH_EVENT =
|
|
160
|
+
100 as const;
|
|
161
|
+
export const MAX_NUMBER_OF_TICKETS_PER_REQUEST_FOR_A_BASH_EVENT = 50 as const;
|
|
162
|
+
|
|
163
|
+
export const MONTHS_PREVIOUS_THAT_STRIPE_ACCOUNTS_WILL_BE_SEARCHED_BY_EMAIL =
|
|
164
|
+
1 as const;
|
|
165
|
+
|
|
166
|
+
export const HTTP_CODE_OK = 200 as const;
|
|
167
|
+
export const HTTP_CODE_TEMPORARY_REDIRECT = 307 as const;
|
|
168
|
+
export const HTTP_CODE_INTERNAL_SERVER_ERR = 500 as const;
|
|
169
|
+
export const HTTP_CODE_BAD_REQUEST = 400 as const;
|
|
170
|
+
export const HTTP_CODE_UNAUTHORIZED = 401 as const;
|
|
171
|
+
export const HTTP_CODE_NOT_FOUND = 404 as const;
|
|
172
|
+
export const ERR_UNAUTHORIZED_REQUEST =
|
|
173
|
+
"Unauthorized to perform requested action. Have you logged in?" as const;
|
|
174
|
+
|
|
141
175
|
export const DEFAULT_PRISMA_TTL_SECONDS = 60 as const;
|
|
142
176
|
export const PRISMA_MEDIA_TTL_SECONDS = 60 * 5; // 5 hours
|
|
143
177
|
export const PRISMA_USER_TTL_SECONDS = 60 * 7; // 7 hours
|
|
@@ -215,7 +249,7 @@ export type ServiceAddonApiParams = {
|
|
|
215
249
|
|
|
216
250
|
export type ServiceBookedDayApiParams = {
|
|
217
251
|
serviceId: string;
|
|
218
|
-
forUserId:
|
|
252
|
+
forUserId: string;
|
|
219
253
|
|
|
220
254
|
startDate: string;
|
|
221
255
|
endDate: string;
|
|
@@ -243,6 +277,7 @@ export type ServiceBookingApiParams = {
|
|
|
243
277
|
serviceId: string;
|
|
244
278
|
bookedDays: ServiceBookedDayApiParams[];
|
|
245
279
|
timezone: string;
|
|
280
|
+
attendeeOption: ServiceAttendeeOption;
|
|
246
281
|
};
|
|
247
282
|
|
|
248
283
|
export type ServiceCanBookApiParams = {} & ServiceBookingApiParams;
|
|
@@ -267,6 +302,12 @@ export type ServiceCanBookApiResult = {
|
|
|
267
302
|
reason?: ServiceCantBookReason;
|
|
268
303
|
};
|
|
269
304
|
|
|
305
|
+
export type ServiceBookingApiResult = {
|
|
306
|
+
canBook: ServiceCanBookApiResult;
|
|
307
|
+
priceToBook: ServiceGetPriceToBookResult;
|
|
308
|
+
booking?: ServiceBookingExt;
|
|
309
|
+
};
|
|
310
|
+
|
|
270
311
|
export interface DeletedAndHiddenTiers {
|
|
271
312
|
deletedTiers: TicketTier[];
|
|
272
313
|
hiddenTiers: TicketTier[];
|
|
@@ -379,6 +420,7 @@ export enum ApiErrorType {
|
|
|
379
420
|
UserExceededMaxTicketNumberForOneRequest,
|
|
380
421
|
UserDoesNotExist,
|
|
381
422
|
StripeCreateCheckoutSessionFailed,
|
|
423
|
+
StripeCreateRefundFailed,
|
|
382
424
|
StripeUserInfoIncomplete,
|
|
383
425
|
TicketsAlreadyPurchasedUsingThisCheckoutSession,
|
|
384
426
|
StripeAccountHasNotSetupTaxData,
|
|
@@ -443,9 +485,15 @@ export interface StripeCreateBashEventDonationCheckoutSessionArgs {
|
|
|
443
485
|
}
|
|
444
486
|
|
|
445
487
|
export interface StripeCreateSetupPaymentMethodSessionArgs {
|
|
446
|
-
currency: string;
|
|
447
|
-
successUrl: string;
|
|
448
|
-
cancelUrl: string;
|
|
488
|
+
// currency: string;
|
|
489
|
+
// successUrl: string;
|
|
490
|
+
// cancelUrl: string;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export interface StripeCreatePayForBookingSessionArgs {
|
|
494
|
+
// currency: string;
|
|
495
|
+
// successUrl: string;
|
|
496
|
+
// cancelUrl: string;
|
|
449
497
|
}
|
|
450
498
|
|
|
451
499
|
export interface StripeSetDefaultPaymentMethodArgs {
|
package/src/extendedSchemas.ts
CHANGED
|
@@ -274,6 +274,8 @@ export interface ServiceBookingExt extends ServiceBooking {
|
|
|
274
274
|
forUser: PublicUser;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
export type ServiceBookingPublicExt = Omit<ServiceBookingExt, "checkout">;
|
|
278
|
+
|
|
277
279
|
export interface ServiceBookingCheckoutExt extends ServiceBookingCheckout {
|
|
278
280
|
// service: ServiceExt; //we don't need service here
|
|
279
281
|
creator: PublicUser;
|
|
@@ -315,6 +317,8 @@ export const FRONT_END_SERVICE_BOOKING_CHECKOUT_DATA_SELECT = {
|
|
|
315
317
|
checkoutDateTime: true,
|
|
316
318
|
totalAmount: true,
|
|
317
319
|
depositAmount: true,
|
|
320
|
+
paidOn: true,
|
|
321
|
+
refundedOn: true,
|
|
318
322
|
} satisfies Prisma.ServiceBookingCheckoutSelect;
|
|
319
323
|
|
|
320
324
|
export const SERVICE_BOOKING_PUBLIC_DATA_TO_INCLUDE = {
|
|
@@ -326,7 +330,8 @@ export const SERVICE_BOOKING_PUBLIC_DATA_TO_INCLUDE = {
|
|
|
326
330
|
export const SERVICE_BOOKING_PRIVATE_DATA_TO_INCLUDE = {
|
|
327
331
|
...SERVICE_BOOKING_PUBLIC_DATA_TO_INCLUDE,
|
|
328
332
|
checkout: {
|
|
329
|
-
select: FRONT_END_SERVICE_BOOKING_CHECKOUT_DATA_SELECT,
|
|
333
|
+
// select: FRONT_END_SERVICE_BOOKING_CHECKOUT_DATA_SELECT,
|
|
334
|
+
include: SERVICE_BOOKING_CHECKOUT_DATA_TO_INCLUDE,
|
|
330
335
|
},
|
|
331
336
|
creator: {
|
|
332
337
|
select: FRONT_END_USER_DATA_TO_SELECT,
|
|
@@ -341,6 +346,9 @@ export interface ServiceExt extends Service {
|
|
|
341
346
|
|
|
342
347
|
stripeAccount?: PublicStripeAccount | null;
|
|
343
348
|
|
|
349
|
+
latitude?: number;
|
|
350
|
+
longitude?: number;
|
|
351
|
+
|
|
344
352
|
// availableDateTimes?: Availability[];
|
|
345
353
|
|
|
346
354
|
// rates?: Rate[];
|
|
@@ -510,6 +518,8 @@ export const VENUE_DATA_TO_REMOVE: RemoveCommonProperties<
|
|
|
510
518
|
export interface BashNotificationExt extends BashNotification {
|
|
511
519
|
creator?: PublicUser;
|
|
512
520
|
bashEvent?: BashEvent;
|
|
521
|
+
service?: ServiceExt;
|
|
522
|
+
serviceBooking?: ServiceBookingExt;
|
|
513
523
|
eventTask?: EventTask;
|
|
514
524
|
invitation?: Invitation;
|
|
515
525
|
reminders?: Reminder[];
|
|
@@ -544,6 +554,12 @@ export const BASH_NOTIFICATION_DATA_TO_INCLUDE = {
|
|
|
544
554
|
image: true,
|
|
545
555
|
},
|
|
546
556
|
},
|
|
557
|
+
service: {
|
|
558
|
+
include: { ...SERVICE_DATA_TO_INCLUDE, bashEvent: undefined },
|
|
559
|
+
},
|
|
560
|
+
serviceBooking: {
|
|
561
|
+
include: SERVICE_BOOKING_PUBLIC_DATA_TO_INCLUDE,
|
|
562
|
+
},
|
|
547
563
|
} satisfies Prisma.BashNotificationInclude;
|
|
548
564
|
|
|
549
565
|
export interface EventTaskExt extends EventTask {
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./utils/qrCodeUtils";
|
|
|
10
10
|
export * from "./utils/sortUtils";
|
|
11
11
|
export * from "./utils/apiUtils";
|
|
12
12
|
export * from "./utils/urlUtils";
|
|
13
|
+
export * from "./utils/stringUtils";
|
|
13
14
|
export * from "./utils/promoCodesUtils";
|
|
14
15
|
export * from "./utils/userPromoCodeUtils";
|
|
15
16
|
export * from "./utils/userSubscriptionUtils";
|
|
@@ -21,6 +22,7 @@ export * from "./utils/service/serviceDBUtils";
|
|
|
21
22
|
export * from "./utils/service/serviceRateUtils";
|
|
22
23
|
export * from "./utils/service/serviceBookingUtils";
|
|
23
24
|
export * from "./utils/service/serviceBookingApiUtils";
|
|
25
|
+
export * from "./utils/service/serviceBookingStatusUtils";
|
|
24
26
|
export * from "./utils/stripeAccountUtils";
|
|
25
27
|
export * from "./utils/entityUtils";
|
|
26
28
|
export * from "./utils/generalDateTimeUtils";
|
package/src/utils/luxonUtils.ts
CHANGED
|
@@ -876,6 +876,22 @@ export function splitRangeByInputTimes(
|
|
|
876
876
|
return segments;
|
|
877
877
|
}
|
|
878
878
|
|
|
879
|
+
export function dateTimeRangeFromDates(
|
|
880
|
+
startDate: Date | string,
|
|
881
|
+
endDate: Date | string,
|
|
882
|
+
interpretAsTimezone?: string,
|
|
883
|
+
timezoneToConvertTo?: string
|
|
884
|
+
): LuxonDateRange {
|
|
885
|
+
return {
|
|
886
|
+
start: dateTimeFromDate(
|
|
887
|
+
startDate,
|
|
888
|
+
interpretAsTimezone,
|
|
889
|
+
timezoneToConvertTo
|
|
890
|
+
),
|
|
891
|
+
end: dateTimeFromDate(endDate, interpretAsTimezone, timezoneToConvertTo),
|
|
892
|
+
} as LuxonDateRange;
|
|
893
|
+
}
|
|
894
|
+
|
|
879
895
|
export function dateTimeFromString(str: string) {
|
|
880
896
|
return DateTime.fromISO(str);
|
|
881
897
|
}
|
|
@@ -883,3 +899,8 @@ export function dateTimeFromString(str: string) {
|
|
|
883
899
|
export function dateTimeToString(dateTime: DateTime): string | null {
|
|
884
900
|
return dateTime.toISO();
|
|
885
901
|
}
|
|
902
|
+
|
|
903
|
+
export function dateRangesFormat(dateTimes: LuxonDateRange[]): string {
|
|
904
|
+
const result = dateTimes.join(", ");
|
|
905
|
+
return result;
|
|
906
|
+
}
|
|
@@ -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
|
}
|
|
@@ -2,14 +2,36 @@ import {
|
|
|
2
2
|
ServiceBookingApiParamsLuxon,
|
|
3
3
|
ServiceBookingApiParams,
|
|
4
4
|
ServiceBookedDayApiParamsLuxon,
|
|
5
|
+
ServiceAddonApiParams,
|
|
6
|
+
ServiceBookedDayApiParams,
|
|
7
|
+
ServicePriceToBookApiResult,
|
|
5
8
|
} from "../../definitions";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
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";
|
|
9
26
|
import {
|
|
10
27
|
ServiceGetPriceToBookFeesParams,
|
|
11
28
|
ServiceBookingDayInfoParams,
|
|
12
29
|
ServiceBookingFee,
|
|
30
|
+
ServiceAddonInput,
|
|
31
|
+
ServiceGetPriceToBookResult,
|
|
32
|
+
serviceGetPriceToBookFees,
|
|
33
|
+
serviceGetPriceToBookExt,
|
|
34
|
+
ServiceGetPriceToBookExtDay,
|
|
13
35
|
} from "./serviceBookingUtils";
|
|
14
36
|
|
|
15
37
|
export type ServiceCantBookReason = {
|
|
@@ -42,34 +64,155 @@ export const ServiceCantBookReasons = {
|
|
|
42
64
|
|
|
43
65
|
export type ServiceCantBookReasons = keyof typeof ServiceCantBookReasons;
|
|
44
66
|
|
|
45
|
-
export function
|
|
46
|
-
|
|
67
|
+
export function apiTransformAddOnsToAddonInputs<
|
|
68
|
+
AddonParams_t extends ServiceAddonApiParams
|
|
69
|
+
>(
|
|
70
|
+
addOnsApi: AddonParams_t[],
|
|
47
71
|
serviceAddons: ServiceAddonExt[]
|
|
48
|
-
):
|
|
49
|
-
// Create a
|
|
72
|
+
): ServiceAddonInput[] {
|
|
73
|
+
// Create a Map for faster lookup by addonId
|
|
50
74
|
const addonMap = new Map<string, ServiceAddonExt>(
|
|
51
75
|
serviceAddons.map((addon) => [addon.id, addon])
|
|
52
76
|
);
|
|
53
77
|
|
|
54
|
-
|
|
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
|
|
55
209
|
const daysToBookParams: ServiceBookingDayInfoParams[] = params.bookedDays.map(
|
|
56
210
|
(day) => {
|
|
57
|
-
//
|
|
58
|
-
const addOnsWithDetails =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Failed to match addon with id: ${addonInput.addonId}`
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
...matchedAddon,
|
|
69
|
-
addonId: addonInput.addonId,
|
|
70
|
-
chosenQuantity: addonInput.chosenQuantity,
|
|
71
|
-
};
|
|
72
|
-
});
|
|
211
|
+
// Use the helper function to transform the day's addOns
|
|
212
|
+
const addOnsWithDetails = apiTransformAddOnsToAddonInputs(
|
|
213
|
+
day.addOns,
|
|
214
|
+
serviceAddons
|
|
215
|
+
);
|
|
73
216
|
|
|
74
217
|
return {
|
|
75
218
|
dateTimeRange: day.dateTimeRange,
|
|
@@ -86,7 +229,7 @@ export function serviceBookingParamsToPriceToBookParams(
|
|
|
86
229
|
|
|
87
230
|
return {
|
|
88
231
|
daysToBookParams: daysToBookParams,
|
|
89
|
-
selectedAttendeeOption: {} as ServiceAttendeeOption, // Placeholder
|
|
232
|
+
selectedAttendeeOption: {} as ServiceAttendeeOption, // Placeholder: update with actual business logic
|
|
90
233
|
timezone: params.timezone,
|
|
91
234
|
};
|
|
92
235
|
}
|
|
@@ -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
|
+
}
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
LuxonDateRange,
|
|
16
16
|
} from "../luxonUtils";
|
|
17
17
|
|
|
18
|
+
export const SERVICE_BOOKING_PROCESSING_FEE_PERCENT = 0.15;
|
|
19
|
+
|
|
18
20
|
export interface ServiceAddonInput extends ServiceAddon {
|
|
19
21
|
chosenQuantity?: number;
|
|
20
22
|
}
|
|
@@ -39,6 +41,7 @@ export interface ServiceBookingFee {
|
|
|
39
41
|
export interface ServiceBookingDayInfoParams {
|
|
40
42
|
dateTimeRange: LuxonDateRange;
|
|
41
43
|
fees: ServiceBookingFee[];
|
|
44
|
+
addOns: ServiceAddonInput[];
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export interface ServiceBookingDayInfo {
|
|
@@ -53,6 +56,8 @@ export interface ServiceBookingDayInfo {
|
|
|
53
56
|
|
|
54
57
|
fees: ServiceBookingFee[];
|
|
55
58
|
totalBeforeTaxes: number;
|
|
59
|
+
|
|
60
|
+
addOns: ServiceAddonInput[];
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
export interface ServiceGetPriceToBookResult {
|
|
@@ -169,6 +174,7 @@ export function serviceGetBookingDayInfo({
|
|
|
169
174
|
priceBreakdown: priceBreakdown,
|
|
170
175
|
dateTimeRange: day.dateTimeRange,
|
|
171
176
|
fees: day.fees,
|
|
177
|
+
addOns: day.addOns,
|
|
172
178
|
totalBeforeTaxes: totalBeforeTaxes,
|
|
173
179
|
} as ServiceBookingDayInfo;
|
|
174
180
|
}
|
|
@@ -204,17 +210,6 @@ export function serviceGetPriceToBookFees(
|
|
|
204
210
|
timezone
|
|
205
211
|
);
|
|
206
212
|
|
|
207
|
-
const processingFee = 53.08;
|
|
208
|
-
const additionalFees = (
|
|
209
|
-
[
|
|
210
|
-
{
|
|
211
|
-
name: "Processing Fee",
|
|
212
|
-
type: "processingFee",
|
|
213
|
-
fee: processingFee,
|
|
214
|
-
},
|
|
215
|
-
] as ServiceBookingFee[]
|
|
216
|
-
).filter((fee) => fee.fee > 0);
|
|
217
|
-
|
|
218
213
|
const bookingDayResults: ServiceBookingDayInfo[] = serviceGetBookingDayInfo({
|
|
219
214
|
filteredRates: filteredRates,
|
|
220
215
|
daysToBookParams: daysToBookParams,
|
|
@@ -225,8 +220,8 @@ export function serviceGetPriceToBookFees(
|
|
|
225
220
|
const durationHours = bookingDateTimeRanges.reduce((sofar, curr) => {
|
|
226
221
|
return sofar + dateTimeRangeHours(curr);
|
|
227
222
|
}, 0);
|
|
228
|
-
const
|
|
229
|
-
return sofar + curr.
|
|
223
|
+
const bookingsTotalBeforeTaxes = bookingDayResults.reduce((sofar, curr) => {
|
|
224
|
+
return sofar + curr.totalBeforeTaxes;
|
|
230
225
|
}, 0);
|
|
231
226
|
const baseHourlyRate =
|
|
232
227
|
serviceRatesAssociation?.serviceGeneralRates?.hourlyRate ?? 0;
|
|
@@ -235,22 +230,34 @@ export function serviceGetPriceToBookFees(
|
|
|
235
230
|
const baseCostDiscount =
|
|
236
231
|
baseCostUndiscounted == 0
|
|
237
232
|
? 0
|
|
238
|
-
: 1 -
|
|
233
|
+
: 1 - bookingsTotalBeforeTaxes / baseCostUndiscounted;
|
|
239
234
|
|
|
240
235
|
const minimumTimeBlockHours =
|
|
241
236
|
serviceRatesAssociation?.serviceGeneralRates?.minimumTimeBlockHours;
|
|
242
237
|
|
|
243
238
|
const attendeeRate = selectedAttendeeOption.rate * durationHours;
|
|
244
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
|
+
|
|
245
252
|
const totalBeforeTaxes =
|
|
246
|
-
|
|
253
|
+
bookingsTotalBeforeTaxes +
|
|
247
254
|
additionalFees.reduce((sofar, fee) => sofar + fee.fee, 0);
|
|
248
255
|
|
|
249
256
|
return {
|
|
250
257
|
serviceId: serviceRatesAssociation?.serviceId,
|
|
251
258
|
daysToBook: bookingDayResults,
|
|
252
259
|
baseCostUndiscounted: baseCostUndiscounted,
|
|
253
|
-
baseCostDiscounted:
|
|
260
|
+
baseCostDiscounted: bookingsTotalBeforeTaxes,
|
|
254
261
|
baseCostDiscount: baseCostDiscount,
|
|
255
262
|
totalBeforeTaxes: totalBeforeTaxes,
|
|
256
263
|
additionalFees: additionalFees,
|
|
@@ -309,6 +316,69 @@ export function serviceGetPriceToBook(
|
|
|
309
316
|
return {
|
|
310
317
|
dateTimeRange: dtRange,
|
|
311
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,
|
|
312
382
|
};
|
|
313
383
|
}
|
|
314
384
|
);
|
package/src/utils/urlUtils.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {isProduction} from "./apiUtils";
|
|
2
|
-
import {BASH_DETAIL_URL} from "../definitions";
|
|
3
|
-
|
|
1
|
+
import { isProduction } from "./apiUtils";
|
|
2
|
+
import { BASH_DETAIL_URL } from "../definitions";
|
|
4
3
|
|
|
5
4
|
const API_HOST = process.env.REACT_APP_API ?? "http://localhost:3500";
|
|
6
5
|
|
|
7
|
-
|
|
8
6
|
export function getFrontendHost(): string {
|
|
9
7
|
const host = isProduction()
|
|
10
8
|
? `${window.location.protocol}//${window.location.host}`
|
|
@@ -25,5 +23,66 @@ export function getSsrBashDetailUrl(bashEventId: string | undefined): string {
|
|
|
25
23
|
return url;
|
|
26
24
|
}
|
|
27
25
|
console.error(`BashEventId was not specified for the ssr bash detail url`);
|
|
28
|
-
return
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface QueryParam {
|
|
30
|
+
key: string;
|
|
31
|
+
value: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function urlAppendQueryParam(url: string, params: QueryParam[]): string {
|
|
35
|
+
// Split the URL into base and hash
|
|
36
|
+
const [base, hash = ""] = url.split("#");
|
|
37
|
+
|
|
38
|
+
// Split the base into path and query
|
|
39
|
+
const [path, query = ""] = base.split("?");
|
|
40
|
+
|
|
41
|
+
// Initialize URLSearchParams with existing query
|
|
42
|
+
const searchParams = new URLSearchParams(query);
|
|
43
|
+
|
|
44
|
+
// Append each new query parameter
|
|
45
|
+
params.forEach(({ key, value }) => searchParams.append(key, value));
|
|
46
|
+
|
|
47
|
+
// Convert searchParams back to string
|
|
48
|
+
const newQuery = searchParams.toString();
|
|
49
|
+
|
|
50
|
+
// Reconstruct the URL
|
|
51
|
+
let newUrl = path;
|
|
52
|
+
if (newQuery) {
|
|
53
|
+
newUrl += `?${newQuery}`;
|
|
54
|
+
}
|
|
55
|
+
if (hash) {
|
|
56
|
+
newUrl += `#${hash}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return newUrl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function urlRemoveQueryParam(url: string, keys: string[]): string {
|
|
63
|
+
// Split the URL into base and hash
|
|
64
|
+
const [base, hash = ""] = url.split("#");
|
|
65
|
+
|
|
66
|
+
// Split the base into path and query
|
|
67
|
+
const [path, query = ""] = base.split("?");
|
|
68
|
+
|
|
69
|
+
// Initialize URLSearchParams with existing query
|
|
70
|
+
const searchParams = new URLSearchParams(query);
|
|
71
|
+
|
|
72
|
+
// Remove each specified query parameter
|
|
73
|
+
keys.forEach((key) => searchParams.delete(key));
|
|
74
|
+
|
|
75
|
+
// Convert searchParams back to string
|
|
76
|
+
const newQuery = searchParams.toString();
|
|
77
|
+
|
|
78
|
+
// Reconstruct the URL
|
|
79
|
+
let newUrl = path;
|
|
80
|
+
if (newQuery) {
|
|
81
|
+
newUrl += `?${newQuery}`;
|
|
82
|
+
}
|
|
83
|
+
if (hash) {
|
|
84
|
+
newUrl += `#${hash}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return newUrl;
|
|
29
88
|
}
|