Merged in feat/SW-2033-implement-new-room-feature-request (pull request #1665)
feat(SW-2033): Added new route for fetching room features, and merged the data with existing availability data * feat(SW-2033): Added new route for fetching room features, and merged the data with existing availability data * fix: issue with total price not including room features * fix: add return null * fix * fix * fixes from PR feedback Approved-by: Arvid Norlin
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
|
import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url"
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
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 { Lang } from "@/constants/languages"
|
||||||
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
||||||
|
|
||||||
@@ -14,7 +20,34 @@ export function useRoomsAvailability(
|
|||||||
bookingCode?: string,
|
bookingCode?: string,
|
||||||
redemption?: boolean
|
redemption?: boolean
|
||||||
) {
|
) {
|
||||||
return trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
const searchParams = useSearchParams()
|
||||||
|
const searchParamsObj = convertSearchParamsToObj<SelectRateSearchParams>(
|
||||||
|
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,
|
adultsCount,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
childArray,
|
childArray,
|
||||||
@@ -24,6 +57,39 @@ export function useRoomsAvailability(
|
|||||||
roomStayEndDate: toDateString,
|
roomStayEndDate: toDateString,
|
||||||
roomStayStartDate: fromDateString,
|
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(
|
export function useHotelPackages(
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export namespace endpoints {
|
|||||||
Reward = "Reward",
|
Reward = "Reward",
|
||||||
Stays = "Stays",
|
Stays = "Stays",
|
||||||
Transaction = "Transaction",
|
Transaction = "Transaction",
|
||||||
|
RoomFeature = "RoomFeature",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +45,9 @@ export namespace endpoints {
|
|||||||
export function hotel(hotelId: string) {
|
export function hotel(hotelId: string) {
|
||||||
return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotel/${hotelId}`
|
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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||||
|
|
||||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { Country } from "@/types/enums/country"
|
import { Country } from "@/types/enums/country"
|
||||||
@@ -159,3 +161,21 @@ export const getHotelsByCityIdentifierInput = z.object({
|
|||||||
export const getLocationsInput = z.object({
|
export const getLocationsInput = z.object({
|
||||||
lang: z.nativeEnum(Lang),
|
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(),
|
||||||
|
})
|
||||||
|
|||||||
@@ -84,4 +84,9 @@ export const metrics = {
|
|||||||
fail: meter.createCounter("trpc.hotel.availability.room-fail"),
|
fail: meter.createCounter("trpc.hotel.availability.room-fail"),
|
||||||
success: meter.createCounter("trpc.hotel.availability.room-success"),
|
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"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { roomConfigurationSchema } from "./schemas/roomAvailability/configuratio
|
|||||||
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
|
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
|
||||||
|
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
import type {
|
import type {
|
||||||
AdditionalData,
|
AdditionalData,
|
||||||
@@ -616,3 +617,32 @@ export const getNearbyHotelIdsSchema = z
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
.transform((data) => data.data.map((hotel) => hotel.id))
|
.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
|
||||||
|
})
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
hotelsAvailabilityInputSchema,
|
hotelsAvailabilityInputSchema,
|
||||||
nearbyHotelIdsInput,
|
nearbyHotelIdsInput,
|
||||||
ratesInputSchema,
|
ratesInputSchema,
|
||||||
|
roomFeaturesInputSchema,
|
||||||
roomPackagesInputSchema,
|
roomPackagesInputSchema,
|
||||||
roomsCombinedAvailabilityInputSchema,
|
roomsCombinedAvailabilityInputSchema,
|
||||||
selectedRoomAvailabilityInputSchema,
|
selectedRoomAvailabilityInputSchema,
|
||||||
@@ -51,6 +52,7 @@ import {
|
|||||||
hotelSchema,
|
hotelSchema,
|
||||||
packagesSchema,
|
packagesSchema,
|
||||||
ratesSchema,
|
ratesSchema,
|
||||||
|
roomFeaturesSchema,
|
||||||
roomsAvailabilitySchema,
|
roomsAvailabilitySchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
import tempRatesData from "./tempRatesData.json"
|
import tempRatesData from "./tempRatesData.json"
|
||||||
@@ -917,6 +919,77 @@ export const hotelQueryRouter = router({
|
|||||||
.concat(unavailableHotels.availability),
|
.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({
|
rates: router({
|
||||||
get: publicProcedure.input(ratesInputSchema).query(async () => {
|
get: publicProcedure.input(ratesInputSchema).query(async () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import type { AvailabilityError } from "@/types/stores/rates"
|
import type { AvailabilityError } from "@/types/stores/rates"
|
||||||
import type {
|
import type {
|
||||||
Product,
|
Product,
|
||||||
@@ -78,3 +79,11 @@ export function findSelectedRate(
|
|||||||
return findProductInRoom(rateCode, room)
|
return findProductInRoom(rateCode, room)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isRoomPackageCode(
|
||||||
|
code: string | null
|
||||||
|
): code is RoomPackageCodeEnum {
|
||||||
|
return Object.values(RoomPackageCodeEnum).includes(
|
||||||
|
code as RoomPackageCodeEnum
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { create, useStore } from "zustand"
|
|||||||
|
|
||||||
import { RatesContext } from "@/contexts/Rates"
|
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 { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
@@ -83,6 +87,7 @@ export function createRatesStore({
|
|||||||
rate: product.rate,
|
rate: product.rate,
|
||||||
roomType: selectedRoom.roomType,
|
roomType: selectedRoom.roomType,
|
||||||
roomTypeCode: selectedRoom.roomTypeCode,
|
roomTypeCode: selectedRoom.roomTypeCode,
|
||||||
|
package: room.packages?.[0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +130,11 @@ export function createRatesStore({
|
|||||||
product = findProductInRoom(room.rateCode, selectedRate)
|
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
|
let rooms: RoomConfiguration[] = roomConfiguration
|
||||||
if (selectedPackage) {
|
if (selectedPackage) {
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export function getSearchParamFromKey(key: string): string {
|
|||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function searchParamsToRecord(searchParams: URLSearchParams) {
|
||||||
|
return Object.fromEntries(searchParams.entries())
|
||||||
|
}
|
||||||
|
|
||||||
export function convertSearchParamsToObj<T extends PartialRoom>(
|
export function convertSearchParamsToObj<T extends PartialRoom>(
|
||||||
searchParams: Record<string, string>
|
searchParams: Record<string, string>
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user