feat: SW-1588 Implemented booking code select-rate

This commit is contained in:
Hrishikesh Vaipurkar
2025-02-14 20:50:42 +01:00
parent 832b6c27e0
commit 8966e56820
28 changed files with 242 additions and 60 deletions

View File

@@ -76,6 +76,7 @@ export default async function DetailsPage({
roomStayStartDate: booking.fromDate, roomStayStartDate: booking.fromDate,
roomStayEndDate: booking.toDate, roomStayEndDate: booking.toDate,
roomTypeCode: room.roomTypeCode, roomTypeCode: room.roomTypeCode,
bookingCode: booking.bookingCode,
} }
const packages = room.packages const packages = room.packages

View File

@@ -18,6 +18,7 @@ export default function TabletCodeInput({
{...register("bookingCode.value", { {...register("bookingCode.value", {
onChange: (e) => updateValue(e.target.value), onChange: (e) => updateValue(e.target.value),
})} })}
defaultValue={defaultValue}
autoComplete="off" autoComplete="off"
/> />
) )

View File

@@ -31,6 +31,11 @@
width: 100%; width: 100%;
} }
.bookingCodeTooltip {
max-width: 560px;
margin-top: var(--Spacing-x2);
}
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
.hideOnMobile { .hideOnMobile {
display: none; display: none;

View File

@@ -115,7 +115,7 @@ export default function BookingCode() {
}, },
})} })}
> >
<Caption color="red" type="bold" asChild> <Caption color="uiTextMediumContrast" asChild>
<span>{codeVoucher}</span> <span>{codeVoucher}</span>
</Caption> </Caption>
</Checkbox> </Checkbox>
@@ -228,7 +228,9 @@ function CodeRulesModal() {
} }
title={codeVoucher} title={codeVoucher}
> >
<Body color="uiTextHighContrast">{bookingCodeTooltipText}</Body> <Body color="uiTextHighContrast" className={styles.bookingCodeTooltip}>
{bookingCodeTooltipText}
</Body>
</Modal> </Modal>
) )
} }

View File

