@ticketboothapp/booking 0.1.11 → 0.1.13

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.
Files changed (255) hide show
  1. package/package.json +2 -1
  2. package/src/app/photo-sessions/photo-packages.ts +75 -0
  3. package/src/assets/icons/partner-logos/getyourguide.svg +8 -0
  4. package/src/assets/icons/plus.svg +3 -0
  5. package/src/colours.css +23 -0
  6. package/src/components/BookingDetails.module.css +1591 -0
  7. package/src/components/BookingDetails.tsx +2072 -354
  8. package/src/components/BookingWidget.tsx +28 -248
  9. package/src/components/JobApplicationDialog.module.css +440 -0
  10. package/src/components/JobApplicationDialog.tsx +620 -0
  11. package/src/components/ManageBookingView.tsx +28 -36
  12. package/src/components/PhoneInputWithCountry.module.css +131 -0
  13. package/src/components/PhoneInputWithCountry.tsx +44 -0
  14. package/src/components/PickupLocationDialog.module.css +360 -0
  15. package/src/components/PickupLocationDialog.tsx +357 -0
  16. package/src/components/PickupLocationMap.tsx +110 -0
  17. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  18. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  19. package/src/components/accordion.css +27 -0
  20. package/src/components/accordion.tsx +29 -0
  21. package/src/components/analytics/AnalyticsConsentRestore.tsx +19 -0
  22. package/src/components/analytics/AnalyticsScripts.tsx +106 -0
  23. package/src/components/analytics/CookieConsentBanner.css +86 -0
  24. package/src/components/analytics/CookieConsentBanner.tsx +102 -0
  25. package/src/components/booking/AddOnsSection.module.css +10 -0
  26. package/src/components/booking/AddOnsSection.tsx +184 -0
  27. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  28. package/src/components/booking/BookingDialog.module.css +643 -0
  29. package/src/components/booking/BookingDialog.tsx +356 -0
  30. package/src/components/booking/BookingFlow.tsx +4385 -0
  31. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  32. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  33. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  34. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  35. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  36. package/src/components/booking/BookingProductGrid.module.css +359 -0
  37. package/src/components/booking/BookingProductGrid.tsx +497 -0
  38. package/src/components/booking/Calendar.module.css +616 -0
  39. package/src/components/{Calendar.tsx → booking/Calendar.tsx} +464 -247
  40. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  41. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  42. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  43. package/src/components/booking/CheckoutForm.module.css +244 -0
  44. package/src/components/booking/CheckoutForm.tsx +364 -0
  45. package/src/components/{CheckoutModal.tsx → booking/CheckoutModal.tsx} +176 -19
  46. package/src/components/booking/DapFlowCollage.tsx +88 -0
  47. package/src/components/booking/DapTourDescription.tsx +35 -0
  48. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  49. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  50. package/src/components/booking/InfoTooltip.tsx +108 -0
  51. package/src/components/booking/ItineraryBox.module.css +258 -0
  52. package/src/components/booking/ItineraryBox.tsx +550 -0
  53. package/src/components/{ItineraryBuilder.tsx → booking/ItineraryBuilder.tsx} +1 -2
  54. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  55. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  56. package/src/components/{MealDrinkAddOnSelector.tsx → booking/MealDrinkAddOnSelector.tsx} +21 -13
  57. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  58. package/src/components/{PickupLocationSelector.tsx → booking/PickupLocationSelector.tsx} +315 -290
  59. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  60. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  61. package/src/components/{PriceBreakdown.tsx → booking/PriceBreakdown.tsx} +3 -3
  62. package/src/components/{PriceSummary.tsx → booking/PriceSummary.tsx} +51 -28
  63. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  64. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  65. package/src/components/booking/PromoCodeInput.module.css +166 -0
  66. package/src/components/booking/PromoCodeInput.tsx +99 -0
  67. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  68. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  69. package/src/components/{TermsAcceptance.tsx → booking/TermsAcceptance.tsx} +9 -8
  70. package/src/components/booking/TicketSelector.module.css +164 -0
  71. package/src/components/booking/TicketSelector.tsx +199 -0
  72. package/src/components/booking/TourDescription.module.css +304 -0
  73. package/src/components/booking/TourDescription.tsx +273 -0
  74. package/src/components/booking/booking-flow-ui.ts +15 -1
  75. package/src/components/booking/booking-flow.css +944 -0
  76. package/src/components/bottom-sheet.module.css +78 -0
  77. package/src/components/bottom-sheet.tsx +60 -0
  78. package/src/components/breadcrumb.module.css +40 -0
  79. package/src/components/breadcrumb.tsx +36 -0
  80. package/src/components/button.css +245 -0
  81. package/src/components/button.tsx +152 -0
  82. package/src/components/client-bottom-sheet.tsx +14 -0
  83. package/src/components/colorable-svg.tsx +29 -0
  84. package/src/components/conditional-footer.tsx +27 -0
  85. package/src/components/contact-us.module.css +147 -0
  86. package/src/components/contact-us.tsx +49 -0
  87. package/src/components/email-signup.css +151 -0
  88. package/src/components/email-signup.tsx +63 -0
  89. package/src/components/faq-wrapper.module.css +47 -0
  90. package/src/components/faq-wrapper.tsx +15 -0
  91. package/src/components/footer.css +187 -0
  92. package/src/components/footer.tsx +143 -0
  93. package/src/components/global-simple-modal.tsx +33 -0
  94. package/src/components/google-review-summary.module.css +77 -0
  95. package/src/components/google-review-summary.tsx +50 -0
  96. package/src/components/hero-image.css +13 -0
  97. package/src/components/hero-image.tsx +44 -0
  98. package/src/components/image.css +29 -0
  99. package/src/components/image.tsx +113 -0
  100. package/src/components/language-aware-link.tsx +72 -0
  101. package/src/components/language-switcher.module.css +124 -0
  102. package/src/components/language-switcher.tsx +75 -0
  103. package/src/components/map-section.css +59 -0
  104. package/src/components/map-section.tsx +63 -0
  105. package/src/components/navbar.module.css +152 -0
  106. package/src/components/navbar.tsx +125 -0
  107. package/src/components/parallax-provider.tsx +11 -0
  108. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  109. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  110. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +19 -35
  111. package/src/components/product-tag.module.css +30 -0
  112. package/src/components/product-tag.tsx +34 -0
  113. package/src/components/product-theme-pages/best-option.module.css +70 -0
  114. package/src/components/product-theme-pages/best-option.tsx +35 -0
  115. package/src/components/product-theme-pages/extended-tour-options.module.css +22 -0
  116. package/src/components/product-theme-pages/extended-tour-options.tsx +11 -0
  117. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  118. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  119. package/src/components/product-theme-pages/photo-gallery.tsx +90 -0
  120. package/src/components/product-theme-pages/product-theme-page-layout.module.css +13 -0
  121. package/src/components/product-theme-pages/product-theme-page-layout.tsx +67 -0
  122. package/src/components/product-theme-pages/top-of-fold.module.css +179 -0
  123. package/src/components/product-theme-pages/top-of-fold.tsx +80 -0
  124. package/src/components/product-tile/image-only-product-tile-desktop.module.css +106 -0
  125. package/src/components/product-tile/image-only-product-tile-desktop.tsx +56 -0
  126. package/src/components/product-tile/image-only-product-tile-mobile.module.css +122 -0
  127. package/src/components/product-tile/image-only-product-tile-mobile.tsx +89 -0
  128. package/src/components/product-tile/image-only-product-tile.tsx +44 -0
  129. package/src/components/product-tile/product-tile-card.module.css +84 -0
  130. package/src/components/product-tile/product-tile-card.tsx +61 -0
  131. package/src/components/review-highlights-section.css +85 -0
  132. package/src/components/review-highlights-section.tsx +127 -0
  133. package/src/components/season-closure-overlay.module.css +99 -0
  134. package/src/components/season-closure-overlay.tsx +98 -0
  135. package/src/components/simple-modal.tsx +69 -0
  136. package/src/components/simple-top-of-fold.module.css +76 -0
  137. package/src/components/simple-top-of-fold.tsx +34 -0
  138. package/src/components/spacer.css +41 -0
  139. package/src/components/spacer.tsx +23 -0
  140. package/src/components/star-rating.module.css +74 -0
  141. package/src/components/star-rating.tsx +48 -0
  142. package/src/components/terms/TermsContent.tsx +178 -0
  143. package/src/components/title-subtitle.module.css +10 -0
  144. package/src/components/title-subtitle.tsx +30 -0
  145. package/src/components/translatable-reviews.tsx +75 -0
  146. package/src/components/value-pill.module.css +59 -0
  147. package/src/components/value-pill.tsx +46 -0
  148. package/src/components/value-props.css +185 -0
  149. package/src/components/value-props.tsx +88 -0
  150. package/src/constants/booking-guide-quiz.ts +64 -0
  151. package/src/constants/contact-info.ts +2 -0
  152. package/src/constants/faq.ts +44 -0
  153. package/src/constants/images.ts +556 -0
  154. package/src/constants/json-ld/faq-json-ld.tsx +170 -0
  155. package/src/constants/json-ld/homepage-json-ld.tsx +138 -0
  156. package/src/constants/json-ld/job-posting-json-ld.tsx +92 -0
  157. package/src/constants/json-ld/organization-json-ld.tsx +62 -0
  158. package/src/constants/json-ld/page-json-ld.tsx +6 -0
  159. package/src/constants/json-ld/product-json-ld.tsx +154 -0
  160. package/src/constants/json-ld/review-json-ld.tsx +377 -0
  161. package/src/constants/navigation-links/footer-links.ts +48 -0
  162. package/src/constants/navigation-links/nav-bar-links.ts +41 -0
  163. package/src/constants/navigation-links/navigation-link.ts +6 -0
  164. package/src/constants/pill-values.ts +210 -0
  165. package/src/constants/products.ts +155 -0
  166. package/src/constants/quiz-recommendations.ts +506 -0
  167. package/src/constants/reviews.ts +75 -0
  168. package/src/constants/staff.ts +197 -0
  169. package/src/constants/value-props.ts +58 -0
  170. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  171. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  172. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  173. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  174. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  175. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  176. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  177. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  178. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  179. package/src/data/product-descriptions/private-tour.en.json +80 -0
  180. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  181. package/src/data/products-config.json +101 -0
  182. package/src/hooks/use-bottom-sheet.tsx +15 -0
  183. package/src/hooks/use-simple-modal.tsx +27 -0
  184. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  185. package/src/hooks/useEmailSubscription.tsx +103 -0
  186. package/src/hooks/useEmbeddedInIframe.ts +16 -0
  187. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  188. package/src/hooks/useQuiz.tsx +210 -0
  189. package/src/index.ts +27 -2
  190. package/src/lib/analytics.ts +197 -0
  191. package/src/lib/booking/booking-source.ts +20 -2
  192. package/src/lib/{checkout-breakdown.ts → booking/checkout-breakdown.ts} +1 -1
  193. package/src/lib/booking/correlation-id.ts +46 -0
  194. package/src/lib/{i18n → booking/i18n}/messages/en.json +48 -4
  195. package/src/lib/{i18n → booking/i18n}/messages/fr.json +48 -4
  196. package/src/lib/booking/itinerary-display.ts +36 -0
  197. package/src/lib/{itinerary-labels.ts → booking/itinerary-labels.ts} +1 -1
  198. package/src/lib/{location-calculations.ts → booking/location-calculations.ts} +4 -4
  199. package/src/lib/{location-utils.ts → booking/location-utils.ts} +26 -0
  200. package/src/lib/{map-utils.ts → booking/map-utils.ts} +3 -3
  201. package/src/lib/booking/normalize-booking-product-id.ts +7 -0
  202. package/src/lib/{pickup-location-types.ts → booking/pickup-location-types.ts} +2 -2
  203. package/src/lib/{pricing.ts → booking/pricing.ts} +2 -2
  204. package/src/lib/booking/product-option-id.ts +35 -0
  205. package/src/lib/booking/source-metadata.ts +72 -7
  206. package/src/lib/booking/sunday-week.ts +14 -0
  207. package/src/lib/booking/trace-context.ts +62 -0
  208. package/src/lib/booking-api.ts +1793 -0
  209. package/src/lib/{constants.ts → booking-constants.ts} +11 -5
  210. package/src/lib/booking-types.ts +36 -0
  211. package/src/lib/currency.ts +38 -45
  212. package/src/lib/dap-descriptions.ts +50 -0
  213. package/src/lib/dap-itinerary-preview.ts +315 -0
  214. package/src/lib/dependent-add-on-api.ts +434 -0
  215. package/src/lib/env.ts +89 -5
  216. package/src/lib/firebase.ts +20 -0
  217. package/src/lib/job-application-api.ts +83 -0
  218. package/src/lib/manage-booking-embed-print.ts +16 -0
  219. package/src/lib/manage-booking-post-checkout.ts +68 -0
  220. package/src/lib/photo-dap-config.ts +228 -0
  221. package/src/lib/pickup/map-utils.ts +56 -0
  222. package/src/lib/pickup/marker-icons.ts +19 -0
  223. package/src/lib/product-descriptions.ts +66 -0
  224. package/src/lib/products-config.ts +73 -0
  225. package/src/providers/booking-dialog-provider.tsx +107 -38
  226. package/src/providers/bottom-sheet-provider.tsx +40 -0
  227. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  228. package/src/radius.css +5 -0
  229. package/src/spacing.css +7 -0
  230. package/src/strings/en.json +1774 -0
  231. package/src/strings/es.json +1573 -0
  232. package/src/strings/fr.json +1573 -0
  233. package/src/strings/index.js +23 -0
  234. package/src/text-style.css +97 -0
  235. package/src/types/fareharbor.d.ts +12 -0
  236. package/src/types/quiz.ts +59 -0
  237. package/src/utils/currency-converter.ts +101 -0
  238. package/src/components/BookingFlow.tsx +0 -2952
  239. package/src/components/LanguageSwitcher.tsx +0 -30
  240. package/src/components/PrivateShuttleBookingFlow.tsx +0 -2290
  241. package/src/components/ProductList.tsx +0 -78
  242. package/src/components/WhatsAppPhoneInput.tsx +0 -224
  243. package/src/components/index.ts +0 -31
  244. package/src/lib/api.ts +0 -801
  245. package/src/lib/booking-api-auth.ts +0 -9
  246. package/src/lib/checkout-breakdown.test.ts +0 -70
  247. package/src/types/google-maps.d.ts +0 -2
  248. /package/src/components/{CurrencySwitcher.tsx → booking/CurrencySwitcher.tsx} +0 -0
  249. /package/src/components/{ErrorBoundary.tsx → booking/ErrorBoundary.tsx} +0 -0
  250. /package/src/lib/{i18n → booking/i18n}/config.ts +0 -0
  251. /package/src/lib/{i18n → booking/i18n}/index.tsx +0 -0
  252. /package/src/lib/{marker-icons.ts → booking/marker-icons.ts} +0 -0
  253. /package/src/lib/{places-api.ts → booking/places-api.ts} +0 -0
  254. /package/src/lib/{theme.ts → booking/theme.ts} +0 -0
  255. /package/src/lib/{utils.ts → booking/utils.ts} +0 -0
