diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts index a4ea86489..54cfee553 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts +++ b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts @@ -1,6 +1,12 @@ +import { useSearchParams } from "next/navigation" +import { useEffect, useMemo, useState } from "react" + import { trpc } from "@/lib/trpc/client" +import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url" + import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { Lang } from "@/constants/languages" import type { ChildrenInRoom } from "@/utils/hotelSearchDetails" @@ -14,16 +20,76 @@ export function useRoomsAvailability( bookingCode?: string, redemption?: boolean ) { - return trpc.hotel.availability.roomsCombinedAvailability.useQuery({ - adultsCount, - bookingCode, - childArray, - hotelId, - lang, - redemption, - roomStayEndDate: toDateString, - roomStayStartDate: fromDateString, - }) + const searchParams = useSearchParams() + const searchParamsObj = convertSearchParamsToObj( + searchParamsToRecord(searchParams) + ) + + const hasPackagesParam = searchParamsObj.rooms.some((room) => room.packages) + const [hasRoomFeatures, setHasRoomFeatures] = useState(hasPackagesParam) + + useEffect(() => { + setHasRoomFeatures(hasPackagesParam) + }, [hasPackagesParam, setHasRoomFeatures]) + + const { data: roomFeatures, isPending: isRoomFeaturesPending } = + trpc.hotel.availability.roomFeatures.useQuery( + { + hotelId, + startDate: fromDateString, + endDate: toDateString, + adultsCount, + childArray, + }, + { + enabled: hasRoomFeatures, + } + ) + + const { data: roomsAvailability, isPending: isRoomsAvailabiltyPending } = + trpc.hotel.availability.roomsCombinedAvailability.useQuery({ + adultsCount, + bookingCode, + childArray, + hotelId, + lang, + redemption, + roomStayEndDate: toDateString, + roomStayStartDate: fromDateString, + }) + + const combinedData = useMemo(() => { + if (!roomsAvailability) { + return undefined + } + if (!roomFeatures) { + return roomsAvailability + } + + return roomsAvailability.map((room, idx) => { + if ("error" in room) { + return room + } + + return { + ...room, + roomConfigurations: room.roomConfigurations.map((config) => ({ + ...config, + features: + roomFeatures?.[idx]?.find( + (r) => r.roomTypeCode === config.roomTypeCode + )?.features ?? [], + })), + } + }) + }, [roomFeatures, roomsAvailability]) + + return { + data: combinedData, + isPending: hasRoomFeatures + ? isRoomsAvailabiltyPending || isRoomFeaturesPending + : isRoomsAvailabiltyPending, + } } export function useHotelPackages( diff --git a/apps/scandic-web/lib/api/endpoints.ts b/apps/scandic-web/lib/api/endpoints.ts index e4b01080c..a85ac3578 100644 --- a/apps/scandic-web/lib/api/endpoints.ts +++ b/apps/scandic-web/lib/api/endpoints.ts @@ -25,6 +25,7 @@ export namespace endpoints { Reward = "Reward", Stays = "Stays", Transaction = "Transaction", + RoomFeature = "RoomFeature", } } @@ -44,6 +45,9 @@ export namespace endpoints { export function hotel(hotelId: string) { return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotel/${hotelId}` } + export function roomFeatures(hotelId: string) { + return `${base.path.availability}/${version}/${base.enitity.RoomFeature}/features/hotel/${hotelId}` + } } /** diff --git a/apps/scandic-web/server/routers/hotels/input.ts b/apps/scandic-web/server/routers/hotels/input.ts index fc4264362..3582367ef 100644 --- a/apps/scandic-web/server/routers/hotels/input.ts +++ b/apps/scandic-web/server/routers/hotels/input.ts @@ -2,6 +2,8 @@ import { z } from "zod" import { Lang } from "@/constants/languages" +import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator" + import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { Country } from "@/types/enums/country" @@ -159,3 +161,21 @@ export const getHotelsByCityIdentifierInput = z.object({ export const getLocationsInput = z.object({ lang: z.nativeEnum(Lang), }) + +export const roomFeaturesInputSchema = z.object({ + hotelId: z.string(), + startDate: z.string(), + endDate: z.string(), + adultsCount: z.array(z.number()), + childArray: z + .array( + nullableArrayObjectValidator( + z.object({ + age: z.number(), + bed: z.nativeEnum(ChildBedMapEnum), + }) + ) + ) + .nullable(), + roomFeatureCode: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(), +}) diff --git a/apps/scandic-web/server/routers/hotels/metrics.ts b/apps/scandic-web/server/routers/hotels/metrics.ts index a5919787e..856866331 100644 --- a/apps/scandic-web/server/routers/hotels/metrics.ts +++ b/apps/scandic-web/server/routers/hotels/metrics.ts @@ -84,4 +84,9 @@ export const metrics = { fail: meter.createCounter("trpc.hotel.availability.room-fail"), success: meter.createCounter("trpc.hotel.availability.room-success"), }, + roomFeatures: { + counter: meter.createCounter("trpc.availability.roomfeature"), + fail: meter.createCounter("trpc.availability.roomfeature-fail"), + success: meter.createCounter("trpc.availability.roomfeature-success"), + }, } diff --git a/apps/scandic-web/server/routers/hotels/output.ts b/apps/scandic-web/server/routers/hotels/output.ts index b96ec1b8a..5557cf962 100644 --- a/apps/scandic-web/server/routers/hotels/output.ts +++ b/apps/scandic-web/server/routers/hotels/output.ts @@ -25,6 +25,7 @@ import { roomConfigurationSchema } from "./schemas/roomAvailability/configuratio import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RateTypeEnum } from "@/types/enums/rateType" import type { AdditionalData, @@ -616,3 +617,32 @@ export const getNearbyHotelIdsSchema = z ), }) .transform((data) => data.data.map((hotel) => hotel.id)) + +export const roomFeaturesSchema = z + .object({ + data: z.object({ + attributes: z.object({ + hotelId: z.number(), + roomFeatures: z + .array( + z.object({ + roomTypeCode: z.string(), + features: z.array( + z.object({ + inventory: z.number(), + code: z.enum([ + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, + ]), + }) + ), + }) + ) + .default([]), + }), + }), + }) + .transform((data) => { + return data.data.attributes.roomFeatures + }) diff --git a/apps/scandic-web/server/routers/hotels/query.ts b/apps/scandic-web/server/routers/hotels/query.ts index 1f2d6cf79..d0a72b453 100644 --- a/apps/scandic-web/server/routers/hotels/query.ts +++ b/apps/scandic-web/server/routers/hotels/query.ts @@ -38,6 +38,7 @@ import { hotelsAvailabilityInputSchema, nearbyHotelIdsInput, ratesInputSchema, + roomFeaturesInputSchema, roomPackagesInputSchema, roomsCombinedAvailabilityInputSchema, selectedRoomAvailabilityInputSchema, @@ -51,6 +52,7 @@ import { hotelSchema, packagesSchema, ratesSchema, + roomFeaturesSchema, roomsAvailabilitySchema, } from "./output" import tempRatesData from "./tempRatesData.json" @@ -917,6 +919,77 @@ export const hotelQueryRouter = router({ .concat(unavailableHotels.availability), } }), + roomFeatures: serviceProcedure + .input(roomFeaturesInputSchema) + .query(async ({ input, ctx }) => { + const { hotelId, startDate, endDate, adultsCount, childArray } = input + + const responses = await Promise.allSettled( + adultsCount.map(async (adultCount, index) => { + const kids = childArray?.[index] + const params = { + hotelId, + roomStayStartDate: startDate, + roomStayEndDate: endDate, + adults: adultCount, + ...(kids?.length && { children: generateChildrenString(kids) }), + } + + metrics.roomFeatures.counter.add(1, params) + + const apiResponse = await api.get( + api.endpoints.v1.Availability.roomFeatures(hotelId), + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = apiResponse.text() + console.error( + "api.availability.roomfeature error", + JSON.stringify({ + query: { hotelId, params }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + metrics.roomFeatures.fail.add(1, params) + return null + } + + const data = await apiResponse.json() + const validatedRoomFeaturesData = roomFeaturesSchema.safeParse(data) + if (!validatedRoomFeaturesData.success) { + console.error( + "api.availability.roomfeature error", + JSON.stringify({ + query: { hotelId, params }, + error: validatedRoomFeaturesData.error, + }) + ) + return null + } + + metrics.roomFeatures.success.add(1, params) + + return validatedRoomFeaturesData.data + }) + ) + + return responses.map((features) => { + if (features.status === "fulfilled") { + return features.value + } + return null + }) + }), }), rates: router({ get: publicProcedure.input(ratesInputSchema).query(async () => { diff --git a/apps/scandic-web/stores/select-rate/helpers.ts b/apps/scandic-web/stores/select-rate/helpers.ts index 6d1d1d367..ab8d69218 100644 --- a/apps/scandic-web/stores/select-rate/helpers.ts +++ b/apps/scandic-web/stores/select-rate/helpers.ts @@ -1,3 +1,4 @@ +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { AvailabilityError } from "@/types/stores/rates" import type { Product, @@ -78,3 +79,11 @@ export function findSelectedRate( return findProductInRoom(rateCode, room) }) } + +export function isRoomPackageCode( + code: string | null +): code is RoomPackageCodeEnum { + return Object.values(RoomPackageCodeEnum).includes( + code as RoomPackageCodeEnum + ) +} diff --git a/apps/scandic-web/stores/select-rate/index.ts b/apps/scandic-web/stores/select-rate/index.ts index 6d1cb8bb7..1fdcc1a7b 100644 --- a/apps/scandic-web/stores/select-rate/index.ts +++ b/apps/scandic-web/stores/select-rate/index.ts @@ -5,7 +5,11 @@ import { create, useStore } from "zustand" import { RatesContext } from "@/contexts/Rates" -import { findProductInRoom, findSelectedRate } from "./helpers" +import { + findProductInRoom, + findSelectedRate, + isRoomPackageCode, +} from "./helpers" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter" @@ -83,6 +87,7 @@ export function createRatesStore({ rate: product.rate, roomType: selectedRoom.roomType, roomTypeCode: selectedRoom.roomTypeCode, + package: room.packages?.[0], } } } @@ -125,7 +130,11 @@ export function createRatesStore({ product = findProductInRoom(room.rateCode, selectedRate) } - const selectedPackage = room.packages?.[0] + // Since features are fetched async based on query string, we need to read from query string to apply correct filtering + const packagesParam = searchParams.get(`room[${idx}].packages`) + const selectedPackage = isRoomPackageCode(packagesParam) + ? packagesParam + : undefined let rooms: RoomConfiguration[] = roomConfiguration if (selectedPackage) { diff --git a/apps/scandic-web/utils/url.ts b/apps/scandic-web/utils/url.ts index fa3897465..087b77950 100644 --- a/apps/scandic-web/utils/url.ts +++ b/apps/scandic-web/utils/url.ts @@ -47,6 +47,10 @@ export function getSearchParamFromKey(key: string): string { return key } +export function searchParamsToRecord(searchParams: URLSearchParams) { + return Object.fromEntries(searchParams.entries()) +} + export function convertSearchParamsToObj( searchParams: Record ) {