@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.
@@ -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, FlightSearchType, FlightDirectionEnum, BaseChatbotService, UserFavorurite, RailSearchCriteria, TimeWindow, LocationTypes, EurostarSearchType, ModalTypes, BaseGetMiRequiringMandatoryDefaultValueFetcher, BaseHttpCancelInterceptor } from '@sabstravtech/obtservices/base';
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.singleOrReturn
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.singleOrReturn
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
- // remove any returns which time conflict with this journey
18089
- const arrivalTimeSplit = result.arrive.split(':');
18090
- const arrivalDate = moment$1();
18091
- arrivalDate.hour(arrivalTimeSplit[0]);
18092
- arrivalDate.minutes(arrivalTimeSplit[1]);
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
- const departTimeSplit = ret.depart.split(':');
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
- const user = this.userService.fullUserDetails.allDetails.value;
18308
- if (!user?.aiTravelAssistantEnabled) {
18309
- return;
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' || tt === 'flight' || tt === 'hotel' || tt === 'eurostar')) {
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
- if (railResults?.[0]) {
18444
- this.resultAwareService.captureRailResults(railResults[0]);
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
- const rawResults = flightSearch.results?.value ?? [];
18940
- const { outbound, inbound, multi } = extractJourneys(rawResults);
18941
- const isMultiCity = (parameters.legs?.length ?? 0) >= 2;
18942
- // Feed ResultAwareService with outbound so its condensed-cards API is
18943
- // populated for downstream card click → filter navigation.
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
- const outboundCondensed = this.resultAwareService.getCondensedResults();
18946
- let inboundCondensed = [];
18947
- if (inbound.length) {
18948
- // Build inbound condensed by re-running the condensation. The
18949
- // ResultAwareService keeps state for outbound, so we condense the
18950
- // raw inbound array via a temporary capture/restore.
18951
- this.resultAwareService.captureFlightResults(inbound);
18952
- inboundCondensed = this.resultAwareService.getCondensedResults();
18953
- // Restore outbound as the "current" capture for click-through filters.
18954
- this.resultAwareService.captureFlightResults(outbound);
18955
- }
18956
- const flightResults = isMultiCity
18957
- ? this.buildMultiCityFlightResults(parameters, outbound)
18958
- : this.buildFlightResults(outboundCondensed, inboundCondensed, parameters, outbound, inbound);
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
- flightResults,
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
- this.chatbotService.setSearching(false);
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
- // sortType is widened on ChatSearchParameters to cover hotel + flight values;
18969
- // narrow back to the flight-only subset accepted by ChatFlightResults.sort.
18970
- const sort = parameters.sortType === 'fastest' || parameters.sortType === 'shortest'
18971
- ? parameters.sortType
18972
- : 'cheapest';
18973
- // Optional airline filter: if the user asked for a specific carrier
18974
- // ("Give me BA flights…"), keep only matching itineraries. Honour it
18975
- // strictly — better to show "no matches" than mislead the user.
18976
- const airlineFilter = (parameters.airlineCode || parameters.airline || '').trim().toLowerCase();
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 (!airlineFilter)
19303
+ if (!airlineFilters.length)
18979
19304
  return list;
18980
- return list.filter((r) => {
18981
- const code = (r.flightNumbers?.match(/^[A-Z]{2,3}/i)?.[0] || '').toLowerCase();
18982
- const name = (r.airline || '').toLowerCase();
18983
- return code === airlineFilter || name.includes(airlineFilter);
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=0 strictly. The form-level
18987
- // maxConnections setting goes to the supplier, but some suppliers ignore
18988
- // it — re-filter client-side so the user gets what they asked for.
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((r) => (r.stops ?? 0) <= max);
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, keep the cheapest fare per flight,
18998
- // then sort. Without dedup we'd often render two or three "cards" that
18999
- // are the same KL1034 with different fare brands (Restricted/Flex/Plus)
19000
- // the user wants one card per flight with the cheapest price shown,
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
- // Key includes outbound flight numbers + inbound flight numbers AND each
19005
- // leg's depart time, so two operationally-distinct flights that happen
19006
- // to share marketing flight numbers (codeshares on different equipment
19007
- // or schedules) don't collapse into one card.
19008
- const parseDurationToMins = (d) => {
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 h = /(\d+)\s*h/i.exec(d);
19012
- const m = /(\d+)\s*m/i.exec(d);
19013
- const total = (h ? +h[1] : 0) * 60 + (m ? +m[1] : 0);
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 = (r) => parseDurationToMins(r.duration) + (r.inbound ? parseDurationToMins(r.inbound.duration) : 0);
19369
+ const totalDurationMins = (result) => parseDurationToMins(result.duration) +
19370
+ (result.inbound ? parseDurationToMins(result.inbound.duration) : 0);
19017
19371
  const dedupeCheapestByFlight = (list) => {
19018
- const seen = new Map();
19019
- for (const r of list) {
19020
- const out = r.flightNumbers || '';
19021
- const inb = r.inbound?.flightNumbers || '';
19022
- if (!out && !inb) {
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
- seen.set(`__idx${r.index}`, r);
19379
+ cheapestByKey.set(`__idx${result.index}`, result);
19026
19380
  continue;
19027
19381
  }
19028
- const key = `${out}|${inb}|${r.depart}|${r.inbound?.depart ?? ''}`;
19029
- const existing = seen.get(key);
19030
- if (!existing || (r.price ?? Infinity) < (existing.price ?? Infinity)) {
19031
- seen.set(key, r);
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
- const arr = Array.from(seen.values());
19035
- if (sort === 'fastest' || sort === 'shortest') {
19036
- arr.sort((a, b) => totalDurationMins(a) - totalDurationMins(b));
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
- else {
19039
- arr.sort((a, b) => (a.price ?? Infinity) - (b.price ?? Infinity));
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
- return arr;
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
- // Pre-compute fare-options groupings for the expand panel: group the raw
19046
- // itineraries by their outbound (+ inbound) flight-numbers signature so a
19047
- // card can show all bookable cabins/brands for the same physical flights.
19048
- // For one-way searches the lookup key on the card side is just the
19049
- // outbound flight numbers (no trailing "|"); use the same shape here so
19050
- // the maps actually match. For returns, include the inbound legs.
19051
- const outFareIndex = this.indexFaresByFlights(rawOutbound, isReturn);
19052
- const inFareIndex = this.indexFaresByFlights(rawInbound, false);
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((r) => this.toFlightCard(r, parameters, 'outbound', outFareIndex));
19462
+ .map((result) => this.toFlightCard(result, parameters, 'outbound', outFareIndex));
19056
19463
  const inboundCards = inDeduped
19057
19464
  .slice(0, MAX_FLIGHT_CARDS)
19058
- .map((r) => this.toFlightCard(r, parameters, 'inbound', inFareIndex));
19059
- return { outbound: outboundCards, inbound: inboundCards, isReturn, parameters, sort };
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 and sort cheapest first.
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()].sort((a, b) => (a?.total?.price ?? Infinity) - (b?.total?.price ?? Infinity));
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?.fareConditions?.outboundFareConditions?.cabinClass || '';
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?.departureDateTime || '').slice(0, 10);
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.departureDateTime && last.arrivalDateTime) {
19216
- const mins = Math.round((new Date(last.arrivalDateTime).getTime() - new Date(first.departureDateTime).getTime()) /
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.departureAirport || leg.departureCode || leg.departure,
19655
+ fromCode: first.originAirport || leg.departureCode || leg.departure,
19222
19656
  fromName: leg.departure,
19223
- toCode: last.arrivalAirport || leg.arrivalCode || leg.arrival,
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.departureDateTime),
19227
- arriveTime: this.formatHHmm(last.arrivalDateTime),
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 (parameters.departureTime) {
19554
- flightParams.departTime = this.normalizeTime(parameters.departureTime);
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 (parameters.returnTime) {
19565
- flightParams.arriveTime = this.normalizeTime(parameters.returnTime);
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
- if (parameters.maxConnections != null) {
19604
- flightParams.maxConnections = parameters.maxConnections;
19605
- if ('directDefaultValue' in flightParams) {
19606
- flightParams.directDefaultValue = parameters.maxConnections === 0;
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
- if (parameters.outboundTimeCriteria) {
19626
- const mapped = toTimeWindow(parameters.outboundTimeCriteria);
19627
- if (mapped)
19628
- flightParams.outTimeCriteria = mapped;
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
- if (parameters.returnTimeCriteria) {
19631
- const mapped = toTimeWindow(parameters.returnTimeCriteria);
19632
- if (mapped)
19633
- flightParams.inTimeCriteria = mapped;
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 → operator1 (which is preferredCarriers[0]).
19651
- // The form's airline field is a typeahead backed by the searchAirlines
19652
- // GraphQL query, so we MUST write a real {code, name} object resolved
19653
- // from that source (an ad-hoc object with the wrong code wouldn't match
19654
- // anything server-side). Use either the IATA code or the name as the
19655
- // search term, prefer an exact IATA match, then an exact name match,
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 the AI passes a "no preference" sentinel — null,
19659
- // empty string, or the literal word "any"/"all". preferredCarriers was
19660
- // already reset to all-nulls at the top of this method, so no carrier
19661
- // filter will be applied to the search in that case.
19662
- const rawAirlineTerm = (parameters.airlineCode || parameters.airline || '').toString().trim();
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 term = (parameters.airlineCode || parameters.airline || '').trim();
19677
- if (term.length >= 2) {
19678
- try {
19679
- const matches = await firstValueFrom(this.searchService.getAirlinesList(term));
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
- constructor(chatbotService, searchService, hotelAvalibilityService, resultAwareService) {
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
- const hotelsWithPrices = candidates
19857
- .filter((hotel) => (hotel.availableRates?.rooms ?? []).some((r) => r?.total > 0))
19858
- .slice(0, MAX_HOTEL_CARDS);
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.roomDescription || room.roomType || 'Standard 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 offices = await firstValueFrom(this.searchService.getOfficeLocationsNoUserAddresses(''));
20684
+ const searchTerm = parameters.arrival?.toLowerCase() ?? '';
20685
+ const offices = await firstValueFrom(this.searchService.getOfficeLocationsNoUserAddresses(searchTerm));
20013
20686
  if (offices?.length > 0) {
20014
- const searchTerm = parameters.arrival?.toLowerCase() ?? '';
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
- /** Safety timeout (ms) waiting for eurostar results. */
20059
- const EUROSTAR_RESULTS_WAIT_MS = 60_000;
20060
- /** Maximum number of outbound/inbound cards to surface in the chat widget. */
20061
- const MAX_EUROSTAR_CARDS = 3;
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 Eurostar searches. Registered with
20738
+ * Handles chatbot-driven car hire searches. Registered with
20064
20739
  * `ChatbotSearchDispatcherService`; not used directly.
20065
20740
  *
20066
- * Populates the Eurostar search state from the chatbot's parameters and, when
20067
- * `autoSearch` is true, runs the search headlessly so it works from any page.
20068
- * Once results arrive, builds `ChatEurostarResults` and patches them onto the
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 ChatbotEurostarSearchService {
20745
+ class ChatbotCarHireSearchService {
20072
20746
  chatbotService;
20073
20747
  searchService;
20748
+ resultAwareService;
20074
20749
  userService;
20075
- travelType = 'eurostar';
20076
- resultsSubscription = null;
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.Eurostar].chosen = true;
20086
- this.searchService.search_objects[ServiceType.Eurostar].originalChosen = true;
20773
+ this.searchService.search_objects[ServiceType.Car].chosen = true;
20774
+ this.searchService.search_objects[ServiceType.Car].originalChosen = true;
20087
20775
  this.searchService.determineHighestSearchPriority();
20088
- // Auto-add the signed-in user as the traveller when none is selected so
20089
- // the search isn't blocked by the "add a traveller" validation rule.
20090
- // Matches the rail handler pattern.
20091
- if (this.searchService.travellerInformation.value.length === 0) {
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
- const departDate = moment$1(parameters.departureDate);
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 eurostarSearch = this.searchService.searches[ServiceType.Eurostar];
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. Keeping them in sync also satisfies the cabin-match
20285
- // validation rule (`checkCabinMatchisValid`).
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 = mappedCabin;
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