import { dt } from "@scandic-hotels/common/dt" import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel" import { getHotelFilters } from "@scandic-hotels/trpc/routers/hotels/filters/utils" import { generateChildrenString } from "@scandic-hotels/trpc/routers/hotels/helpers" import { serverClient } from "../../trpc" import { getHotel } from "../../trpc/memoizedRequests" import type { Lang } from "@scandic-hotels/common/constants/language" import type { HotelsAvailabilityItem } from "@scandic-hotels/trpc/types/availability" import type { Child } from "@scandic-hotels/trpc/types/child" import type { AdditionalData, Hotel, Restaurant, } from "@scandic-hotels/trpc/types/hotel" import type { HotelLocation, Location, } from "@scandic-hotels/trpc/types/locations" import type { CategorizedHotelFilters, SelectHotelFilter } from "../../types" type AvailabilityInput = { cityId: string roomStayStartDate: string roomStayEndDate: string adults: number children?: string bookingCode?: string redemption?: boolean } type AlternativeHotelsAvailabilityInput = { roomStayStartDate: string roomStayEndDate: string adults: number children?: string bookingCode?: string redemption?: boolean } interface AvailabilityResponse { availability: HotelsAvailabilityItem[] } export interface HotelResponse { availability: HotelsAvailabilityItem hotel: Hotel additionalData: AdditionalData url: string | null restaurants: Restaurant[] } type Result = AvailabilityResponse | null type SettledResult = PromiseSettledResult[] async function enhanceHotels(hotels: HotelsAvailabilityItem[], language: Lang) { return await Promise.allSettled( hotels.map(async (availability) => { const hotelData = await getHotel({ hotelId: availability.hotelId.toString(), isCardOnlyPayment: false, language, }) if (!hotelData) { return null } return { availability, hotel: hotelData.hotel, additionalData: hotelData.additionalData, url: hotelData.url, restaurants: hotelData.restaurants, } }) ) } async function fetchAlternativeHotels( hotelId: string, input: AlternativeHotelsAvailabilityInput ) { const caller = await serverClient() const alternativeHotelIds = await caller.hotel.nearbyHotelIds({ hotelId, }) if (!alternativeHotelIds) { return null } return await caller.hotel.availability.hotelsByHotelIds({ ...input, hotelIds: alternativeHotelIds, }) } async function fetchAvailableHotels(input: AvailabilityInput) { const caller = await serverClient() return await caller.hotel.availability.hotelsByCity(input) } async function fetchBookingCodeAvailableHotels(input: AvailabilityInput) { const caller = await serverClient() return await caller.hotel.availability.hotelsByCityWithBookingCode(input) } function getFulfilledResponses(result: PromiseSettledResult[]) { const fulfilledResponses: NonNullable[] = [] for (const res of result) { if (res.status === "fulfilled" && res.value) { fulfilledResponses.push(res.value) } } return fulfilledResponses } function getHotelAvailabilityItems(hotels: AvailabilityResponse[]) { return hotels.map((hotel) => hotel.availability) } // Filter out hotels that are unavailable for // at least one room. function sortAndFilterHotelsByAvailability( fulfilledHotels: HotelsAvailabilityItem[][] ) { const availableHotels = new Map< HotelsAvailabilityItem["hotelId"], HotelsAvailabilityItem >() const unavailableHotels = new Map< HotelsAvailabilityItem["hotelId"], HotelsAvailabilityItem >() const unavailableHotelIds = new Set() for (const availabilityHotels of fulfilledHotels) { for (const hotel of availabilityHotels) { if (hotel.status === AvailabilityEnum.Available) { if (availableHotels.has(hotel.hotelId)) { const currentAddedHotel = availableHotels.get(hotel.hotelId) // Make sure the cheapest version of the room is the one // we keep so that it matches the cheapest room on select-rate if ( (hotel.productType?.public && currentAddedHotel?.productType?.public && hotel.productType.public.localPrice.pricePerNight < currentAddedHotel.productType.public.localPrice .pricePerNight) || (hotel.productType?.member && currentAddedHotel?.productType?.member && hotel.productType.member.localPrice.pricePerNight < currentAddedHotel.productType.member.localPrice.pricePerNight) ) { availableHotels.set(hotel.hotelId, hotel) } } else { availableHotels.set(hotel.hotelId, hotel) } } else { unavailableHotels.set(hotel.hotelId, hotel) unavailableHotelIds.add(hotel.hotelId) } } } for (const [hotelId] of unavailableHotelIds.entries()) { if (availableHotels.has(hotelId)) { availableHotels.delete(hotelId) } } return [ Array.from(availableHotels.values()), Array.from(unavailableHotels.values()), ].flat() } type GetHotelsInput = { fromDate: string toDate: string rooms: { adults: number childrenInRoom?: Child[] }[] isAlternativeFor: HotelLocation | null bookingCode: string | undefined city: Location redemption: boolean lang: Lang } export async function getHotels({ rooms, fromDate, toDate, isAlternativeFor, bookingCode, city, redemption, lang, }: GetHotelsInput) { let availableHotelsResponse: SettledResult = [] // Return empty array (forced No availability) when search dates are invalid if ( dt(fromDate).isBefore(dt(), "day") || dt(toDate).isSameOrBefore(fromDate, "day") ) { return [] } if (isAlternativeFor) { availableHotelsResponse = await Promise.allSettled( rooms.map(async (room) => { return fetchAlternativeHotels(isAlternativeFor.id, { adults: room.adults, bookingCode, children: room.childrenInRoom ? generateChildrenString(room.childrenInRoom) : undefined, redemption, roomStayEndDate: toDate, roomStayStartDate: fromDate, }) }) ) } else if (bookingCode) { availableHotelsResponse = await Promise.allSettled( rooms.map(async (room) => { return fetchBookingCodeAvailableHotels({ adults: room.adults, bookingCode, children: room.childrenInRoom ? generateChildrenString(room.childrenInRoom) : undefined, cityId: city.id, roomStayStartDate: fromDate, roomStayEndDate: toDate, }) }) ) } else { availableHotelsResponse = await Promise.allSettled( rooms.map( async (room) => await fetchAvailableHotels({ adults: room.adults, children: room.childrenInRoom ? generateChildrenString(room.childrenInRoom) : undefined, cityId: city.id, redemption, roomStayEndDate: toDate, roomStayStartDate: fromDate, }) ) ) } const fulfilledAvailabilities = getFulfilledResponses( availableHotelsResponse ) const availablilityItems = getHotelAvailabilityItems(fulfilledAvailabilities) const availableHotels = sortAndFilterHotelsByAvailability(availablilityItems) if (!availableHotels.length) { return [] } const hotelsResponse = await enhanceHotels(availableHotels, lang) const hotels = getFulfilledResponses(hotelsResponse) return hotels } export async function fetchHotelFiltersAndMapToCategorizedFilters( hotels: HotelResponse[], showBookingCodeFilter: boolean, lang: Lang ): Promise { const defaultFilters = { facilityFilters: [], surroundingsFilters: [] } if (!hotels.length) { return defaultFilters } const { countryFilters, ...hotelFilters } = await getHotelFilters(lang) const allFlattenedFilters = Object.values(hotelFilters).flat() const filters = hotels.flatMap(({ hotel, availability }) => { const hotelFilterData = allFlattenedFilters.map((filter) => { const hotelHasFilter = hotel.detailedFacilities.some( (facility) => facility.id.toString() === filter.id ) return { ...filter, hotelId: hotelHasFilter ? hotel.operaId : null, hotelIds: hotelHasFilter ? [hotel.operaId] : [], bookingCodeFilteredIds: (availability.bookingCode || !showBookingCodeFilter) && hotelHasFilter ? [hotel.operaId] : [], } }) return hotelFilterData }) const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))] const filterList: SelectHotelFilter[] = uniqueFilterIds .map((filterId) => { const filter = filters.find((f) => f.id === filterId) // List and include all hotel Ids having same filter / amenity if (filter) { const matchingFilters = filters.filter((f) => f.id === filterId) filter.hotelIds = matchingFilters .map((f) => f.hotelId) .filter((id) => id !== null) filter.bookingCodeFilteredIds = [ ...new Set( matchingFilters.flatMap((f) => f.bookingCodeFilteredIds ?? []) ), ] } return filter }) .filter((filter): filter is SelectHotelFilter => filter !== undefined) .sort((a, b) => b.sortOrder - a.sortOrder) const facilityFilters = filterList.filter( (filter) => filter.filterType === "facility" ) const surroundingsFilters = filterList.filter( (filter) => filter.filterType === "surroundings" ) return { facilityFilters, surroundingsFilters, } }