feat: bedtypes is selectable again
This commit is contained in:
committed by
Michael Zetterberg
parent
f62723c6e5
commit
afb37d0cc5
@@ -1,14 +1,19 @@
|
||||
import deepmerge from "deepmerge"
|
||||
import stringify from "json-stable-stringify-without-jsonify"
|
||||
|
||||
import { REDEMPTION } from "@/constants/booking"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { badRequestError } from "@/server/errors/trpc"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { cache } from "@/utils/cache"
|
||||
|
||||
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
||||
import { type RoomFeaturesInput, roomPackagesInputSchema } from "./input"
|
||||
import { metrics } from "./metrics"
|
||||
import {
|
||||
type Cities,
|
||||
@@ -16,15 +21,31 @@ import {
|
||||
citiesSchema,
|
||||
countriesSchema,
|
||||
getHotelIdsSchema,
|
||||
hotelsAvailabilitySchema,
|
||||
hotelSchema,
|
||||
locationsSchema,
|
||||
packagesSchema,
|
||||
roomFeaturesSchema,
|
||||
roomsAvailabilitySchema,
|
||||
} from "./output"
|
||||
import { getHotel } from "./query"
|
||||
|
||||
import type { z } from "zod"
|
||||
|
||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
import type {
|
||||
DestinationPagesHotelData,
|
||||
Room as RoomCategory,
|
||||
} from "@/types/hotel"
|
||||
import type { PackagesInput } from "@/types/requests/packages"
|
||||
import type {
|
||||
HotelsAvailabilityInputSchema,
|
||||
HotelsByHotelIdsAvailabilityInputSchema,
|
||||
RoomsAvailabilityInputRoom,
|
||||
RoomsAvailabilityInputSchema,
|
||||
} from "@/types/trpc/routers/hotel/availability"
|
||||
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
||||
import type {
|
||||
CitiesGroupedByCountry,
|
||||
CityLocation,
|
||||
@@ -34,9 +55,9 @@ import type {
|
||||
Products,
|
||||
RateDefinition,
|
||||
RedemptionsProduct,
|
||||
RoomConfiguration,
|
||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
import type { Endpoint } from "@/lib/api/endpoints"
|
||||
import type { selectedRoomAvailabilityInputSchema } from "./input"
|
||||
|
||||
export function getPoiGroupByCategoryName(category: string | undefined) {
|
||||
if (!category) return PointOfInterestGroupEnum.LOCATION
|
||||
@@ -608,51 +629,184 @@ function findProduct(product: Products, rateDefinition: RateDefinition) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSelectedRoomAvailability(
|
||||
input: z.input<typeof selectedRoomAvailabilityInputSchema>,
|
||||
lang: string,
|
||||
serviceToken: string,
|
||||
userPoints?: number
|
||||
export const getHotel = cache(
|
||||
async (input: HotelInput, serviceToken: string) => {
|
||||
const callable = async function (
|
||||
hotelId: HotelInput["hotelId"],
|
||||
language: HotelInput["language"],
|
||||
isCardOnlyPayment?: HotelInput["isCardOnlyPayment"]
|
||||
) {
|
||||
/**
|
||||
* Since API expects the params appended and not just
|
||||
* a comma separated string we need to initialize the
|
||||
* SearchParams with a sequence of pairs
|
||||
* (include=City&include=NearbyHotels&include=Restaurants etc.)
|
||||
**/
|
||||
const params = new URLSearchParams([
|
||||
["include", "AdditionalData"],
|
||||
["include", "City"],
|
||||
["include", "NearbyHotels"],
|
||||
["include", "Restaurants"],
|
||||
["include", "RoomCategories"],
|
||||
["language", toApiLang(language)],
|
||||
])
|
||||
metrics.hotel.counter.add(1, {
|
||||
hotelId,
|
||||
language,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.hotelData start",
|
||||
JSON.stringify({ query: { hotelId, params: params.toString() } })
|
||||
)
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.hotel.fail.add(1, {
|
||||
hotelId,
|
||||
language,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.hotelData error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params: params.toString() },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateHotelData = hotelSchema.safeParse(apiJson)
|
||||
|
||||
if (!validateHotelData.success) {
|
||||
metrics.hotel.fail.add(1, {
|
||||
hotelId,
|
||||
language,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validateHotelData.error),
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.hotels.hotelData validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params: params.toString() },
|
||||
error: validateHotelData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
metrics.hotel.success.add(1, {
|
||||
hotelId,
|
||||
language,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.hotelData success",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params: params.toString() },
|
||||
})
|
||||
)
|
||||
const hotelData = validateHotelData.data
|
||||
|
||||
if (isCardOnlyPayment) {
|
||||
hotelData.hotel.merchantInformationData.alternatePaymentOptions = []
|
||||
}
|
||||
|
||||
const gallery = hotelData.additionalData?.gallery
|
||||
if (gallery) {
|
||||
const smallerImages = gallery.smallerImages
|
||||
const hotelGalleryImages =
|
||||
hotelData.hotel.hotelType === HotelTypeEnum.Signature
|
||||
? smallerImages.slice(0, 10)
|
||||
: smallerImages.slice(0, 6)
|
||||
hotelData.hotel.galleryImages = hotelGalleryImages
|
||||
}
|
||||
|
||||
return hotelData
|
||||
}
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`,
|
||||
async () => {
|
||||
return callable(input.hotelId, input.language, input.isCardOnlyPayment)
|
||||
},
|
||||
env.CACHE_TIME_HOTELS
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export async function getHotelsAvailabilityByCity(
|
||||
input: HotelsAvailabilityInputSchema,
|
||||
apiLang: string,
|
||||
token: string, // Either service token or user access token in case of redemption search
|
||||
userPoints: number = 0
|
||||
) {
|
||||
const {
|
||||
adults,
|
||||
bookingCode,
|
||||
children,
|
||||
hotelId,
|
||||
roomStayEndDate,
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
redemption,
|
||||
} = input
|
||||
|
||||
const params: Record<string, string | number | undefined> = {
|
||||
const params: Record<string, string | number> = {
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
...(children && { children }),
|
||||
...(bookingCode && { bookingCode }),
|
||||
...(redemption && { isRedemption: "true" }),
|
||||
language: lang,
|
||||
...(redemption ? { isRedemption: "true" } : {}),
|
||||
language: apiLang,
|
||||
}
|
||||
|
||||
metrics.selectedRoomAvailability.counter.add(1, input)
|
||||
metrics.hotelsAvailability.counter.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
redemption,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.selectedRoomAvailability start",
|
||||
JSON.stringify({ query: { hotelId: input.hotelId, params } })
|
||||
"api.hotels.hotelsAvailability start",
|
||||
JSON.stringify({ query: { cityId, params } })
|
||||
)
|
||||
const apiResponseAvailability = await api.get(
|
||||
api.endpoints.v1.Availability.hotel(hotelId.toString()),
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Availability.city(cityId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponseAvailability.ok) {
|
||||
const text = await apiResponseAvailability.text()
|
||||
metrics.selectedRoomAvailability.fail.add(1, {
|
||||
hotelId,
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.hotelsAvailability.fail.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
@@ -660,61 +814,611 @@ export async function getSelectedRoomAvailability(
|
||||
bookingCode,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponseAvailability.status,
|
||||
statusText: apiResponseAvailability.statusText,
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.selectedRoomAvailability error",
|
||||
"api.hotels.hotelsAvailability error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
query: { cityId, params },
|
||||
error: {
|
||||
status: apiResponseAvailability.status,
|
||||
statusText: apiResponseAvailability.statusText,
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw new Error("Failed to fetch selected room availability")
|
||||
throw new Error("Failed to fetch hotels availability by city")
|
||||
}
|
||||
const apiJsonAvailability = await apiResponseAvailability.json()
|
||||
const validateAvailabilityData =
|
||||
roomsAvailabilitySchema.safeParse(apiJsonAvailability)
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
||||
if (!validateAvailabilityData.success) {
|
||||
metrics.selectedRoomAvailability.fail.add(1, {
|
||||
hotelId,
|
||||
metrics.hotelsAvailability.fail.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
redemption,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validateAvailabilityData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.selectedRoomAvailability validation error",
|
||||
"api.hotels.hotelsAvailability validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
query: { cityId, params },
|
||||
error: validateAvailabilityData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
const { rateDefinitions, roomConfigurations } = validateAvailabilityData.data
|
||||
|
||||
const rateDefinition = rateDefinitions.find(
|
||||
(rd) => rd.rateCode === input.rateCode
|
||||
metrics.hotelsAvailability.success.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
redemption,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.hotelsAvailability success",
|
||||
JSON.stringify({
|
||||
query: { cityId, params: params },
|
||||
})
|
||||
)
|
||||
if (redemption) {
|
||||
validateAvailabilityData.data.data.forEach((data) => {
|
||||
data.attributes.productType?.redemptions?.forEach((r) => {
|
||||
r.hasEnoughPoints = userPoints >= r.localPrice.pointsPerStay
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
availability: validateAvailabilityData.data.data.flatMap(
|
||||
(hotels) => hotels.attributes
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHotelsAvailabilityByHotelIds(
|
||||
input: HotelsByHotelIdsAvailabilityInputSchema,
|
||||
apiLang: string,
|
||||
serviceToken: string
|
||||
) {
|
||||
const {
|
||||
hotelIds,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
} = input
|
||||
|
||||
const params = new URLSearchParams([
|
||||
["roomStayStartDate", roomStayStartDate],
|
||||
["roomStayEndDate", roomStayEndDate],
|
||||
["adults", adults.toString()],
|
||||
["children", children ?? ""],
|
||||
["bookingCode", bookingCode],
|
||||
["language", apiLang],
|
||||
])
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
return cacheClient.cacheOrGet(
|
||||
`${apiLang}:hotels:availability:${hotelIds.join(",")}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
|
||||
async () => {
|
||||
/**
|
||||
* Since API expects the params appended and not just
|
||||
* a comma separated string we need to initialize the
|
||||
* SearchParams with a sequence of pairs
|
||||
* (hotelIds=810&hotelIds=879&hotelIds=222 etc.)
|
||||
**/
|
||||
|
||||
hotelIds.forEach((hotelId) =>
|
||||
params.append("hotelIds", hotelId.toString())
|
||||
)
|
||||
metrics.hotelsByHotelIdAvailability.counter.add(1, {
|
||||
hotelIds,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.hotelsByHotelIdAvailability start",
|
||||
JSON.stringify({ query: { params } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Availability.hotels(),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
||||
hotelIds,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.hotelsByHotelIdAvailability error",
|
||||
JSON.stringify({
|
||||
query: { params },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw new Error("Failed to fetch hotels availability by hotelIds")
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateAvailabilityData =
|
||||
hotelsAvailabilitySchema.safeParse(apiJson)
|
||||
if (!validateAvailabilityData.success) {
|
||||
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
||||
hotelIds,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validateAvailabilityData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.hotelsByHotelIdAvailability validation error",
|
||||
JSON.stringify({
|
||||
query: { params },
|
||||
error: validateAvailabilityData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
metrics.hotelsByHotelIdAvailability.success.add(1, {
|
||||
hotelIds,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.hotelsByHotelIdAvailability success",
|
||||
JSON.stringify({
|
||||
query: { params },
|
||||
})
|
||||
)
|
||||
return {
|
||||
availability: validateAvailabilityData.data.data.flatMap(
|
||||
(hotels) => hotels.attributes
|
||||
),
|
||||
}
|
||||
},
|
||||
env.CACHE_TIME_CITY_SEARCH
|
||||
)
|
||||
}
|
||||
|
||||
async function getRoomFeaturesInventory(
|
||||
input: RoomFeaturesInput,
|
||||
token: string
|
||||
) {
|
||||
const {
|
||||
adults,
|
||||
childrenInRoom,
|
||||
endDate,
|
||||
hotelId,
|
||||
roomFeatureCodes,
|
||||
startDate,
|
||||
} = input
|
||||
const cacheClient = await getCacheClient()
|
||||
return cacheClient.cacheOrGet(
|
||||
stringify(input),
|
||||
async function () {
|
||||
const params = {
|
||||
adults,
|
||||
hotelId,
|
||||
roomFeatureCode: roomFeatureCodes,
|
||||
roomStayEndDate: endDate,
|
||||
roomStayStartDate: startDate,
|
||||
...(childrenInRoom?.length && {
|
||||
children: generateChildrenString(childrenInRoom),
|
||||
}),
|
||||
}
|
||||
|
||||
metrics.roomFeatures.counter.add(1, params)
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Availability.roomFeatures(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = apiResponse.text()
|
||||
console.error(
|
||||
"api.availability.roomfeature error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
metrics.roomFeatures.fail.add(1, params)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
const validatedRoomFeaturesData = roomFeaturesSchema.safeParse(data)
|
||||
if (!validatedRoomFeaturesData.success) {
|
||||
console.error(
|
||||
"api.availability.roomfeature error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validatedRoomFeaturesData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
metrics.roomFeatures.success.add(1, params)
|
||||
|
||||
return validatedRoomFeaturesData.data
|
||||
},
|
||||
"5m"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getPackages(
|
||||
rawInput: PackagesInput,
|
||||
serviceToken: string
|
||||
) {
|
||||
const parsedInput = roomPackagesInputSchema.safeParse(rawInput)
|
||||
if (!parsedInput.success) {
|
||||
console.info(`Failed to parse input for Get Packages`)
|
||||
console.error(parsedInput.error)
|
||||
return null
|
||||
}
|
||||
const input = parsedInput.data
|
||||
const cacheClient = await getCacheClient()
|
||||
return cacheClient.cacheOrGet(
|
||||
stringify(input),
|
||||
async function () {
|
||||
const {
|
||||
adults,
|
||||
children,
|
||||
endDate,
|
||||
hotelId,
|
||||
lang,
|
||||
packageCodes,
|
||||
startDate,
|
||||
} = input
|
||||
const apiLang = toApiLang(lang)
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
adults: adults.toString(),
|
||||
children: children.toString(),
|
||||
endDate,
|
||||
language: apiLang,
|
||||
startDate,
|
||||
})
|
||||
|
||||
packageCodes.forEach((code) => {
|
||||
searchParams.append("packageCodes", code)
|
||||
})
|
||||
|
||||
const params = searchParams.toString()
|
||||
|
||||
metrics.packages.counter.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.packages start",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Package.Packages.hotel(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
searchParams
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
metrics.packages.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.packages error",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validatedPackagesData = packagesSchema.safeParse(apiJson)
|
||||
if (!validatedPackagesData.success) {
|
||||
metrics.packages.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedPackagesData.error),
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.hotels.packages validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validatedPackagesData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
metrics.packages.success.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.packages success",
|
||||
JSON.stringify({ query: { hotelId, params: params } })
|
||||
)
|
||||
|
||||
return validatedPackagesData.data
|
||||
},
|
||||
"3h"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getRoomsAvailability(
|
||||
input: RoomsAvailabilityInputSchema,
|
||||
token: string,
|
||||
serviceToken: string,
|
||||
userPoints: number | undefined
|
||||
) {
|
||||
const {
|
||||
booking: { bookingCode, fromDate, hotelId, rooms, searchType, toDate },
|
||||
lang,
|
||||
} = input
|
||||
|
||||
const redemption = searchType === REDEMPTION
|
||||
|
||||
const apiLang = toApiLang(lang)
|
||||
|
||||
const kids = rooms
|
||||
.map((r) => r.childrenInRoom)
|
||||
.filter(Boolean)
|
||||
.map((kid) => JSON.stringify(kid))
|
||||
const metricsData = {
|
||||
adultsCount: rooms.map((r) => r.adults),
|
||||
bookingCode,
|
||||
childArray: kids.length ? kids : undefined,
|
||||
hotelId,
|
||||
roomStayEndDate: toDate,
|
||||
roomStayStartDate: fromDate,
|
||||
}
|
||||
|
||||
metrics.roomsAvailability.counter.add(1, metricsData)
|
||||
|
||||
console.info(
|
||||
"api.hotels.roomsAvailability start",
|
||||
JSON.stringify({ query: { hotelId, params: metricsData } })
|
||||
)
|
||||
|
||||
const baseCacheKey = {
|
||||
bookingCode,
|
||||
fromDate,
|
||||
hotelId,
|
||||
lang,
|
||||
searchType,
|
||||
toDate,
|
||||
}
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
const availabilityResponses = await Promise.allSettled(
|
||||
rooms.map(async (room: RoomsAvailabilityInputRoom) => {
|
||||
const cacheKey = {
|
||||
...baseCacheKey,
|
||||
room,
|
||||
}
|
||||
return await cacheClient.cacheOrGet(
|
||||
stringify(cacheKey),
|
||||
async function () {
|
||||
{
|
||||
const params = {
|
||||
adults: room.adults,
|
||||
language: apiLang,
|
||||
roomStayStartDate: fromDate,
|
||||
roomStayEndDate: toDate,
|
||||
...(room.childrenInRoom?.length && {
|
||||
children: generateChildrenString(room.childrenInRoom),
|
||||
}),
|
||||
...(room.bookingCode && { bookingCode: room.bookingCode }),
|
||||
...(redemption && { isRedemption: "true" }),
|
||||
}
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Availability.hotel(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: 60,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.roomsAvailability.fail.add(1, metricsData)
|
||||
console.error("Failed API call", { params, text })
|
||||
return { error: "http_error", details: text }
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateAvailabilityData =
|
||||
roomsAvailabilitySchema.safeParse(apiJson)
|
||||
if (!validateAvailabilityData.success) {
|
||||
console.error("Validation error", {
|
||||
params,
|
||||
error: validateAvailabilityData.error,
|
||||
})
|
||||
metrics.roomsAvailability.fail.add(1, metricsData)
|
||||
return {
|
||||
error: "validation_error",
|
||||
details: validateAvailabilityData.error,
|
||||
}
|
||||
}
|
||||
|
||||
if (redemption) {
|
||||
for (const roomConfig of validateAvailabilityData.data
|
||||
.roomConfigurations) {
|
||||
for (const product of roomConfig.redemptions) {
|
||||
if (userPoints) {
|
||||
product.redemption.hasEnoughPoints =
|
||||
userPoints >= product.redemption.localPrice.pointsPerStay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const roomFeatures = await getPackages(
|
||||
{
|
||||
adults: room.adults,
|
||||
children: room.childrenInRoom?.length,
|
||||
endDate: input.booking.toDate,
|
||||
hotelId: input.booking.hotelId,
|
||||
lang,
|
||||
packageCodes: [
|
||||
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||
RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||
RoomPackageCodeEnum.PET_ROOM,
|
||||
],
|
||||
startDate: input.booking.fromDate,
|
||||
},
|
||||
serviceToken
|
||||
)
|
||||
|
||||
if (roomFeatures) {
|
||||
validateAvailabilityData.data.packages = roomFeatures
|
||||
}
|
||||
|
||||
// Fetch packages
|
||||
if (room.packages?.length) {
|
||||
const roomFeaturesInventory = await getRoomFeaturesInventory(
|
||||
{
|
||||
adults: room.adults,
|
||||
childrenInRoom: room.childrenInRoom,
|
||||
endDate: input.booking.toDate,
|
||||
hotelId: input.booking.hotelId,
|
||||
lang,
|
||||
roomFeatureCodes: room.packages,
|
||||
startDate: input.booking.fromDate,
|
||||
},
|
||||
serviceToken
|
||||
)
|
||||
|
||||
if (roomFeaturesInventory) {
|
||||
const features = roomFeaturesInventory.reduce<
|
||||
Record<string, number>
|
||||
>((fts, feat) => {
|
||||
fts[feat.roomTypeCode] = feat.features?.[0]?.inventory ?? 0
|
||||
return fts
|
||||
}, {})
|
||||
|
||||
const updatedRoomConfigurations =
|
||||
validateAvailabilityData.data.roomConfigurations
|
||||
// This filter is needed since we can get availability
|
||||
// back from roomFeatures yet the availability call
|
||||
// says there are no rooms left...
|
||||
.filter((rc) => rc.roomsLeft)
|
||||
.filter((rc) => features?.[rc.roomTypeCode])
|
||||
.map((rc) => ({
|
||||
...rc,
|
||||
roomsLeft: features[rc.roomTypeCode],
|
||||
status: AvailabilityEnum.Available,
|
||||
}))
|
||||
|
||||
validateAvailabilityData.data.roomConfigurations =
|
||||
updatedRoomConfigurations
|
||||
}
|
||||
}
|
||||
|
||||
return validateAvailabilityData.data
|
||||
}
|
||||
},
|
||||
"1m"
|
||||
)
|
||||
})
|
||||
)
|
||||
metrics.roomsAvailability.success.add(1, metricsData)
|
||||
|
||||
const data = availabilityResponses.map((availability) => {
|
||||
if (availability.status === "fulfilled") {
|
||||
return availability.value
|
||||
}
|
||||
return {
|
||||
details: availability.reason,
|
||||
error: "request_failure",
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export function getSelectedRoomAvailability(
|
||||
rateCode: string,
|
||||
rateDefinitions: RateDefinition[],
|
||||
roomConfigurations: RoomConfiguration[],
|
||||
roomTypeCode: string,
|
||||
userPoints: number | undefined
|
||||
) {
|
||||
const rateDefinition = rateDefinitions.find((rd) => rd.rateCode === rateCode)
|
||||
if (!rateDefinition) {
|
||||
return null
|
||||
}
|
||||
|
||||
const selectedRoom = roomConfigurations.find(
|
||||
(room) =>
|
||||
room.roomTypeCode === input.roomTypeCode &&
|
||||
room.roomTypeCode === roomTypeCode &&
|
||||
room.products.find((product) => findProduct(product, rateDefinition))
|
||||
)
|
||||
|
||||
@@ -753,3 +1457,90 @@ export async function getSelectedRoomAvailability(
|
||||
selectedRoom,
|
||||
}
|
||||
}
|
||||
|
||||
// Used to ensure `Available` rooms
|
||||
// are shown before all `NotAvailable`
|
||||
const statusLookup = {
|
||||
[AvailabilityEnum.Available]: 1,
|
||||
[AvailabilityEnum.NotAvailable]: 2,
|
||||
}
|
||||
|
||||
export function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
|
||||
// @ts-expect-error - array indexing
|
||||
return statusLookup[a.status] - statusLookup[b.status]
|
||||
}
|
||||
|
||||
export function getBedTypes(
|
||||
rooms: RoomConfiguration[],
|
||||
roomType: string,
|
||||
roomCategories?: RoomCategory[]
|
||||
) {
|
||||
if (!roomCategories) {
|
||||
return []
|
||||
}
|
||||
|
||||
return rooms
|
||||
.filter((room) => room.roomType === roomType)
|
||||
.map((availRoom) => {
|
||||
const matchingRoom = roomCategories
|
||||
?.find((room) =>
|
||||
room.roomTypes
|
||||
.map((roomType) => roomType.code)
|
||||
.includes(availRoom.roomTypeCode)
|
||||
)
|
||||
?.roomTypes.find((roomType) => roomType.code === availRoom.roomTypeCode)
|
||||
|
||||
if (matchingRoom) {
|
||||
return {
|
||||
description: matchingRoom.description,
|
||||
size: matchingRoom.mainBed.widthRange,
|
||||
value: matchingRoom.code,
|
||||
type: matchingRoom.mainBed.type,
|
||||
extraBed: matchingRoom.fixedExtraBed
|
||||
? {
|
||||
type: matchingRoom.fixedExtraBed.type,
|
||||
description: matchingRoom.fixedExtraBed.description,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter((bed): bed is BedTypeSelection => Boolean(bed))
|
||||
}
|
||||
|
||||
export function mergeRoomTypes(roomConfigurations: RoomConfiguration[]) {
|
||||
// Initial sort to guarantee if one bed is NotAvailable and whereas
|
||||
// the other is Available to make sure data is added to the correct
|
||||
// roomConfig
|
||||
roomConfigurations.sort(sortRoomConfigs)
|
||||
|
||||
const roomConfigs = new Map<string, RoomConfiguration>()
|
||||
for (const roomConfig of roomConfigurations) {
|
||||
if (roomConfigs.has(roomConfig.roomType)) {
|
||||
const currentRoomConf = roomConfigs.get(roomConfig.roomType)
|
||||
if (currentRoomConf) {
|
||||
currentRoomConf.features = roomConfig.features.reduce(
|
||||
(feats, feature) => {
|
||||
const currentFeatureIndex = feats.findIndex(
|
||||
(f) => f.code === feature.code
|
||||
)
|
||||
if (currentFeatureIndex !== -1) {
|
||||
feats[currentFeatureIndex].inventory =
|
||||
feats[currentFeatureIndex].inventory + feature.inventory
|
||||
} else {
|
||||
feats.push(feature)
|
||||
}
|
||||
return feats
|
||||
},
|
||||
currentRoomConf.features
|
||||
)
|
||||
currentRoomConf.roomsLeft =
|
||||
currentRoomConf.roomsLeft + roomConfig.roomsLeft
|
||||
roomConfigs.set(currentRoomConf.roomType, currentRoomConf)
|
||||
}
|
||||
} else {
|
||||
roomConfigs.set(roomConfig.roomType, roomConfig)
|
||||
}
|
||||
}
|
||||
return Array.from(roomConfigs.values())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user