package/src/lib/api.ts DELETED
@@ -1,801 +0,0 @@
1
- import { ENV } from './env';
2
-
3
- const API_URL = ENV.API_URL;
4
- const BASIC_AUTH = ENV.BASIC_AUTH;
5
- const COMPANY_ID = ENV.COMPANY_ID;
6
-
7
- // API client configuration - allows passing auth token for embedded use
8
- let authToken: string | null = null;
9
-
10
- /**
11
- * Set authentication token for API calls (e.g., when embedded in provider dashboard)
12
- * @param token JWT token (Bearer token) - if set, will be used instead of Basic Auth
13
- */
14
- export function setAuthToken(token: string | null) {
15
- authToken = token;
16
- }
17
-
18
- /**
19
- * Get authorization header - prefers Bearer token if set, falls back to Basic Auth
20
- */
21
- function getAuthHeader(): string | undefined {
22
- if (authToken) {
23
- return `Bearer ${authToken}`;
24
- }
25
- if (BASIC_AUTH) {
26
- return `Basic ${BASIC_AUTH}`;
27
- }
28
- return undefined;
29
- }
30
-
31
- // ============ Types ============
32
-
33
- export interface PickupLocation {
34
- id: string;
35
- name: string;
36
- address: string;
37
- coordinates?: {
38
- lat: number;
39
- lng: number;
40
- };
41
- pickupTimeOffsetMinutes?: number; // Minutes after tour start for pickup
42
- /** Minutes from each last-destination name to this pickup; drop-off = return + value for product's last stop. Keys match product.destinations[].name. */
43
- travelMinutesFromDestination?: Record<string, number>;
44
- freeParking?: boolean; // Whether this location has free parking available
45
- notes?: string; // Additional notes or disclaimers about this location
46
- driverNotes?: string; // Notes for drivers (e.g., parking instructions, special access codes)
47
- }
48
-
49
- export interface ItineraryItem {
50
- destinationName: string; // Reference to a destination name from product.destinations
51
- travelTimeFromPreviousHours: number; // Hours to travel from previous destination (or from tour start for first destination) to arrive at this destination
52
- durationHours?: number; // Hours spent at this destination (used to calculate departure time: arrival + duration). If null, assume immediate departure.
53
- }
54
-
55
- export interface ItineraryOverride {
56
- startDate: string; // MM-dd format (e.g., "06-02")
57
- endDate: string; // MM-dd format (e.g., "06-14")
58
- itinerary: ItineraryItem[]; // Override itinerary for dates in this range
59
- }
60
-
61
- export interface ProductOption {
62
- optionId: string;
63
- name: string;
64
- description: string | null;
65
- pricing: Record<string, number>;
66
- status: string;
67
- gygOnly?: boolean; // If true, this option is only visible to GetYourGuide
68
- visible?: boolean; // If false, this option is hidden from internal systems
69
- mostPopular?: boolean; // If true, this option is marked as most popular and will be auto-selected
70
- privateShuttleConfig?: {
71
- baseDurationMinutes: number;
72
- suggestedStartTimes: string[]; // "HH:mm" format
73
- depositConfig: {
74
- percentage?: number;
75
- fixedAmount?: number;
76
- };
77
- balanceChargeDaysBefore?: number;
78
- itineraryBuilderConfig?: {
79
- includedLocationsCount: number;
80
- extraLocationCostPerHour: number;
81
- extraLocationHoursPerStop: number;
82
- optionBlacklist: string[];
83
- defaultDestinations?: string[]; // Destination IDs pre-selected by default for this option
84
- };
85
- };
86
- itinerary?: ItineraryItem[]; // Itinerary for this option (which destinations, in what order, with what timings)
87
- itineraryOverrides?: ItineraryOverride[]; // Date-specific itinerary overrides (MM-dd format for startDate/endDate)
88
- }
89
-
90
- export interface Destination {
91
- name: string;
92
- latitude: number;
93
- longitude: number;
94
- }
95
-
96
- export interface ItineraryBuilderDestination {
97
- id: string;
98
- label: string;
99
- latitude: number;
100
- longitude: number;
101
- }
102
-
103
- export interface ItineraryBuilder {
104
- destinations: ItineraryBuilderDestination[];
105
- }
106
-
107
- export interface CompanySettings {
108
- currency?: string;
109
- timezone?: string;
110
- }
111
-
112
- export interface Company {
113
- companyId: string;
114
- name: string;
115
- status: string;
116
- settings?: CompanySettings;
117
- }
118
-
119
- export interface Product {
120
- productId: string;
121
- companyId?: string; // Company that owns the product (included when returned from API)
122
- name: string;
123
- description: string | null;
124
- status: string;
125
- mostPopular?: boolean; // True if any option is marked most popular
126
- productType?: 'STANDARD' | 'PRIVATE_SHUTTLE'; // Product type (defaults to STANDARD)
127
- options: ProductOption[];
128
- /** Lowest base price per currency from ticketbooth-product-prices (FE displays only; no conversion). */
129
- minPriceByCurrency?: Record<string, number>;
130
- pickupLocations?: PickupLocation[];
131
- destinations?: Destination[]; // Destination locations for this product (for map display)
132
- itineraryBuilder?: ItineraryBuilder; // For PRIVATE_SHUTTLE: shared destinations and distance pairs
133
- }
134
-
135
- export interface ReturnOption {
136
- returnAvailabilityId: string;
137
- dateTime: string;
138
- vacancies: number;
139
- totalCapacity: number;
140
- bookedCapacity?: number; // Currently booked (for admin: show booked/total on scheduled returns)
141
- pricesByCategory?: {
142
- retailPrices: Array<{
143
- category: string;
144
- price: number;
145
- }>;
146
- };
147
- priceAdjustmentByCurrency?: Record<string, number>; // Per-person per currency; FE uses only this
148
- returnLocation: string; // Display name of return location
149
- mostPopular?: boolean; // If true, this return option is marked as most popular and will be auto-selected
150
- }
151
-
152
- /** Applied pricing adjustment (dynamic rule or deal) for price breakdown display */
153
- export interface AppliedAdjustment {
154
- type: string;
155
- id: string;
156
- name: string;
157
- /** currency -> amount so FE displays without conversion */
158
- changeByCurrency?: Record<string, number>;
159
- adjustmentType?: string;
160
- adjustmentValue?: number;
161
- }
162
-
163
- export interface Availability {
164
- dateTime: string;
165
- productId: string;
166
- vacancies: number;
167
- totalCapacity?: number; // Slot max (for admin capacity display: bookedCapacity = totalCapacity - vacancies)
168
- bookedCapacity?: number; // Currently booked (from API; or derived as totalCapacity - vacancies)
169
- resourceCount?: number; // Number of resources (e.g. shuttles) associated with this availability
170
- currency: string;
171
- availabilityId?: string;
172
- productOptionId?: string; // Track which product option this availability belongs to
173
- productType?: 'STANDARD' | 'PRIVATE_SHUTTLE'; // Product type from availability data
174
- suggestedStartTimes?: string[]; // For Private Shuttle: suggested start times in "HH:mm" format
175
- pricesByCategory?: {
176
- retailPrices: Array<{
177
- category: string;
178
- price: number;
179
- }>;
180
- };
181
- rates?: Array<{
182
- rateId: string;
183
- category: string;
184
- available: number;
185
- price?: number;
186
- priceByCurrency?: Record<string, number>; // Rate price per currency; use this, no FE conversion
187
- appliedAdjustments?: AppliedAdjustment[];
188
- applied_adjustments?: AppliedAdjustment[]; // snake_case fallback from API
189
- }>;
190
- returnOptions?: ReturnOption[]; // Return time options for this availability
191
- }
192
-
193
- export interface CancellationPolicyOption {
194
- id: string;
195
- label: string;
196
- /** currency -> per-booking fee to upgrade to this policy */
197
- feeByCurrency: Record<string, number>;
198
- }
199
-
200
- export interface PricingConfig {
201
- /** Combined GST/tax + service charge on pre-tax subtotal (from backend pricing config). */
202
- taxRate: number;
203
- currenciesWithTaxIncluded: string[];
204
- /** Fee name -> metadata (optional; empty/missing when no product fees). */
205
- fees?: Record<string, { feePerPerson: number; description?: string }>;
206
- /** currency -> (fee name -> amount) so FE displays without conversion */
207
- feesByCurrency?: Record<string, Record<string, number>>;
208
- exchangeRates?: Record<string, number>;
209
- /** Optional - list of upgrade options (id, label, fee per currency). If present, booking flow can offer upgrade. */
210
- cancellationPolicies?: CancellationPolicyOption[];
211
- }
212
-
213
- /** Precomputed prices from ticketbooth-product-prices: category -> currency -> price (display only; rates[].price is for GYG). */
214
- export type PrecomputedPricesByCategory = Record<string, Record<string, number>>;
215
-
216
- export interface GetAvailabilitiesResponse {
217
- data: {
218
- availabilities: Availability[];
219
- pricingConfig?: PricingConfig;
220
- precomputedPrices?: PrecomputedPricesByCategory;
221
- resourcePriceByCurrency?: Record<string, number>; // Private Shuttle only; FE uses this, no conversion
222
- resourcePriceByOption?: Record<string, Record<string, number>>; // When allOptions: optionId -> (currency -> price)
223
- };
224
- }
225
-
226
- export interface BookingItem {
227
- category: string;
228
- count: number;
229
- }
230
-
231
- export interface ReserveRequest {
232
- productId: string; // GetYourGuide passes productOptionId values here
233
- dateTime: string; // For Private Shuttle: date only (YYYY-MM-DD), for Standard: full datetime
234
- gygBookingReference?: string; // Optional - only required for GYG bookings
235
- bookingItems: BookingItem[]; // For Private Shuttle: [{ category: "RESOURCE", count: numberOfResources }]
236
- pickupLocationId?: string;
237
- returnAvailabilityId?: string; // Optional return time availability ID
238
- currency?: string; // Optional - currency code (CAD, USD, EUR, GBP, AUD)
239
- travelerHotel?: string; // Optional - hotel/pickup location text (e.g., "I don't know")
240
- startTime?: string; // Optional - ISO 8601 format with time (for Private Shuttle: user-selected start time)
241
- passengerCount?: number; // Optional - total passenger count (for Private Shuttle: used to calculate resource count)
242
- promoCode?: string; // Optional - promo/voucher code for pricing
243
- cancellationPolicyId?: string; // Optional - id of selected cancellation policy upgrade (e.g. "flexible")
244
- draftItinerary?: { destinations: string[]; planningNotes?: string };
245
- childSafetySeatsCount?: number;
246
- foodRestrictions?: string;
247
- addOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }>;
248
- /** Admin only: additional hours add-on (extends duration; price in checkout breakdown). */
249
- additionalHoursCount?: number;
250
- }
251
-
252
- export interface ReserveResponse {
253
- data: {
254
- reservationReference: string;
255
- expiresAt: string;
256
- totalAmount: number;
257
- currency: string;
258
- };
259
- }
260
-
261
- /** One line item in the checkout breakdown (what the user sees in Review & pay). Sent so we charge and store exactly that. */
262
- export interface CheckoutReceiptLine {
263
- label: string;
264
- amount: number;
265
- type?: string; // e.g. "TICKET", "RETURN_OPTION", "CANCELLATION_UPGRADE", "FEE", "TAX", "PROMO_CODE", "ROUNDING"
266
- quantity?: number;
267
- }
268
-
269
- /** Breakdown from frontend (what the user saw). When sent we charge totalAmount and store this as the receipt. */
270
- export interface CheckoutBreakdown {
271
- lineItems: CheckoutReceiptLine[];
272
- totalAmount: number;
273
- currency: string;
274
- }
275
-
276
- /** Request for creating a Payment Intent (embedded checkout modal). */
277
- export interface CreatePaymentIntentRequest {
278
- productId: string;
279
- optionId: string;
280
- date: string;
281
- time: string;
282
- quantity: number;
283
- customerEmail?: string;
284
- customerFirstName?: string;
285
- customerLastName?: string;
286
- currency?: string;
287
- reservationReference: string;
288
- travelerHotel?: string;
289
- /** Pickup location ID (persisted in Stripe metadata so webhook sets it on booking). */
290
- pickupLocationId?: string;
291
- itineraryDisplay?: ItineraryDisplayStep[];
292
- /** Return option at checkout (persisted so webhook has full context for receipt). */
293
- returnAvailabilityId?: string;
294
- /** Promo/voucher at checkout (persisted so webhook has full context for receipt). */
295
- promoCode?: string;
296
- /** Cancellation policy at checkout (persisted so webhook has full context for receipt). */
297
- cancellationPolicyId?: string;
298
- /** ISO timestamp of terms/policy acceptance (evidence for disputes). */
299
- termsAcceptedAt?: string;
300
- /** When true, do not send confirmation at creation only (provider dashboard). */
301
- skipConfirmationCommunications?: boolean;
302
- /** When true, store on booking and do not auto-send any communications (provider dashboard). */
303
- disableAutoCommunications?: boolean;
304
- /** What the user sees (Review & pay). When sent we charge totalAmount and store this as the receipt. */
305
- checkoutBreakdown?: CheckoutBreakdown;
306
- /** When deposit-only payment: pass DEPOSIT with amounts so backend stores correct plan/status. */
307
- paymentPlanType?: 'DEPOSIT' | 'PAY_IN_FULL';
308
- depositAmount?: number;
309
- balanceAmount?: number;
310
- totalAmount?: number;
311
- /** Days before booking to charge balance (e.g. 7). Used when paymentPlanType=DEPOSIT. */
312
- balanceChargeDaysBefore?: number;
313
- }
314
-
315
- export interface CreatePaymentIntentResponse {
316
- clientSecret?: string;
317
- /** True when total is 0 (e.g. voucher); no payment. Call confirmFreeBooking then redirect to success. */
318
- freeBooking?: boolean;
319
- /** Amount that will be charged (same as Stripe). Enables UI to show "You will be charged X". */
320
- totalAmount?: number;
321
- /** Currency for totalAmount (e.g. "CAD"). */
322
- currency?: string;
323
- }
324
-
325
- export const ItineraryStepType = {
326
- pickup: 'pickup',
327
- arrive: 'arrive',
328
- depart: 'depart',
329
- drop_off: 'drop_off',
330
- trip_end: 'trip_end',
331
- draft: 'draft',
332
- other: 'other',
333
- } as const;
334
- export type ItineraryStepType = (typeof ItineraryStepType)[keyof typeof ItineraryStepType];
335
-
336
- /** Backend stores only data; labels are built on FE from stepType + place for i18n. */
337
- export interface ItineraryDisplayStep {
338
- stepType: ItineraryStepType;
339
- time: string;
340
- /** pickup/drop_off: location name or "your_pickup_location"; arrive/depart: destination name or "the_destination"; trip_end: null */
341
- place?: string | null;
342
- }
343
-
344
- export interface ConfirmFreeBookingRequest {
345
- reservationReference: string;
346
- productId: string;
347
- optionId: string;
348
- date: string;
349
- time: string;
350
- customerEmail?: string;
351
- customerFirstName?: string;
352
- customerLastName?: string;
353
- currency?: string;
354
- travelerHotel?: string;
355
- pickupLocationId?: string;
356
- itineraryDisplay?: ItineraryDisplayStep[];
357
- /** ISO timestamp of terms/policy acceptance (evidence for disputes). */
358
- termsAcceptedAt?: string;
359
- /** When true, do not send confirmation at creation only (provider dashboard). */
360
- skipConfirmationCommunications?: boolean;
361
- /** When true, store on booking and do not auto-send any communications (provider dashboard). */
362
- disableAutoCommunications?: boolean;
363
- }
364
-
365
- export interface ConfirmFreeBookingResponse {
366
- bookingReference: string;
367
- reservationReference: string;
368
- }
369
-
370
- export async function confirmFreeBooking(
371
- request: ConfirmFreeBookingRequest
372
- ): Promise<ConfirmFreeBookingResponse> {
373
- const response = await fetch(`${API_URL}/checkout/confirm-free-booking`, {
374
- method: 'POST',
375
- headers: { 'Content-Type': 'application/json' },
376
- body: JSON.stringify(request),
377
- });
378
- if (!response.ok) {
379
- const err = await response.json();
380
- throw new Error(err.errorMessage || err.error || 'Failed to confirm free booking');
381
- }
382
- const data = await response.json();
383
- return (data.data ?? data) as ConfirmFreeBookingResponse;
384
- }
385
-
386
- export interface ConfirmBookingWithoutPaymentRequest {
387
- reservationReference: string;
388
- productId: string;
389
- optionId: string;
390
- date: string;
391
- time: string;
392
- customerEmail?: string;
393
- customerFirstName?: string;
394
- customerLastName?: string;
395
- currency?: string;
396
- travelerHotel?: string;
397
- pickupLocationId?: string;
398
- itineraryDisplay?: ItineraryDisplayStep[];
399
- termsAcceptedAt?: string;
400
- skipConfirmationCommunications?: boolean;
401
- disableAutoCommunications?: boolean;
402
- checkoutBreakdown: { lineItems: Array<{ label: string; amount: number; type?: string; quantity?: number }>; totalAmount: number; currency: string };
403
- depositAmount: number;
404
- balanceAmount: number;
405
- totalAmount: number;
406
- balanceChargeDaysBefore?: number;
407
- }
408
-
409
- export interface ConfirmBookingWithoutPaymentResponse {
410
- bookingReference: string;
411
- reservationReference: string;
412
- }
413
-
414
- export async function confirmBookingWithoutPayment(
415
- request: ConfirmBookingWithoutPaymentRequest
416
- ): Promise<ConfirmBookingWithoutPaymentResponse> {
417
- const response = await fetch(`${API_URL}/checkout/confirm-booking-without-payment`, {
418
- method: 'POST',
419
- headers: { 'Content-Type': 'application/json' },
420
- body: JSON.stringify(request),
421
- });
422
- if (!response.ok) {
423
- const err = await response.json();
424
- throw new Error(err.errorMessage || err.error || 'Failed to confirm booking');
425
- }
426
- const data = await response.json();
427
- return (data.data ?? data) as ConfirmBookingWithoutPaymentResponse;
428
- }
429
-
430
- export async function createManagePaymentIntent(
431
- bookingReference: string,
432
- lastName: string,
433
- paymentType: 'deposit' | 'full'
434
- ): Promise<CreateBalancePaymentIntentResponse> {
435
- const response = await fetch(`${API_URL}/checkout/manage-payment-intent`, {
436
- method: 'POST',
437
- headers: { 'Content-Type': 'application/json' },
438
- body: JSON.stringify({ bookingReference, lastName, paymentType }),
439
- });
440
- if (!response.ok) {
441
- const err = await response.json();
442
- throw new Error(err.errorMessage || err.error || 'Failed to create payment');
443
- }
444
- const data = await response.json();
445
- return (data.data ?? data) as CreateBalancePaymentIntentResponse;
446
- }
447
-
448
- export interface CreateBalancePaymentIntentResponse {
449
- clientSecret: string;
450
- amount: number;
451
- currency: string;
452
- }
453
-
454
- export async function createBalancePaymentIntent(
455
- bookingReference: string,
456
- lastName: string
457
- ): Promise<CreateBalancePaymentIntentResponse> {
458
- const response = await fetch(`${API_URL}/checkout/balance-payment-intent`, {
459
- method: 'POST',
460
- headers: { 'Content-Type': 'application/json' },
461
- body: JSON.stringify({ bookingReference, lastName }),
462
- });
463
- if (!response.ok) {
464
- const err = await response.json();
465
- throw new Error(err.errorMessage || err.error || 'Failed to create payment');
466
- }
467
- const data = await response.json();
468
- return (data.data ?? data) as CreateBalancePaymentIntentResponse;
469
- }
470
-
471
- // ============ API Functions ============
472
-
473
- export async function getProducts(): Promise<Product[]> {
474
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
475
-
476
- const authHeader = getAuthHeader();
477
- if (authHeader) {
478
- headers['Authorization'] = authHeader;
479
- }
480
-
481
- const response = await fetch(`${API_URL}/1/products?companyId=${COMPANY_ID}`, {
482
- method: 'GET',
483
- headers,
484
- });
485
-
486
- if (!response.ok) {
487
- const error = await response.json();
488
- throw new Error(error.errorMessage || 'Failed to get products');
489
- }
490
-
491
- const data = await response.json();
492
- return data.data?.products || [];
493
- }
494
-
495
- export interface AddOnVariant {
496
- id: string;
497
- label: string;
498
- priceAdjustment: number;
499
- }
500
-
501
- export interface AddOn {
502
- addOnId: string;
503
- name: string;
504
- description?: string | null;
505
- price: number;
506
- currency: string;
507
- preCheckout: boolean;
508
- postCheckout: boolean;
509
- variantType: 'none' | 'single_choice' | 'multi_quantity';
510
- variants: AddOnVariant[];
511
- productOptionIds: string[];
512
- }
513
-
514
- export async function getAddOns(
515
- companyId: string,
516
- options?: { productOptionId?: string; preCheckout?: boolean }
517
- ): Promise<AddOn[]> {
518
- const params = new URLSearchParams({ companyId });
519
- if (options?.productOptionId) params.set('productOptionId', options.productOptionId);
520
- if (options?.preCheckout !== undefined) params.set('preCheckout', String(options.preCheckout));
521
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
522
- const authHeader = getAuthHeader();
523
- if (authHeader) headers['Authorization'] = authHeader;
524
- const response = await fetch(`${API_URL}/1/add-ons?${params}`, { method: 'GET', headers });
525
- if (!response.ok) {
526
- const err = await response.json();
527
- throw new Error(err.errorMessage || err.error || 'Failed to get add-ons');
528
- }
529
- const data = await response.json();
530
- return data.data?.addOns || [];
531
- }
532
-
533
- export async function getProduct(productId: string): Promise<Product | null> {
534
- const products = await getProducts();
535
- return products.find(p => p.productId === productId) || null;
536
- }
537
-
538
- export interface ValidatePromoResponse {
539
- valid: boolean;
540
- name?: string;
541
- error?: string;
542
- /** When set, this promo forces the user to use this cancellation policy. Frontend should auto-select and optionally hide the selector. */
543
- forcedCancellationPolicyId?: string;
544
- /** Label for the forced policy (for display when policy has show_at_checkout=false). */
545
- forcedCancellationPolicyLabel?: string;
546
- }
547
-
548
- export async function validatePromoCode(
549
- promoCode: string,
550
- companyId: string,
551
- productId?: string,
552
- hasOngoingDiscount?: boolean
553
- ): Promise<ValidatePromoResponse> {
554
- const params = new URLSearchParams({ promoCode: promoCode.trim(), companyId });
555
- if (productId && productId.trim()) params.set('productId', productId.trim());
556
- if (hasOngoingDiscount === true) params.set('hasOngoingDiscount', 'true');
557
- const response = await fetch(`${API_URL}/1/validate-promo?${params}`, {
558
- method: 'GET',
559
- headers: { 'Content-Type': 'application/json' },
560
- });
561
- if (!response.ok) {
562
- const err = await response.json();
563
- throw new Error(err.errorMessage || err.error || 'Failed to validate promo code');
564
- }
565
- const data = await response.json();
566
- return data.data ?? { valid: false, error: 'Invalid response' };
567
- }
568
-
569
- export interface GetPromoDiscountResponse {
570
- discount: number;
571
- currency: string;
572
- /** When true, gift card applies after tax (tax on full subtotal, discount pays total due). */
573
- isGiftCard?: boolean;
574
- /** When true, voucher discount includes tax on free portion; tax on full subtotal. */
575
- isVoucher?: boolean;
576
- }
577
-
578
- export async function getPromoDiscount(
579
- promoCode: string,
580
- companyId: string,
581
- productId: string,
582
- optionId: string,
583
- currency: string,
584
- items: Array<{ category: string; qty: number }>,
585
- dateTime?: string,
586
- /** Optional subtotal (tickets + fees) so backend applies percentage to same amount as UI (e.g. 20% of 165.98 = 33.20). */
587
- subtotal?: number
588
- ): Promise<GetPromoDiscountResponse> {
589
- const itemsStr = items.map((i) => `${i.category}:${i.qty}`).join(',');
590
- const params = new URLSearchParams({
591
- promoCode: promoCode.trim(),
592
- companyId,
593
- productId,
594
- optionId,
595
- currency,
596
- items: itemsStr,
597
- });
598
- if (dateTime) params.set('dateTime', dateTime);
599
- if (subtotal != null && subtotal > 0) params.set('subtotal', String(subtotal));
600
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
601
- const authHeader = getAuthHeader();
602
- if (authHeader) headers['Authorization'] = authHeader;
603
- const response = await fetch(`${API_URL}/1/get-promo-discount?${params}`, {
604
- method: 'GET',
605
- headers,
606
- });
607
- if (!response.ok) {
608
- const err = await response.json();
609
- throw new Error(err.errorMessage || err.error || 'Failed to get promo discount');
610
- }
611
- const data = await response.json();
612
- return data.data ?? { discount: 0, currency, isGiftCard: false, isVoucher: false };
613
- }
614
-
615
- export interface GetAvailabilitiesOptions {
616
- promoCode?: string | null;
617
- /** When true and productId is a PRIVATE_SHUTTLE product, returns availabilities for ALL options (date-first flow) */
618
- allOptions?: boolean;
619
- }
620
-
621
- export async function getAvailabilities(
622
- productIdOrOptionId: string, // productId (p_) or productOptionId (po_)
623
- startDate: string,
624
- endDate: string,
625
- options?: GetAvailabilitiesOptions
626
- ): Promise<{
627
- availabilities: Availability[];
628
- pricingConfig?: PricingConfig;
629
- precomputedPrices?: PrecomputedPricesByCategory;
630
- resourcePriceByCurrency?: Record<string, number>;
631
- resourcePriceByOption?: Record<string, Record<string, number>>;
632
- }> {
633
- const params = new URLSearchParams({ productId: productIdOrOptionId, startDate, endDate });
634
- if (options?.promoCode && String(options.promoCode).trim()) {
635
- params.set('promoCode', String(options.promoCode).trim());
636
- }
637
- if (options?.allOptions === true) {
638
- params.set('allOptions', 'true');
639
- }
640
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
641
-
642
- const authHeader = getAuthHeader();
643
- if (authHeader) {
644
- headers['Authorization'] = authHeader;
645
- }
646
-
647
- const response = await fetch(`${API_URL}/1/get-availabilities?${params}`, {
648
- method: 'GET',
649
- headers,
650
- });
651
-
652
- if (!response.ok) {
653
- const error = await response.json();
654
- throw new Error(error.errorMessage || 'Failed to get availabilities');
655
- }
656
-
657
- const data: GetAvailabilitiesResponse = await response.json();
658
- return {
659
- availabilities: data.data?.availabilities || [],
660
- pricingConfig: data.data?.pricingConfig,
661
- precomputedPrices: data.data?.precomputedPrices,
662
- resourcePriceByCurrency: data.data?.resourcePriceByCurrency,
663
- resourcePriceByOption: data.data?.resourcePriceByOption,
664
- };
665
- }
666
-
667
- export async function createReservation(
668
- request: ReserveRequest
669
- ): Promise<ReserveResponse['data']> {
670
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
671
-
672
- const authHeader = getAuthHeader();
673
- if (authHeader) {
674
- headers['Authorization'] = authHeader;
675
- }
676
-
677
- const response = await fetch(`${API_URL}/1/reserve`, {
678
- method: 'POST',
679
- headers,
680
- body: JSON.stringify({ data: request }),
681
- });
682
-
683
- const responseText = await response.text();
684
- console.log('Reservation API response status:', response.status);
685
- console.log('Reservation API response body:', responseText);
686
-
687
- if (!response.ok) {
688
- let error;
689
- try {
690
- error = JSON.parse(responseText);
691
- } catch {
692
- error = { errorMessage: responseText || 'Failed to create reservation' };
693
- }
694
- throw new Error(error.errorMessage || error.message || 'Failed to create reservation');
695
- }
696
-
697
- let data;
698
- try {
699
- data = JSON.parse(responseText);
700
- } catch {
701
- console.error('Failed to parse reservation response as JSON:', responseText);
702
- throw new Error('Invalid JSON response from server');
703
- }
704
-
705
- // Check if this is an error response (backend returns 200 with errorCode/errorMessage)
706
- if (data.errorCode || data.errorMessage) {
707
- console.error('Backend returned error response:', data);
708
- throw new Error(data.errorMessage || data.error || 'Failed to create reservation');
709
- }
710
-
711
- // Validate success response structure
712
- if (!data || !data.data) {
713
- console.error('Invalid reservation response structure:', data);
714
- console.error('Response text was:', responseText);
715
- throw new Error('Invalid response from server: missing data field');
716
- }
717
-
718
- if (!data.data.reservationReference) {
719
- console.error('Invalid reservation response: missing reservationReference', data);
720
- throw new Error('Invalid response from server: missing reservationReference');
721
- }
722
-
723
- return data.data;
724
- }
725
-
726
- export async function getCompany(companyId: string): Promise<Company> {
727
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
728
-
729
- const authHeader = getAuthHeader();
730
- if (authHeader) {
731
- headers['Authorization'] = authHeader;
732
- }
733
-
734
- const response = await fetch(`${API_URL}/1/companies/${companyId}`, {
735
- method: 'GET',
736
- headers,
737
- });
738
-
739
- if (!response.ok) {
740
- const error = await response.json();
741
- throw new Error(error.errorMessage || 'Failed to get company');
742
- }
743
-
744
- const data = await response.json();
745
- return data.data?.company || null;
746
- }
747
-
748
- export async function cancelReservation(
749
- reservationReference: string
750
- ): Promise<void> {
751
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
752
-
753
- const authHeader = getAuthHeader();
754
- if (authHeader) {
755
- headers['Authorization'] = authHeader;
756
- }
757
-
758
- const response = await fetch(`${API_URL}/1/cancel-reservation`, {
759
- method: 'POST',
760
- headers,
761
- body: JSON.stringify({
762
- data: {
763
- reservationReference,
764
- gygBookingReference: '' // Empty for non-GYG bookings
765
- }
766
- }),
767
- });
768
-
769
- if (!response.ok) {
770
- const responseText = await response.text();
771
- let error;
772
- try {
773
- error = JSON.parse(responseText);
774
- } catch {
775
- error = { errorMessage: responseText || 'Failed to cancel reservation' };
776
- }
777
- throw new Error(error.errorMessage || error.message || 'Failed to cancel reservation');
778
- }
779
- }
780
-
781
- export async function createPaymentIntent(
782
- request: CreatePaymentIntentRequest
783
- ): Promise<CreatePaymentIntentResponse> {
784
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
785
- const authHeader = getAuthHeader();
786
- if (authHeader) headers['Authorization'] = authHeader;
787
-
788
- const response = await fetch(`${API_URL}/checkout/payment-intent`, {
789
- method: 'POST',
790
- headers,
791
- body: JSON.stringify(request),
792
- });
793
-
794
- if (!response.ok) {
795
- const error = await response.json();
796
- throw new Error(error.error || 'Failed to create payment intent');
797
- }
798
-
799
- const data = await response.json();
800
- return data.data || data;
801
- }