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:
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" })
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.pointsList {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointsRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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} />
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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<
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]>
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user