Files
web/packages/booking-flow/lib/components/SelectHotel/helpers.ts
Erik Tiekstra 0c6a4cf186 feat(BOOK-463): Fetching hotel filters from CMS and using these inside the destination pages and select hotel page
* feat(BOOK-463): Fetching hotel filters from CMS and using these inside the destination pages

* fix(BOOK-698): fetch hotel filters from CMS on select hotel page

Approved-by: Bianca Widstam
2026-01-12 12:02:25 +00:00

341 lines
9.7 KiB
TypeScript

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<Result>[]
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<T>(result: PromiseSettledResult<T | null>[]) {
const fulfilledResponses: NonNullable<T>[] = []
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<HotelsAvailabilityItem["hotelId"]>()
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<AvailabilityResponse>(
availableHotelsResponse
)
const availablilityItems = getHotelAvailabilityItems(fulfilledAvailabilities)
const availableHotels = sortAndFilterHotelsByAvailability(availablilityItems)
if (!availableHotels.length) {
return []
}
const hotelsResponse = await enhanceHotels(availableHotels, lang)
const hotels = getFulfilledResponses<HotelResponse>(hotelsResponse)
return hotels
}
export async function fetchHotelFiltersAndMapToCategorizedFilters(
hotels: HotelResponse[],
showBookingCodeFilter: boolean,
lang: Lang
): Promise<CategorizedHotelFilters> {
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,
}
}