@sabstravtech/obtservices 0.2.2606091300 → 0.2.2606301620
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/angular/lib/vendor/services/chatbot-car-hire-search.service.d.ts +54 -0
- package/angular/lib/vendor/services/chatbot-flight-search.service.d.ts +53 -0
- package/angular/lib/vendor/services/chatbot-hotel-search.service.d.ts +9 -1
- package/angular/lib/vendor/services/chatbot-lounge-search.service.d.ts +32 -0
- package/angular/lib/vendor/services/chatbot-search-dispatcher.service.d.ts +4 -1
- package/angular/lib/vendor/services/chatbot-taxi-search.service.d.ts +21 -0
- package/angular/lib/vendor/services/chatbot.service.d.ts +21 -0
- package/angular/lib/vendor/services/result-aware.service.d.ts +18 -1
- package/angular/lib/vendor/types/graphql.angular.types.d.ts +0 -8
- package/angular/services.d.ts +2 -0
- package/base/interfaces.d.ts +0 -1
- package/base/lib/vendor/classes/rail-enterprise-search.d.ts +1 -1
- package/base/lib/vendor/interfaces/chatbot.types.d.ts +230 -6
- package/base/lib/vendor/interfaces/rail-basket-item.interface.d.ts +1 -0
- package/base/lib/vendor/services/chatbot.service.d.ts +15 -3
- package/base/lib/vendor/types/graphql.types.d.ts +0 -8
- package/fesm2022/sabstravtech-obtservices-angular.mjs +2116 -245
- package/fesm2022/sabstravtech-obtservices-angular.mjs.map +1 -1
- package/fesm2022/sabstravtech-obtservices-base.mjs +85 -88
- package/fesm2022/sabstravtech-obtservices-base.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { Injectable, Component, Inject, NgModule, Pipe } from '@angular/core';
|
|
3
|
-
import { BaseEnvironment, BaseEventMessenager, BaseHelperRoutines, PaymentMethodType, SchemaFormat, ServiceProvider, BaseModalOpenerService, BaseAcceptNewPriceUpdater, BaseAddItemToBasketUpdater, BaseAddUserToBasketItemUpdater, BaseCreateBasketUpdater, BaseGetBasketFetcher, BaseBookBasketUpdater, BaseDeleteBasketUpdater, BaseRemoveItemFromBasketUpdater, BaseRevalidateBasketUpdater, BaseGetUserFetcher, BaseGetUserBasketsFetcher, BaseSetBasketItemLeadPassengerUpdater, BaseMoveBasketItemToBasket, BaseUpdateBasketNoteUpdater, BaseUpdateBasketTitleUpdater, BaseUserUiConfigsFetcher, BaseUserProductsFetcher, BaseUserMessagesFetcher, BaseGetCompaniesFetcher, BaseGetCompanyFetcher, BaseConfirmMessagesUpdater, BaseSetUserLanguageUpdater, BaseEditUserUpdater, BaseCreateGuestUpdater, BaseGetUserGuestsFetcher, BaseGetOfficeFetcher, BaseRedirectApproverFetcher, BaseAddRedirectApproverUpdater, BaseRemoveRedirectApproverUpdater, BaseStorageService, BaseWebTokenService, BaseSearchCompanyApproversFetcher, BaseAddFavouriteUserUpdater, BaseGetUserFavouriteUsersFetcher, BaseRemoveFavouriteUserUpdater, BaseApexxCreateCardUpdater, BaseApexxDeleteCardUpdater, BaseApexxUpdateCardUpdater, BaseGetApexxListCardsFetcher, BaseSetBasketItemApexxTokenUpdater, BaseGetUserMiFetcher, BaseGetUserDocumentLoyaltyFetcher, BaseGetUserEmergencyContactFetcher, BaseDeleteGuestUpdater, BaseSearchUserWithEmailFetcher, BaseGetOfficeUsersFetcher, BaseRemovePreferredTransportHubUpdater, BaseSetPreferredTransportHubUpdater, BaseGetUserPreferredTransportHubsFetcher, BaseGetUserMiDefaultValuesFetcher, BaseGetUserMiStackFetcher, BaseSetUserMIDefaultValueUpdater, BaseGetEmptyUserMiDefaultValuesFetcher, BaseSetEmptyUserMIDefaultValueUpdater, BaseSearchCompanyUsersFetcher, BaseTrainlineTenantIsFrenchFetcher, BaseUserService, BaseSetPaymentOptionUpdater, BaseGetCancellationInfoFetcher, BaseAddGuestToBasketItemUpdater, BaseAddUpdatedItemToBasketUpdater, BaseCreateBasketNoteUpdater, BaseGetBasketNotesFetcher, BaseGetBasketApprovalTimestampsFetcher, BaseAddBasketItemMarkupUpdater, BaseAddBasketItemCustomRemarkUpdater, BaseGetBasketItemCustomRemarksFetcher, BaseChangeBasketOwnershipUpdater, BaseGetBasketQuoteFetcher, BaseGetProcessTermsPriceFetcher, BaseAcceptProcessTermsUpdater, BaseResendApproverEmailFetcher, BaseCheckIfBasketRequiresApprovalFetcher, BaseGetOfficeApproversFetcher, BaseSelectBasketApproverUpdater, BaseGetDraftBasketsFetcher, BaseCreateBasketFromDraftUpdater, BaseSelectBasketNotifyApproverUpdater, BaseCanAmendBookingFetcher, BaseGetUserApproversFetcher, BaseGetMIApproversFetcher, BaseGetBasketApprovalInfoFetcher, BaseShareBasketUpdater, BaseGetUserSharedBasketsFetcher, BaseGetTrainSeatMapFetcher, BaseBeforeAmendCabSearchFetcher, BaseBeforeAmendCarHireSearchFetcher, BaseSendBackToQueueUpdater, BaseGetBasketCo2InfoFetcher, BaseSelectBasketMultiLevelApproversUpdater, BaseValidateBasketMiFetcher, BaseValidateBasketItemMiFetcher, BaseSetMIValuesUpdater, BaseSearchMIAutoSuggestValuesFetcher, BaseGetTrainSeatmapEvolviFetcher, BaseReserveRailSeatsUpdater, BaseGetEvolviSeatmapsAreEnabledFetcher, BasegetRequestedBookingUpdateFetcher, BaseUpdateExchangeBasketUpdater, BaseDownloadETicketFetcher, BaseEnterpriseBasketService, BaseHotelAvalibilityQuoteFetcher, BaseHotelAvalibilityService, AbstractHttpCallService, BaseSearchAirportsFetcher, BaseSearchAirlinesFetcher, BaseSearchCityFetcher, BaseSearchPostcodeFetcher, BaseSearchRailStationsFetcher, BaseSaveUserAddressUpdater, BaseEditUserAddressUpdater, BaseUserAddressesFetcher, BaseGetUserCompanyOfficesFetcher, BaseSearchHotelChainsFetcher, BaseGetConfermaRoomImagesFetcher, BaseValidateIsPostcodeValidTester, BaseDeleteUserAddressUpdater, BaseDeleteRecentSearchUpdater, BaseSaveRecentSearchUpdater, BaseGetUserCarbonAllowanceFetcher, BaseSaveFavouriteSearchUpdater, BaseGetHotelChainsFetcher, BaseGetRailProvidersFetcher, BaseGetAllAirlinesFetcher, BaseGetCarHireProvidersFetcher, BaseLoungeQuoteFetcher, BaseParkingQuoteFetcher, BaseFlightQuoteFetcher, BaseHotelQuoteFetcher, BaseCarhireQuoteFetcher, BaseCarHireAvailabilityDetailFetcher, BaseCabhireQuoteFetcher, BaseRailQuoteFetcher, BaseIrlQuoteFetcher, BaseSearchDepotFetcher, BaseGetRailStationInfoFetcher, BaseGetRailStationFetcher, BaseGetIrlSupplierStationFetcher, BaseGetRiskAlertsFetcher, BaseSearchConfermaQuicklistFetcher, BaseGetUserRecentSearchesFetcher, BaseGetRailcardsFetcher, BaseGetCovidMicrositeTokenFetcher, BaseEurostarQuoteFetcher, BaseSearchUsersCanBookForFetcher, BaseSearchIrlStationsFetcher, BaseIrlDiscountCardFetcher, BaseGetFlightFareRulesFetcher, BaseGetFlightSeatMapFetcher, BaseGetRailLiveDeparturesFetcher, BaseHotelRulesFetcher, BaseApplyJitFlightRulesFetcher, BaseGetUserFavouriteSearchesFetcher, BaseDeleteFavouriteSearchUpdater, BaseEmailBasketFetcher, BaseFastTrackQuoteFetcher, BaseSendOfflineNotificationFetcher, BaseGenerateBasketPdfFetcher, BaseEmailFerryBookingFetcher, BaseGetFlightExtrasOptionsFetcher, BaseConvertCurrencyFetcher, BaseGetCurrencyConversionRatesFetcher, BaseGetHotelDetailsFetcher, BaseGetServiceFetcher, BaseGetFlightBrandedFaresFetcher, BaseGetTrainlineSearchConfigFetcher, BaseSearchRailInwardFetcher, BaseRailSearchExchangeFetcher, BaseCreateItineraryExchangeUpdater, BaseGetAirAvailabilityFetcher, BaseGetFlightAtNewClassFetcher, BaseGetFlightUpsellOffersFetcher, BaseGetOfficesFetcher, BaseFlightSearchExchangeFetcher, BaseFlightExchangeDetailsFetcher, BaseEmailApartmentBookingFetcher, BaseEmailSeasonTicketBookingFetcher, BaseSuggestPlacesFetcher, BaseGetLatLonFromHereIdFetcher, BaseGetGmtItineraryOptionsFetcher, BaseGetGutCitySuggestionsFetcher, BaseGetGutLocationSuggestionsFetcher, BaseGetRouteHappyFetcher, BaseGetMultipleHotelRatingFetcher, BaseEmailMeetingRoomBookingFetcher, BaseEmailAmbulanceBookingFetcher, BaseGetEntLocationByPostcode, BaseGetEntLocationByCity, BaseGetUserRecentBoltSearchesFetcher, BaseSaveRecentBoltSearchUpdater, BaseGetFerryPortsFetcher, BaseGetBannerFetcher, BaseUpdateDOBUpdater, BaseUpdateFlightSeatMapUpdater, BaseDeleteAllRecentSearchesUpdater, BaseGetOfficeDivisionsFetcher, BaseSearchGeoLocationFetcher, BaseSearchDiscoverLocationFetcher, BaseGetJyrneyUrlFetcher, BaseGetPriceHiddenSabreExchangeFetcher, BaseEnterpriseSearchService, BaseGetLatestVersionsFetcher, BaseLogonService, BaseCancelBookingUpdater, BaseAmendBookingUpdater, BaseCheckForDuplicateBookingsFetcher, BaseEnterpriseMyBookingsService, BaseRouteHappyService, BaseHttpCancelService, BaseMessagesService, RailFareNameKeywords, RailSplitType, isGroupTicket, RailFareTypes, SingleOrReturn,
|
|
3
|
+
import { BaseEnvironment, BaseEventMessenager, BaseHelperRoutines, PaymentMethodType, SchemaFormat, ServiceProvider, BaseModalOpenerService, BaseAcceptNewPriceUpdater, BaseAddItemToBasketUpdater, BaseAddUserToBasketItemUpdater, BaseCreateBasketUpdater, BaseGetBasketFetcher, BaseBookBasketUpdater, BaseDeleteBasketUpdater, BaseRemoveItemFromBasketUpdater, BaseRevalidateBasketUpdater, BaseGetUserFetcher, BaseGetUserBasketsFetcher, BaseSetBasketItemLeadPassengerUpdater, BaseMoveBasketItemToBasket, BaseUpdateBasketNoteUpdater, BaseUpdateBasketTitleUpdater, BaseUserUiConfigsFetcher, BaseUserProductsFetcher, BaseUserMessagesFetcher, BaseGetCompaniesFetcher, BaseGetCompanyFetcher, BaseConfirmMessagesUpdater, BaseSetUserLanguageUpdater, BaseEditUserUpdater, BaseCreateGuestUpdater, BaseGetUserGuestsFetcher, BaseGetOfficeFetcher, BaseRedirectApproverFetcher, BaseAddRedirectApproverUpdater, BaseRemoveRedirectApproverUpdater, BaseStorageService, BaseWebTokenService, BaseSearchCompanyApproversFetcher, BaseAddFavouriteUserUpdater, BaseGetUserFavouriteUsersFetcher, BaseRemoveFavouriteUserUpdater, BaseApexxCreateCardUpdater, BaseApexxDeleteCardUpdater, BaseApexxUpdateCardUpdater, BaseGetApexxListCardsFetcher, BaseSetBasketItemApexxTokenUpdater, BaseGetUserMiFetcher, BaseGetUserDocumentLoyaltyFetcher, BaseGetUserEmergencyContactFetcher, BaseDeleteGuestUpdater, BaseSearchUserWithEmailFetcher, BaseGetOfficeUsersFetcher, BaseRemovePreferredTransportHubUpdater, BaseSetPreferredTransportHubUpdater, BaseGetUserPreferredTransportHubsFetcher, BaseGetUserMiDefaultValuesFetcher, BaseGetUserMiStackFetcher, BaseSetUserMIDefaultValueUpdater, BaseGetEmptyUserMiDefaultValuesFetcher, BaseSetEmptyUserMIDefaultValueUpdater, BaseSearchCompanyUsersFetcher, BaseTrainlineTenantIsFrenchFetcher, BaseUserService, BaseSetPaymentOptionUpdater, BaseGetCancellationInfoFetcher, BaseAddGuestToBasketItemUpdater, BaseAddUpdatedItemToBasketUpdater, BaseCreateBasketNoteUpdater, BaseGetBasketNotesFetcher, BaseGetBasketApprovalTimestampsFetcher, BaseAddBasketItemMarkupUpdater, BaseAddBasketItemCustomRemarkUpdater, BaseGetBasketItemCustomRemarksFetcher, BaseChangeBasketOwnershipUpdater, BaseGetBasketQuoteFetcher, BaseGetProcessTermsPriceFetcher, BaseAcceptProcessTermsUpdater, BaseResendApproverEmailFetcher, BaseCheckIfBasketRequiresApprovalFetcher, BaseGetOfficeApproversFetcher, BaseSelectBasketApproverUpdater, BaseGetDraftBasketsFetcher, BaseCreateBasketFromDraftUpdater, BaseSelectBasketNotifyApproverUpdater, BaseCanAmendBookingFetcher, BaseGetUserApproversFetcher, BaseGetMIApproversFetcher, BaseGetBasketApprovalInfoFetcher, BaseShareBasketUpdater, BaseGetUserSharedBasketsFetcher, BaseGetTrainSeatMapFetcher, BaseBeforeAmendCabSearchFetcher, BaseBeforeAmendCarHireSearchFetcher, BaseSendBackToQueueUpdater, BaseGetBasketCo2InfoFetcher, BaseSelectBasketMultiLevelApproversUpdater, BaseValidateBasketMiFetcher, BaseValidateBasketItemMiFetcher, BaseSetMIValuesUpdater, BaseSearchMIAutoSuggestValuesFetcher, BaseGetTrainSeatmapEvolviFetcher, BaseReserveRailSeatsUpdater, BaseGetEvolviSeatmapsAreEnabledFetcher, BasegetRequestedBookingUpdateFetcher, BaseUpdateExchangeBasketUpdater, BaseDownloadETicketFetcher, BaseEnterpriseBasketService, BaseHotelAvalibilityQuoteFetcher, BaseHotelAvalibilityService, AbstractHttpCallService, BaseSearchAirportsFetcher, BaseSearchAirlinesFetcher, BaseSearchCityFetcher, BaseSearchPostcodeFetcher, BaseSearchRailStationsFetcher, BaseSaveUserAddressUpdater, BaseEditUserAddressUpdater, BaseUserAddressesFetcher, BaseGetUserCompanyOfficesFetcher, BaseSearchHotelChainsFetcher, BaseGetConfermaRoomImagesFetcher, BaseValidateIsPostcodeValidTester, BaseDeleteUserAddressUpdater, BaseDeleteRecentSearchUpdater, BaseSaveRecentSearchUpdater, BaseGetUserCarbonAllowanceFetcher, BaseSaveFavouriteSearchUpdater, BaseGetHotelChainsFetcher, BaseGetRailProvidersFetcher, BaseGetAllAirlinesFetcher, BaseGetCarHireProvidersFetcher, BaseLoungeQuoteFetcher, BaseParkingQuoteFetcher, BaseFlightQuoteFetcher, BaseHotelQuoteFetcher, BaseCarhireQuoteFetcher, BaseCarHireAvailabilityDetailFetcher, BaseCabhireQuoteFetcher, BaseRailQuoteFetcher, BaseIrlQuoteFetcher, BaseSearchDepotFetcher, BaseGetRailStationInfoFetcher, BaseGetRailStationFetcher, BaseGetIrlSupplierStationFetcher, BaseGetRiskAlertsFetcher, BaseSearchConfermaQuicklistFetcher, BaseGetUserRecentSearchesFetcher, BaseGetRailcardsFetcher, BaseGetCovidMicrositeTokenFetcher, BaseEurostarQuoteFetcher, BaseSearchUsersCanBookForFetcher, BaseSearchIrlStationsFetcher, BaseIrlDiscountCardFetcher, BaseGetFlightFareRulesFetcher, BaseGetFlightSeatMapFetcher, BaseGetRailLiveDeparturesFetcher, BaseHotelRulesFetcher, BaseApplyJitFlightRulesFetcher, BaseGetUserFavouriteSearchesFetcher, BaseDeleteFavouriteSearchUpdater, BaseEmailBasketFetcher, BaseFastTrackQuoteFetcher, BaseSendOfflineNotificationFetcher, BaseGenerateBasketPdfFetcher, BaseEmailFerryBookingFetcher, BaseGetFlightExtrasOptionsFetcher, BaseConvertCurrencyFetcher, BaseGetCurrencyConversionRatesFetcher, BaseGetHotelDetailsFetcher, BaseGetServiceFetcher, BaseGetFlightBrandedFaresFetcher, BaseGetTrainlineSearchConfigFetcher, BaseSearchRailInwardFetcher, BaseRailSearchExchangeFetcher, BaseCreateItineraryExchangeUpdater, BaseGetAirAvailabilityFetcher, BaseGetFlightAtNewClassFetcher, BaseGetFlightUpsellOffersFetcher, BaseGetOfficesFetcher, BaseFlightSearchExchangeFetcher, BaseFlightExchangeDetailsFetcher, BaseEmailApartmentBookingFetcher, BaseEmailSeasonTicketBookingFetcher, BaseSuggestPlacesFetcher, BaseGetLatLonFromHereIdFetcher, BaseGetGmtItineraryOptionsFetcher, BaseGetGutCitySuggestionsFetcher, BaseGetGutLocationSuggestionsFetcher, BaseGetRouteHappyFetcher, BaseGetMultipleHotelRatingFetcher, BaseEmailMeetingRoomBookingFetcher, BaseEmailAmbulanceBookingFetcher, BaseGetEntLocationByPostcode, BaseGetEntLocationByCity, BaseGetUserRecentBoltSearchesFetcher, BaseSaveRecentBoltSearchUpdater, BaseGetFerryPortsFetcher, BaseGetBannerFetcher, BaseUpdateDOBUpdater, BaseUpdateFlightSeatMapUpdater, BaseDeleteAllRecentSearchesUpdater, BaseGetOfficeDivisionsFetcher, BaseSearchGeoLocationFetcher, BaseSearchDiscoverLocationFetcher, BaseGetJyrneyUrlFetcher, BaseGetPriceHiddenSabreExchangeFetcher, BaseEnterpriseSearchService, BaseGetLatestVersionsFetcher, BaseLogonService, BaseCancelBookingUpdater, BaseAmendBookingUpdater, BaseCheckForDuplicateBookingsFetcher, BaseEnterpriseMyBookingsService, BaseRouteHappyService, BaseHttpCancelService, BaseMessagesService, RailFareNameKeywords, RailSplitType, isGroupTicket, RailFareTypes, SingleOrReturn, makeFareRules, FlightDirectionEnum, FlightSearchType, BaseChatbotService, UserFavorurite, RailSearchCriteria, TimeWindow, LocationTypes, EurostarSearchType, ModalTypes, BaseGetMiRequiringMandatoryDefaultValueFetcher, BaseHttpCancelInterceptor } from '@sabstravtech/obtservices/base';
|
|
4
4
|
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
|
5
5
|
import * as i1 from 'apollo-angular';
|
|
6
6
|
import { gql } from 'apollo-angular';
|
|
@@ -8340,11 +8340,6 @@ const GetProcessTermsPriceDocument = gql `
|
|
|
8340
8340
|
) {
|
|
8341
8341
|
amount
|
|
8342
8342
|
currency
|
|
8343
|
-
taxItemsWithoutCode {
|
|
8344
|
-
name
|
|
8345
|
-
amount
|
|
8346
|
-
currency
|
|
8347
|
-
}
|
|
8348
8343
|
}
|
|
8349
8344
|
}
|
|
8350
8345
|
`;
|
|
@@ -17529,6 +17524,12 @@ class ResultAwareService {
|
|
|
17529
17524
|
currentServiceType = null;
|
|
17530
17525
|
railSourceId = null;
|
|
17531
17526
|
railReturnJourneys = [];
|
|
17527
|
+
/** Raw flight itineraries per leg, kept so the chat cards' "Fare Rules" links
|
|
17528
|
+
* can resolve a fare back to its underlying journey. Outbound holds the
|
|
17529
|
+
* outbound + combined-return journeys; inbound holds dual-single return
|
|
17530
|
+
* journeys (priced separately). */
|
|
17531
|
+
flightOutboundRaw = [];
|
|
17532
|
+
flightInboundRaw = [];
|
|
17532
17533
|
resultsAvailableSubject = new BehaviorSubject(false);
|
|
17533
17534
|
resultsAvailable$ = this.resultsAvailableSubject.asObservable();
|
|
17534
17535
|
constructor(searchService, basketService, router) {
|
|
@@ -17570,6 +17571,24 @@ class ResultAwareService {
|
|
|
17570
17571
|
this.condensedResults = this.condenseHotelResults(results);
|
|
17571
17572
|
this.resultsAvailableSubject.next(this.condensedResults.length > 0);
|
|
17572
17573
|
}
|
|
17574
|
+
captureCarHireResults(results) {
|
|
17575
|
+
this.currentServiceType = ServiceType.Car;
|
|
17576
|
+
this.rawResults = results;
|
|
17577
|
+
this.resultsAvailableSubject.next(results.length > 0);
|
|
17578
|
+
}
|
|
17579
|
+
async selectCarHire(cardIndex) {
|
|
17580
|
+
const result = this.rawResults[cardIndex];
|
|
17581
|
+
if (!result) {
|
|
17582
|
+
return;
|
|
17583
|
+
}
|
|
17584
|
+
// const searchParams = this.searchService.searches[ServiceType.Car] as any;
|
|
17585
|
+
// await searchParams.addCarHireToBasket(result, searchParams.getSearchQuery());
|
|
17586
|
+
// this.basketService.toggleMenu();
|
|
17587
|
+
const basket = (await this.basketService.getDefaultBasket())?.subject.value;
|
|
17588
|
+
if (basket?.id) {
|
|
17589
|
+
this.router.navigate(['/itinerary', basket.id]);
|
|
17590
|
+
}
|
|
17591
|
+
}
|
|
17573
17592
|
isHotelSearch() {
|
|
17574
17593
|
return this.currentServiceType === ServiceType.Hotel;
|
|
17575
17594
|
}
|
|
@@ -17640,6 +17659,39 @@ class ResultAwareService {
|
|
|
17640
17659
|
return;
|
|
17641
17660
|
await searchParams.openRailTicketDetailsDialog(journey, foundFare, foundFare.terms, foundFare.identifiers, this.railSourceId || '');
|
|
17642
17661
|
}
|
|
17662
|
+
/** Store the raw flight journeys for each leg so the chat cards can open fare
|
|
17663
|
+
* rules. Called by the chat flight-search service after a search/re-rank. */
|
|
17664
|
+
setFlightRawLegs(outbound, inbound) {
|
|
17665
|
+
this.flightOutboundRaw = outbound || [];
|
|
17666
|
+
this.flightInboundRaw = inbound || [];
|
|
17667
|
+
}
|
|
17668
|
+
/**
|
|
17669
|
+
* Open the existing OBT flight "Fare Rules" dialog for a fare displayed in the
|
|
17670
|
+
* chat — mirrors the results page's fares-table behaviour (makeFareRules +
|
|
17671
|
+
* openFlightFareRules) so the user sees the same conditions modal.
|
|
17672
|
+
*/
|
|
17673
|
+
async openFlightFareDetails(ticket) {
|
|
17674
|
+
if (!ticket || ticket.journeyIndex == null)
|
|
17675
|
+
return;
|
|
17676
|
+
const journeys = ticket.isReturnLeg ? this.flightInboundRaw : this.flightOutboundRaw;
|
|
17677
|
+
const journey = journeys?.[ticket.journeyIndex];
|
|
17678
|
+
if (!journey)
|
|
17679
|
+
return;
|
|
17680
|
+
const searchParams = this.searchService.searches[ServiceType.Flight];
|
|
17681
|
+
if (typeof searchParams?.openFlightFareRules !== 'function')
|
|
17682
|
+
return;
|
|
17683
|
+
const { rules, flights } = makeFareRules(journey, FlightDirectionEnum.Outbound);
|
|
17684
|
+
const isReturn = (journey.inboundFlights?.length ?? 0) > 0;
|
|
17685
|
+
let inboundRules = null;
|
|
17686
|
+
let inboundFlights = null;
|
|
17687
|
+
if (isReturn) {
|
|
17688
|
+
const inbound = makeFareRules(journey, FlightDirectionEnum.Inbound);
|
|
17689
|
+
inboundRules = inbound.rules;
|
|
17690
|
+
inboundFlights = inbound.flights;
|
|
17691
|
+
}
|
|
17692
|
+
const flightDetail = isReturn ? journey : null;
|
|
17693
|
+
searchParams.openFlightFareRules(rules, flights, journey.sourceId, { centered: true, size: 'xl' }, null, flightDetail, inboundRules, inboundFlights);
|
|
17694
|
+
}
|
|
17643
17695
|
collectFareTickets(journey, sections, journeyIndex, isReturnLeg) {
|
|
17644
17696
|
const tickets = [];
|
|
17645
17697
|
for (const section of sections) {
|
|
@@ -17959,6 +18011,10 @@ class ResultAwareService {
|
|
|
17959
18011
|
let condensedReturns = [];
|
|
17960
18012
|
for (let i = 0; i < returnJourneys.length; i++) {
|
|
17961
18013
|
const rj = returnJourneys[i];
|
|
18014
|
+
// Cancelled services have no selectable fares; omit them from the results.
|
|
18015
|
+
if (rj.cancelled) {
|
|
18016
|
+
continue;
|
|
18017
|
+
}
|
|
17962
18018
|
const cheapestFare = this.getCheapestRailFare(rj);
|
|
17963
18019
|
const expensiveFare = this.getMostExpensiveRailFare(rj);
|
|
17964
18020
|
const cheapestValid = cheapestFare &&
|
|
@@ -17970,6 +18026,8 @@ class ResultAwareService {
|
|
|
17970
18026
|
index: i,
|
|
17971
18027
|
depart: this.formatTime(rj.departDateTime),
|
|
17972
18028
|
arrive: this.formatTime(rj.arriveDateTime),
|
|
18029
|
+
departDateTime: rj.departDateTime,
|
|
18030
|
+
arriveDateTime: rj.arriveDateTime,
|
|
17973
18031
|
stops: parseInt(rj.changes || '0', 10),
|
|
17974
18032
|
duration: this.computeRailDuration(rj),
|
|
17975
18033
|
price: cheapestValid ? cheapestFare.price : 0,
|
|
@@ -17986,6 +18044,8 @@ class ResultAwareService {
|
|
|
17986
18044
|
index: i,
|
|
17987
18045
|
depart: this.formatTime(rj.departDateTime),
|
|
17988
18046
|
arrive: this.formatTime(rj.arriveDateTime),
|
|
18047
|
+
departDateTime: rj.departDateTime,
|
|
18048
|
+
arriveDateTime: rj.arriveDateTime,
|
|
17989
18049
|
stops: parseInt(rj.changes || '0', 10),
|
|
17990
18050
|
duration: this.computeRailDuration(rj),
|
|
17991
18051
|
price: expensiveValid ? expensiveFare.price : 0,
|
|
@@ -18001,6 +18061,10 @@ class ResultAwareService {
|
|
|
18001
18061
|
const parsdJourneys = [];
|
|
18002
18062
|
for (let i = 0; i < journeys.length; i++) {
|
|
18003
18063
|
const journey = journeys[i];
|
|
18064
|
+
// Cancelled services have no selectable fares; omit them from the results.
|
|
18065
|
+
if (journey.cancelled) {
|
|
18066
|
+
continue;
|
|
18067
|
+
}
|
|
18004
18068
|
const cheapestFare = this.getCheapestRailFare(journey);
|
|
18005
18069
|
const hasValidFare = cheapestFare && cheapestFare.price > 0;
|
|
18006
18070
|
const result = {
|
|
@@ -18008,6 +18072,8 @@ class ResultAwareService {
|
|
|
18008
18072
|
operator: journey.journeyLegs?.[0]?.operatorName || '',
|
|
18009
18073
|
depart: this.formatTime(journey.departDateTime),
|
|
18010
18074
|
arrive: this.formatTime(journey.arriveDateTime),
|
|
18075
|
+
departDateTime: journey.departDateTime,
|
|
18076
|
+
arriveDateTime: journey.arriveDateTime,
|
|
18011
18077
|
stops: parseInt(journey.changes || '0', 10),
|
|
18012
18078
|
duration: this.computeRailDuration(journey),
|
|
18013
18079
|
price: hasValidFare ? cheapestFare.price : 0,
|
|
@@ -18015,7 +18081,7 @@ class ResultAwareService {
|
|
|
18015
18081
|
co2: journey.co2PerPassenger || 0,
|
|
18016
18082
|
policyStatus: hasValidFare ? this.derivePolicyStatus(cheapestFare) : 'UNAVAILABLE',
|
|
18017
18083
|
policyMessages: hasValidFare ? this.derivePolicyMessages(cheapestFare) : [],
|
|
18018
|
-
singleOrReturn: cheapestFare
|
|
18084
|
+
singleOrReturn: cheapestFare?.singleOrReturn
|
|
18019
18085
|
};
|
|
18020
18086
|
// Attach return options so the LLM can select both legs
|
|
18021
18087
|
if (condensedReturns.length > 0) {
|
|
@@ -18030,6 +18096,8 @@ class ResultAwareService {
|
|
|
18030
18096
|
operator: journey.journeyLegs?.[0]?.operatorName || '',
|
|
18031
18097
|
depart: this.formatTime(journey.departDateTime),
|
|
18032
18098
|
arrive: this.formatTime(journey.arriveDateTime),
|
|
18099
|
+
departDateTime: journey.departDateTime,
|
|
18100
|
+
arriveDateTime: journey.arriveDateTime,
|
|
18033
18101
|
stops: parseInt(journey.changes || '0', 10),
|
|
18034
18102
|
duration: this.computeRailDuration(journey),
|
|
18035
18103
|
price: expensiveValid ? expensiveFare.price : 0,
|
|
@@ -18037,7 +18105,7 @@ class ResultAwareService {
|
|
|
18037
18105
|
co2: journey.co2PerPassenger || 0,
|
|
18038
18106
|
policyStatus: expensiveValid ? this.derivePolicyStatus(expensiveFare) : 'UNAVAILABLE',
|
|
18039
18107
|
policyMessages: expensiveValid ? this.derivePolicyMessages(expensiveFare) : [],
|
|
18040
|
-
singleOrReturn: expensiveFare
|
|
18108
|
+
singleOrReturn: expensiveFare?.singleOrReturn
|
|
18041
18109
|
};
|
|
18042
18110
|
// console.log(`[ResultAwareService] - Condensed Returns: ${condensedReturns.length}`);
|
|
18043
18111
|
// Attach return options so the LLM can select both legs
|
|
@@ -18085,18 +18153,28 @@ class ResultAwareService {
|
|
|
18085
18153
|
ret.price = 0;
|
|
18086
18154
|
});
|
|
18087
18155
|
}
|
|
18088
|
-
//
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
|
|
18092
|
-
|
|
18156
|
+
// Remove any returns which depart before this outbound arrives. Prefer the
|
|
18157
|
+
// full ISO datetimes so a return on a later calendar day isn't dropped just
|
|
18158
|
+
// because its time-of-day is earlier than the outbound arrival (e.g. arrive
|
|
18159
|
+
// 17:44 day 1, return departs 16:14 day 2). Fall back to a time-of-day
|
|
18160
|
+
// comparison when a datetime is missing (same-day / open returns).
|
|
18161
|
+
const timeOfDay = (time) => {
|
|
18162
|
+
const [hour, minute] = time.split(':');
|
|
18163
|
+
return moment$1().hour(Number(hour)).minute(Number(minute)).second(0).millisecond(0);
|
|
18164
|
+
};
|
|
18165
|
+
const departsAfterArrival = (ret) => {
|
|
18166
|
+
if (result.arriveDateTime != null && ret.departDateTime != null) {
|
|
18167
|
+
const arrival = moment$1(result.arriveDateTime);
|
|
18168
|
+
const depart = moment$1(ret.departDateTime);
|
|
18169
|
+
if (arrival.isValid() && depart.isValid()) {
|
|
18170
|
+
return depart.isAfter(arrival);
|
|
18171
|
+
}
|
|
18172
|
+
}
|
|
18173
|
+
return timeOfDay(ret.depart).isAfter(timeOfDay(result.arrive));
|
|
18174
|
+
};
|
|
18093
18175
|
condensedReturns = condensedReturns.filter((ret) => {
|
|
18094
18176
|
try {
|
|
18095
|
-
|
|
18096
|
-
const retDepartTime = moment$1();
|
|
18097
|
-
retDepartTime.hour(departTimeSplit[0]);
|
|
18098
|
-
retDepartTime.minute(departTimeSplit[1]);
|
|
18099
|
-
return retDepartTime.isAfter(arrivalDate);
|
|
18177
|
+
return departsAfterArrival(ret);
|
|
18100
18178
|
}
|
|
18101
18179
|
catch (error) {
|
|
18102
18180
|
return false;
|
|
@@ -18203,6 +18281,11 @@ class ChatbotService extends BaseChatbotService {
|
|
|
18203
18281
|
/** Back-compat boolean stream for callers that only care whether a search is in flight. */
|
|
18204
18282
|
searching$ = this.searchingSubject.pipe(map((s) => s.searching));
|
|
18205
18283
|
fillSearchForm$ = new Subject();
|
|
18284
|
+
/** Fires after the car hire chatbot handler has finished populating city/depot params,
|
|
18285
|
+
* so the component can call updateLocation() to populate the depot dropdown options. */
|
|
18286
|
+
carHireParamsReady$ = new Subject();
|
|
18287
|
+
/** Re-rank already-cached results (no new search) — routed to the handler's resort(). */
|
|
18288
|
+
resortResults$ = new Subject();
|
|
18206
18289
|
riskFlagSubject = new BehaviorSubject(null);
|
|
18207
18290
|
riskFlag$ = this.riskFlagSubject.asObservable();
|
|
18208
18291
|
faqsSubject = new BehaviorSubject([]);
|
|
@@ -18266,6 +18349,9 @@ class ChatbotService extends BaseChatbotService {
|
|
|
18266
18349
|
notifyFillForm(parameters, autoSearch) {
|
|
18267
18350
|
this.fillSearchForm$.next({ parameters, autoSearch });
|
|
18268
18351
|
}
|
|
18352
|
+
notifyResort(parameters) {
|
|
18353
|
+
this.resortResults$.next({ parameters });
|
|
18354
|
+
}
|
|
18269
18355
|
notifyRiskFlag(flag) {
|
|
18270
18356
|
this.riskFlagSubject.next(flag);
|
|
18271
18357
|
}
|
|
@@ -18294,20 +18380,58 @@ class ChatbotService extends BaseChatbotService {
|
|
|
18294
18380
|
return 'web';
|
|
18295
18381
|
}
|
|
18296
18382
|
getUserCapabilities() {
|
|
18383
|
+
const flightDefaults = this.userService.getUserFavoriteObject(UserFavorurite.FlightSearchDefaults) || {};
|
|
18297
18384
|
return {
|
|
18298
18385
|
isCrown: this.userService.crownIsSet(),
|
|
18299
18386
|
isAgent: this.userService.userIsAgent(),
|
|
18300
18387
|
uiConfigs: {
|
|
18301
|
-
hideHotelSearchOptions: this.userService.isUserFavoriteSet(new UserFavorurite('hideHotelSearchOptions'))
|
|
18388
|
+
hideHotelSearchOptions: this.userService.isUserFavoriteSet(new UserFavorurite('hideHotelSearchOptions')),
|
|
18389
|
+
allowedFlightCabins: this.resolveAllowedFlightCabins(flightDefaults.classSelection),
|
|
18390
|
+
flightAnyCabinAllowed: flightDefaults.classSelection
|
|
18391
|
+
? flightDefaults.classSelection.any !== true
|
|
18392
|
+
: true,
|
|
18393
|
+
flightMultiCityEnabled: flightDefaults.enableMultiCityForFlight === true,
|
|
18394
|
+
flightMultiCityMaxLegs: flightDefaults.multiCityMaxSelectors
|
|
18395
|
+
? Number(flightDefaults.multiCityMaxSelectors)
|
|
18396
|
+
: undefined,
|
|
18397
|
+
hideFlightVia: this.userService.isUserFavoriteSet(new UserFavorurite('HideFlightVia')),
|
|
18398
|
+
flightMinTimeFlexibilityHours: flightDefaults.setMinimumTimeWindow
|
|
18399
|
+
? Number(flightDefaults.setMinimumTimeWindow)
|
|
18400
|
+
: undefined
|
|
18302
18401
|
}
|
|
18303
18402
|
};
|
|
18304
18403
|
}
|
|
18404
|
+
/**
|
|
18405
|
+
* Translate the flight search form's `classSelection` config (where a `true`
|
|
18406
|
+
* value HIDES a cabin) into the list of cabin classes the user is allowed to
|
|
18407
|
+
* search/book, in the cabin enum the chatbot/AI uses. Returns undefined when
|
|
18408
|
+
* no class is hidden (i.e. no restriction).
|
|
18409
|
+
*/
|
|
18410
|
+
resolveAllowedFlightCabins(classSelection) {
|
|
18411
|
+
if (!classSelection) {
|
|
18412
|
+
return undefined;
|
|
18413
|
+
}
|
|
18414
|
+
const map = [
|
|
18415
|
+
['economy', 'Economy'],
|
|
18416
|
+
['premium', 'PremiumEconomy'],
|
|
18417
|
+
['business', 'Business'],
|
|
18418
|
+
['first', 'First']
|
|
18419
|
+
];
|
|
18420
|
+
const allowed = map.filter(([key]) => classSelection[key] !== true).map(([, label]) => label);
|
|
18421
|
+
return allowed.length < map.length ? allowed : undefined;
|
|
18422
|
+
}
|
|
18423
|
+
getUserFirstName() {
|
|
18424
|
+
return this.userService.fullUserDetails.allDetails.value?.forename ?? undefined;
|
|
18425
|
+
}
|
|
18305
18426
|
// ── Public override ──────────────────────────────────────────────────────
|
|
18427
|
+
/** Set by ChatbotCarHireSearchService when the user is choosing from a depot list. */
|
|
18428
|
+
depotSelectionHandler = null;
|
|
18306
18429
|
connect() {
|
|
18307
|
-
|
|
18308
|
-
|
|
18309
|
-
|
|
18310
|
-
|
|
18430
|
+
// TODO: restore guard → check aiTravelAssistantEnabled
|
|
18431
|
+
// const user = this.userService.fullUserDetails.allDetails.value;
|
|
18432
|
+
// if (!(user as any)?.aiTravelAssistantEnabled) {
|
|
18433
|
+
// return;
|
|
18434
|
+
// }
|
|
18311
18435
|
super.connect();
|
|
18312
18436
|
}
|
|
18313
18437
|
clearSession() {
|
|
@@ -18317,14 +18441,39 @@ class ChatbotService extends BaseChatbotService {
|
|
|
18317
18441
|
injectMessage(message) {
|
|
18318
18442
|
this.addMessage(message);
|
|
18319
18443
|
}
|
|
18444
|
+
/** Remove the last assistant message from the UI (e.g. when it's been superseded). */
|
|
18445
|
+
retractLastAssistantMessage() {
|
|
18446
|
+
this.removeLastAssistantMessage();
|
|
18447
|
+
}
|
|
18448
|
+
async beforeEmit(text) {
|
|
18449
|
+
if (this.depotSelectionHandler) {
|
|
18450
|
+
const handled = await this.depotSelectionHandler(text);
|
|
18451
|
+
if (handled)
|
|
18452
|
+
return true;
|
|
18453
|
+
}
|
|
18454
|
+
return false;
|
|
18455
|
+
}
|
|
18320
18456
|
handleResponse(env) {
|
|
18321
18457
|
super.handleResponse(env);
|
|
18458
|
+
const tt = env.parameters?.travelType;
|
|
18459
|
+
// Re-rank the already-cached results (cheapest/fastest/nearest-time) without
|
|
18460
|
+
// a new backend search. The handler reads the session's cached itineraries
|
|
18461
|
+
// and re-renders the cards.
|
|
18462
|
+
if (env.action === 'sort_results' && tt === 'flight') {
|
|
18463
|
+
this.notifyResort(env.parameters);
|
|
18464
|
+
return;
|
|
18465
|
+
}
|
|
18322
18466
|
// Auto-trigger a search as soon as the bot signals parameters are complete.
|
|
18323
18467
|
// The dispatcher routes to the travel-type handler, which owns the search +
|
|
18324
18468
|
// result-capture flow.
|
|
18325
|
-
const tt = env.parameters?.travelType;
|
|
18326
18469
|
if (env.state === 'PARAMS_COMPLETE' &&
|
|
18327
|
-
(tt === 'rail' ||
|
|
18470
|
+
(tt === 'rail' ||
|
|
18471
|
+
tt === 'flight' ||
|
|
18472
|
+
tt === 'hotel' ||
|
|
18473
|
+
tt === 'car-hire' ||
|
|
18474
|
+
tt === 'eurostar' ||
|
|
18475
|
+
tt === 'lounge' ||
|
|
18476
|
+
tt === 'taxi')) {
|
|
18328
18477
|
this.notifyFillForm(env.parameters, true);
|
|
18329
18478
|
}
|
|
18330
18479
|
}
|
|
@@ -18440,8 +18589,9 @@ class ChatbotRailSearchService {
|
|
|
18440
18589
|
return;
|
|
18441
18590
|
}
|
|
18442
18591
|
const railResults = railSearch.results.value;
|
|
18443
|
-
|
|
18444
|
-
|
|
18592
|
+
const resultIndex = parameters.isEuRail ? (railResults?.[1] ? 1 : 0) : 0;
|
|
18593
|
+
if (railResults?.[resultIndex]) {
|
|
18594
|
+
this.resultAwareService.captureRailResults(railResults[resultIndex]);
|
|
18445
18595
|
}
|
|
18446
18596
|
else {
|
|
18447
18597
|
console.warn('[ChatbotRailSearchService] no rail results to capture (startSearch returned', ok, ')');
|
|
@@ -18575,6 +18725,7 @@ class ChatbotRailSearchService {
|
|
|
18575
18725
|
// ── Param population ─────────────────────────────────────────────────────
|
|
18576
18726
|
async populateRailParams(parameters, departNgbDate) {
|
|
18577
18727
|
const railParams = this.searchService.searches[ServiceType.Rail];
|
|
18728
|
+
railParams.changeLocation(parameters.isEuRail ? 'EU' : 'UK');
|
|
18578
18729
|
railParams.outbound_date = departNgbDate;
|
|
18579
18730
|
if (parameters.departureTime) {
|
|
18580
18731
|
railParams.outbound_time = this.normalizeTime(parameters.departureTime);
|
|
@@ -18793,6 +18944,39 @@ function extractJourneys(results) {
|
|
|
18793
18944
|
}
|
|
18794
18945
|
const FLIGHT_RESULTS_WAIT_MS = 90_000;
|
|
18795
18946
|
const MAX_FLIGHT_CARDS = 3;
|
|
18947
|
+
/**
|
|
18948
|
+
* Order raw itineraries by the SAME "Preferred" ranking the normal results
|
|
18949
|
+
* page applies by default (FlightEnterpriseSearch._sortItinaryRoutines
|
|
18950
|
+
* [FlightSortTypes.Preferred]): highest `preferenceLevel` first, then the
|
|
18951
|
+
* `preferred` flag, otherwise stable. Replicated here (not imported) so the
|
|
18952
|
+
* chatbot's default cards are the top of the same list the user would see on
|
|
18953
|
+
* the results page — a headless chatbot search never mounts the FlightSortBy
|
|
18954
|
+
* component that would otherwise set this default, so `results.value` arrives
|
|
18955
|
+
* in raw supplier order and must be ordered explicitly.
|
|
18956
|
+
*/
|
|
18957
|
+
function sortByPreferred(list) {
|
|
18958
|
+
return [...list].sort((a, b) => {
|
|
18959
|
+
const aLevel = a?.preferenceLevel ?? 0;
|
|
18960
|
+
const bLevel = b?.preferenceLevel ?? 0;
|
|
18961
|
+
if (aLevel && bLevel) {
|
|
18962
|
+
if (aLevel > bLevel)
|
|
18963
|
+
return -1;
|
|
18964
|
+
if (aLevel < bLevel)
|
|
18965
|
+
return 1;
|
|
18966
|
+
}
|
|
18967
|
+
else if (aLevel && !bLevel) {
|
|
18968
|
+
return -1;
|
|
18969
|
+
}
|
|
18970
|
+
else if (!aLevel && bLevel) {
|
|
18971
|
+
return 1;
|
|
18972
|
+
}
|
|
18973
|
+
if (a?.preferred && !b?.preferred)
|
|
18974
|
+
return -1;
|
|
18975
|
+
if (!a?.preferred && b?.preferred)
|
|
18976
|
+
return 1;
|
|
18977
|
+
return 0;
|
|
18978
|
+
});
|
|
18979
|
+
}
|
|
18796
18980
|
/**
|
|
18797
18981
|
* Handles chatbot-driven flight searches. Registered with
|
|
18798
18982
|
* `ChatbotSearchDispatcherService`; not used directly.
|
|
@@ -18809,7 +18993,20 @@ class ChatbotFlightSearchService {
|
|
|
18809
18993
|
resultAwareService;
|
|
18810
18994
|
userService;
|
|
18811
18995
|
travelType = 'flight';
|
|
18996
|
+
/** The flight form exposes four preferred-carrier slots. */
|
|
18997
|
+
static MAX_PREFERRED_CARRIERS = 4;
|
|
18998
|
+
/** Terms that mean "no airline preference" and should be ignored. */
|
|
18999
|
+
static NO_CARRIER_PREFERENCE = new Set(['', 'any', 'all']);
|
|
19000
|
+
/** Regional/subsidiary markers, so "KLM" doesn't resolve to "KLM Cityhopper". */
|
|
19001
|
+
static CARRIER_SUBSIDIARY = /\b(cityhopper|cityline|cityjet|express|regional|connect|shuttle|airlink|link|commuter)\b/i;
|
|
18812
19002
|
resultsSubscription = null;
|
|
19003
|
+
/**
|
|
19004
|
+
* The most recent full search parameters (origin, destination, dates,
|
|
19005
|
+
* cabin, etc.). Cached so a {@link resort} call — whose own parameters carry
|
|
19006
|
+
* only the new ranking intent (sortType / time target) — can still render
|
|
19007
|
+
* card headers and dates for the trip that produced the cached results.
|
|
19008
|
+
*/
|
|
19009
|
+
lastSearchParameters = null;
|
|
18813
19010
|
constructor(chatbotService, searchService, resultAwareService, userService) {
|
|
18814
19011
|
this.chatbotService = chatbotService;
|
|
18815
19012
|
this.searchService = searchService;
|
|
@@ -18817,6 +19014,7 @@ class ChatbotFlightSearchService {
|
|
|
18817
19014
|
this.userService = userService;
|
|
18818
19015
|
}
|
|
18819
19016
|
async handle(parameters, autoSearch) {
|
|
19017
|
+
this.lastSearchParameters = parameters;
|
|
18820
19018
|
this.searchService.resetSearchChosenObjects();
|
|
18821
19019
|
this.searchService.resetSearchPriorities();
|
|
18822
19020
|
this.searchService.search_objects[ServiceType.Flight].chosen = true;
|
|
@@ -18936,127 +19134,342 @@ class ChatbotFlightSearchService {
|
|
|
18936
19134
|
return EMPTY;
|
|
18937
19135
|
}))
|
|
18938
19136
|
.subscribe(() => {
|
|
18939
|
-
|
|
18940
|
-
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
|
|
19137
|
+
this.buildAndPatchResultsFromCache(parameters);
|
|
19138
|
+
this.chatbotService.setSearching(false);
|
|
19139
|
+
});
|
|
19140
|
+
}
|
|
19141
|
+
/**
|
|
19142
|
+
* Re-render the chat flight cards from whatever the flight-search session
|
|
19143
|
+
* currently holds — the same cached results that "View Full Results" shows.
|
|
19144
|
+
* Used both by the post-search subscription and by {@link resort} (which
|
|
19145
|
+
* re-ranks without a new backend search). Reads `results.value` (falling
|
|
19146
|
+
* back to the unfiltered `fullResults`), re-extracts the journeys, and builds
|
|
19147
|
+
* cards using `parameters` (sort/time/airline/stops) so the same data can be
|
|
19148
|
+
* re-ordered for a new preference.
|
|
19149
|
+
*/
|
|
19150
|
+
buildAndPatchResultsFromCache(parameters) {
|
|
19151
|
+
const flightSearch = this.searchService.searches[ServiceType.Flight];
|
|
19152
|
+
// `results.value` is the *filtered* bundle. For dual-single return
|
|
19153
|
+
// searches applyFilters() can collapse multi[].results to [] when
|
|
19154
|
+
// client-side filters (e.g. the `via` gateway) don't match the
|
|
19155
|
+
// supplier's itineraries. Fall back to the unfiltered `fullResults` so
|
|
19156
|
+
// the chat still surfaces the options.
|
|
19157
|
+
const filtered = flightSearch.results?.value ?? [];
|
|
19158
|
+
let extracted = extractJourneys(filtered);
|
|
19159
|
+
if (!extracted.outbound.length &&
|
|
19160
|
+
Array.isArray(flightSearch.fullResults) &&
|
|
19161
|
+
flightSearch.fullResults.length) {
|
|
19162
|
+
extracted = extractJourneys(flightSearch.fullResults);
|
|
19163
|
+
}
|
|
19164
|
+
const { outbound: rawOutboundOrder, inbound: rawInboundOrder } = extracted;
|
|
19165
|
+
// Baseline order = the normal results page's default "Preferred" ranking, so
|
|
19166
|
+
// the chatbot's default top-N cards are the same flights the user would see
|
|
19167
|
+
// at the top of a normal search. Explicit re-ranks (cheapest/fastest/time)
|
|
19168
|
+
// are re-sorted downstream in `buildFlightResults`, so this is a safe base.
|
|
19169
|
+
const outbound = sortByPreferred(rawOutboundOrder);
|
|
19170
|
+
const inbound = sortByPreferred(rawInboundOrder);
|
|
19171
|
+
const isMultiCity = (parameters.legs?.length ?? 0) >= 2;
|
|
19172
|
+
// Feed ResultAwareService with outbound so its condensed-cards API is
|
|
19173
|
+
// populated for downstream card click → filter navigation.
|
|
19174
|
+
this.resultAwareService.captureFlightResults(outbound);
|
|
19175
|
+
// Keep both raw leg arrays so the chat cards' "Fare Rules" links can resolve
|
|
19176
|
+
// a fare's underlying journey (outbound/combined-return vs dual-single inbound).
|
|
19177
|
+
this.resultAwareService.setFlightRawLegs(outbound, inbound);
|
|
19178
|
+
const outboundCondensed = this.resultAwareService.getCondensedResults();
|
|
19179
|
+
let inboundCondensed = [];
|
|
19180
|
+
if (inbound.length) {
|
|
19181
|
+
// Build inbound condensed by re-running the condensation. The
|
|
19182
|
+
// ResultAwareService keeps state for outbound, so we condense the
|
|
19183
|
+
// raw inbound array via a temporary capture/restore.
|
|
19184
|
+
this.resultAwareService.captureFlightResults(inbound);
|
|
19185
|
+
inboundCondensed = this.resultAwareService.getCondensedResults();
|
|
19186
|
+
// Restore outbound as the "current" capture for click-through filters.
|
|
18944
19187
|
this.resultAwareService.captureFlightResults(outbound);
|
|
18945
|
-
|
|
18946
|
-
|
|
18947
|
-
|
|
18948
|
-
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
18958
|
-
|
|
19188
|
+
}
|
|
19189
|
+
const flightResults = isMultiCity
|
|
19190
|
+
? this.buildMultiCityFlightResults(parameters, outbound)
|
|
19191
|
+
: this.buildFlightResults(outboundCondensed, inboundCondensed, parameters, outbound, inbound);
|
|
19192
|
+
this.chatbotService.patchAssistantMessage({
|
|
19193
|
+
flightResults,
|
|
19194
|
+
state: 'RESULTS_READY'
|
|
19195
|
+
});
|
|
19196
|
+
}
|
|
19197
|
+
/**
|
|
19198
|
+
* Re-rank the results already in the flight-search session for a new user
|
|
19199
|
+
* preference (cheapest / fastest / shortest / nearest-to-time) WITHOUT
|
|
19200
|
+
* running a new backend search. The cached itineraries are the same set the
|
|
19201
|
+
* results page shows; we just re-order them via `buildFlightResults` and
|
|
19202
|
+
* patch fresh cards onto the latest assistant message.
|
|
19203
|
+
*/
|
|
19204
|
+
async resort(parameters) {
|
|
19205
|
+
const flightSearch = this.searchService.searches[ServiceType.Flight];
|
|
19206
|
+
const hasCache = !!flightSearch.results?.value?.length ||
|
|
19207
|
+
(Array.isArray(flightSearch.fullResults) && flightSearch.fullResults.length > 0);
|
|
19208
|
+
if (!hasCache) {
|
|
19209
|
+
// Nothing cached to re-rank (e.g. the page was reloaded since the last
|
|
19210
|
+
// search). sort_results only carries ranking fields, not a full set of
|
|
19211
|
+
// search params, so we can't silently re-run the search — ask the user
|
|
19212
|
+
// to search again rather than showing an empty card stack.
|
|
18959
19213
|
this.chatbotService.patchAssistantMessage({
|
|
18960
|
-
|
|
19214
|
+
text: `I don't have those flight results to hand any more — could you run the search again and I'll re-rank them for you?`,
|
|
18961
19215
|
state: 'RESULTS_READY'
|
|
18962
19216
|
});
|
|
18963
|
-
|
|
18964
|
-
}
|
|
19217
|
+
return;
|
|
19218
|
+
}
|
|
19219
|
+
// The re-rank parameters carry only the new ranking intent (sortType /
|
|
19220
|
+
// time target). Merge them over the last full search so the cards keep the
|
|
19221
|
+
// trip's origin/destination/dates (used for headers + date labels) while
|
|
19222
|
+
// applying the requested ordering.
|
|
19223
|
+
const merged = {
|
|
19224
|
+
...(this.lastSearchParameters ?? {}),
|
|
19225
|
+
...parameters
|
|
19226
|
+
};
|
|
19227
|
+
this.buildAndPatchResultsFromCache(merged);
|
|
18965
19228
|
}
|
|
18966
19229
|
buildFlightResults(outbound, inbound, parameters, rawOutbound = [], rawInbound = []) {
|
|
18967
19230
|
const isReturn = !!parameters.returnDate;
|
|
18968
|
-
//
|
|
18969
|
-
//
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
19231
|
+
// Flight sort options the chatbot can re-rank by. Mirrors the normal results
|
|
19232
|
+
// page's sort dropdown (FlightSortTypes) so "cheapest/fastest/most direct/
|
|
19233
|
+
// lowest emissions/preferred/earliest|latest departure|arrival" all show the
|
|
19234
|
+
// same top flights the page would. `preferred` is the default order, so it's
|
|
19235
|
+
// treated as "no re-rank" (the list already arrives in Preferred order).
|
|
19236
|
+
const FLIGHT_SORTS = new Set([
|
|
19237
|
+
'cheapest',
|
|
19238
|
+
'fastest',
|
|
19239
|
+
'shortest',
|
|
19240
|
+
'most_direct',
|
|
19241
|
+
'lowest_emissions',
|
|
19242
|
+
'earliest_departure',
|
|
19243
|
+
'latest_departure',
|
|
19244
|
+
'earliest_arrival',
|
|
19245
|
+
'latest_arrival'
|
|
19246
|
+
]);
|
|
19247
|
+
const requestedSort = parameters.sortType;
|
|
19248
|
+
// The AI sends `sortType` when the user explicitly asks to rank. When present
|
|
19249
|
+
// (and not the default `preferred`) it overrides time-proximity ordering;
|
|
19250
|
+
// when absent we fall back to a target-time ranking if one exists.
|
|
19251
|
+
const hasExplicitSort = !!requestedSort && FLIGHT_SORTS.has(requestedSort);
|
|
19252
|
+
// Narrowed label kept for the ChatFlightResults.sort metadata field (price/
|
|
19253
|
+
// duration family only); has no effect on the ordering applied below.
|
|
19254
|
+
const sort = requestedSort === 'fastest' || requestedSort === 'shortest' ? requestedSort : 'cheapest';
|
|
19255
|
+
// Time target for proximity ordering: "arrive by 5pm" ranks cards nearest
|
|
19256
|
+
// 17:00 first, "depart at 9am" nearest 09:00. `field` records whether the
|
|
19257
|
+
// user anchored on arrival or departure.
|
|
19258
|
+
const targetTimeToMinutes = (time) => {
|
|
19259
|
+
if (!time)
|
|
19260
|
+
return null;
|
|
19261
|
+
const match = /^(\d{2})(\d{2})$/.exec(this.normalizeTime(time));
|
|
19262
|
+
return match ? +match[1] * 60 + +match[2] : null;
|
|
19263
|
+
};
|
|
19264
|
+
const cardTimeToMinutes = (time) => {
|
|
19265
|
+
if (!time)
|
|
19266
|
+
return null;
|
|
19267
|
+
const match = /^(\d{1,2}):(\d{2})/.exec(time);
|
|
19268
|
+
return match ? +match[1] * 60 + +match[2] : null;
|
|
19269
|
+
};
|
|
19270
|
+
// Build a proximity target from the trip params. `sort_results` re-ranks
|
|
19271
|
+
// carry the time in `departureTime` with an explicit `outboundTimeCriteria`
|
|
19272
|
+
// flag ("arrive by 11" → departureTime '11:00' + criteria 'Arrive'); the
|
|
19273
|
+
// search path may instead set `arrivalTime` directly. Honour the criteria
|
|
19274
|
+
// flag first so an arrival target ranks by arrival, not departure.
|
|
19275
|
+
const resolveTarget = (primary, arrivalAlt, criteria) => {
|
|
19276
|
+
if (criteria === 'Arrive') {
|
|
19277
|
+
const value = primary ?? arrivalAlt;
|
|
19278
|
+
return value ? { mins: targetTimeToMinutes(value), field: 'arrive' } : null;
|
|
19279
|
+
}
|
|
19280
|
+
if (criteria === 'Depart') {
|
|
19281
|
+
const value = primary ?? arrivalAlt;
|
|
19282
|
+
return value ? { mins: targetTimeToMinutes(value), field: 'depart' } : null;
|
|
19283
|
+
}
|
|
19284
|
+
if (primary)
|
|
19285
|
+
return { mins: targetTimeToMinutes(primary), field: 'depart' };
|
|
19286
|
+
if (arrivalAlt)
|
|
19287
|
+
return { mins: targetTimeToMinutes(arrivalAlt), field: 'arrive' };
|
|
19288
|
+
return null;
|
|
19289
|
+
};
|
|
19290
|
+
const outboundTarget = resolveTarget(parameters.departureTime, parameters.arrivalTime, parameters.outboundTimeCriteria);
|
|
19291
|
+
const inboundTarget = resolveTarget(parameters.returnTime, parameters.returnArrivalTime, parameters.returnTimeCriteria);
|
|
19292
|
+
// How each leg's time target should shape the cards: `around` → proximity
|
|
19293
|
+
// rank; `before`/`after` → bound filter + Preferred order; undefined (initial
|
|
19294
|
+
// search) → pure constraint, cards keep Preferred order to mirror the page.
|
|
19295
|
+
const outboundTimeMode = parameters.outboundTimeMode;
|
|
19296
|
+
const inboundTimeMode = parameters.returnTimeMode;
|
|
19297
|
+
// Optional airline filter: when the user asked for specific carrier(s)
|
|
19298
|
+
// ("Give me BA flights", "BA or Virgin"), keep only matching itineraries
|
|
19299
|
+
// strictly — better to show "no matches" than mislead the user. Matches if
|
|
19300
|
+
// the result matches ANY of the requested airlines.
|
|
19301
|
+
const airlineFilters = this.collectCarrierTerms(parameters).map((t) => t.toLowerCase());
|
|
18977
19302
|
const applyAirline = (list) => {
|
|
18978
|
-
if (!
|
|
19303
|
+
if (!airlineFilters.length)
|
|
18979
19304
|
return list;
|
|
18980
|
-
return list.filter((
|
|
18981
|
-
const
|
|
18982
|
-
const
|
|
18983
|
-
return
|
|
19305
|
+
return list.filter((result) => {
|
|
19306
|
+
const carrierCode = (result.flightNumbers?.match(/^[A-Z]{2,3}/i)?.[0] || '').toLowerCase();
|
|
19307
|
+
const airlineName = (result.airline || '').toLowerCase();
|
|
19308
|
+
return airlineFilters.some((f) => carrierCode === f || airlineName.includes(f));
|
|
18984
19309
|
});
|
|
18985
19310
|
};
|
|
18986
|
-
// Honour "direct only" / maxConnections
|
|
18987
|
-
//
|
|
18988
|
-
//
|
|
19311
|
+
// Honour "direct only" / maxConnections strictly. The form-level setting
|
|
19312
|
+
// goes to the supplier, but some suppliers ignore it — re-filter
|
|
19313
|
+
// client-side so the user gets what they asked for.
|
|
18989
19314
|
const applyStops = (list) => {
|
|
18990
19315
|
if (parameters.maxConnections == null)
|
|
18991
19316
|
return list;
|
|
18992
19317
|
const max = parameters.maxConnections;
|
|
18993
|
-
return list.filter((
|
|
19318
|
+
return list.filter((result) => (result.stops ?? 0) <= max);
|
|
19319
|
+
};
|
|
19320
|
+
// Drop next-day arrivals when the user anchored on a same-day arrival
|
|
19321
|
+
// ("arrive before 17:00 tomorrow"). Some suppliers honour the arrival
|
|
19322
|
+
// *time-of-day* window but still return overnight itineraries that land the
|
|
19323
|
+
// following day (e.g. depart 20:50 → arrive 15:35 +1, 17h45m, 1 stop). Their
|
|
19324
|
+
// clock-time arrival sits inside the window, so without this they'd pass the
|
|
19325
|
+
// supplier filter, fool the time-proximity sort, and render as nonsensical
|
|
19326
|
+
// same-day flights. Only applied when an arrival target exists so plain
|
|
19327
|
+
// departure-time or unconstrained searches keep legitimate red-eyes.
|
|
19328
|
+
const applyArrivalWindow = (list, target) => {
|
|
19329
|
+
if (!target || target.field !== 'arrive')
|
|
19330
|
+
return list;
|
|
19331
|
+
return list.filter((result) => this.arrivalDayOffset(result) <= 0);
|
|
19332
|
+
};
|
|
19333
|
+
// Honour an explicit "before"/"after" time bound ("arrive by 11", "depart
|
|
19334
|
+
// after 9") strictly. Unlike a proximity target this doesn't re-order — it
|
|
19335
|
+
// just drops cards that fall outside the bound so the kept ones still show
|
|
19336
|
+
// in Preferred order (matching the normal results page). Only applied for an
|
|
19337
|
+
// explicit bound mode; an initial search leaves the cards untouched.
|
|
19338
|
+
const applyTimeBound = (list, target, mode) => {
|
|
19339
|
+
if (!target || target.mins == null || (mode !== 'before' && mode !== 'after')) {
|
|
19340
|
+
return list;
|
|
19341
|
+
}
|
|
19342
|
+
const boundMins = target.mins;
|
|
19343
|
+
return list.filter((result) => {
|
|
19344
|
+
const label = target.field === 'arrive' ? result.arrive : result.depart;
|
|
19345
|
+
const mins = cardTimeToMinutes(label);
|
|
19346
|
+
if (mins == null)
|
|
19347
|
+
return true;
|
|
19348
|
+
return mode === 'before' ? mins <= boundMins : mins >= boundMins;
|
|
19349
|
+
});
|
|
18994
19350
|
};
|
|
18995
|
-
const outFiltered = applyStops(applyAirline(outbound));
|
|
18996
|
-
const inFiltered = applyStops(applyAirline(inbound));
|
|
18997
|
-
// Dedupe by physical-flight signature,
|
|
18998
|
-
// then sort. Without
|
|
18999
|
-
//
|
|
19000
|
-
//
|
|
19001
|
-
// and the existing fare-options expand panel (built via
|
|
19002
|
-
// indexFaresByFlights) surfaces the other bookable fares.
|
|
19351
|
+
const outFiltered = applyTimeBound(applyArrivalWindow(applyStops(applyAirline(outbound)), outboundTarget), outboundTarget, outboundTimeMode);
|
|
19352
|
+
const inFiltered = applyTimeBound(applyArrivalWindow(applyStops(applyAirline(inbound)), inboundTarget), inboundTarget, inboundTimeMode);
|
|
19353
|
+
// Dedupe by physical-flight signature, keeping the cheapest fare per flight,
|
|
19354
|
+
// then sort. Without this we'd render the same flight several times with
|
|
19355
|
+
// different fare brands (Restricted/Flex/Plus); the fare-options expand
|
|
19356
|
+
// panel (built via indexFaresByFlights) surfaces the other bookable fares.
|
|
19003
19357
|
//
|
|
19004
|
-
//
|
|
19005
|
-
//
|
|
19006
|
-
//
|
|
19007
|
-
|
|
19008
|
-
|
|
19009
|
-
if (!d)
|
|
19358
|
+
// The key includes both legs' flight numbers AND depart times so two
|
|
19359
|
+
// operationally-distinct flights sharing marketing numbers (codeshares on
|
|
19360
|
+
// different equipment/schedules) don't collapse into one card.
|
|
19361
|
+
const parseDurationToMins = (duration) => {
|
|
19362
|
+
if (!duration)
|
|
19010
19363
|
return Number.POSITIVE_INFINITY;
|
|
19011
|
-
const
|
|
19012
|
-
const
|
|
19013
|
-
const total = (
|
|
19364
|
+
const hours = /(\d+)\s*h/i.exec(duration);
|
|
19365
|
+
const minutes = /(\d+)\s*m/i.exec(duration);
|
|
19366
|
+
const total = (hours ? +hours[1] : 0) * 60 + (minutes ? +minutes[1] : 0);
|
|
19014
19367
|
return total > 0 ? total : Number.POSITIVE_INFINITY;
|
|
19015
19368
|
};
|
|
19016
|
-
const totalDurationMins = (
|
|
19369
|
+
const totalDurationMins = (result) => parseDurationToMins(result.duration) +
|
|
19370
|
+
(result.inbound ? parseDurationToMins(result.inbound.duration) : 0);
|
|
19017
19371
|
const dedupeCheapestByFlight = (list) => {
|
|
19018
|
-
const
|
|
19019
|
-
for (const
|
|
19020
|
-
const
|
|
19021
|
-
const
|
|
19022
|
-
if (!
|
|
19372
|
+
const cheapestByKey = new Map();
|
|
19373
|
+
for (const result of list) {
|
|
19374
|
+
const outboundNumbers = result.flightNumbers || '';
|
|
19375
|
+
const inboundNumbers = result.inbound?.flightNumbers || '';
|
|
19376
|
+
if (!outboundNumbers && !inboundNumbers) {
|
|
19023
19377
|
// No flight numbers to key on (shouldn't happen for real results);
|
|
19024
19378
|
// fall back to per-index so we don't collapse unrelated cards.
|
|
19025
|
-
|
|
19379
|
+
cheapestByKey.set(`__idx${result.index}`, result);
|
|
19026
19380
|
continue;
|
|
19027
19381
|
}
|
|
19028
|
-
const key = `${
|
|
19029
|
-
const existing =
|
|
19030
|
-
if (!existing || (
|
|
19031
|
-
|
|
19382
|
+
const key = `${outboundNumbers}|${inboundNumbers}|${result.depart}|${result.inbound?.depart ?? ''}`;
|
|
19383
|
+
const existing = cheapestByKey.get(key);
|
|
19384
|
+
if (!existing || (result.price ?? Infinity) < (existing.price ?? Infinity)) {
|
|
19385
|
+
cheapestByKey.set(key, result);
|
|
19032
19386
|
}
|
|
19033
19387
|
}
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
|
|
19388
|
+
return Array.from(cheapestByKey.values());
|
|
19389
|
+
};
|
|
19390
|
+
// Time-of-day in minutes for a card, preferring the raw ISO datetime (so
|
|
19391
|
+
// next-day arrivals sort after same-day ones) and falling back to the HH:MM
|
|
19392
|
+
// label. Used by the earliest/latest departure & arrival sorts.
|
|
19393
|
+
const cardClockMins = (result, field) => {
|
|
19394
|
+
const iso = field === 'arrive' ? result.arriveDateTime : result.departDateTime;
|
|
19395
|
+
if (iso) {
|
|
19396
|
+
const parsed = Date.parse(iso);
|
|
19397
|
+
if (!Number.isNaN(parsed))
|
|
19398
|
+
return parsed;
|
|
19037
19399
|
}
|
|
19038
|
-
|
|
19039
|
-
|
|
19400
|
+
const label = field === 'arrive' ? result.arrive : result.depart;
|
|
19401
|
+
const mins = cardTimeToMinutes(label);
|
|
19402
|
+
return mins == null ? Number.POSITIVE_INFINITY : mins;
|
|
19403
|
+
};
|
|
19404
|
+
// Comparators for each explicit flight sort — mirrors the normal results
|
|
19405
|
+
// page's sort dropdown so the chatbot's top cards match what the user would
|
|
19406
|
+
// see there. Keyed by the AI's sortType. `preferred` is intentionally
|
|
19407
|
+
// absent: it's the default order, handled by the fall-through below.
|
|
19408
|
+
const sortComparators = {
|
|
19409
|
+
cheapest: (a, b) => (a.price ?? Infinity) - (b.price ?? Infinity),
|
|
19410
|
+
fastest: (a, b) => totalDurationMins(a) - totalDurationMins(b),
|
|
19411
|
+
shortest: (a, b) => totalDurationMins(a) - totalDurationMins(b),
|
|
19412
|
+
most_direct: (a, b) => (a.stops ?? Infinity) - (b.stops ?? Infinity) ||
|
|
19413
|
+
totalDurationMins(a) - totalDurationMins(b),
|
|
19414
|
+
lowest_emissions: (a, b) => (a.co2 ?? Number.POSITIVE_INFINITY) - (b.co2 ?? Number.POSITIVE_INFINITY),
|
|
19415
|
+
earliest_departure: (a, b) => cardClockMins(a, 'depart') - cardClockMins(b, 'depart'),
|
|
19416
|
+
latest_departure: (a, b) => cardClockMins(b, 'depart') - cardClockMins(a, 'depart'),
|
|
19417
|
+
earliest_arrival: (a, b) => cardClockMins(a, 'arrive') - cardClockMins(b, 'arrive'),
|
|
19418
|
+
latest_arrival: (a, b) => cardClockMins(b, 'arrive') - cardClockMins(a, 'arrive')
|
|
19419
|
+
};
|
|
19420
|
+
// Order a deduped leg for display:
|
|
19421
|
+
// 1. proximity mode ("around/closest to/at" a time) + no explicit sort ->
|
|
19422
|
+
// nearest to that time first
|
|
19423
|
+
// 2. an explicit sortType -> the matching comparator (price/duration/
|
|
19424
|
+
// stops/co2/departure/arrival)
|
|
19425
|
+
// 3. otherwise (incl. `preferred`, and "by/before/after" bounds which are
|
|
19426
|
+
// filtered above) -> preserve the incoming "Preferred" order (the normal
|
|
19427
|
+
// results page default), so the chatbot's default cards match the top of
|
|
19428
|
+
// the user's normal search.
|
|
19429
|
+
const orderLeg = (legResults, target, mode) => {
|
|
19430
|
+
if (mode === 'around' && !hasExplicitSort && target && target.mins != null) {
|
|
19431
|
+
const targetMins = target.mins;
|
|
19432
|
+
return [...legResults].sort((a, b) => {
|
|
19433
|
+
const aMinutes = cardTimeToMinutes(target.field === 'arrive' ? a.arrive : a.depart);
|
|
19434
|
+
const bMinutes = cardTimeToMinutes(target.field === 'arrive' ? b.arrive : b.depart);
|
|
19435
|
+
const aDistance = aMinutes == null ? Number.POSITIVE_INFINITY : Math.abs(aMinutes - targetMins);
|
|
19436
|
+
const bDistance = bMinutes == null ? Number.POSITIVE_INFINITY : Math.abs(bMinutes - targetMins);
|
|
19437
|
+
if (aDistance !== bDistance)
|
|
19438
|
+
return aDistance - bDistance;
|
|
19439
|
+
// Tie-break on price so equally-close flights show cheapest first.
|
|
19440
|
+
return (a.price ?? Infinity) - (b.price ?? Infinity);
|
|
19441
|
+
});
|
|
19040
19442
|
}
|
|
19041
|
-
|
|
19443
|
+
const comparator = requestedSort ? sortComparators[requestedSort] : undefined;
|
|
19444
|
+
if (comparator) {
|
|
19445
|
+
return [...legResults].sort(comparator);
|
|
19446
|
+
}
|
|
19447
|
+
// Default (incl. `preferred`): keep the Preferred order the list carries.
|
|
19448
|
+
return legResults;
|
|
19042
19449
|
};
|
|
19043
|
-
const outDeduped = dedupeCheapestByFlight(outFiltered);
|
|
19044
|
-
const inDeduped = dedupeCheapestByFlight(inFiltered);
|
|
19045
|
-
//
|
|
19046
|
-
//
|
|
19047
|
-
//
|
|
19048
|
-
|
|
19049
|
-
|
|
19050
|
-
//
|
|
19051
|
-
|
|
19052
|
-
|
|
19450
|
+
const outDeduped = orderLeg(dedupeCheapestByFlight(outFiltered), outboundTarget, outboundTimeMode);
|
|
19451
|
+
const inDeduped = orderLeg(dedupeCheapestByFlight(inFiltered), inboundTarget, inboundTimeMode);
|
|
19452
|
+
// Group raw itineraries by their flight-number signature so each card can
|
|
19453
|
+
// show all bookable cabins/brands for the same physical flights. One-way
|
|
19454
|
+
// keys use just the outbound numbers; returns include the inbound legs.
|
|
19455
|
+
const outFareIndex = this.indexFaresByFlights(rawOutbound, isReturn, false);
|
|
19456
|
+
const inFareIndex = this.indexFaresByFlights(rawInbound, false, true);
|
|
19457
|
+
// Display only the top N for the chosen ordering. Follow-up questions
|
|
19458
|
+
// ("which is cheapest?", "any BA?") are answered by the AI over the
|
|
19459
|
+
// condensed results, so the chat UI doesn't carry the full set or re-sort.
|
|
19053
19460
|
const outboundCards = outDeduped
|
|
19054
19461
|
.slice(0, MAX_FLIGHT_CARDS)
|
|
19055
|
-
.map((
|
|
19462
|
+
.map((result) => this.toFlightCard(result, parameters, 'outbound', outFareIndex));
|
|
19056
19463
|
const inboundCards = inDeduped
|
|
19057
19464
|
.slice(0, MAX_FLIGHT_CARDS)
|
|
19058
|
-
.map((
|
|
19059
|
-
return {
|
|
19465
|
+
.map((result) => this.toFlightCard(result, parameters, 'inbound', inFareIndex));
|
|
19466
|
+
return {
|
|
19467
|
+
outbound: outboundCards,
|
|
19468
|
+
inbound: inboundCards,
|
|
19469
|
+
isReturn,
|
|
19470
|
+
parameters,
|
|
19471
|
+
sort
|
|
19472
|
+
};
|
|
19060
19473
|
}
|
|
19061
19474
|
/**
|
|
19062
19475
|
* Builds a map keyed by an itinerary's flight-number signature to all
|
|
@@ -19064,7 +19477,7 @@ class ChatbotFlightSearchService {
|
|
|
19064
19477
|
* For combined-return journeys the key includes both outbound + inbound
|
|
19065
19478
|
* flight numbers; for dual-single legs it's just that leg's numbers.
|
|
19066
19479
|
*/
|
|
19067
|
-
indexFaresByFlights(raw, includeInbound) {
|
|
19480
|
+
indexFaresByFlights(raw, includeInbound, isReturnLeg = false) {
|
|
19068
19481
|
const map = new Map();
|
|
19069
19482
|
if (!raw?.length)
|
|
19070
19483
|
return map;
|
|
@@ -19072,10 +19485,17 @@ class ChatbotFlightSearchService {
|
|
|
19072
19485
|
const key = this.flightSignature(it, includeInbound);
|
|
19073
19486
|
if (!key)
|
|
19074
19487
|
return;
|
|
19075
|
-
const fare = this.toFareTicket(it, idx);
|
|
19488
|
+
const fare = this.toFareTicket(it, idx, isReturnLeg);
|
|
19076
19489
|
if (!fare)
|
|
19077
19490
|
return;
|
|
19078
19491
|
const arr = map.get(key) ?? [];
|
|
19492
|
+
// Mirror the full-results grouping (FlightTicketsComponent): suppliers
|
|
19493
|
+
// often return the identical fare for the same physical flight more than
|
|
19494
|
+
// once. The results page drops a fare when one with the same price is
|
|
19495
|
+
// already in the group; do the same so the chat fare panel doesn't render
|
|
19496
|
+
// duplicate ticket rows.
|
|
19497
|
+
if (arr.some((existing) => existing.price === fare.price))
|
|
19498
|
+
return;
|
|
19079
19499
|
arr.push(fare);
|
|
19080
19500
|
map.set(key, arr);
|
|
19081
19501
|
});
|
|
@@ -19098,7 +19518,7 @@ class ChatbotFlightSearchService {
|
|
|
19098
19518
|
.join('/');
|
|
19099
19519
|
return `${out}|${inn}`;
|
|
19100
19520
|
}
|
|
19101
|
-
toFareTicket(it, idx) {
|
|
19521
|
+
toFareTicket(it, idx, isReturnLeg = false) {
|
|
19102
19522
|
const price = it?.total?.price;
|
|
19103
19523
|
if (!(price > 0))
|
|
19104
19524
|
return null;
|
|
@@ -19120,7 +19540,8 @@ class ChatbotFlightSearchService {
|
|
|
19120
19540
|
brand: brand || undefined,
|
|
19121
19541
|
price,
|
|
19122
19542
|
currency: it?.total?.currency || 'GBP',
|
|
19123
|
-
journeyIndex: idx
|
|
19543
|
+
journeyIndex: idx,
|
|
19544
|
+
isReturnLeg: isReturnLeg || undefined
|
|
19124
19545
|
};
|
|
19125
19546
|
}
|
|
19126
19547
|
/**
|
|
@@ -19147,7 +19568,10 @@ class ChatbotFlightSearchService {
|
|
|
19147
19568
|
if (!journeys.length || !legs.length) {
|
|
19148
19569
|
return baseResults;
|
|
19149
19570
|
}
|
|
19150
|
-
// Dedupe by full-itinerary flight signature
|
|
19571
|
+
// Dedupe by full-itinerary flight signature, keeping the cheapest fare per
|
|
19572
|
+
// physical itinerary. The Map preserves first-occurrence order, so the list
|
|
19573
|
+
// stays in the incoming "Preferred" order (the normal page default) — we
|
|
19574
|
+
// don't re-rank by price here, matching the single/return default.
|
|
19151
19575
|
const bySig = new Map();
|
|
19152
19576
|
for (const j of journeys) {
|
|
19153
19577
|
const sig = (j?.outboundFlights ?? [])
|
|
@@ -19158,7 +19582,8 @@ class ChatbotFlightSearchService {
|
|
|
19158
19582
|
bySig.set(sig, j);
|
|
19159
19583
|
}
|
|
19160
19584
|
}
|
|
19161
|
-
const sorted = [...bySig.values()]
|
|
19585
|
+
const sorted = [...bySig.values()];
|
|
19586
|
+
const fareIndex = this.indexFaresByFlights(journeys, false, false);
|
|
19162
19587
|
const cards = [];
|
|
19163
19588
|
for (let i = 0; i < Math.min(MAX_FLIGHT_CARDS, sorted.length); i++) {
|
|
19164
19589
|
const journey = sorted[i];
|
|
@@ -19168,7 +19593,15 @@ class ChatbotFlightSearchService {
|
|
|
19168
19593
|
const airline = journey?.outboundFlights?.[0]?.marketingCarrierName ||
|
|
19169
19594
|
journey?.outboundFlights?.[0]?.airline ||
|
|
19170
19595
|
'';
|
|
19171
|
-
const cabin = journey?.
|
|
19596
|
+
const cabin = journey?.outboundFlights?.[0]?.cabinClass ||
|
|
19597
|
+
journey?.fareConditions?.outboundFareConditions?.cabinClass ||
|
|
19598
|
+
'';
|
|
19599
|
+
let fares;
|
|
19600
|
+
const fareKey = this.flightSignature(journey, false);
|
|
19601
|
+
const tickets = fareKey ? fareIndex.get(fareKey) : undefined;
|
|
19602
|
+
if (tickets && tickets.length) {
|
|
19603
|
+
fares = { tickets, currency: tickets[0].currency };
|
|
19604
|
+
}
|
|
19172
19605
|
cards.push({
|
|
19173
19606
|
index: i,
|
|
19174
19607
|
airline,
|
|
@@ -19178,7 +19611,8 @@ class ChatbotFlightSearchService {
|
|
|
19178
19611
|
fromPrice: journey?.total?.price ?? 0,
|
|
19179
19612
|
currency: journey?.total?.currency || 'GBP',
|
|
19180
19613
|
cabin: this.formatCabinLabel(cabin),
|
|
19181
|
-
policyStatus: journey?.preferenceLevel === 'IN_POLICY' ? 'IN_POLICY' : undefined
|
|
19614
|
+
policyStatus: journey?.preferenceLevel === 'IN_POLICY' ? 'IN_POLICY' : undefined,
|
|
19615
|
+
fares
|
|
19182
19616
|
});
|
|
19183
19617
|
}
|
|
19184
19618
|
return { ...baseResults, outbound: cards };
|
|
@@ -19193,7 +19627,7 @@ class ChatbotFlightSearchService {
|
|
|
19193
19627
|
const buckets = legs.map(() => []);
|
|
19194
19628
|
let currentLeg = 0;
|
|
19195
19629
|
for (const f of flights) {
|
|
19196
|
-
const fDate = (f?.
|
|
19630
|
+
const fDate = (f?.dateTimeDeparting || '').slice(0, 10);
|
|
19197
19631
|
// Advance currentLeg while the leg's date is strictly before this
|
|
19198
19632
|
// flight's date — preserves ordering even when a leg connects overnight.
|
|
19199
19633
|
while (currentLeg + 1 < legs.length && legs[currentLeg + 1].date <= fDate) {
|
|
@@ -19212,24 +19646,46 @@ class ChatbotFlightSearchService {
|
|
|
19212
19646
|
.filter(Boolean)
|
|
19213
19647
|
.join('/');
|
|
19214
19648
|
let duration = '';
|
|
19215
|
-
if (first.
|
|
19216
|
-
const mins = Math.round((new Date(last.
|
|
19649
|
+
if (first.dateTimeDeparting && last.dateTimeArriving) {
|
|
19650
|
+
const mins = Math.round((new Date(last.dateTimeArriving).getTime() - new Date(first.dateTimeDeparting).getTime()) /
|
|
19217
19651
|
60000);
|
|
19218
19652
|
duration = this.formatDuration(mins);
|
|
19219
19653
|
}
|
|
19220
19654
|
return {
|
|
19221
|
-
fromCode: first.
|
|
19655
|
+
fromCode: first.originAirport || leg.departureCode || leg.departure,
|
|
19222
19656
|
fromName: leg.departure,
|
|
19223
|
-
toCode: last.
|
|
19657
|
+
toCode: last.destinationAirport || leg.arrivalCode || leg.arrival,
|
|
19224
19658
|
toName: leg.arrival,
|
|
19225
19659
|
departDate: this.formatDate(leg.date),
|
|
19226
|
-
departTime: this.formatHHmm(first.
|
|
19227
|
-
arriveTime: this.formatHHmm(last.
|
|
19660
|
+
departTime: this.formatHHmm(first.dateTimeDeparting),
|
|
19661
|
+
arriveTime: this.formatHHmm(last.dateTimeArriving),
|
|
19228
19662
|
duration,
|
|
19229
19663
|
stops: Math.max(0, flights.length - 1),
|
|
19664
|
+
arrivalDayOffset: this.arrivalDayOffset({
|
|
19665
|
+
departDateTime: first.dateTimeDeparting,
|
|
19666
|
+
arriveDateTime: last.dateTimeArriving
|
|
19667
|
+
}),
|
|
19230
19668
|
flightNumbers
|
|
19231
19669
|
};
|
|
19232
19670
|
}
|
|
19671
|
+
/**
|
|
19672
|
+
* Calendar days the arrival falls after departure (0 = same day, 1 = next
|
|
19673
|
+
* day). Compares date components rather than elapsed time so a 23:30 → 00:40
|
|
19674
|
+
* hop still counts as +1. Returns 0 when datetimes are missing/invalid so a
|
|
19675
|
+
* lack of data never hides an otherwise-valid result. Accepts either a full
|
|
19676
|
+
* `CondensedResult` (outbound) or its `inbound` sub-leg.
|
|
19677
|
+
*/
|
|
19678
|
+
arrivalDayOffset(leg) {
|
|
19679
|
+
if (!leg.departDateTime || !leg.arriveDateTime)
|
|
19680
|
+
return 0;
|
|
19681
|
+
const dep = new Date(leg.departDateTime);
|
|
19682
|
+
const arr = new Date(leg.arriveDateTime);
|
|
19683
|
+
if (Number.isNaN(dep.getTime()) || Number.isNaN(arr.getTime()))
|
|
19684
|
+
return 0;
|
|
19685
|
+
const depDay = Date.UTC(dep.getFullYear(), dep.getMonth(), dep.getDate());
|
|
19686
|
+
const arrDay = Date.UTC(arr.getFullYear(), arr.getMonth(), arr.getDate());
|
|
19687
|
+
return Math.round((arrDay - depDay) / 86_400_000);
|
|
19688
|
+
}
|
|
19233
19689
|
formatHHmm(iso) {
|
|
19234
19690
|
if (!iso)
|
|
19235
19691
|
return '';
|
|
@@ -19269,6 +19725,7 @@ class ChatbotFlightSearchService {
|
|
|
19269
19725
|
arriveTime: r.arrive,
|
|
19270
19726
|
duration: r.duration,
|
|
19271
19727
|
stops: r.stops,
|
|
19728
|
+
arrivalDayOffset: this.arrivalDayOffset(r),
|
|
19272
19729
|
flightNumbers: r.flightNumbers ?? ''
|
|
19273
19730
|
};
|
|
19274
19731
|
// For combined-return itineraries (journeys), each result already has
|
|
@@ -19284,6 +19741,7 @@ class ChatbotFlightSearchService {
|
|
|
19284
19741
|
arriveTime: r.inbound.arrive,
|
|
19285
19742
|
duration: r.inbound.duration,
|
|
19286
19743
|
stops: r.inbound.stops,
|
|
19744
|
+
arrivalDayOffset: this.arrivalDayOffset(r.inbound),
|
|
19287
19745
|
flightNumbers: r.inbound.flightNumbers ?? ''
|
|
19288
19746
|
}
|
|
19289
19747
|
: undefined;
|
|
@@ -19459,6 +19917,9 @@ class ChatbotFlightSearchService {
|
|
|
19459
19917
|
else if ('cabinClass' in flightParams && FlightCabinClass.Any) {
|
|
19460
19918
|
flightParams.cabinClass = FlightCabinClass.Any;
|
|
19461
19919
|
}
|
|
19920
|
+
if ('returnCabinClass' in flightParams) {
|
|
19921
|
+
flightParams.returnCabinClass = flightParams.cabinClass;
|
|
19922
|
+
}
|
|
19462
19923
|
}
|
|
19463
19924
|
if (parameters.adults != null) {
|
|
19464
19925
|
flightParams.adults = parameters.adults;
|
|
@@ -19543,15 +20004,27 @@ class ChatbotFlightSearchService {
|
|
|
19543
20004
|
// keep the previously-stated cabin for the same journey.
|
|
19544
20005
|
try {
|
|
19545
20006
|
flightParams.cabinClass = FlightCabinClass.Economy;
|
|
20007
|
+
if ('returnCabinClass' in flightParams) {
|
|
20008
|
+
flightParams.returnCabinClass = FlightCabinClass.Economy;
|
|
20009
|
+
}
|
|
19546
20010
|
}
|
|
19547
20011
|
catch (err) {
|
|
19548
20012
|
console.warn('[ChatbotFlight] failed to reset cabinClass', err);
|
|
19549
20013
|
}
|
|
19550
20014
|
}
|
|
19551
20015
|
}
|
|
20016
|
+
// OBT keeps a single time-of-day per leg; the criteria (set further down)
|
|
20017
|
+
// decides whether it means "depart at" or "arrive by". So an arrival time
|
|
20018
|
+
// ("arriving before 5pm") and a departure time share the same field — we
|
|
20019
|
+
// accept either and remember which the user gave so we can default the
|
|
20020
|
+
// criteria accordingly.
|
|
20021
|
+
const outboundLegTime = parameters.departureTime || parameters.arrivalTime;
|
|
20022
|
+
const outboundArrivalGiven = !parameters.departureTime && !!parameters.arrivalTime;
|
|
20023
|
+
const returnLegTime = parameters.returnTime || parameters.returnArrivalTime;
|
|
20024
|
+
const returnArrivalGiven = !parameters.returnTime && !!parameters.returnArrivalTime;
|
|
19552
20025
|
flightParams.departDate = departNgbDate;
|
|
19553
|
-
if (
|
|
19554
|
-
flightParams.departTime = this.normalizeTime(
|
|
20026
|
+
if (outboundLegTime) {
|
|
20027
|
+
flightParams.departTime = this.normalizeTime(outboundLegTime);
|
|
19555
20028
|
}
|
|
19556
20029
|
if (parameters.returnDate) {
|
|
19557
20030
|
const returnDate = moment$1(parameters.returnDate);
|
|
@@ -19561,8 +20034,8 @@ class ChatbotFlightSearchService {
|
|
|
19561
20034
|
day: returnDate.date()
|
|
19562
20035
|
};
|
|
19563
20036
|
flightParams.chosenSearchType = FlightSearchType.return;
|
|
19564
|
-
if (
|
|
19565
|
-
flightParams.arriveTime = this.normalizeTime(
|
|
20037
|
+
if (returnLegTime) {
|
|
20038
|
+
flightParams.arriveTime = this.normalizeTime(returnLegTime);
|
|
19566
20039
|
}
|
|
19567
20040
|
}
|
|
19568
20041
|
else {
|
|
@@ -19581,6 +20054,27 @@ class ChatbotFlightSearchService {
|
|
|
19581
20054
|
else if ('cabinClass' in flightParams && FlightCabinClass.Any) {
|
|
19582
20055
|
flightParams.cabinClass = FlightCabinClass.Any;
|
|
19583
20056
|
}
|
|
20057
|
+
// OBT tracks outbound and return cabin separately. When the user wants a
|
|
20058
|
+
// DIFFERENT cabin on the return (e.g. business out, economy back) the AI
|
|
20059
|
+
// sends `returnCabinClass`; honour it. Otherwise mirror the outbound cabin
|
|
20060
|
+
// — without this a "First class return" search would ask for First out /
|
|
20061
|
+
// Economy back (returnCabinClass would stay at its Economy default).
|
|
20062
|
+
if ('returnCabinClass' in flightParams) {
|
|
20063
|
+
const rcc = String(parameters.returnCabinClass ?? '')
|
|
20064
|
+
.trim()
|
|
20065
|
+
.toLowerCase();
|
|
20066
|
+
if (parameters.returnCabinClass && rcc && rcc !== 'any' && rcc !== 'all') {
|
|
20067
|
+
flightParams.returnCabinClass = this.mapCabinClass(parameters.returnCabinClass);
|
|
20068
|
+
}
|
|
20069
|
+
else if (parameters.returnCabinClass &&
|
|
20070
|
+
(rcc === 'any' || rcc === 'all') &&
|
|
20071
|
+
FlightCabinClass.Any) {
|
|
20072
|
+
flightParams.returnCabinClass = FlightCabinClass.Any;
|
|
20073
|
+
}
|
|
20074
|
+
else {
|
|
20075
|
+
flightParams.returnCabinClass = flightParams.cabinClass;
|
|
20076
|
+
}
|
|
20077
|
+
}
|
|
19584
20078
|
}
|
|
19585
20079
|
// Passenger breakdown — if the AI passed adults/children/infants, prefer
|
|
19586
20080
|
// those; otherwise fall back to total `passengers` as adults.
|
|
@@ -19600,11 +20094,13 @@ class ChatbotFlightSearchService {
|
|
|
19600
20094
|
// so that if the user later navigates to the flight search form (whose
|
|
19601
20095
|
// ngOnInit re-derives maxConnections from this flag) the "Direct only"
|
|
19602
20096
|
// checkbox stays ticked instead of being silently reset.
|
|
19603
|
-
|
|
19604
|
-
|
|
19605
|
-
|
|
19606
|
-
|
|
19607
|
-
|
|
20097
|
+
// Always set maxConnections so a previous "direct only" search doesn't
|
|
20098
|
+
// persist when the user removes that constraint on a same-journey turn
|
|
20099
|
+
// (the AI omits the field). `null` = no connection limit.
|
|
20100
|
+
flightParams.maxConnections =
|
|
20101
|
+
parameters.maxConnections != null ? parameters.maxConnections : null;
|
|
20102
|
+
if ('directDefaultValue' in flightParams) {
|
|
20103
|
+
flightParams.directDefaultValue = parameters.maxConnections === 0;
|
|
19608
20104
|
}
|
|
19609
20105
|
if (parameters.includeNearbyAirports != null && 'includeNearbyAirports' in flightParams) {
|
|
19610
20106
|
flightParams.includeNearbyAirports = parameters.includeNearbyAirports;
|
|
@@ -19622,15 +20118,35 @@ class ChatbotFlightSearchService {
|
|
|
19622
20118
|
return TimeWindow.Anytime;
|
|
19623
20119
|
return null;
|
|
19624
20120
|
};
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
|
|
20121
|
+
// OBT uses a single time-of-day per leg (departTime / arriveTime, set above
|
|
20122
|
+
// from departureTime / returnTime) and the criteria reinterprets it:
|
|
20123
|
+
// Depart -> a ±flexibility window centred on the time
|
|
20124
|
+
// Arrive -> a fixed 2h window ending at the time (makeArrivalTimeWindow)
|
|
20125
|
+
// Anytime -> no window at all (every flight for the date)
|
|
20126
|
+
// So the rule is simply: if the user gave a time for a leg, honour their
|
|
20127
|
+
// criteria (defaulting to "Depart" / depart-after); if they gave no time,
|
|
20128
|
+
// search Anytime. The latter is essential — without it the engine windows
|
|
20129
|
+
// around the leg's *default* time-of-day and returns the wrong flights
|
|
20130
|
+
// (e.g. a 0730-0930 arrival window when the user only said "arriving before
|
|
20131
|
+
// 5pm" but no parseable time reached us, or a stray departure window that
|
|
20132
|
+
// wipes out the return leg of a long-haul return such as SYD→LHR).
|
|
20133
|
+
if (outboundLegTime) {
|
|
20134
|
+
flightParams.outTimeCriteria =
|
|
20135
|
+
(parameters.outboundTimeCriteria && toTimeWindow(parameters.outboundTimeCriteria)) ||
|
|
20136
|
+
(outboundArrivalGiven ? TimeWindow.Arrive : TimeWindow.Depart);
|
|
19629
20137
|
}
|
|
19630
|
-
|
|
19631
|
-
|
|
19632
|
-
|
|
19633
|
-
|
|
20138
|
+
else {
|
|
20139
|
+
flightParams.outTimeCriteria = TimeWindow.Anytime;
|
|
20140
|
+
}
|
|
20141
|
+
if (parameters.returnDate) {
|
|
20142
|
+
if (returnLegTime) {
|
|
20143
|
+
flightParams.inTimeCriteria =
|
|
20144
|
+
(parameters.returnTimeCriteria && toTimeWindow(parameters.returnTimeCriteria)) ||
|
|
20145
|
+
(returnArrivalGiven ? TimeWindow.Arrive : TimeWindow.Depart);
|
|
20146
|
+
}
|
|
20147
|
+
else {
|
|
20148
|
+
flightParams.inTimeCriteria = TimeWindow.Anytime;
|
|
20149
|
+
}
|
|
19634
20150
|
}
|
|
19635
20151
|
if (parameters.timeFlexibility != null) {
|
|
19636
20152
|
flightParams.outTimeFlexibility = parameters.timeFlexibility;
|
|
@@ -19647,23 +20163,18 @@ class ChatbotFlightSearchService {
|
|
|
19647
20163
|
flightParams.showVia = true;
|
|
19648
20164
|
}
|
|
19649
20165
|
}
|
|
19650
|
-
// Preferred airline →
|
|
19651
|
-
//
|
|
19652
|
-
//
|
|
19653
|
-
//
|
|
19654
|
-
//
|
|
19655
|
-
//
|
|
19656
|
-
// then a substring match.
|
|
20166
|
+
// Preferred airline(s) → preferredCarriers[0..3]. The form exposes four
|
|
20167
|
+
// carrier slots; the chatbot may pass a single airline (airline/airlineCode)
|
|
20168
|
+
// or up to four in `airlines`. The form's airline field is a typeahead
|
|
20169
|
+
// backed by the searchAirlines GraphQL query, so we MUST write real
|
|
20170
|
+
// {code, name} objects resolved from that source (an ad-hoc object with the
|
|
20171
|
+
// wrong code wouldn't match anything server-side).
|
|
19657
20172
|
//
|
|
19658
|
-
// Skip entirely when
|
|
19659
|
-
//
|
|
19660
|
-
//
|
|
19661
|
-
|
|
19662
|
-
|
|
19663
|
-
const isAnyAirline = !rawAirlineTerm ||
|
|
19664
|
-
rawAirlineTerm.toLowerCase() === 'any' ||
|
|
19665
|
-
rawAirlineTerm.toLowerCase() === 'all';
|
|
19666
|
-
if (!isAnyAirline) {
|
|
20173
|
+
// Skip entirely when there's no real preference (null, empty, or "any"/
|
|
20174
|
+
// "all"). preferredCarriers was already reset to all-nulls at the top of
|
|
20175
|
+
// this method, so no carrier filter will be applied to the search.
|
|
20176
|
+
const carrierTerms = this.collectCarrierTerms(parameters);
|
|
20177
|
+
if (carrierTerms.length) {
|
|
19667
20178
|
if (!Array.isArray(flightParams.preferredCarriers)) {
|
|
19668
20179
|
try {
|
|
19669
20180
|
flightParams.preferredCarriers = [];
|
|
@@ -19673,39 +20184,12 @@ class ChatbotFlightSearchService {
|
|
|
19673
20184
|
}
|
|
19674
20185
|
}
|
|
19675
20186
|
if (Array.isArray(flightParams.preferredCarriers)) {
|
|
19676
|
-
const
|
|
19677
|
-
|
|
19678
|
-
|
|
19679
|
-
|
|
19680
|
-
const list = matches || [];
|
|
19681
|
-
const upper = term.toUpperCase();
|
|
19682
|
-
const lower = term.toLowerCase();
|
|
19683
|
-
// Avoid silently selecting a regional/subsidiary carrier when the
|
|
19684
|
-
// user named the parent airline. E.g. "KLM" → should match
|
|
19685
|
-
// "KLM Royal Dutch Airlines" not "KLM Cityhopper" (which would
|
|
19686
|
-
// also match a naive substring/startsWith check first).
|
|
19687
|
-
const isSubsidiary = (name) => /\b(cityhopper|cityline|cityjet|express|regional|connect|shuttle|airlink|link|commuter)\b/i.test(name);
|
|
19688
|
-
const startsWith = list.filter((a) => (a.name || '').toLowerCase().startsWith(lower));
|
|
19689
|
-
const startsWithNonSub = startsWith.filter((a) => !isSubsidiary(a.name || ''));
|
|
19690
|
-
const containsNonSub = list.filter((a) => !isSubsidiary(a.name || '') && (a.name || '').toLowerCase().includes(lower));
|
|
19691
|
-
const resolved = list.find((a) => (a.code || '').toUpperCase() === upper) ||
|
|
19692
|
-
list.find((a) => (a.name || '').toLowerCase() === lower) ||
|
|
19693
|
-
startsWithNonSub[0] ||
|
|
19694
|
-
startsWith[0] ||
|
|
19695
|
-
containsNonSub[0] ||
|
|
19696
|
-
list.find((a) => (a.name || '').toLowerCase().includes(lower)) ||
|
|
19697
|
-
list[0];
|
|
19698
|
-
if (resolved) {
|
|
19699
|
-
flightParams.preferredCarriers[0] = {
|
|
19700
|
-
code: resolved.code,
|
|
19701
|
-
name: resolved.name
|
|
19702
|
-
};
|
|
19703
|
-
}
|
|
19704
|
-
}
|
|
19705
|
-
catch (err) {
|
|
19706
|
-
console.warn('[ChatbotFlightSearchService] airline lookup failed', err);
|
|
20187
|
+
const resolvedCarriers = await Promise.all(carrierTerms.map((term) => this.resolveCarrier(term)));
|
|
20188
|
+
resolvedCarriers.forEach((carrier, idx) => {
|
|
20189
|
+
if (carrier) {
|
|
20190
|
+
flightParams.preferredCarriers[idx] = { code: carrier.code, name: carrier.name };
|
|
19707
20191
|
}
|
|
19708
|
-
}
|
|
20192
|
+
});
|
|
19709
20193
|
}
|
|
19710
20194
|
}
|
|
19711
20195
|
// Apply the airports we already resolved at the top of this method.
|
|
@@ -19758,6 +20242,75 @@ class ChatbotFlightSearchService {
|
|
|
19758
20242
|
}
|
|
19759
20243
|
return null;
|
|
19760
20244
|
}
|
|
20245
|
+
/**
|
|
20246
|
+
* Build the de-duplicated list of carrier search terms from the chat
|
|
20247
|
+
* parameters. Prefers the `airlines` array (up to {@link MAX_PREFERRED_CARRIERS})
|
|
20248
|
+
* when present, else falls back to the singular airline/airlineCode. Drops
|
|
20249
|
+
* "no preference" sentinels ("any"/"all"/empty) and terms shorter than 2 chars.
|
|
20250
|
+
*/
|
|
20251
|
+
collectCarrierTerms(parameters) {
|
|
20252
|
+
const candidates = parameters.airlines?.length
|
|
20253
|
+
? parameters.airlines.map((a) => a?.code || a?.name || '')
|
|
20254
|
+
: [parameters.airlineCode || parameters.airline || ''];
|
|
20255
|
+
const terms = [];
|
|
20256
|
+
const seen = new Set();
|
|
20257
|
+
for (const candidate of candidates) {
|
|
20258
|
+
const term = String(candidate).trim();
|
|
20259
|
+
const key = term.toLowerCase();
|
|
20260
|
+
if (term.length < 2 ||
|
|
20261
|
+
ChatbotFlightSearchService.NO_CARRIER_PREFERENCE.has(key) ||
|
|
20262
|
+
seen.has(key)) {
|
|
20263
|
+
continue;
|
|
20264
|
+
}
|
|
20265
|
+
seen.add(key);
|
|
20266
|
+
terms.push(term);
|
|
20267
|
+
if (terms.length >= ChatbotFlightSearchService.MAX_PREFERRED_CARRIERS) {
|
|
20268
|
+
break;
|
|
20269
|
+
}
|
|
20270
|
+
}
|
|
20271
|
+
return terms;
|
|
20272
|
+
}
|
|
20273
|
+
/**
|
|
20274
|
+
* Resolve a single airline search term to a real {code, name} from the
|
|
20275
|
+
* searchAirlines GraphQL source, trying the highest-confidence match first:
|
|
20276
|
+
* exact IATA code, exact name, then non-subsidiary startsWith/contains — so
|
|
20277
|
+
* "KLM" resolves to "KLM Royal Dutch Airlines" rather than "KLM Cityhopper".
|
|
20278
|
+
*/
|
|
20279
|
+
async resolveCarrier(term) {
|
|
20280
|
+
const trimmed = (term || '').trim();
|
|
20281
|
+
if (trimmed.length < 2) {
|
|
20282
|
+
return null;
|
|
20283
|
+
}
|
|
20284
|
+
let list;
|
|
20285
|
+
try {
|
|
20286
|
+
list = (await firstValueFrom(this.searchService.getAirlinesList(trimmed))) || [];
|
|
20287
|
+
}
|
|
20288
|
+
catch (err) {
|
|
20289
|
+
console.warn('[ChatbotFlightSearchService] airline lookup failed', err);
|
|
20290
|
+
return null;
|
|
20291
|
+
}
|
|
20292
|
+
const upper = trimmed.toUpperCase();
|
|
20293
|
+
const lower = trimmed.toLowerCase();
|
|
20294
|
+
const name = (a) => (a.name || '').toLowerCase();
|
|
20295
|
+
const isSubsidiary = (a) => ChatbotFlightSearchService.CARRIER_SUBSIDIARY.test(a.name || '');
|
|
20296
|
+
const strategies = [
|
|
20297
|
+
(a) => (a.code || '').toUpperCase() === upper,
|
|
20298
|
+
(a) => name(a) === lower,
|
|
20299
|
+
(a) => name(a).startsWith(lower) && !isSubsidiary(a),
|
|
20300
|
+
(a) => name(a).startsWith(lower),
|
|
20301
|
+
(a) => name(a).includes(lower) && !isSubsidiary(a),
|
|
20302
|
+
(a) => name(a).includes(lower)
|
|
20303
|
+
];
|
|
20304
|
+
let resolved;
|
|
20305
|
+
for (const matches of strategies) {
|
|
20306
|
+
resolved = list.find(matches);
|
|
20307
|
+
if (resolved) {
|
|
20308
|
+
break;
|
|
20309
|
+
}
|
|
20310
|
+
}
|
|
20311
|
+
resolved = resolved || list[0];
|
|
20312
|
+
return resolved ? { code: resolved.code, name: resolved.name } : null;
|
|
20313
|
+
}
|
|
19761
20314
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotFlightSearchService, deps: [{ token: ChatbotService }, { token: EnterpriseSearchService }, { token: ResultAwareService }, { token: UserService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
19762
20315
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotFlightSearchService, providedIn: 'root' });
|
|
19763
20316
|
}
|
|
@@ -19788,15 +20341,35 @@ class ChatbotHotelSearchService {
|
|
|
19788
20341
|
searchService;
|
|
19789
20342
|
hotelAvalibilityService;
|
|
19790
20343
|
resultAwareService;
|
|
20344
|
+
userService;
|
|
19791
20345
|
travelType = 'hotel';
|
|
19792
20346
|
availabilitySubscription = null;
|
|
19793
|
-
|
|
20347
|
+
lastParameters = null;
|
|
20348
|
+
markAsLoading() {
|
|
20349
|
+
const hotelSearch = this.searchService.searches[ServiceType.Hotel];
|
|
20350
|
+
hotelSearch?.isLoading?.next(true);
|
|
20351
|
+
}
|
|
20352
|
+
get hasHotelResults() {
|
|
20353
|
+
const hotelSearch = this.searchService.searches[ServiceType.Hotel];
|
|
20354
|
+
return (hotelSearch?.results?.value?.length ?? 0) > 0;
|
|
20355
|
+
}
|
|
20356
|
+
constructor(chatbotService, searchService, hotelAvalibilityService, resultAwareService, userService) {
|
|
19794
20357
|
this.chatbotService = chatbotService;
|
|
19795
20358
|
this.searchService = searchService;
|
|
19796
20359
|
this.hotelAvalibilityService = hotelAvalibilityService;
|
|
19797
20360
|
this.resultAwareService = resultAwareService;
|
|
20361
|
+
this.userService = userService;
|
|
19798
20362
|
}
|
|
19799
20363
|
async handle(parameters, autoSearch) {
|
|
20364
|
+
this.lastParameters = parameters;
|
|
20365
|
+
if ((parameters.resultOffset ?? 0) > 0) {
|
|
20366
|
+
await this.runSearchFromOffset(parameters);
|
|
20367
|
+
return;
|
|
20368
|
+
}
|
|
20369
|
+
const hotelSearch = this.searchService.searches[ServiceType.Hotel];
|
|
20370
|
+
if (hotelSearch?.filters?.hotelName) {
|
|
20371
|
+
hotelSearch.filters.hotelName = '';
|
|
20372
|
+
}
|
|
19800
20373
|
this.searchService.resetSearchChosenObjects();
|
|
19801
20374
|
this.searchService.resetSearchPriorities();
|
|
19802
20375
|
this.searchService.search_objects[ServiceType.Hotel].chosen = true;
|
|
@@ -19815,8 +20388,18 @@ class ChatbotHotelSearchService {
|
|
|
19815
20388
|
await this.runSearch(parameters);
|
|
19816
20389
|
}
|
|
19817
20390
|
// ── Search + availability ─────────────────────────────────────────────────
|
|
20391
|
+
ensureSelfTraveller() {
|
|
20392
|
+
if (this.searchService.travellerInformation.value.length > 0) {
|
|
20393
|
+
return;
|
|
20394
|
+
}
|
|
20395
|
+
const traveller = this.userService.bobo ?? this.userService.createSelfTraveller();
|
|
20396
|
+
if (traveller) {
|
|
20397
|
+
this.searchService.addTraveller(traveller);
|
|
20398
|
+
}
|
|
20399
|
+
}
|
|
19818
20400
|
async runSearch(parameters) {
|
|
19819
20401
|
const hotelSearch = this.searchService.searches[ServiceType.Hotel];
|
|
20402
|
+
this.ensureSelfTraveller();
|
|
19820
20403
|
this.resultAwareService.clearResults();
|
|
19821
20404
|
let hasResults = false;
|
|
19822
20405
|
this.chatbotService.setSearching(true, 'hotel');
|
|
@@ -19853,9 +20436,15 @@ class ChatbotHotelSearchService {
|
|
|
19853
20436
|
}
|
|
19854
20437
|
});
|
|
19855
20438
|
this.sortHotels(candidates, parameters.sortType);
|
|
19856
|
-
|
|
19857
|
-
|
|
19858
|
-
.
|
|
20439
|
+
let hotelsWithPrices = candidates.filter((hotel) => (hotel.availableRates?.rooms ?? []).some((r) => r?.total > 0));
|
|
20440
|
+
if (parameters.hotelName) {
|
|
20441
|
+
const nameTerm = parameters.hotelName.toLowerCase();
|
|
20442
|
+
const nameFiltered = hotelsWithPrices.filter((hotel) => (hotel.name || '').toLowerCase().includes(nameTerm));
|
|
20443
|
+
if (nameFiltered.length > 0) {
|
|
20444
|
+
hotelsWithPrices = nameFiltered;
|
|
20445
|
+
}
|
|
20446
|
+
}
|
|
20447
|
+
hotelsWithPrices = hotelsWithPrices.slice(0, MAX_HOTEL_CARDS);
|
|
19859
20448
|
this.resultAwareService.captureHotelResults(hotelsWithPrices);
|
|
19860
20449
|
const condensed = this.resultAwareService.getCondensedResults();
|
|
19861
20450
|
if (!condensed.length) {
|
|
@@ -19879,6 +20468,73 @@ class ChatbotHotelSearchService {
|
|
|
19879
20468
|
});
|
|
19880
20469
|
}
|
|
19881
20470
|
// ── Card building ─────────────────────────────────────────────────────────
|
|
20471
|
+
async runSearchFromOffset(parameters) {
|
|
20472
|
+
const hotelSearch = this.searchService.searches[ServiceType.Hotel];
|
|
20473
|
+
const offset = parameters.resultOffset ?? 0;
|
|
20474
|
+
const allResults = hotelSearch.results?.value ?? [];
|
|
20475
|
+
if (!allResults.length) {
|
|
20476
|
+
this.injectNoResultsMessage();
|
|
20477
|
+
return;
|
|
20478
|
+
}
|
|
20479
|
+
const candidates = allResults.slice(offset, offset + AVAILABILITY_CANDIDATE_COUNT);
|
|
20480
|
+
if (!candidates.length) {
|
|
20481
|
+
this.chatbotService.injectMessage({
|
|
20482
|
+
id: `hotel-no-more-${Date.now()}`,
|
|
20483
|
+
role: 'assistant',
|
|
20484
|
+
text: 'There are no more hotel results to show.',
|
|
20485
|
+
timestamp: new Date()
|
|
20486
|
+
});
|
|
20487
|
+
return;
|
|
20488
|
+
}
|
|
20489
|
+
this.chatbotService.setSearching(true, 'hotel');
|
|
20490
|
+
const availabilitySubjects = candidates.map((hotel) => this.hotelAvalibilityService.getAvailabilityForId(hotelSearch.makeHotelAvalilityObject(hotel), hotel));
|
|
20491
|
+
const allRoomsReady$ = combineLatest(availabilitySubjects).pipe(filter((results) => results.every((r) => r?.rooms !== undefined)), take(1));
|
|
20492
|
+
const fetchingDone$ = this.hotelAvalibilityService.fetching.pipe(skipWhile((fetching) => !fetching), filter((fetching) => !fetching), take(1));
|
|
20493
|
+
this.availabilitySubscription?.unsubscribe();
|
|
20494
|
+
this.availabilitySubscription = race(allRoomsReady$, fetchingDone$, timer(HOTEL_AVAILABILITY_WAIT_MS))
|
|
20495
|
+
.pipe(take(1), catchError$1((err) => {
|
|
20496
|
+
console.warn('[ChatbotHotelSearchService] offset availability polling error', err);
|
|
20497
|
+
this.chatbotService.setSearching(false);
|
|
20498
|
+
return EMPTY;
|
|
20499
|
+
}))
|
|
20500
|
+
.subscribe(() => {
|
|
20501
|
+
candidates.forEach((hotel, i) => {
|
|
20502
|
+
const availability = availabilitySubjects[i].value;
|
|
20503
|
+
if (availability?.rooms !== undefined) {
|
|
20504
|
+
hotel.availableRates = { ...availability, rooms: availability.rooms ?? [] };
|
|
20505
|
+
}
|
|
20506
|
+
});
|
|
20507
|
+
this.sortHotels(candidates, parameters.sortType);
|
|
20508
|
+
let hotelsWithPrices = candidates.filter((hotel) => (hotel.availableRates?.rooms ?? []).some((r) => r?.total > 0));
|
|
20509
|
+
hotelsWithPrices = hotelsWithPrices.slice(0, MAX_HOTEL_CARDS);
|
|
20510
|
+
if (!hotelsWithPrices.length) {
|
|
20511
|
+
this.injectNoResultsMessage();
|
|
20512
|
+
this.chatbotService.setSearching(false);
|
|
20513
|
+
return;
|
|
20514
|
+
}
|
|
20515
|
+
this.resultAwareService.captureHotelResults(hotelsWithPrices);
|
|
20516
|
+
const condensed = this.resultAwareService.getCondensedResults();
|
|
20517
|
+
const cards = this.buildHotelCards(condensed, hotelsWithPrices);
|
|
20518
|
+
const checkin = hotelSearch?.checkin_date
|
|
20519
|
+
? moment$1(hotelSearch.checkin_date).format('D MMM')
|
|
20520
|
+
: '';
|
|
20521
|
+
const checkout = hotelSearch?.checkout_date
|
|
20522
|
+
? moment$1(hotelSearch.checkout_date).format('D MMM')
|
|
20523
|
+
: '';
|
|
20524
|
+
const dateRange = checkin && checkout ? `${checkin} - ${checkout}` : '';
|
|
20525
|
+
const searchHeader = [parameters.arrival, dateRange].filter(Boolean).join(' · ');
|
|
20526
|
+
const hotelResults = { cards, searchHeader, parameters };
|
|
20527
|
+
this.chatbotService.injectMessage({
|
|
20528
|
+
id: `hotel-more-${Date.now()}`,
|
|
20529
|
+
role: 'assistant',
|
|
20530
|
+
text: '',
|
|
20531
|
+
timestamp: new Date(),
|
|
20532
|
+
hotelResults,
|
|
20533
|
+
state: 'RESULTS_READY'
|
|
20534
|
+
});
|
|
20535
|
+
this.chatbotService.setSearching(false);
|
|
20536
|
+
});
|
|
20537
|
+
}
|
|
19882
20538
|
buildHotelCards(condensed, rawHotels) {
|
|
19883
20539
|
return condensed.slice(0, MAX_HOTEL_CARDS).map((r, i) => {
|
|
19884
20540
|
const raw = rawHotels[i];
|
|
@@ -19886,14 +20542,20 @@ class ChatbotHotelSearchService {
|
|
|
19886
20542
|
.filter((room) => room && !room.unavailable && room.total > 0)
|
|
19887
20543
|
.map((room) => ({
|
|
19888
20544
|
roomId: room.roomId,
|
|
19889
|
-
description: room.
|
|
20545
|
+
description: room.roomType || room.roomDescription || 'Standard Room',
|
|
19890
20546
|
total: room.total,
|
|
19891
20547
|
prpn: room.prpn,
|
|
19892
20548
|
currencyCode: room.currencyCode ?? 'GBP',
|
|
19893
20549
|
unavailable: !!room.unavailable,
|
|
19894
20550
|
policyStatus: room.requiresReasonKeys?.length
|
|
19895
20551
|
? 'ALLOW_WITH_REASON'
|
|
19896
|
-
: 'IN_POLICY'
|
|
20552
|
+
: 'IN_POLICY',
|
|
20553
|
+
rateSource: room.rateSource ?? null,
|
|
20554
|
+
rateCode: room.rateCode ?? null,
|
|
20555
|
+
rateDescription: room.rateDescription ?? null,
|
|
20556
|
+
roomDescription: room.roomDescription ?? null,
|
|
20557
|
+
policies: room.policies ?? null,
|
|
20558
|
+
additional: room.additional ?? null
|
|
19897
20559
|
}));
|
|
19898
20560
|
return {
|
|
19899
20561
|
index: r.index,
|
|
@@ -19909,6 +20571,7 @@ class ChatbotHotelSearchService {
|
|
|
19909
20571
|
policyMessages: r.policyMessages,
|
|
19910
20572
|
co2: r.co2,
|
|
19911
20573
|
currencyCode: rooms[0]?.currencyCode ?? 'GBP',
|
|
20574
|
+
source: raw?.availableRates?.source ?? null,
|
|
19912
20575
|
rooms
|
|
19913
20576
|
};
|
|
19914
20577
|
});
|
|
@@ -19940,12 +20603,21 @@ class ChatbotHotelSearchService {
|
|
|
19940
20603
|
break;
|
|
19941
20604
|
case 'cheapest':
|
|
19942
20605
|
default:
|
|
20606
|
+
hotels.sort((a, b) => this.cheapestRoomTotal(a) - this.cheapestRoomTotal(b));
|
|
19943
20607
|
break;
|
|
19944
20608
|
}
|
|
19945
20609
|
}
|
|
20610
|
+
cheapestRoomTotal(hotel) {
|
|
20611
|
+
const totals = (hotel.availableRates?.rooms ?? [])
|
|
20612
|
+
.map((room) => room?.total)
|
|
20613
|
+
.filter((total) => typeof total === 'number' && total > 0);
|
|
20614
|
+
return totals.length ? Math.min(...totals) : Number.POSITIVE_INFINITY;
|
|
20615
|
+
}
|
|
19946
20616
|
// ── Param population ──────────────────────────────────────────────────────
|
|
19947
20617
|
async populateHotelParams(parameters, departDate, departNgbDate) {
|
|
19948
20618
|
const hotelParams = this.searchService.searches[ServiceType.Hotel];
|
|
20619
|
+
hotelParams.hotel_name = '';
|
|
20620
|
+
hotelParams.hotel_chain = null;
|
|
19949
20621
|
hotelParams.checkin_date = departDate;
|
|
19950
20622
|
hotelParams.checkin_date_ngb = departNgbDate;
|
|
19951
20623
|
if (parameters.returnDate) {
|
|
@@ -20009,11 +20681,10 @@ class ChatbotHotelSearchService {
|
|
|
20009
20681
|
const resolvedLocationType = locationTypeMap[locationType] ?? LocationTypes.Address;
|
|
20010
20682
|
hotelParams.location_type_select = resolvedLocationType;
|
|
20011
20683
|
if (resolvedLocationType === LocationTypes.Office) {
|
|
20012
|
-
const
|
|
20684
|
+
const searchTerm = parameters.arrival?.toLowerCase() ?? '';
|
|
20685
|
+
const offices = await firstValueFrom(this.searchService.getOfficeLocationsNoUserAddresses(searchTerm));
|
|
20013
20686
|
if (offices?.length > 0) {
|
|
20014
|
-
|
|
20015
|
-
const match = offices.find((o) => o.name.toLowerCase().includes(searchTerm)) ?? offices[0];
|
|
20016
|
-
hotelParams.office = match;
|
|
20687
|
+
hotelParams.office = offices[0];
|
|
20017
20688
|
}
|
|
20018
20689
|
}
|
|
20019
20690
|
else if (resolvedLocationType === LocationTypes.Address) {
|
|
@@ -20047,60 +20718,67 @@ class ChatbotHotelSearchService {
|
|
|
20047
20718
|
}
|
|
20048
20719
|
}
|
|
20049
20720
|
}
|
|
20050
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotHotelSearchService, deps: [{ token: ChatbotService }, { token: EnterpriseSearchService }, { token: HotelAvalibilityService }, { token: ResultAwareService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
20721
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotHotelSearchService, deps: [{ token: ChatbotService }, { token: EnterpriseSearchService }, { token: HotelAvalibilityService }, { token: ResultAwareService }, { token: UserService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
20051
20722
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotHotelSearchService, providedIn: 'root' });
|
|
20052
20723
|
}
|
|
20053
20724
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotHotelSearchService, decorators: [{
|
|
20054
20725
|
type: Injectable,
|
|
20055
20726
|
args: [{ providedIn: 'root' }]
|
|
20056
|
-
}], ctorParameters: () => [{ type: ChatbotService }, { type: EnterpriseSearchService }, { type: HotelAvalibilityService }, { type: ResultAwareService }] });
|
|
20727
|
+
}], ctorParameters: () => [{ type: ChatbotService }, { type: EnterpriseSearchService }, { type: HotelAvalibilityService }, { type: ResultAwareService }, { type: UserService }] });
|
|
20057
20728
|
|
|
20058
|
-
/**
|
|
20059
|
-
const
|
|
20060
|
-
/**
|
|
20061
|
-
|
|
20729
|
+
/** Maximum number of car hire cards to show in the chat widget. */
|
|
20730
|
+
const MAX_CAR_HIRE_CARDS = 3;
|
|
20731
|
+
/** Default driver age used for delivery/collection searches when the user
|
|
20732
|
+
* hasn't supplied one (matches the form's minimum selectable age). */
|
|
20733
|
+
const DEFAULT_DRIVER_AGE = 25;
|
|
20734
|
+
/** Radius (miles) to retry a depot lookup at before giving up, when nothing
|
|
20735
|
+
* is found within the requested distance. */
|
|
20736
|
+
const WIDE_SEARCH_RADIUS_MILES = 20;
|
|
20062
20737
|
/**
|
|
20063
|
-
* Handles chatbot-driven
|
|
20738
|
+
* Handles chatbot-driven car hire searches. Registered with
|
|
20064
20739
|
* `ChatbotSearchDispatcherService`; not used directly.
|
|
20065
20740
|
*
|
|
20066
|
-
* Populates the
|
|
20067
|
-
*
|
|
20068
|
-
*
|
|
20069
|
-
* last assistant message via {@link ChatbotService.patchAssistantMessage}.
|
|
20741
|
+
* Populates the car hire search state from the chatbot's parameters, runs the
|
|
20742
|
+
* search headlessly, then injects a `ChatCarHireResults` message so the chat
|
|
20743
|
+
* widget can render car hire cards.
|
|
20070
20744
|
*/
|
|
20071
|
-
class
|
|
20745
|
+
class ChatbotCarHireSearchService {
|
|
20072
20746
|
chatbotService;
|
|
20073
20747
|
searchService;
|
|
20748
|
+
resultAwareService;
|
|
20074
20749
|
userService;
|
|
20075
|
-
travelType = '
|
|
20076
|
-
|
|
20077
|
-
constructor(chatbotService, searchService, userService) {
|
|
20750
|
+
travelType = 'car-hire';
|
|
20751
|
+
pendingDepotChoice = null;
|
|
20752
|
+
constructor(chatbotService, searchService, resultAwareService, userService) {
|
|
20078
20753
|
this.chatbotService = chatbotService;
|
|
20079
20754
|
this.searchService = searchService;
|
|
20755
|
+
this.resultAwareService = resultAwareService;
|
|
20080
20756
|
this.userService = userService;
|
|
20081
20757
|
}
|
|
20758
|
+
ensureSelfTraveller() {
|
|
20759
|
+
if (this.searchService.travellerInformation.value.length > 0) {
|
|
20760
|
+
return;
|
|
20761
|
+
}
|
|
20762
|
+
const traveller = this.userService.bobo ?? this.userService.createSelfTraveller();
|
|
20763
|
+
if (traveller) {
|
|
20764
|
+
this.searchService.addTraveller(traveller);
|
|
20765
|
+
}
|
|
20766
|
+
}
|
|
20082
20767
|
async handle(parameters, autoSearch) {
|
|
20768
|
+
// Clear any pending depot choice from a previous interaction.
|
|
20769
|
+
this.pendingDepotChoice = null;
|
|
20770
|
+
this.chatbotService.depotSelectionHandler = null;
|
|
20083
20771
|
this.searchService.resetSearchChosenObjects();
|
|
20084
20772
|
this.searchService.resetSearchPriorities();
|
|
20085
|
-
this.searchService.search_objects[ServiceType.
|
|
20086
|
-
this.searchService.search_objects[ServiceType.
|
|
20773
|
+
this.searchService.search_objects[ServiceType.Car].chosen = true;
|
|
20774
|
+
this.searchService.search_objects[ServiceType.Car].originalChosen = true;
|
|
20087
20775
|
this.searchService.determineHighestSearchPriority();
|
|
20088
|
-
|
|
20089
|
-
|
|
20090
|
-
|
|
20091
|
-
|
|
20092
|
-
const selfTraveller = this.userService.createSelfTraveller();
|
|
20093
|
-
if (selfTraveller) {
|
|
20094
|
-
this.searchService.addTraveller(selfTraveller);
|
|
20095
|
-
}
|
|
20776
|
+
const result = await this.populateCarHireParams(parameters);
|
|
20777
|
+
if (result.status !== 'ok') {
|
|
20778
|
+
this.injectLocationHelpMessage(result, parameters, autoSearch);
|
|
20779
|
+
return;
|
|
20096
20780
|
}
|
|
20097
|
-
|
|
20098
|
-
const departNgbDate = {
|
|
20099
|
-
year: departDate.year(),
|
|
20100
|
-
month: departDate.month() + 1,
|
|
20101
|
-
day: departDate.date()
|
|
20102
|
-
};
|
|
20103
|
-
await this.populateEurostarParams(parameters, departNgbDate);
|
|
20781
|
+
this.chatbotService.carHireParamsReady$.next();
|
|
20104
20782
|
if (!autoSearch) {
|
|
20105
20783
|
return;
|
|
20106
20784
|
}
|
|
@@ -20108,7 +20786,970 @@ class ChatbotEurostarSearchService {
|
|
|
20108
20786
|
}
|
|
20109
20787
|
// ── Search ────────────────────────────────────────────────────────────────
|
|
20110
20788
|
async runSearch(parameters) {
|
|
20111
|
-
const
|
|
20789
|
+
const carHireSearch = this.searchService.searches[ServiceType.Car];
|
|
20790
|
+
this.ensureSelfTraveller();
|
|
20791
|
+
this.resultAwareService.clearResults();
|
|
20792
|
+
this.chatbotService.setSearching(true);
|
|
20793
|
+
const hasResults = await carHireSearch.startSearch();
|
|
20794
|
+
if (!hasResults) {
|
|
20795
|
+
this.injectNoResultsMessage();
|
|
20796
|
+
this.chatbotService.setSearching(false);
|
|
20797
|
+
return;
|
|
20798
|
+
}
|
|
20799
|
+
const results$ = carHireSearch.results;
|
|
20800
|
+
results$
|
|
20801
|
+
.pipe(filter((results) => results?.length > 0), take(1), catchError$1((err) => {
|
|
20802
|
+
console.warn('[ChatbotCarHireSearchService] results polling error', err);
|
|
20803
|
+
this.chatbotService.setSearching(false);
|
|
20804
|
+
return EMPTY;
|
|
20805
|
+
}))
|
|
20806
|
+
.subscribe((results) => {
|
|
20807
|
+
const topResults = results.slice(0, MAX_CAR_HIRE_CARDS);
|
|
20808
|
+
const cards = this.buildCarHireCards(topResults, parameters);
|
|
20809
|
+
this.resultAwareService.captureCarHireResults(topResults);
|
|
20810
|
+
const pickupName = parameters.departure || '';
|
|
20811
|
+
const pickupDate = parameters.departureDate
|
|
20812
|
+
? moment$1(parameters.departureDate).format('ddd D MMM')
|
|
20813
|
+
: '';
|
|
20814
|
+
const dropoffDate = parameters.returnDate
|
|
20815
|
+
? moment$1(parameters.returnDate).format('ddd D MMM')
|
|
20816
|
+
: '';
|
|
20817
|
+
const dateRange = pickupDate && dropoffDate ? `${pickupDate} – ${dropoffDate}` : pickupDate;
|
|
20818
|
+
const searchHeader = [pickupName, dateRange].filter(Boolean).join(' · ');
|
|
20819
|
+
const carHireResults = { cards, searchHeader, parameters };
|
|
20820
|
+
this.chatbotService.patchAssistantMessage({ carHireResults, state: 'RESULTS_READY' });
|
|
20821
|
+
this.chatbotService.setSearching(false);
|
|
20822
|
+
});
|
|
20823
|
+
}
|
|
20824
|
+
// ── Card building ─────────────────────────────────────────────────────────
|
|
20825
|
+
buildCarHireCards(results, parameters) {
|
|
20826
|
+
return results.map((r, i) => {
|
|
20827
|
+
const display = r.displayRecord;
|
|
20828
|
+
const identifiers = r.identifiers;
|
|
20829
|
+
const vehicleDescription = display?.vehicle?.description || identifiers?.makeModel || '';
|
|
20830
|
+
const vendorName = display?.vendor?.name || identifiers?.vendor || '';
|
|
20831
|
+
const vendorCode = identifiers?.vendor || undefined;
|
|
20832
|
+
const vendorLogoUrl = vendorCode
|
|
20833
|
+
? `https://images.sabscorp.com/images/carhire/${vendorCode}.gif`
|
|
20834
|
+
: undefined;
|
|
20835
|
+
const price = display?.price ?? 0;
|
|
20836
|
+
const pickupDt = identifiers?.pickUpDateTime || parameters.departureDate || '';
|
|
20837
|
+
const dropoffDt = identifiers?.dropOffDateTime || parameters.returnDate || '';
|
|
20838
|
+
const durationDays = this.calculateDays(pickupDt, dropoffDt);
|
|
20839
|
+
const pricePerDay = durationDays > 0 ? price / durationDays : undefined;
|
|
20840
|
+
const pickupLocation = identifiers?.pickUpLocationName ||
|
|
20841
|
+
display?.pickupLocation?.name ||
|
|
20842
|
+
parameters.departure ||
|
|
20843
|
+
'';
|
|
20844
|
+
const dropoffLocation = identifiers?.dropOffLocationName ||
|
|
20845
|
+
display?.returnLocation?.name ||
|
|
20846
|
+
parameters.arrival ||
|
|
20847
|
+
pickupLocation;
|
|
20848
|
+
let policyStatus;
|
|
20849
|
+
if (r.unavailable) {
|
|
20850
|
+
policyStatus = 'UNAVAILABLE';
|
|
20851
|
+
}
|
|
20852
|
+
else if (r.requiresReasonKeys?.length || r.requiresApprovalKeys?.length) {
|
|
20853
|
+
policyStatus = 'ALLOW_WITH_REASON';
|
|
20854
|
+
}
|
|
20855
|
+
else {
|
|
20856
|
+
policyStatus = 'IN_POLICY';
|
|
20857
|
+
}
|
|
20858
|
+
const currencyCode = display?.charges?.[0]?.currencyCode || 'GBP';
|
|
20859
|
+
return {
|
|
20860
|
+
index: i + 1,
|
|
20861
|
+
vehicleDescription,
|
|
20862
|
+
vendorName,
|
|
20863
|
+
vendorCode,
|
|
20864
|
+
vendorLogoUrl,
|
|
20865
|
+
carClass: display?.vehicle?.class || undefined,
|
|
20866
|
+
carType: display?.vehicle?.type || undefined,
|
|
20867
|
+
transmission: display?.vehicle?.transmission || undefined,
|
|
20868
|
+
airCon: display?.vehicle?.aircon ?? undefined,
|
|
20869
|
+
doorsCount: identifiers?.doorCount || undefined,
|
|
20870
|
+
price,
|
|
20871
|
+
pricePerDay,
|
|
20872
|
+
currencyCode,
|
|
20873
|
+
pickupLocation,
|
|
20874
|
+
dropoffLocation,
|
|
20875
|
+
pickupDateTime: pickupDt,
|
|
20876
|
+
dropoffDateTime: dropoffDt,
|
|
20877
|
+
policyStatus,
|
|
20878
|
+
policyMessages: r.requiresReasonMessages ?? undefined,
|
|
20879
|
+
co2: r.co2PerItem || undefined
|
|
20880
|
+
};
|
|
20881
|
+
});
|
|
20882
|
+
}
|
|
20883
|
+
calculateDays(pickupDt, dropoffDt) {
|
|
20884
|
+
if (!pickupDt || !dropoffDt) {
|
|
20885
|
+
return 0;
|
|
20886
|
+
}
|
|
20887
|
+
const pickup = moment$1(pickupDt);
|
|
20888
|
+
const dropoff = moment$1(dropoffDt);
|
|
20889
|
+
return Math.max(1, dropoff.diff(pickup, 'days'));
|
|
20890
|
+
}
|
|
20891
|
+
injectNoResultsMessage() {
|
|
20892
|
+
this.chatbotService.injectMessage({
|
|
20893
|
+
id: `car-hire-no-results-${Date.now()}`,
|
|
20894
|
+
role: 'assistant',
|
|
20895
|
+
text: 'No car hire results found for your search. Try adjusting your dates or location.',
|
|
20896
|
+
timestamp: new Date(),
|
|
20897
|
+
state: 'SELECTING_RESULT'
|
|
20898
|
+
});
|
|
20899
|
+
}
|
|
20900
|
+
injectLocationHelpMessage(result, parameters, autoSearch) {
|
|
20901
|
+
let text;
|
|
20902
|
+
let depotChoices;
|
|
20903
|
+
if (result.status === 'location-not-found') {
|
|
20904
|
+
text = `I couldn't find a pickup location matching "${result.term}". Could you give a nearby town, city, airport, or postcode?`;
|
|
20905
|
+
}
|
|
20906
|
+
else if (result.status === 'depot-not-found') {
|
|
20907
|
+
text = `I couldn't find any car hire locations near ${result.location}, even within ${WIDE_SEARCH_RADIUS_MILES} miles. Could you try a nearby town, city, or airport?`;
|
|
20908
|
+
}
|
|
20909
|
+
else {
|
|
20910
|
+
const MAX_SHOWN = 5;
|
|
20911
|
+
const shown = result.depots.slice(0, MAX_SHOWN);
|
|
20912
|
+
depotChoices = shown.map((d, i) => ({
|
|
20913
|
+
index: i,
|
|
20914
|
+
label: `${d.vendor} — ${d.addressLines?.[0] ?? ''}`
|
|
20915
|
+
}));
|
|
20916
|
+
const more = result.depots.length > MAX_SHOWN
|
|
20917
|
+
? ` Showing ${MAX_SHOWN} of ${result.depots.length} — type "show all" to see the full list.`
|
|
20918
|
+
: '';
|
|
20919
|
+
text = `I found several car hire locations near ${result.location}. Which would you prefer?${more}`;
|
|
20920
|
+
this.pendingDepotChoice = {
|
|
20921
|
+
depots: shown,
|
|
20922
|
+
allDepots: result.depots,
|
|
20923
|
+
pickup: result.pickup,
|
|
20924
|
+
parameters,
|
|
20925
|
+
autoSearch,
|
|
20926
|
+
deliveryCollection: result.deliveryCollection
|
|
20927
|
+
};
|
|
20928
|
+
this.chatbotService.depotSelectionHandler = (reply) => this.handleDepotSelectionReply(reply);
|
|
20929
|
+
}
|
|
20930
|
+
// The agent already responded ("I've set up the search...") before handle()
|
|
20931
|
+
// ran. Remove that message so our more accurate reply replaces it.
|
|
20932
|
+
this.chatbotService.retractLastAssistantMessage();
|
|
20933
|
+
this.chatbotService.injectMessage({
|
|
20934
|
+
id: `car-hire-location-help-${Date.now()}`,
|
|
20935
|
+
role: 'assistant',
|
|
20936
|
+
text,
|
|
20937
|
+
timestamp: new Date(),
|
|
20938
|
+
state: 'COLLECTING_PARAMS',
|
|
20939
|
+
depotChoices
|
|
20940
|
+
});
|
|
20941
|
+
}
|
|
20942
|
+
async handleDepotSelectionReply(reply) {
|
|
20943
|
+
const pending = this.pendingDepotChoice;
|
|
20944
|
+
if (!pending) {
|
|
20945
|
+
return false;
|
|
20946
|
+
}
|
|
20947
|
+
const normalised = reply.trim().toLowerCase();
|
|
20948
|
+
if (normalised === 'show all') {
|
|
20949
|
+
const allChoices = pending.allDepots.map((d, i) => ({
|
|
20950
|
+
index: i,
|
|
20951
|
+
label: `${d.vendor} — ${d.addressLines?.[0] ?? ''}`
|
|
20952
|
+
}));
|
|
20953
|
+
// Update shown list to the full set so subsequent number picks work
|
|
20954
|
+
this.pendingDepotChoice = { ...pending, depots: pending.allDepots };
|
|
20955
|
+
this.chatbotService.injectMessage({
|
|
20956
|
+
id: `car-hire-depot-all-${Date.now()}`,
|
|
20957
|
+
role: 'assistant',
|
|
20958
|
+
text: `Here are all ${pending.allDepots.length} locations:`,
|
|
20959
|
+
timestamp: new Date(),
|
|
20960
|
+
state: 'COLLECTING_PARAMS',
|
|
20961
|
+
depotChoices: allChoices
|
|
20962
|
+
});
|
|
20963
|
+
return true;
|
|
20964
|
+
}
|
|
20965
|
+
// Parse a number from the reply (handles "5", "the 5th", "number 5", etc.)
|
|
20966
|
+
const match = normalised.match(/\b(\d+)\b/);
|
|
20967
|
+
if (!match) {
|
|
20968
|
+
return false;
|
|
20969
|
+
}
|
|
20970
|
+
const index = parseInt(match[1], 10) - 1;
|
|
20971
|
+
const activeList = pending.depots;
|
|
20972
|
+
if (index < 0 || index >= activeList.length) {
|
|
20973
|
+
this.chatbotService.injectMessage({
|
|
20974
|
+
id: `car-hire-depot-invalid-${Date.now()}`,
|
|
20975
|
+
role: 'assistant',
|
|
20976
|
+
text: `Please choose a number between 1 and ${activeList.length}.`,
|
|
20977
|
+
timestamp: new Date(),
|
|
20978
|
+
state: 'COLLECTING_PARAMS'
|
|
20979
|
+
});
|
|
20980
|
+
return true;
|
|
20981
|
+
}
|
|
20982
|
+
const chosen = activeList[index];
|
|
20983
|
+
// Clear pending state and handler before continuing
|
|
20984
|
+
this.pendingDepotChoice = null;
|
|
20985
|
+
this.chatbotService.depotSelectionHandler = null;
|
|
20986
|
+
const carHireSearch = this.searchService.searches[ServiceType.Car];
|
|
20987
|
+
const isPostcode = !!pending.parameters.pickupPostcode;
|
|
20988
|
+
if (isPostcode) {
|
|
20989
|
+
carHireSearch.postcodePickupRaw = pending.pickup;
|
|
20990
|
+
carHireSearch.postcodePickup = chosen;
|
|
20991
|
+
carHireSearch.locationType = LocationTypes.Postcode;
|
|
20992
|
+
// Mirror pickup to dropoff — postcode searches always return to same point
|
|
20993
|
+
// unless a distinct dropoff postcode was provided (rare; not in depot-choice flow).
|
|
20994
|
+
carHireSearch.postcodeDropoffRaw = pending.pickup;
|
|
20995
|
+
carHireSearch.postcodeDropoff = chosen;
|
|
20996
|
+
}
|
|
20997
|
+
else {
|
|
20998
|
+
carHireSearch.cityPickup = this.toCityLocation(pending.pickup);
|
|
20999
|
+
carHireSearch.cityPickupDepot = chosen;
|
|
21000
|
+
carHireSearch.locationType = LocationTypes.City;
|
|
21001
|
+
await this.applyCityDropoff(carHireSearch, pending.parameters, pending.deliveryCollection);
|
|
21002
|
+
// If there's no distinct dropoff location, force it to match the chosen
|
|
21003
|
+
// pickup depot exactly.
|
|
21004
|
+
const hasDistinctDropoff = !!pending.parameters.arrival &&
|
|
21005
|
+
pending.parameters.arrival !== pending.parameters.departure;
|
|
21006
|
+
if (!hasDistinctDropoff) {
|
|
21007
|
+
carHireSearch.cityDropoff = carHireSearch.cityPickup;
|
|
21008
|
+
carHireSearch.cityDropoffDepot = carHireSearch.cityPickupDepot;
|
|
21009
|
+
}
|
|
21010
|
+
}
|
|
21011
|
+
this.chatbotService.carHireParamsReady$.next();
|
|
21012
|
+
if (pending.autoSearch) {
|
|
21013
|
+
// Inject a confirmation before running the search so results appear
|
|
21014
|
+
// below this message rather than above the user's selection reply.
|
|
21015
|
+
const depotLabel = chosen.addressLines?.[0]
|
|
21016
|
+
? `${chosen.addressLines[0]}, ${chosen.cityName}`
|
|
21017
|
+
: chosen.cityName;
|
|
21018
|
+
this.chatbotService.injectMessage({
|
|
21019
|
+
id: `car-hire-depot-confirm-${Date.now()}`,
|
|
21020
|
+
role: 'assistant',
|
|
21021
|
+
text: `Searching for cars at ${depotLabel}…`,
|
|
21022
|
+
timestamp: new Date(),
|
|
21023
|
+
state: 'COLLECTING_PARAMS'
|
|
21024
|
+
});
|
|
21025
|
+
await this.runSearch(pending.parameters);
|
|
21026
|
+
}
|
|
21027
|
+
return true;
|
|
21028
|
+
}
|
|
21029
|
+
// ── Param population ──────────────────────────────────────────────────────
|
|
21030
|
+
async populateCarHireParams(parameters) {
|
|
21031
|
+
const carHireSearch = this.searchService.searches[ServiceType.Car];
|
|
21032
|
+
// Pickup datetime
|
|
21033
|
+
const pickupDate = moment$1(parameters.departureDate);
|
|
21034
|
+
carHireSearch.pickup_datetime = parameters.departureTime
|
|
21035
|
+
? moment$1(`${parameters.departureDate} ${parameters.departureTime}`, 'YYYY-MM-DD HH:mm')
|
|
21036
|
+
: pickupDate.clone().hour(10).minute(0).second(0);
|
|
21037
|
+
carHireSearch.pickup_time = parameters.departureTime || '10:00';
|
|
21038
|
+
// Dropoff datetime
|
|
21039
|
+
if (parameters.returnDate) {
|
|
21040
|
+
carHireSearch.dropoff_datetime = parameters.returnTime
|
|
21041
|
+
? moment$1(`${parameters.returnDate} ${parameters.returnTime}`, 'YYYY-MM-DD HH:mm')
|
|
21042
|
+
: moment$1(parameters.returnDate).hour(10).minute(0).second(0);
|
|
21043
|
+
carHireSearch.dropoff_time = parameters.returnTime || '10:00';
|
|
21044
|
+
}
|
|
21045
|
+
carHireSearch.return_to_pickup =
|
|
21046
|
+
parameters.returnToPickup ??
|
|
21047
|
+
(!parameters.arrival || parameters.arrival === parameters.departure);
|
|
21048
|
+
carHireSearch.adults = parameters.passengers || 1;
|
|
21049
|
+
// Delivery/collection is an Enterprise-only service applied on top of a
|
|
21050
|
+
// postcode/city search. It forces the Enterprise depot lookup, hides the
|
|
21051
|
+
// vendor (Any), and requires a driver age.
|
|
21052
|
+
const deliveryCollection = !!parameters.delivery || !!parameters.collection;
|
|
21053
|
+
carHireSearch.delivery = !!parameters.delivery;
|
|
21054
|
+
carHireSearch.collection = !!parameters.collection;
|
|
21055
|
+
carHireSearch.driverAge =
|
|
21056
|
+
parameters.driverAge || (deliveryCollection ? DEFAULT_DRIVER_AGE : 0);
|
|
21057
|
+
// Clear optional vehicle preferences left over from a previous search — the
|
|
21058
|
+
// dispatcher's reset only clears the chosen flags, not these tempParams, so
|
|
21059
|
+
// a prior vendor/class/transmission would otherwise persist into this search.
|
|
21060
|
+
carHireSearch.chosenVendor = '-';
|
|
21061
|
+
carHireSearch.carClass = '';
|
|
21062
|
+
carHireSearch.carType = '';
|
|
21063
|
+
carHireSearch.transmission = '';
|
|
21064
|
+
carHireSearch.airCon = null;
|
|
21065
|
+
if (parameters.carClass) {
|
|
21066
|
+
carHireSearch.carClass = parameters.carClass;
|
|
21067
|
+
}
|
|
21068
|
+
if (parameters.carType) {
|
|
21069
|
+
// AI agents returns the enum key name (e.g. 'Suv') but the enum value may differ (e.g. 'SUV').
|
|
21070
|
+
// Look up by key name first; fall back to treating the value as-is.
|
|
21071
|
+
carHireSearch.carType = (CarHireType[parameters.carType] ?? parameters.carType);
|
|
21072
|
+
}
|
|
21073
|
+
if (parameters.carTransmission) {
|
|
21074
|
+
carHireSearch.transmission = parameters.carTransmission;
|
|
21075
|
+
}
|
|
21076
|
+
if (parameters.airCon !== undefined) {
|
|
21077
|
+
carHireSearch.airCon = parameters.airCon;
|
|
21078
|
+
}
|
|
21079
|
+
// Delivery/collection forces Enterprise (Any vendor), so only honour a
|
|
21080
|
+
// requested vendor for standard searches.
|
|
21081
|
+
if (parameters.carVendor && !deliveryCollection) {
|
|
21082
|
+
const vendorCode = this.resolveVendorCode(carHireSearch, parameters.carVendor);
|
|
21083
|
+
if (vendorCode) {
|
|
21084
|
+
carHireSearch.chosenVendor = vendorCode;
|
|
21085
|
+
}
|
|
21086
|
+
}
|
|
21087
|
+
// Location: postcode pickups run in postcode mode (locationType = Postcode);
|
|
21088
|
+
// otherwise resolve pickup/dropoff through the shared discover-locations
|
|
21089
|
+
// service (consistent with the hotel chatbot). This returns typed candidates
|
|
21090
|
+
// (Airport / City) with coordinates, so it works across all car hire
|
|
21091
|
+
// providers and is not limited to Enterprise or to GB.
|
|
21092
|
+
if (parameters.pickupPostcode) {
|
|
21093
|
+
return this.applyPostcodePickupAndDropoff(carHireSearch, parameters, deliveryCollection);
|
|
21094
|
+
}
|
|
21095
|
+
const pickup = await this.resolveLocation(parameters.departure || parameters.departureCode || '', this.prefersAirport(parameters.departure, parameters.departureCode), parameters.departureCode);
|
|
21096
|
+
if (!pickup) {
|
|
21097
|
+
return {
|
|
21098
|
+
status: 'location-not-found',
|
|
21099
|
+
term: parameters.departure || parameters.departureCode || ''
|
|
21100
|
+
};
|
|
21101
|
+
}
|
|
21102
|
+
if (pickup.type === LocationTypes.Airport) {
|
|
21103
|
+
// Delivery/collection is not available for airport pickups — the form
|
|
21104
|
+
// forces both off in that case, so mirror that here.
|
|
21105
|
+
carHireSearch.delivery = false;
|
|
21106
|
+
carHireSearch.collection = false;
|
|
21107
|
+
carHireSearch.locationType = LocationTypes.Airport;
|
|
21108
|
+
const pickupAirport = await this.enrichAirportDetails(pickup);
|
|
21109
|
+
carHireSearch.pickup_location = pickupAirport;
|
|
21110
|
+
carHireSearch.pickup_country = pickupAirport.countryCode || '';
|
|
21111
|
+
await this.applyAirportDropoff(carHireSearch, parameters);
|
|
21112
|
+
return { status: 'ok' };
|
|
21113
|
+
}
|
|
21114
|
+
const depots = await this.applyCityPickup(carHireSearch, pickup, deliveryCollection);
|
|
21115
|
+
if (!depots.length) {
|
|
21116
|
+
return {
|
|
21117
|
+
status: 'depot-not-found',
|
|
21118
|
+
location: pickup.name || parameters.departure || ''
|
|
21119
|
+
};
|
|
21120
|
+
}
|
|
21121
|
+
if (depots.length > 1) {
|
|
21122
|
+
return {
|
|
21123
|
+
status: 'depot-choice',
|
|
21124
|
+
location: pickup.name || parameters.departure || '',
|
|
21125
|
+
depots,
|
|
21126
|
+
pickup,
|
|
21127
|
+
deliveryCollection
|
|
21128
|
+
};
|
|
21129
|
+
}
|
|
21130
|
+
carHireSearch.locationType = LocationTypes.City;
|
|
21131
|
+
await this.applyCityDropoff(carHireSearch, parameters, deliveryCollection);
|
|
21132
|
+
return { status: 'ok' };
|
|
21133
|
+
}
|
|
21134
|
+
prefersAirport(name, code) {
|
|
21135
|
+
return !!code || /\bairport\b/i.test(name ?? '');
|
|
21136
|
+
}
|
|
21137
|
+
// Maps a vendor name from the chatbot (e.g. "Avis", "Enterprise") to the
|
|
21138
|
+
// vendor code the search form expects (e.g. "ZI", "ET"). The vendor dropdown
|
|
21139
|
+
// is keyed by code, so assigning a raw name never matches an option.
|
|
21140
|
+
resolveVendorCode(carHireSearch, vendorName) {
|
|
21141
|
+
const query = vendorName.trim().toLowerCase();
|
|
21142
|
+
if (!query) {
|
|
21143
|
+
return undefined;
|
|
21144
|
+
}
|
|
21145
|
+
const vendors = (carHireSearch.vendors ?? []);
|
|
21146
|
+
const match = vendors.find((v) => v.name.toLowerCase() === query) ??
|
|
21147
|
+
vendors.find((v) => v.name.toLowerCase().startsWith(query)) ??
|
|
21148
|
+
vendors.find((v) => v.name.toLowerCase().includes(query));
|
|
21149
|
+
return match?.code;
|
|
21150
|
+
}
|
|
21151
|
+
// ── Location resolution ─────────────────────────────────────────────────────
|
|
21152
|
+
async resolveLocation(term, preferAirport, iataHint) {
|
|
21153
|
+
const query = term.replace(/,\s*/g, ' ').trim();
|
|
21154
|
+
if (query.length < 3) {
|
|
21155
|
+
return undefined;
|
|
21156
|
+
}
|
|
21157
|
+
// Airport pickups resolve through the airport-specific search (getAirportsList),
|
|
21158
|
+
// which is ranked and lets us match the exact IATA. This avoids the ambiguity
|
|
21159
|
+
// of the generic discover-locations search, which can return a like-named
|
|
21160
|
+
// airport in the wrong country first (e.g. "Barcelona" → Venezuela, not Spain).
|
|
21161
|
+
if (preferAirport) {
|
|
21162
|
+
const airport = await this.resolveAirportLocation(query, iataHint);
|
|
21163
|
+
if (airport) {
|
|
21164
|
+
return airport;
|
|
21165
|
+
}
|
|
21166
|
+
}
|
|
21167
|
+
try {
|
|
21168
|
+
const locations = await firstValueFrom(this.searchService.getDiscoverLocations(query));
|
|
21169
|
+
if (!locations?.length) {
|
|
21170
|
+
return undefined;
|
|
21171
|
+
}
|
|
21172
|
+
return this.pickBestLocation(locations, preferAirport, query, iataHint);
|
|
21173
|
+
}
|
|
21174
|
+
catch {
|
|
21175
|
+
return undefined;
|
|
21176
|
+
}
|
|
21177
|
+
}
|
|
21178
|
+
async resolveAirportLocation(query, iataHint) {
|
|
21179
|
+
const validIata = !!iataHint && /^[A-Z]{3}$/i.test(iataHint.trim());
|
|
21180
|
+
const term = validIata ? iataHint.trim() : query.replace(/\bairport\b/i, '').trim();
|
|
21181
|
+
if (term.length < 3) {
|
|
21182
|
+
return undefined;
|
|
21183
|
+
}
|
|
21184
|
+
try {
|
|
21185
|
+
const airports = await firstValueFrom(this.searchService.getAirportsList(term, undefined, undefined));
|
|
21186
|
+
if (!airports?.length) {
|
|
21187
|
+
return undefined;
|
|
21188
|
+
}
|
|
21189
|
+
const chosen = validIata
|
|
21190
|
+
? airports.find((a) => a.gateway?.toUpperCase() === iataHint.trim().toUpperCase()) ??
|
|
21191
|
+
airports[0]
|
|
21192
|
+
: airports[0];
|
|
21193
|
+
return this.airportToLocation(chosen);
|
|
21194
|
+
}
|
|
21195
|
+
catch {
|
|
21196
|
+
return undefined;
|
|
21197
|
+
}
|
|
21198
|
+
}
|
|
21199
|
+
airportToLocation(a) {
|
|
21200
|
+
return {
|
|
21201
|
+
name: a.destination ?? '',
|
|
21202
|
+
type: LocationTypes.Airport,
|
|
21203
|
+
iata: a.gateway,
|
|
21204
|
+
gateway: a.gateway,
|
|
21205
|
+
country: a.country,
|
|
21206
|
+
countryCode: a.countryCode,
|
|
21207
|
+
latitude: a.latitude,
|
|
21208
|
+
longitude: a.longitude
|
|
21209
|
+
};
|
|
21210
|
+
}
|
|
21211
|
+
pickBestLocation(locations, preferAirport, query, iataHint) {
|
|
21212
|
+
const airports = locations.filter((l) => l.type === LocationTypes.Airport);
|
|
21213
|
+
const cities = locations.filter((l) => l.type === LocationTypes.City);
|
|
21214
|
+
const validIata = iataHint && /^[A-Z]{3}$/i.test(iataHint.trim());
|
|
21215
|
+
if (validIata) {
|
|
21216
|
+
const exact = airports.find((l) => l.iata?.toUpperCase() === iataHint.toUpperCase());
|
|
21217
|
+
if (exact) {
|
|
21218
|
+
return exact;
|
|
21219
|
+
}
|
|
21220
|
+
}
|
|
21221
|
+
const firstWord = query
|
|
21222
|
+
.toLowerCase()
|
|
21223
|
+
.replace(/\b(airport|station|hub|central station)\b/g, '')
|
|
21224
|
+
.trim()
|
|
21225
|
+
.split(' ')[0];
|
|
21226
|
+
const nameMatches = (pool) => firstWord ? pool.find((l) => l.name.toLowerCase().includes(firstWord)) : undefined;
|
|
21227
|
+
if (preferAirport && airports.length) {
|
|
21228
|
+
return nameMatches(airports) ?? airports[0];
|
|
21229
|
+
}
|
|
21230
|
+
// When not preferring an airport, only return city results — never silently
|
|
21231
|
+
// fall back to an airport for a city query (e.g. "Manchester" → MAN).
|
|
21232
|
+
return nameMatches(cities) ?? cities[0];
|
|
21233
|
+
}
|
|
21234
|
+
toAirportDetails(loc) {
|
|
21235
|
+
return {
|
|
21236
|
+
destination: loc.name,
|
|
21237
|
+
gateway: loc.iata || loc.gateway || '',
|
|
21238
|
+
country: loc.country,
|
|
21239
|
+
countryCode: loc.countryCode,
|
|
21240
|
+
postcode: loc.postcode,
|
|
21241
|
+
latitude: loc.latitude,
|
|
21242
|
+
longitude: loc.longitude
|
|
21243
|
+
};
|
|
21244
|
+
}
|
|
21245
|
+
// Enriches a discover-location airport result with full OBTAirportDetails
|
|
21246
|
+
// (including countryCode) by looking up via IATA — mirrors what the normal
|
|
21247
|
+
// car hire UI does via getParamsForCarbyAirport → getAirportByCode.
|
|
21248
|
+
async enrichAirportDetails(loc) {
|
|
21249
|
+
const iata = loc.iata || loc.gateway;
|
|
21250
|
+
if (iata) {
|
|
21251
|
+
try {
|
|
21252
|
+
const airports = await firstValueFrom(this.searchService.getAirportsList(iata, undefined, undefined));
|
|
21253
|
+
if (airports?.length) {
|
|
21254
|
+
return airports[0];
|
|
21255
|
+
}
|
|
21256
|
+
}
|
|
21257
|
+
catch {
|
|
21258
|
+
// fall through to basic mapping
|
|
21259
|
+
}
|
|
21260
|
+
}
|
|
21261
|
+
return this.toAirportDetails(loc);
|
|
21262
|
+
}
|
|
21263
|
+
async applyAirportDropoff(carHireSearch, parameters) {
|
|
21264
|
+
if (carHireSearch.return_to_pickup) {
|
|
21265
|
+
carHireSearch.dropoff_location = carHireSearch.pickup_location;
|
|
21266
|
+
return;
|
|
21267
|
+
}
|
|
21268
|
+
const dropoff = await this.resolveLocation(parameters.arrival || parameters.arrivalCode || '', this.prefersAirport(parameters.arrival, parameters.arrivalCode), parameters.arrivalCode);
|
|
21269
|
+
const dropoffAirport = dropoff && dropoff.type === LocationTypes.Airport
|
|
21270
|
+
? await this.enrichAirportDetails(dropoff)
|
|
21271
|
+
: carHireSearch.pickup_location;
|
|
21272
|
+
carHireSearch.dropoff_location = dropoffAirport;
|
|
21273
|
+
carHireSearch.dropoff_country = dropoffAirport.countryCode || carHireSearch.pickup_country || '';
|
|
21274
|
+
}
|
|
21275
|
+
async applyCityPickup(carHireSearch, loc, deliveryCollection) {
|
|
21276
|
+
if (deliveryCollection) {
|
|
21277
|
+
const depots = await this.fetchEnterpriseDepots(loc, 'city');
|
|
21278
|
+
if (!depots.length) {
|
|
21279
|
+
return [];
|
|
21280
|
+
}
|
|
21281
|
+
if (depots.length === 1) {
|
|
21282
|
+
carHireSearch.cityPickup = this.toCityLocation(loc);
|
|
21283
|
+
carHireSearch.cityPickupDepot = depots[0];
|
|
21284
|
+
}
|
|
21285
|
+
return depots;
|
|
21286
|
+
}
|
|
21287
|
+
const depots = await this.resolveCityDepots(loc, carHireSearch);
|
|
21288
|
+
if (!depots.length) {
|
|
21289
|
+
return [];
|
|
21290
|
+
}
|
|
21291
|
+
if (depots.length === 1) {
|
|
21292
|
+
carHireSearch.cityPickup = this.toCityLocation(loc);
|
|
21293
|
+
carHireSearch.cityPickupDepot = depots[0];
|
|
21294
|
+
}
|
|
21295
|
+
return depots;
|
|
21296
|
+
}
|
|
21297
|
+
async applyCityDropoff(carHireSearch, parameters, deliveryCollection) {
|
|
21298
|
+
if (!carHireSearch.return_to_pickup) {
|
|
21299
|
+
const dropoff = await this.resolveLocation(parameters.arrival || parameters.arrivalCode || '', this.prefersAirport(parameters.arrival, parameters.arrivalCode), parameters.arrivalCode);
|
|
21300
|
+
const dropoffDepot = dropoff
|
|
21301
|
+
? await this.resolveDepot(dropoff, '5', deliveryCollection, 'city')
|
|
21302
|
+
: undefined;
|
|
21303
|
+
if (dropoff && dropoffDepot) {
|
|
21304
|
+
carHireSearch.cityDropoff = this.toCityLocation(dropoff);
|
|
21305
|
+
carHireSearch.cityDropoffDepot = dropoffDepot;
|
|
21306
|
+
return;
|
|
21307
|
+
}
|
|
21308
|
+
}
|
|
21309
|
+
// Return-to-pickup, or unresolved dropoff — mirror the pickup so the search
|
|
21310
|
+
// stays valid (getSearchQuery reads both pickup and dropoff depots).
|
|
21311
|
+
carHireSearch.cityDropoff = carHireSearch.cityPickup;
|
|
21312
|
+
carHireSearch.cityDropoffDepot = carHireSearch.cityPickupDepot;
|
|
21313
|
+
}
|
|
21314
|
+
toCityLocation(loc) {
|
|
21315
|
+
// discover-locations returns name as "Manchester(GB)" — strip the country
|
|
21316
|
+
// code suffix so the city name matches what the depot filter expects.
|
|
21317
|
+
const plainName = loc.city || loc.name.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
|
21318
|
+
return {
|
|
21319
|
+
id: loc.id ?? '',
|
|
21320
|
+
name: plainName,
|
|
21321
|
+
type: LocationTypes.City,
|
|
21322
|
+
country: loc.country,
|
|
21323
|
+
countryCode: loc.countryCode,
|
|
21324
|
+
city: loc.city,
|
|
21325
|
+
latitude: loc.latitude,
|
|
21326
|
+
longitude: loc.longitude
|
|
21327
|
+
};
|
|
21328
|
+
}
|
|
21329
|
+
async resolveCityDepots(loc, carHireSearch, isPostCode = false) {
|
|
21330
|
+
if (loc.latitude == null || loc.longitude == null) {
|
|
21331
|
+
return [];
|
|
21332
|
+
}
|
|
21333
|
+
const cityName = loc.city || loc.name.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
|
21334
|
+
// Mirror updateDepot: enterprise-only users use getEntCarhireLocationsCity,
|
|
21335
|
+
// everyone else uses the lat/lng getCarHireDepots path.
|
|
21336
|
+
if (!carHireSearch.hasNonEnterpriseCarAccess) {
|
|
21337
|
+
if (!cityName || !loc.countryCode) {
|
|
21338
|
+
return [];
|
|
21339
|
+
}
|
|
21340
|
+
try {
|
|
21341
|
+
const raw = await firstValueFrom(carHireSearch.getEntCarhireLocationsCity(loc.countryCode, cityName, carHireSearch.chosenVendor));
|
|
21342
|
+
return (raw ?? []).map((l) => this.toEnterpriseDepot(l));
|
|
21343
|
+
}
|
|
21344
|
+
catch {
|
|
21345
|
+
return [];
|
|
21346
|
+
}
|
|
21347
|
+
}
|
|
21348
|
+
const raw = await this.fetchDepotsWithinRadius(loc, '5');
|
|
21349
|
+
const filtered = carHireSearch.filterCarHireDepots(raw, cityName, isPostCode);
|
|
21350
|
+
if (filtered.length) {
|
|
21351
|
+
carHireSearch.chosenDistance = '5';
|
|
21352
|
+
return carHireSearch.sortCarHireDepots(filtered);
|
|
21353
|
+
}
|
|
21354
|
+
// Nothing within 5 miles — widen before giving up.
|
|
21355
|
+
const rawWide = await this.fetchDepotsWithinRadius(loc, String(WIDE_SEARCH_RADIUS_MILES));
|
|
21356
|
+
const filteredWide = carHireSearch.filterCarHireDepots(rawWide, cityName, isPostCode);
|
|
21357
|
+
if (filteredWide.length) {
|
|
21358
|
+
carHireSearch.chosenDistance = String(WIDE_SEARCH_RADIUS_MILES);
|
|
21359
|
+
}
|
|
21360
|
+
return carHireSearch.sortCarHireDepots(filteredWide);
|
|
21361
|
+
}
|
|
21362
|
+
async fetchDepotsWithinRadius(loc, distance) {
|
|
21363
|
+
try {
|
|
21364
|
+
const depots = await firstValueFrom(this.searchService.getCarHireDepots(loc.latitude, loc.longitude, distance));
|
|
21365
|
+
return depots ?? [];
|
|
21366
|
+
}
|
|
21367
|
+
catch {
|
|
21368
|
+
return [];
|
|
21369
|
+
}
|
|
21370
|
+
}
|
|
21371
|
+
// Resolves a single depot for a location — used for dropoff and postcode paths.
|
|
21372
|
+
// Delivery/collection uses the Enterprise lookup; standard searches use lat/lng.
|
|
21373
|
+
async resolveDepot(loc, distance, deliveryCollection, by, carHireSearch) {
|
|
21374
|
+
if (deliveryCollection) {
|
|
21375
|
+
return this.resolveEnterpriseDepot(loc, by);
|
|
21376
|
+
}
|
|
21377
|
+
const search = carHireSearch ?? this.searchService.searches[ServiceType.Car];
|
|
21378
|
+
const depots = await this.resolveCityDepots(loc, search, by === 'postcode');
|
|
21379
|
+
return depots[0];
|
|
21380
|
+
}
|
|
21381
|
+
async resolveEnterpriseDepot(loc, by) {
|
|
21382
|
+
const depots = await this.fetchEnterpriseDepots(loc, by);
|
|
21383
|
+
return depots[0];
|
|
21384
|
+
}
|
|
21385
|
+
async fetchEnterpriseDepots(loc, by) {
|
|
21386
|
+
const country = loc.countryCode;
|
|
21387
|
+
const term = loc.name;
|
|
21388
|
+
if (!country || !term) {
|
|
21389
|
+
return [];
|
|
21390
|
+
}
|
|
21391
|
+
try {
|
|
21392
|
+
const obs = by === 'postcode'
|
|
21393
|
+
? this.searchService.getEntLocationsPostcode(country, term, '-')
|
|
21394
|
+
: this.searchService.getEntLocationsCity(country, term, '-');
|
|
21395
|
+
const locations = await firstValueFrom(obs);
|
|
21396
|
+
return (locations ?? []).map((l) => this.toEnterpriseDepot(l));
|
|
21397
|
+
}
|
|
21398
|
+
catch {
|
|
21399
|
+
return [];
|
|
21400
|
+
}
|
|
21401
|
+
}
|
|
21402
|
+
toEnterpriseDepot(loc) {
|
|
21403
|
+
return {
|
|
21404
|
+
addressLines: loc.address || null,
|
|
21405
|
+
cityName: loc.city || null,
|
|
21406
|
+
countryCode: loc.country || null,
|
|
21407
|
+
extendedLocationCode: loc.code || null,
|
|
21408
|
+
latitude: null,
|
|
21409
|
+
locationCode: loc.postcode || null,
|
|
21410
|
+
locationType: null,
|
|
21411
|
+
longitude: null,
|
|
21412
|
+
vendor: loc.vendor,
|
|
21413
|
+
vendorCode: loc.vendor
|
|
21414
|
+
};
|
|
21415
|
+
}
|
|
21416
|
+
// ── Postcode resolution ─────────────────────────────────────────────────────
|
|
21417
|
+
async applyPostcodePickupAndDropoff(carHireSearch, parameters, deliveryCollection) {
|
|
21418
|
+
const country = parameters.country || 'GB';
|
|
21419
|
+
const distance = String(parameters.distance ?? 5);
|
|
21420
|
+
carHireSearch.chosenDistance = distance;
|
|
21421
|
+
const pickup = await this.resolvePostcode(parameters.pickupPostcode, country);
|
|
21422
|
+
if (!pickup) {
|
|
21423
|
+
return { status: 'location-not-found', term: parameters.pickupPostcode };
|
|
21424
|
+
}
|
|
21425
|
+
// Delivery/collection uses Enterprise-only depot lookup (mirrors the form's
|
|
21426
|
+
// behaviour when delivery/collection checkboxes are checked).
|
|
21427
|
+
if (deliveryCollection) {
|
|
21428
|
+
const allDepots = await this.fetchEnterpriseDepots(pickup, 'postcode');
|
|
21429
|
+
if (!allDepots.length) {
|
|
21430
|
+
return { status: 'depot-not-found', location: parameters.pickupPostcode };
|
|
21431
|
+
}
|
|
21432
|
+
if (allDepots.length > 1) {
|
|
21433
|
+
return {
|
|
21434
|
+
status: 'depot-choice',
|
|
21435
|
+
location: parameters.pickupPostcode,
|
|
21436
|
+
depots: allDepots,
|
|
21437
|
+
pickup,
|
|
21438
|
+
deliveryCollection
|
|
21439
|
+
};
|
|
21440
|
+
}
|
|
21441
|
+
carHireSearch.postcodePickupRaw = pickup;
|
|
21442
|
+
carHireSearch.postcodePickup = allDepots[0];
|
|
21443
|
+
carHireSearch.postcodeDropoffRaw = pickup;
|
|
21444
|
+
carHireSearch.postcodeDropoff = allDepots[0];
|
|
21445
|
+
return { status: 'ok' };
|
|
21446
|
+
}
|
|
21447
|
+
const allDepots = await this.resolveCityDepots(pickup, carHireSearch, true);
|
|
21448
|
+
if (!allDepots.length) {
|
|
21449
|
+
return { status: 'depot-not-found', location: parameters.pickupPostcode };
|
|
21450
|
+
}
|
|
21451
|
+
if (allDepots.length > 1) {
|
|
21452
|
+
return {
|
|
21453
|
+
status: 'depot-choice',
|
|
21454
|
+
location: parameters.pickupPostcode,
|
|
21455
|
+
depots: allDepots,
|
|
21456
|
+
pickup,
|
|
21457
|
+
deliveryCollection
|
|
21458
|
+
};
|
|
21459
|
+
}
|
|
21460
|
+
const pickupDepot = allDepots[0];
|
|
21461
|
+
carHireSearch.postcodePickupRaw = pickup;
|
|
21462
|
+
carHireSearch.postcodePickup = pickupDepot;
|
|
21463
|
+
if (carHireSearch.return_to_pickup || !parameters.dropoffPostcode) {
|
|
21464
|
+
carHireSearch.postcodeDropoffRaw = pickup;
|
|
21465
|
+
carHireSearch.postcodeDropoff = pickupDepot;
|
|
21466
|
+
return { status: 'ok' };
|
|
21467
|
+
}
|
|
21468
|
+
const dropoff = await this.resolvePostcode(parameters.dropoffPostcode, country);
|
|
21469
|
+
const dropoffDepots = dropoff
|
|
21470
|
+
? await this.resolveCityDepots(dropoff, carHireSearch, true)
|
|
21471
|
+
: [];
|
|
21472
|
+
if (dropoff && dropoffDepots.length) {
|
|
21473
|
+
carHireSearch.postcodeDropoffRaw = dropoff;
|
|
21474
|
+
carHireSearch.postcodeDropoff = dropoffDepots[0];
|
|
21475
|
+
return { status: 'ok' };
|
|
21476
|
+
}
|
|
21477
|
+
// Unresolved dropoff — mirror the pickup so the search stays valid.
|
|
21478
|
+
carHireSearch.postcodeDropoffRaw = pickup;
|
|
21479
|
+
carHireSearch.postcodeDropoff = pickupDepot;
|
|
21480
|
+
return { status: 'ok' };
|
|
21481
|
+
}
|
|
21482
|
+
async resolvePostcode(term, country) {
|
|
21483
|
+
const query = term.trim();
|
|
21484
|
+
if (query.length < 2) {
|
|
21485
|
+
return undefined;
|
|
21486
|
+
}
|
|
21487
|
+
try {
|
|
21488
|
+
const results = await firstValueFrom(this.searchService.getPostcodeList(query, country));
|
|
21489
|
+
return results?.[0];
|
|
21490
|
+
}
|
|
21491
|
+
catch {
|
|
21492
|
+
return undefined;
|
|
21493
|
+
}
|
|
21494
|
+
}
|
|
21495
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotCarHireSearchService, deps: [{ token: ChatbotService }, { token: EnterpriseSearchService }, { token: ResultAwareService }, { token: UserService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
21496
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotCarHireSearchService, providedIn: 'root' });
|
|
21497
|
+
}
|
|
21498
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotCarHireSearchService, decorators: [{
|
|
21499
|
+
type: Injectable,
|
|
21500
|
+
args: [{ providedIn: 'root' }]
|
|
21501
|
+
}], ctorParameters: () => [{ type: ChatbotService }, { type: EnterpriseSearchService }, { type: ResultAwareService }, { type: UserService }] });
|
|
21502
|
+
|
|
21503
|
+
const MAX_TAXI_CARDS = 3;
|
|
21504
|
+
class ChatbotTaxiSearchService {
|
|
21505
|
+
chatbotService;
|
|
21506
|
+
searchService;
|
|
21507
|
+
resultAwareService;
|
|
21508
|
+
travelType = 'taxi';
|
|
21509
|
+
constructor(chatbotService, searchService, resultAwareService) {
|
|
21510
|
+
this.chatbotService = chatbotService;
|
|
21511
|
+
this.searchService = searchService;
|
|
21512
|
+
this.resultAwareService = resultAwareService;
|
|
21513
|
+
}
|
|
21514
|
+
async handle(parameters, autoSearch) {
|
|
21515
|
+
this.searchService.resetSearchChosenObjects();
|
|
21516
|
+
this.searchService.resetSearchPriorities();
|
|
21517
|
+
this.searchService.search_objects[ServiceType.Cab].chosen = true;
|
|
21518
|
+
this.searchService.search_objects[ServiceType.Cab].originalChosen = true;
|
|
21519
|
+
this.searchService.determineHighestSearchPriority();
|
|
21520
|
+
if (autoSearch) {
|
|
21521
|
+
this.chatbotService.setSearching(true, 'taxi');
|
|
21522
|
+
}
|
|
21523
|
+
await this.populateTaxiParams(parameters);
|
|
21524
|
+
if (!autoSearch) {
|
|
21525
|
+
return;
|
|
21526
|
+
}
|
|
21527
|
+
await this.runSearch(parameters);
|
|
21528
|
+
}
|
|
21529
|
+
async runSearch(parameters) {
|
|
21530
|
+
const taxiSearch = this.searchService.searches[ServiceType.Cab];
|
|
21531
|
+
this.resultAwareService.clearResults();
|
|
21532
|
+
const hasResults = await taxiSearch.startSearch();
|
|
21533
|
+
if (!hasResults) {
|
|
21534
|
+
this.injectNoResultsMessage();
|
|
21535
|
+
this.chatbotService.setSearching(false);
|
|
21536
|
+
return;
|
|
21537
|
+
}
|
|
21538
|
+
const results$ = taxiSearch.results;
|
|
21539
|
+
results$
|
|
21540
|
+
.pipe(filter((results) => results?.length > 0), take(1), catchError$1((err) => {
|
|
21541
|
+
console.warn('[ChatbotTaxiSearchService] results polling error', err);
|
|
21542
|
+
this.chatbotService.setSearching(false);
|
|
21543
|
+
return EMPTY;
|
|
21544
|
+
}))
|
|
21545
|
+
.subscribe((results) => {
|
|
21546
|
+
const topResults = results.slice(0, MAX_TAXI_CARDS);
|
|
21547
|
+
const cards = this.buildTaxiCards(topResults, parameters);
|
|
21548
|
+
const fromName = parameters.departure || '';
|
|
21549
|
+
const toName = parameters.arrival || '';
|
|
21550
|
+
const dateStr = parameters.departureDate
|
|
21551
|
+
? moment$1(parameters.departureDate).format('D MMM')
|
|
21552
|
+
: '';
|
|
21553
|
+
const searchHeader = [fromName, toName, dateStr].filter(Boolean).join(' · ');
|
|
21554
|
+
const taxiResults = { cards, searchHeader, parameters };
|
|
21555
|
+
this.chatbotService.patchAssistantMessage({ taxiResults, state: 'RESULTS_READY' });
|
|
21556
|
+
this.chatbotService.setSearching(false);
|
|
21557
|
+
});
|
|
21558
|
+
}
|
|
21559
|
+
buildTaxiCards(results, parameters) {
|
|
21560
|
+
return results.map((r, i) => {
|
|
21561
|
+
const fromLoc = r.fromLocation?.airportStationName ||
|
|
21562
|
+
r.fromLocation?.streetAndNumber ||
|
|
21563
|
+
parameters.departure ||
|
|
21564
|
+
'';
|
|
21565
|
+
const toLoc = r.toLocation?.airportStationName ||
|
|
21566
|
+
r.toLocation?.streetAndNumber ||
|
|
21567
|
+
parameters.arrival ||
|
|
21568
|
+
'';
|
|
21569
|
+
let policyStatus;
|
|
21570
|
+
if (r.unavailable) {
|
|
21571
|
+
policyStatus = 'UNAVAILABLE';
|
|
21572
|
+
}
|
|
21573
|
+
else if (r.requiresReasonKeys?.length || r.requiresApprovalKeys?.length) {
|
|
21574
|
+
policyStatus = 'ALLOW_WITH_REASON';
|
|
21575
|
+
}
|
|
21576
|
+
else {
|
|
21577
|
+
policyStatus = 'IN_POLICY';
|
|
21578
|
+
}
|
|
21579
|
+
return {
|
|
21580
|
+
index: i + 1,
|
|
21581
|
+
supplierName: r.supplierName || '',
|
|
21582
|
+
vehicleType: r.vehicleType || '',
|
|
21583
|
+
price: r.price ?? 0,
|
|
21584
|
+
currencyCode: r.currency || 'GBP',
|
|
21585
|
+
departs: r.departs || '',
|
|
21586
|
+
fromLocation: fromLoc,
|
|
21587
|
+
toLocation: toLoc,
|
|
21588
|
+
tripDistanceMetres: r.tripDistanceMetres ?? undefined,
|
|
21589
|
+
tripDurationSeconds: r.tripDurationSeconds ?? undefined,
|
|
21590
|
+
policyStatus,
|
|
21591
|
+
policyMessages: r.requiresReasonMessages ?? undefined,
|
|
21592
|
+
co2: r.co2PerItem || undefined,
|
|
21593
|
+
};
|
|
21594
|
+
});
|
|
21595
|
+
}
|
|
21596
|
+
injectNoResultsMessage() {
|
|
21597
|
+
this.chatbotService.injectMessage({
|
|
21598
|
+
id: `taxi-no-results-${Date.now()}`,
|
|
21599
|
+
role: 'assistant',
|
|
21600
|
+
text: 'No taxi results found for your search. Try adjusting your pickup, dropoff or time.',
|
|
21601
|
+
timestamp: new Date(),
|
|
21602
|
+
state: 'SELECTING_RESULT',
|
|
21603
|
+
});
|
|
21604
|
+
}
|
|
21605
|
+
async populateTaxiParams(parameters) {
|
|
21606
|
+
const taxiSearch = this.searchService.searches[ServiceType.Cab];
|
|
21607
|
+
const departDate = moment$1(parameters.departureDate);
|
|
21608
|
+
taxiSearch.date = {
|
|
21609
|
+
year: departDate.year(),
|
|
21610
|
+
month: departDate.month() + 1,
|
|
21611
|
+
day: departDate.date(),
|
|
21612
|
+
};
|
|
21613
|
+
if (parameters.departureTime) {
|
|
21614
|
+
const parsed = moment$1(parameters.departureTime, ['HH:mm', 'H:mm', 'ha', 'h:mma']);
|
|
21615
|
+
taxiSearch.time = parsed.isValid() ? parsed.format('HH:mm') : parameters.departureTime;
|
|
21616
|
+
}
|
|
21617
|
+
taxiSearch.adults = parameters.passengers || 1;
|
|
21618
|
+
if (parameters.vehicleType) {
|
|
21619
|
+
taxiSearch.vehicleType = parameters.vehicleType;
|
|
21620
|
+
}
|
|
21621
|
+
const [fromResolved, toResolved, viaResolved] = await Promise.all([
|
|
21622
|
+
this.resolveGeoLocation(parameters.departure, taxiSearch),
|
|
21623
|
+
this.resolveGeoLocation(parameters.arrival, taxiSearch),
|
|
21624
|
+
parameters.via ? this.resolveGeoLocation(parameters.via, taxiSearch) : Promise.resolve(null),
|
|
21625
|
+
]);
|
|
21626
|
+
if (fromResolved) {
|
|
21627
|
+
const { result, address } = fromResolved;
|
|
21628
|
+
if (result.terminals?.length) {
|
|
21629
|
+
taxiSearch.fromTerminals = result.terminals;
|
|
21630
|
+
}
|
|
21631
|
+
address.Terminal = parameters.pickupTerminal
|
|
21632
|
+
? this.matchTerminal(parameters.pickupTerminal, result.terminals || [])
|
|
21633
|
+
: null;
|
|
21634
|
+
if (parameters.pickupFlightNumber) {
|
|
21635
|
+
address.FlightNumber = parameters.pickupFlightNumber;
|
|
21636
|
+
}
|
|
21637
|
+
taxiSearch.fromAddress = address;
|
|
21638
|
+
}
|
|
21639
|
+
if (toResolved) {
|
|
21640
|
+
const { result, address } = toResolved;
|
|
21641
|
+
if (result.terminals?.length) {
|
|
21642
|
+
taxiSearch.toTerminals = result.terminals;
|
|
21643
|
+
}
|
|
21644
|
+
address.Terminal = parameters.dropoffTerminal
|
|
21645
|
+
? this.matchTerminal(parameters.dropoffTerminal, result.terminals || [])
|
|
21646
|
+
: null;
|
|
21647
|
+
if (parameters.dropoffFlightNumber) {
|
|
21648
|
+
address.FlightNumber = parameters.dropoffFlightNumber;
|
|
21649
|
+
}
|
|
21650
|
+
taxiSearch.toAddress = address;
|
|
21651
|
+
}
|
|
21652
|
+
if (viaResolved) {
|
|
21653
|
+
taxiSearch.viasTo = viaResolved.address;
|
|
21654
|
+
}
|
|
21655
|
+
}
|
|
21656
|
+
async resolveGeoLocation(searchTerm, taxiSearch) {
|
|
21657
|
+
if (!searchTerm)
|
|
21658
|
+
return null;
|
|
21659
|
+
try {
|
|
21660
|
+
const results = await firstValueFrom(this.searchService.getGeoLocations(searchTerm));
|
|
21661
|
+
if (results?.length) {
|
|
21662
|
+
const term = searchTerm.toLowerCase();
|
|
21663
|
+
let location = results[0];
|
|
21664
|
+
if (term.includes('airport')) {
|
|
21665
|
+
location = results.find((r) => r.type === 'Airport') ?? results[0];
|
|
21666
|
+
}
|
|
21667
|
+
else if (term.includes('station') || term.includes('rail')) {
|
|
21668
|
+
location = results.find((r) => r.type === 'Train Station') ?? results[0];
|
|
21669
|
+
}
|
|
21670
|
+
const address = taxiSearch.getAddress(location);
|
|
21671
|
+
if (location.type === 'Airport' && !address?.Town) {
|
|
21672
|
+
address.Town = location.name;
|
|
21673
|
+
}
|
|
21674
|
+
return { result: location, address };
|
|
21675
|
+
}
|
|
21676
|
+
}
|
|
21677
|
+
catch {
|
|
21678
|
+
}
|
|
21679
|
+
return null;
|
|
21680
|
+
}
|
|
21681
|
+
matchTerminal(aiTerminal, terminals) {
|
|
21682
|
+
const t = aiTerminal.toLowerCase().trim();
|
|
21683
|
+
if (!t || t.includes('any') || t.includes('main'))
|
|
21684
|
+
return null;
|
|
21685
|
+
const byValue = terminals.find((x) => x.value.toLowerCase() === t);
|
|
21686
|
+
if (byValue)
|
|
21687
|
+
return byValue.key;
|
|
21688
|
+
const byKey = terminals.find((x) => x.key.toLowerCase().includes(t) || t.includes(x.value.toLowerCase()));
|
|
21689
|
+
return byKey ? byKey.key : null;
|
|
21690
|
+
}
|
|
21691
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotTaxiSearchService, deps: [{ token: ChatbotService }, { token: EnterpriseSearchService }, { token: ResultAwareService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
21692
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotTaxiSearchService, providedIn: 'root' });
|
|
21693
|
+
}
|
|
21694
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotTaxiSearchService, decorators: [{
|
|
21695
|
+
type: Injectable,
|
|
21696
|
+
args: [{ providedIn: 'root' }]
|
|
21697
|
+
}], ctorParameters: () => [{ type: ChatbotService }, { type: EnterpriseSearchService }, { type: ResultAwareService }] });
|
|
21698
|
+
|
|
21699
|
+
/** Safety timeout (ms) waiting for eurostar results. */
|
|
21700
|
+
const EUROSTAR_RESULTS_WAIT_MS = 60_000;
|
|
21701
|
+
/** Maximum number of outbound/inbound cards to surface in the chat widget. */
|
|
21702
|
+
const MAX_EUROSTAR_CARDS = 3;
|
|
21703
|
+
/**
|
|
21704
|
+
* Handles chatbot-driven Eurostar searches. Registered with
|
|
21705
|
+
* `ChatbotSearchDispatcherService`; not used directly.
|
|
21706
|
+
*
|
|
21707
|
+
* Populates the Eurostar search state from the chatbot's parameters and, when
|
|
21708
|
+
* `autoSearch` is true, runs the search headlessly so it works from any page.
|
|
21709
|
+
* Once results arrive, builds `ChatEurostarResults` and patches them onto the
|
|
21710
|
+
* last assistant message via {@link ChatbotService.patchAssistantMessage}.
|
|
21711
|
+
*/
|
|
21712
|
+
class ChatbotEurostarSearchService {
|
|
21713
|
+
chatbotService;
|
|
21714
|
+
searchService;
|
|
21715
|
+
userService;
|
|
21716
|
+
travelType = 'eurostar';
|
|
21717
|
+
resultsSubscription = null;
|
|
21718
|
+
constructor(chatbotService, searchService, userService) {
|
|
21719
|
+
this.chatbotService = chatbotService;
|
|
21720
|
+
this.searchService = searchService;
|
|
21721
|
+
this.userService = userService;
|
|
21722
|
+
}
|
|
21723
|
+
async handle(parameters, autoSearch) {
|
|
21724
|
+
this.searchService.resetSearchChosenObjects();
|
|
21725
|
+
this.searchService.resetSearchPriorities();
|
|
21726
|
+
this.searchService.search_objects[ServiceType.Eurostar].chosen = true;
|
|
21727
|
+
this.searchService.search_objects[ServiceType.Eurostar].originalChosen = true;
|
|
21728
|
+
this.searchService.determineHighestSearchPriority();
|
|
21729
|
+
// Auto-add the signed-in user as the traveller when none is selected so
|
|
21730
|
+
// the search isn't blocked by the "add a traveller" validation rule.
|
|
21731
|
+
// Matches the rail handler pattern.
|
|
21732
|
+
if (this.searchService.travellerInformation.value.length === 0) {
|
|
21733
|
+
const selfTraveller = this.userService.createSelfTraveller();
|
|
21734
|
+
if (selfTraveller) {
|
|
21735
|
+
this.searchService.addTraveller(selfTraveller);
|
|
21736
|
+
}
|
|
21737
|
+
}
|
|
21738
|
+
const departDate = moment$1(parameters.departureDate);
|
|
21739
|
+
const departNgbDate = {
|
|
21740
|
+
year: departDate.year(),
|
|
21741
|
+
month: departDate.month() + 1,
|
|
21742
|
+
day: departDate.date()
|
|
21743
|
+
};
|
|
21744
|
+
await this.populateEurostarParams(parameters, departNgbDate);
|
|
21745
|
+
if (!autoSearch) {
|
|
21746
|
+
return;
|
|
21747
|
+
}
|
|
21748
|
+
await this.runSearch(parameters);
|
|
21749
|
+
}
|
|
21750
|
+
// ── Search ────────────────────────────────────────────────────────────────
|
|
21751
|
+
async runSearch(parameters) {
|
|
21752
|
+
const eurostarSearch = this.searchService.searches[ServiceType.Eurostar];
|
|
20112
21753
|
// Surface OBT's existing validation (past dates, missing stations,
|
|
20113
21754
|
// same origin/destination, return-before-depart, incompatible cabins)
|
|
20114
21755
|
// before triggering the search — matches the rail handler pattern.
|
|
@@ -20281,11 +21922,15 @@ class ChatbotEurostarSearchService {
|
|
|
20281
21922
|
// used by the Eurostar search class internals. Always set both outbound
|
|
20282
21923
|
// and return cabin classes — `updateSerchQuery` calls
|
|
20283
21924
|
// `turnCabinTypeToName(returnCabinClass)` on return trips and crashes if
|
|
20284
|
-
// it's undefined.
|
|
20285
|
-
//
|
|
21925
|
+
// it's undefined. The return leg uses its own class when the user asked for
|
|
21926
|
+
// a different one (e.g. business out, standard return); otherwise it
|
|
21927
|
+
// mirrors the outbound class, satisfying the cabin-match validation rule
|
|
21928
|
+
// (`checkCabinMatchisValid`).
|
|
20286
21929
|
const mappedCabin = this.mapCabinClass(parameters.cabinClass);
|
|
20287
21930
|
eurostarParams.cabinClass = mappedCabin;
|
|
20288
|
-
eurostarParams.returnCabinClass =
|
|
21931
|
+
eurostarParams.returnCabinClass = parameters.returnCabinClass
|
|
21932
|
+
? this.mapCabinClass(parameters.returnCabinClass)
|
|
21933
|
+
: mappedCabin;
|
|
20289
21934
|
// Always set maxConnections so a previous "direct only" search doesn't
|
|
20290
21935
|
// persist when the user removes that constraint. `null` = no limit.
|
|
20291
21936
|
eurostarParams.maxConnections =
|
|
@@ -20352,6 +21997,216 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
|
|
|
20352
21997
|
args: [{ providedIn: 'root' }]
|
|
20353
21998
|
}], ctorParameters: () => [{ type: ChatbotService }, { type: EnterpriseSearchService }, { type: UserService }] });
|
|
20354
21999
|
|
|
22000
|
+
/** How long (ms) to wait for lounge results before giving up. */
|
|
22001
|
+
const LOUNGE_RESULTS_WAIT_MS = 60_000;
|
|
22002
|
+
/** Maximum number of lounge cards to show in chat. */
|
|
22003
|
+
const MAX_LOUNGE_CARDS = 3;
|
|
22004
|
+
/**
|
|
22005
|
+
* Handles chatbot-driven lounge searches. Registered with
|
|
22006
|
+
* `ChatbotSearchDispatcherService`; not used directly.
|
|
22007
|
+
*
|
|
22008
|
+
* Populates the lounge search state from the chatbot's parameters and runs
|
|
22009
|
+
* `startSearch()` headlessly so chatbot lounge searches work from any page.
|
|
22010
|
+
* Once results arrive, builds `ChatLoungeResultCard`s and patches them onto
|
|
22011
|
+
* the last assistant message via {@link ChatbotService.patchAssistantMessage}.
|
|
22012
|
+
*/
|
|
22013
|
+
class ChatbotLoungeSearchService {
|
|
22014
|
+
chatbotService;
|
|
22015
|
+
searchService;
|
|
22016
|
+
resultAwareService;
|
|
22017
|
+
userService;
|
|
22018
|
+
travelType = 'lounge';
|
|
22019
|
+
resultsSubscription = null;
|
|
22020
|
+
constructor(chatbotService, searchService, resultAwareService, userService) {
|
|
22021
|
+
this.chatbotService = chatbotService;
|
|
22022
|
+
this.searchService = searchService;
|
|
22023
|
+
this.resultAwareService = resultAwareService;
|
|
22024
|
+
this.userService = userService;
|
|
22025
|
+
}
|
|
22026
|
+
async handle(parameters, autoSearch) {
|
|
22027
|
+
this.searchService.resetSearchChosenObjects();
|
|
22028
|
+
this.searchService.resetSearchPriorities();
|
|
22029
|
+
this.searchService.search_objects[ServiceType.Lounge].chosen = true;
|
|
22030
|
+
this.searchService.search_objects[ServiceType.Lounge].originalChosen = true;
|
|
22031
|
+
this.searchService.determineHighestSearchPriority();
|
|
22032
|
+
if (this.searchService.travellerInformation.value.length === 0) {
|
|
22033
|
+
const selfTraveller = this.userService.createSelfTraveller();
|
|
22034
|
+
if (selfTraveller) {
|
|
22035
|
+
this.searchService.addTraveller(selfTraveller);
|
|
22036
|
+
}
|
|
22037
|
+
}
|
|
22038
|
+
if (autoSearch) {
|
|
22039
|
+
this.subscribeToLoungeResults(parameters);
|
|
22040
|
+
}
|
|
22041
|
+
await this.populateLoungeParams(parameters);
|
|
22042
|
+
if (!autoSearch) {
|
|
22043
|
+
return;
|
|
22044
|
+
}
|
|
22045
|
+
const loungeSearch = this.searchService.searches[ServiceType.Lounge];
|
|
22046
|
+
const validationMessages = Array.isArray(loungeSearch.validationMessages)
|
|
22047
|
+
? loungeSearch.validationMessages.filter(Boolean)
|
|
22048
|
+
: [];
|
|
22049
|
+
if (validationMessages.length) {
|
|
22050
|
+
this.resultsSubscription?.unsubscribe();
|
|
22051
|
+
this.resultsSubscription = null;
|
|
22052
|
+
this.chatbotService.patchAssistantMessage({
|
|
22053
|
+
text: this.formatValidationMessages(validationMessages),
|
|
22054
|
+
state: 'RESULTS_READY'
|
|
22055
|
+
});
|
|
22056
|
+
this.chatbotService.setSearching(false);
|
|
22057
|
+
return;
|
|
22058
|
+
}
|
|
22059
|
+
const ok = await loungeSearch.startSearch();
|
|
22060
|
+
if (!ok) {
|
|
22061
|
+
this.resultsSubscription?.unsubscribe();
|
|
22062
|
+
this.resultsSubscription = null;
|
|
22063
|
+
const messages = Array.isArray(loungeSearch.validationMessages)
|
|
22064
|
+
? loungeSearch.validationMessages.filter(Boolean)
|
|
22065
|
+
: [];
|
|
22066
|
+
this.chatbotService.patchAssistantMessage({
|
|
22067
|
+
text: messages.length
|
|
22068
|
+
? this.formatValidationMessages(messages)
|
|
22069
|
+
: `I couldn't find any lounges — please check the airport and date and try again.`,
|
|
22070
|
+
state: 'RESULTS_READY'
|
|
22071
|
+
});
|
|
22072
|
+
this.chatbotService.setSearching(false);
|
|
22073
|
+
}
|
|
22074
|
+
}
|
|
22075
|
+
// ── Result capture + card building ────────────────────────────────────────
|
|
22076
|
+
subscribeToLoungeResults(parameters) {
|
|
22077
|
+
this.resultsSubscription?.unsubscribe();
|
|
22078
|
+
this.resultAwareService.clearResults();
|
|
22079
|
+
this.chatbotService.setSearching(true, 'lounge');
|
|
22080
|
+
const loungeSearch = this.searchService.searches[ServiceType.Lounge];
|
|
22081
|
+
this.resultsSubscription = loungeSearch.results
|
|
22082
|
+
.pipe(filter((results) => Array.isArray(results) && results.length > 0), take(1), timeout$1(LOUNGE_RESULTS_WAIT_MS), catchError$1((err) => {
|
|
22083
|
+
console.warn('[ChatbotLoungeSearchService] lounge results timed out or errored', err);
|
|
22084
|
+
this.chatbotService.patchAssistantMessage({
|
|
22085
|
+
text: `I couldn't find any lounges for that airport. It may not have lounge access available.`,
|
|
22086
|
+
state: 'RESULTS_READY'
|
|
22087
|
+
});
|
|
22088
|
+
this.chatbotService.setSearching(false);
|
|
22089
|
+
return EMPTY;
|
|
22090
|
+
}))
|
|
22091
|
+
.subscribe((results) => {
|
|
22092
|
+
const cards = this.buildLoungeCards(results, parameters);
|
|
22093
|
+
const airport = typeof loungeSearch.locationCode === 'object'
|
|
22094
|
+
? loungeSearch.locationCode.destination ?? parameters.departure
|
|
22095
|
+
: parameters.departure;
|
|
22096
|
+
const dateLabel = parameters.departureDate
|
|
22097
|
+
? moment$1(parameters.departureDate).format('D MMM YYYY')
|
|
22098
|
+
: '';
|
|
22099
|
+
const searchHeader = [airport, dateLabel].filter(Boolean).join(' · ');
|
|
22100
|
+
const loungeResults = { cards, searchHeader, parameters };
|
|
22101
|
+
this.chatbotService.patchAssistantMessage({ loungeResults, state: 'RESULTS_READY' });
|
|
22102
|
+
this.chatbotService.setSearching(false);
|
|
22103
|
+
});
|
|
22104
|
+
}
|
|
22105
|
+
buildLoungeCards(results, parameters) {
|
|
22106
|
+
return results.slice(0, MAX_LOUNGE_CARDS).map((r, i) => {
|
|
22107
|
+
const policyStatus = r.unavailable
|
|
22108
|
+
? 'UNAVAILABLE'
|
|
22109
|
+
: r.requiresReasonKeys?.length
|
|
22110
|
+
? 'ALLOW_WITH_REASON'
|
|
22111
|
+
: 'IN_POLICY';
|
|
22112
|
+
const isPreferred = !!(r.tmcPreferred || r.companyPreferred || r.officePreferred);
|
|
22113
|
+
return {
|
|
22114
|
+
index: i,
|
|
22115
|
+
date: parameters.departureDate
|
|
22116
|
+
? moment$1(parameters.departureDate).format('ddd, D MMM YYYY')
|
|
22117
|
+
: '',
|
|
22118
|
+
name: r.name ?? '',
|
|
22119
|
+
airport: r.lounge?.airport ?? parameters.departure,
|
|
22120
|
+
terminal: r.lounge?.terminal != null ? String(r.lounge.terminal) : undefined,
|
|
22121
|
+
price: r.price ?? 0,
|
|
22122
|
+
currency: r.currency ?? 'GBP',
|
|
22123
|
+
policyStatus,
|
|
22124
|
+
policyMessages: r.requiresReasonMessages ?? [],
|
|
22125
|
+
isPreferred,
|
|
22126
|
+
unavailable: !!r.unavailable,
|
|
22127
|
+
unavailableMessage: r.unavailableMessage ?? undefined,
|
|
22128
|
+
openTime: r.lounge?.openTime ?? undefined,
|
|
22129
|
+
closeTime: r.lounge?.closeTime ?? undefined
|
|
22130
|
+
};
|
|
22131
|
+
});
|
|
22132
|
+
}
|
|
22133
|
+
formatValidationMessages(messages) {
|
|
22134
|
+
if (messages.length === 1) {
|
|
22135
|
+
return `I couldn't run that search — ${messages[0]}. Please adjust and try again.`;
|
|
22136
|
+
}
|
|
22137
|
+
const bullets = messages.map((m) => `• ${m}`).join('\n');
|
|
22138
|
+
return `I couldn't run that search — please fix the following and try again:\n${bullets}`;
|
|
22139
|
+
}
|
|
22140
|
+
// ── Param population ──────────────────────────────────────────────────────
|
|
22141
|
+
async populateLoungeParams(parameters) {
|
|
22142
|
+
const loungeSearch = this.searchService.searches[ServiceType.Lounge];
|
|
22143
|
+
// Resolve airport
|
|
22144
|
+
const airport = await this.resolveAirport(parameters.locationCode, parameters.departure);
|
|
22145
|
+
if (airport) {
|
|
22146
|
+
loungeSearch.locationCode = airport;
|
|
22147
|
+
if (airport.countryCode) {
|
|
22148
|
+
loungeSearch.countryCode = airport.countryCode;
|
|
22149
|
+
}
|
|
22150
|
+
}
|
|
22151
|
+
// Set arrival datetime from date + time
|
|
22152
|
+
const departDate = moment$1(parameters.departureDate);
|
|
22153
|
+
if (parameters.departureTime) {
|
|
22154
|
+
const timeParsed = moment$1(parameters.departureTime, ['H:mm', 'HH:mm', 'ha', 'h:mma', 'h a', 'h:mm a']);
|
|
22155
|
+
if (timeParsed.isValid()) {
|
|
22156
|
+
departDate.hours(timeParsed.hours()).minutes(timeParsed.minutes()).seconds(0);
|
|
22157
|
+
}
|
|
22158
|
+
}
|
|
22159
|
+
loungeSearch.arrivalDatetime = departDate;
|
|
22160
|
+
// Terminal
|
|
22161
|
+
if (parameters.terminal) {
|
|
22162
|
+
loungeSearch.terminal = parameters.terminal;
|
|
22163
|
+
}
|
|
22164
|
+
// Flight number
|
|
22165
|
+
if (parameters.flight) {
|
|
22166
|
+
loungeSearch.flightNumber = parameters.flight;
|
|
22167
|
+
}
|
|
22168
|
+
// Passengers
|
|
22169
|
+
if (parameters.passengers != null) {
|
|
22170
|
+
loungeSearch.adults = parameters.passengers;
|
|
22171
|
+
}
|
|
22172
|
+
if (parameters.children != null) {
|
|
22173
|
+
loungeSearch.children = parameters.children;
|
|
22174
|
+
}
|
|
22175
|
+
}
|
|
22176
|
+
async resolveAirport(code, name) {
|
|
22177
|
+
const searchTerm = code ?? name;
|
|
22178
|
+
if (!searchTerm)
|
|
22179
|
+
return null;
|
|
22180
|
+
try {
|
|
22181
|
+
const airports = await firstValueFrom(this.searchService.getAirportsList(searchTerm, null, null, true));
|
|
22182
|
+
if (!airports?.length) {
|
|
22183
|
+
// Fall back to name if code search returned nothing
|
|
22184
|
+
if (code && name) {
|
|
22185
|
+
const byName = await firstValueFrom(this.searchService.getAirportsList(name, null, null, true));
|
|
22186
|
+
return byName?.[0] ?? null;
|
|
22187
|
+
}
|
|
22188
|
+
return null;
|
|
22189
|
+
}
|
|
22190
|
+
// Prefer exact IATA code match
|
|
22191
|
+
if (code) {
|
|
22192
|
+
const exact = airports.find((a) => a?.gateway?.toUpperCase() === code.toUpperCase());
|
|
22193
|
+
if (exact)
|
|
22194
|
+
return exact;
|
|
22195
|
+
}
|
|
22196
|
+
return airports[0];
|
|
22197
|
+
}
|
|
22198
|
+
catch {
|
|
22199
|
+
return null;
|
|
22200
|
+
}
|
|
22201
|
+
}
|
|
22202
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotLoungeSearchService, deps: [{ token: ChatbotService }, { token: EnterpriseSearchService }, { token: ResultAwareService }, { token: UserService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
22203
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotLoungeSearchService, providedIn: 'root' });
|
|
22204
|
+
}
|
|
22205
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotLoungeSearchService, decorators: [{
|
|
22206
|
+
type: Injectable,
|
|
22207
|
+
args: [{ providedIn: 'root' }]
|
|
22208
|
+
}], ctorParameters: () => [{ type: ChatbotService }, { type: EnterpriseSearchService }, { type: ResultAwareService }, { type: UserService }] });
|
|
22209
|
+
|
|
20355
22210
|
/**
|
|
20356
22211
|
* Routes `ChatbotService.fillSearchForm$` events to per-travel-type handlers.
|
|
20357
22212
|
*
|
|
@@ -20368,9 +22223,9 @@ class ChatbotSearchDispatcherService {
|
|
|
20368
22223
|
chatbotService;
|
|
20369
22224
|
initialised = false;
|
|
20370
22225
|
handlers;
|
|
20371
|
-
constructor(chatbotService, rail, flight, hotel, eurostar) {
|
|
22226
|
+
constructor(chatbotService, rail, flight, hotel, carHire, eurostar, lounge, taxi) {
|
|
20372
22227
|
this.chatbotService = chatbotService;
|
|
20373
|
-
this.handlers = [rail, flight, hotel, eurostar];
|
|
22228
|
+
this.handlers = [rail, flight, hotel, carHire, eurostar, lounge, taxi];
|
|
20374
22229
|
}
|
|
20375
22230
|
/** Call once at app start. Idempotent. */
|
|
20376
22231
|
init() {
|
|
@@ -20389,6 +22244,22 @@ class ChatbotSearchDispatcherService {
|
|
|
20389
22244
|
console.error(`[ChatbotSearchDispatcherService] handler '${parameters.travelType}' failed:`, err);
|
|
20390
22245
|
});
|
|
20391
22246
|
});
|
|
22247
|
+
this.chatbotService.resortResults$.subscribe(({ parameters }) => {
|
|
22248
|
+
const handler = this.findHandler(parameters?.travelType);
|
|
22249
|
+
if (!handler?.resort) {
|
|
22250
|
+
// Handler doesn't support cache re-ranking — fall back to a fresh
|
|
22251
|
+
// search so the user still gets re-ordered cards.
|
|
22252
|
+
if (handler) {
|
|
22253
|
+
handler.handle(parameters, true).catch((err) => {
|
|
22254
|
+
console.error(`[ChatbotSearchDispatcherService] resort fallback '${parameters.travelType}' failed:`, err);
|
|
22255
|
+
});
|
|
22256
|
+
}
|
|
22257
|
+
return;
|
|
22258
|
+
}
|
|
22259
|
+
handler.resort(parameters).catch((err) => {
|
|
22260
|
+
console.error(`[ChatbotSearchDispatcherService] resort '${parameters.travelType}' failed:`, err);
|
|
22261
|
+
});
|
|
22262
|
+
});
|
|
20392
22263
|
}
|
|
20393
22264
|
/**
|
|
20394
22265
|
* Whether a global handler is registered for the given travel type. Used by
|
|
@@ -20404,13 +22275,13 @@ class ChatbotSearchDispatcherService {
|
|
|
20404
22275
|
}
|
|
20405
22276
|
return this.handlers.find((h) => h.travelType === travelType);
|
|
20406
22277
|
}
|
|
20407
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotSearchDispatcherService, deps: [{ token: ChatbotService }, { token: ChatbotRailSearchService }, { token: ChatbotFlightSearchService }, { token: ChatbotHotelSearchService }, { token: ChatbotEurostarSearchService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
22278
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotSearchDispatcherService, deps: [{ token: ChatbotService }, { token: ChatbotRailSearchService }, { token: ChatbotFlightSearchService }, { token: ChatbotHotelSearchService }, { token: ChatbotCarHireSearchService }, { token: ChatbotEurostarSearchService }, { token: ChatbotLoungeSearchService }, { token: ChatbotTaxiSearchService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
20408
22279
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotSearchDispatcherService, providedIn: 'root' });
|
|
20409
22280
|
}
|
|
20410
22281
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatbotSearchDispatcherService, decorators: [{
|
|
20411
22282
|
type: Injectable,
|
|
20412
22283
|
args: [{ providedIn: 'root' }]
|
|
20413
|
-
}], ctorParameters: () => [{ type: ChatbotService }, { type: ChatbotRailSearchService }, { type: ChatbotFlightSearchService }, { type: ChatbotHotelSearchService }, { type: ChatbotEurostarSearchService }] });
|
|
22284
|
+
}], ctorParameters: () => [{ type: ChatbotService }, { type: ChatbotRailSearchService }, { type: ChatbotFlightSearchService }, { type: ChatbotHotelSearchService }, { type: ChatbotCarHireSearchService }, { type: ChatbotEurostarSearchService }, { type: ChatbotLoungeSearchService }, { type: ChatbotTaxiSearchService }] });
|
|
20414
22285
|
|
|
20415
22286
|
class BasketPanelComponent extends WithSubscriptionComponent {
|
|
20416
22287
|
basketService;
|
|
@@ -20700,5 +22571,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
|
|
|
20700
22571
|
* Generated bundle index. Do not edit.
|
|
20701
22572
|
*/
|
|
20702
22573
|
|
|
20703
|
-
export { AcceptNewPriceDocument, AcceptNewPriceGQL, AcceptNewPriceUpdater, AcceptProcessTermsDocument, AcceptProcessTermsGQL, AccessibilityInfoFragmentDoc, AddBasketItemCustomRemarkDocument, AddBasketItemCustomRemarkGQL, AddBasketItemCustomRemarkUpdater, AddBasketItemMarkupDocument, AddBasketItemMarkupGQL, AddBasketItemMarkupUpdater, AddFavouriteUserDocument, AddFavouriteUserGQL, AddFavouriteUserUpdater, AddGuestToBasketItemUpdater, AddGuestsToBasketItemDocument, AddGuestsToBasketItemGQL, AddItemToBasketDocument, AddItemToBasketGQL, AddItemToBasketUpdater, AddRedirectApproverDocument, AddRedirectApproverGQL, AddRedirectApproverUpdater, AddToBasketFieldsFragmentDoc, AddUpdatedItemToBasketDocument, AddUpdatedItemToBasketGQL, AddUpdatedItemToBasketUpdater, AddUserToBasketItemUpdater, AddUsersToBasketItemDocument, AddUsersToBasketItemGQL, AddressType, AmendBookingDocument, AmendBookingGQL, AmendBookingUpdater, ApexxCardStatus, ApexxCreateCardDocument, ApexxCreateCardGQL, ApexxDeleteCardDocument, ApexxDeleteCardGQL, ApexxDeleteCardUpdater, ApexxListCardsDocument, ApexxListCardsGQL, ApexxUpdateCardDocument, ApexxUpdateCardGQL, ApexxUpdateCardUpdater, ApplyJitFlightRulesDocument, ApplyJitFlightRulesGQL, ApplyJitHotelRulesDocument, ApplyJitHotelRulesGQL, ApprovalEventType, ApproverUserFieldsFragmentDoc, AuthHttpService, BaggageType, BannerDisplayTarget, BasketFieldsFragmentDoc, BasketItemStatus, BasketMinimalFieldsFragmentDoc, BasketPanelComponent, BasketStatus, BeforeAmendCabSearchDocument, BeforeAmendCabSearchFetcher, BeforeAmendCabSearchGQL, BeforeAmendCarHireSearchDocument, BeforeAmendCarHireSearchFetcher, BeforeAmendCarHireSearchGQL, BookBasketDocument, BookBasketFieldsFragmentDoc, BookBasketGQL, BookBasketUpdater, BookerType, CabHireVehicleType, CabhireQuoteFetcher, CanAmendBookingDocument, CanAmendBookingFetcher, CanAmendBookingGQL, CancelBookingDocument, CancelBookingGQL, CancelBookingUpdater, CarHireAvailabilityDetailFetcher, CarHireAvailabilityDetailRequestDocument, CarHireAvailabilityDetailRequestGQL, CarHireClass, CarHireTransmission, CarHireType, CarbonPolicyDuration, CarbonPolicySource, CarhireQuoteFetcher, ChangeBasketOwnershipDocument, ChangeBasketOwnershipGQL, ChangeBasketOwnershipUpdater, ChangeMode, ChatbotEurostarSearchService, ChatbotFlightSearchService, ChatbotHotelSearchService, ChatbotRailSearchService, ChatbotSearchDispatcherService, ChatbotService, CheckForDuplicateBookingsDocument, CheckForDuplicateBookingsFetcher, CheckForDuplicateBookingsGQL, CheckIfBasketRequiresApprovalDocument, CheckIfBasketRequiresApprovalFetcher, CheckIfBasketRequiresApprovalGQL, CompanyType, ConfirmMessagesDocument, ConfirmMessagesGQL, ConvertCurrencyDocument, ConvertCurrencyFetcher, ConvertCurrencyGQL, CreateBasketDocument, CreateBasketFieldsFragmentDoc, CreateBasketFromDraftDocument, CreateBasketFromDraftGQL, CreateBasketFromDraftUpdater, CreateBasketGQL, CreateBasketNoteDocument, CreateBasketNoteGQL, CreateBasketNoteUpdater, CreateBasketUpdater, CreateGuestDocument, CreateGuestGQL, CreateGuestUpdater, CreateItineraryExchangeDocument, CreateItineraryExchangeGQL, CurrencyCode, CycleInfoFragmentDoc, DeleteAllRecentSearchesDocument, DeleteAllRecentSearchesGQL, DeleteAllRecentSearchesUpdater, DeleteBasketDocument, DeleteBasketGQL, DeleteBasketUpdater, DeleteFavouriteSearchDocument, DeleteFavouriteSearchGQL, DeleteFavouriteSearchUpdater, DeleteGuestDocument, DeleteGuestGQL, DeleteGuestUpdater, DeleteRecentSearchDocument, DeleteRecentSearchGQL, DeleteUserAddressDocument, DeleteUserAddressGQL, DiscountType, DistanceTypes, DivisionFieldsFragmentDoc, DocGender, DocType, DownloadETicketDocument, DownloadETicketFetcher, DownloadETicketGQL, DraftBasketType, EditUserAddressDocument, EditUserAddressGQL, EditUserAddressUpdater, EditUserDocument, EditUserGQL, EmailAmbulanceBookingFetcher, EmailAmbulanceBookingReqDocument, EmailAmbulanceBookingReqGQL, EmailApartmentBookingFetcher, EmailApartmentBookingReqDocument, EmailApartmentBookingReqGQL, EmailBasketDocument, EmailBasketFetcher, EmailBasketGQL, EmailFerryBookingFetcher, EmailFerryBookingReqDocument, EmailFerryBookingReqGQL, EmailMeetingRoomBookingFetcher, EmailMeetingRoomBookingReqDocument, EmailMeetingRoomBookingReqGQL, EmailSeasonTicketBookingFetcher, EmailSeasonTicketBookingReqDocument, EmailSeasonTicketBookingReqGQL, EntLocationsByCityDocument, EntLocationsByCityGQL, EntLocationsByPostcodeDocument, EntLocationsByPostcodeGQL, EnterpriseBasketService, EnterpriseMyBookingsService, EnterpriseSearchService, Environment, ErrorsFragmentDoc, EurostarQuoteFetcher, EventMessenager, EvolviSeatmapsAreEnabledDocument, EvolviSeatmapsAreEnabledGQL, ExchangeType, ExternalHttpService, FailedEmail, FareRuleType, FareType, FerryOrEurotunnel, FlightCabinClass, FlightItineraryFieldsFragmentDoc, FlightPassengerType, FlightQuoteFetcher, GenerateBasketPdfDocument, GenerateBasketPdfFetcher, GenerateBasketPdfGQL, GetAirAvailabilityDocument, GetAirAvailabilityFetcher, GetAirAvailabilityGQL, GetAllAirlinesDocument, GetAllAirlinesFetcher, GetAllAirlinesGQL, GetApexxListCardsFetcher, GetBannerDocument, GetBannerFetcher, GetBannerGQL, GetBasketApprovalInfoDocument, GetBasketApprovalInfoFetcher, GetBasketApprovalInfoGQL, GetBasketApprovalTimestampsDocument, GetBasketApprovalTimestampsGQL, GetBasketCo2InfoFetcher, GetBasketFetcher, GetBasketItemCustomRemarksDocument, GetBasketItemCustomRemarksFetcher, GetBasketItemCustomRemarksGQL, GetBasketNotesDocument, GetBasketNotesFetcher, GetBasketNotesGQL, GetBasketQuoteDocument, GetBasketQuoteFetcher, GetBasketQuoteGQL, GetBrandedFaresDocument, GetBrandedFaresGQL, GetCancellationInfoDocument, GetCancellationInfoFetcher, GetCancellationInfoGQL, GetCarHireDepotsDocument, GetCarHireDepotsGQL, GetCarHireProvidersDocument, GetCarHireProvidersFetcher, GetCarHireProvidersGQL, GetCompaniesDocument, GetCompaniesFetcher, GetCompaniesGQL, GetCompanyDocument, GetCompanyFetcher, GetCompanyGQL, GetConfermaQuicklistDocument, GetConfermaQuicklistGQL, GetConfermaRoomImagesDocument, GetConfermaRoomImagesGQL, GetCovidMicrositeTokenDocument, GetCovidMicrositeTokenFetcher, GetCovidMicrositeTokenGQL, GetCurrencyConversionRatesDocument, GetCurrencyConversionRatesFetcher, GetCurrencyConversionRatesGQL, GetDivisionDocument, GetDivisionGQL, GetDraftBasketsDocument, GetDraftBasketsFetcher, GetDraftBasketsGQL, GetEmptyUserMiDefaultValuesDocument, GetEmptyUserMiDefaultValuesFetcher, GetEmptyUserMiDefaultValuesGQL, GetEntLocationByPostcode, GetEvolviSeatmapsAreEnabledFetcher, GetFerryPortsDocument, GetFerryPortsFetcher, GetFerryPortsGQL, GetFlightAtNewClassDocument, GetFlightAtNewClassFetcher, GetFlightAtNewClassGQL, GetFlightBrandedFaresFetcher, GetFlightExchangeDetailsDocument, GetFlightExchangeDetailsFetcher, GetFlightExchangeDetailsGQL, GetFlightExtrasOptionsDocument, GetFlightExtrasOptionsFetcher, GetFlightExtrasOptionsGQL, GetFlightFareRulesDocument, GetFlightFareRulesFetcher, GetFlightFareRulesGQL, GetFlightSearchExchangeDocument, GetFlightSearchExchangeFetcher, GetFlightSearchExchangeGQL, GetFlightSeatMapDocument, GetFlightSeatMapFetcher, GetFlightSeatMapGQL, GetFlightUpsellOffersDocument, GetFlightUpsellOffersFetcher, GetFlightUpsellOffersGQL, GetGmtItineraryOptionsDocument, GetGmtItineraryOptionsFetcher, GetGmtItineraryOptionsGQL, GetGutCitySuggestionsDocument, GetGutCitySuggestionsFetcher, GetGutCitySuggestionsGQL, GetGutLocationSuggestionsDocument, GetGutLocationSuggestionsFetcher, GetGutLocationSuggestionsGQL, GetHotelChainsDocument, GetHotelChainsFetcher, GetHotelChainsGQL, GetHotelDetailsDocument, GetHotelDetailsFetcher, GetHotelDetailsGQL, GetIrlDiscountCardsDocument, GetIrlDiscountCardsGQL, GetIrlSupplierStationDocument, GetIrlSupplierStationFetcher, GetIrlSupplierStationGQL, GetJyrneyUrlDocument, GetJyrneyUrlFetcher, GetJyrneyUrlGQL, GetLatLonFromHereIdDocument, GetLatLonFromHereIdFetcher, GetLatLonFromHereIdGQL, GetLatestVersionsDocument, GetLatestVersionsFetcher, GetLatestVersionsGQL, GetMIApproversFetcher, GetMiApproversDocument, GetMiApproversGQL, GetMiRequiringMandatoryDefaultValueDocument, GetMiRequiringMandatoryDefaultValueFetcher, GetMiRequiringMandatoryDefaultValueGQL, GetMultipleHotelRatingDocument, GetMultipleHotelRatingFetcher, GetMultipleHotelRatingGQL, GetMultipleHotelsAvailabilityDocument, GetMultipleHotelsAvailabilityGQL, GetOfficeApproversDocument, GetOfficeApproversFetcher, GetOfficeApproversGQL, GetOfficeDivisionsDocument, GetOfficeDivisionsFetcher, GetOfficeDivisionsGQL, GetOfficeDocument, GetOfficeFetcher, GetOfficeGQL, GetOfficeUsersDocument, GetOfficeUsersFetcher, GetOfficeUsersGQL, GetOfficesDocument, GetOfficesFetcher, GetOfficesGQL, GetPriceHiddenSabreExchangeDocument, GetPriceHiddenSabreExchangeFetcher, GetPriceHiddenSabreExchangeGQL, GetProcessTermsPriceDocument, GetProcessTermsPriceGQL, GetRailcardsFetcher as GetRailCardsFetcher, GetRailLiveDeparturesDocument, GetRailLiveDeparturesFetcher, GetRailLiveDeparturesGQL, GetRailProvidersDocument, GetRailProvidersFetcher, GetRailProvidersGQL, GetRailSearchExchangeDocument, GetRailSearchExchangeGQL, GetRailStationDocument, GetRailStationFetcher, GetRailStationGQL, GetRailStationInfoDocument, GetRailStationInfoFetcher, GetRailStationInfoGQL, GetRailcardsDocument, GetRailcardsFetcher, GetRailcardsGQL, GetRedirectApproverDocument, RedirectApproverFetcher as GetRedirectApproverFetcher, GetRedirectApproverGQL, GetRequestedBookingUpdateDocument, GetRequestedBookingUpdateFetcher, GetRequestedBookingUpdateGQL, GetRiskAlertsDocument, GetRiskAlertsFetcher, GetRiskAlertsGQL, GetRouteHappyDocument, GetRouteHappyFetcher, GetRouteHappyGQL, SearchCompanyApproversFetcher as GetSearchCompanyApproversFetcher, GetServiceDocument, GetServiceFetcher, GetServiceGQL, GetTrainSeatMapDocument, GetTrainSeatMapEvolviDocument, GetTrainSeatMapEvolviGQL, GetTrainSeatMapGQL, GetTrainSeatmapEvolviFetcher, GetTrainSeatmapFetcher, GetTrainlineSearchConfigDocument, GetTrainlineSearchConfigFetcher, GetTrainlineSearchConfigGQL, GetUserAddressesDocument, GetUserAddressesGQL, GetUserApproversDocument, GetUserApproversFetcher, GetUserApproversGQL, GetUserBasketCo2InfoDocument, GetUserBasketCo2InfoGQL, GetUserBasketDocument, GetUserBasketGQL, GetUserBasketsDocument, GetUserBasketsFetcher, GetUserBasketsGQL, GetUserCarbonAllowanceDocument, GetUserCarbonAllowanceFetcher, GetUserCarbonAllowanceGQL, GetUserCompanyOfficesDocument, GetUserCompanyOfficesFetcher, GetUserCompanyOfficesGQL, GetUserCarbonAllowanceFetcher as GetUserCurrentCarbonAllowanceFetcher, GetUserDocument, GetUserDocumentLoyaltyDocument, GetUserDocumentLoyaltyFetcher, GetUserDocumentLoyaltyGQL, GetUserEmergencyContactDocument, GetUserEmergencyContactFetcher, GetUserEmergencyContactGQL, GetUserFavouriteSearchesDocument, GetUserFavouriteSearchesFetcher, GetUserFavouriteSearchesGQL, GetUserFavouriteUsersDocument, GetUserFavouriteUsersFetcher, GetUserFavouriteUsersGQL, GetUserFetcher, GetUserGQL, GetUserGuestsDocument, GetUserGuestsFetcher, GetUserGuestsGQL, GetUserMessagesDocument, GetUserMessagesGQL, GetUserMiDefaultValuesDocument, GetUserMiDefaultValuesFetcher, GetUserMiDefaultValuesGQL, GetUserMiDocument, GetUserMiFetcher, GetUserMiGQL, GetUserMiStackDocument, GetUserMiStackFetcher, GetUserMiStackGQL, GetUserPhoneDocument, GetUserPhoneGQL, GetUserPhoneNumbersDocument, GetUserPhoneNumbersGQL, GetUserPreferredTransportHubsDocument, GetUserPreferredTransportHubsFetcher, GetUserPreferredTransportHubsGQL, GetUserRecentBoltSearchesDocument, GetUserRecentBoltSearchesGQL, GetUserRecentSearchesDocument, GetUserRecentSearchesFetcher, GetUserRecentSearchesGQL, GetUserServicesDocument, GetUserServicesGQL, GetUserSharedBasketsDocument, GetUserSharedBasketsFetcher, GetUserSharedBasketsGQL, GetUserUiConfigsDocument, GetUserUiConfigsGQL, GmtItineraryAirportOptionFragmentFragmentDoc, GmtItineraryEurostarOptionFragmentFragmentDoc, GmtItineraryRailStationOptioFragmentFragmentDoc, GroupedFieldsFragmentDoc, HelpInfoFragmentDoc, HelperRoutines, HotelAvalibilityQuoteFetcher, HotelAvalibilityService, HotelQuoteFetcher, HotelStaticTagDisplay, HttpCallService, HttpCancelInterceptor, HttpCancelService, InvokedUserFieldsFragmentDoc, IrlClass, IrlJourneyType, IrlSupplier, IrlTravellerStatus, IsPostcodeValidDocument, IsPostcodeValidGQL, ItineraryEmailTo, LeadPassengerType, LocationType, Location_DetailFragmentDoc, LogLevelType, LogonService, LoungeQuoteFetcher, LoungesInfoFragmentDoc, ManagementInfoDefaultValuePrepopulation, ManagementInfoFieldFragmentDoc, ManagementInfoScope, ManagementInfoSpecialValueSource, ManagementInfoUiPresentation, ManagementInfoValueFieldFragmentDoc, ManagementInfoValueHandling, MarkupFareType, MarkupFlightCabinClass, MarkupFlightSpecialFares, MeetingRoomCatering, MeetingRoomDuration, MeetingRoomEquipment, MeetingRoomLayout, MemoizePipe, MessageTarget, MessageType, MessagesService, ModalOpenerService, MoveItemToADifferentBasketDocument, MoveItemToADifferentBasketGQL, OBTServicesModule, OBTproviders, OfficeFieldsFragmentDoc, OwnerFieldsFragmentDoc, OysterCardsInfoFragmentDoc, ParkingQuoteFetcher, ParkingStationFragmentDoc, PassengerInputType, PassengerType, PaymentMethodFieldsFragmentDoc, PaymentMethodWithCardFieldsFragmentDoc, PhoneType, PlaceCategory, PreferenceKey, ProfileConfigOption, PromptUpdateService, RailAccountsListFragmentDoc, RailCallingPointsFragmentDoc, RailClass, RailFareProcessingService, RailJourneyFareFragmentDoc, RailJourneyOptionFragmentDoc, RailQuoteFetcher, RailResultsStateService, RailSearchComposition, RailSearchJourneyType, RailSeatInfoFragmentDoc, RailSeatPreferenceTypeFragmentDoc, RailSegmentFieldsFragmentDoc, RailStationClassification, RailStoppingPattern, RailTicketFieldsFragmentDoc, RailTicketOptionFieldsFragmentDoc, RailTicketQueueListFragmentDoc, RedirectApproverFetcher, RemoveFavouriteUserDocument, RemoveFavouriteUserGQL, RemoveFavouriteUserUpdater, RemoveItemFromBasketDocument, RemoveItemFromBasketGQL, RemoveItemFromBasketUpdater, RemovePreferredTransportHubDocument, RemovePreferredTransportHubGQL, RemovePreferredTransportHubUpdater, RemoveRedirectApproverDocument, RemoveRedirectApproverGQL, RemoveRedirectApproverUpdater, ResendApproverEmailDocument, ResendApproverEmailFetcher, ResendApproverEmailGQL, ReserveRailSeatsDocument, ReserveRailSeatsGQL, ReserveRailSeatsUpdater, ResultAwareService, RevalidateBasketDocument, RevalidateBasketGQL, RevalidateBasketUpdater, RiskLevel, RoomAvaliabilityFragmentDoc, RouteHappyService, SaveFavouriteSearchDocument, SaveFavouriteSearchGQL, SaveFavouriteSearchUpdater, SaveRecentBoltSearchDocument, SaveRecentBoltSearchGQL, SaveRecentBoltSearchUpdater, SaveRecentSearchDocument, SaveRecentSearchGQL, SaveRecentSearchUpdater, SaveUserAddressDocument, SaveUserAddressGQL, SaveUserAddressUpdater, SearchAirlinesDocument, SearchAirlinesFetcher, SearchAirlinesGQL, SearchAirportsDocument, SearchAirportsFetcher, SearchAirportsGQL, SearchCabHireDocument, SearchCabHireGQL, SearchCarHireDocument, SearchCarHireGQL, SearchCitiesDocument, SearchCitiesGQL, SearchCityFetcher, SearchCompanyApproversDocument, SearchCompanyApproversFetcher, SearchCompanyApproversGQL, SearchCompanyUsersDocument, SearchCompanyUsersFetcher, SearchCompanyUsersGQL, SearchDepotFetcher, SearchDiscoverLocationDocument, SearchDiscoverLocationFetcher, SearchDiscoverLocationGQL, SearchDocumentValidationService, SearchEurostarDocument, SearchEurostarGQL, SearchFastTrackDocument, SearchFastTrackGQL, SearchFlightsV2Document, SearchFlightsV2GQL, SearchGeoLocationDocument, SearchGeoLocationFetcher, SearchGeoLocationGQL, SearchHotelChainsDocument, SearchHotelChainsFetcher, SearchHotelChainsGQL, SearchHotelsDocument, SearchHotelsGQL, SearchIrlDocument, SearchIrlGQL, SearchIrlStationsFetcher, SearchIrlSupplierStationsDocument, SearchIrlSupplierStationsGQL, SearchLoungesDocument, SearchLoungesGQL, SearchMIAutoSuggestValuesFetcher, SearchMiAutoSuggestValuesDocument, SearchMiAutoSuggestValuesGQL, SearchParkingDocument, SearchParkingGQL, SearchPaymentValidationService, SearchPostcodeFetcher, SearchPostcodesDocument, SearchPostcodesGQL, SearchRailDocument, SearchRailGQL, SearchRailInwardDocument, SearchRailInwardFetcher, SearchRailInwardGQL, SearchRailStationsDocument, SearchRailStationsFetcher, SearchRailStationsGQL, SearchUserAddressDocument, SearchUserAddressGQL, SearchUserCanBookForDocument, SearchUserCanBookForGQL, SearchUserWithEmailDocument, SearchUserWithEmailFetcher, SearchUserWithEmailGQL, SearchUsersCanBookForFetcher, SeasonTicketCardType, SeasonTicketClassType, SeasonTicketDeliveryType, SeasonTicketLondonUnderground, SeasonTicketType, SelectBasketApproverDocument, SelectBasketApproverGQL, SelectBasketApproverUpdater, SelectBasketMultiLevelApproversDocument, SelectBasketMultiLevelApproversGQL, SelectBasketMultiLevelApproversUpdater, SelectBasketNotifyApproverDocument, SelectBasketNotifyApproverGQL, SelectBasketNotifyApproverUpdater, SendBackToQueueDocument, SendBackToQueueGQL, SendBackToQueueUpdater, SendOfflineNotificationDocument, SendOfflineNotificationFetcher, SendOfflineNotificationGQL, ServiceMinimalFieldsFragmentDoc, ServiceType, SetBasketItemApexxTokenDocument, SetBasketItemApexxTokenGQL, SetBasketItemApexxTokenUpdater, SetBasketItemLeadPassengerDocument, SetBasketItemLeadPassengerGQL, SetBasketItemPaymentMethodDocument, SetBasketItemPaymentMethodGQL, SetEmptyUserMIDefaultValueUpdater, SetEmptyUserMiDefaultValueDocument, SetEmptyUserMiDefaultValueGQL, SetMIValuesUpdater, SetMiValuesDocument, SetMiValuesGQL, SetPaymentOptionUpdater, SetPreferredTransportHubDocument, SetPreferredTransportHubGQL, SetPreferredTransportHubUpdater, SetUserLanguageDocument, SetUserLanguageGQL, SetUserMIDefaultValueUpdater, SetUserMiDefaultValueDocument, SetUserMiDefaultValueGQL, ShareBasketDocument, ShareBasketGQL, ShareBasketUpdater, ShoppingInfoFragmentDoc, SmartCardsInfoFragmentDoc, SplitRailJourneyFareFragmentDoc, StorageService, SuggestPlacesDocument, SuggestPlacesFetcher, SuggestPlacesGQL, TicketOfficeInfoFragmentDoc, TicketQueueService, TicketType, TimeType, ToiletsBabyInfoFragmentDoc, TrainlineTenantIsFrenchDocument, TrainlineTenantIsFrenchFetcher, TrainlineTenantIsFrenchGQL, TransportType, TravellerGuestsFieldsFragmentDoc, TravellerUserFieldsFragmentDoc, UiConfigTarget, UpdateBasketNotesDocument, UpdateBasketNotesGQL, UpdateBasketTitleDocument, UpdateBasketTitleGQL, UpdateDOBUpdater, UpdateDobDocument, UpdateDobGQL, UpdateExchangeBasketDocument, UpdateExchangeBasketGQL, UpdateExchangeBasketUpdater, UpdateFlightSeatMapDocument, UpdateFlightSeatMapGQL, UpdateFlightSeatMapUpdater, UserApproversFieldsFragmentDoc, UserFieldsFragmentDoc, UserMessagesFetcher, UserMiFragmentDoc, UserMinimalFieldsFragmentDoc, UserProductsFetcher, UserService, UserSummaryFieldsFragmentDoc, UserUiConfigsFetcher, UsersFragmentDoc, ValidateBasketItemMiDocument, ValidateBasketItemMiFetcher, ValidateBasketItemMiGQL, ValidateBasketMiDocument, ValidateBasketMiFetcher, ValidateBasketMiGQL, VehicleType, WebTokenService, WifiInfoFragmentDoc, WithSubscriptionComponent };
|
|
22574
|
+
export { AcceptNewPriceDocument, AcceptNewPriceGQL, AcceptNewPriceUpdater, AcceptProcessTermsDocument, AcceptProcessTermsGQL, AccessibilityInfoFragmentDoc, AddBasketItemCustomRemarkDocument, AddBasketItemCustomRemarkGQL, AddBasketItemCustomRemarkUpdater, AddBasketItemMarkupDocument, AddBasketItemMarkupGQL, AddBasketItemMarkupUpdater, AddFavouriteUserDocument, AddFavouriteUserGQL, AddFavouriteUserUpdater, AddGuestToBasketItemUpdater, AddGuestsToBasketItemDocument, AddGuestsToBasketItemGQL, AddItemToBasketDocument, AddItemToBasketGQL, AddItemToBasketUpdater, AddRedirectApproverDocument, AddRedirectApproverGQL, AddRedirectApproverUpdater, AddToBasketFieldsFragmentDoc, AddUpdatedItemToBasketDocument, AddUpdatedItemToBasketGQL, AddUpdatedItemToBasketUpdater, AddUserToBasketItemUpdater, AddUsersToBasketItemDocument, AddUsersToBasketItemGQL, AddressType, AmendBookingDocument, AmendBookingGQL, AmendBookingUpdater, ApexxCardStatus, ApexxCreateCardDocument, ApexxCreateCardGQL, ApexxDeleteCardDocument, ApexxDeleteCardGQL, ApexxDeleteCardUpdater, ApexxListCardsDocument, ApexxListCardsGQL, ApexxUpdateCardDocument, ApexxUpdateCardGQL, ApexxUpdateCardUpdater, ApplyJitFlightRulesDocument, ApplyJitFlightRulesGQL, ApplyJitHotelRulesDocument, ApplyJitHotelRulesGQL, ApprovalEventType, ApproverUserFieldsFragmentDoc, AuthHttpService, BaggageType, BannerDisplayTarget, BasketFieldsFragmentDoc, BasketItemStatus, BasketMinimalFieldsFragmentDoc, BasketPanelComponent, BasketStatus, BeforeAmendCabSearchDocument, BeforeAmendCabSearchFetcher, BeforeAmendCabSearchGQL, BeforeAmendCarHireSearchDocument, BeforeAmendCarHireSearchFetcher, BeforeAmendCarHireSearchGQL, BookBasketDocument, BookBasketFieldsFragmentDoc, BookBasketGQL, BookBasketUpdater, BookerType, CabHireVehicleType, CabhireQuoteFetcher, CanAmendBookingDocument, CanAmendBookingFetcher, CanAmendBookingGQL, CancelBookingDocument, CancelBookingGQL, CancelBookingUpdater, CarHireAvailabilityDetailFetcher, CarHireAvailabilityDetailRequestDocument, CarHireAvailabilityDetailRequestGQL, CarHireClass, CarHireTransmission, CarHireType, CarbonPolicyDuration, CarbonPolicySource, CarhireQuoteFetcher, ChangeBasketOwnershipDocument, ChangeBasketOwnershipGQL, ChangeBasketOwnershipUpdater, ChangeMode, ChatbotCarHireSearchService, ChatbotEurostarSearchService, ChatbotFlightSearchService, ChatbotHotelSearchService, ChatbotRailSearchService, ChatbotSearchDispatcherService, ChatbotService, ChatbotTaxiSearchService, CheckForDuplicateBookingsDocument, CheckForDuplicateBookingsFetcher, CheckForDuplicateBookingsGQL, CheckIfBasketRequiresApprovalDocument, CheckIfBasketRequiresApprovalFetcher, CheckIfBasketRequiresApprovalGQL, CompanyType, ConfirmMessagesDocument, ConfirmMessagesGQL, ConvertCurrencyDocument, ConvertCurrencyFetcher, ConvertCurrencyGQL, CreateBasketDocument, CreateBasketFieldsFragmentDoc, CreateBasketFromDraftDocument, CreateBasketFromDraftGQL, CreateBasketFromDraftUpdater, CreateBasketGQL, CreateBasketNoteDocument, CreateBasketNoteGQL, CreateBasketNoteUpdater, CreateBasketUpdater, CreateGuestDocument, CreateGuestGQL, CreateGuestUpdater, CreateItineraryExchangeDocument, CreateItineraryExchangeGQL, CurrencyCode, CycleInfoFragmentDoc, DeleteAllRecentSearchesDocument, DeleteAllRecentSearchesGQL, DeleteAllRecentSearchesUpdater, DeleteBasketDocument, DeleteBasketGQL, DeleteBasketUpdater, DeleteFavouriteSearchDocument, DeleteFavouriteSearchGQL, DeleteFavouriteSearchUpdater, DeleteGuestDocument, DeleteGuestGQL, DeleteGuestUpdater, DeleteRecentSearchDocument, DeleteRecentSearchGQL, DeleteUserAddressDocument, DeleteUserAddressGQL, DiscountType, DistanceTypes, DivisionFieldsFragmentDoc, DocGender, DocType, DownloadETicketDocument, DownloadETicketFetcher, DownloadETicketGQL, DraftBasketType, EditUserAddressDocument, EditUserAddressGQL, EditUserAddressUpdater, EditUserDocument, EditUserGQL, EmailAmbulanceBookingFetcher, EmailAmbulanceBookingReqDocument, EmailAmbulanceBookingReqGQL, EmailApartmentBookingFetcher, EmailApartmentBookingReqDocument, EmailApartmentBookingReqGQL, EmailBasketDocument, EmailBasketFetcher, EmailBasketGQL, EmailFerryBookingFetcher, EmailFerryBookingReqDocument, EmailFerryBookingReqGQL, EmailMeetingRoomBookingFetcher, EmailMeetingRoomBookingReqDocument, EmailMeetingRoomBookingReqGQL, EmailSeasonTicketBookingFetcher, EmailSeasonTicketBookingReqDocument, EmailSeasonTicketBookingReqGQL, EntLocationsByCityDocument, EntLocationsByCityGQL, EntLocationsByPostcodeDocument, EntLocationsByPostcodeGQL, EnterpriseBasketService, EnterpriseMyBookingsService, EnterpriseSearchService, Environment, ErrorsFragmentDoc, EurostarQuoteFetcher, EventMessenager, EvolviSeatmapsAreEnabledDocument, EvolviSeatmapsAreEnabledGQL, ExchangeType, ExternalHttpService, FailedEmail, FareRuleType, FareType, FerryOrEurotunnel, FlightCabinClass, FlightItineraryFieldsFragmentDoc, FlightPassengerType, FlightQuoteFetcher, GenerateBasketPdfDocument, GenerateBasketPdfFetcher, GenerateBasketPdfGQL, GetAirAvailabilityDocument, GetAirAvailabilityFetcher, GetAirAvailabilityGQL, GetAllAirlinesDocument, GetAllAirlinesFetcher, GetAllAirlinesGQL, GetApexxListCardsFetcher, GetBannerDocument, GetBannerFetcher, GetBannerGQL, GetBasketApprovalInfoDocument, GetBasketApprovalInfoFetcher, GetBasketApprovalInfoGQL, GetBasketApprovalTimestampsDocument, GetBasketApprovalTimestampsGQL, GetBasketCo2InfoFetcher, GetBasketFetcher, GetBasketItemCustomRemarksDocument, GetBasketItemCustomRemarksFetcher, GetBasketItemCustomRemarksGQL, GetBasketNotesDocument, GetBasketNotesFetcher, GetBasketNotesGQL, GetBasketQuoteDocument, GetBasketQuoteFetcher, GetBasketQuoteGQL, GetBrandedFaresDocument, GetBrandedFaresGQL, GetCancellationInfoDocument, GetCancellationInfoFetcher, GetCancellationInfoGQL, GetCarHireDepotsDocument, GetCarHireDepotsGQL, GetCarHireProvidersDocument, GetCarHireProvidersFetcher, GetCarHireProvidersGQL, GetCompaniesDocument, GetCompaniesFetcher, GetCompaniesGQL, GetCompanyDocument, GetCompanyFetcher, GetCompanyGQL, GetConfermaQuicklistDocument, GetConfermaQuicklistGQL, GetConfermaRoomImagesDocument, GetConfermaRoomImagesGQL, GetCovidMicrositeTokenDocument, GetCovidMicrositeTokenFetcher, GetCovidMicrositeTokenGQL, GetCurrencyConversionRatesDocument, GetCurrencyConversionRatesFetcher, GetCurrencyConversionRatesGQL, GetDivisionDocument, GetDivisionGQL, GetDraftBasketsDocument, GetDraftBasketsFetcher, GetDraftBasketsGQL, GetEmptyUserMiDefaultValuesDocument, GetEmptyUserMiDefaultValuesFetcher, GetEmptyUserMiDefaultValuesGQL, GetEntLocationByPostcode, GetEvolviSeatmapsAreEnabledFetcher, GetFerryPortsDocument, GetFerryPortsFetcher, GetFerryPortsGQL, GetFlightAtNewClassDocument, GetFlightAtNewClassFetcher, GetFlightAtNewClassGQL, GetFlightBrandedFaresFetcher, GetFlightExchangeDetailsDocument, GetFlightExchangeDetailsFetcher, GetFlightExchangeDetailsGQL, GetFlightExtrasOptionsDocument, GetFlightExtrasOptionsFetcher, GetFlightExtrasOptionsGQL, GetFlightFareRulesDocument, GetFlightFareRulesFetcher, GetFlightFareRulesGQL, GetFlightSearchExchangeDocument, GetFlightSearchExchangeFetcher, GetFlightSearchExchangeGQL, GetFlightSeatMapDocument, GetFlightSeatMapFetcher, GetFlightSeatMapGQL, GetFlightUpsellOffersDocument, GetFlightUpsellOffersFetcher, GetFlightUpsellOffersGQL, GetGmtItineraryOptionsDocument, GetGmtItineraryOptionsFetcher, GetGmtItineraryOptionsGQL, GetGutCitySuggestionsDocument, GetGutCitySuggestionsFetcher, GetGutCitySuggestionsGQL, GetGutLocationSuggestionsDocument, GetGutLocationSuggestionsFetcher, GetGutLocationSuggestionsGQL, GetHotelChainsDocument, GetHotelChainsFetcher, GetHotelChainsGQL, GetHotelDetailsDocument, GetHotelDetailsFetcher, GetHotelDetailsGQL, GetIrlDiscountCardsDocument, GetIrlDiscountCardsGQL, GetIrlSupplierStationDocument, GetIrlSupplierStationFetcher, GetIrlSupplierStationGQL, GetJyrneyUrlDocument, GetJyrneyUrlFetcher, GetJyrneyUrlGQL, GetLatLonFromHereIdDocument, GetLatLonFromHereIdFetcher, GetLatLonFromHereIdGQL, GetLatestVersionsDocument, GetLatestVersionsFetcher, GetLatestVersionsGQL, GetMIApproversFetcher, GetMiApproversDocument, GetMiApproversGQL, GetMiRequiringMandatoryDefaultValueDocument, GetMiRequiringMandatoryDefaultValueFetcher, GetMiRequiringMandatoryDefaultValueGQL, GetMultipleHotelRatingDocument, GetMultipleHotelRatingFetcher, GetMultipleHotelRatingGQL, GetMultipleHotelsAvailabilityDocument, GetMultipleHotelsAvailabilityGQL, GetOfficeApproversDocument, GetOfficeApproversFetcher, GetOfficeApproversGQL, GetOfficeDivisionsDocument, GetOfficeDivisionsFetcher, GetOfficeDivisionsGQL, GetOfficeDocument, GetOfficeFetcher, GetOfficeGQL, GetOfficeUsersDocument, GetOfficeUsersFetcher, GetOfficeUsersGQL, GetOfficesDocument, GetOfficesFetcher, GetOfficesGQL, GetPriceHiddenSabreExchangeDocument, GetPriceHiddenSabreExchangeFetcher, GetPriceHiddenSabreExchangeGQL, GetProcessTermsPriceDocument, GetProcessTermsPriceGQL, GetRailcardsFetcher as GetRailCardsFetcher, GetRailLiveDeparturesDocument, GetRailLiveDeparturesFetcher, GetRailLiveDeparturesGQL, GetRailProvidersDocument, GetRailProvidersFetcher, GetRailProvidersGQL, GetRailSearchExchangeDocument, GetRailSearchExchangeGQL, GetRailStationDocument, GetRailStationFetcher, GetRailStationGQL, GetRailStationInfoDocument, GetRailStationInfoFetcher, GetRailStationInfoGQL, GetRailcardsDocument, GetRailcardsFetcher, GetRailcardsGQL, GetRedirectApproverDocument, RedirectApproverFetcher as GetRedirectApproverFetcher, GetRedirectApproverGQL, GetRequestedBookingUpdateDocument, GetRequestedBookingUpdateFetcher, GetRequestedBookingUpdateGQL, GetRiskAlertsDocument, GetRiskAlertsFetcher, GetRiskAlertsGQL, GetRouteHappyDocument, GetRouteHappyFetcher, GetRouteHappyGQL, SearchCompanyApproversFetcher as GetSearchCompanyApproversFetcher, GetServiceDocument, GetServiceFetcher, GetServiceGQL, GetTrainSeatMapDocument, GetTrainSeatMapEvolviDocument, GetTrainSeatMapEvolviGQL, GetTrainSeatMapGQL, GetTrainSeatmapEvolviFetcher, GetTrainSeatmapFetcher, GetTrainlineSearchConfigDocument, GetTrainlineSearchConfigFetcher, GetTrainlineSearchConfigGQL, GetUserAddressesDocument, GetUserAddressesGQL, GetUserApproversDocument, GetUserApproversFetcher, GetUserApproversGQL, GetUserBasketCo2InfoDocument, GetUserBasketCo2InfoGQL, GetUserBasketDocument, GetUserBasketGQL, GetUserBasketsDocument, GetUserBasketsFetcher, GetUserBasketsGQL, GetUserCarbonAllowanceDocument, GetUserCarbonAllowanceFetcher, GetUserCarbonAllowanceGQL, GetUserCompanyOfficesDocument, GetUserCompanyOfficesFetcher, GetUserCompanyOfficesGQL, GetUserCarbonAllowanceFetcher as GetUserCurrentCarbonAllowanceFetcher, GetUserDocument, GetUserDocumentLoyaltyDocument, GetUserDocumentLoyaltyFetcher, GetUserDocumentLoyaltyGQL, GetUserEmergencyContactDocument, GetUserEmergencyContactFetcher, GetUserEmergencyContactGQL, GetUserFavouriteSearchesDocument, GetUserFavouriteSearchesFetcher, GetUserFavouriteSearchesGQL, GetUserFavouriteUsersDocument, GetUserFavouriteUsersFetcher, GetUserFavouriteUsersGQL, GetUserFetcher, GetUserGQL, GetUserGuestsDocument, GetUserGuestsFetcher, GetUserGuestsGQL, GetUserMessagesDocument, GetUserMessagesGQL, GetUserMiDefaultValuesDocument, GetUserMiDefaultValuesFetcher, GetUserMiDefaultValuesGQL, GetUserMiDocument, GetUserMiFetcher, GetUserMiGQL, GetUserMiStackDocument, GetUserMiStackFetcher, GetUserMiStackGQL, GetUserPhoneDocument, GetUserPhoneGQL, GetUserPhoneNumbersDocument, GetUserPhoneNumbersGQL, GetUserPreferredTransportHubsDocument, GetUserPreferredTransportHubsFetcher, GetUserPreferredTransportHubsGQL, GetUserRecentBoltSearchesDocument, GetUserRecentBoltSearchesGQL, GetUserRecentSearchesDocument, GetUserRecentSearchesFetcher, GetUserRecentSearchesGQL, GetUserServicesDocument, GetUserServicesGQL, GetUserSharedBasketsDocument, GetUserSharedBasketsFetcher, GetUserSharedBasketsGQL, GetUserUiConfigsDocument, GetUserUiConfigsGQL, GmtItineraryAirportOptionFragmentFragmentDoc, GmtItineraryEurostarOptionFragmentFragmentDoc, GmtItineraryRailStationOptioFragmentFragmentDoc, GroupedFieldsFragmentDoc, HelpInfoFragmentDoc, HelperRoutines, HotelAvalibilityQuoteFetcher, HotelAvalibilityService, HotelQuoteFetcher, HotelStaticTagDisplay, HttpCallService, HttpCancelInterceptor, HttpCancelService, InvokedUserFieldsFragmentDoc, IrlClass, IrlJourneyType, IrlSupplier, IrlTravellerStatus, IsPostcodeValidDocument, IsPostcodeValidGQL, ItineraryEmailTo, LeadPassengerType, LocationType, Location_DetailFragmentDoc, LogLevelType, LogonService, LoungeQuoteFetcher, LoungesInfoFragmentDoc, ManagementInfoDefaultValuePrepopulation, ManagementInfoFieldFragmentDoc, ManagementInfoScope, ManagementInfoSpecialValueSource, ManagementInfoUiPresentation, ManagementInfoValueFieldFragmentDoc, ManagementInfoValueHandling, MarkupFareType, MarkupFlightCabinClass, MarkupFlightSpecialFares, MeetingRoomCatering, MeetingRoomDuration, MeetingRoomEquipment, MeetingRoomLayout, MemoizePipe, MessageTarget, MessageType, MessagesService, ModalOpenerService, MoveItemToADifferentBasketDocument, MoveItemToADifferentBasketGQL, OBTServicesModule, OBTproviders, OfficeFieldsFragmentDoc, OwnerFieldsFragmentDoc, OysterCardsInfoFragmentDoc, ParkingQuoteFetcher, ParkingStationFragmentDoc, PassengerInputType, PassengerType, PaymentMethodFieldsFragmentDoc, PaymentMethodWithCardFieldsFragmentDoc, PhoneType, PlaceCategory, PreferenceKey, ProfileConfigOption, PromptUpdateService, RailAccountsListFragmentDoc, RailCallingPointsFragmentDoc, RailClass, RailFareProcessingService, RailJourneyFareFragmentDoc, RailJourneyOptionFragmentDoc, RailQuoteFetcher, RailResultsStateService, RailSearchComposition, RailSearchJourneyType, RailSeatInfoFragmentDoc, RailSeatPreferenceTypeFragmentDoc, RailSegmentFieldsFragmentDoc, RailStationClassification, RailStoppingPattern, RailTicketFieldsFragmentDoc, RailTicketOptionFieldsFragmentDoc, RailTicketQueueListFragmentDoc, RedirectApproverFetcher, RemoveFavouriteUserDocument, RemoveFavouriteUserGQL, RemoveFavouriteUserUpdater, RemoveItemFromBasketDocument, RemoveItemFromBasketGQL, RemoveItemFromBasketUpdater, RemovePreferredTransportHubDocument, RemovePreferredTransportHubGQL, RemovePreferredTransportHubUpdater, RemoveRedirectApproverDocument, RemoveRedirectApproverGQL, RemoveRedirectApproverUpdater, ResendApproverEmailDocument, ResendApproverEmailFetcher, ResendApproverEmailGQL, ReserveRailSeatsDocument, ReserveRailSeatsGQL, ReserveRailSeatsUpdater, ResultAwareService, RevalidateBasketDocument, RevalidateBasketGQL, RevalidateBasketUpdater, RiskLevel, RoomAvaliabilityFragmentDoc, RouteHappyService, SaveFavouriteSearchDocument, SaveFavouriteSearchGQL, SaveFavouriteSearchUpdater, SaveRecentBoltSearchDocument, SaveRecentBoltSearchGQL, SaveRecentBoltSearchUpdater, SaveRecentSearchDocument, SaveRecentSearchGQL, SaveRecentSearchUpdater, SaveUserAddressDocument, SaveUserAddressGQL, SaveUserAddressUpdater, SearchAirlinesDocument, SearchAirlinesFetcher, SearchAirlinesGQL, SearchAirportsDocument, SearchAirportsFetcher, SearchAirportsGQL, SearchCabHireDocument, SearchCabHireGQL, SearchCarHireDocument, SearchCarHireGQL, SearchCitiesDocument, SearchCitiesGQL, SearchCityFetcher, SearchCompanyApproversDocument, SearchCompanyApproversFetcher, SearchCompanyApproversGQL, SearchCompanyUsersDocument, SearchCompanyUsersFetcher, SearchCompanyUsersGQL, SearchDepotFetcher, SearchDiscoverLocationDocument, SearchDiscoverLocationFetcher, SearchDiscoverLocationGQL, SearchDocumentValidationService, SearchEurostarDocument, SearchEurostarGQL, SearchFastTrackDocument, SearchFastTrackGQL, SearchFlightsV2Document, SearchFlightsV2GQL, SearchGeoLocationDocument, SearchGeoLocationFetcher, SearchGeoLocationGQL, SearchHotelChainsDocument, SearchHotelChainsFetcher, SearchHotelChainsGQL, SearchHotelsDocument, SearchHotelsGQL, SearchIrlDocument, SearchIrlGQL, SearchIrlStationsFetcher, SearchIrlSupplierStationsDocument, SearchIrlSupplierStationsGQL, SearchLoungesDocument, SearchLoungesGQL, SearchMIAutoSuggestValuesFetcher, SearchMiAutoSuggestValuesDocument, SearchMiAutoSuggestValuesGQL, SearchParkingDocument, SearchParkingGQL, SearchPaymentValidationService, SearchPostcodeFetcher, SearchPostcodesDocument, SearchPostcodesGQL, SearchRailDocument, SearchRailGQL, SearchRailInwardDocument, SearchRailInwardFetcher, SearchRailInwardGQL, SearchRailStationsDocument, SearchRailStationsFetcher, SearchRailStationsGQL, SearchUserAddressDocument, SearchUserAddressGQL, SearchUserCanBookForDocument, SearchUserCanBookForGQL, SearchUserWithEmailDocument, SearchUserWithEmailFetcher, SearchUserWithEmailGQL, SearchUsersCanBookForFetcher, SeasonTicketCardType, SeasonTicketClassType, SeasonTicketDeliveryType, SeasonTicketLondonUnderground, SeasonTicketType, SelectBasketApproverDocument, SelectBasketApproverGQL, SelectBasketApproverUpdater, SelectBasketMultiLevelApproversDocument, SelectBasketMultiLevelApproversGQL, SelectBasketMultiLevelApproversUpdater, SelectBasketNotifyApproverDocument, SelectBasketNotifyApproverGQL, SelectBasketNotifyApproverUpdater, SendBackToQueueDocument, SendBackToQueueGQL, SendBackToQueueUpdater, SendOfflineNotificationDocument, SendOfflineNotificationFetcher, SendOfflineNotificationGQL, ServiceMinimalFieldsFragmentDoc, ServiceType, SetBasketItemApexxTokenDocument, SetBasketItemApexxTokenGQL, SetBasketItemApexxTokenUpdater, SetBasketItemLeadPassengerDocument, SetBasketItemLeadPassengerGQL, SetBasketItemPaymentMethodDocument, SetBasketItemPaymentMethodGQL, SetEmptyUserMIDefaultValueUpdater, SetEmptyUserMiDefaultValueDocument, SetEmptyUserMiDefaultValueGQL, SetMIValuesUpdater, SetMiValuesDocument, SetMiValuesGQL, SetPaymentOptionUpdater, SetPreferredTransportHubDocument, SetPreferredTransportHubGQL, SetPreferredTransportHubUpdater, SetUserLanguageDocument, SetUserLanguageGQL, SetUserMIDefaultValueUpdater, SetUserMiDefaultValueDocument, SetUserMiDefaultValueGQL, ShareBasketDocument, ShareBasketGQL, ShareBasketUpdater, ShoppingInfoFragmentDoc, SmartCardsInfoFragmentDoc, SplitRailJourneyFareFragmentDoc, StorageService, SuggestPlacesDocument, SuggestPlacesFetcher, SuggestPlacesGQL, TicketOfficeInfoFragmentDoc, TicketQueueService, TicketType, TimeType, ToiletsBabyInfoFragmentDoc, TrainlineTenantIsFrenchDocument, TrainlineTenantIsFrenchFetcher, TrainlineTenantIsFrenchGQL, TransportType, TravellerGuestsFieldsFragmentDoc, TravellerUserFieldsFragmentDoc, UiConfigTarget, UpdateBasketNotesDocument, UpdateBasketNotesGQL, UpdateBasketTitleDocument, UpdateBasketTitleGQL, UpdateDOBUpdater, UpdateDobDocument, UpdateDobGQL, UpdateExchangeBasketDocument, UpdateExchangeBasketGQL, UpdateExchangeBasketUpdater, UpdateFlightSeatMapDocument, UpdateFlightSeatMapGQL, UpdateFlightSeatMapUpdater, UserApproversFieldsFragmentDoc, UserFieldsFragmentDoc, UserMessagesFetcher, UserMiFragmentDoc, UserMinimalFieldsFragmentDoc, UserProductsFetcher, UserService, UserSummaryFieldsFragmentDoc, UserUiConfigsFetcher, UsersFragmentDoc, ValidateBasketItemMiDocument, ValidateBasketItemMiFetcher, ValidateBasketItemMiGQL, ValidateBasketMiDocument, ValidateBasketMiFetcher, ValidateBasketMiGQL, VehicleType, WebTokenService, WifiInfoFragmentDoc, WithSubscriptionComponent };
|
|
20704
22575
|
//# sourceMappingURL=sabstravtech-obtservices-angular.mjs.map
|