@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.
- package/package.json +2 -1
- package/src/app/photo-sessions/photo-packages.ts +75 -0
- package/src/assets/icons/partner-logos/getyourguide.svg +8 -0
- package/src/assets/icons/plus.svg +3 -0
- package/src/colours.css +23 -0
- package/src/components/BookingDetails.module.css +1591 -0
- package/src/components/BookingDetails.tsx +2072 -354
- package/src/components/BookingWidget.tsx +28 -248
- package/src/components/JobApplicationDialog.module.css +440 -0
- package/src/components/JobApplicationDialog.tsx +620 -0
- package/src/components/ManageBookingView.tsx +28 -36
- package/src/components/PhoneInputWithCountry.module.css +131 -0
- package/src/components/PhoneInputWithCountry.tsx +44 -0
- package/src/components/PickupLocationDialog.module.css +360 -0
- package/src/components/PickupLocationDialog.tsx +357 -0
- package/src/components/PickupLocationMap.tsx +110 -0
- package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
- package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
- package/src/components/accordion.css +27 -0
- package/src/components/accordion.tsx +29 -0
- package/src/components/analytics/AnalyticsConsentRestore.tsx +19 -0
- package/src/components/analytics/AnalyticsScripts.tsx +106 -0
- package/src/components/analytics/CookieConsentBanner.css +86 -0
- package/src/components/analytics/CookieConsentBanner.tsx +102 -0
- package/src/components/booking/AddOnsSection.module.css +10 -0
- package/src/components/booking/AddOnsSection.tsx +184 -0
- package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
- package/src/components/booking/BookingDialog.module.css +643 -0
- package/src/components/booking/BookingDialog.tsx +356 -0
- package/src/components/booking/BookingFlow.tsx +4385 -0
- package/src/components/booking/BookingFlowCollage.module.css +148 -0
- package/src/components/booking/BookingFlowCollage.tsx +184 -0
- package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
- package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
- package/src/components/booking/BookingFlowPreview.tsx +51 -0
- package/src/components/booking/BookingProductGrid.module.css +359 -0
- package/src/components/booking/BookingProductGrid.tsx +497 -0
- package/src/components/booking/Calendar.module.css +616 -0
- package/src/components/{Calendar.tsx → booking/Calendar.tsx} +464 -247
- package/src/components/booking/CancellationPolicySelector.module.css +124 -0
- package/src/components/booking/CancellationPolicySelector.tsx +142 -0
- package/src/components/booking/ChangeBookingDialog.tsx +562 -0
- package/src/components/booking/CheckoutForm.module.css +244 -0
- package/src/components/booking/CheckoutForm.tsx +364 -0
- package/src/components/{CheckoutModal.tsx → booking/CheckoutModal.tsx} +176 -19
- package/src/components/booking/DapFlowCollage.tsx +88 -0
- package/src/components/booking/DapTourDescription.tsx +35 -0
- package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
- package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
- package/src/components/booking/InfoTooltip.tsx +108 -0
- package/src/components/booking/ItineraryBox.module.css +258 -0
- package/src/components/booking/ItineraryBox.tsx +550 -0
- package/src/components/{ItineraryBuilder.tsx → booking/ItineraryBuilder.tsx} +1 -2
- package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
- package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
- package/src/components/{MealDrinkAddOnSelector.tsx → booking/MealDrinkAddOnSelector.tsx} +21 -13
- package/src/components/booking/PickupLocationSelector.module.css +124 -0
- package/src/components/{PickupLocationSelector.tsx → booking/PickupLocationSelector.tsx} +315 -290
- package/src/components/booking/PickupTimeSelector.module.css +134 -0
- package/src/components/booking/PickupTimeSelector.tsx +112 -0
- package/src/components/{PriceBreakdown.tsx → booking/PriceBreakdown.tsx} +3 -3
- package/src/components/{PriceSummary.tsx → booking/PriceSummary.tsx} +51 -28
- package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
- package/src/components/booking/PromoCodeInput.module.css +166 -0
- package/src/components/booking/PromoCodeInput.tsx +99 -0
- package/src/components/booking/ReturnTimeSelector.module.css +173 -0
- package/src/components/booking/ReturnTimeSelector.tsx +145 -0
- package/src/components/{TermsAcceptance.tsx → booking/TermsAcceptance.tsx} +9 -8
- package/src/components/booking/TicketSelector.module.css +164 -0
- package/src/components/booking/TicketSelector.tsx +199 -0
- package/src/components/booking/TourDescription.module.css +304 -0
- package/src/components/booking/TourDescription.tsx +273 -0
- package/src/components/booking/booking-flow-ui.ts +15 -1
- package/src/components/booking/booking-flow.css +944 -0
- package/src/components/bottom-sheet.module.css +78 -0
- package/src/components/bottom-sheet.tsx +60 -0
- package/src/components/breadcrumb.module.css +40 -0
- package/src/components/breadcrumb.tsx +36 -0
- package/src/components/button.css +245 -0
- package/src/components/button.tsx +152 -0
- package/src/components/client-bottom-sheet.tsx +14 -0
- package/src/components/colorable-svg.tsx +29 -0
- package/src/components/conditional-footer.tsx +27 -0
- package/src/components/contact-us.module.css +147 -0
- package/src/components/contact-us.tsx +49 -0
- package/src/components/email-signup.css +151 -0
- package/src/components/email-signup.tsx +63 -0
- package/src/components/faq-wrapper.module.css +47 -0
- package/src/components/faq-wrapper.tsx +15 -0
- package/src/components/footer.css +187 -0
- package/src/components/footer.tsx +143 -0
- package/src/components/global-simple-modal.tsx +33 -0
- package/src/components/google-review-summary.module.css +77 -0
- package/src/components/google-review-summary.tsx +50 -0
- package/src/components/hero-image.css +13 -0
- package/src/components/hero-image.tsx +44 -0
- package/src/components/image.css +29 -0
- package/src/components/image.tsx +113 -0
- package/src/components/language-aware-link.tsx +72 -0
- package/src/components/language-switcher.module.css +124 -0
- package/src/components/language-switcher.tsx +75 -0
- package/src/components/map-section.css +59 -0
- package/src/components/map-section.tsx +63 -0
- package/src/components/navbar.module.css +152 -0
- package/src/components/navbar.tsx +125 -0
- package/src/components/parallax-provider.tsx +11 -0
- package/src/components/partner/PartnerBookingPage.module.css +130 -0
- package/src/components/partner/PartnerBookingPage.tsx +390 -0
- package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +19 -35
- package/src/components/product-tag.module.css +30 -0
- package/src/components/product-tag.tsx +34 -0
- package/src/components/product-theme-pages/best-option.module.css +70 -0
- package/src/components/product-theme-pages/best-option.tsx +35 -0
- package/src/components/product-theme-pages/extended-tour-options.module.css +22 -0
- package/src/components/product-theme-pages/extended-tour-options.tsx +11 -0
- package/src/components/product-theme-pages/image-modal.tsx +248 -0
- package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
- package/src/components/product-theme-pages/photo-gallery.tsx +90 -0
- package/src/components/product-theme-pages/product-theme-page-layout.module.css +13 -0
- package/src/components/product-theme-pages/product-theme-page-layout.tsx +67 -0
- package/src/components/product-theme-pages/top-of-fold.module.css +179 -0
- package/src/components/product-theme-pages/top-of-fold.tsx +80 -0
- package/src/components/product-tile/image-only-product-tile-desktop.module.css +106 -0
- package/src/components/product-tile/image-only-product-tile-desktop.tsx +56 -0
- package/src/components/product-tile/image-only-product-tile-mobile.module.css +122 -0
- package/src/components/product-tile/image-only-product-tile-mobile.tsx +89 -0
- package/src/components/product-tile/image-only-product-tile.tsx +44 -0
- package/src/components/product-tile/product-tile-card.module.css +84 -0
- package/src/components/product-tile/product-tile-card.tsx +61 -0
- package/src/components/review-highlights-section.css +85 -0
- package/src/components/review-highlights-section.tsx +127 -0
- package/src/components/season-closure-overlay.module.css +99 -0
- package/src/components/season-closure-overlay.tsx +98 -0
- package/src/components/simple-modal.tsx +69 -0
- package/src/components/simple-top-of-fold.module.css +76 -0
- package/src/components/simple-top-of-fold.tsx +34 -0
- package/src/components/spacer.css +41 -0
- package/src/components/spacer.tsx +23 -0
- package/src/components/star-rating.module.css +74 -0
- package/src/components/star-rating.tsx +48 -0
- package/src/components/terms/TermsContent.tsx +178 -0
- package/src/components/title-subtitle.module.css +10 -0
- package/src/components/title-subtitle.tsx +30 -0
- package/src/components/translatable-reviews.tsx +75 -0
- package/src/components/value-pill.module.css +59 -0
- package/src/components/value-pill.tsx +46 -0
- package/src/components/value-props.css +185 -0
- package/src/components/value-props.tsx +88 -0
- package/src/constants/booking-guide-quiz.ts +64 -0
- package/src/constants/contact-info.ts +2 -0
- package/src/constants/faq.ts +44 -0
- package/src/constants/images.ts +556 -0
- package/src/constants/json-ld/faq-json-ld.tsx +170 -0
- package/src/constants/json-ld/homepage-json-ld.tsx +138 -0
- package/src/constants/json-ld/job-posting-json-ld.tsx +92 -0
- package/src/constants/json-ld/organization-json-ld.tsx +62 -0
- package/src/constants/json-ld/page-json-ld.tsx +6 -0
- package/src/constants/json-ld/product-json-ld.tsx +154 -0
- package/src/constants/json-ld/review-json-ld.tsx +377 -0
- package/src/constants/navigation-links/footer-links.ts +48 -0
- package/src/constants/navigation-links/nav-bar-links.ts +41 -0
- package/src/constants/navigation-links/navigation-link.ts +6 -0
- package/src/constants/pill-values.ts +210 -0
- package/src/constants/products.ts +155 -0
- package/src/constants/quiz-recommendations.ts +506 -0
- package/src/constants/reviews.ts +75 -0
- package/src/constants/staff.ts +197 -0
- package/src/constants/value-props.ts +58 -0
- package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
- package/src/data/dap-descriptions/session-elopements.en.json +60 -0
- package/src/data/dap-descriptions/session-proposals.en.json +60 -0
- package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
- package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
- package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
- package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
- package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
- package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
- package/src/data/product-descriptions/private-tour.en.json +80 -0
- package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
- package/src/data/products-config.json +101 -0
- package/src/hooks/use-bottom-sheet.tsx +15 -0
- package/src/hooks/use-simple-modal.tsx +27 -0
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
- package/src/hooks/useEmailSubscription.tsx +103 -0
- package/src/hooks/useEmbeddedInIframe.ts +16 -0
- package/src/hooks/useIsBookingLaunchLive.ts +49 -0
- package/src/hooks/useQuiz.tsx +210 -0
- package/src/index.ts +27 -2
- package/src/lib/analytics.ts +197 -0
- package/src/lib/booking/booking-source.ts +20 -2
- package/src/lib/{checkout-breakdown.ts → booking/checkout-breakdown.ts} +1 -1
- package/src/lib/booking/correlation-id.ts +46 -0
- package/src/lib/{i18n → booking/i18n}/messages/en.json +48 -4
- package/src/lib/{i18n → booking/i18n}/messages/fr.json +48 -4
- package/src/lib/booking/itinerary-display.ts +36 -0
- package/src/lib/{itinerary-labels.ts → booking/itinerary-labels.ts} +1 -1
- package/src/lib/{location-calculations.ts → booking/location-calculations.ts} +4 -4
- package/src/lib/{location-utils.ts → booking/location-utils.ts} +26 -0
- package/src/lib/{map-utils.ts → booking/map-utils.ts} +3 -3
- package/src/lib/booking/normalize-booking-product-id.ts +7 -0
- package/src/lib/{pickup-location-types.ts → booking/pickup-location-types.ts} +2 -2
- package/src/lib/{pricing.ts → booking/pricing.ts} +2 -2
- package/src/lib/booking/product-option-id.ts +35 -0
- package/src/lib/booking/source-metadata.ts +72 -7
- package/src/lib/booking/sunday-week.ts +14 -0
- package/src/lib/booking/trace-context.ts +62 -0
- package/src/lib/booking-api.ts +1793 -0
- package/src/lib/{constants.ts → booking-constants.ts} +11 -5
- package/src/lib/booking-types.ts +36 -0
- package/src/lib/currency.ts +38 -45
- package/src/lib/dap-descriptions.ts +50 -0
- package/src/lib/dap-itinerary-preview.ts +315 -0
- package/src/lib/dependent-add-on-api.ts +434 -0
- package/src/lib/env.ts +89 -5
- package/src/lib/firebase.ts +20 -0
- package/src/lib/job-application-api.ts +83 -0
- package/src/lib/manage-booking-embed-print.ts +16 -0
- package/src/lib/manage-booking-post-checkout.ts +68 -0
- package/src/lib/photo-dap-config.ts +228 -0
- package/src/lib/pickup/map-utils.ts +56 -0
- package/src/lib/pickup/marker-icons.ts +19 -0
- package/src/lib/product-descriptions.ts +66 -0
- package/src/lib/products-config.ts +73 -0
- package/src/providers/booking-dialog-provider.tsx +107 -38
- package/src/providers/bottom-sheet-provider.tsx +40 -0
- package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
- package/src/radius.css +5 -0
- package/src/spacing.css +7 -0
- package/src/strings/en.json +1774 -0
- package/src/strings/es.json +1573 -0
- package/src/strings/fr.json +1573 -0
- package/src/strings/index.js +23 -0
- package/src/text-style.css +97 -0
- package/src/types/fareharbor.d.ts +12 -0
- package/src/types/quiz.ts +59 -0
- package/src/utils/currency-converter.ts +101 -0
- package/src/components/BookingFlow.tsx +0 -2952
- package/src/components/LanguageSwitcher.tsx +0 -30
- package/src/components/PrivateShuttleBookingFlow.tsx +0 -2290
- package/src/components/ProductList.tsx +0 -78
- package/src/components/WhatsAppPhoneInput.tsx +0 -224
- package/src/components/index.ts +0 -31
- package/src/lib/api.ts +0 -801
- package/src/lib/booking-api-auth.ts +0 -9
- package/src/lib/checkout-breakdown.test.ts +0 -70
- package/src/types/google-maps.d.ts +0 -2
- /package/src/components/{CurrencySwitcher.tsx → booking/CurrencySwitcher.tsx} +0 -0
- /package/src/components/{ErrorBoundary.tsx → booking/ErrorBoundary.tsx} +0 -0
- /package/src/lib/{i18n → booking/i18n}/config.ts +0 -0
- /package/src/lib/{i18n → booking/i18n}/index.tsx +0 -0
- /package/src/lib/{marker-icons.ts → booking/marker-icons.ts} +0 -0
- /package/src/lib/{places-api.ts → booking/places-api.ts} +0 -0
- /package/src/lib/{theme.ts → booking/theme.ts} +0 -0
- /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
|
-
}
|