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:
Hrishikesh Vaipurkar
2025-03-24 08:54:02 +00:00
parent b972679c6e
commit c5e294c7ea
57 changed files with 1113 additions and 657 deletions

View File

@@ -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)