import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel" import { generateChildrenString } from "@scandic-hotels/trpc/routers/hotels/helpers" import { type AdditionalData, type Hotel, } from "@scandic-hotels/trpc/types/hotel" import { type HotelLocation, type Location, } from "@scandic-hotels/trpc/types/locations" import { getHotel } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import { getLang } from "@/i18n/serverContext" import type { HotelsAvailabilityItem } from "@scandic-hotels/trpc/types/availability" import type { Child } from "@scandic-hotels/trpc/types/child" import type { AlternativeHotelsAvailabilityInput, AvailabilityInput, } from "@/types/components/hotelReservation/selectHotel/availabilityInput" import type { CategorizedHotelFilters, HotelFilter, } from "@/types/components/hotelReservation/selectHotel/hotelFilters" interface AvailabilityResponse { availability: HotelsAvailabilityItem[] } export interface HotelResponse { availability: HotelsAvailabilityItem hotel: Hotel additionalData: AdditionalData } type Result = AvailabilityResponse | null type SettledResult = PromiseSettledResult[] async function enhanceHotels(hotels: HotelsAvailabilityItem[]) { const language = await getLang() 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, } }) ) } 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 } export async function getHotels({ rooms, fromDate, toDate, isAlternativeFor, bookingCode, city, redemption, }: GetHotelsInput) { let availableHotelsResponse: SettledResult = [] 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) const hotels = getFulfilledResponses(hotelsResponse) return hotels } const hotelSurroundingsFilterNames = [ "Hotel surroundings", "Hotel omgivelser", "Hotelumgebung", "Hotellia lähellä", "Hotellomgivelser", "Omgivningar", ] const hotelFacilitiesFilterNames = [ "Hotel facilities", "Hotellfaciliteter", "Hotelfaciliteter", "Hotel faciliteter", "Hotel-Infos", "Hotellin palvelut", ] export function getFiltersFromHotels( hotels: HotelResponse[] ): CategorizedHotelFilters { const defaultFilters = { facilityFilters: [], surroundingsFilters: [] } if (!hotels.length) { return defaultFilters } const filters = hotels.flatMap(({ hotel }) => hotel.detailedFacilities.map( (facility) => { ...facility, hotelId: hotel.operaId, hotelIds: [hotel.operaId], } ) ) const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))] const filterList: HotelFilter[] = uniqueFilterIds .map((filterId) => { const filter = filters.find((f) => f.id === filterId) // List and include all hotel Ids having same filter / amenity if (filter) { filter.hotelIds = filters .filter((f) => f.id === filterId) .map((f) => f.hotelId) } return filter }) .filter((filter): filter is HotelFilter => filter !== undefined) .sort((a, b) => b.sortOrder - a.sortOrder) return filterList.reduce((filters, filter) => { if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter)) { filters.surroundingsFilters.push(filter) } if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter)) { filters.facilityFilters.push(filter) } return filters }, defaultFilters) }