@@ -24,6 +24,8 @@ import {
HotelCardListingTypeEnum, HotelCardListingTypeEnum,
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
import { RateTypeEnum } from "@/types/enums/rateType"
export default function HotelCardListing({ export default function HotelCardListing({
hotelData, hotelData,
@@ -55,10 +57,11 @@ export default function HotelCardListing({
? sortedHotels.filter( ? sortedHotels.filter(
(hotel) => (hotel) =>
!hotel.price || !hotel.price ||
activeCodeFilter === "all" || activeCodeFilter === BookingCodeFilterEnum.All ||
(activeCodeFilter === "discounted" && (activeCodeFilter === BookingCodeFilterEnum.Discounted &&
hotel.price?.public?.rateType?.toLowerCase() !== "regular") || hotel.price?.public?.rateType !== RateTypeEnum.Regular) ||
activeCodeFilter === hotel.price?.public?.rateType?.toLowerCase() (activeCodeFilter === BookingCodeFilterEnum.Regular &&
hotel.price?.public?.rateType === RateTypeEnum.Regular)
) )
: sortedHotels : sortedHotels

View File

@@ -11,6 +11,8 @@ import styles from "./bookingCodeFilter.module.css"
import type { Key } from "react" import type { Key } from "react"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
export default function BookingCodeFilter() { export default function BookingCodeFilter() {
const intl = useIntl() const intl = useIntl()
const activeCodeFilter = useBookingCodeFilterStore( const activeCodeFilter = useBookingCodeFilterStore(
@@ -21,20 +23,20 @@ export default function BookingCodeFilter() {
const bookingCodeFilterItems = [ const bookingCodeFilterItems = [
{ {
label: intl.formatMessage({ id: "Discounted rooms" }), label: intl.formatMessage({ id: "Discounted rooms" }),
value: "discounted", value: BookingCodeFilterEnum.Discounted,
}, },
{ {
label: intl.formatMessage({ id: "Full price rooms" }), label: intl.formatMessage({ id: "Full price rooms" }),
value: "regular", value: BookingCodeFilterEnum.Regular,
}, },
{ {
label: intl.formatMessage({ id: "See all" }), label: intl.formatMessage({ id: "See all" }),
value: "all", value: BookingCodeFilterEnum.All,
}, },
] ]
function updateFilter(selectedFilter: Key) { function updateFilter(selectedFilter: Key) {
setFilter(selectedFilter as string) setFilter(selectedFilter as BookingCodeFilterEnum)
} }
return ( return (

View File

@@ -28,6 +28,8 @@ import styles from "./selectHotelMapContent.module.css"
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map" import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
import { RateTypeEnum } from "@/types/enums/rateType"
const SKELETON_LOAD_DELAY = 750 const SKELETON_LOAD_DELAY = 750
@@ -83,10 +85,11 @@ export default function SelectHotelContent({
? hotelPins.filter( ? hotelPins.filter(
(hotel) => (hotel) =>
!hotel.publicPrice || !hotel.publicPrice ||
activeCodeFilter === "all" || activeCodeFilter === BookingCodeFilterEnum.All ||
(activeCodeFilter === "discounted" && (activeCodeFilter === BookingCodeFilterEnum.Discounted &&
hotel.rateType?.toLowerCase() !== "regular") || hotel.rateType !== RateTypeEnum.Regular) ||
activeCodeFilter === hotel.rateType?.toLowerCase() (activeCodeFilter === BookingCodeFilterEnum.Regular &&
hotel.rateType === RateTypeEnum.Regular)
) )
: hotelPins : hotelPins
return updatedHotelsList.filter((hotel) => return updatedHotelsList.filter((hotel) =>

View File

@@ -101,32 +101,57 @@ export default function PriceList({
</div> </div>
)} )}
<div className={styles.priceRow}> {memberLocalPrice && !publicLocalPrice.regularPricePerNight ? (
<dt> <div className={styles.priceRow}>
<Caption type="bold" color={memberLocalPrice ? "red" : "disabled"}> <dt>
{intl.formatMessage({ id: "Member price" })} <Caption type="bold" color={memberLocalPrice ? "red" : "disabled"}>
</Caption> {intl.formatMessage({ id: "Member price" })}
</dt> </Caption>
<dd> </dt>
{memberLocalPrice ? ( <dd>
<div className={styles.price}> {memberLocalPrice ? (
<Subtitle type="two" color="red"> <div className={styles.price}>
{totalMemberLocalPricePerNight} <Subtitle type="two" color="red">
</Subtitle> {totalMemberLocalPricePerNight}
<Body color="red" textTransform="bold"> </Subtitle>
{memberLocalPrice.currency} <Body color="red" textTransform="bold">
<span className={styles.perNight}> {memberLocalPrice.currency}
/{intl.formatMessage({ id: "night" })} <span className={styles.perNight}>
</span> /{intl.formatMessage({ id: "night" })}
</span>
</Body>
</div>
) : (
<Body textTransform="bold" color="disabled">
-
</Body> </Body>
</div> )}
) : ( </dd>
<Body textTransform="bold" color="disabled"> </div>
- ) : (
</Body> publicLocalPrice.regularPricePerNight && (
)} <div className={styles.priceRow}>
</dd> <dt>
</div> <Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Regular price" })}
</Caption>
</dt>
<dd>
<div className={styles.priceStriked}>
<Subtitle type="two" color="uiTextHighContrast">
{publicLocalPrice.regularPricePerNight}
</Subtitle>
<Body color="uiTextHighContrast" textTransform="bold">
{publicLocalPrice.currency}
<span className={styles.perNight}>
/{intl.formatMessage({ id: "night" })}
</span>
</Body>
</div>
</dd>
</div>
)
)}
{showRequestedPrice && ( {showRequestedPrice && (
<div className={styles.priceRow}> <div className={styles.priceRow}>
<dt> <dt>

View File

@@ -17,6 +17,12 @@
gap: var(--Spacing-x-half); gap: var(--Spacing-x-half);
} }
.priceStriked {
display: flex;
gap: var(--Spacing-x-half);
text-decoration: line-through;
}
.perNight { .perNight {
font-weight: 400; font-weight: 400;
font-size: var(--typography-Caption-Regular-fontSize); font-size: var(--typography-Caption-Regular-fontSize);

View File

@@ -100,4 +100,5 @@ input[type="radio"]:checked + .card .checkIcon {
.termsIcon { .termsIcon {
padding-right: var(--Spacing-x1); padding-right: var(--Spacing-x1);
flex-shrink: 0; flex-shrink: 0;
flex-basis: 32px;
} }

View File

@@ -9,7 +9,7 @@ import { useRatesStore } from "@/stores/select-rate"
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek" import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
import { getRates } from "@/components/HotelReservation/SelectRate/utils" import { getRates } from "@/components/HotelReservation/SelectRate/utils"
import { getIconForFeatureCode } from "@/components/HotelReservation/utils" import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
import { ErrorCircleIcon } from "@/components/Icons" import { ErrorCircleIcon, PriceTagIcon } from "@/components/Icons"
import ImageGallery from "@/components/ImageGallery" import ImageGallery from "@/components/ImageGallery"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
@@ -28,7 +28,9 @@ import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHote
import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard" import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { HotelTypeEnum } from "@/types/enums/hotelType" import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability" import type { Product, RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability"
import { RateTypeEnum } from "@/types/enums/rateType"
import { useSearchParams } from "next/navigation"
function getBreakfastMessage( function getBreakfastMessage(
publicBreakfastIncluded: boolean, publicBreakfastIncluded: boolean,
@@ -72,6 +74,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const lessThanFiveRoomsLeft = const lessThanFiveRoomsLeft =
roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5 roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5
const searchParams = useSearchParams()
const bookingCode = searchParams.get("bookingCode")
const { const {
hotelId, hotelId,
hotelType, hotelType,
@@ -138,39 +143,52 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const payLater = intl.formatMessage({ id: "Pay later" }) const payLater = intl.formatMessage({ id: "Pay later" })
const payNow = intl.formatMessage({ id: "Pay now" }) const payNow = intl.formatMessage({ id: "Pay now" })
function getRate(rateCode: string) { function getRate(rateCode: string, rateDefinition?: RateDefinition) {
switch (rateCode) { switch (rateCode) {
case "change": case "change":
return { return {
isFlex: false, isFlex: false,
notAvailable: false, notAvailable: false,
title: freeBooking, title: freeBooking,
terms: rateDefinition?.generalTerms,
} }
case "flex": case "flex":
return { return {
isFlex: true, isFlex: true,
notAvailable: false, notAvailable: false,
title: freeCancelation, title: freeCancelation,
terms: rateDefinition?.generalTerms,
} }
case "save": case "save":
return { return {
isFlex: false, isFlex: false,
notAvailable: false, notAvailable: false,
title: nonRefundable, title: nonRefundable,
terms: rateDefinition?.generalTerms,
}
case "special":
return {
isFlex: rateDefinition?.cancellationRule === "CancellableBefore6PM",
notAvailable: false,
title: rateDefinition?.title ?? "",
terms: rateDefinition?.generalTerms,
} }
default: default:
throw new Error( throw new Error(
`Unknown key for rate, should be "change", "flex" or "save", but got ${rateCode}` `Unknown key for rate, should be "change", "flex", "save" or "special", but got ${rateCode}`
) )
} }
} }
function getRateInfo(product: Product) { function getRateInfo(product: Product) {
const rateDefinition = rateDefinitions?.filter((rateDefinition) =>
rateDefinition.rateCode === product.productType.public.rateCode
)[0]
if ( if (
!product.productType.public.rateCode && !product.productType.public.rateCode &&
!product.productType.member?.rateCode !product.productType.member?.rateCode
) { ) {
const possibleRate = getRate(product.productType.public.rate) const possibleRate = getRate(product.productType.public.rate, rateDefinition)
if (possibleRate) { if (possibleRate) {
return { return {
...possibleRate, ...possibleRate,
@@ -181,6 +199,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
isFlex: false, isFlex: false,
notAvailable: true, notAvailable: true,
title: "", title: "",
terms: undefined,
} }
} }
@@ -198,14 +217,31 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
) )
} }
if (!publicRate && !memberRate) {
throw new Error(
"We should never make it here without any single available rateCode"
)
}
const specialRate = publicRate || memberRate
if (product.productType.public.rateType !== "Regular" && specialRate) {
return getRate(specialRate, rateDefinition)
}
if (!publicRate || !memberRate) { if (!publicRate || !memberRate) {
throw new Error("We should never make it where without rateCodes") throw new Error(
"We should never make it here without both public and member rateCodes"
)
} }
const key = isUserLoggedIn && isMainRoom ? memberRate : publicRate const key = isUserLoggedIn && isMainRoom ? memberRate : publicRate
return getRate(key) return getRate(key, rateDefinition)
} }
const isSpecialRate =
bookingCode &&
roomConfiguration.products.every((item) => {
return item.productType.public.rateType !== RateTypeEnum.Regular
})
return ( return (
<li className={classNames}> <li className={classNames}>
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
@@ -289,6 +325,12 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
) : ( ) : (
<> <>
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption> <Caption color="uiTextHighContrast">{breakfastMessage}</Caption>
{bookingCode ? (
<span className={!isSpecialRate ? styles.strikedText : ""}>
<PriceTagIcon />
{bookingCode}
</span>
) : null}
{roomConfiguration.products.map((product) => { {roomConfiguration.products.map((product) => {
const rate = getRateInfo(product) const rate = getRateInfo(product)
const isSelectedRateCode = const isSelectedRateCode =
@@ -311,6 +353,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
roomType={roomConfiguration.roomType} roomType={roomConfiguration.roomType}
roomTypeCode={roomConfiguration.roomTypeCode} roomTypeCode={roomConfiguration.roomTypeCode}
title={rate.title} title={rate.title}
priceInformation={rate.terms}
/> />
) )
})} })}

View File

@@ -98,3 +98,6 @@ div[data-multiroom="true"] .imageContainer {
margin: 0; margin: 0;
padding: var(--Spacing-x2); padding: var(--Spacing-x2);
} }
.strikedText {
text-decoration: line-through;
}

View File

@@ -1,9 +1,11 @@
"use client" "use client"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useSearchParams } from "next/navigation"
import { alternativeHotels } from "@/constants/routes/hotelReservation" import { alternativeHotels } from "@/constants/routes/hotelReservation"
import Alert from "@/components/TempDesignSystem/Alert" import Alert from "@/components/TempDesignSystem/Alert"
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
import { useRoomContext } from "@/contexts/Room" import { useRoomContext } from "@/contexts/Room"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
@@ -14,14 +16,48 @@ import styles from "./roomSelectionPanel.module.css"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"
import { RateTypeEnum } from "@/types/enums/rateType"
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
export default function RoomSelectionPanel() { export default function RoomSelectionPanel() {
const { rooms } = useRoomContext() const { rooms } = useRoomContext()
const searchParams = useSearchParams()
const bookingCode = searchParams.get("bookingCode")
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const noAvailableRooms = rooms.every( const noAvailableRooms = rooms.every(
(roomConfig) => roomConfig.status === AvailabilityEnum.NotAvailable (roomConfig) => roomConfig.status === AvailabilityEnum.NotAvailable
) )
const activeCodeFilter = useBookingCodeFilterStore((state) => state.activeCodeFilter)
let filteredRooms = rooms, isRegularRatesAvailableWithCode: boolean = false
if (bookingCode) {
isRegularRatesAvailableWithCode =
!!bookingCode ?
rooms?.some((room) => {
return (
room.status === "Available" &&
room.products.some(
(product) =>
product.productType.public.rateType === RateTypeEnum.Regular
)
)
})
: false
filteredRooms = !isRegularRatesAvailableWithCode || activeCodeFilter === BookingCodeFilterEnum.All
? rooms : rooms.filter((room) => {
return room.products.every(
(product) =>
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
product.productType.public.rateType !== RateTypeEnum.Regular) ||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
product.productType.public.rateType === RateTypeEnum.Regular)
)
})
}
return ( return (
<> <>
{noAvailableRooms ? ( {noAvailableRooms ? (
@@ -41,8 +77,9 @@ export default function RoomSelectionPanel() {
</div> </div>
) : null} ) : null}
<RoomTypeFilter /> <RoomTypeFilter />
{bookingCode && isRegularRatesAvailableWithCode ? <BookingCodeFilter /> : null}
<ul className={styles.roomList}> <ul className={styles.roomList}>
{rooms.map((roomConfiguration) => ( {filteredRooms.map((roomConfiguration) => (
<RoomCard <RoomCard
key={roomConfiguration.roomTypeCode} key={roomConfiguration.roomTypeCode}
roomConfiguration={roomConfiguration} roomConfiguration={roomConfiguration}

View File

@@ -22,6 +22,7 @@ export function RoomsContainer({
hotelId, hotelId,
hotelData, hotelData,
toDate, toDate,
bookingCode,
}: RoomsContainerProps) { }: RoomsContainerProps) {
const { data: session } = useSession() const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session) const isUserLoggedIn = isValidClientSession(session)
@@ -39,7 +40,8 @@ export function RoomsContainer({
fromDateString, fromDateString,
toDateString, toDateString,
lang, lang,
childArray childArray,
bookingCode
) )
const { data: packages, isPending: isLoadingPackages } = useHotelPackages( const { data: packages, isPending: isLoadingPackages } = useHotelPackages(

View File

@@ -33,7 +33,7 @@ export default async function SelectRatePage({
if (!searchDetails?.hotel) { if (!searchDetails?.hotel) {
return notFound() return notFound()
} }
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } = const { hotel, adultsInRoom, childrenInRoom, selectHotelParams, bookingCode } =
searchDetails searchDetails
const { fromDate, toDate } = getValidDates( const { fromDate, toDate } = getValidDates(

View File

@@ -2,6 +2,7 @@ import { trpc } from "@/lib/trpc/client"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import { RateTypeEnum } from "@/types/enums/rateType"
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
@@ -38,14 +39,17 @@ export function getRates(
) { ) {
return { return {
change: rateDefinitions.filter( change: rateDefinitions.filter(
(rate) => rate.cancellationRule === "Changeable" (rate) => rate.cancellationRule === "Changeable" && rate.rateType === RateTypeEnum.Regular
), ),
flex: rateDefinitions.filter( flex: rateDefinitions.filter(
(rate) => rate.cancellationRule === "CancellableBefore6PM" (rate) => rate.cancellationRule === "CancellableBefore6PM" && rate.rateType === RateTypeEnum.Regular
), ),
save: rateDefinitions.filter( save: rateDefinitions.filter(
(rate) => rate.cancellationRule === "NotCancellable" (rate) => rate.cancellationRule === "NotCancellable" && rate.rateType === RateTypeEnum.Regular
), ),
special: rateDefinitions.filter(
(rate) => rate.rateType !== RateTypeEnum.Regular
)
} }
} }
@@ -55,7 +59,8 @@ export function useRoomsAvailability(
fromDateString: string, fromDateString: string,
toDateString: string, toDateString: string,
lang: Lang, lang: Lang,
childArray?: Child[] childArray?: Child[],
bookingCode?: string,
) { ) {
const returnValue = const returnValue =
trpc.hotel.availability.roomsCombinedAvailability.useQuery({ trpc.hotel.availability.roomsCombinedAvailability.useQuery({
@@ -65,6 +70,7 @@ export function useRoomsAvailability(
uniqueAdultsCount, uniqueAdultsCount,
childArray, childArray,
lang, lang,
bookingCode,
}) })
const combinedAvailability = returnValue.data?.length const combinedAvailability = returnValue.data?.length

View File

@@ -79,7 +79,9 @@ function InnerModal({
> >
{({ close }) => ( {({ close }) => (
<> <>
<header className={styles.header}> <header
className={`${styles.header} ${!subtitle ? styles.verticalCenter : ""}`}
>
<div> <div>
{title && ( {title && (
<Subtitle type="one" color="uiTextHighContrast"> <Subtitle type="one" color="uiTextHighContrast">

View File

@@ -67,6 +67,10 @@
justify-content: center; justify-content: center;
} }
.verticalCenter {
align-items: center;
}
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.overlay { .overlay {
display: flex; display: flex;

View File

@@ -524,6 +524,7 @@
"Reference": "Reference", "Reference": "Reference",
"Reference #{bookingNr}": "Reference #{bookingNr}", "Reference #{bookingNr}": "Reference #{bookingNr}",
"Reference number": "Reference number", "Reference number": "Reference number",
"Regular price": "Regular price",
"Relax": "Relax", "Relax": "Relax",
"Remember code": "Remember code", "Remember code": "Remember code",
"Remove card from member profile": "Remove card from member profile", "Remove card from member profile": "Remove card from member profile",

View File

@@ -70,6 +70,7 @@ import type { HotelDataWithUrl } from "@/types/hotel"
import type { import type {
HotelsAvailabilityInputSchema, HotelsAvailabilityInputSchema,
HotelsByHotelIdsAvailabilityInputSchema, HotelsByHotelIdsAvailabilityInputSchema,
RoomsAvailabilityInputSchema,
} from "@/types/trpc/routers/hotel/availability" } from "@/types/trpc/routers/hotel/availability"
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel" import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations" import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
@@ -801,6 +802,8 @@ export const hotelQueryRouter = router({
}) })
const bookingCodeAvailabilityResponse = const bookingCodeAvailabilityResponse =
await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken) await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
// If API or network failed with no response
if (!bookingCodeAvailabilityResponse) { if (!bookingCodeAvailabilityResponse) {
metrics.hotelsAvailabilityBookingCode.fail.add(1, { metrics.hotelsAvailabilityBookingCode.fail.add(1, {
...input, ...input,

View File

@@ -6,6 +6,8 @@ export const priceSchema = z.object({
currency: z.nativeEnum(CurrencyEnum), currency: z.nativeEnum(CurrencyEnum),
pricePerNight: z.coerce.number(), pricePerNight: z.coerce.number(),
pricePerStay: z.coerce.number(), pricePerStay: z.coerce.number(),
regularPricePerNight: z.coerce.number().optional(),
regularPricePerStay: z.coerce.number().optional(),
}) })
export const productTypePriceSchema = z.object({ export const productTypePriceSchema = z.object({

View File

@@ -5,6 +5,7 @@ import { productSchema } from "./product"
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 { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { RateTypeEnum } from "@/types/enums/rateType"
export const roomConfigurationSchema = z export const roomConfigurationSchema = z
.object({ .object({
@@ -43,6 +44,11 @@ export const roomConfigurationSchema = z
return product return product
} }
// Return rate even if single when special rates
if (product.productType.public.rateType !== RateTypeEnum.Regular) {
return product
}
/** /**
* Reset both rateCodes if one is missing to show `No prices available` for the same reason as * Reset both rateCodes if one is missing to show `No prices available` for the same reason as
* mentioned above. * mentioned above.
@@ -67,12 +73,14 @@ export const roomConfigurationSchema = z
/** /**
* When all products miss at least one rateCode (member or public), we change the status to NotAvailable * When all products miss at least one rateCode (member or public), we change the status to NotAvailable
* since we cannot as of now (31 january) guarantee the flow with missing rateCodes. * since we cannot as of now (31 january) guarantee the flow with missing rateCodes.
* Exception Special rate (Booking code rates)
* *
* TODO: (Maybe) notify somewhere that this happened * TODO: (Maybe) notify somewhere that this happened
*/ */
const allProductsMissAtLeastOneRateCode = data.products.every( const allProductsMissAtLeastOneRateCode = data.products.every(
({ productType }) => ({ productType }) =>
!productType.public.rateCode || !productType.member?.rateCode (!productType.public.rateCode || !productType.member?.rateCode) &&
productType.public.rateType === RateTypeEnum.Regular
) )
if (allProductsMissAtLeastOneRateCode) { if (allProductsMissAtLeastOneRateCode) {
data.status = AvailabilityEnum.NotAvailable data.status = AvailabilityEnum.NotAvailable

View File

@@ -3,7 +3,7 @@ import { z } from "zod"
export const rateDefinitionSchema = z.object({ export const rateDefinitionSchema = z.object({
breakfastIncluded: z.boolean(), breakfastIncluded: z.boolean(),
cancellationRule: z.string(), cancellationRule: z.string(),
cancellationText: z.string(), cancellationText: z.string().optional(),
generalTerms: z.array(z.string()), generalTerms: z.array(z.string()),
mustBeGuaranteed: z.boolean(), mustBeGuaranteed: z.boolean(),
rateCode: z.string(), rateCode: z.string(),

View File

@@ -1,13 +1,15 @@
import { create } from "zustand" import { create } from "zustand"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
interface BookingCodeFilterState { interface BookingCodeFilterState {
activeCodeFilter: string activeCodeFilter: keyof typeof BookingCodeFilterEnum
setFilter: (filter: string) => void setFilter: (filter: BookingCodeFilterEnum) => void
} }
export const useBookingCodeFilterStore = create<BookingCodeFilterState>( export const useBookingCodeFilterStore = create<BookingCodeFilterState>(
(set) => ({ (set) => ({
activeCodeFilter: "discounted", activeCodeFilter: BookingCodeFilterEnum.Discounted,
setFilter: (filter) => set({ activeCodeFilter: filter }), setFilter: (filter) => set({ activeCodeFilter: filter }),
}) })
) )

View File

@@ -9,4 +9,5 @@ export interface RoomsContainerProps {
hotelId: number hotelId: number
toDate: Date toDate: Date
hotelData: HotelData | null hotelData: HotelData | null
bookingCode?: string
} }

View File

@@ -0,0 +1,5 @@
export enum BookingCodeFilterEnum {
Discounted = "Discounted",
Regular = "Regular",
All = "All",
}

View File

@@ -0,0 +1,10 @@
export enum RateTypeEnum {
Arb = "Arb",
BonusCheque = "BonusCheque",
Company = "Company",
Promotion = "Promotion",
Redemption = "Redemption",
Regular = "Regular",
TravelAgent = "TravelAgent",
Voucher = "Voucher",
}

View File

@@ -3,6 +3,7 @@ import type { z } from "zod"
import type { import type {
getHotelsByHotelIdsAvailabilityInputSchema, getHotelsByHotelIdsAvailabilityInputSchema,
hotelsAvailabilityInputSchema, hotelsAvailabilityInputSchema,
roomsAvailabilityInputSchema,
} from "@/server/routers/hotels/input" } from "@/server/routers/hotels/input"
import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output" import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output"
import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType" import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType"
@@ -15,6 +16,9 @@ export type HotelsAvailabilityInputSchema = z.output<
export type HotelsByHotelIdsAvailabilityInputSchema = z.output< export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
typeof getHotelsByHotelIdsAvailabilityInputSchema typeof getHotelsByHotelIdsAvailabilityInputSchema
> >
export type RoomsAvailabilityInputSchema = z.output<
typeof roomsAvailabilityInputSchema
>
export type ProductType = z.output<typeof productTypeSchema> export type ProductType = z.output<typeof productTypeSchema>
export type ProductTypePrices = z.output<typeof productTypePriceSchema> export type ProductTypePrices = z.output<typeof productTypePriceSchema>