feat(SW-1717): rewrite select-rate to show all variants of rate-cards

This commit is contained in:
Simon Emanuelsson
2025-03-25 11:25:44 +01:00
committed by Michael Zetterberg
parent adde77eaa9
commit ebaea78fb3
118 changed files with 4601 additions and 4374 deletions

View File

@@ -45,7 +45,7 @@ export const roomsCombinedAvailabilityInputSchema = z.object({
rateCode: z.string().optional(),
roomStayEndDate: z.string(),
roomStayStartDate: z.string(),
redemption: z.boolean().optional(),
redemption: z.boolean().optional().default(false),
})
export const selectedRoomAvailabilityInputSchema = z.object({

View File

@@ -2,6 +2,8 @@ import { z } from "zod"
import { toLang } from "@/server/utils"
import { nullableStringValidator } from "@/utils/zod/stringValidator"
import { occupancySchema } from "./schemas/availability/occupancy"
import { productTypeSchema } from "./schemas/availability/productType"
import { citySchema } from "./schemas/city"
@@ -23,6 +25,7 @@ import { roomConfigurationSchema } from "./schemas/roomAvailability/configuratio
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { RateTypeEnum } from "@/types/enums/rateType"
import type {
AdditionalData,
City,
@@ -101,20 +104,6 @@ export const hotelsAvailabilitySchema = z.object({
),
})
function everyRateHasBreakfastIncluded(
product: Product,
rateDefinitions: RateDefinition[],
userType: "member" | "public"
) {
const rateDefinition = rateDefinitions.find(
(rd) => rd.rateCode === product[userType]?.rateCode
)
if (!rateDefinition) {
return false
}
return rateDefinition.breakfastIncluded
}
function getRate(rate: RateDefinition) {
switch (rate.cancellationRule) {
case "CancellableBefore6PM":
@@ -124,7 +113,9 @@ function getRate(rate: RateDefinition) {
case "NotCancellable":
return "save"
default:
console.info(`Should never happen!`)
console.info(
`Unknown cancellationRule [${rate.cancellationRule}]. This should never happen!`
)
return null
}
}
@@ -151,10 +142,11 @@ function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
return statusLookup[a.status] - statusLookup[b.status]
}
const baseRoomsCombinedAvailabilitySchema = z
export const roomsAvailabilitySchema = z
.object({
data: z.object({
attributes: z.object({
bookingCode: nullableStringValidator,
checkInDate: z.string(),
checkOutDate: z.string(),
hotelId: z.number(),
@@ -204,159 +196,213 @@ const baseRoomsCombinedAvailabilitySchema = z
type: z.string().optional(),
}),
})
.transform(({ data: { attributes } }) => {
const rateDefinitions = attributes.rateDefinitions
const cancellationRuleLookup = rateDefinitions.reduce((acc, val) => {
// @ts-expect-error - index of cancellationRule TS
acc[val.rateCode] = cancellationRules[val.cancellationRule]
return acc
}, {})
function transformRoomConfigs({
data: { attributes },
}: typeof baseRoomsCombinedAvailabilitySchema._type) {
const rateDefinitions = attributes.rateDefinitions
const cancellationRuleLookup = rateDefinitions.reduce((acc, val) => {
// @ts-expect-error - index of cancellationRule TS
acc[val.rateCode] = cancellationRules[val.cancellationRule]
return acc
}, {})
function getProductRateCode(product: Product) {
if ("corporateCheque" in product) {
return product.corporateCheque.rateCode
}
if ("redemption" in product && product.redemption) {
return product.redemption.rateCode
}
if ("voucher" in product) {
return product.voucher.rateCode
}
if ("public" in product && product.public) {
return product.public.rateCode
}
if ("member" in product && product.member) {
return product.member.rateCode
}
return ""
}
const roomConfigurations = attributes.roomConfigurations
.map((room) => {
if (room.products.length) {
room.breakfastIncludedInAllRatesMember = room.products.every(
(product) =>
everyRateHasBreakfastIncluded(product, rateDefinitions, "member")
)
room.breakfastIncludedInAllRatesPublic = room.products.every(
(product) =>
everyRateHasBreakfastIncluded(product, rateDefinitions, "public")
)
function sortProductsBasedOnCancellationRule(a: Product, b: Product) {
// @ts-expect-error - index
const lookUpA = cancellationRuleLookup[getProductRateCode(a)]
// @ts-expect-error - index
const lookUpB = cancellationRuleLookup[getProductRateCode(b)]
return lookUpA - lookUpB
}
room.products = room.products.map((product) => {
const publicRate = product.public
if (publicRate?.rateCode) {
const publicRateDefinition = rateDefinitions.find(
(rateDefinition) =>
rateDefinition.rateCode === publicRate.rateCode
)
if (publicRateDefinition) {
const rate = getRate(publicRateDefinition)
if (rate) {
product.rate = rate
if (rate === "flex") {
product.isFlex = true
}
}
}
}
function findRateDefintion(rateCode: string) {
return rateDefinitions.find(
(rateDefinition) => rateDefinition.rateCode === rateCode
)
}
const memberRate = product.member
if (memberRate?.rateCode) {
const memberRateDefinition = rateDefinitions.find(
(rate) => rate.rateCode === memberRate.rateCode
)
if (memberRateDefinition) {
const rate = getRate(memberRateDefinition)
if (rate) {
product.rate = rate
if (rate === "flex") {
product.isFlex = true
}
}
}
}
const voucherRate = product.voucher
if (voucherRate?.rateCode) {
const voucherRateDefinition = rateDefinitions.find(
(rate) => rate.rateCode === voucherRate.rateCode
)
if (voucherRateDefinition) {
const rate = getRate(voucherRateDefinition)
if (rate) {
product.rate = rate
if (rate === "flex") {
product.isFlex = true
}
}
}
}
const chequeRate = product.bonusCheque
if (chequeRate?.rateCode) {
const chequeRateDefinition = rateDefinitions.find(
(rate) => rate.rateCode === chequeRate.rateCode
)
if (chequeRateDefinition) {
const rate = getRate(chequeRateDefinition)
if (rate) {
product.rate = rate
if (rate === "flex") {
product.isFlex = true
}
}
}
}
return product
})
// CancellationRule is the same for public and member per product
// Sorting to guarantee order based on rate
room.products = room.products.sort(
(a, b) =>
// @ts-expect-error - index
cancellationRuleLookup[a.public?.rateCode || a.member?.rateCode] -
// @ts-expect-error - index
cancellationRuleLookup[b.public?.rateCode || b.member?.rateCode]
)
function getRateDetails(product: Product) {
let rateCode = ""
if ("corporateCheque" in product) {
rateCode = product.corporateCheque.rateCode
} else if ("redemption" in product && product.redemption) {
rateCode = product.redemption.rateCode
} else if ("voucher" in product && product.voucher) {
rateCode = product.voucher.rateCode
} else if ("public" in product && product.public) {
rateCode = product.public.rateCode
} else if ("member" in product && product.member) {
rateCode = product.member.rateCode
}
return room
})
.sort(sortRoomConfigs)
if (!rateCode) {
return null
}
return {
...attributes,
roomConfigurations,
}
}
const rateDefinition = findRateDefintion(rateCode)
export const roomsAvailabilitySchema = z
.object({
data: z.object({
attributes: z.object({
checkInDate: z.string(),
checkOutDate: z.string(),
hotelId: z.number(),
mustBeGuaranteed: z.boolean().optional(),
occupancy: occupancySchema.optional(),
rateDefinitions: z.array(rateDefinitionSchema),
roomConfigurations: z.array(roomConfigurationSchema),
}),
relationships: relationshipsSchema.optional(),
type: z.string().optional(),
}),
})
.transform(transformRoomConfigs)
if (!rateDefinition) {
return null
}
export const roomsCombinedAvailabilitySchema =
baseRoomsCombinedAvailabilitySchema.transform(transformRoomConfigs)
const rate = getRate(rateDefinition)
if (!rate) {
return null
}
export const redemptionRoomsCombinedAvailabilitySchema =
baseRoomsCombinedAvailabilitySchema.transform((data) => {
// In Redemption, rates are always Flex terms
data.data.attributes.roomConfigurations =
data.data.attributes.roomConfigurations
.map((room) => {
room.products = room.products.map((product) => {
product.rate = "flex"
product.isFlex = true
return product
})
return room
})
.sort(
// @ts-expect-error - array indexing
(a, b) => statusLookup[a.status] - statusLookup[b.status]
)
product.breakfastIncluded = rateDefinition.breakfastIncluded
product.rate = rate
product.rateDefinition = rateDefinition
return transformRoomConfigs(data)
return product
}
const roomConfigurations = attributes.roomConfigurations
.map((room) => {
if (room.products.length) {
const breakfastIncluded = []
const breakfastIncludedMember = []
for (const product of room.products) {
if ("corporateCheque" in product) {
const rateDetails = getRateDetails(product)
if (rateDetails) {
breakfastIncluded.push(rateDetails.breakfastIncluded)
room.code.push({
...rateDetails,
corporateCheque: product.corporateCheque,
})
}
continue
}
if ("voucher" in product) {
const rateDetails = getRateDetails(product)
if (rateDetails) {
breakfastIncluded.push(rateDetails.breakfastIncluded)
room.code.push({
...rateDetails,
voucher: product.voucher,
})
}
continue
}
// Redemption is an array
if (Array.isArray(product)) {
if (product.length) {
for (const redemption of product) {
const rateDetails = getRateDetails(redemption)
if (rateDetails) {
breakfastIncluded.push(rateDetails.breakfastIncluded)
room.redemptions.push({
...redemption,
...rateDetails,
})
}
}
}
continue
}
if (
("member" in product && product.member) ||
("public" in product && product.public)
) {
const memberRate = product.member
const publicRate = product.public
const rateCode = publicRate?.rateCode ?? memberRate?.rateCode
const rateDetails = getRateDetails(product)
const rateDetailsMember = getRateDetails({
...product,
public: null,
})
if (rateDetailsMember) {
breakfastIncludedMember.push(
rateDetailsMember.breakfastIncluded
)
}
if (rateDetails && rateCode) {
const rateDefinition = findRateDefintion(rateCode)
if (rateDefinition) {
switch (rateDefinition.rateType) {
case RateTypeEnum.PublicPromotion:
room.campaign.push({
...rateDetails,
member: memberRate,
public: publicRate,
})
break
case RateTypeEnum.Regular:
room.regular.push({
...rateDetails,
member: memberRate,
public: publicRate,
})
break
default:
room.code.push({
...rateDetails,
member: memberRate,
public: publicRate,
})
}
}
continue
}
}
}
room.breakfastIncludedInAllRates =
!!breakfastIncluded.length && breakfastIncluded.every(Boolean)
room.breakfastIncludedInAllRatesMember =
!!breakfastIncludedMember.length &&
breakfastIncludedMember.every(Boolean)
// CancellationRule is the same for public and member per product
// Sorting to guarantee order based on rate
room.campaign.sort(sortProductsBasedOnCancellationRule)
room.code.sort(sortProductsBasedOnCancellationRule)
room.redemptions.sort(sortProductsBasedOnCancellationRule)
room.regular.sort(sortProductsBasedOnCancellationRule)
const hasCampaignProducts = room.campaign.length
const hasCodeProducts = room.code.length
const hasRedemptionProducts = room.redemptions.length
const hasRegularProducts = room.regular.length
if (
!hasCampaignProducts &&
!hasCodeProducts &&
!hasRedemptionProducts &&
!hasRegularProducts
) {
room.status = AvailabilityEnum.NotAvailable
}
}
return room
})
.sort(sortRoomConfigs)
return {
...attributes,
roomConfigurations,
}
})
export const ratesSchema = z.array(rateSchema)
@@ -507,7 +553,7 @@ export const ancillaryPackagesSchema = z
description: item.descriptions.html,
imageUrl: item.images[0]?.imageSizes.small,
price: {
totalPrice: item.variants.ancillary.price.totalPrice,
total: item.variants.ancillary.price.totalPrice,
currency: item.variants.ancillary.price.currency,
},
points: item.variants.ancillaryLoyalty?.points,

View File

@@ -3,11 +3,10 @@ import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import * as api from "@/lib/api"
import { dt } from "@/lib/dt"
import { badRequestError } from "@/server/errors/trpc"
import { badRequestError, unauthorizedError } from "@/server/errors/trpc"
import {
contentStackBaseWithServiceProcedure,
protectedProcedure,
protectedServiceProcedure,
publicProcedure,
router,
safeProtectedServiceProcedure,
@@ -52,9 +51,7 @@ import {
hotelSchema,
packagesSchema,
ratesSchema,
redemptionRoomsCombinedAvailabilitySchema,
roomsAvailabilitySchema,
roomsCombinedAvailabilitySchema,
} from "./output"
import tempRatesData from "./tempRatesData.json"
import {
@@ -65,6 +62,7 @@ import {
getHotelIdsByCountry,
getHotelsByHotelIds,
getLocations,
getSelectedRoomAvailability,
} from "./utils"
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
@@ -75,8 +73,6 @@ import type { HotelDataWithUrl } from "@/types/hotel"
import type {
HotelsAvailabilityInputSchema,
HotelsByHotelIdsAvailabilityInputSchema,
RoomsCombinedAvailabilityInputSchema,
SelectedRoomAvailabilitySchema,
} from "@/types/trpc/routers/hotel/availability"
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
@@ -472,353 +468,6 @@ export const getHotelsAvailabilityByHotelIds = async (
)
}
async function getRoomsCombinedAvailability(
input: RoomsCombinedAvailabilityInputSchema,
token: string // Either service token or user access token in case of redemption search
) {
const { lang } = input
const apiLang = toApiLang(lang)
const {
adultsCount,
bookingCode,
childArray,
hotelId,
rateCode,
roomStayEndDate,
roomStayStartDate,
redemption,
} = input
const metricsData = {
hotelId,
roomStayStartDate,
roomStayEndDate,
adultsCount,
childArray: childArray ? JSON.stringify(childArray) : undefined,
bookingCode,
}
metrics.roomsCombinedAvailability.counter.add(1, metricsData)
console.info(
"api.hotels.roomsCombinedAvailability start",
JSON.stringify({ query: { hotelId, params: metricsData } })
)
const availabilityResponses = await Promise.allSettled(
adultsCount.map(async (adultCount: number, idx: number) => {
const kids = childArray?.[idx]
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults: adultCount,
...(kids?.length && {
children: generateChildrenString(kids),
}),
...(bookingCode && { bookingCode }),
...(redemption && { isRedemption: "true" }),
language: apiLang,
}
const apiResponse = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.roomsCombinedAvailability.fail.add(1, metricsData)
console.error("Failed API call", { params, text })
return { error: "http_error", details: text }
}
const apiJson = await apiResponse.json()
const validateAvailabilityData = redemption
? redemptionRoomsCombinedAvailabilitySchema.safeParse(apiJson)
: roomsCombinedAvailabilitySchema.safeParse(apiJson)
if (!validateAvailabilityData.success) {
console.error("Validation error", {
params,
error: validateAvailabilityData.error,
})
metrics.roomsCombinedAvailability.fail.add(1, metricsData)
return {
error: "validation_error",
details: validateAvailabilityData.error,
}
}
if (rateCode) {
validateAvailabilityData.data.mustBeGuaranteed =
validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === rateCode
)?.mustBeGuaranteed
}
return validateAvailabilityData.data
})
)
metrics.roomsCombinedAvailability.success.add(1, metricsData)
return availabilityResponses.map((availability) => {
if (availability.status === "fulfilled") {
return availability.value
}
return {
details: availability.reason,
error: "request_failure",
}
})
}
export const getRoomAvailability = async (
input: SelectedRoomAvailabilitySchema,
lang: Lang,
token: string, // Either service token or user access token in case of redemption search
serviceToken?: string // In Redemption we need serviceToken for hotel api call
) => {
const {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
rateCode,
counterRateCode,
roomTypeCode,
redemption,
inputLang,
} = input
const language = inputLang ?? lang
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
...(redemption && { isRedemption: "true" }),
language: toApiLang(language),
}
metrics.selectedRoomAvailability.counter.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.selectedRoomAvailability start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponseAvailability = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
params
)
if (!apiResponseAvailability.ok) {
const text = await apiResponseAvailability.text()
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
}),
})
console.error(
"api.hotels.selectedRoomAvailability error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
},
})
)
throw new Error("Failed to fetch selected room availability")
}
const apiJsonAvailability = await apiResponseAvailability.json()
const validateAvailabilityData =
roomsAvailabilitySchema.safeParse(apiJsonAvailability)
if (!validateAvailabilityData.success) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.selectedRoomAvailability validation error",
JSON.stringify({
query: { hotelId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
const hotelData = await getHotel(
{
hotelId,
isCardOnlyPayment: false,
language,
},
serviceToken ?? token
)
const rooms = validateAvailabilityData.data.roomConfigurations
const selectedRoom = rooms.find((room) => room.roomTypeCode === roomTypeCode)
if (!selectedRoom) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
roomTypeCode,
error_type: "not_found",
error: `Couldn't find selected room with input: ${roomTypeCode}`,
})
console.error("No matching room found")
return null
}
const availableRoomsInCategory = rooms.filter(
(room) => room.roomType === selectedRoom?.roomType
)
const rateTypes = selectedRoom.products.find(
(rate) =>
rate.public?.rateCode === rateCode ||
rate.member?.rateCode === rateCode ||
rate.redemptions?.find((r) => r?.rateCode === rateCode) ||
rate.bonusCheque?.rateCode === rateCode ||
rate.voucher?.rateCode === rateCode
)
if (!rateTypes) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "not_found",
error: `Couldn't find rateTypes for selected room: ${JSON.stringify(selectedRoom)}`,
})
console.error("No matching rate found")
return null
}
const rates = rateTypes
const rateDefinition = validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === rateCode
)
const memberRateDefinition =
validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === counterRateCode
)
const bedTypes = availableRoomsInCategory
.map((availRoom) => {
const matchingRoom = hotelData?.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))
metrics.selectedRoomAvailability.success.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.selectedRoomAvailability success",
JSON.stringify({
query: { hotelId, params: params },
})
)
return {
bedTypes,
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
cancellationRule: rateDefinition?.cancellationRule,
cancellationText: rateDefinition?.cancellationText ?? "",
chequeRate: rates?.bonusCheque,
isFlexRate:
rateDefinition?.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM,
memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed,
memberRate: rates?.member,
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
publicRate: rates?.public,
redemptionRate: rates?.redemptions?.find((r) => r?.rateCode === rateCode),
rate: selectedRoom.products[0].rate,
rateDefinitionTitle: rateDefinition?.title ?? "",
rateDetails: rateDefinition?.generalTerms,
// Send rate Title when it is a booking code rate
rateTitle:
rateDefinition?.rateType !== RateTypeEnum.Regular
? rateDefinition?.title
: undefined,
rateType: rateDefinition?.rateType ?? "",
selectedRoom,
voucherRate: rates?.voucher,
}
}
export const hotelQueryRouter = router({
availability: router({
hotelsByCity: serviceProcedure
@@ -847,33 +496,360 @@ export const hotelQueryRouter = router({
return getHotelsAvailabilityByHotelIds(input, apiLang, ctx.serviceToken)
}),
roomsCombinedAvailability: serviceProcedure
roomsCombinedAvailability: safeProtectedServiceProcedure
.input(roomsCombinedAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
return getRoomsCombinedAvailability(input, ctx.serviceToken)
}),
roomsCombinedAvailabilityWithRedemption: protectedProcedure
.input(roomsCombinedAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
return getRoomsCombinedAvailability(
.use(async ({ ctx, input, next }) => {
if (input.redemption) {
if (ctx.session?.token.access_token) {
return next({
ctx: {
token: ctx.session.token.access_token,
},
input,
})
}
throw unauthorizedError()
}
return next({
ctx: {
token: ctx.serviceToken,
},
input,
ctx.session.token.access_token
})
})
.query(
async ({
ctx,
input: {
adultsCount,
bookingCode,
childArray,
hotelId,
lang,
rateCode,
redemption,
roomStayEndDate,
roomStayStartDate,
},
}) => {
const apiLang = toApiLang(lang)
const metricsData = {
hotelId,
roomStayStartDate,
roomStayEndDate,
adultsCount,
childArray: childArray ? JSON.stringify(childArray) : undefined,
bookingCode,
}
metrics.roomsCombinedAvailability.counter.add(1, metricsData)
console.info(
"api.hotels.roomsCombinedAvailability start",
JSON.stringify({ query: { hotelId, params: metricsData } })
)
const availabilityResponses = await Promise.allSettled(
adultsCount.map(async (adultCount: number, idx: number) => {
const kids = childArray?.[idx]
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults: adultCount,
...(kids?.length && {
children: generateChildrenString(kids),
}),
...(bookingCode && { bookingCode }),
language: apiLang,
...(redemption ? { isRedemption: "true" } : {}),
}
const apiResponse = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${ctx.token}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.roomsCombinedAvailability.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.roomsCombinedAvailability.fail.add(1, metricsData)
return {
error: "validation_error",
details: validateAvailabilityData.error,
}
}
if (rateCode) {
validateAvailabilityData.data.mustBeGuaranteed =
validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === rateCode
)?.mustBeGuaranteed
}
return validateAvailabilityData.data
})
)
metrics.roomsCombinedAvailability.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
}
),
room: safeProtectedServiceProcedure
.input(selectedRoomAvailabilityInputSchema)
.use(async ({ ctx, input, next }) => {
if (input.redemption) {
if (ctx.session?.token.access_token) {
return next({
ctx: {
token: ctx.session.token.access_token,
},
input,
})
}
throw unauthorizedError()
}
return next({
ctx: {
token: ctx.serviceToken,
},
input,
})
})
.query(async ({ input, ctx }) => {
let selectedRoomData = await getSelectedRoomAvailability(
input,
toApiLang(ctx.lang),
ctx.token
)
}),
room: serviceProcedure
.input(selectedRoomAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
return getRoomAvailability(input, ctx.lang, ctx.serviceToken)
}),
roomWithRedemption: protectedServiceProcedure
.input(selectedRoomAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
return getRoomAvailability(
input,
ctx.lang,
ctx.session.token.access_token,
const {
adults,
bookingCode,
children,
counterRateCode,
hotelId,
inputLang,
roomStayEndDate,
roomStayStartDate,
roomTypeCode,
} = input
if (!selectedRoomData) {
// There is no way to differentiate if a rateCode
// selected is a bookingCode rateCode or just a
// regular rateCode, hence we need to make a second
// request without the bookingCode if no availability
// is found
if (bookingCode) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
roomTypeCode,
error_type: "not_found",
error: `Couldn't find selected room with input: ${roomTypeCode}`,
})
console.error(
"No matching room found when making the request with bookingCode, attempting without"
)
metrics.selectedRoomAvailability.counter.add(1, {
adults,
children,
hotelId,
roomStayEndDate,
roomStayStartDate,
})
const { bookingCode: extractedBookingCode, ...regularRatesInput } =
input
selectedRoomData = await getSelectedRoomAvailability(
regularRatesInput,
toApiLang(ctx.lang),
ctx.token
)
if (!selectedRoomData) {
metrics.selectedRoomAvailability.fail.add(1, {
adults,
children,
hotelId,
roomStayEndDate,
roomStayStartDate,
roomTypeCode,
error_type: "not_found",
error: `Couldn't find selected room with input: ${roomTypeCode}`,
})
console.error("No matching room found even without bookingCode")
return null
}
} else {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
roomTypeCode,
error_type: "not_found",
error: `Couldn't find selected room with input: ${roomTypeCode}`,
})
console.error("No matching room found")
return null
}
}
const hotelData = await getHotel(
{
hotelId,
isCardOnlyPayment: false,
language: inputLang ?? ctx.lang,
},
ctx.serviceToken
)
const {
product,
rateDefinition,
rateDefinitions,
rooms,
selectedRoom,
} = selectedRoomData
const availableRoomsInCategory = rooms.filter(
(room) => room.roomType === selectedRoom?.roomType
)
if (!product) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "not_found",
error: `Couldn't find rateTypes for selected room: ${JSON.stringify(selectedRoom)}`,
})
console.error("No matching rate found")
return null
}
let memberRateDefinition = undefined
if ("member" in product) {
memberRateDefinition = rateDefinitions.find(
(rate) => rate.rateCode === counterRateCode
)
}
const bedTypes = availableRoomsInCategory
.map((availRoom) => {
const matchingRoom = hotelData?.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))
metrics.selectedRoomAvailability.success.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.selectedRoomAvailability success",
JSON.stringify({
query: {
hotelId,
params: {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
language: inputLang ?? ctx.lang,
},
},
})
)
return {
bedTypes,
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
cancellationRule: rateDefinition?.cancellationRule,
cancellationText: rateDefinition?.cancellationText ?? "",
isFlexRate:
rateDefinition?.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM,
memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed,
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
product,
rate: product.rate,
rateDefinitionTitle: rateDefinition?.title ?? "",
rateDetails: rateDefinition?.generalTerms,
// Send rate Title when it is a booking code rate
rateTitle:
rateDefinition?.rateType !== RateTypeEnum.Regular
? rateDefinition?.title
: undefined,
rateType: rateDefinition?.rateType ?? "",
selectedRoom,
}
}),
hotelsByCityWithBookingCode: serviceProcedure
.input(hotelsAvailabilityInputSchema)
@@ -896,23 +872,15 @@ export const hotelQueryRouter = router({
return null
}
// Do not search for regular rates if voucher or corporate cheque codes
const isVoucherOrChqRate =
bookingCodeAvailabilityResponse?.availability.some(
(hotel) =>
hotel.productType?.bonusCheque || hotel.productType?.voucher
)
// Get regular availability of hotels which don't have availability with booking code.
const unavailableHotelIds = !isVoucherOrChqRate
? bookingCodeAvailabilityResponse?.availability
.filter((hotel) => {
return hotel.status === "NotAvailable"
})
.flatMap((hotel) => {
return hotel.hotelId
})
: null
const unavailableHotelIds =
bookingCodeAvailabilityResponse?.availability
.filter((hotel) => {
return hotel.status === "NotAvailable"
})
.flatMap((hotel) => {
return hotel.hotelId
})
// All hotels have availability with booking code no need to fetch regular prices.
// return response as is without any filtering as below.

View File

@@ -1,7 +1,7 @@
import { z } from "zod"
import {
productTypeChequeSchema,
productTypeCorporateChequeSchema,
productTypePointsSchema,
productTypePriceSchema,
productTypeVoucherSchema,
@@ -9,7 +9,7 @@ import {
export const productTypeSchema = z
.object({
bonusCheque: productTypeChequeSchema.optional(),
bonusCheque: productTypeCorporateChequeSchema.optional(),
public: productTypePriceSchema.optional(),
member: productTypePriceSchema.optional(),
redemptions: z.array(productTypePointsSchema).optional(),

View File

@@ -1,63 +1,76 @@
import { z } from "zod"
import { nullableNumberValidator } from "@/utils/zod/numberValidator"
import { nullableStringValidator } from "@/utils/zod/stringValidator"
import { CurrencyEnum } from "@/types/enums/currency"
import { RateTypeEnum } from "@/types/enums/rateType"
export const corporateChequeSchema = z
.object({
additionalPricePerStay: nullableNumberValidator,
currency: z.nativeEnum(CurrencyEnum).nullish(),
numberOfBonusCheques: nullableNumberValidator,
})
.transform((data) => ({
additionalPricePerStay: data.additionalPricePerStay,
currency: data.currency,
numberOfCheques: data.numberOfBonusCheques,
}))
export const redemptionSchema = z.object({
additionalPricePerStay: nullableNumberValidator,
currency: z.nativeEnum(CurrencyEnum).nullish(),
pointsPerNight: nullableNumberValidator,
pointsPerStay: nullableNumberValidator,
})
export const priceSchema = z.object({
currency: z.nativeEnum(CurrencyEnum),
pricePerNight: z.coerce.number(),
pricePerStay: z.coerce.number(),
regularPricePerNight: z.coerce.number().optional(),
regularPricePerStay: z.coerce.number().optional(),
})
export const pointsSchema = z
.object({
currency: z.nativeEnum(CurrencyEnum).optional(),
pricePerStay: z.coerce.number().optional(),
pointsPerStay: z.coerce.number(),
additionalPricePerStay: z.coerce.number().optional(),
additionalPriceCurrency: z.nativeEnum(CurrencyEnum).optional(),
})
.transform((data) => ({
...data,
additionalPriceCurrency: data.currency,
currency: CurrencyEnum.POINTS,
pricePerStay: data.pointsPerStay,
price: data.pointsPerStay,
additionalPrice: data.additionalPricePerStay,
}))
export const voucherSchema = z.object({
currency: z.nativeEnum(CurrencyEnum),
pricePerStay: z.number(),
})
export const chequeSchema = z.object({
additionalPricePerStay: z.number().optional(),
currency: z.nativeEnum(CurrencyEnum).optional(),
numberOfBonusCheques: z.coerce.number(),
omnibusPricePerNight: nullableNumberValidator,
pricePerNight: nullableNumberValidator,
pricePerStay: nullableNumberValidator,
regularPricePerNight: nullableNumberValidator,
regularPricePerStay: nullableNumberValidator,
})
const partialPriceSchema = z.object({
rateCode: z.string(),
rateType: z.string().optional(),
rateCode: nullableStringValidator,
rateType: z.nativeEnum(RateTypeEnum).catch((err) => {
const issue = err.error.issues[0]
// This is necessary to handle cases were a
// new `rateType` is added in the API that has
// not yet been handled in web
if (issue.code === "invalid_enum_value") {
return issue.received.toString() as RateTypeEnum
}
return RateTypeEnum.Regular
}),
})
export const productTypePriceSchema = partialPriceSchema.extend({
localPrice: priceSchema,
requestedPrice: priceSchema.optional(),
})
export const productTypeCorporateChequeSchema = z
.object({
localPrice: corporateChequeSchema,
requestedPrice: corporateChequeSchema.nullish(),
})
.merge(partialPriceSchema)
export const productTypePointsSchema = partialPriceSchema.extend({
localPrice: pointsSchema,
requestedPrice: pointsSchema.optional(),
})
export const productTypePriceSchema = z
.object({
localPrice: priceSchema,
requestedPrice: priceSchema.nullish(),
})
.merge(partialPriceSchema)
export const productTypeVoucherSchema = partialPriceSchema.extend({
numberOfVouchers: z.coerce.number(),
})
export const productTypePointsSchema = z
.object({
localPrice: redemptionSchema,
requestedPrice: redemptionSchema.nullish(),
})
.merge(partialPriceSchema)
export const productTypeChequeSchema = partialPriceSchema.extend({
localPrice: chequeSchema,
requestedPrice: chequeSchema.optional(),
})
export const productTypeVoucherSchema = z
.object({
numberOfVouchers: nullableNumberValidator,
})
.merge(partialPriceSchema)

View File

@@ -1,14 +1,24 @@
import { z } from "zod"
import { productSchema } from "./product"
import {
corporateChequeProduct,
priceProduct,
productSchema,
redemptionProduct,
voucherProduct,
} from "./product"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import {
AvailabilityEnum,
} from "@/types/components/hotelReservation/selectHotel/selectHotel"
import {
RoomPackageCodeEnum,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
export const roomConfigurationSchema = z
.object({
breakfastIncludedInAllRatesMember: z.boolean().default(false),
breakfastIncludedInAllRatesPublic: z.boolean().default(false),
breakfastIncludedInAllRates: z.boolean().default(false),
features: z
.array(
z.object({
@@ -25,36 +35,36 @@ export const roomConfigurationSchema = z
roomsLeft: z.number(),
roomType: z.string(),
roomTypeCode: z.string(),
status: z.string(),
})
.transform((data) => {
if (data.products.length) {
if (data.products[0].redemptions) {
// No need of rate check in reward night scenario
return { ...data }
} else {
const isVoucher = data.products.some((product) => product.voucher)
const isCorpChq = data.products.some((product) => product.bonusCheque)
if (isVoucher || isCorpChq) {
return {
...data,
}
}
/**
* Just guaranteeing that if all products all miss
* both public and member rateCode that status is
* set to `NotAvailable`
*/
const allProductsMissBothRateCodes = data.products.every(
(product) => !product.public?.rateCode && !product.member?.rateCode
)
if (allProductsMissBothRateCodes) {
return {
...data,
status: AvailabilityEnum.NotAvailable,
}
}
}
}
return data
status: z
.nativeEnum(AvailabilityEnum)
.nullish()
.default(AvailabilityEnum.NotAvailable),
// Red
campaign: z
.array(priceProduct)
.nullish()
.transform(val => val ? val.filter(Boolean) : []),
// Blue
code: z
.array(
z
.union([
corporateChequeProduct,
priceProduct,
voucherProduct,
])
)
.nullish()
.transform(val => val ? val.filter(Boolean) : []),
// Beige
regular: z
.array(priceProduct)
.nullish()
.transform(val => val ? val.filter(Boolean) : []),
// Burgundy
redemptions: z
.array(redemptionProduct)
.nullish()
.transform(val => val ? val.filter(Boolean) : []),
})

View File

@@ -1,28 +1,161 @@
import { z } from "zod"
import {
productTypeCorporateChequeSchema,
productTypePointsSchema,
productTypeChequeSchema,
productTypePriceSchema,
productTypeVoucherSchema,
} from "../productTypePrice"
import { rateDefinitionSchema } from "./rateDefinition"
export const productSchema = z
const baseProductSchema = z.object({
// Is breakfast included on product
breakfastIncluded: z.boolean().default(false),
// Used to set the rate that we use to chose titles etc.
rate: z.enum(["change", "flex", "save"]).default("save"),
rateDefinition: rateDefinitionSchema.nullish().transform((val) =>
val
? val
: {
breakfastIncluded: false,
cancellationRule: "",
cancellationText: "",
displayPriceRed: false,
isCampaignRate: false,
isMemberRate: false,
isPackageRate: false,
generalTerms: [],
mustBeGuaranteed: false,
rateCode: "",
rateType: "",
title: "",
}
),
})
function mapBaseProduct(baseProduct: typeof baseProductSchema._type) {
return {
breakfastIncluded: baseProduct.breakfastIncluded,
rate: baseProduct.rate,
rateDefinition: baseProduct.rateDefinition,
}
}
const rawCorporateChequeProduct = z
.object({
// Is product flex rate
isFlex: z.boolean().default(false),
productType: z.object({
bonusCheque: productTypeChequeSchema.optional(),
member: productTypePriceSchema.optional(),
public: productTypePriceSchema.optional(),
redemptions: z.array(productTypePointsSchema).optional(),
voucher: productTypeVoucherSchema.optional(),
}),
// Used to set the rate that we use to chose titles etc.
rate: z.enum(["change", "flex", "save"]).default("save"),
productType: z
.object({
bonusCheque: productTypeCorporateChequeSchema,
})
.transform((data) => ({
corporateCheque: data.bonusCheque,
})),
})
.transform((data) => ({
.merge(baseProductSchema)
function transformCorporateCheque(
data: z.output<typeof rawCorporateChequeProduct>
) {
return {
...data.productType,
isFlex: data.isFlex,
rate: data.rate,
}))
...mapBaseProduct(data),
}
}
export const corporateChequeProduct = rawCorporateChequeProduct.transform(
transformCorporateCheque
)
const rawPriceProduct = z
.object({
productType: z.object({
member: productTypePriceSchema.nullish().default(null),
public: productTypePriceSchema.nullish().default(null),
}),
})
.merge(baseProductSchema)
function transformPriceProduct(data: z.output<typeof rawPriceProduct>) {
return {
...data.productType,
...mapBaseProduct(data),
}
}
export const priceProduct = rawPriceProduct.transform(transformPriceProduct)
export const redemptionProduct = z
.object({
redemption: productTypePointsSchema,
})
.merge(baseProductSchema)
const rawRedemptionsProduct = z.object({
type: z.literal("REDEMPTION").optional().default("REDEMPTION"),
productType: z.object({
redemptions: z
.array(productTypePointsSchema.merge(baseProductSchema))
.transform((data) =>
data.map(
({ breakfastIncluded, rate, rateDefinition, ...redemption }) => ({
breakfastIncluded,
rate,
rateDefinition,
redemption,
})
)
),
}),
})
export const redemptionsProduct = rawRedemptionsProduct.transform(
(data) => data.productType.redemptions
)
const rawVoucherProduct = z
.object({
type: z.literal("VOUCHER").optional().default("VOUCHER"),
productType: z.object({
voucher: productTypeVoucherSchema,
}),
})
.merge(baseProductSchema)
function transformVoucherProduct(data: z.output<typeof rawVoucherProduct>) {
return {
...data.productType,
...mapBaseProduct(data),
}
}
export const voucherProduct = rawVoucherProduct.transform(
transformVoucherProduct
)
export const productSchema = z.union([
corporateChequeProduct,
redemptionsProduct,
voucherProduct,
priceProduct,
])
// export const productSchema = z.discriminatedUnion(
// "type",
// [
// rawCorporateChequeProduct,
// rawPriceProduct,
// rawRedemptionsProduct,
// rawVoucherProduct,
// ]
// )
// .transform(data => {
// switch (data.type) {
// case "CORPORATECHEQUE":
// return transformCorporateCheque(data)
// case "PRICEPRODUCT":
// return transformPriceProduct(data)
// case "REDEMPTION":
// return data.productType.redemptions
// case "VOUCHER":
// return transformVoucherProduct(data)
// }
// })

View File

@@ -1,12 +1,17 @@
import { nullableStringValidator } from "@/utils/zod/stringValidator"
import { z } from "zod"
export const rateDefinitionSchema = z.object({
breakfastIncluded: z.boolean(),
cancellationRule: z.string(),
cancellationText: z.string().optional(),
cancellationText: nullableStringValidator,
displayPriceRed: z.boolean().default(false),
generalTerms: z.array(z.string()),
isCampaignRate: z.boolean().default(false),
isMemberRate: z.boolean().default(false),
isPackageRate: z.boolean().default(false),
mustBeGuaranteed: z.boolean(),
rateCode: z.string(),
rateType: z.string().optional(),
rateType: nullableStringValidator,
title: z.string(),
})

View File

@@ -3,6 +3,7 @@ import deepmerge from "deepmerge"
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 { getCacheClient } from "@/services/dataCache"
@@ -16,16 +17,26 @@ import {
countriesSchema,
getHotelIdsSchema,
locationsSchema,
roomsAvailabilitySchema,
} from "./output"
import { getHotel } from "./query"
import type { z } from "zod"
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
import type { HotelDataWithUrl } from "@/types/hotel"
import type {
CitiesGroupedByCountry,
CityLocation,
} from "@/types/trpc/routers/hotel/locations"
import type {
Product,
Products,
RateDefinition,
RedemptionsProduct,
} 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
@@ -532,3 +543,173 @@ export async function getHotelsByHotelIds({
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
}
function findProduct(product: Products, rateDefinition: RateDefinition) {
if ("corporateCheque" in product) {
return product.corporateCheque.rateCode === rateDefinition.rateCode
}
if (("member" in product && product.member) || "public" in product) {
let isMemberRate = false
if (product.member) {
isMemberRate = product.member.rateCode === rateDefinition.rateCode
}
let isPublicRate = false
if (product.public) {
isPublicRate = product.public.rateCode === rateDefinition.rateCode
}
return isMemberRate || isPublicRate
}
if ("voucher" in product) {
return product.voucher.rateCode === rateDefinition.rateCode
}
if (Array.isArray(product)) {
return product.find(
(r) => r.redemption.rateCode === rateDefinition.rateCode
)
}
}
export async function getSelectedRoomAvailability(
input: z.input<typeof selectedRoomAvailabilityInputSchema>,
lang: string,
serviceToken: string
) {
const {
adults,
bookingCode,
children,
hotelId,
inputLang,
roomStayEndDate,
roomStayStartDate,
redemption,
} = input
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
...(redemption && { isRedemption: "true" }),
language: inputLang ?? lang,
}
metrics.selectedRoomAvailability.counter.add(1, input)
console.info(
"api.hotels.selectedRoomAvailability start",
JSON.stringify({ query: { hotelId: input.hotelId, params } })
)
const apiResponseAvailability = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${serviceToken}`,
},
},
params
)
if (!apiResponseAvailability.ok) {
const text = await apiResponseAvailability.text()
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
}),
})
console.error(
"api.hotels.selectedRoomAvailability error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
},
})
)
throw new Error("Failed to fetch selected room availability")
}
const apiJsonAvailability = await apiResponseAvailability.json()
const validateAvailabilityData =
roomsAvailabilitySchema.safeParse(apiJsonAvailability)
if (!validateAvailabilityData.success) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.selectedRoomAvailability validation error",
JSON.stringify({
query: { hotelId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
const { rateDefinitions, roomConfigurations } = validateAvailabilityData.data
const rateDefinition = rateDefinitions.find(
(rd) => rd.rateCode === input.rateCode
)
if (!rateDefinition) {
return null
}
const selectedRoom = roomConfigurations.find(
(room) =>
room.roomTypeCode === input.roomTypeCode &&
room.products.find((product) => findProduct(product, rateDefinition))
)
if (!selectedRoom) {
return null
}
let product: Product | RedemptionsProduct | undefined =
selectedRoom.products.find((product) =>
findProduct(product, rateDefinition)
)
if (!product) {
return null
}
if (Array.isArray(product)) {
const redemptionProduct = product.find(
(r) => r.redemption.rateCode === rateDefinition.rateCode
)
if (!redemptionProduct) {
return null
}
product = redemptionProduct
}
return {
rateDefinition,
rateDefinitions,
rooms: roomConfigurations,
product,
selectedRoom,
}
}