Files
web/apps/scandic-web/components/HotelReservation/SelectHotel/helpers.ts
Anton Gunnarsson bbcabfa0ba Merged in feat/sw-2864-move-hotels-router-to-trpc-package (pull request #2410)
feat (SW-2864): Move booking router to trpc package

* Add env to trpc package

* Add eslint to trpc package

* Apply lint rules

* Use direct imports from trpc package

* Add lint-staged config to trpc

* Move lang enum to common

* Restructure trpc package folder structure

* WIP first step

* update internal imports in trpc

* Fix most errors in scandic-web

Just 100 left...

* Move Props type out of trpc

* Fix CategorizedFilters types

* Move more schemas in hotel router

* Fix deps

* fix getNonContentstackUrls

* Fix import error

* Fix entry error handling

* Fix generateMetadata metrics

* Fix alertType enum

* Fix duplicated types

* lint:fix

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package

* Fix broken imports

* Move booking router to trpc package

* Merge branch 'master' into feat/sw-2864-move-hotels-router-to-trpc-package


Approved-by: Linus Flood
2025-06-26 09:02:59 +00:00

315 lines
8.7 KiB
TypeScript

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<Result>[]
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<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
}
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<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[]
): CategorizedHotelFilters {
const defaultFilters = { facilityFilters: [], surroundingsFilters: [] }
if (!hotels.length) {
return defaultFilters
}
const filters = hotels.flatMap(({ hotel }) =>
hotel.detailedFacilities.map(
(facility) =>
<HotelFilter>{
...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<CategorizedHotelFilters>((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)
}