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
315 lines
8.7 KiB
TypeScript
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)
|
|
}
|