Files
web/apps/scandic-web/components/HotelReservation/SelectHotel/helpers.ts
2025-04-01 08:18:22 +00:00

295 lines
8.8 KiB
TypeScript

import { getHotel } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
import { getLang } from "@/i18n/serverContext"
import { generateChildrenString } from "../utils"
import type {
AlternativeHotelsAvailabilityInput,
AvailabilityInput,
} from "@/types/components/hotelReservation/selectHotel/availabilityInput"
import type { CategorizedFilters } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import type { AdditionalData, DetailedFacility, Hotel } from "@/types/hotel"
import type { HotelsAvailabilityItem } from "@/types/trpc/routers/hotel/availability"
import type {
HotelLocation,
Location,
} from "@/types/trpc/routers/hotel/locations"
interface AvailabilityResponse {
availability: HotelsAvailabilityItem[]
}
export interface HotelResponse {
availability: HotelsAvailabilityItem
hotel: Hotel
additionalData: AdditionalData
}
type Result = AvailabilityResponse | null
type SettledResult = PromiseSettledResult<Result>[]
async function enhanceHotels(hotels: HotelsAvailabilityItem[]) {
const language = 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 alternativeHotelIds = await serverClient().hotel.nearbyHotelIds({
hotelId,
})
if (!alternativeHotelIds) {
return null
}
return await serverClient().hotel.availability.hotelsByHotelIds({
...input,
hotelIds: alternativeHotelIds,
})
}
async function fetchAvailableHotels(input: AvailabilityInput) {
return await serverClient().hotel.availability.hotelsByCity(input)
}
async function fetchAvailableHotelsWithRedemption(input: AvailabilityInput) {
return await serverClient().hotel.availability.hotelsByCityWithRedemption(
input
)
}
async function fetchBookingCodeAvailableHotels(input: AvailabilityInput) {
return await serverClient().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()
}
export async function getHotels(
booking: SelectHotelSearchParams,
isAlternativeFor: HotelLocation | null,
bookingCode: string | undefined,
city: Location,
redemption: boolean
) {
let availableHotelsResponse: SettledResult = []
if (isAlternativeFor) {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(async (room) => {
return fetchAlternativeHotels(isAlternativeFor.id, {
adults: room.adults,
bookingCode,
children: room.childrenInRoom
? generateChildrenString(room.childrenInRoom)
: undefined,
redemption,
roomStayEndDate: booking.toDate,
roomStayStartDate: booking.fromDate,
})
})
)
} else if (bookingCode) {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(async (room) => {
return fetchBookingCodeAvailableHotels({
adults: room.adults,
bookingCode,
children: room.childrenInRoom
? generateChildrenString(room.childrenInRoom)
: undefined,
cityId: city.id,
roomStayStartDate: booking.fromDate,
roomStayEndDate: booking.toDate,
})
})
)
} else if (redemption) {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(
async (room) =>
await fetchAvailableHotelsWithRedemption({
adults: room.adults,
children: room.childrenInRoom
? generateChildrenString(room.childrenInRoom)
: undefined,
cityId: city.id,
redemption,
roomStayEndDate: booking.toDate,
roomStayStartDate: booking.fromDate,
})
)
)
} else {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(
async (room) =>
await fetchAvailableHotels({
adults: room.adults,
children: room.childrenInRoom
? generateChildrenString(room.childrenInRoom)
: undefined,
cityId: city.id,
redemption,
roomStayEndDate: booking.toDate,
roomStayStartDate: booking.fromDate,
})
)
)
}
const fulfilledAvailabilities = getFulfilledResponses<AvailabilityResponse>(
availableHotelsResponse
)
const availablilityItems = getHotelAvailabilityItems(fulfilledAvailabilities)
const availableHotels = sortAndFilterHotelsByAvailability(availablilityItems)
if (!availableHotels.length) {
return []
}
const hotelsResponse = await enhanceHotels(availableHotels)
const hotels = getFulfilledResponses<HotelResponse>(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[]
): CategorizedFilters {
const defaultFilters = { facilityFilters: [], surroundingsFilters: [] }
if (!hotels.length) {
return defaultFilters
}
const filters = hotels.flatMap(({ hotel }) => hotel.detailedFacilities)
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
const filterList: DetailedFacility[] = uniqueFilterIds
.map((filterId) => filters.find((filter) => filter.id === filterId))
.filter((filter): filter is DetailedFacility => filter !== undefined)
.sort((a, b) => b.sortOrder - a.sortOrder)
return filterList.reduce<CategorizedFilters>((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)
}