Merged in feat/SW-1356-reward-night-booking-2- (pull request #1559)

feat: SW-1356 Reward night bookingflow

* feat: SW-1356 Reward night bookingflow

* feat: SW-1356 Removed extra param booking call

* feat: SW-1356 Optimized as review comments

* feat: SW-1356 Schema validation updates

* feat: SW-1356 Fix after rebase

* feat: SW-1356 Optimised price.redemptions check

* feat: SW-1356 Updated Props naming


Approved-by: Arvid Norlin
This commit is contained in:
Hrishikesh Vaipurkar
2025-03-24 08:54:02 +00:00
parent b972679c6e
commit c5e294c7ea
57 changed files with 1113 additions and 657 deletions

View File

@@ -1,6 +1,7 @@
import { notFound, redirect } from "next/navigation" import { notFound, redirect } from "next/navigation"
import { Suspense } from "react" import { Suspense } from "react"
import { REDEMPTION } from "@/constants/booking"
import { selectRate } from "@/constants/routes/hotelReservation" import { selectRate } from "@/constants/routes/hotelReservation"
import { import {
getBreakfastPackages, getBreakfastPackages,
@@ -83,6 +84,7 @@ export default async function DetailsPage({
roomStayEndDate: booking.toDate, roomStayEndDate: booking.toDate,
roomStayStartDate: booking.fromDate, roomStayStartDate: booking.fromDate,
roomTypeCode: room.roomTypeCode, roomTypeCode: room.roomTypeCode,
redemption: booking.searchType === REDEMPTION,
}) })
if (!roomAvailability) { if (!roomAvailability) {
@@ -107,6 +109,7 @@ export default async function DetailsPage({
roomRate: { roomRate: {
memberRate: roomAvailability?.memberRate, memberRate: roomAvailability?.memberRate,
publicRate: roomAvailability.publicRate, publicRate: roomAvailability.publicRate,
redemptionRate: roomAvailability.redemptionRate,
}, },
isAvailable: isAvailable:
roomAvailability.selectedRoom.status === AvailabilityEnum.Available, roomAvailability.selectedRoom.status === AvailabilityEnum.Available,

View File

@@ -30,9 +30,7 @@ export default function Voucher() {
return ( return (
<div className={styles.optionsContainer}> <div className={styles.optionsContainer}>
<div className={styles.vouchers}> <BookingCode />
<BookingCode />
</div>
<div className={styles.options}> <div className={styles.options}>
{env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? ( {env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? (
<> <>
@@ -81,7 +79,6 @@ export function VoucherSkeleton() {
const intl = useIntl() const intl = useIntl()
const vouchers = intl.formatMessage({ id: "Code / Voucher" }) const vouchers = intl.formatMessage({ id: "Code / Voucher" })
const bonus = intl.formatMessage({ id: "Use Bonus Cheque" })
const reward = intl.formatMessage({ id: "Book Reward Night" }) const reward = intl.formatMessage({ id: "Book Reward Night" })
const form = useForm() const form = useForm()
@@ -89,7 +86,7 @@ export function VoucherSkeleton() {
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<div className={styles.optionsContainer}> <div className={styles.optionsContainer}>
<div className={styles.vouchers}> <div>
<label> <label>
<Caption type="bold" color="red" asChild> <Caption type="bold" color="red" asChild>
<span>{vouchers}</span> <span>{vouchers}</span>
@@ -98,21 +95,11 @@ export function VoucherSkeleton() {
<SkeletonShimmer width={"100%"} /> <SkeletonShimmer width={"100%"} />
</div> </div>
<div className={styles.options}> <div className={styles.options}>
{env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? null : (
<div className={styles.option}>
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<span>{bonus}</span>
</Caption>
</Checkbox>
</div>
)}
<div className={styles.option}> <div className={styles.option}>
<Checkbox name="redemption"> <SkeletonShimmer width="24px" height="24px" />
<Caption color="uiTextMediumContrast" asChild> <Caption color="uiTextMediumContrast" asChild>
<span>{reward}</span> <span>{reward}</span>
</Caption> </Caption>
</Checkbox>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -9,7 +9,7 @@ import { formId } from "@/components/HotelReservation/EnterDetails/Payment/Payme
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting" import { formatPriceWithAdditionalPrice } from "@/utils/numberFormatting"
import styles from "./bottomSheet.module.css" import styles from "./bottomSheet.module.css"
@@ -57,10 +57,12 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
> >
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption> <Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
<Subtitle> <Subtitle>
{formatPrice( {formatPriceWithAdditionalPrice(
intl, intl,
totalPrice.local.price, totalPrice.local.price,
totalPrice.local.currency totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)} )}
</Subtitle> </Subtitle>
<Caption color="baseTextHighContrast" type="underline"> <Caption color="baseTextHighContrast" type="underline">

View File

@@ -20,7 +20,10 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting" import {
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import PriceDetailsTable from "./PriceDetailsTable" import PriceDetailsTable from "./PriceDetailsTable"
@@ -170,10 +173,12 @@ export default function SummaryUI({
memberPrice.amount, memberPrice.amount,
memberPrice.currency memberPrice.currency
) )
: formatPrice( : formatPriceWithAdditionalPrice(
intl, intl,
room.roomPrice.perStay.local.price, room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)} )}
</Body> </Body>
</div> </div>
@@ -383,10 +388,12 @@ export default function SummaryUI({
</div> </div>
<div> <div>
<Body textTransform="bold" data-testid="total-price"> <Body textTransform="bold" data-testid="total-price">
{formatPrice( {formatPriceWithAdditionalPrice(
intl, intl,
totalPrice.local.price, totalPrice.local.price,
totalPrice.local.currency totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)} )}
</Body> </Body>
{totalPrice.local.regularPrice ? ( {totalPrice.local.regularPrice ? (

View File

@@ -3,17 +3,16 @@ import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./hotelPointsCard.module.css" import styles from "./hotelPointsRow.module.css"
import type { PointsCardProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps" import type { PointsRowProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps"
export default function HotelPointsCard({ export default function HotelPointsRow({
productTypePoints, pointsPerStay,
redemptionPrice, additionalPricePerStay,
}: PointsCardProps) { additionalPriceCurrency,
}: PointsRowProps) {
const intl = useIntl() const intl = useIntl()
const pointsPerStay =
productTypePoints?.localPrice.pointsPerStay ?? redemptionPrice
return ( return (
<div className={styles.poinstRow}> <div className={styles.poinstRow}>
@@ -23,14 +22,14 @@ export default function HotelPointsCard({
<Caption color="uiTextHighContrast"> <Caption color="uiTextHighContrast">
{intl.formatMessage({ id: "Points" })} {intl.formatMessage({ id: "Points" })}
</Caption> </Caption>
{productTypePoints?.localPrice.pricePerStay ? ( {additionalPricePerStay ? (
<> <>
+ +
<Subtitle type="two" color="uiTextHighContrast"> <Subtitle type="two" color="uiTextHighContrast">
{productTypePoints.localPrice.pricePerStay} {additionalPricePerStay}
</Subtitle> </Subtitle>
<Caption color="uiTextHighContrast"> <Caption color="uiTextHighContrast">
{productTypePoints.localPrice.currency} {additionalPriceCurrency}
</Caption> </Caption>
</> </>
) : null} ) : null}

View File

@@ -22,7 +22,7 @@ import { getSingleDecimal } from "@/utils/numberFormatting"
import ReadMore from "../ReadMore" import ReadMore from "../ReadMore"
import TripAdvisorChip from "../TripAdvisorChip" import TripAdvisorChip from "../TripAdvisorChip"
import HotelPointsCard from "./HotelPointsCard" import HotelPointsRow from "./HotelPointsRow"
import HotelPriceCard from "./HotelPriceCard" import HotelPriceCard from "./HotelPriceCard"
import NoPriceAvailableCard from "./NoPriceAvailableCard" import NoPriceAvailableCard from "./NoPriceAvailableCard"
import { hotelCardVariants } from "./variants" import { hotelCardVariants } from "./variants"
@@ -154,9 +154,7 @@ function HotelCard({
) : ( ) : (
<> <>
{bookingCode && ( {bookingCode && (
<span <span className={`${fullPrice ? styles.strikedText : ""}`}>
className={`${styles.bookingCode} ${fullPrice ? styles.strikedText : ""}`}
>
<PriceTagIcon height={20} width={20} /> <PriceTagIcon height={20} width={20} />
{bookingCode} {bookingCode}
</span> </span>
@@ -173,21 +171,23 @@ function HotelCard({
isMemberPrice isMemberPrice
/> />
)} )}
{price?.redemption && ( {!!price?.redemptions?.length && (
<div className={styles.pointsCard}> <div className={styles.pointsCard}>
<Caption> <Caption>
{intl.formatMessage({ id: "Available rates" })} {intl.formatMessage({ id: "Available rates" })}
</Caption> </Caption>
{/* Display rate with full points option */} {price.redemptions.map((redemption) => (
<HotelPointsCard productTypePoints={price.redemption} /> <HotelPointsRow
{/* Display rate with partial points option A */} key={redemption.rateCode}
{price.redemptionA && ( pointsPerStay={redemption.localPrice.pointsPerStay}
<HotelPointsCard productTypePoints={price.redemptionA} /> additionalPricePerStay={
)} redemption.localPrice.additionalPricePerStay
{/* Display rate with partial points option B */} }
{price.redemptionB && ( additionalPriceCurrency={
<HotelPointsCard productTypePoints={price.redemptionB} /> redemption.localPrice.additionalPriceCurrency
)} }
/>
))}
</div> </div>
)} )}
<Button <Button

View File

@@ -11,7 +11,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { isValidClientSession } from "@/utils/clientSession" import { isValidClientSession } from "@/utils/clientSession"
import HotelPointsCard from "../../HotelCard/HotelPointsCard" import HotelPointsRow from "../../HotelCard/HotelPointsRow"
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard" import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
import HotelCardDialogImage from "../HotelCardDialogImage" import HotelCardDialogImage from "../HotelCardDialogImage"
@@ -117,7 +117,7 @@ export default function ListingHotelCardDialog({
</Subtitle> </Subtitle>
)} )}
{redemptionPrice && ( {redemptionPrice && (
<HotelPointsCard redemptionPrice={redemptionPrice} /> <HotelPointsRow pointsPerStay={redemptionPrice} />
)} )}
</div> </div>
</div> </div>

View File

@@ -13,7 +13,7 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { isValidClientSession } from "@/utils/clientSession" import { isValidClientSession } from "@/utils/clientSession"
import HotelPointsCard from "../../HotelCard/HotelPointsCard" import HotelPointsRow from "../../HotelCard/HotelPointsRow"
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard" import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
import HotelCardDialogImage from "../HotelCardDialogImage" import HotelCardDialogImage from "../HotelCardDialogImage"
@@ -132,7 +132,7 @@ export default function StandaloneHotelCardDialog({
</Subtitle> </Subtitle>
)} )}
{redemptionPrice && ( {redemptionPrice && (
<HotelPointsCard redemptionPrice={redemptionPrice} /> <HotelPointsRow pointsPerStay={redemptionPrice} />
)} )}
</div> </div>
<Button <Button

View File

@@ -20,7 +20,7 @@ export default function HotelCardDialogListing({
}: HotelCardDialogListingProps) { }: HotelCardDialogListingProps) {
const intl = useIntl() const intl = useIntl()
const isRedemption = hotels?.find( const isRedemption = hotels?.find(
(hotel) => hotel.availability.productType?.redemption (hotel) => hotel.availability.productType?.redemptions?.length
) )
const currencyValue = isRedemption const currencyValue = isRedemption
? intl.formatMessage({ id: "Points" }) ? intl.formatMessage({ id: "Points" })

View File

@@ -11,6 +11,9 @@ export function getHotelPins(
return hotels.map(({ availability, hotel, additionalData }) => { return hotels.map(({ availability, hotel, additionalData }) => {
const productType = availability.productType const productType = availability.productType
const redemptionRate = productType?.redemptions?.find(
(r) => r?.localPrice.pointsPerStay
)
return { return {
coordinates: { coordinates: {
lat: hotel.location.latitude, lat: hotel.location.latitude,
@@ -19,8 +22,7 @@ export function getHotelPins(
name: hotel.name, name: hotel.name,
publicPrice: productType?.public?.localPrice.pricePerNight ?? null, publicPrice: productType?.public?.localPrice.pricePerNight ?? null,
memberPrice: productType?.member?.localPrice.pricePerNight ?? null, memberPrice: productType?.member?.localPrice.pricePerNight ?? null,
redemptionPrice: redemptionPrice: redemptionRate?.localPrice.pointsPerStay ?? null,
productType?.redemption?.localPrice.pointsPerNight ?? null,
rateType: rateType:
productType?.public?.rateType ?? productType?.member?.rateType ?? null, productType?.public?.rateType ?? productType?.member?.rateType ?? null,
currency: currency:

View File

@@ -5,6 +5,9 @@ function getPricePerNight(hotel: HotelResponse): number {
return ( return (
hotel.availability.productType?.member?.localPrice?.pricePerNight ?? hotel.availability.productType?.member?.localPrice?.pricePerNight ??
hotel.availability.productType?.public?.localPrice?.pricePerNight ?? hotel.availability.productType?.public?.localPrice?.pricePerNight ??
hotel.availability.productType?.redemptions?.find(
(r) => r?.localPrice.pointsPerStay
)?.localPrice?.pointsPerStay ??
Infinity Infinity
) )
} }
@@ -49,7 +52,7 @@ export function getSortedHotels({
(hotel.availability.productType?.public?.rateType?.toLowerCase() !== (hotel.availability.productType?.public?.rateType?.toLowerCase() !==
"regular" || "regular" ||
hotel.availability.productType?.member?.rateType?.toLowerCase() !== hotel.availability.productType?.member?.rateType?.toLowerCase() !==
"regular") && "regular") &&
!!hotel.availability.productType !!hotel.availability.productType
) )
const regularHotels = hotels.filter( const regularHotels = hotels.filter(

View File

@@ -77,6 +77,12 @@ async function fetchAvailableHotels(input: AvailabilityInput) {
return await serverClient().hotel.availability.hotelsByCity(input) return await serverClient().hotel.availability.hotelsByCity(input)
} }
async function fetchAvailableHotelsWithRedemption(input: AvailabilityInput) {
return await serverClient().hotel.availability.hotelsByCityWithRedemption(
input
)
}
async function fetchBookingCodeAvailableHotels(input: AvailabilityInput) { async function fetchBookingCodeAvailableHotels(input: AvailabilityInput) {
return await serverClient().hotel.availability.hotelsByCityWithBookingCode( return await serverClient().hotel.availability.hotelsByCityWithBookingCode(
input input
@@ -192,6 +198,22 @@ export async function getHotels(
}) })
}) })
) )
} else if (redemption) {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(
async (room) =>
await fetchAvailableHotelsWithRedemption({
adults: room.adults,
children: room.childrenInRoom
? generateChildrenString(room.childrenInRoom)
: undefined,
cityId: city.id,
redemption,
roomStayEndDate: booking.toDate,
roomStayStartDate: booking.fromDate,
})
)
)
} else { } else {
availableHotelsResponse = await Promise.allSettled( availableHotelsResponse = await Promise.allSettled(
booking.rooms.map( booking.rooms.map(

View File

@@ -18,7 +18,10 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting" import {
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import PriceDetailsTable from "./PriceDetailsTable" import PriceDetailsTable from "./PriceDetailsTable"
@@ -151,10 +154,12 @@ export default function Summary({
<div className={styles.entry}> <div className={styles.entry}>
<Body color="uiTextHighContrast">{room.roomType}</Body> <Body color="uiTextHighContrast">{room.roomType}</Body>
<Body color={showDiscounted ? "red" : "uiTextHighContrast"}> <Body color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPrice( {formatPriceWithAdditionalPrice(
intl, intl,
room.roomPrice.perStay.local.price, room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)} )}
</Body> </Body>
</div> </div>
@@ -269,10 +274,12 @@ export default function Summary({
textTransform="bold" textTransform="bold"
data-testid="total-price" data-testid="total-price"
> >
{formatPrice( {formatPriceWithAdditionalPrice(
intl, intl,
totalPrice.local.price, totalPrice.local.price,
totalPrice.local.currency totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)} )}
</Body> </Body>
{booking.bookingCode && totalPrice.local.regularPrice && ( {booking.bookingCode && totalPrice.local.regularPrice && (

View File

@@ -7,7 +7,7 @@ import { useRatesStore } from "@/stores/select-rate"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting" import { formatPriceWithAdditionalPrice } from "@/utils/numberFormatting"
import Summary from "./Summary" import Summary from "./Summary"
@@ -144,11 +144,16 @@ export default function MobileSummary({
className={styles.priceDetailsButton} className={styles.priceDetailsButton}
> >
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption> <Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}> <Subtitle
{formatPrice( color={showDiscounted ? "red" : "uiTextHighContrast"}
className={styles.wrappedText}
>
{formatPriceWithAdditionalPrice(
intl, intl,
totalPriceToShow.local.price, totalPriceToShow.local.price,
totalPriceToShow.local.currency totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)} )}
</Subtitle> </Subtitle>
<Caption color="baseTextHighContrast" type="underline"> <Caption color="baseTextHighContrast" type="underline">

View File

@@ -67,6 +67,10 @@
z-index: 10; z-index: 10;
} }
.wrappedText {
white-space: normal;
}
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.bottomSheet { .bottomSheet {
padding: var(--Spacing-x2) 0 var(--Spacing-x7); padding: var(--Spacing-x2) 0 var(--Spacing-x7);

View File

@@ -3,6 +3,7 @@ import { useRouter } from "next/navigation"
import { useState, useTransition } from "react" import { useState, useTransition } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { REDEMPTION } from "@/constants/booking"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
@@ -13,13 +14,20 @@ import Body from "@/components/TempDesignSystem/Text/Body"
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"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting" import {
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import MobileSummary from "./MobileSummary" import MobileSummary from "./MobileSummary"
import { calculateTotalPrice } from "./utils" import { calculateTotalPrice } from "./utils"
import styles from "./rateSummary.module.css" import styles from "./rateSummary.module.css"
import {
PointsPriceSchema,
type Price,
} from "@/types/components/hotelReservation/price"
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
@@ -27,6 +35,8 @@ import { RateTypeEnum } from "@/types/enums/rateType"
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
const { const {
bookingCode,
isRedemption,
bookingRooms, bookingRooms,
dates, dates,
petRoomPackage, petRoomPackage,
@@ -34,6 +44,8 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
roomsAvailability, roomsAvailability,
searchParams, searchParams,
} = useRatesStore((state) => ({ } = useRatesStore((state) => ({
bookingCode: state.booking.bookingCode,
isRedemption: state.booking.searchType === REDEMPTION,
bookingRooms: state.booking.rooms, bookingRooms: state.booking.rooms,
dates: { dates: {
checkInDate: state.booking.fromDate, checkInDate: state.booking.fromDate,
@@ -58,7 +70,6 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
const checkInDate = new Date(dates.checkInDate) const checkInDate = new Date(dates.checkInDate)
const checkOutDate = new Date(dates.checkOutDate) const checkOutDate = new Date(dates.checkOutDate)
const nights = dt(checkOutDate).diff(dt(checkInDate), "days") const nights = dt(checkOutDate).diff(dt(checkInDate), "days")
const bookingCode = params.get("bookingCode")
const totalNights = intl.formatMessage( const totalNights = intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" }, { id: "{totalNights, plural, one {# night} other {# nights}}" },
@@ -128,11 +139,11 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
) )
const showDiscounted = isUserLoggedIn || isBookingCodeRate const showDiscounted = isUserLoggedIn || isBookingCodeRate
const totalPriceToShow = calculateTotalPrice( // In case of reward night (redemption) only single room booking is supported by business rules
rateSummary, const totalPriceToShow: Price =
isUserLoggedIn, isRedemption && rateSummary[0].redemption
petRoomPackage ? PointsPriceSchema.parse(rateSummary[0].redemption)
) : calculateTotalPrice(rateSummary, isUserLoggedIn, petRoomPackage)
return ( return (
<form action={`details?${params}`} method="GET" onSubmit={handleSubmit}> <form action={`details?${params}`} method="GET" onSubmit={handleSubmit}>
@@ -231,10 +242,12 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
color={showDiscounted ? "red" : "uiTextHighContrast"} color={showDiscounted ? "red" : "uiTextHighContrast"}
textAlign="right" textAlign="right"
> >
{formatPrice( {formatPriceWithAdditionalPrice(
intl, intl,
totalPriceToShow.local.price, totalPriceToShow.local.price,
totalPriceToShow.local.currency totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)} )}
</Subtitle> </Subtitle>
{bookingCode && totalPriceToShow.local.regularPrice && ( {bookingCode && totalPriceToShow.local.regularPrice && (
@@ -270,10 +283,12 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
{intl.formatMessage({ id: "Total price" })} {intl.formatMessage({ id: "Total price" })}
</Caption> </Caption>
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}> <Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPrice( {formatPriceWithAdditionalPrice(
intl, intl,
totalPriceToShow.local.price, totalPriceToShow.local.price,
totalPriceToShow.local.currency totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)} )}
</Subtitle> </Subtitle>
<Footnote <Footnote

View File

@@ -19,7 +19,7 @@ export default function PriceList({
publicPrice = {}, publicPrice = {},
memberPrice = {}, memberPrice = {},
petRoomPackage, petRoomPackage,
rateTitle, rateName,
}: PriceListProps) { }: PriceListProps) {
const intl = useIntl() const intl = useIntl()
const { isMainRoom } = useRoomContext() const { isMainRoom } = useRoomContext()
@@ -69,14 +69,14 @@ export default function PriceList({
) )
const priceLabelColor = const priceLabelColor =
rateTitle && !memberLocalPrice ? "red" : "uiTextHighContrast" rateName && !memberLocalPrice ? "red" : "uiTextHighContrast"
return ( return (
<dl className={styles.priceList}> <dl className={styles.priceList}>
{isUserLoggedIn && isMainRoom && memberLocalPrice ? null : ( {isUserLoggedIn && isMainRoom && memberLocalPrice ? null : (
<div className={styles.priceRow}> <div className={styles.priceRow}>
<dt> <dt>
{rateTitle ? null : ( {rateName ? null : (
<Caption <Caption
type="bold" type="bold"
color={ color={

View File

@@ -8,6 +8,11 @@
align-items: baseline; align-items: baseline;
} }
.pointsRow {
justify-content: flex-start;
gap: var(--Spacing-x-half);
}
.priceTable { .priceTable {
margin: 0; margin: 0;
} }

View File

@@ -53,6 +53,16 @@ input[type="radio"]:checked + .card .checkIcon {
right: -10px; right: -10px;
} }
.title {
display: flex;
height: 24px;
padding: var(--Spacing-x-half) var(--Spacing-x1);
justify-content: center;
align-items: center;
gap: var(--Spacing-x1);
align-self: stretch;
}
.header { .header {
display: flex; display: flex;
gap: var(--Spacing-x-half); gap: var(--Spacing-x-half);

View File

@@ -27,7 +27,7 @@ export default function FlexibilityOption({
roomType, roomType,
roomTypeCode, roomTypeCode,
title, title,
rateTitle, rateName,
}: FlexibilityOptionProps) { }: FlexibilityOptionProps) {
const intl = useIntl() const intl = useIntl()
const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn) const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn)
@@ -104,9 +104,9 @@ export default function FlexibilityOption({
value={rate.rateCode} value={rate.rateCode}
/> />
<div className={styles.card}> <div className={styles.card}>
{rateTitle ? ( {rateName ? (
<div className={styles.header}> <div className={styles.header}>
<Caption>{rateTitle}</Caption> <Caption>{rateName}</Caption>
</div> </div>
) : null} ) : null}
<div className={styles.header}> <div className={styles.header}>
@@ -120,8 +120,8 @@ export default function FlexibilityOption({
/> />
</Button> </Button>
} }
title={rateTitle ? rateTitle : title} title={rateName ? rateName : title}
subtitle={rateTitle ? `${title} (${paymentTerm})` : paymentTerm} subtitle={rateName ? `${title} (${paymentTerm})` : paymentTerm}
> >
<div className={styles.terms}> <div className={styles.terms}>
{priceInformation?.map((info) => ( {priceInformation?.map((info) => (
@@ -150,7 +150,7 @@ export default function FlexibilityOption({
memberPrice={product.member} memberPrice={product.member}
petRoomPackage={petRoomPackage} petRoomPackage={petRoomPackage}
publicPrice={product.public} publicPrice={product.public}
rateTitle={rateTitle} rateName={rateName}
/> />
<div className={styles.checkIcon}> <div className={styles.checkIcon}>

View File

@@ -0,0 +1,53 @@
import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./pointsList.module.css"
import type { ProductTypePoints } from "@/types/trpc/routers/hotel/availability"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
export default function PointsList({
product,
handleSelect,
redemptions,
}: {
product: Product
handleSelect: (product: Product, selectedRateCode: string) => void
redemptions: ProductTypePoints[]
}) {
const intl = useIntl()
return (
<div className={styles.pointsList}>
{redemptions.map((redemption) => (
<label key={redemption.rateCode} className={styles.pointsRow}>
{/* ToDo Handle with appropriate Input or Radio component in UI implementation ticket */}
<input
name="redepmtionRate"
onChange={() => {
handleSelect(product, redemption.rateCode)
}}
type="radio"
value={redemption.rateCode}
/>
<Subtitle>{redemption.localPrice.pointsPerStay}</Subtitle>
<Caption>
{" "}
{intl.formatMessage({ id: "Points" })}
{redemption.localPrice.additionalPricePerStay &&
redemption.localPrice.additionalPriceCurrency
? ` + ${formatPrice(
intl,
redemption.localPrice.additionalPricePerStay,
redemption.localPrice.additionalPriceCurrency
)}`
: null}
</Caption>
</label>
))}
</div>
)
}

View File

@@ -0,0 +1,10 @@
.pointsList {
margin: 0;
}
.pointsRow {
display: flex;
align-items: baseline;
justify-content: flex-start;
gap: var(--Spacing-x-half);
}

View File

@@ -0,0 +1,125 @@
"use client"
import { useIntl } from "react-intl"
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import Label from "@/components/TempDesignSystem/Form/Label"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import PointsList from "./PointsList"
import styles from "../FlexibilityOption/flexibilityOption.module.css"
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
export default function FlexibilityOptionPoints({
features,
paymentTerm,
priceInformation,
product,
roomType,
roomTypeCode,
title,
// Reward night rate tile obtianed from the ratedefinition
rateName,
}: FlexibilityOptionProps) {
const intl = useIntl()
const rewardNightTitle =
rateName ?? intl.formatMessage({ id: "Reward night" })
const {
actions: { selectRateRedemption },
} = useRoomContext()
if (!product?.redemptions?.length) {
return (
<div className={styles.noPricesCard}>
<div className={styles.header}>
<InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" />
<div className={styles.priceType}>
<Caption>{title}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div>
</div>
<Label size="regular" className={styles.noPricesLabel}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "No prices available" })}
</Caption>
</Label>
</div>
)
}
function handleSelect(product: Product, selectedRateCode?: string) {
selectRateRedemption(
{
features,
product,
roomType,
roomTypeCode,
},
selectedRateCode
)
}
return (
<div>
<div className={styles.title}>
<Caption>
{rewardNightTitle}
{" ∙ "}
{intl.formatMessage({ id: "Breakfast included" })}
</Caption>
</div>
<div className={styles.card}>
<div className={styles.header}>
<Modal
trigger={
<Button intent="text">
<InfoCircleIcon
width={16}
height={16}
color="uiTextMediumContrast"
/>
</Button>
}
title={rewardNightTitle}
subtitle={`${title} ${paymentTerm}`}
>
{priceInformation?.length ? (
<div className={styles.terms}>
{priceInformation.map((info) => (
<Body
key={info}
color="uiTextHighContrast"
className={styles.termsText}
>
<CheckIcon
color="uiSemanticSuccess"
width={20}
height={20}
className={styles.termsIcon}
/>
{info}
</Body>
))}
</div>
) : null}
</Modal>
<div className={styles.priceType}>
<Caption color="uiTextHighContrast">{title}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div>
</div>
<PointsList
product={product}
handleSelect={handleSelect}
redemptions={product.redemptions}
/>
</div>
</div>
)
}

View File

@@ -4,6 +4,7 @@ import { useSearchParams } from "next/navigation"
import { createElement } from "react" import { createElement } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { REDEMPTION } from "@/constants/booking"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek" import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
@@ -18,6 +19,7 @@ import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import { cardVariants } from "./cardVariants" import { cardVariants } from "./cardVariants"
import FlexibilityOption from "./FlexibilityOption" import FlexibilityOption from "./FlexibilityOption"
import FlexibilityOptionPoints from "./FlexibilityOptionPoints"
import RoomSize from "./RoomSize" import RoomSize from "./RoomSize"
import styles from "./roomCard.module.css" import styles from "./roomCard.module.css"
@@ -72,6 +74,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const searchParams = useSearchParams() const searchParams = useSearchParams()
const bookingCode = searchParams.get("bookingCode") const bookingCode = searchParams.get("bookingCode")
const isRedemption = searchParams.get("searchtype") === REDEMPTION
const { hotelId, hotelType, isUserLoggedIn, petRoomPackage, roomCategories } = const { hotelId, hotelType, isUserLoggedIn, petRoomPackage, roomCategories } =
useRatesStore((state) => ({ useRatesStore((state) => ({
@@ -152,6 +155,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
* Get terms and rate title from the rate definitions when booking code rate * Get terms and rate title from the rate definitions when booking code rate
* or public promotion is in play. Returns undefined when product is not available * or public promotion is in play. Returns undefined when product is not available
* *
* In case of redemption it will always return first redemption as terms
* and title are same for all various redemption rates
*
* @param product - Either public or member product type * @param product - Either public or member product type
* @param rateDefinitions - List of rate definitions * @param rateDefinitions - List of rate definitions
* @returns RateDefinition | undefined * @returns RateDefinition | undefined
@@ -160,10 +166,18 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
product: Product, product: Product,
rateDefinitions: RateDefinition[] rateDefinitions: RateDefinition[]
) { ) {
return rateDefinitions.find((rateDefinition) => let rateCode = ""
isUserLoggedIn && product.member && isMainRoom if (isUserLoggedIn && product.member && isMainRoom) {
? rateDefinition.rateCode === product.member?.rateCode rateCode = product.member.rateCode
: rateDefinition.rateCode === product.public?.rateCode } else if (product.public?.rateCode) {
rateCode = product.public.rateCode
} else if (product.redemptions?.length) {
// In case of redemption there will be same rate terms and title
// irrespective of ratecodes
return rateDefinitions[0]
}
return rateDefinitions.find(
(rateDefinition) => rateDefinition.rateCode === rateCode
) )
} }
@@ -263,7 +277,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
) : ( ) : (
<> <>
<span> <span>
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption> {isRedemption ? null : (
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption>
)}
{bookingCode ? ( {bookingCode ? (
<span className={!isBookingCodeRate ? styles.strikedText : ""}> <span className={!isBookingCodeRate ? styles.strikedText : ""}>
<PriceTagIcon /> <PriceTagIcon />
@@ -275,29 +291,30 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const rateTitle = getRateTitle(product.rate) const rateTitle = getRateTitle(product.rate)
const isAvailable = const isAvailable =
product.public || product.public ||
(product.member && isUserLoggedIn && isMainRoom) (product.member && isUserLoggedIn && isMainRoom) ||
product.redemptions?.length
const rateDefinition = getRateDefinition( const rateDefinition = getRateDefinition(
product, product,
roomAvailability.rateDefinitions roomAvailability.rateDefinitions
) )
return ( const props = {
<FlexibilityOption features: roomConfiguration.features,
key={product.rate} paymentTerm: product.isFlex ? payLater : payNow,
features={roomConfiguration.features} petRoomPackage: petRoomPackageSelected,
paymentTerm={product.isFlex ? payLater : payNow} priceInformation: rateDefinition?.generalTerms,
petRoomPackage={petRoomPackageSelected} product: isAvailable ? product : undefined,
priceInformation={rateDefinition?.generalTerms} roomType: roomConfiguration.roomType,
product={isAvailable ? product : undefined} roomTypeCode: roomConfiguration.roomTypeCode,
roomType={roomConfiguration.roomType} title: rateTitle,
roomTypeCode={roomConfiguration.roomTypeCode} rateName:
title={rateTitle} isBookingCodeRate || isRedemption
rateTitle={ ? rateDefinition?.title
product.public && : undefined,
product.public?.rateType !== RateTypeEnum.Regular }
? rateDefinition?.title return isRedemption ? (
: undefined <FlexibilityOptionPoints key={product.rate} {...props} />
} ) : (
/> <FlexibilityOption key={product.rate} {...props} />
) )
})} })}
</> </>

View File

@@ -1,4 +1,5 @@
"use client" "use client"
import { REDEMPTION } from "@/constants/booking"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
@@ -24,6 +25,9 @@ export function RoomsContainer({
const fromDateString = dt(fromDate).format("YYYY-MM-DD") const fromDateString = dt(fromDate).format("YYYY-MM-DD")
const toDateString = dt(toDate).format("YYYY-MM-DD") const toDateString = dt(toDate).format("YYYY-MM-DD")
const redemption = booking.searchType
? booking.searchType === REDEMPTION
: undefined
const { data: roomsAvailability, isPending: isLoadingAvailability } = const { data: roomsAvailability, isPending: isLoadingAvailability } =
useRoomsAvailability( useRoomsAvailability(
@@ -33,7 +37,8 @@ export function RoomsContainer({
toDateString, toDateString,
lang, lang,
childArray, childArray,
booking.bookingCode booking.bookingCode,
redemption
) )
const { data: packages, isPending: isLoadingPackages } = useHotelPackages( const { data: packages, isPending: isLoadingPackages } = useHotelPackages(

View File

@@ -11,9 +11,10 @@ export function useRoomsAvailability(
toDateString: string, toDateString: string,
lang: Lang, lang: Lang,
childArray: ChildrenInRoom, childArray: ChildrenInRoom,
bookingCode?: string bookingCode?: string,
redemption?: boolean
) { ) {
return trpc.hotel.availability.roomsCombinedAvailability.useQuery({ const params = {
adultsCount, adultsCount,
bookingCode, bookingCode,
childArray, childArray,
@@ -21,7 +22,17 @@ export function useRoomsAvailability(
lang, lang,
roomStayEndDate: toDateString, roomStayEndDate: toDateString,
roomStayStartDate: fromDateString, roomStayStartDate: fromDateString,
}) redemption,
}
const roomsAvailability = redemption
? trpc.hotel.availability.roomsCombinedAvailabilityWithRedemption.useQuery(
params
)
: trpc.hotel.availability.roomsCombinedAvailability.useQuery(params)
return roomsAvailability
} }
export function useHotelPackages( export function useHotelPackages(

View File

@@ -648,6 +648,7 @@
"Restaurants": "Restauranter", "Restaurants": "Restauranter",
"Restaurants & Bars": "Restaurants & Bars", "Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Gentag den nye adgangskode", "Retype new password": "Gentag den nye adgangskode",
"Reward night": "Bonusnat",
"Room": "Værelse", "Room": "Værelse",
"Room & Terms": "Værelse & Vilkår", "Room & Terms": "Værelse & Vilkår",
"Room amenities": "Værelsesfaciliteter", "Room amenities": "Værelsesfaciliteter",

View File

@@ -647,6 +647,7 @@
"Restaurants": "Restaurants", "Restaurants": "Restaurants",
"Restaurants & Bars": "Restaurants & Bars", "Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Neues Passwort erneut eingeben", "Retype new password": "Neues Passwort erneut eingeben",
"Reward night": "Bonusnacht",
"Room": "Zimmer", "Room": "Zimmer",
"Room & Terms": "Zimmer & Bedingungen", "Room & Terms": "Zimmer & Bedingungen",
"Room amenities": "Zimmerausstattung", "Room amenities": "Zimmerausstattung",

View File

@@ -646,6 +646,7 @@
"Restaurants": "Restaurants", "Restaurants": "Restaurants",
"Restaurants & Bars": "Restaurants & Bars", "Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Retype new password", "Retype new password": "Retype new password",
"Reward night": "Reward night",
"Room": "Room", "Room": "Room",
"Room & Terms": "Room & Terms", "Room & Terms": "Room & Terms",
"Room amenities": "Room amenities", "Room amenities": "Room amenities",

View File

@@ -646,6 +646,7 @@
"Restaurants": "Ravintolat", "Restaurants": "Ravintolat",
"Restaurants & Bars": "Restaurants & Bars", "Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Kirjoita uusi salasana uudelleen", "Retype new password": "Kirjoita uusi salasana uudelleen",
"Reward night": "Palkintoyö",
"Room": "Huone", "Room": "Huone",
"Room & Terms": "Huone & Ehdot", "Room & Terms": "Huone & Ehdot",
"Room amenities": "Huoneen mukavuudet", "Room amenities": "Huoneen mukavuudet",

View File

@@ -645,6 +645,7 @@
"Restaurants": "Restauranter", "Restaurants": "Restauranter",
"Restaurants & Bars": "Restaurants & Bars", "Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Skriv inn nytt passord på nytt", "Retype new password": "Skriv inn nytt passord på nytt",
"Reward night": "Bonusnatt",
"Room": "Rom", "Room": "Rom",
"Room & Terms": "Rom & Vilkår", "Room & Terms": "Rom & Vilkår",
"Room amenities": "Romfasiliteter", "Room amenities": "Romfasiliteter",

View File

@@ -645,6 +645,7 @@
"Restaurants": "Restauranger", "Restaurants": "Restauranger",
"Restaurants & Bars": "Restaurants & Bars", "Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Upprepa nytt lösenord", "Retype new password": "Upprepa nytt lösenord",
"Reward night": "Bonusnatt",
"Room": "Rum", "Room": "Rum",
"Room & Terms": "Rum & Villkor", "Room & Terms": "Rum & Villkor",
"Room amenities": "Bekvämligheter på rummet", "Room amenities": "Bekvämligheter på rummet",

View File

@@ -88,7 +88,11 @@ export const getSelectedRoomAvailability = cache(
function getMemoizedSelectedRoomAvailability( function getMemoizedSelectedRoomAvailability(
input: GetSelectedRoomAvailabilityInput input: GetSelectedRoomAvailabilityInput
) { ) {
return serverClient().hotel.availability.room(input) if (input.redemption) {
return serverClient().hotel.availability.roomWithRedemption(input)
} else {
return serverClient().hotel.availability.room(input)
}
} }
) )

View File

@@ -19,6 +19,9 @@ export default function RoomProvider({
) )
const selectFilter = useRatesStore((state) => state.actions.selectFilter(idx)) const selectFilter = useRatesStore((state) => state.actions.selectFilter(idx))
const selectRate = useRatesStore((state) => state.actions.selectRate(idx)) const selectRate = useRatesStore((state) => state.actions.selectRate(idx))
const selectRateRedemption = useRatesStore((state) =>
state.actions.selectRateRedemption(idx)
)
const roomNr = idx + 1 const roomNr = idx + 1
return ( return (
<RoomContext.Provider <RoomContext.Provider
@@ -29,6 +32,7 @@ export default function RoomProvider({
modifyRate, modifyRate,
selectFilter, selectFilter,
selectRate, selectRate,
selectRateRedemption,
}, },
isActiveRoom: activeRoom === idx, isActiveRoom: activeRoom === idx,
isMainRoom: roomNr === 1, isMainRoom: roomNr === 1,

View File

@@ -17,6 +17,7 @@ const roomsSchema = z
) )
.default([]), .default([]),
rateCode: z.string(), rateCode: z.string(),
redemptionCode: z.string().optional(),
roomTypeCode: z.coerce.string(), roomTypeCode: z.coerce.string(),
guest: z.object({ guest: z.object({
becomeMember: z.boolean(), becomeMember: z.boolean(),

View File

@@ -45,6 +45,7 @@ export const roomsCombinedAvailabilityInputSchema = z.object({
rateCode: z.string().optional(), rateCode: z.string().optional(),
roomStayEndDate: z.string(), roomStayEndDate: z.string(),
roomStayStartDate: z.string(), roomStayStartDate: z.string(),
redemption: z.boolean().optional(),
}) })
export const selectedRoomAvailabilityInputSchema = z.object({ export const selectedRoomAvailabilityInputSchema = z.object({
@@ -59,6 +60,7 @@ export const selectedRoomAvailabilityInputSchema = z.object({
counterRateCode: z.string().optional(), counterRateCode: z.string().optional(),
packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(), packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(),
lang: z.nativeEnum(Lang).optional(), lang: z.nativeEnum(Lang).optional(),
redemption: z.boolean().optional(),
}) })
export type GetSelectedRoomAvailabilityInput = z.input< export type GetSelectedRoomAvailabilityInput = z.input<

View File

@@ -151,7 +151,7 @@ function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
return statusLookup[a.status] - statusLookup[b.status] return statusLookup[a.status] - statusLookup[b.status]
} }
export const roomsCombinedAvailabilitySchema = z const baseRoomsCombinedAvailabilitySchema = z
.object({ .object({
data: z.object({ data: z.object({
attributes: z.object({ attributes: z.object({
@@ -204,83 +204,86 @@ export const roomsCombinedAvailabilitySchema = z
type: z.string().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 function transformRoomConfigs({
.map((room) => { data: { attributes },
if (room.products.length) { }: typeof baseRoomsCombinedAvailabilitySchema._type) {
room.breakfastIncludedInAllRatesMember = room.products.every( const rateDefinitions = attributes.rateDefinitions
(product) => const cancellationRuleLookup = rateDefinitions.reduce((acc, val) => {
everyRateHasBreakfastIncluded(product, rateDefinitions, "member") // @ts-expect-error - index of cancellationRule TS
) acc[val.rateCode] = cancellationRules[val.cancellationRule]
room.breakfastIncludedInAllRatesPublic = room.products.every( return acc
(product) => }, {})
everyRateHasBreakfastIncluded(product, rateDefinitions, "public")
)
room.products = room.products.map((product) => { const roomConfigurations = attributes.roomConfigurations
const publicRate = product.public .map((room) => {
if (publicRate?.rateCode) { if (room.products.length) {
const publicRateDefinition = rateDefinitions.find( room.breakfastIncludedInAllRatesMember = room.products.every(
(rateDefinition) => (product) =>
rateDefinition.rateCode === publicRate.rateCode everyRateHasBreakfastIncluded(product, rateDefinitions, "member")
) )
if (publicRateDefinition) { room.breakfastIncludedInAllRatesPublic = room.products.every(
const rate = getRate(publicRateDefinition) (product) =>
if (rate) { everyRateHasBreakfastIncluded(product, rateDefinitions, "public")
product.rate = rate )
if (rate === "flex") {
product.isFlex = true 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 const memberRate = product.member
if (memberRate?.rateCode) { if (memberRate?.rateCode) {
const memberRateDefinition = rateDefinitions.find( const memberRateDefinition = rateDefinitions.find(
(rate) => rate.rateCode === memberRate.rateCode (rate) => rate.rateCode === memberRate.rateCode
) )
if (memberRateDefinition) { if (memberRateDefinition) {
const rate = getRate(memberRateDefinition) const rate = getRate(memberRateDefinition)
if (rate) { if (rate) {
product.rate = rate product.rate = rate
if (rate === "flex") { if (rate === "flex") {
product.isFlex = true product.isFlex = true
}
} }
} }
} }
}
return product return product
}) })
// CancellationRule is the same for public and member per product // CancellationRule is the same for public and member per product
// Sorting to guarantee order based on rate // Sorting to guarantee order based on rate
room.products = room.products.sort( room.products = room.products.sort(
(a, b) => (a, b) =>
// @ts-expect-error - index // @ts-expect-error - index
cancellationRuleLookup[a.public?.rateCode || a.member?.rateCode] - cancellationRuleLookup[a.public?.rateCode || a.member?.rateCode] -
// @ts-expect-error - index // @ts-expect-error - index
cancellationRuleLookup[b.public?.rateCode || b.member?.rateCode] cancellationRuleLookup[b.public?.rateCode || b.member?.rateCode]
) )
} }
return room return room
}) })
.sort(sortRoomConfigs) .sort(sortRoomConfigs)
return { return {
...attributes, ...attributes,
roomConfigurations, roomConfigurations,
} }
}) }
export const roomsAvailabilitySchema = z export const roomsAvailabilitySchema = z
.object({ .object({
@@ -298,85 +301,30 @@ export const roomsAvailabilitySchema = z
type: z.string().optional(), type: z.string().optional(),
}), }),
}) })
.transform(({ data: { attributes } }) => { .transform(transformRoomConfigs)
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 export const roomsCombinedAvailabilitySchema =
.map((room) => { baseRoomsCombinedAvailabilitySchema.transform(transformRoomConfigs)
if (room.products.length) {
room.breakfastIncludedInAllRatesMember = room.products.every(
(product) =>
everyRateHasBreakfastIncluded(product, rateDefinitions, "member")
)
room.breakfastIncludedInAllRatesPublic = room.products.every(
(product) =>
everyRateHasBreakfastIncluded(product, rateDefinitions, "public")
)
export const redemptionRoomsCombinedAvailabilitySchema =
baseRoomsCombinedAvailabilitySchema.transform((data) => {
// In Redemption, rates are always Flex terms
data.data.attributes.roomConfigurations =
data.data.attributes.roomConfigurations
.map((room) => {
room.products = room.products.map((product) => { room.products = room.products.map((product) => {
const publicRate = product.public product.rate = "flex"
if (publicRate?.rateCode) { product.isFlex = true
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 return product
}) })
return room
})
.sort(
// @ts-expect-error - array indexing
(a, b) => statusLookup[a.status] - statusLookup[b.status]
)
// CancellationRule is the same for public and member per product return transformRoomConfigs(data)
// 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(
// @ts-expect-error - array indexing
(a, b) => statusLookup[a.status] - statusLookup[b.status]
)
return {
...attributes,
roomConfigurations,
}
}) })
export const ratesSchema = z.array(rateSchema) export const ratesSchema = z.array(rateSchema)

View File

@@ -6,6 +6,7 @@ import { badRequestError } from "@/server/errors/trpc"
import { import {
contentStackBaseWithServiceProcedure, contentStackBaseWithServiceProcedure,
protectedProcedure, protectedProcedure,
protectedServcieProcedure,
publicProcedure, publicProcedure,
router, router,
safeProtectedServiceProcedure, safeProtectedServiceProcedure,
@@ -50,6 +51,7 @@ import {
hotelSchema, hotelSchema,
packagesSchema, packagesSchema,
ratesSchema, ratesSchema,
redemptionRoomsCombinedAvailabilitySchema,
roomsAvailabilitySchema, roomsAvailabilitySchema,
roomsCombinedAvailabilitySchema, roomsCombinedAvailabilitySchema,
} from "./output" } from "./output"
@@ -72,9 +74,12 @@ import type { HotelDataWithUrl } from "@/types/hotel"
import type { import type {
HotelsAvailabilityInputSchema, HotelsAvailabilityInputSchema,
HotelsByHotelIdsAvailabilityInputSchema, HotelsByHotelIdsAvailabilityInputSchema,
RoomsCombinedAvailabilityInputSchema,
SelectedRoomAvailabilitySchema,
} 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"
import type { Lang } from "@/constants/routes/hotelReservation"
export const getHotel = cache( export const getHotel = cache(
async (input: HotelInput, serviceToken: string) => { async (input: HotelInput, serviceToken: string) => {
@@ -467,6 +472,346 @@ export const getHotelsAvailabilityByHotelIds = async (
) )
} }
async function getRoomsCombinedAvailability(
input: RoomsCombinedAvailabilityInputSchema,
token: string // Either service token or user access token in case of redemption search
) {
const { lang } = input
const apiLang = toApiLang(lang)
const {
adultsCount,
bookingCode,
childArray,
hotelId,
rateCode,
roomStayEndDate,
roomStayStartDate,
redemption,
} = input
const metricsData = {
hotelId,
roomStayStartDate,
roomStayEndDate,
adultsCount,
childArray: childArray ? JSON.stringify(childArray) : undefined,
bookingCode,
}
metrics.roomsCombinedAvailability.counter.add(1, metricsData)
console.info(
"api.hotels.roomsCombinedAvailability start",
JSON.stringify({ query: { hotelId, params: metricsData } })
)
const availabilityResponses = await Promise.allSettled(
adultsCount.map(async (adultCount: number, idx: number) => {
const kids = childArray?.[idx]
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults: adultCount,
...(kids?.length && {
children: generateChildrenString(kids),
}),
...(bookingCode && { bookingCode }),
...(redemption && { isRedemption: "true" }),
language: apiLang,
}
const apiResponse = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
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 = redemption
? redemptionRoomsCombinedAvailabilitySchema.safeParse(apiJson)
: 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)
return availabilityResponses.map((availability) => {
if (availability.status === "fulfilled") {
return availability.value
}
return {
details: availability.reason,
error: "request_failure",
}
})
}
export const getRoomAvailability = async (
input: SelectedRoomAvailabilitySchema,
lang: Lang,
token: string, // Either service token or user access token in case of redemption search
serviceToken?: string // In Redemption we need serviceToken for hotel api call
) => {
const {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
rateCode,
counterRateCode,
roomTypeCode,
redemption,
} = input
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
...(redemption && { isRedemption: "true" }),
language: toApiLang(lang),
}
metrics.selectedRoomAvailability.counter.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.selectedRoomAvailability start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponseAvailability = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
params
)
if (!apiResponseAvailability.ok) {
const text = await apiResponseAvailability.text()
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
}),
})
console.error(
"api.hotels.selectedRoomAvailability error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
},
})
)
throw new Error("Failed to fetch selected room availability")
}
const apiJsonAvailability = await apiResponseAvailability.json()
const validateAvailabilityData =
roomsAvailabilitySchema.safeParse(apiJsonAvailability)
if (!validateAvailabilityData.success) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.selectedRoomAvailability validation error",
JSON.stringify({
query: { hotelId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
const hotelData = await getHotel(
{
hotelId,
isCardOnlyPayment: false,
language: lang,
},
serviceToken ?? token
)
const rooms = validateAvailabilityData.data.roomConfigurations
const selectedRoom = rooms.find((room) => room.roomTypeCode === roomTypeCode)
if (!selectedRoom) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
roomTypeCode,
error_type: "not_found",
error: `Couldn't find selected room with input: ${roomTypeCode}`,
})
console.error("No matching room found")
return null
}
const availableRoomsInCategory = rooms.filter(
(room) => room.roomType === selectedRoom?.roomType
)
const rateTypes = selectedRoom.products.find(
(rate) =>
rate.public?.rateCode === rateCode ||
rate.member?.rateCode === rateCode ||
rate.redemptions?.find((r) => r?.rateCode === rateCode)
)
if (!rateTypes) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "not_found",
error: `Couldn't find rateTypes for selected room: ${JSON.stringify(selectedRoom)}`,
})
console.error("No matching rate found")
return null
}
const rates = rateTypes
const rateDefinition = validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === rateCode
)
const memberRateDefinition =
validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === counterRateCode
)
const bedTypes = availableRoomsInCategory
.map((availRoom) => {
const matchingRoom = hotelData?.roomCategories
?.find((room) =>
room.roomTypes
.map((roomType) => roomType.code)
.includes(availRoom.roomTypeCode)
)
?.roomTypes.find((roomType) => roomType.code === availRoom.roomTypeCode)
if (matchingRoom) {
return {
description: matchingRoom.description,
size: matchingRoom.mainBed.widthRange,
value: matchingRoom.code,
type: matchingRoom.mainBed.type,
extraBed: matchingRoom.fixedExtraBed
? {
type: matchingRoom.fixedExtraBed.type,
description: matchingRoom.fixedExtraBed.description,
}
: undefined,
}
}
})
.filter((bed): bed is BedTypeSelection => Boolean(bed))
metrics.selectedRoomAvailability.success.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.selectedRoomAvailability success",
JSON.stringify({
query: { hotelId, params: params },
})
)
return {
bedTypes,
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
cancellationRule: rateDefinition?.cancellationRule,
cancellationText: rateDefinition?.cancellationText ?? "",
isFlexRate:
rateDefinition?.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM,
memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed,
memberRate: rates?.member,
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
publicRate: rates?.public,
redemptionRate: rates?.redemptions?.find((r) => r?.rateCode === rateCode),
rate: selectedRoom.products[0].rate,
rateDefinitionTitle: rateDefinition?.title ?? "",
rateDetails: rateDefinition?.generalTerms,
// Send rate Title when it is a booking code rate
rateTitle:
rateDefinition?.rateType !== RateTypeEnum.Regular
? rateDefinition?.title
: undefined,
rateType: rateDefinition?.rateType ?? "",
selectedRoom,
}
}
export const hotelQueryRouter = router({ export const hotelQueryRouter = router({
availability: router({ availability: router({
hotelsByCity: serviceProcedure hotelsByCity: serviceProcedure
@@ -497,344 +842,31 @@ export const hotelQueryRouter = router({
roomsCombinedAvailability: serviceProcedure roomsCombinedAvailability: serviceProcedure
.input(roomsCombinedAvailabilityInputSchema) .input(roomsCombinedAvailabilityInputSchema)
.query( .query(async ({ input, ctx }) => {
async ({ return getRoomsCombinedAvailability(input, ctx.serviceToken)
ctx, }),
input: { roomsCombinedAvailabilityWithRedemption: protectedProcedure
adultsCount, .input(roomsCombinedAvailabilityInputSchema)
bookingCode, .query(async ({ input, ctx }) => {
childArray, return getRoomsCombinedAvailability(
hotelId, input,
lang, ctx.session.token.access_token
rateCode, )
roomStayEndDate, }),
roomStayStartDate,
},
}) => {
const apiLang = toApiLang(lang)
const metricsData = {
hotelId,
roomStayStartDate,
roomStayEndDate,
adultsCount,
childArray: childArray ? JSON.stringify(childArray) : undefined,
bookingCode,
}
metrics.roomsCombinedAvailability.counter.add(1, metricsData)
console.info(
"api.hotels.roomsCombinedAvailability start",
JSON.stringify({ query: { hotelId, params: metricsData } })
)
const availabilityResponses = await Promise.allSettled(
adultsCount.map(async (adultCount: number, idx: number) => {
const kids = childArray?.[idx]
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults: adultCount,
...(kids?.length && {
children: generateChildrenString(kids),
}),
...(bookingCode && { bookingCode }),
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 =
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",
}
})
return data
}
),
room: serviceProcedure room: serviceProcedure
.input(selectedRoomAvailabilityInputSchema) .input(selectedRoomAvailabilityInputSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const { lang } = input return getRoomAvailability(input, ctx.lang, ctx.serviceToken)
}),
const { roomWithRedemption: protectedServcieProcedure
hotelId, .input(selectedRoomAvailabilityInputSchema)
roomStayStartDate, .query(async ({ input, ctx }) => {
roomStayEndDate, return getRoomAvailability(
adults, input,
children, ctx.lang,
bookingCode, ctx.session.token.access_token,
rateCode,
counterRateCode,
roomTypeCode,
} = input
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
language: lang ?? toApiLang(ctx.lang),
}
metrics.selectedRoomAvailability.counter.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.selectedRoomAvailability start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponseAvailability = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponseAvailability.ok) {
const text = await apiResponseAvailability.text()
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
}),
})
console.error(
"api.hotels.selectedRoomAvailability error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
},
})
)
throw new Error("Failed to fetch selected room availability")
}
const apiJsonAvailability = await apiResponseAvailability.json()
const validateAvailabilityData =
roomsAvailabilitySchema.safeParse(apiJsonAvailability)
if (!validateAvailabilityData.success) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.selectedRoomAvailability validation error",
JSON.stringify({
query: { hotelId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
const hotelData = await getHotel(
{
hotelId,
isCardOnlyPayment: false,
language: lang ?? ctx.lang,
},
ctx.serviceToken ctx.serviceToken
) )
const rooms = validateAvailabilityData.data.roomConfigurations
const selectedRoom = rooms.find(
(room) => room.roomTypeCode === roomTypeCode
)
if (!selectedRoom) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
roomTypeCode,
error_type: "not_found",
error: `Couldn't find selected room with input: ${roomTypeCode}`,
})
console.error("No matching room found")
return null
}
const availableRoomsInCategory = rooms.filter(
(room) => room.roomType === selectedRoom?.roomType
)
const rateTypes = selectedRoom.products.find(
(rate) =>
rate.public?.rateCode === rateCode ||
rate.member?.rateCode === rateCode
)
if (!rateTypes) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "not_found",
error: `Couldn't find rateTypes for selected room: ${JSON.stringify(selectedRoom)}`,
})
console.error("No matching rate found")
return null
}
const rates = rateTypes
const rateDefinition =
validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === rateCode
)
const memberRateDefinition =
validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === counterRateCode
)
const bedTypes = availableRoomsInCategory
.map((availRoom) => {
const matchingRoom = hotelData?.roomCategories
?.find((room) =>
room.roomTypes
.map((roomType) => roomType.code)
.includes(availRoom.roomTypeCode)
)
?.roomTypes.find(
(roomType) => roomType.code === availRoom.roomTypeCode
)
if (matchingRoom) {
return {
description: matchingRoom.description,
size: matchingRoom.mainBed.widthRange,
value: matchingRoom.code,
type: matchingRoom.mainBed.type,
extraBed: matchingRoom.fixedExtraBed
? {
type: matchingRoom.fixedExtraBed.type,
description: matchingRoom.fixedExtraBed.description,
}
: undefined,
}
}
})
.filter((bed): bed is BedTypeSelection => Boolean(bed))
metrics.selectedRoomAvailability.success.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.selectedRoomAvailability success",
JSON.stringify({
query: { hotelId, params: params },
})
)
return {
bedTypes,
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
cancellationRule: rateDefinition?.cancellationRule,
cancellationText: rateDefinition?.cancellationText ?? "",
isFlexRate:
rateDefinition?.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM,
memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed,
memberRate: rates?.member,
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
publicRate: rates?.public,
rate: selectedRoom.products[0].rate,
rateDefinitionTitle: rateDefinition?.title ?? "",
rateDetails: rateDefinition?.generalTerms,
// Send rate Title when it is a booking code rate
rateTitle:
rateDefinition?.rateType !== RateTypeEnum.Regular
? rateDefinition?.title
: undefined,
rateType: rateDefinition?.rateType ?? "",
selectedRoom,
}
}), }),
hotelsByCityWithBookingCode: serviceProcedure hotelsByCityWithBookingCode: serviceProcedure
.input(hotelsAvailabilityInputSchema) .input(hotelsAvailabilityInputSchema)

View File

@@ -9,8 +9,6 @@ export const productTypeSchema = z
.object({ .object({
public: productTypePriceSchema.optional(), public: productTypePriceSchema.optional(),
member: productTypePriceSchema.optional(), member: productTypePriceSchema.optional(),
redemption: productTypePointsSchema.optional(), redemptions: z.array(productTypePointsSchema).optional(),
redemptionA: productTypePointsSchema.optional(),
redemptionB: productTypePointsSchema.optional(),
}) })
.optional() .optional()

View File

@@ -10,22 +10,31 @@ export const priceSchema = z.object({
regularPricePerStay: z.coerce.number().optional(), regularPricePerStay: z.coerce.number().optional(),
}) })
export const pointsSchema = z.object({ export const pointsSchema = z
currency: z.nativeEnum(CurrencyEnum).optional(), .object({
pricePerNight: z.coerce.number().optional(), currency: z.nativeEnum(CurrencyEnum).optional(),
pricePerStay: z.coerce.number().optional(), pricePerStay: z.coerce.number().optional(),
pointsPerNight: z.number(), pointsPerStay: z.coerce.number(),
pointsPerStay: z.number(), additionalPricePerStay: z.coerce.number().optional(),
}) additionalPriceCurrency: z.nativeEnum(CurrencyEnum).optional(),
})
.transform((data) => ({
...data,
additionalPriceCurrency: data.currency,
currency: CurrencyEnum.POINTS,
pricePerStay: data.pointsPerStay,
price: data.pointsPerStay,
additionalPrice: data.additionalPricePerStay,
}))
const partialPriceSchema = z.object({ const partialPriceSchema = z.object({
rateCode: z.string(), rateCode: z.string(),
rateType: z.string().optional(), rateType: z.string().optional(),
requestedPrice: priceSchema.optional(),
}) })
export const productTypePriceSchema = partialPriceSchema.extend({ export const productTypePriceSchema = partialPriceSchema.extend({
localPrice: priceSchema, localPrice: priceSchema,
requestedPrice: priceSchema.optional(),
}) })
export const productTypePointsSchema = partialPriceSchema.extend({ export const productTypePointsSchema = partialPriceSchema.extend({

View File

@@ -29,21 +29,25 @@ export const roomConfigurationSchema = z
}) })
.transform((data) => { .transform((data) => {
if (data.products.length) { if (data.products.length) {
/** if (data.products[0].redemptions) {
* Just guaranteeing that if all products all miss // No need of rate check in reward night scenario
* both public and member rateCode that status is return { ...data }
* set to `NotAvailable` } else {
*/ /**
const allProductsMissBothRateCodes = data.products.every( * Just guaranteeing that if all products all miss
(product) => !product.public?.rateCode && !product.member?.rateCode * both public and member rateCode that status is
) * set to `NotAvailable`
if (allProductsMissBothRateCodes) { */
return { const allProductsMissBothRateCodes = data.products.every(
...data, (product) => !product.public?.rateCode && !product.member?.rateCode
status: AvailabilityEnum.NotAvailable, )
if (allProductsMissBothRateCodes) {
return {
...data,
status: AvailabilityEnum.NotAvailable,
}
} }
} }
} }
return data return data
}) })

View File

@@ -1,6 +1,9 @@
import { z } from "zod" import { z } from "zod"
import { productTypePriceSchema } from "../productTypePrice" import {
productTypePointsSchema,
productTypePriceSchema,
} from "../productTypePrice"
export const productSchema = z export const productSchema = z
.object({ .object({
@@ -9,6 +12,7 @@ export const productSchema = z
productType: z.object({ productType: z.object({
member: productTypePriceSchema.optional(), member: productTypePriceSchema.optional(),
public: productTypePriceSchema.optional(), public: productTypePriceSchema.optional(),
redemptions: z.array(productTypePointsSchema).optional(),
}), }),
// Used to set the rate that we use to chose titles etc. // Used to set the rate that we use to chose titles etc.
rate: z.enum(["change", "flex", "save"]).default("save"), rate: z.enum(["change", "flex", "save"]).default("save"),

View File

@@ -206,3 +206,6 @@ export const contentStackBaseWithProtectedProcedure =
export const safeProtectedServiceProcedure = export const safeProtectedServiceProcedure =
safeProtectedProcedure.unstable_concat(serviceProcedure) safeProtectedProcedure.unstable_concat(serviceProcedure)
export const protectedServcieProcedure =
protectedProcedure.unstable_concat(serviceProcedure)

View File

@@ -2,9 +2,10 @@ import isEqual from "fast-deep-equal"
import { detailsStorageName } from "." import { detailsStorageName } from "."
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details" import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
import type { Price } from "@/types/components/hotelReservation/price" import type { Price } from "@/types/components/hotelReservation/price"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { CurrencyEnum } from "@/types/enums/currency"
import { StepEnum } from "@/types/enums/step" import { StepEnum } from "@/types/enums/step"
import type { import type {
DetailsState, DetailsState,
@@ -131,18 +132,43 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
} }
} }
if (roomRate.redemptionRate) {
return {
// ToDo Handle perNight as undefined
perNight: {
requested: undefined,
local: {
currency:
roomRate.redemptionRate.localPrice.currency ?? CurrencyEnum.POINTS,
price: roomRate.redemptionRate.localPrice.pointsPerStay,
additionalPrice:
roomRate.redemptionRate.localPrice.additionalPricePerStay,
additionalPriceCurrency:
roomRate.redemptionRate.localPrice.additionalPriceCurrency,
},
},
perStay: {
requested: undefined,
local: {
currency:
roomRate.redemptionRate.localPrice.currency ?? CurrencyEnum.POINTS,
price: roomRate.redemptionRate.localPrice.pointsPerStay,
additionalPrice:
roomRate.redemptionRate.localPrice.additionalPricePerStay,
additionalPriceCurrency:
roomRate.redemptionRate.localPrice.additionalPriceCurrency,
},
},
}
}
throw new Error( throw new Error(
`Unable to calculate RoomPrice since user is neither a member or memberRate is missing, or publicRate is missing` `Unable to calculate RoomPrice since user is neither a member or memberRate is missing, or publicRate is missing`
) )
} }
type TotalPrice = {
requested: { currency: string; price: number } | undefined
local: { currency: string; price: number; regularPrice?: number }
}
export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) { export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
return roomRates.reduce<TotalPrice>( return roomRates.reduce<Price>(
(total, roomRate, idx) => { (total, roomRate, idx) => {
const isFirstRoom = idx === 0 const isFirstRoom = idx === 0
const rate = const rate =

View File

@@ -3,6 +3,7 @@ import { produce } from "immer"
import { useContext } from "react" import { useContext } from "react"
import { create, useStore } from "zustand" import { create, useStore } from "zustand"
import { REDEMPTION } from "@/constants/booking"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { DetailsContext } from "@/contexts/Details" import { DetailsContext } from "@/contexts/Details"
@@ -20,6 +21,9 @@ import {
} from "./helpers" } from "./helpers"
import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast" import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast"
import {
PointsPriceSchema,
type Price} from "@/types/components/hotelReservation/price";
import { StepEnum } from "@/types/enums/step" import { StepEnum } from "@/types/enums/step"
import type { import type {
DetailsState, DetailsState,
@@ -49,11 +53,20 @@ export function createDetailsStore(
breakfastPackages: BreakfastPackages | null breakfastPackages: BreakfastPackages | null
) { ) {
const isMember = !!user const isMember = !!user
const isRedemption =
new URLSearchParams(searchParams).get("searchtype") === REDEMPTION
const initialTotalPrice = getTotalPrice( let initialTotalPrice: Price
initialState.rooms.map((r) => r.roomRate), if (isRedemption && initialState.rooms[0].roomRate.redemptionRate) {
isMember initialTotalPrice = PointsPriceSchema.parse(
) initialState.rooms[0].roomRate.redemptionRate
)
} else {
initialTotalPrice = getTotalPrice(
initialState.rooms.map((r) => r.roomRate),
isMember
)
}
initialState.rooms.forEach((room) => { initialState.rooms.forEach((room) => {
if (room.roomFeatures) { if (room.roomFeatures) {

View File

@@ -91,10 +91,15 @@ export function createRatesStore({
rc.products.find( rc.products.find(
(product) => (product) =>
product.public?.rateCode === room.rateCode || product.public?.rateCode === room.rateCode ||
product.member?.rateCode === room.rateCode product.member?.rateCode === room.rateCode ||
product.redemptions?.find(
(redemption) => redemption?.rateCode === room.rateCode
)
) )
) )
const redemptionProduct = selectedRoom?.products[0].redemptions?.find(
(r) => r?.rateCode === room.rateCode
)
const product = selectedRoom?.products.find( const product = selectedRoom?.products.find(
(p) => (p) =>
p.public?.rateCode === room.rateCode || p.public?.rateCode === room.rateCode ||
@@ -105,10 +110,19 @@ export function createRatesStore({
features: selectedRoom.features, features: selectedRoom.features,
member: product.member, member: product.member,
public: product.public, public: product.public,
redemption: undefined,
rate: product.rate, rate: product.rate,
roomType: selectedRoom.roomType, roomType: selectedRoom.roomType,
roomTypeCode: selectedRoom.roomTypeCode, roomTypeCode: selectedRoom.roomTypeCode,
} }
} else if (selectedRoom && redemptionProduct) {
rateSummary[idx] = {
features: selectedRoom.features,
redemption: redemptionProduct,
rate: selectedRoom?.products[0].rate,
roomType: selectedRoom.roomType,
roomTypeCode: selectedRoom.roomTypeCode,
}
} }
} }
}) })
@@ -200,6 +214,7 @@ export function createRatesStore({
package: state.rooms[idx].selectedPackage, package: state.rooms[idx].selectedPackage,
rate: selectedRate.product.rate, rate: selectedRate.product.rate,
public: selectedRate.product.public, public: selectedRate.product.public,
redemption: undefined,
roomType: selectedRate.roomType, roomType: selectedRate.roomType,
roomTypeCode: selectedRate.roomTypeCode, roomTypeCode: selectedRate.roomTypeCode,
} }
@@ -240,6 +255,51 @@ export function createRatesStore({
state.activeRoom = idx + 1 state.activeRoom = idx + 1
} }
state.searchParams = new ReadonlyURLSearchParams(searchParams)
window.history.pushState(
{},
"",
`${state.pathname}?${searchParams}`
)
})
)
}
},
selectRateRedemption(idx) {
return function (selectedRate, selectedRateCode?: string) {
return set(
produce((state: RatesState) => {
const redemptionRate = selectedRate.product.redemptions?.find(
(r) => r?.rateCode === selectedRateCode
)
if (!redemptionRate) {
return
}
state.rooms[idx].selectedRate = selectedRate
state.rateSummary[idx] = {
features: selectedRate.features,
package: state.rooms[idx].selectedPackage,
rate: selectedRate.product.rate,
roomType: selectedRate.roomType,
roomTypeCode: selectedRate.roomTypeCode,
redemption: redemptionRate,
}
const searchParams = new URLSearchParams(state.searchParams)
if (redemptionRate.rateCode) {
searchParams.set(
`room[${idx}].ratecode`,
redemptionRate.rateCode
)
}
searchParams.set(
`room[${idx}].roomtype`,
selectedRate.roomTypeCode
)
state.searchParams = new ReadonlyURLSearchParams(searchParams) state.searchParams = new ReadonlyURLSearchParams(searchParams)
window.history.pushState( window.history.pushState(
{}, {},
@@ -291,11 +351,11 @@ export function createRatesStore({
selectedRate: selectedRate:
selectedRate && product selectedRate && product
? { ? {
features: selectedRate.features, features: selectedRate.features,
product, product,
roomType: selectedRate.roomType, roomType: selectedRate.roomType,
roomTypeCode: selectedRate.roomTypeCode, roomTypeCode: selectedRate.roomTypeCode,
} }
: null, : null,
} }
}), }),

View File

@@ -7,11 +7,13 @@ import type {
guestDetailsSchema, guestDetailsSchema,
signedInDetailsSchema, signedInDetailsSchema,
} from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema" } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema"
import type { productTypePointsSchema } from "@/server/routers/hotels/schemas/productTypePrice"
import type { Price } from "../price" import type { Price } from "../price"
export type DetailsSchema = z.output<typeof guestDetailsSchema> export type DetailsSchema = z.output<typeof guestDetailsSchema>
export type MultiroomDetailsSchema = z.output<typeof multiroomDetailsSchema> export type MultiroomDetailsSchema = z.output<typeof multiroomDetailsSchema>
export type SignedInDetailsSchema = z.output<typeof signedInDetailsSchema> export type SignedInDetailsSchema = z.output<typeof signedInDetailsSchema>
export type ProductTypePointsSchema = z.output<typeof productTypePointsSchema>
export interface RoomPrice { export interface RoomPrice {
perNight: Price perNight: Price
@@ -29,4 +31,5 @@ export type JoinScandicFriendsCardProps = {
export type RoomRate = { export type RoomRate = {
memberRate?: Product["member"] memberRate?: Product["member"]
publicRate?: Product["public"] publicRate?: Product["public"]
redemptionRate?: ProductTypePointsSchema
} }

View File

@@ -1,10 +1,31 @@
import { z } from "zod"
import { CurrencyEnum } from "@/types/enums/currency"
interface TPrice { interface TPrice {
currency: string currency: string
price: number price: number
regularPrice?: number regularPrice?: number
additionalPrice?: number
additionalPriceCurrency?: string
} }
export interface Price { export interface Price {
requested: TPrice | undefined requested?: TPrice
local: TPrice local: TPrice
} }
export const PointsPriceSchema = z
.object({
localPrice: z.object({
currency: z.nativeEnum(CurrencyEnum),
price: z.number(),
additionalPrice: z.number().optional(),
additionalPriceCurrency: z.nativeEnum(CurrencyEnum).optional(),
}),
})
.transform((data) => ({
local: {
...data.localPrice,
},
}))

View File

@@ -1,14 +1,12 @@
import type { import type { ProductTypePrices } from "@/types/trpc/routers/hotel/availability"
ProductTypePoints,
ProductTypePrices,
} from "@/types/trpc/routers/hotel/availability"
export type PriceCardProps = { export type PriceCardProps = {
productTypePrices: ProductTypePrices productTypePrices: ProductTypePrices
isMemberPrice?: boolean isMemberPrice?: boolean
} }
export type PointsCardProps = { export type PointsRowProps = {
productTypePoints?: ProductTypePoints pointsPerStay: number
redemptionPrice?: number additionalPricePerStay?: number
additionalPriceCurrency?: string
} }

View File

@@ -22,12 +22,12 @@ export type FlexibilityOptionProps = {
roomType: RoomConfiguration["roomType"] roomType: RoomConfiguration["roomType"]
roomTypeCode: RoomConfiguration["roomTypeCode"] roomTypeCode: RoomConfiguration["roomTypeCode"]
title: string title: string
rateTitle?: string // This is for the rates via booking codes rateName?: string // Obtained in case of booking code and redemption rates
} }
export interface PriceListProps { export interface PriceListProps {
publicPrice?: ProductPrice | Record<string, never> publicPrice?: ProductPrice | Record<string, never>
memberPrice?: ProductPrice | Record<string, never> memberPrice?: ProductPrice | Record<string, never>
petRoomPackage?: RoomPackage petRoomPackage?: RoomPackage
rateTitle?: string // This is for the rates via booking codes rateName?: string // Obtained in case of booking code and redemption rates
} }

View File

@@ -3,6 +3,7 @@ import type {
RoomConfiguration, RoomConfiguration,
} from "@/types/trpc/routers/hotel/roomAvailability" } from "@/types/trpc/routers/hotel/roomAvailability"
import type { ChildBedMapEnum } from "../../bookingWidget/enums" import type { ChildBedMapEnum } from "../../bookingWidget/enums"
import type { ProductTypePointsSchema } from "../enterDetails/details"
import type { RoomPackageCodeEnum } from "./roomFilter" import type { RoomPackageCodeEnum } from "./roomFilter"
export interface Child { export interface Child {
@@ -43,20 +44,14 @@ export type Rate = {
roomTypeCode: RoomConfiguration["roomTypeCode"] roomTypeCode: RoomConfiguration["roomTypeCode"]
} & ( } & (
| { | {
member?: undefined member?: NonNullable<Product["member"]>
public?: undefined public?: NonNullable<Product["public"]>
redemption?: never
} }
| { | {
member?: never member?: never
public: NonNullable<Product["public"]>
}
| {
member: NonNullable<Product["member"]>
public?: never public?: never
} redemption: NonNullable<ProductTypePointsSchema>
| {
member: NonNullable<Product["member"]>
public: NonNullable<Product["public"]>
} }
) )

View File

@@ -11,6 +11,10 @@ export interface RoomContextValue extends SelectedRoom {
modifyRate: () => void modifyRate: () => void
selectFilter: (code: RoomPackageCodeEnum | undefined) => void selectFilter: (code: RoomPackageCodeEnum | undefined) => void
selectRate: (rate: SelectedRate) => void selectRate: (rate: SelectedRate) => void
selectRateRedemption: (
rate: SelectedRate,
selectedRateCode?: string
) => void
} }
isActiveRoom: boolean isActiveRoom: boolean
isMainRoom: boolean isMainRoom: boolean

View File

@@ -4,5 +4,6 @@ export enum CurrencyEnum {
NOK = "NOK", NOK = "NOK",
PLN = "PLN", PLN = "PLN",
SEK = "SEK", SEK = "SEK",
POINTS = "POINTS",
Unknown = "Unknown", Unknown = "Unknown",
} }

View File

@@ -27,6 +27,9 @@ interface Actions {
modifyRate: (idx: number) => () => void modifyRate: (idx: number) => () => void
selectFilter: (idx: number) => (code: RoomPackageCodeEnum | undefined) => void selectFilter: (idx: number) => (code: RoomPackageCodeEnum | undefined) => void
selectRate: (idx: number) => (rate: SelectedRate) => void selectRate: (idx: number) => (rate: SelectedRate) => void
selectRateRedemption: (
idx: number
) => (rate: SelectedRate, selectedRateCode?: string) => void
} }
export interface SelectedRate { export interface SelectedRate {

View File

@@ -1,9 +1,12 @@
import {
type getHotelsByHotelIdsAvailabilityInputSchema,
type hotelsAvailabilityInputSchema,
type roomsCombinedAvailabilityInputSchema,
type selectedRoomAvailabilityInputSchema,
} from "@/server/routers/hotels/input"
import type { z } from "zod" import type { z } from "zod"
import type {
getHotelsByHotelIdsAvailabilityInputSchema,
hotelsAvailabilityInputSchema,
} 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"
import type { import type {
@@ -18,6 +21,12 @@ export type HotelsAvailabilityInputSchema = z.output<
export type HotelsByHotelIdsAvailabilityInputSchema = z.output< export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
typeof getHotelsByHotelIdsAvailabilityInputSchema typeof getHotelsByHotelIdsAvailabilityInputSchema
> >
export type RoomsCombinedAvailabilityInputSchema = z.output<
typeof roomsCombinedAvailabilityInputSchema
>
export type SelectedRoomAvailabilitySchema = z.output<
typeof selectedRoomAvailabilityInputSchema
>
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>
export type ProductTypePoints = z.output<typeof productTypePointsSchema> export type ProductTypePoints = z.output<typeof productTypePointsSchema>

View File

@@ -22,3 +22,19 @@ export function formatPrice(intl: IntlShape, price: number, currency: string) {
}) })
return `${localizedPrice} ${currency}` return `${localizedPrice} ${currency}`
} }
// This will handle redemption and bonus cheque (corporate cheque) scneario with partial payments
export function formatPriceWithAdditionalPrice(
intl: IntlShape,
points: number,
pointsCurrency: string,
additionalPrice?: number,
additionalPriceCurrency?: string
) {
const formattedAdditionalPrice =
additionalPrice && additionalPriceCurrency
? `+ ${formatPrice(intl, additionalPrice, additionalPriceCurrency)}`
: ""
return `${formatPrice(intl, points, pointsCurrency)} ${formattedAdditionalPrice}`
}