diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx index 16e01506f..42ae5fd3e 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx @@ -86,6 +86,7 @@ export default async function DetailsPage({ hotelId: booking.hotelId, packageCodes: room.packages, startDate: booking.fromDate, + lang, }) : null diff --git a/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton.tsx b/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton.tsx index dea2d20e5..f14ca0e06 100644 --- a/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton.tsx +++ b/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton.tsx @@ -6,7 +6,7 @@ type Props = { count?: number } -export async function RoomsContainerSkeleton({ count = 4 }: Props) { +export function RoomsContainerSkeleton({ count = 4 }: Props) { return (
diff --git a/components/HotelReservation/SelectRate/RoomsContainer/index.tsx b/components/HotelReservation/SelectRate/RoomsContainer/index.tsx index 2e5806d2f..fb14da42d 100644 --- a/components/HotelReservation/SelectRate/RoomsContainer/index.tsx +++ b/components/HotelReservation/SelectRate/RoomsContainer/index.tsx @@ -1,109 +1,59 @@ +"use client" +import { useSession } from "next-auth/react" + import { dt } from "@/lib/dt" -import { - getHotel, - getPackages, - getRoomsAvailability, -} from "@/lib/trpc/memoizedRequests" -import { auth } from "@/auth" -import { generateChildrenString } from "@/components/HotelReservation/utils" +import useLang from "@/hooks/useLang" import RatesProvider from "@/providers/RatesProvider" -import { isValidSession } from "@/utils/session" +import { isValidClientSession } from "@/utils/clientSession" -import { combineRoomAvailabilities } from "../utils" +import { useHotelPackages, useRoomsAvailability } from "../utils" import RateSummary from "./RateSummary" import Rooms from "./Rooms" +import { RoomsContainerSkeleton } from "./RoomsContainerSkeleton" -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer" -import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { Lang } from "@/constants/languages" -export function preload( - hotelId: string, - lang: Lang, - fromDate: string, - toDate: string, - adults: number[], - children?: Child[] -) { - void getHotel({ hotelId, isCardOnlyPayment: false, language: lang }) - void getPackages({ - adults: adults[0], - children: children ? children?.length : undefined, - endDate: toDate, - hotelId, - packageCodes: [ - RoomPackageCodeEnum.ACCESSIBILITY_ROOM, - RoomPackageCodeEnum.PET_ROOM, - RoomPackageCodeEnum.ALLERGY_ROOM, - ], - startDate: fromDate, - }) - - const uniqueAdultsCount = Array.from(new Set(adults)) - uniqueAdultsCount.forEach((adultsInRoom) => { - void getRoomsAvailability({ - adults: adultsInRoom, - children: children ? generateChildrenString(children) : undefined, - hotelId: +hotelId, - roomStayEndDate: toDate, - roomStayStartDate: fromDate, - }) - }) -} - -export async function RoomsContainer({ +export function RoomsContainer({ adultArray, booking, childArray, fromDate, hotelId, - lang, + hotelData, toDate, }: RoomsContainerProps) { - const session = await auth() - const isUserLoggedIn = isValidSession(session) + const { data: session } = useSession() + const isUserLoggedIn = isValidClientSession(session) + const lang = useLang() const fromDateString = dt(fromDate).format("YYYY-MM-DD") const toDateString = dt(toDate).format("YYYY-MM-DD") - const hotelData = await getHotel({ - hotelId: hotelId.toString(), - isCardOnlyPayment: false, - language: lang, - }) - const uniqueAdultsCount = Array.from(new Set(adultArray)) - const roomsAvailabilityResults = await Promise.allSettled( - uniqueAdultsCount.map((adultCount) => - getRoomsAvailability({ - adults: adultCount, - hotelId: hotelId, - roomStayEndDate: toDateString, - roomStayStartDate: fromDateString, - children: - childArray && childArray.length > 0 - ? generateChildrenString(childArray) - : undefined, - }) + + const { isPending: isLoadingAvailability, data: roomsAvailability } = + useRoomsAvailability( + uniqueAdultsCount, + hotelId, + fromDateString, + toDateString, + lang, + childArray ) + + const { data: packages, isPending: isLoadingPackages } = useHotelPackages( + adultArray, + childArray, + fromDateString, + toDateString, + hotelId, + lang ) - const roomsAvailability = combineRoomAvailabilities(roomsAvailabilityResults) - - const packages = await getPackages({ - adults: adultArray[0], - children: childArray ? childArray.length : undefined, - endDate: toDateString, - hotelId: hotelId.toString(), - packageCodes: [ - RoomPackageCodeEnum.ACCESSIBILITY_ROOM, - RoomPackageCodeEnum.PET_ROOM, - RoomPackageCodeEnum.ALLERGY_ROOM, - ], - startDate: fromDateString, - }) + if (isLoadingAvailability || isLoadingPackages) { + return + } if (!hotelData?.hotel) { return null diff --git a/components/HotelReservation/SelectRate/index.tsx b/components/HotelReservation/SelectRate/index.tsx index 7c17ab1ed..a08773c36 100644 --- a/components/HotelReservation/SelectRate/index.tsx +++ b/components/HotelReservation/SelectRate/index.tsx @@ -10,11 +10,7 @@ import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreserva import HotelInfoCard, { HotelInfoCardSkeleton, } from "@/components/HotelReservation/SelectRate/HotelInfoCard" -import { - preload, - RoomsContainer, -} from "@/components/HotelReservation/SelectRate/RoomsContainer" -import { RoomsContainerSkeleton } from "@/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton" +import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer" import TrackingSDK from "@/components/TrackingSDK" import { setLang } from "@/i18n/serverContext" import { convertSearchParamsToObj } from "@/utils/url" @@ -45,15 +41,6 @@ export default async function SelectRatePage({ selectHotelParams.toDate ) - preload( - hotel.id, - params.lang, - fromDate.format("YYYY-MM-DD"), - toDate.format("YYYY-MM-DD"), - adultsInRoom, - childrenInRoom - ) - const hotelData = await getHotel({ hotelId: hotel.id, isCardOnlyPayment: false, @@ -104,18 +91,17 @@ export default async function SelectRatePage({ - }> - - - + + + [] @@ -37,3 +42,56 @@ export function getRates( ), } } + +export function useRoomsAvailability( + uniqueAdultsCount: number[], + hotelId: number, + fromDateString: string, + toDateString: string, + lang: Lang, + childArray?: Child[] +) { + const returnValue = + trpc.hotel.availability.roomsCombinedAvailability.useQuery({ + hotelId, + roomStayStartDate: fromDateString, + roomStayEndDate: toDateString, + uniqueAdultsCount, + childArray, + lang, + }) + + const combinedAvailability = returnValue.data?.length + ? combineRoomAvailabilities( + returnValue.data as PromiseSettledResult[] + ) + : null + + return { + ...returnValue, + data: combinedAvailability, + } +} + +export function useHotelPackages( + adultArray: number[], + childArray: Child[] | undefined, + fromDateString: string, + toDateString: string, + hotelId: number, + lang: Lang +) { + return trpc.hotel.packages.get.useQuery({ + adults: adultArray[0], // Using the first adult count + children: childArray ? childArray.length : undefined, + endDate: toDateString, + hotelId: hotelId.toString(), + packageCodes: [ + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + ], + startDate: fromDateString, + lang: lang, + }) +} diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 2b8aff066..c4fdaa547 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -15,7 +15,6 @@ import type { import type { Lang } from "@/constants/languages" import type { GetHotelsByCSFilterInput, - GetRoomsAvailabilityInput, GetSelectedRoomAvailabilityInput, } from "@/server/routers/hotels/input" import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input" @@ -87,12 +86,6 @@ export const getHotelPage = cache(async function getMemoizedHotelPage() { return serverClient().contentstack.hotelPage.get() }) -export const getRoomsAvailability = cache( - async function getMemoizedRoomAvailability(input: GetRoomsAvailabilityInput) { - return serverClient().hotel.availability.rooms(input) - } -) - export const getSelectedRoomAvailability = cache( function getMemoizedSelectedRoomAvailability( input: GetSelectedRoomAvailabilityInput diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 08bbd5cbf..481d04a18 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -2,6 +2,7 @@ import { z } from "zod" import { Lang } from "@/constants/languages" +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { Country } from "@/types/enums/country" @@ -23,14 +24,22 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({ bookingCode: z.string().optional().default(""), }) -export const roomsAvailabilityInputSchema = z.object({ +export const roomsCombinedAvailabilityInputSchema = z.object({ hotelId: z.number(), roomStayStartDate: z.string(), roomStayEndDate: z.string(), - adults: z.number(), - children: z.string().optional(), + uniqueAdultsCount: z.array(z.number()), + childArray: z + .array( + z.object({ + bed: z.nativeEnum(ChildBedMapEnum), + age: z.number(), + }) + ) + .optional(), bookingCode: z.string().optional(), rateCode: z.string().optional(), + lang: z.nativeEnum(Lang), }) export const selectedRoomAvailabilityInputSchema = z.object({ @@ -49,10 +58,6 @@ export type GetSelectedRoomAvailabilityInput = z.input< typeof selectedRoomAvailabilityInputSchema > -export type GetRoomsAvailabilityInput = z.input< - typeof roomsAvailabilityInputSchema -> - export const ratesInputSchema = z.object({ hotelId: z.string(), }) @@ -109,6 +114,7 @@ export const roomPackagesInputSchema = z.object({ adults: z.number(), children: z.number().optional().default(0), packageCodes: z.array(z.string()).optional().default([]), + lang: z.nativeEnum(Lang), }) export const cityCoordinatesInputSchema = z.object({ city: z.string(), diff --git a/server/routers/hotels/metrics.ts b/server/routers/hotels/metrics.ts index 2f8a10e37..a5919787e 100644 --- a/server/routers/hotels/metrics.ts +++ b/server/routers/hotels/metrics.ts @@ -70,10 +70,14 @@ export const metrics = { fail: meter.createCounter("trpc.hotel.packages.get-fail"), success: meter.createCounter("trpc.hotel.packages.get-success"), }, - roomAvailability: { - counter: meter.createCounter("trpc.hotel.availability.rooms"), - fail: meter.createCounter("trpc.hotel.availability.rooms-fail"), - success: meter.createCounter("trpc.hotel.availability.rooms-success"), + roomsCombinedAvailability: { + counter: meter.createCounter("trpc.hotel.roomsCombinedAvailability.rooms"), + fail: meter.createCounter( + "trpc.hotel.roomsCombinedAvailability.rooms-fail" + ), + success: meter.createCounter( + "trpc.hotel.roomsCombinedAvailability.rooms-success" + ), }, selectedRoomAvailability: { counter: meter.createCounter("trpc.hotel.availability.room"), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 7311abbd0..324fab639 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -35,7 +35,7 @@ import { nearbyHotelIdsInput, ratesInputSchema, roomPackagesInputSchema, - roomsAvailabilityInputSchema, + roomsCombinedAvailabilityInputSchema, selectedRoomAvailabilityInputSchema, } from "./input" import { metrics } from "./metrics" @@ -470,128 +470,98 @@ export const hotelQueryRouter = router({ const apiLang = toApiLang(lang) return getHotelsAvailabilityByHotelIds(input, apiLang, ctx.serviceToken) }), - rooms: serviceProcedure - .input(roomsAvailabilityInputSchema) + + roomsCombinedAvailability: serviceProcedure + .input(roomsCombinedAvailabilityInputSchema) .query(async ({ input, ctx }) => { - const { lang } = ctx + const { lang } = input const apiLang = toApiLang(lang) const { hotelId, roomStayStartDate, roomStayEndDate, - adults, - children, + uniqueAdultsCount, + childArray, bookingCode, rateCode, } = input - const params: Record = { - roomStayStartDate, - roomStayEndDate, - adults, - ...(children && { children }), - ...(bookingCode && { bookingCode }), - language: apiLang, - } - - metrics.roomAvailability.counter.add(1, { + const metricsData = { hotelId, roomStayStartDate, roomStayEndDate, - adults, - children, + uniqueAdultsCount, + childArray: childArray ? JSON.stringify(childArray) : undefined, bookingCode, - }) + } + + metrics.roomsCombinedAvailability.counter.add(1, metricsData) + console.info( - "api.hotels.roomsAvailability start", - JSON.stringify({ query: { hotelId, params } }) - ) - const apiResponse = await api.get( - api.endpoints.v1.Availability.hotel(hotelId.toString()), - { - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - }, - params + "api.hotels.roomsCombinedAvailability start", + JSON.stringify({ query: { hotelId, params: metricsData } }) ) - if (!apiResponse.ok) { - const text = await apiResponse.text() - metrics.roomAvailability.fail.add(1, { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }), - }) - console.error( - "api.hotels.roomsAvailability error", - JSON.stringify({ - query: { hotelId, params }, - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - text, + const availabilityResponses = await Promise.allSettled( + uniqueAdultsCount.map(async (adultCount: number) => { + const params: Record = { + roomStayStartDate, + roomStayEndDate, + adults: adultCount, + ...(childArray && + childArray.length > 0 && { + children: childArray.join(","), + }), + ...(bookingCode && { bookingCode }), + language: apiLang, + } + + const apiResponse = await api.get( + api.endpoints.v1.Availability.hotel(hotelId.toString()), + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, }, - }) - ) - return null - } - const apiJson = await apiResponse.json() + params + ) - const validateAvailabilityData = - roomsAvailabilitySchema.safeParse(apiJson) - if (!validateAvailabilityData.success) { - metrics.roomAvailability.fail.add(1, { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - error_type: "validation_error", - error: JSON.stringify(validateAvailabilityData.error), - }) - console.error( - "api.hotels.roomsAvailability validation error", - JSON.stringify({ - query: { hotelId, params }, - error: validateAvailabilityData.error, - }) - ) - return null - } - metrics.roomAvailability.success.add(1, { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - }) - console.info( - "api.hotels.roomsAvailability success", - JSON.stringify({ - query: { hotelId, params: 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 }) ) - - if (rateCode) { - validateAvailabilityData.data.mustBeGuaranteed = - validateAvailabilityData.data.rateDefinitions.filter( - (rate) => rate.rateCode === rateCode - )[0].mustBeGuaranteed - } - - return validateAvailabilityData.data + metrics.roomsCombinedAvailability.success.add(1, metricsData) + return availabilityResponses }), room: serviceProcedure .input(selectedRoomAvailabilityInputSchema) @@ -885,40 +855,35 @@ export const hotelQueryRouter = router({ }), }), rates: router({ - get: publicProcedure - .input(ratesInputSchema) - .query(async ({ input, ctx }) => { - // TODO: Do a real API call when the endpoint is ready - // const { hotelId } = input + get: publicProcedure.input(ratesInputSchema).query(async ({}) => { + // TODO: Do a real API call when the endpoint is ready + // const { hotelId } = input - // const params = new URLSearchParams() - // const apiLang = toApiLang(language) - // params.set("hotelId", hotelId.toString()) - // params.set("language", apiLang) + // const params = new URLSearchParams() + // const apiLang = toApiLang(language) + // params.set("hotelId", hotelId.toString()) + // params.set("language", apiLang) - console.info("api.hotels.rates start", JSON.stringify({})) - const validatedHotelData = ratesSchema.safeParse(tempRatesData) + console.info("api.hotels.rates start", JSON.stringify({})) + const validatedHotelData = ratesSchema.safeParse(tempRatesData) - if (!tempRatesData) { - console.error( - "api.hotels.rates error", - JSON.stringify({ error: null }) - ) - //Can't return null here since consuming component does not handle null yet - // return null - } - if (!validatedHotelData.success) { - console.error( - "api.hotels.rates validation error", - JSON.stringify({ - error: validatedHotelData.error, - }) - ) - throw badRequestError() - } - console.info("api.hotels.rates success", JSON.stringify({})) - return validatedHotelData.data - }), + if (!tempRatesData) { + console.error("api.hotels.rates error", JSON.stringify({ error: null })) + //Can't return null here since consuming component does not handle null yet + // return null + } + if (!validatedHotelData.success) { + console.error( + "api.hotels.rates validation error", + JSON.stringify({ + error: validatedHotelData.error, + }) + ) + throw badRequestError() + } + console.info("api.hotels.rates success", JSON.stringify({})) + return validatedHotelData.data + }), }), get: serviceProcedure .input(hotelInputSchema) @@ -1544,7 +1509,7 @@ export const hotelQueryRouter = router({ const { hotelId, startDate, endDate, adults, children, packageCodes } = input - const { lang } = ctx + const { lang } = input const apiLang = toApiLang(lang) diff --git a/server/routers/hotels/telemetry.ts b/server/routers/hotels/telemetry.ts index 9351f3f8d..df9474896 100644 --- a/server/routers/hotels/telemetry.ts +++ b/server/routers/hotels/telemetry.ts @@ -35,16 +35,6 @@ export const hotelsByHotelIdAvailabilityFailCounter = meter.createCounter( "trpc.hotel.availability.hotels-by-hotel-id-fail" ) -export const roomsAvailabilityCounter = meter.createCounter( - "trpc.hotel.availability.rooms" -) -export const roomsAvailabilitySuccessCounter = meter.createCounter( - "trpc.hotel.availability.rooms-success" -) -export const roomsAvailabilityFailCounter = meter.createCounter( - "trpc.hotel.availability.rooms-fail" -) - export const selectedRoomAvailabilityCounter = meter.createCounter( "trpc.hotel.availability.room" ) diff --git a/types/components/hotelReservation/selectRate/roomsContainer.ts b/types/components/hotelReservation/selectRate/roomsContainer.ts index 63fb1efdc..e1f5eebf2 100644 --- a/types/components/hotelReservation/selectRate/roomsContainer.ts +++ b/types/components/hotelReservation/selectRate/roomsContainer.ts @@ -1,4 +1,4 @@ -import type { Lang } from "@/constants/languages" +import type { HotelData } from "@/types/hotel" import type { Child, SelectRateSearchParams } from "./selectRate" export interface RoomsContainerProps { @@ -7,6 +7,6 @@ export interface RoomsContainerProps { childArray?: Child[] fromDate: Date hotelId: number - lang: Lang toDate: Date + hotelData: HotelData | null }