fix: we showed duplicate rooms because every bed represents a room
This commit is contained in:
@@ -17,94 +17,89 @@ import styles from "./hotelInfoCard.module.css"
|
|||||||
|
|
||||||
import type { HotelInfoCardProps } from "@/types/components/hotelReservation/selectRate/hotelInfoCard"
|
import type { HotelInfoCardProps } from "@/types/components/hotelReservation/selectRate/hotelInfoCard"
|
||||||
|
|
||||||
export default async function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
|
export default async function HotelInfoCard({ hotel }: HotelInfoCardProps) {
|
||||||
const hotel = hotelData?.hotel
|
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
const sortedFacilities = hotel?.detailedFacilities
|
const sortedFacilities = hotel.detailedFacilities
|
||||||
.sort((a, b) => b.sortOrder - a.sortOrder)
|
.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
|
|
||||||
const galleryImages = mapApiImagesToGalleryImages(hotel?.galleryImages || [])
|
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={styles.container}>
|
<article className={styles.container}>
|
||||||
{hotel && (
|
<section className={styles.wrapper}>
|
||||||
<section className={styles.wrapper}>
|
<div className={styles.imageWrapper}>
|
||||||
<div className={styles.imageWrapper}>
|
<ImageGallery title={hotel.name} images={galleryImages} fill />
|
||||||
<ImageGallery title={hotel.name} images={galleryImages} fill />
|
{hotel.ratings?.tripAdvisor && (
|
||||||
{hotel.ratings?.tripAdvisor && (
|
<TripAdvisorChip rating={hotel.ratings.tripAdvisor.rating} />
|
||||||
<TripAdvisorChip rating={hotel.ratings.tripAdvisor.rating} />
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className={styles.hotelContent}>
|
||||||
<div className={styles.hotelContent}>
|
<div className={styles.hotelInformation}>
|
||||||
<div className={styles.hotelInformation}>
|
<Title as="h2" textTransform="uppercase">
|
||||||
<Title as="h2" textTransform="uppercase">
|
{hotel.name}
|
||||||
{hotel.name}
|
</Title>
|
||||||
</Title>
|
<div className={styles.hotelAddressDescription}>
|
||||||
<div className={styles.hotelAddressDescription}>
|
<Caption color="uiTextMediumContrast">
|
||||||
<Caption color="uiTextMediumContrast">
|
{intl.formatMessage(
|
||||||
{intl.formatMessage(
|
{
|
||||||
{
|
id: "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center",
|
||||||
id: "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center",
|
},
|
||||||
},
|
{
|
||||||
{
|
address: hotel.address.streetAddress,
|
||||||
address: hotel.address.streetAddress,
|
city: hotel.address.city,
|
||||||
city: hotel.address.city,
|
distanceToCityCenterInKm: getSingleDecimal(
|
||||||
distanceToCityCenterInKm: getSingleDecimal(
|
hotel.location.distanceToCentre / 1000
|
||||||
hotel.location.distanceToCentre / 1000
|
),
|
||||||
),
|
}
|
||||||
}
|
)}
|
||||||
)}
|
</Caption>
|
||||||
</Caption>
|
<Body color="uiTextHighContrast">
|
||||||
<Body color="uiTextHighContrast">
|
{hotel.hotelContent.texts.descriptions?.medium}
|
||||||
{hotel.hotelContent.texts.descriptions?.medium}
|
</Body>
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Divider color="subtle" variant="vertical" />
|
|
||||||
<div className={styles.facilities}>
|
|
||||||
<div className={styles.facilityList}>
|
|
||||||
<Body textTransform="bold" className={styles.facilityTitle}>
|
|
||||||
{intl.formatMessage({ id: "At the hotel" })}
|
|
||||||
</Body>
|
|
||||||
{sortedFacilities?.map((facility) => {
|
|
||||||
const IconComponent = mapFacilityToIcon(facility.id)
|
|
||||||
return (
|
|
||||||
<div className={styles.facilitiesItem} key={facility.id}>
|
|
||||||
{IconComponent && (
|
|
||||||
<IconComponent
|
|
||||||
className={styles.facilitiesIcon}
|
|
||||||
color="grey80"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Body color="uiTextHighContrast">{facility.name}</Body>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<ReadMore
|
|
||||||
label={intl.formatMessage({ id: "See all amenities" })}
|
|
||||||
hotelId={hotel.operaId}
|
|
||||||
hotel={hotel}
|
|
||||||
showCTA={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<Divider color="subtle" variant="vertical" />
|
||||||
)}
|
<div className={styles.facilities}>
|
||||||
{hotel?.specialAlerts.map((alert) => {
|
<div className={styles.facilityList}>
|
||||||
return (
|
<Body textTransform="bold" className={styles.facilityTitle}>
|
||||||
<div className={styles.hotelAlert} key={`wrapper_${alert.id}`}>
|
{intl.formatMessage({ id: "At the hotel" })}
|
||||||
<Alert
|
</Body>
|
||||||
key={alert.id}
|
{sortedFacilities?.map((facility) => {
|
||||||
type={alert.type}
|
const IconComponent = mapFacilityToIcon(facility.id)
|
||||||
heading={alert.heading}
|
return (
|
||||||
text={alert.text}
|
<div className={styles.facilitiesItem} key={facility.id}>
|
||||||
|
{IconComponent && (
|
||||||
|
<IconComponent
|
||||||
|
className={styles.facilitiesIcon}
|
||||||
|
color="grey80"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Body color="uiTextHighContrast">{facility.name}</Body>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<ReadMore
|
||||||
|
label={intl.formatMessage({ id: "See all amenities" })}
|
||||||
|
hotelId={hotel.operaId}
|
||||||
|
hotel={hotel}
|
||||||
|
showCTA={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
})}
|
</section>
|
||||||
|
{hotel.specialAlerts.map((alert) => (
|
||||||
|
<div className={styles.hotelAlert} key={`wrapper_${alert.id}`}>
|
||||||
|
<Alert
|
||||||
|
key={alert.id}
|
||||||
|
type={alert.type}
|
||||||
|
heading={alert.heading}
|
||||||
|
text={alert.text}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ function getBreakfastMessage(
|
|||||||
|
|
||||||
export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lessThanFiveRoomsLeft =
|
|
||||||
roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5
|
|
||||||
|
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const bookingCode = searchParams.get("bookingCode")
|
const bookingCode = searchParams.get("bookingCode")
|
||||||
@@ -85,6 +83,8 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
}))
|
}))
|
||||||
const { isMainRoom, roomAvailability, roomNr, selectedPackage } =
|
const { isMainRoom, roomAvailability, roomNr, selectedPackage } =
|
||||||
useRoomContext()
|
useRoomContext()
|
||||||
|
const showLowInventory =
|
||||||
|
roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5
|
||||||
|
|
||||||
if (!roomAvailability || !("rateDefinitions" in roomAvailability)) {
|
if (!roomAvailability || !("rateDefinitions" in roomAvailability)) {
|
||||||
return null
|
return null
|
||||||
@@ -177,7 +177,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
<li className={classNames}>
|
<li className={classNames}>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
<div className={styles.chipContainer}>
|
<div className={styles.chipContainer}>
|
||||||
{lessThanFiveRoomsLeft ? (
|
{showLowInventory ? (
|
||||||
<span className={styles.chip}>
|
<span className={styles.chip}>
|
||||||
<Footnote color="burgundy" textTransform="uppercase">
|
<Footnote color="burgundy" textTransform="uppercase">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
@@ -211,16 +211,16 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{occupancy.max === occupancy.min
|
{occupancy.max === occupancy.min
|
||||||
? intl.formatMessage(
|
? intl.formatMessage(
|
||||||
{ id: "{guests, plural, one {# guest} other {# guests}}" },
|
{ id: "{guests, plural, one {# guest} other {# guests}}" },
|
||||||
{ guests: occupancy.max }
|
{ guests: occupancy.max }
|
||||||
)
|
)
|
||||||
: intl.formatMessage(
|
: intl.formatMessage(
|
||||||
{ id: "{min}-{max} guests" },
|
{ id: "{min}-{max} guests" },
|
||||||
{
|
{
|
||||||
min: occupancy.min,
|
min: occupancy.min,
|
||||||
max: occupancy.max,
|
max: occupancy.max,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
)}
|
)}
|
||||||
<RoomSize roomSize={roomSize} />
|
<RoomSize roomSize={roomSize} />
|
||||||
@@ -293,7 +293,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
title={rateTitle}
|
title={rateTitle}
|
||||||
rateTitle={
|
rateTitle={
|
||||||
product.public &&
|
product.public &&
|
||||||
product.public?.rateType !== RateTypeEnum.Regular
|
product.public?.rateType !== RateTypeEnum.Regular
|
||||||
? rateDefinition?.title
|
? rateDefinition?.title
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export function RoomsContainer({
|
|||||||
childArray,
|
childArray,
|
||||||
fromDate,
|
fromDate,
|
||||||
hotelData,
|
hotelData,
|
||||||
hotelId,
|
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
toDate,
|
toDate,
|
||||||
}: RoomsContainerProps) {
|
}: RoomsContainerProps) {
|
||||||
@@ -29,7 +28,7 @@ export function RoomsContainer({
|
|||||||
const { data: roomsAvailability, isPending: isLoadingAvailability } =
|
const { data: roomsAvailability, isPending: isLoadingAvailability } =
|
||||||
useRoomsAvailability(
|
useRoomsAvailability(
|
||||||
adultArray,
|
adultArray,
|
||||||
hotelId,
|
hotelData.hotel.id,
|
||||||
fromDateString,
|
fromDateString,
|
||||||
toDateString,
|
toDateString,
|
||||||
lang,
|
lang,
|
||||||
@@ -42,7 +41,7 @@ export function RoomsContainer({
|
|||||||
childArray,
|
childArray,
|
||||||
fromDateString,
|
fromDateString,
|
||||||
toDateString,
|
toDateString,
|
||||||
hotelId,
|
hotelData.hotel.id,
|
||||||
lang
|
lang
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,10 +49,6 @@ export function RoomsContainer({
|
|||||||
return <RoomsContainerSkeleton />
|
return <RoomsContainerSkeleton />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hotelData?.hotel) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packages === null) {
|
if (packages === null) {
|
||||||
// TODO: Log packages error
|
// TODO: Log packages error
|
||||||
console.error("[RoomsContainer] unable to fetch packages")
|
console.error("[RoomsContainer] unable to fetch packages")
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import { Suspense } from "react"
|
|||||||
import { getHotel } from "@/lib/trpc/memoizedRequests"
|
import { getHotel } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth"
|
||||||
import HotelInfoCard, {
|
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
||||||
HotelInfoCardSkeleton,
|
|
||||||
} from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
|
||||||
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
|
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
|
||||||
import TrackingSDK from "@/components/TrackingSDK"
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
@@ -33,17 +31,21 @@ export default async function SelectRatePage({
|
|||||||
const { adultsInRoom, childrenInRoom, hotel, noOfRooms, selectHotelParams } =
|
const { adultsInRoom, childrenInRoom, hotel, noOfRooms, selectHotelParams } =
|
||||||
searchDetails
|
searchDetails
|
||||||
|
|
||||||
const { fromDate, toDate } = getValidDates(
|
|
||||||
selectHotelParams.fromDate,
|
|
||||||
selectHotelParams.toDate
|
|
||||||
)
|
|
||||||
|
|
||||||
const hotelData = await getHotel({
|
const hotelData = await getHotel({
|
||||||
hotelId: hotel.id,
|
hotelId: hotel.id,
|
||||||
isCardOnlyPayment: false,
|
isCardOnlyPayment: false,
|
||||||
language: params.lang,
|
language: params.lang,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!hotelData) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fromDate, toDate } = getValidDates(
|
||||||
|
selectHotelParams.fromDate,
|
||||||
|
selectHotelParams.toDate
|
||||||
|
)
|
||||||
|
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
const isUserLoggedIn = isValidSession(session)
|
const isUserLoggedIn = isValidSession(session)
|
||||||
|
|
||||||
@@ -59,20 +61,17 @@ export default async function SelectRatePage({
|
|||||||
hotel.id,
|
hotel.id,
|
||||||
hotel.name,
|
hotel.name,
|
||||||
noOfRooms,
|
noOfRooms,
|
||||||
hotelData?.hotel.address.country,
|
hotelData.hotel.address.country,
|
||||||
hotelData?.hotel.address.city,
|
hotelData.hotel.address.city,
|
||||||
selectHotelParams.city
|
selectHotelParams.city
|
||||||
)
|
)
|
||||||
|
|
||||||
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
||||||
|
|
||||||
const hotelId = +hotel.id
|
|
||||||
const suspenseKey = stringify(searchParams)
|
const suspenseKey = stringify(searchParams)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Suspense fallback={<HotelInfoCardSkeleton />}>
|
<HotelInfoCard hotel={hotelData.hotel} />
|
||||||
<HotelInfoCard hotelData={hotelData} />
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
<RoomsContainer
|
<RoomsContainer
|
||||||
adultArray={adultsInRoom}
|
adultArray={adultsInRoom}
|
||||||
@@ -80,7 +79,6 @@ export default async function SelectRatePage({
|
|||||||
childArray={childrenInRoom}
|
childArray={childrenInRoom}
|
||||||
fromDate={arrivalDate}
|
fromDate={arrivalDate}
|
||||||
hotelData={hotelData}
|
hotelData={hotelData}
|
||||||
hotelId={hotelId}
|
|
||||||
isUserLoggedIn={isUserLoggedIn}
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
toDate={departureDate}
|
toDate={departureDate}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,38 +6,22 @@ import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
|||||||
|
|
||||||
export function useRoomsAvailability(
|
export function useRoomsAvailability(
|
||||||
adultsCount: number[],
|
adultsCount: number[],
|
||||||
hotelId: number,
|
hotelId: string,
|
||||||
fromDateString: string,
|
fromDateString: string,
|
||||||
toDateString: string,
|
toDateString: string,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
childArray: ChildrenInRoom,
|
childArray: ChildrenInRoom,
|
||||||
bookingCode?: string
|
bookingCode?: string
|
||||||
) {
|
) {
|
||||||
const roomsAvailability =
|
return trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
||||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
adultsCount,
|
||||||
adultsCount,
|
bookingCode,
|
||||||
bookingCode,
|
childArray,
|
||||||
childArray,
|
hotelId,
|
||||||
hotelId,
|
lang,
|
||||||
lang,
|
roomStayEndDate: toDateString,
|
||||||
roomStayEndDate: toDateString,
|
roomStayStartDate: fromDateString,
|
||||||
roomStayStartDate: fromDateString,
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = roomsAvailability.data?.map((ra) => {
|
|
||||||
if (ra.status === "fulfilled") {
|
|
||||||
return ra.value
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
details: ra.reason,
|
|
||||||
error: "request_failure",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
...roomsAvailability,
|
|
||||||
data,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useHotelPackages(
|
export function useHotelPackages(
|
||||||
@@ -45,14 +29,14 @@ export function useHotelPackages(
|
|||||||
childArray: ChildrenInRoom,
|
childArray: ChildrenInRoom,
|
||||||
fromDateString: string,
|
fromDateString: string,
|
||||||
toDateString: string,
|
toDateString: string,
|
||||||
hotelId: number,
|
hotelId: string,
|
||||||
lang: Lang
|
lang: Lang
|
||||||
) {
|
) {
|
||||||
return trpc.hotel.packages.get.useQuery({
|
return trpc.hotel.packages.get.useQuery({
|
||||||
adults: adultArray[0], // Using the first adult count
|
adults: adultArray[0], // Using the first adult count
|
||||||
children: childArray?.[0]?.length, // Using the first children count
|
children: childArray?.[0]?.length, // Using the first children count
|
||||||
endDate: toDateString,
|
endDate: toDateString,
|
||||||
hotelId: hotelId.toString(),
|
hotelId,
|
||||||
packageCodes: [
|
packageCodes: [
|
||||||
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||||
RoomPackageCodeEnum.PET_ROOM,
|
RoomPackageCodeEnum.PET_ROOM,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default function BookingConfirmationProvider({
|
|||||||
rooms,
|
rooms,
|
||||||
vat,
|
vat,
|
||||||
}
|
}
|
||||||
|
|
||||||
storeRef.current = createBookingConfirmationStore(initialData)
|
storeRef.current = createBookingConfirmationStore(initialData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const roomsCombinedAvailabilityInputSchema = z.object({
|
|||||||
.nullable()
|
.nullable()
|
||||||
)
|
)
|
||||||
.nullish(),
|
.nullish(),
|
||||||
hotelId: z.number(),
|
hotelId: z.string(),
|
||||||
lang: z.nativeEnum(Lang),
|
lang: z.nativeEnum(Lang),
|
||||||
rateCode: z.string().optional(),
|
rateCode: z.string().optional(),
|
||||||
roomStayEndDate: z.string(),
|
roomStayEndDate: z.string(),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
Product,
|
Product,
|
||||||
RateDefinition,
|
RateDefinition,
|
||||||
|
RoomConfiguration,
|
||||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
||||||
@@ -145,6 +146,142 @@ const statusLookup = {
|
|||||||
[AvailabilityEnum.NotAvailable]: 2,
|
[AvailabilityEnum.NotAvailable]: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
|
||||||
|
// @ts-expect-error - array indexing
|
||||||
|
return statusLookup[a.status] - statusLookup[b.status]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const roomsCombinedAvailabilitySchema = z
|
||||||
|
.object({
|
||||||
|
data: z.object({
|
||||||
|
attributes: z.object({
|
||||||
|
checkInDate: z.string(),
|
||||||
|
checkOutDate: z.string(),
|
||||||
|
hotelId: z.number(),
|
||||||
|
mustBeGuaranteed: z.boolean().optional(),
|
||||||
|
occupancy: occupancySchema.optional(),
|
||||||
|
rateDefinitions: z.array(rateDefinitionSchema),
|
||||||
|
roomConfigurations: z
|
||||||
|
.array(roomConfigurationSchema)
|
||||||
|
.transform((data) => {
|
||||||
|
// Initial sort to guarantee if one bed is NotAvailable and whereas
|
||||||
|
// the other is Available to make sure data is added to the correct
|
||||||
|
// roomConfig
|
||||||
|
const configs = data.sort(sortRoomConfigs)
|
||||||
|
const roomConfigs = new Map<string, RoomConfiguration>()
|
||||||
|
for (const roomConfig of configs) {
|
||||||
|
if (roomConfigs.has(roomConfig.roomType)) {
|
||||||
|
const currentRoomConf = roomConfigs.get(roomConfig.roomType)
|
||||||
|
if (currentRoomConf) {
|
||||||
|
currentRoomConf.features = roomConfig.features.reduce(
|
||||||
|
(feats, feature) => {
|
||||||
|
const currentFeatureIndex = feats.findIndex(
|
||||||
|
(f) => f.code === feature.code
|
||||||
|
)
|
||||||
|
if (currentFeatureIndex !== -1) {
|
||||||
|
feats[currentFeatureIndex].inventory =
|
||||||
|
feats[currentFeatureIndex].inventory +
|
||||||
|
feature.inventory
|
||||||
|
} else {
|
||||||
|
feats.push(feature)
|
||||||
|
}
|
||||||
|
return feats
|
||||||
|
},
|
||||||
|
currentRoomConf.features
|
||||||
|
)
|
||||||
|
currentRoomConf.roomsLeft =
|
||||||
|
currentRoomConf.roomsLeft + roomConfig.roomsLeft
|
||||||
|
roomConfigs.set(currentRoomConf.roomType, currentRoomConf)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
roomConfigs.set(roomConfig.roomType, roomConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(roomConfigs.values())
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
relationships: relationshipsSchema.optional(),
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return room
|
||||||
|
})
|
||||||
|
.sort(sortRoomConfigs)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...attributes,
|
||||||
|
roomConfigurations,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const roomsAvailabilitySchema = z
|
export const roomsAvailabilitySchema = z
|
||||||
.object({
|
.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
packagesSchema,
|
packagesSchema,
|
||||||
ratesSchema,
|
ratesSchema,
|
||||||
roomsAvailabilitySchema,
|
roomsAvailabilitySchema,
|
||||||
|
roomsCombinedAvailabilitySchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
import tempRatesData from "./tempRatesData.json"
|
import tempRatesData from "./tempRatesData.json"
|
||||||
import {
|
import {
|
||||||
@@ -495,95 +496,110 @@ export const hotelQueryRouter = router({
|
|||||||
|
|
||||||
roomsCombinedAvailability: serviceProcedure
|
roomsCombinedAvailability: serviceProcedure
|
||||||
.input(roomsCombinedAvailabilityInputSchema)
|
.input(roomsCombinedAvailabilityInputSchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(
|
||||||
const { lang } = input
|
async ({
|
||||||
const apiLang = toApiLang(lang)
|
ctx,
|
||||||
const {
|
input: {
|
||||||
adultsCount,
|
adultsCount,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
childArray,
|
childArray,
|
||||||
hotelId,
|
hotelId,
|
||||||
rateCode,
|
lang,
|
||||||
roomStayEndDate,
|
rateCode,
|
||||||
roomStayStartDate,
|
roomStayEndDate,
|
||||||
} = input
|
roomStayStartDate,
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
const apiLang = toApiLang(lang)
|
||||||
|
|
||||||
const metricsData = {
|
const metricsData = {
|
||||||
hotelId,
|
hotelId,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
adultsCount,
|
adultsCount,
|
||||||
childArray: childArray ? JSON.stringify(childArray) : undefined,
|
childArray: childArray ? JSON.stringify(childArray) : undefined,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.roomsCombinedAvailability.counter.add(1, metricsData)
|
metrics.roomsCombinedAvailability.counter.add(1, metricsData)
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
"api.hotels.roomsCombinedAvailability start",
|
"api.hotels.roomsCombinedAvailability start",
|
||||||
JSON.stringify({ query: { hotelId, params: metricsData } })
|
JSON.stringify({ query: { hotelId, params: metricsData } })
|
||||||
)
|
)
|
||||||
|
|
||||||
const availabilityResponses = await Promise.allSettled(
|
const availabilityResponses = await Promise.allSettled(
|
||||||
adultsCount.map(async (adultCount: number, idx: number) => {
|
adultsCount.map(async (adultCount: number, idx: number) => {
|
||||||
const kids = childArray?.[idx]
|
const kids = childArray?.[idx]
|
||||||
const params: Record<string, string | number | undefined> = {
|
const params: Record<string, string | number | undefined> = {
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
adults: adultCount,
|
adults: adultCount,
|
||||||
...(kids?.length && {
|
...(kids?.length && {
|
||||||
children: generateChildrenString(kids),
|
children: generateChildrenString(kids),
|
||||||
}),
|
}),
|
||||||
...(bookingCode && { bookingCode }),
|
...(bookingCode && { bookingCode }),
|
||||||
language: apiLang,
|
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 =
|
|
||||||
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) {
|
const apiResponse = await api.get(
|
||||||
validateAvailabilityData.data.mustBeGuaranteed =
|
api.endpoints.v1.Availability.hotel(hotelId.toString()),
|
||||||
validateAvailabilityData.data.rateDefinitions.find(
|
{
|
||||||
(rate) => rate.rateCode === rateCode
|
headers: {
|
||||||
)?.mustBeGuaranteed
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
return validateAvailabilityData.data
|
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",
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
|
||||||
metrics.roomsCombinedAvailability.success.add(1, metricsData)
|
return data
|
||||||
return availabilityResponses
|
}
|
||||||
}),
|
),
|
||||||
room: serviceProcedure
|
room: serviceProcedure
|
||||||
.input(selectedRoomAvailabilityInputSchema)
|
.input(selectedRoomAvailabilityInputSchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
@@ -771,9 +787,9 @@ export const hotelQueryRouter = router({
|
|||||||
type: matchingRoom.mainBed.type,
|
type: matchingRoom.mainBed.type,
|
||||||
extraBed: matchingRoom.fixedExtraBed
|
extraBed: matchingRoom.fixedExtraBed
|
||||||
? {
|
? {
|
||||||
type: matchingRoom.fixedExtraBed.type,
|
type: matchingRoom.fixedExtraBed.type,
|
||||||
description: matchingRoom.fixedExtraBed.description,
|
description: matchingRoom.fixedExtraBed.description,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1102,9 +1118,9 @@ export const hotelQueryRouter = router({
|
|||||||
|
|
||||||
return hotelData
|
return hotelData
|
||||||
? {
|
? {
|
||||||
...hotelData,
|
...hotelData,
|
||||||
url: hotelPage?.url ?? null,
|
url: hotelPage?.url ?? null,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,8 +45,5 @@ export const roomConfigurationSchema = z
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creating a new objekt since data is frozen (readony)
|
return data
|
||||||
// and can cause errors to be thrown if trying to manipulate
|
|
||||||
// object elsewhere
|
|
||||||
return { ...data }
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { HotelData } from "@/types/hotel"
|
import type { Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
export interface HotelInfoCardProps {
|
export interface HotelInfoCardProps {
|
||||||
hotelData: HotelData | null
|
hotel: Hotel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ export interface RoomsContainerProps {
|
|||||||
bookingCode?: string
|
bookingCode?: string
|
||||||
childArray: ChildrenInRoom
|
childArray: ChildrenInRoom
|
||||||
fromDate: Date
|
fromDate: Date
|
||||||
hotelData: HotelData | null
|
hotelData: HotelData
|
||||||
hotelId: number
|
|
||||||
isUserLoggedIn: boolean
|
isUserLoggedIn: boolean
|
||||||
toDate: Date
|
toDate: Date
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user