@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash-app/bash-common",
3
- "version": "29.68.0",
3
+ "version": "29.69.0",
4
4
  "description": "Common data and scripts to use on the frontend and backend",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -140,25 +140,30 @@ model BashEventPromoCode {
140
140
  }
141
141
 
142
142
  model BashNotification {
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
- reminders Reminder[]
158
- redirectUrl String?
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
- requestedOn DateTime?
1679
- confirmedOn DateTime?
1680
- paidOn DateTime?
1681
- refundedOn DateTime?
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
- totalAmount Int //stored in cents
1687
- depositAmount Int
1688
- checkout ServiceBookingCheckout?
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
  }
@@ -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: number;
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 {
@@ -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";
@@ -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 {BASH_FEE_PERCENTAGE, NumberOfTicketsForDate, PRICE_DOLLARS_AND_CENTS_RATIO} from "../definitions";
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(totalInDollars: number, promoCode: BashEventPromoCode | undefined): number {
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(promoCode.discountAmountInCents);
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 = totalInDollars * (promoCode.discountAmountPercentage / 100);
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(ticketTiers: TicketTier[], ticketList: Map<string, NumberOfTicketsForDate[]>): number {
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(price: number | undefined | null): number {
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(price: number | undefined | null): number {
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 { ServiceAddonExt } from "../../extendedSchemas";
7
- import { dateTimeFromString } from "../luxonUtils";
8
- import { ServiceAttendeeOption } from "./attendeeOptionUtils";
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 serviceBookingParamsToPriceToBookParams(
46
- params: ServiceBookingApiParamsLuxon,
67
+ export function apiTransformAddOnsToAddonInputs<
68
+ AddonParams_t extends ServiceAddonApiParams
69
+ >(
70
+ addOnsApi: AddonParams_t[],
47
71
  serviceAddons: ServiceAddonExt[]
48
- ): ServiceGetPriceToBookFeesParams {
49
- // Create a HashMap (Map) for faster lookup by addonId
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
- // Map the addOns from the params to the format expected by ServiceGetPriceToBookFeesParams
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
- // Map over each day's addOns and match with the addonMap to get additional data
58
- const addOnsWithDetails = day.addOns.map((addonInput) => {
59
- const matchedAddon = addonMap.get(addonInput.addonId);
60
-
61
- if (!matchedAddon) {
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, should be determined based on business logic
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 baseCostDiscounted = bookingDayResults.reduce((sofar, curr) => {
229
- return sofar + curr.baseCostDiscounted;
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 - baseCostDiscounted / baseCostUndiscounted;
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
- baseCostDiscounted +
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: 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
  );
@@ -0,0 +1,8 @@
1
+ export function formatString<T extends Record<string, string | number>>(
2
+ template: string,
3
+ data: T
4
+ ): string {
5
+ return template.replace(/\$?{(\w+)}/g, (_, key) => {
6
+ return data[key] !== undefined ? data[key].toString() : "";
7
+ });
8
+ }
@@ -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
  }