Merged in feat/SW-1356-reward-night-booking-2- (pull request #1559)
feat: SW-1356 Reward night bookingflow * feat: SW-1356 Reward night bookingflow * feat: SW-1356 Removed extra param booking call * feat: SW-1356 Optimized as review comments * feat: SW-1356 Schema validation updates * feat: SW-1356 Fix after rebase * feat: SW-1356 Optimised price.redemptions check * feat: SW-1356 Updated Props naming Approved-by: Arvid Norlin
This commit is contained in:
@@ -17,6 +17,7 @@ const roomsSchema = z
|
||||
)
|
||||
.default([]),
|
||||
rateCode: z.string(),
|
||||
redemptionCode: z.string().optional(),
|
||||
roomTypeCode: z.coerce.string(),
|
||||
guest: z.object({
|
||||
becomeMember: z.boolean(),
|
||||
|
||||
@@ -45,6 +45,7 @@ export const roomsCombinedAvailabilityInputSchema = z.object({
|
||||
rateCode: z.string().optional(),
|
||||
roomStayEndDate: z.string(),
|
||||
roomStayStartDate: z.string(),
|
||||
redemption: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const selectedRoomAvailabilityInputSchema = z.object({
|
||||
@@ -59,6 +60,7 @@ export const selectedRoomAvailabilityInputSchema = z.object({
|
||||
counterRateCode: z.string().optional(),
|
||||
packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(),
|
||||
lang: z.nativeEnum(Lang).optional(),
|
||||
redemption: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type GetSelectedRoomAvailabilityInput = z.input<
|
||||
|
||||
@@ -151,7 +151,7 @@ function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
|
||||
return statusLookup[a.status] - statusLookup[b.status]
|
||||
}
|
||||
|
||||
export const roomsCombinedAvailabilitySchema = z
|
||||
const baseRoomsCombinedAvailabilitySchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
@@ -204,83 +204,86 @@ export const roomsCombinedAvailabilitySchema = 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
|
||||
}, {})
|
||||
|
||||
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 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
|
||||
}, {})
|
||||
|
||||
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
|
||||
}
|
||||
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")
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return product
|
||||
})
|
||||
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]
|
||||
)
|
||||
}
|
||||
// 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]
|
||||
)
|
||||
}
|
||||
|
||||
return room
|
||||
})
|
||||
.sort(sortRoomConfigs)
|
||||
return room
|
||||
})
|
||||
.sort(sortRoomConfigs)
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
roomConfigurations,
|
||||
}
|
||||
})
|
||||
return {
|
||||
...attributes,
|
||||
roomConfigurations,
|
||||
}
|
||||
}
|
||||
|
||||
export const roomsAvailabilitySchema = z
|
||||
.object({
|
||||
@@ -298,85 +301,30 @@ export const roomsAvailabilitySchema = 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
|
||||
}, {})
|
||||
.transform(transformRoomConfigs)
|
||||
|
||||
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")
|
||||
)
|
||||
export const roomsCombinedAvailabilitySchema =
|
||||
baseRoomsCombinedAvailabilitySchema.transform(transformRoomConfigs)
|
||||
|
||||
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) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
product.rate = "flex"
|
||||
product.isFlex = true
|
||||
return product
|
||||
})
|
||||
return room
|
||||
})
|
||||
.sort(
|
||||
// @ts-expect-error - array indexing
|
||||
(a, b) => statusLookup[a.status] - statusLookup[b.status]
|
||||
)
|
||||
|
||||
// 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]
|
||||
)
|
||||
}
|
||||
|
||||
return room
|
||||
})
|
||||
.sort(
|
||||
// @ts-expect-error - array indexing
|
||||
(a, b) => statusLookup[a.status] - statusLookup[b.status]
|
||||
)
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
roomConfigurations,
|
||||
}
|
||||
return transformRoomConfigs(data)
|
||||
})
|
||||
|
||||
export const ratesSchema = z.array(rateSchema)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { badRequestError } from "@/server/errors/trpc"
|
||||
import {
|
||||
contentStackBaseWithServiceProcedure,
|
||||
protectedProcedure,
|
||||
protectedServcieProcedure,
|
||||
publicProcedure,
|
||||
router,
|
||||
safeProtectedServiceProcedure,
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
hotelSchema,
|
||||
packagesSchema,
|
||||
ratesSchema,
|
||||
redemptionRoomsCombinedAvailabilitySchema,
|
||||
roomsAvailabilitySchema,
|
||||
roomsCombinedAvailabilitySchema,
|
||||
} from "./output"
|
||||
@@ -72,9 +74,12 @@ 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"
|
||||
import type { Lang } from "@/constants/routes/hotelReservation"
|
||||
|
||||
export const getHotel = cache(
|
||||
async (input: HotelInput, serviceToken: string) => {
|
||||
@@ -467,6 +472,346 @@ 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,
|
||||
} = input
|
||||
|
||||
const params: Record<string, string | number | undefined> = {
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
...(children && { children }),
|
||||
...(bookingCode && { bookingCode }),
|
||||
...(redemption && { isRedemption: "true" }),
|
||||
language: toApiLang(lang),
|
||||
}
|
||||
|
||||
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: lang,
|
||||
},
|
||||
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)
|
||||
)
|
||||
|
||||
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 ?? "",
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
export const hotelQueryRouter = router({
|
||||
availability: router({
|
||||
hotelsByCity: serviceProcedure
|
||||
@@ -497,344 +842,31 @@ export const hotelQueryRouter = router({
|
||||
|
||||
roomsCombinedAvailability: serviceProcedure
|
||||
.input(roomsCombinedAvailabilityInputSchema)
|
||||
.query(
|
||||
async ({
|
||||
ctx,
|
||||
input: {
|
||||
adultsCount,
|
||||
bookingCode,
|
||||
childArray,
|
||||
hotelId,
|
||||
lang,
|
||||
rateCode,
|
||||
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,
|
||||
}
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Availability.hotel(hotelId.toString()),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
},
|
||||
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 =
|
||||
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)
|
||||
|
||||
const data = availabilityResponses.map((availability) => {
|
||||
if (availability.status === "fulfilled") {
|
||||
return availability.value
|
||||
}
|
||||
return {
|
||||
details: availability.reason,
|
||||
error: "request_failure",
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
),
|
||||
.query(async ({ input, ctx }) => {
|
||||
return getRoomsCombinedAvailability(input, ctx.serviceToken)
|
||||
}),
|
||||
roomsCombinedAvailabilityWithRedemption: protectedProcedure
|
||||
.input(roomsCombinedAvailabilityInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return getRoomsCombinedAvailability(
|
||||
input,
|
||||
ctx.session.token.access_token
|
||||
)
|
||||
}),
|
||||
room: serviceProcedure
|
||||
.input(selectedRoomAvailabilityInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { lang } = input
|
||||
|
||||
const {
|
||||
hotelId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
rateCode,
|
||||
counterRateCode,
|
||||
roomTypeCode,
|
||||
} = input
|
||||
|
||||
const params: Record<string, string | number | undefined> = {
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
...(children && { children }),
|
||||
...(bookingCode && { bookingCode }),
|
||||
language: lang ?? toApiLang(ctx.lang),
|
||||
}
|
||||
|
||||
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 ${ctx.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 hotelData = await getHotel(
|
||||
{
|
||||
hotelId,
|
||||
isCardOnlyPayment: false,
|
||||
language: lang ?? ctx.lang,
|
||||
},
|
||||
return getRoomAvailability(input, ctx.lang, ctx.serviceToken)
|
||||
}),
|
||||
roomWithRedemption: protectedServcieProcedure
|
||||
.input(selectedRoomAvailabilityInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return getRoomAvailability(
|
||||
input,
|
||||
ctx.lang,
|
||||
ctx.session.token.access_token,
|
||||
ctx.serviceToken
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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 ?? "",
|
||||
isFlexRate:
|
||||
rateDefinition?.cancellationRule ===
|
||||
CancellationRuleEnum.CancellableBefore6PM,
|
||||
memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed,
|
||||
memberRate: rates?.member,
|
||||
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
|
||||
publicRate: rates?.public,
|
||||
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,
|
||||
}
|
||||
}),
|
||||
hotelsByCityWithBookingCode: serviceProcedure
|
||||
.input(hotelsAvailabilityInputSchema)
|
||||
|
||||
@@ -9,8 +9,6 @@ export const productTypeSchema = z
|
||||
.object({
|
||||
public: productTypePriceSchema.optional(),
|
||||
member: productTypePriceSchema.optional(),
|
||||
redemption: productTypePointsSchema.optional(),
|
||||
redemptionA: productTypePointsSchema.optional(),
|
||||
redemptionB: productTypePointsSchema.optional(),
|
||||
redemptions: z.array(productTypePointsSchema).optional(),
|
||||
})
|
||||
.optional()
|
||||
|
||||
@@ -10,22 +10,31 @@ export const priceSchema = z.object({
|
||||
regularPricePerStay: z.coerce.number().optional(),
|
||||
})
|
||||
|
||||
export const pointsSchema = z.object({
|
||||
currency: z.nativeEnum(CurrencyEnum).optional(),
|
||||
pricePerNight: z.coerce.number().optional(),
|
||||
pricePerStay: z.coerce.number().optional(),
|
||||
pointsPerNight: z.number(),
|
||||
pointsPerStay: z.number(),
|
||||
})
|
||||
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,
|
||||
}))
|
||||
|
||||
const partialPriceSchema = z.object({
|
||||
rateCode: z.string(),
|
||||
rateType: z.string().optional(),
|
||||
requestedPrice: priceSchema.optional(),
|
||||
})
|
||||
|
||||
export const productTypePriceSchema = partialPriceSchema.extend({
|
||||
localPrice: priceSchema,
|
||||
requestedPrice: priceSchema.optional(),
|
||||
})
|
||||
|
||||
export const productTypePointsSchema = partialPriceSchema.extend({
|
||||
|
||||
@@ -29,21 +29,25 @@ export const roomConfigurationSchema = z
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.products.length) {
|
||||
/**
|
||||
* 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,
|
||||
if (data.products[0].redemptions) {
|
||||
// No need of rate check in reward night scenario
|
||||
return { ...data }
|
||||
} else {
|
||||
/**
|
||||
* 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
|
||||
})
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { productTypePriceSchema } from "../productTypePrice"
|
||||
import {
|
||||
productTypePointsSchema,
|
||||
productTypePriceSchema,
|
||||
} from "../productTypePrice"
|
||||
|
||||
export const productSchema = z
|
||||
.object({
|
||||
@@ -9,6 +12,7 @@ export const productSchema = z
|
||||
productType: z.object({
|
||||
member: productTypePriceSchema.optional(),
|
||||
public: productTypePriceSchema.optional(),
|
||||
redemptions: z.array(productTypePointsSchema).optional(),
|
||||
}),
|
||||
// Used to set the rate that we use to chose titles etc.
|
||||
rate: z.enum(["change", "flex", "save"]).default("save"),
|
||||
|
||||
@@ -206,3 +206,6 @@ export const contentStackBaseWithProtectedProcedure =
|
||||
|
||||
export const safeProtectedServiceProcedure =
|
||||
safeProtectedProcedure.unstable_concat(serviceProcedure)
|
||||
|
||||
export const protectedServcieProcedure =
|
||||
protectedProcedure.unstable_concat(serviceProcedure)
|
||||
|
||||
Reference in New Issue
Block a user