feat: add multiroom tracking to booking flow
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
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 { 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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
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 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 {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user