Merged in fix/allow-single-rateCode (pull request #1438)

fix: allow rates that only have either of member or public to be selectable

* fix: allow rates that only have either of member or public to be selectable


Approved-by: Michael Zetterberg
This commit is contained in:
Simon.Emanuelsson
2025-03-03 08:28:55 +00:00
committed by Linus Flood
parent 3f01266a75
commit c3e3fa62ec
30 changed files with 487 additions and 573 deletions

View File

@@ -64,8 +64,6 @@ export const roomRate: RoomRate = {
pricePerStay: 132, pricePerStay: 132,
currency: CurrencyEnum.EUR, currency: CurrencyEnum.EUR,
}, },
oldRateCode: "",
rate: "",
}, },
publicRate: { publicRate: {
rateCode: "SAVEEU", rateCode: "SAVEEU",
@@ -79,8 +77,6 @@ export const roomRate: RoomRate = {
pricePerStay: 133, pricePerStay: 133,
currency: CurrencyEnum.EUR, currency: CurrencyEnum.EUR,
}, },
oldRateCode: "",
rate: "",
}, },
} }

View File

@@ -84,7 +84,7 @@ export default function Details({ user }: DetailsProps) {
id={`${formID}-room-${roomNr}`} id={`${formID}-room-${roomNr}`}
onSubmit={methods.handleSubmit(onSubmit)} onSubmit={methods.handleSubmit(onSubmit)}
> >
{user ? null : <JoinScandicFriendsCard />} {user || !memberRate ? null : <JoinScandicFriendsCard />}
<div className={styles.container}> <div className={styles.container}>
<Footnote <Footnote
color="uiTextHighContrast" color="uiTextHighContrast"

View File

@@ -269,7 +269,7 @@ export default function PaymentClient({
smsConfirmationRequested: data.smsConfirmation, smsConfirmationRequested: data.smsConfirmation,
roomPrice: { roomPrice: {
memberPrice: room.roomRate.memberRate?.localPrice.pricePerStay, memberPrice: room.roomRate.memberRate?.localPrice.pricePerStay,
publicPrice: room.roomRate.publicRate.localPrice.pricePerStay, publicPrice: room.roomRate.publicRate?.localPrice.pricePerStay,
}, },
})), })),
payment: { payment: {

View File

@@ -394,7 +394,7 @@ export default function SummaryUI({
</div> </div>
<Divider className={styles.bottomDivider} color="primaryLightSubtle" /> <Divider className={styles.bottomDivider} color="primaryLightSubtle" />
</div> </div>
{showSignupPromo && memberPrice ? ( {showSignupPromo && memberPrice && !isMember ? (
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} /> <SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
) : null} ) : null}
</section> </section>

View File

@@ -25,6 +25,7 @@ import styles from "./summary.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details" import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary" import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary"
import { RateTypeEnum } from "@/types/enums/rateType"
export default function Summary({ export default function Summary({
booking, booking,
@@ -56,6 +57,11 @@ export default function Summary({
const memberPrice = getMemberPrice(rooms[0].roomRate) const memberPrice = getMemberPrice(rooms[0].roomRate)
const containsBookingCodeRate = rooms.find(
(room) => room.roomRate.publicRate?.rateType !== RateTypeEnum.Regular
)
const showDiscounted = containsBookingCodeRate || isMember
return ( return (
<section className={styles.summary}> <section className={styles.summary}>
<header className={styles.header}> <header className={styles.header}>
@@ -103,6 +109,9 @@ export default function Summary({
const memberPrice = getMemberPrice(room.roomRate) const memberPrice = getMemberPrice(room.roomRate)
const showMemberPrice = !!(isMember && memberPrice && roomNumber === 1) const showMemberPrice = !!(isMember && memberPrice && roomNumber === 1)
const isBookingCodeRate =
room.roomRate.publicRate?.rateType !== RateTypeEnum.Regular
const showDiscounted = isBookingCodeRate || showMemberPrice
const adultsMsg = intl.formatMessage( const adultsMsg = intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" }, { id: "{totalAdults, plural, one {# adult} other {# adults}}" },
@@ -134,7 +143,7 @@ export default function Summary({
) : null} ) : null}
<div className={styles.entry}> <div className={styles.entry}>
<Body color="uiTextHighContrast">{room.roomType}</Body> <Body color="uiTextHighContrast">{room.roomType}</Body>
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}> <Body color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPrice( {formatPrice(
intl, intl,
room.roomPrice.perStay.local.price, room.roomPrice.perStay.local.price,
@@ -249,7 +258,11 @@ export default function Summary({
/> />
</div> </div>
<div> <div>
<Body textTransform="bold" data-testid="total-price"> <Body
color={showDiscounted ? "red" : "uiTextHighContrast"}
textTransform="bold"
data-testid="total-price"
>
{formatPrice( {formatPrice(
intl, intl,
totalPrice.local.price, totalPrice.local.price,

View File

@@ -14,6 +14,7 @@ import Summary from "./Summary"
import styles from "./mobileSummary.module.css" import styles from "./mobileSummary.module.css"
import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
import { RateTypeEnum } from "@/types/enums/rateType"
export default function MobileSummary({ export default function MobileSummary({
isAllRoomsSelected, isAllRoomsSelected,
@@ -71,19 +72,24 @@ export default function MobileSummary({
roomPrice: { roomPrice: {
perNight: { perNight: {
local: { local: {
price: room.public.localPrice.pricePerNight, price: (room.public?.localPrice.pricePerNight ||
currency: room.public.localPrice.currency, room.member?.localPrice.pricePerNight)!,
currency: (room.public?.localPrice.currency ||
room.member?.localPrice.currency)!,
}, },
requested: undefined, requested: undefined,
}, },
perStay: { perStay: {
local: { local: {
price: room.public.localPrice.pricePerStay, price: (room.public?.localPrice.pricePerStay ||
currency: room.public.localPrice.currency, room.member?.localPrice.pricePerStay)!,
currency: (room.public?.localPrice.currency ||
room.member?.localPrice.currency)!,
}, },
requested: undefined, requested: undefined,
}, },
currency: room.public.localPrice.currency, currency: (room.public?.localPrice.currency ||
room.member?.localPrice.currency)!,
}, },
roomRate: { roomRate: {
...room.public, ...room.public,
@@ -91,13 +97,23 @@ export default function MobileSummary({
publicRate: room.public, publicRate: room.public,
}, },
rateDetails: rateDefinitions.find( rateDetails: rateDefinitions.find(
(rate) => rate.rateCode === room.public.rateCode (rate) =>
rate.rateCode === room.public?.rateCode ||
rate.rateCode === room.member?.rateCode
)?.generalTerms, )?.generalTerms,
cancellationText: cancellationText:
rateDefinitions.find((rate) => rate.rateCode === room.public.rateCode) rateDefinitions.find(
?.cancellationText ?? "", (rate) =>
rate.rateCode === room.public?.rateCode ||
rate.rateCode === room.member?.rateCode
)?.cancellationText ?? "",
})) }))
const containsBookingCodeRate = rateSummary.find(
(rate) => rate.public?.rateType !== RateTypeEnum.Regular
)
const showDiscounted = containsBookingCodeRate || isUserLoggedIn
return ( return (
<div className={styles.wrapper} data-open={isSummaryOpen}> <div className={styles.wrapper} data-open={isSummaryOpen}>
<div className={styles.content}> <div className={styles.content}>
@@ -122,7 +138,7 @@ 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> <Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPrice( {formatPrice(
intl, intl,
totalPriceToShow.local.price, totalPriceToShow.local.price,

View File

@@ -6,7 +6,6 @@ import { useIntl } from "react-intl"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import { getRates } from "@/components/HotelReservation/SelectRate/utils"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop" import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile" import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
@@ -23,6 +22,8 @@ import styles from "./rateSummary.module.css"
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 { RateTypeEnum } from "@/types/enums/rateType"
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
const { const {
@@ -86,19 +87,13 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
const hasMemberRates = rateSummary.some((room) => room.member) const hasMemberRates = rateSummary.some((room) => room.member)
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
const rates = getRates(roomsAvailability.rateDefinitions)
const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
const freeBooking = intl.formatMessage({ id: "Free rebooking" }) const freeBooking = intl.formatMessage({ id: "Free rebooking" })
const payLater = intl.formatMessage({ id: "Pay later" }) const payLater = intl.formatMessage({ id: "Pay later" })
const payNow = intl.formatMessage({ id: "Pay now" }) const payNow = intl.formatMessage({ id: "Pay now" })
function getRateDetails(rateCode: string) { function getRateDetails(rate: Rate["rate"]) {
const rate = Object.keys(rates).find((k) =>
rates[k as keyof typeof rates].find((a) => a.rateCode === rateCode)
)
switch (rate) { switch (rate) {
case "change": case "change":
return `${freeBooking}, ${payNow}` return `${freeBooking}, ${payNow}`
@@ -122,6 +117,11 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
return null return null
} }
const isBookingCodeRate = rateSummary.some(
(rate) => rate.public?.rateType !== RateTypeEnum.Regular
)
const showDiscounted = isUserLoggedIn || isBookingCodeRate
const totalPriceToShow = calculateTotalPrice( const totalPriceToShow = calculateTotalPrice(
rateSummary, rateSummary,
isUserLoggedIn, isUserLoggedIn,
@@ -134,7 +134,6 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
<div className={styles.content}> <div className={styles.content}>
<div className={styles.summaryText}> <div className={styles.summaryText}>
{rateSummary.map((room, index) => { {rateSummary.map((room, index) => {
const isMainRoom = index + 1 === 1
return ( return (
<div key={index} className={styles.roomSummary}> <div key={index} className={styles.roomSummary}>
{rateSummary.length > 1 ? ( {rateSummary.length > 1 ? (
@@ -147,11 +146,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
</Subtitle> </Subtitle>
<Body color="uiTextMediumContrast">{room.roomType}</Body> <Body color="uiTextMediumContrast">{room.roomType}</Body>
<Caption color="uiTextMediumContrast"> <Caption color="uiTextMediumContrast">
{getRateDetails( {getRateDetails(room.rate)}
isUserLoggedIn && room.member && isMainRoom
? room.member?.rateCode
: room.public.rateCode
)}
</Caption> </Caption>
</> </>
) : ( ) : (
@@ -160,11 +155,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
{room.roomType} {room.roomType}
</Subtitle> </Subtitle>
<Body color="uiTextMediumContrast"> <Body color="uiTextMediumContrast">
{getRateDetails( {getRateDetails(room.rate)}
isUserLoggedIn && room.member && isMainRoom
? room.member?.rateCode
: room.public.rateCode
)}
</Body> </Body>
</> </>
)} )}
@@ -206,9 +197,8 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
: 0 : 0
return total + memberPrice + petRoomPrice return total + memberPrice + petRoomPrice
}, 0), }, 0),
currency: currency: (rateSummary[0].member?.localPrice.currency ??
rateSummary[0].member?.localPrice.currency ?? rateSummary[0].public?.localPrice.currency)!,
rateSummary[0].public.localPrice.currency,
}} }}
/> />
</div> </div>
@@ -225,7 +215,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
<div className={styles.summaryPrice}> <div className={styles.summaryPrice}>
<div className={styles.summaryPriceTextDesktop}> <div className={styles.summaryPriceTextDesktop}>
<Subtitle <Subtitle
color={isUserLoggedIn ? "red" : "uiTextHighContrast"} color={showDiscounted ? "red" : "uiTextHighContrast"}
textAlign="right" textAlign="right"
> >
{formatPrice( {formatPrice(
@@ -253,7 +243,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
<Caption color="uiTextHighContrast"> <Caption color="uiTextHighContrast">
{intl.formatMessage({ id: "Total price" })} {intl.formatMessage({ id: "Total price" })}
</Caption> </Caption>
<Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}> <Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPrice( {formatPrice(
intl, intl,
totalPriceToShow.local.price, totalPriceToShow.local.price,

View File

@@ -12,10 +12,15 @@ export const calculateTotalPrice = (
) => { ) => {
return selectedRateSummary.reduce<Price>( return selectedRateSummary.reduce<Price>(
(total, room, idx) => { (total, room, idx) => {
const priceToUse = const rate =
isUserLoggedIn && room.member && idx + 1 === 1 isUserLoggedIn && room.member && idx + 1 === 1
? room.member ? room.member
: room.public : room.public
if (!rate) {
return total
}
const isPetRoom = room.features.find( const isPetRoom = room.features.find(
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
) )
@@ -31,18 +36,16 @@ export const calculateTotalPrice = (
return { return {
local: { local: {
currency: priceToUse.localPrice.currency, currency: rate.localPrice.currency,
price: price:
total.local.price + total.local.price + rate.localPrice.pricePerStay + petRoomPrice,
priceToUse.localPrice.pricePerStay +
petRoomPrice,
}, },
requested: priceToUse.requestedPrice requested: rate.requestedPrice
? { ? {
currency: priceToUse.requestedPrice.currency, currency: rate.requestedPrice.currency,
price: price:
(total.requested?.price ?? 0) + (total.requested?.price ?? 0) +
priceToUse.requestedPrice.pricePerStay + rate.requestedPrice.pricePerStay +
petRoomPrice, petRoomPrice,
} }
: undefined, : undefined,
@@ -50,7 +53,8 @@ export const calculateTotalPrice = (
}, },
{ {
local: { local: {
currency: selectedRateSummary[0].public.localPrice.currency, currency: (selectedRateSummary[0].public?.localPrice.currency ||
selectedRateSummary[0].member?.localPrice.currency)!,
price: 0, price: 0,
}, },
requested: undefined, requested: undefined,

View File

@@ -3,7 +3,6 @@ import { useIntl } from "react-intl"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import { getRates } from "@/components/HotelReservation/SelectRate/utils"
import { EditIcon } from "@/components/Icons" import { EditIcon } from "@/components/Icons"
import Image from "@/components/Image" import Image from "@/components/Image"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
@@ -15,9 +14,11 @@ import { useRoomContext } from "@/contexts/SelectRate/Room"
import styles from "./selectedRoomPanel.module.css" import styles from "./selectedRoomPanel.module.css"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
export default function SelectedRoomPanel() { export default function SelectedRoomPanel() {
const intl = useIntl() const intl = useIntl()
const { isUserLoggedIn, rateDefinitions, roomCategories } = useRatesStore( const { isUserLoggedIn, roomCategories } = useRatesStore(
(state) => ({ (state) => ({
isUserLoggedIn: state.isUserLoggedIn, isUserLoggedIn: state.isUserLoggedIn,
rateDefinitions: state.roomsAvailability?.rateDefinitions, rateDefinitions: state.roomsAvailability?.rateDefinitions,
@@ -37,23 +38,13 @@ export default function SelectedRoomPanel() {
) )
)?.images )?.images
if (!rateDefinitions) {
return null
}
const rates = getRates(rateDefinitions)
const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
const freeBooking = intl.formatMessage({ id: "Free rebooking" }) const freeBooking = intl.formatMessage({ id: "Free rebooking" })
const payLater = intl.formatMessage({ id: "Pay later" }) const payLater = intl.formatMessage({ id: "Pay later" })
const payNow = intl.formatMessage({ id: "Pay now" }) const payNow = intl.formatMessage({ id: "Pay now" })
function getRateDetails(rateCode: string) { function getRateTitle(rate: Rate["rate"]) {
const rate = Object.keys(rates).find((k) =>
rates[k as keyof typeof rates].find((a) => a.rateCode === rateCode)
)
switch (rate) { switch (rate) {
case "change": case "change":
return `${freeBooking}, ${payNow}` return `${freeBooking}, ${payNow}`
@@ -65,10 +56,14 @@ export default function SelectedRoomPanel() {
} }
} }
const rate = if (!selectedRate) {
isUserLoggedIn && isMainRoom && selectedRate?.product.productType.member return null
? selectedRate?.product.productType.member }
: selectedRate?.product.productType.public
const selectedProduct =
isUserLoggedIn && isMainRoom && selectedRate.product?.member
? selectedRate.product?.member
: selectedRate.product?.public
return ( return (
<div className={styles.selectedRoomPanel}> <div className={styles.selectedRoomPanel}>
@@ -80,20 +75,21 @@ export default function SelectedRoomPanel() {
)} )}
</Caption> </Caption>
<Subtitle className={styles.subtitle} color="uiTextHighContrast"> <Subtitle className={styles.subtitle} color="uiTextHighContrast">
{selectedRate?.roomType} {selectedRate.roomType}
</Subtitle> </Subtitle>
<Body color="uiTextMediumContrast"> <Body color="uiTextMediumContrast">
{rate?.rateCode ? getRateDetails(rate.rateCode) : null} {getRateTitle(selectedRate.product.rate)}
</Body> </Body>
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
{rate?.localPrice.pricePerNight} {rate?.localPrice.currency}/ {selectedProduct?.localPrice.pricePerNight}{" "}
{selectedProduct?.localPrice.currency}/
{intl.formatMessage({ id: "night" })} {intl.formatMessage({ id: "night" })}
</Body> </Body>
</div> </div>
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
{images?.[0]?.imageSizes?.tiny ? ( {images?.[0]?.imageSizes?.tiny ? (
<Image <Image
alt={selectedRate?.roomType ?? images[0].metaData?.altText ?? ""} alt={selectedRate.roomType ?? images[0].metaData?.altText ?? ""}
className={styles.img} className={styles.img}
height={300} height={300}
src={images[0].imageSizes.tiny} src={images[0].imageSizes.tiny}

View File

@@ -64,8 +64,9 @@ export default function PriceList({
// When it is Promotion rate either booking code rate or public // When it is Promotion rate either booking code rate or public
// Show striked Regular rate which is overtaken by the Promotional rate when member rate is not available // Show striked Regular rate which is overtaken by the Promotional rate when member rate is not available
const showOvertakingPrice = const showOvertakingPrice = !!(
!memberLocalPrice && publicLocalPrice.regularPricePerNight !memberLocalPrice && publicLocalPrice.regularPricePerNight
)
const priceLabelColor = const priceLabelColor =
rateTitle && !memberLocalPrice ? "red" : "uiTextHighContrast" rateTitle && !memberLocalPrice ? "red" : "uiTextHighContrast"

View File

@@ -16,10 +16,10 @@ import PriceTable from "./PriceList"
import styles from "./flexibilityOption.module.css" import styles from "./flexibilityOption.module.css"
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
import { RateTypeEnum } from "@/types/enums/rateType"
export default function FlexibilityOption({ export default function FlexibilityOption({
features, features,
isSelected,
paymentTerm, paymentTerm,
priceInformation, priceInformation,
petRoomPackage, petRoomPackage,
@@ -35,6 +35,7 @@ export default function FlexibilityOption({
actions: { selectRate }, actions: { selectRate },
isMainRoom, isMainRoom,
roomNr, roomNr,
selectedRate,
} = useRoomContext() } = useRoomContext()
function handleSelect() { function handleSelect() {
@@ -48,7 +49,11 @@ export default function FlexibilityOption({
} }
} }
if (!product) { const isBookingCodeRate = product?.public?.rateType !== RateTypeEnum.Regular
if (
!product ||
(isMainRoom && isUserLoggedIn && !product.member && !isBookingCodeRate)
) {
return ( return (
<div className={styles.noPricesCard}> <div className={styles.noPricesCard}>
<div className={styles.header}> <div className={styles.header}>
@@ -67,9 +72,26 @@ export default function FlexibilityOption({
) )
} }
const { public: publicPrice, member: memberPrice } = product.productType const productMember = product.member
const rate = const selectedRateMember = selectedRate?.product.member
isUserLoggedIn && isMainRoom && memberPrice ? memberPrice : publicPrice const bothMemberExist = productMember && selectedRateMember
const selectedRateIsMember =
bothMemberExist && productMember.rateCode === selectedRateMember.rateCode
const productPublic = product.public
const selectedRatePublic = selectedRate?.product.public
const bothPublicExist = productPublic && selectedRatePublic
const selectedRateIsPublic =
bothPublicExist && productPublic.rateCode === selectedRatePublic.rateCode
const isSelected = !!(
(selectedRateIsMember || selectedRateIsPublic) &&
selectedRate?.roomTypeCode === roomTypeCode
)
const rate = (
isUserLoggedIn && isMainRoom && product.member && !isBookingCodeRate
? product.member
: product.public
)!
return ( return (
<label> <label>
@@ -125,9 +147,9 @@ export default function FlexibilityOption({
</div> </div>
</div> </div>
<PriceTable <PriceTable
memberPrice={memberPrice} memberPrice={product.member}
petRoomPackage={petRoomPackage} petRoomPackage={petRoomPackage}
publicPrice={publicPrice} publicPrice={product.public}
rateTitle={rateTitle} rateTitle={rateTitle}
/> />

View File

@@ -7,7 +7,6 @@ import { useIntl } from "react-intl"
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"
import { getRates } from "@/components/HotelReservation/SelectRate/utils"
import { getIconForFeatureCode } from "@/components/HotelReservation/utils" import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
import { ErrorCircleIcon, PriceTagIcon } from "@/components/Icons" import { ErrorCircleIcon, PriceTagIcon } from "@/components/Icons"
import ImageGallery from "@/components/ImageGallery" import ImageGallery from "@/components/ImageGallery"
@@ -91,7 +90,11 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
rateDefinitions: state.roomsAvailability?.rateDefinitions, rateDefinitions: state.roomsAvailability?.rateDefinitions,
roomCategories: state.roomCategories, roomCategories: state.roomCategories,
})) }))
const { isMainRoom, roomNr, selectedPackage, selectedRate } = useRoomContext() const { isMainRoom, roomNr, selectedPackage } = useRoomContext()
if (!rateDefinitions) {
return null
}
const classNames = cardVariants({ const classNames = cardVariants({
availability: availability:
@@ -119,12 +122,6 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
roomNr roomNr
) )
if (!rateDefinitions) {
return null
}
const rates = getRates(rateDefinitions)
const petRoomPackageSelected = const petRoomPackageSelected =
(selectedPackage === RoomPackageCodeEnum.PET_ROOM && petRoomPackage) || (selectedPackage === RoomPackageCodeEnum.PET_ROOM && petRoomPackage) ||
undefined undefined
@@ -144,93 +141,19 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const payLater = intl.formatMessage({ id: "Pay later" }) const payLater = intl.formatMessage({ id: "Pay later" })
const payNow = intl.formatMessage({ id: "Pay now" }) const payNow = intl.formatMessage({ id: "Pay now" })
function getRate(rateCode: string) { function getRateTitle(rateCode: Product["rate"]) {
switch (rateCode) { switch (rateCode) {
case "change": case "change":
return { return freeBooking
isFlex: false,
notAvailable: false,
title: freeBooking,
}
case "flex": case "flex":
return { return freeCancelation
isFlex: true,
notAvailable: false,
title: freeCancelation,
}
case "save": case "save":
return { return nonRefundable
isFlex: false,
notAvailable: false,
title: nonRefundable,
}
default: default:
throw new Error( return ""
`Unknown key for rate, should be "change", "flex", "save", but got ${rateCode}`
)
} }
} }
function getRateInfo(product: Product) {
if (
!product.productType.public.rateCode &&
!product.productType.member?.rateCode
) {
const possibleRate = getRate(product.productType.public.rate)
if (possibleRate) {
return {
...possibleRate,
notAvailable: true,
}
}
return {
isFlex: false,
notAvailable: true,
title: "",
}
}
const publicRate = Object.keys(rates).find((k) =>
rates[k as keyof typeof rates].find(
(a) => a.rateCode === product.productType.public.rateCode
)
)
let memberRate
if (product.productType.member) {
memberRate = Object.keys(rates).find((k) =>
rates[k as keyof typeof rates].find(
(a) => a.rateCode === product.productType.member!.rateCode
)
)
}
// At least one rate is required to proceed here
if (!publicRate && !memberRate) {
throw new Error(
"We should never make it here without any single available rateCode"
)
}
// Booking code scenario which has various rate types in which only
// public rate code is allowed/obtained from the API
const isBookingCodeRate =
product.productType.public.rateType !== RateTypeEnum.Regular
if (isBookingCodeRate) {
// @ts-expect-error: <(publicRate || memberRate) types as `string | undefined` instead of just `string`>
return getRate(publicRate || memberRate)
}
// Regular rates (Save, Change, Flex) requires both public and member rates availability
if (!publicRate || !memberRate) {
throw new Error(
"We should never make it here without both public and member rateCodes"
)
}
const key = isUserLoggedIn && isMainRoom ? memberRate : publicRate
return getRate(key)
}
/** /**
* 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
@@ -244,16 +167,16 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
rateDefinitions: RateDefinition[] rateDefinitions: RateDefinition[]
) { ) {
return rateDefinitions.find((rateDefinition) => return rateDefinitions.find((rateDefinition) =>
isUserLoggedIn && product.productType.member isUserLoggedIn && product.member && isMainRoom
? rateDefinition.rateCode === product.productType.member.rateCode ? rateDefinition.rateCode === product.member?.rateCode
: rateDefinition.rateCode === product.productType.public.rateCode : rateDefinition.rateCode === product.public?.rateCode
) )
} }
const isBookingCodeRate = const isBookingCodeRate =
bookingCode && bookingCode &&
roomConfiguration.products.every((item) => { roomConfiguration.products.every((item) => {
return item.productType.public.rateType !== RateTypeEnum.Regular return item.public?.rateType !== RateTypeEnum.Regular
}) })
return ( return (
@@ -294,9 +217,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
<Caption color="uiTextMediumContrast"> <Caption color="uiTextMediumContrast">
{occupancy.max === occupancy.min {occupancy.max === occupancy.min
? intl.formatMessage( ? intl.formatMessage(
{ id: "guests.plural" }, { id: "guests.plural" },
{ guests: occupancy.max } { guests: occupancy.max }
) )
: intl.formatMessage({ id: "guests.span" }, occupancy)} : intl.formatMessage({ id: "guests.span" }, occupancy)}
</Caption> </Caption>
)} )}
@@ -349,33 +272,25 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
) : null} ) : null}
</span> </span>
{roomConfiguration.products.map((product) => { {roomConfiguration.products.map((product) => {
const rate = getRateInfo(product) const rateTitle = getRateTitle(product.rate)
const isAvailable =
product.public ||
(product.member && isUserLoggedIn && isMainRoom)
const rateDefinition = getRateDefinition(product, rateDefinitions) const rateDefinition = getRateDefinition(product, rateDefinitions)
const isSelectedRateCode =
selectedRate?.product.productType.public.rateCode ===
product.productType.public.rateCode ||
(selectedRate?.product.productType.member?.rateCode ===
product.productType.member?.rateCode &&
// handle undefined === undefined scenarios in booking code rates
product.productType.member?.rateCode !== undefined)
return ( return (
<FlexibilityOption <FlexibilityOption
key={rate.title} key={product.rate}
features={roomConfiguration.features} features={roomConfiguration.features}
isSelected={ paymentTerm={product.isFlex ? payLater : payNow}
isSelectedRateCode &&
selectedRate?.roomTypeCode ===
roomConfiguration.roomTypeCode
}
paymentTerm={rate.isFlex ? payLater : payNow}
petRoomPackage={petRoomPackageSelected} petRoomPackage={petRoomPackageSelected}
product={rate.notAvailable ? undefined : product} priceInformation={rateDefinition?.generalTerms}
product={isAvailable ? product : undefined}
roomType={roomConfiguration.roomType} roomType={roomConfiguration.roomType}
roomTypeCode={roomConfiguration.roomTypeCode} roomTypeCode={roomConfiguration.roomTypeCode}
title={rate.title} title={rateTitle}
priceInformation={rateDefinition?.generalTerms}
rateTitle={ rateTitle={
product.productType.public.rateType !== RateTypeEnum.Regular product.public &&
product.public?.rateType !== RateTypeEnum.Regular
? rateDefinition?.title ? rateDefinition?.title
: undefined : undefined
} }

View File

@@ -1,9 +1,11 @@
"use client" "use client"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useEffect } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { alternativeHotels } from "@/constants/routes/hotelReservation" import { alternativeHotels } from "@/constants/routes/hotelReservation"
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter" import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
import { useRatesStore } from "@/stores/select-rate"
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter" import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
import Alert from "@/components/TempDesignSystem/Alert" import Alert from "@/components/TempDesignSystem/Alert"
@@ -22,6 +24,9 @@ import { RateTypeEnum } from "@/types/enums/rateType"
export default function RoomSelectionPanel() { export default function RoomSelectionPanel() {
const { rooms } = useRoomContext() const { rooms } = useRoomContext()
const isSingleRoomAndHasSelection = useRatesStore(
(state) => state.booking.rooms.length === 1 && !!state.rateSummary.length
)
const searchParams = useSearchParams() const searchParams = useSearchParams()
const bookingCode = searchParams.get("bookingCode") const bookingCode = searchParams.get("bookingCode")
const intl = useIntl() const intl = useIntl()
@@ -41,8 +46,7 @@ export default function RoomSelectionPanel() {
(room) => (room) =>
room.status === AvailabilityEnum.Available && room.status === AvailabilityEnum.Available &&
room.products.some( room.products.some(
(product) => (product) => product.public?.rateType === RateTypeEnum.Regular
product.productType.public.rateType === RateTypeEnum.Regular
) )
) )
@@ -56,8 +60,7 @@ export default function RoomSelectionPanel() {
(room) => (room) =>
room.status === AvailabilityEnum.Available && room.status === AvailabilityEnum.Available &&
room.products.some( room.products.some(
(product) => (product) => product.public?.rateType !== RateTypeEnum.Regular
product.productType.public.rateType !== RateTypeEnum.Regular
) )
) )
@@ -71,22 +74,44 @@ export default function RoomSelectionPanel() {
(room) => (room) =>
room.status === AvailabilityEnum.Available && room.status === AvailabilityEnum.Available &&
room.products.every( room.products.every(
(product) => (product) => product.public?.rateType !== RateTypeEnum.Regular
product.productType.public.rateType !== RateTypeEnum.Regular
) )
) )
const regularRateRooms = rooms.filter( const regularRateRooms = rooms.filter(
(room) => (room) =>
room.status === AvailabilityEnum.Available && room.status === AvailabilityEnum.Available &&
room.products.every( room.products.every(
(product) => (product) => product.public?.rateType === RateTypeEnum.Regular
product.productType.public.rateType === RateTypeEnum.Regular
) )
) )
// Show booking code filter when both of the booking code rates or regular rates are available // Show booking code filter when both of the booking code rates or regular rates are available
const showBookingCodeFilter = const showBookingCodeFilter =
isRegularRatesAvailableWithCode && isBookingCodeRatesAvailable isRegularRatesAvailableWithCode && isBookingCodeRatesAvailable
useEffect(() => {
if (isSingleRoomAndHasSelection) {
// Required to prevent the history.pushState on the first selection
// to scroll user back to top
requestAnimationFrame(() => {
const SCROLL_OFFSET = 100
const selectedInputRoomCard = document.querySelector(
`.${styles.roomList} li:has(input[type=radio]:checked)`
)
if (selectedInputRoomCard) {
const elementPosition =
selectedInputRoomCard.getBoundingClientRect().top
const offsetPosition =
elementPosition + window.scrollY - SCROLL_OFFSET
window.scrollTo({
top: offsetPosition,
behavior: "instant",
})
}
})
}
}, [isSingleRoomAndHasSelection])
return ( return (
<> <>
{noAvailableRooms || (bookingCode && !isBookingCodeRatesAvailable) ? ( {noAvailableRooms || (bookingCode && !isBookingCodeRatesAvailable) ? (
@@ -117,27 +142,27 @@ export default function RoomSelectionPanel() {
<RoomTypeFilter /> <RoomTypeFilter />
{showBookingCodeFilter ? <BookingCodeFilter /> : null} {showBookingCodeFilter ? <BookingCodeFilter /> : null}
<ul className={styles.roomList}> <ul className={styles.roomList}>
{/* Show either Booking code filtered rooms or all the rooms */} {/* Show either Booking code filtered rooms or all the rooms */}
{showAllRooms {showAllRooms
? rooms.map((roomConfiguration) => ( ? rooms.map((roomConfiguration) => (
<RoomCard
key={roomConfiguration.roomTypeCode}
roomConfiguration={roomConfiguration}
/>
))
: activeCodeFilter === BookingCodeFilterEnum.Discounted
? bookingCodeDiscountedRooms.map((roomConfiguration) => (
<RoomCard <RoomCard
key={roomConfiguration.roomTypeCode} key={roomConfiguration.roomTypeCode}
roomConfiguration={roomConfiguration} roomConfiguration={roomConfiguration}
/> />
)) ))
: activeCodeFilter === BookingCodeFilterEnum.Discounted : regularRateRooms.map((roomConfiguration) => (
? bookingCodeDiscountedRooms.map((roomConfiguration) => ( <RoomCard
<RoomCard key={roomConfiguration.roomTypeCode}
key={roomConfiguration.roomTypeCode} roomConfiguration={roomConfiguration}
roomConfiguration={roomConfiguration} />
/> ))}
))
: regularRateRooms.map((roomConfiguration) => (
<RoomCard
key={roomConfiguration.roomTypeCode}
roomConfiguration={roomConfiguration}
/>
))}
</ul> </ul>
</> </>
) )

View File

@@ -30,10 +30,14 @@ export default function Rooms() {
useEffect(() => { useEffect(() => {
const pricesWithCurrencies = visibleRooms.flatMap((room) => const pricesWithCurrencies = visibleRooms.flatMap((room) =>
room.products.map((product) => ({ room.products
price: product.productType.public.localPrice.pricePerNight, .filter((product) => product.member || product.public)
currency: product.productType.public.localPrice.currency, .map((product) => ({
})) currency: (product.public?.localPrice.currency ||
product.member?.localPrice.currency)!,
price: (product.public?.localPrice.pricePerNight ||
product.member?.localPrice.pricePerNight)!,
}))
) )
const lowestPrice = pricesWithCurrencies.reduce( const lowestPrice = pricesWithCurrencies.reduce(
(minPrice, { price }) => Math.min(minPrice, price), (minPrice, { price }) => Math.min(minPrice, price),

View File

@@ -33,22 +33,6 @@ export function combineRoomAvailabilities(
}, null) }, null)
} }
export function getRates(
rateDefinitions: RoomsAvailability["rateDefinitions"]
) {
return {
change: rateDefinitions.filter(
(rate) => rate.cancellationRule === "Changeable"
),
flex: rateDefinitions.filter(
(rate) => rate.cancellationRule === "CancellableBefore6PM"
),
save: rateDefinitions.filter(
(rate) => rate.cancellationRule === "NotCancellable"
),
}
}
export function useRoomsAvailability( export function useRoomsAvailability(
uniqueAdultsCount: number[], uniqueAdultsCount: number[],
hotelId: number, hotelId: number,

View File

@@ -1,19 +1,23 @@
"use client" "use client"
import deepmerge from "deepmerge"
import { useEffect, useRef } from "react" import { useEffect, useRef } from "react"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { createDetailsStore } from "@/stores/enter-details" import { createDetailsStore } from "@/stores/enter-details"
import { import {
calcTotalPrice, calcTotalPrice,
checkIsSameBedTypes,
checkIsSameBooking as checkIsSameBooking, checkIsSameBooking as checkIsSameBooking,
clearSessionStorage, clearSessionStorage,
readFromSessionStorage, readFromSessionStorage,
writeToSessionStorage,
} from "@/stores/enter-details/helpers" } from "@/stores/enter-details/helpers"
import { multiroomDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema"
import { guestDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema"
import { DetailsContext } from "@/contexts/Details" import { DetailsContext } from "@/contexts/Details"
import type { DetailsStore } from "@/types/contexts/enter-details" import type { DetailsStore } from "@/types/contexts/enter-details"
import { StepEnum } from "@/types/enums/step"
import type { DetailsProviderProps } from "@/types/providers/enter-details" import type { DetailsProviderProps } from "@/types/providers/enter-details"
import type { InitialState } from "@/types/stores/enter-details" import type { InitialState } from "@/types/stores/enter-details"
@@ -71,46 +75,80 @@ export default function EnterDetailsProvider({
return return
} }
const store = storeRef.current?.getState()
if (!store) {
return
}
const updatedRooms = storedValues.rooms.map((storedRoom, idx) => { const updatedRooms = storedValues.rooms.map((storedRoom, idx) => {
const currentRoom = booking.rooms[idx] // Need to create a deep new copy
const room = rooms[idx] // since store is readonly
const currentRoom = deepmerge({}, store.rooms[idx])
if (!storedRoom.room?.bedType) { if (!currentRoom.room.bedType && storedRoom.room.bedType) {
return storedRoom const sameBed = currentRoom.room.bedTypes.find(
} (bedType) => bedType.value === storedRoom.room.bedType?.roomTypeCode
)
const isSameBedTypes = checkIsSameBedTypes( if (sameBed) {
storedRoom.room.bedType.roomTypeCode, currentRoom.room.bedType = {
currentRoom.roomTypeCode description: sameBed.description,
) roomTypeCode: sameBed.value,
if (isSameBedTypes) { }
return storedRoom currentRoom.steps[StepEnum.selectBed].isValid = true
}
if (room?.bedTypes?.length === 1 && room.bedTypes[0]) {
return {
...storedRoom,
bedType: {
roomTypeCode: room.bedTypes[0].value,
description: room.bedTypes[0].description,
},
} }
} }
// Remove bed type selection if bedtypes change if (
return { currentRoom.steps[StepEnum.breakfast] &&
...storedRoom, currentRoom.room.breakfast === undefined &&
bedType: undefined, (storedRoom.room.breakfast || storedRoom.room.breakfast === false)
) {
currentRoom.room.breakfast = storedRoom.room.breakfast
currentRoom.steps[StepEnum.breakfast].isValid = true
} }
// User is already added for main room
if (!user || (user && idx > 0)) {
currentRoom.room.guest = deepmerge(
currentRoom.room.guest,
storedRoom.room.guest
)
}
const validGuest =
idx > 0
? multiroomDetailsSchema.safeParse(currentRoom.room.guest)
: guestDetailsSchema.safeParse(currentRoom.room.guest)
if (validGuest.success) {
currentRoom.steps[StepEnum.details].isValid = true
}
const invalidStep = Object.values(currentRoom.steps).find(
(step) => !step.isValid
)
currentRoom.isComplete = !invalidStep
currentRoom.currentStep = invalidStep ? invalidStep.step : null
return currentRoom
}) })
const canProceedToPayment = updatedRooms.every((room) => room.isComplete) const canProceedToPayment = updatedRooms.every((room) => room.isComplete)
const nights = dt(booking.toDate).diff(booking.fromDate, "days") const nights = dt(booking.toDate).diff(booking.fromDate, "days")
const currency = const currency = (updatedRooms[0].room.roomRate.publicRate?.localPrice
updatedRooms[0].room.roomRate.publicRate.localPrice.currency .currency ||
updatedRooms[0].room.roomRate.memberRate?.localPrice.currency)!
const totalPrice = calcTotalPrice(updatedRooms, currency, !!user, nights) const totalPrice = calcTotalPrice(updatedRooms, currency, !!user, nights)
const activeRoom = updatedRooms.findIndex((room) => !room.isComplete)
writeToSessionStorage({
activeRoom,
booking,
rooms: updatedRooms,
})
storeRef.current?.setState({ storeRef.current?.setState({
activeRoom: storedValues.activeRoom, activeRoom: storedValues.activeRoom,
canProceedToPayment, canProceedToPayment,

View File

@@ -44,11 +44,8 @@ const roomsSchema = z.array(
accessibility: z.boolean(), accessibility: z.boolean(),
}), }),
roomPrice: z.object({ roomPrice: z.object({
publicPrice: z.number().or(z.string().transform((val) => Number(val))), memberPrice: z.number().nullish(),
memberPrice: z publicPrice: z.number().nullish(),
.number()
.or(z.string().transform((val) => Number(val)))
.optional(),
}), }),
}) })
) )

View File

@@ -105,7 +105,7 @@ function everyRateHasBreakfastIncluded(
userType: "member" | "public" userType: "member" | "public"
) { ) {
const rateDefinition = rateDefinitions.find( const rateDefinition = rateDefinitions.find(
(rd) => rd.rateCode === product.productType[userType]?.rateCode (rd) => rd.rateCode === product[userType]?.rateCode
) )
if (!rateDefinition) { if (!rateDefinition) {
return false return false
@@ -113,10 +113,7 @@ function everyRateHasBreakfastIncluded(
return rateDefinition.breakfastIncluded return rateDefinition.breakfastIncluded
} }
function getRate(rate: RateDefinition | undefined) { function getRate(rate: RateDefinition) {
if (!rate) {
return null
}
switch (rate.cancellationRule) { switch (rate.cancellationRule) {
case "CancellableBefore6PM": case "CancellableBefore6PM":
return "flex" return "flex"
@@ -156,77 +153,79 @@ export const roomsAvailabilitySchema = z
type: z.string().optional(), type: z.string().optional(),
}), }),
}) })
.transform((o) => { .transform(({ data: { attributes } }) => {
const cancellationRuleLookup = o.data.attributes.rateDefinitions.reduce( const rateDefinitions = attributes.rateDefinitions
(acc, val) => { const cancellationRuleLookup = rateDefinitions.reduce((acc, val) => {
// @ts-expect-error - index of cancellationRule TS // @ts-expect-error - index of cancellationRule TS
acc[val.rateCode] = cancellationRules[val.cancellationRule] acc[val.rateCode] = cancellationRules[val.cancellationRule]
return acc return acc
}, }, {})
{}
)
o.data.attributes.roomConfigurations = attributes.roomConfigurations = attributes.roomConfigurations.map(
o.data.attributes.roomConfigurations.map((room) => { (room) => {
if (room.products.length) { if (room.products.length) {
room.breakfastIncludedInAllRatesMember = room.products.every( room.breakfastIncludedInAllRatesMember = room.products.every(
(product) => (product) =>
everyRateHasBreakfastIncluded( everyRateHasBreakfastIncluded(product, rateDefinitions, "member")
product,
o.data.attributes.rateDefinitions,
"member"
)
) )
room.breakfastIncludedInAllRatesPublic = room.products.every( room.breakfastIncludedInAllRatesPublic = room.products.every(
(product) => (product) =>
everyRateHasBreakfastIncluded( everyRateHasBreakfastIncluded(product, rateDefinitions, "public")
product,
o.data.attributes.rateDefinitions,
"public"
)
) )
room.products = room.products.map((product) => { room.products = room.products.map((product) => {
const publicRateDefinition = o.data.attributes.rateDefinitions.find( const publicRate = product.public
(rate) => if (publicRate?.rateCode) {
product.productType.public.rateCode const publicRateDefinition = rateDefinitions.find(
? rate.rateCode === product.productType.public.rateCode (rateDefinition) =>
: rate.rateCode === product.productType.public.oldRateCode rateDefinition.rateCode === publicRate.rateCode
) )
const publicRate = getRate(publicRateDefinition) if (publicRateDefinition) {
const memberRateDefinition = o.data.attributes.rateDefinitions.find( const rate = getRate(publicRateDefinition)
(rate) => if (rate) {
product.productType.member?.rateCode product.rate = rate
? rate.rateCode === product.productType.member?.rateCode if (rate === "flex") {
: rate.rateCode === product.productType.member?.oldRateCode product.isFlex = true
) }
const memberRate = getRate(memberRateDefinition) }
}
if (publicRate) {
product.productType.public.rate = publicRate
} }
if (memberRate && product.productType.member) {
product.productType.member.rate = memberRate 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
}) })
// CancellationRule is the same for public and member per product
// Sorting to guarantee order based on rate
room.products = room.products.sort(
(a, b) =>
// @ts-expect-error - index
cancellationRuleLookup[a.public?.rateCode || a.member?.rateCode] -
// @ts-expect-error - index
cancellationRuleLookup[b.public?.rateCode || b.member?.rateCode]
)
} }
// CancellationRule is the same for public and member per product
// Sorting to guarantee order based on rate
room.products = room.products.sort(
(a, b) =>
// @ts-expect-error - index
cancellationRuleLookup[a.productType.public.rateCode] -
// @ts-expect-error - index
cancellationRuleLookup[b.productType.public.rateCode]
)
return room return room
}) }
)
return o.data.attributes return attributes
}) })
export const ratesSchema = z.array(rateSchema) export const ratesSchema = z.array(rateSchema)

View File

@@ -535,7 +535,6 @@ export const hotelQueryRouter = router({
} }
const apiJson = await apiResponse.json() const apiJson = await apiResponse.json()
const validateAvailabilityData = const validateAvailabilityData =
roomsAvailabilitySchema.safeParse(apiJson) roomsAvailabilitySchema.safeParse(apiJson)
@@ -710,8 +709,8 @@ export const hotelQueryRouter = router({
const rateTypes = selectedRoom.products.find( const rateTypes = selectedRoom.products.find(
(rate) => (rate) =>
rate.productType.public?.rateCode === rateCode || rate.public?.rateCode === rateCode ||
rate.productType.member?.rateCode === rateCode rate.member?.rateCode === rateCode
) )
if (!rateTypes) { if (!rateTypes) {
@@ -728,7 +727,7 @@ export const hotelQueryRouter = router({
console.error("No matching rate found") console.error("No matching rate found")
return null return null
} }
const rates = rateTypes.productType const rates = rateTypes
const rateDefinition = const rateDefinition =
validateAvailabilityData.data.rateDefinitions.find( validateAvailabilityData.data.rateDefinitions.find(
@@ -786,7 +785,7 @@ export const hotelQueryRouter = router({
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed, mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
breakfastIncluded: !!rateDefinition?.breakfastIncluded, breakfastIncluded: !!rateDefinition?.breakfastIncluded,
memberRate: rates?.member, memberRate: rates?.member,
publicRate: rates.public, publicRate: rates?.public,
bedTypes, bedTypes,
} }
}), }),

View File

@@ -15,10 +15,4 @@ export const productTypePriceSchema = z.object({
rateCode: z.string(), rateCode: z.string(),
rateType: z.string().optional(), rateType: z.string().optional(),
requestedPrice: priceSchema.optional(), requestedPrice: priceSchema.optional(),
// This is only used when a product is filtered out
// so that we can still map out the correct titles a.so.
oldRateCode: z.string().default(""),
// Used to set the rate that we use to chose
// titles etc.
rate: z.string().default(""),
}) })

View File

@@ -1,11 +1,9 @@
import deepmerge from "deepmerge"
import { z } from "zod" import { z } from "zod"
import { productSchema } from "./product" import { productSchema } from "./product"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { RateTypeEnum } from "@/types/enums/rateType"
export const roomConfigurationSchema = z export const roomConfigurationSchema = z
.object({ .object({
@@ -31,60 +29,15 @@ export const roomConfigurationSchema = z
}) })
.transform((data) => { .transform((data) => {
if (data.products.length) { if (data.products.length) {
const someProductsMissAtLeastOneRateCode = data.products.some(
({ productType }) =>
!productType.public.rateCode || !productType.member?.rateCode
)
if (someProductsMissAtLeastOneRateCode) {
data.products = data.products.map((product) => {
if (
product.productType.public.rateCode &&
product.productType.member?.rateCode
) {
return product
}
// Return rate with only public available when it is a booking code rate
// which can be any one of other rate types
if (product.productType.public.rateType !== RateTypeEnum.Regular) {
return product
}
/**
* Reset both rateCodes if one is missing to show `No prices available` for the same reason as
* mentioned above.
*
* TODO: (Maybe) notify somewhere that this happened
*/
return deepmerge(product, {
productType: {
member: {
rateCode: "",
oldRateCode: product.productType.member?.rateCode,
},
public: {
rateCode: "",
oldRateCode: product.productType.public.rateCode,
},
},
})
})
}
/** /**
* When all products miss at least one rateCode (member or public), we change the status to NotAvailable * Just guaranteeing that if all products all miss
* since we cannot as of now (31 january) guarantee the flow with missing rateCodes. * both public and member rateCode that status is
* This rule applies to regular rates (Save, Change and Flex) * set to `NotAvailable`
* Exception Booking code rate
*
* TODO: (Maybe) notify somewhere that this happened
*/ */
const allProductsMissAtLeastOneRateCode = data.products.every( const allProductsMissBothRateCodes = data.products.every(
({ productType }) => (product) => !product.public?.rateCode && !product.member?.rateCode
(!productType.public.rateCode || !productType.member?.rateCode) &&
productType.public.rateType === RateTypeEnum.Regular
) )
if (allProductsMissAtLeastOneRateCode) { if (allProductsMissBothRateCodes) {
data.status = AvailabilityEnum.NotAvailable data.status = AvailabilityEnum.NotAvailable
} }
} }

View File

@@ -2,20 +2,19 @@ import { z } from "zod"
import { productTypePriceSchema } from "../productTypePrice" import { productTypePriceSchema } from "../productTypePrice"
import { CurrencyEnum } from "@/types/enums/currency" export const productSchema = z
.object({
export const productSchema = z.object({ // Is product flex rate
productType: z.object({ isFlex: z.boolean().default(false),
member: productTypePriceSchema.optional(), productType: z.object({
public: productTypePriceSchema.default({ member: productTypePriceSchema.optional(),
localPrice: { public: productTypePriceSchema.optional(),
currency: CurrencyEnum.SEK,
pricePerNight: 0,
pricePerStay: 0,
},
rateCode: "",
rateType: "",
requestedPrice: undefined,
}), }),
}), // Used to set the rate that we use to chose titles etc.
}) rate: z.enum(["change", "flex", "save"]).default("save"),
})
.transform((data) => ({
...data.productType,
isFlex: data.isFlex,
rate: data.rate,
}))

View File

@@ -25,13 +25,6 @@ export function extractGuestFromUser(user: NonNullable<SafeUser>) {
} }
} }
export function checkIsSameBedTypes(
storedBedTypes: string,
bedTypesData: string
) {
return storedBedTypes === bedTypesData
}
export function checkIsSameBooking( export function checkIsSameBooking(
prev: SelectRateSearchParams, prev: SelectRateSearchParams,
next: SelectRateSearchParams next: SelectRateSearchParams
@@ -111,28 +104,34 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
} }
} }
return { if (roomRate.publicRate) {
perNight: { return {
requested: roomRate.publicRate.requestedPrice && { perNight: {
currency: roomRate.publicRate.requestedPrice.currency, requested: roomRate.publicRate.requestedPrice && {
price: roomRate.publicRate.requestedPrice.pricePerNight, currency: roomRate.publicRate.requestedPrice.currency,
price: roomRate.publicRate.requestedPrice.pricePerNight,
},
local: {
currency: roomRate.publicRate.localPrice.currency,
price: roomRate.publicRate.localPrice.pricePerNight,
},
}, },
local: { perStay: {
currency: roomRate.publicRate.localPrice.currency, requested: roomRate.publicRate.requestedPrice && {
price: roomRate.publicRate.localPrice.pricePerNight, currency: roomRate.publicRate.requestedPrice.currency,
price: roomRate.publicRate.requestedPrice.pricePerStay,
},
local: {
currency: roomRate.publicRate.localPrice.currency,
price: roomRate.publicRate.localPrice.pricePerStay,
},
}, },
}, }
perStay: {
requested: roomRate.publicRate.requestedPrice && {
currency: roomRate.publicRate.requestedPrice.currency,
price: roomRate.publicRate.requestedPrice.pricePerStay,
},
local: {
currency: roomRate.publicRate.localPrice.currency,
price: roomRate.publicRate.localPrice.pricePerStay,
},
},
} }
throw new Error(
`Unable to calculate RoomPrice since user is neither a member or memberRate is missing, or publicRate is missing`
)
} }
type TotalPrice = { type TotalPrice = {
@@ -149,6 +148,10 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
? roomRate.memberRate ? roomRate.memberRate
: roomRate.publicRate : roomRate.publicRate
if (!rate) {
return total
}
return { return {
requested: rate.requestedPrice requested: rate.requestedPrice
? { ? {
@@ -168,7 +171,8 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
{ {
requested: undefined, requested: undefined,
local: { local: {
currency: roomRates[0].publicRate.localPrice.currency, currency: (roomRates[0].publicRate?.localPrice.currency ||
roomRates[0].memberRate?.localPrice.currency)!,
price: 0, price: 0,
}, },
} }
@@ -191,6 +195,10 @@ export function calcTotalPrice(
isFirstRoomAndMember || join isFirstRoomAndMember || join
) )
if (!roomPrice) {
return acc
}
const breakfastRequestedPrice = room.breakfast const breakfastRequestedPrice = room.breakfast
? parseInt(room.breakfast.requestedPrice?.price ?? 0) ? parseInt(room.breakfast.requestedPrice?.price ?? 0)
: 0 : 0

View File

@@ -25,7 +25,6 @@ import type {
DetailsState, DetailsState,
InitialState, InitialState,
RoomState, RoomState,
RoomStatus,
} from "@/types/stores/enter-details" } from "@/types/stores/enter-details"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
@@ -74,7 +73,7 @@ export function createDetailsStore(
}) })
const rooms: RoomState[] = initialState.rooms.map((room, idx) => { const rooms: RoomState[] = initialState.rooms.map((room, idx) => {
const steps: RoomStatus["steps"] = { const steps: RoomState["steps"] = {
[StepEnum.selectBed]: { [StepEnum.selectBed]: {
step: StepEnum.selectBed, step: StepEnum.selectBed,
isValid: !!room.bedType, isValid: !!room.bedType,
@@ -225,6 +224,13 @@ export function createDetailsStore(
state.rooms[idx].steps[StepEnum.selectBed].isValid = true state.rooms[idx].steps[StepEnum.selectBed].isValid = true
state.rooms[idx].room.bedType = bedType state.rooms[idx].room.bedType = bedType
const isAllStepsCompleted = checkRoomProgress(
state.rooms[idx].steps
)
if (isAllStepsCompleted) {
state.rooms[idx].isComplete = true
}
handleStepProgression(state.rooms[idx], state) handleStepProgression(state.rooms[idx], state)
writeToSessionStorage({ writeToSessionStorage({
@@ -331,6 +337,13 @@ export function createDetailsStore(
currentRoom.room.breakfast = breakfast currentRoom.room.breakfast = breakfast
const isAllStepsCompleted = checkRoomProgress(
state.rooms[idx].steps
)
if (isAllStepsCompleted) {
state.rooms[idx].isComplete = true
}
handleStepProgression(currentRoom, state) handleStepProgression(currentRoom, state)
writeToSessionStorage({ writeToSessionStorage({

View File

@@ -1,70 +1,6 @@
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import {
RoomPackageCodeEnum,
type RoomPackages,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type {
Rate,
RateCode,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
interface CalculateRoomSummaryParams {
availablePackages: RoomPackages
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }>
selectedPackagesByRoom: Record<number, RoomPackageCodeEnum[]>
selectedRate: RateCode
roomIndex: number
}
export function calculateRoomSummary({
selectedRate,
roomIndex,
getFilteredRooms,
availablePackages,
roomCategories,
selectedPackagesByRoom,
}: CalculateRoomSummaryParams): Rate | null {
const filteredRooms = getFilteredRooms(roomIndex)
const selectedPackages = selectedPackagesByRoom[roomIndex] || []
const room = filteredRooms.find(
(room) => room.roomTypeCode === selectedRate.roomTypeCode
)
if (!room) return null
const product = room.products.find(
(product) =>
product.productType.public.rateCode === selectedRate.publicRateCode
)
if (!product) return null
const petRoomPackage = selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM)
? availablePackages.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)
: undefined
const features = filteredRooms.find((room) =>
room.features.some(
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
)
)?.features
const roomType = roomCategories.find((roomCategory) =>
roomCategory.roomTypes.some((type) => type.code === room.roomTypeCode)
)
return {
features: petRoomPackage && features ? features : [],
priceName: selectedRate.name,
priceTerm: selectedRate.paymentTerm,
public: product.productType.public,
member: product.productType.member,
roomType: roomType?.name ?? room.roomType,
roomTypeCode: room.roomTypeCode,
}
}
/** /**
* Get the lowest priced room for each room type that appears more than once. * Get the lowest priced room for each room type that appears more than once.
*/ */
@@ -119,12 +55,11 @@ export function filterDuplicateRoomTypesByLowestPrice(
if (previousRoom) { if (previousRoom) {
products.forEach((product) => { products.forEach((product) => {
const { productType } = product const publicProduct = product?.public || {
const publicProduct = productType.public || {
requestedPrice: null, requestedPrice: null,
localPrice: null, localPrice: null,
} }
const memberProduct = productType.member || { const memberProduct = product?.member || {
requestedPrice: null, requestedPrice: null,
localPrice: null, localPrice: null,
} }
@@ -154,34 +89,28 @@ export function filterDuplicateRoomTypesByLowestPrice(
currentRequestedPrice < currentRequestedPrice <
Math.min( Math.min(
Number( Number(
previousLowest.products[0].productType.public.requestedPrice previousLowest.products[0].public?.requestedPrice?.pricePerNight
?.pricePerNight
) ?? Infinity, ) ?? Infinity,
Number( Number(
previousLowest.products[0].productType.member?.requestedPrice previousLowest.products[0].member?.requestedPrice?.pricePerNight
?.pricePerNight
) ?? Infinity ) ?? Infinity
) || ) ||
(currentRequestedPrice === (currentRequestedPrice ===
Math.min( Math.min(
Number( Number(
previousLowest.products[0].productType.public.requestedPrice previousLowest.products[0].public?.requestedPrice?.pricePerNight
?.pricePerNight
) ?? Infinity, ) ?? Infinity,
Number( Number(
previousLowest.products[0].productType.member?.requestedPrice previousLowest.products[0].member?.requestedPrice?.pricePerNight
?.pricePerNight
) ?? Infinity ) ?? Infinity
) && ) &&
currentLocalPrice < currentLocalPrice <
Math.min( Math.min(
Number( Number(
previousLowest.products[0].productType.public.localPrice previousLowest.products[0].public?.localPrice?.pricePerNight
?.pricePerNight
) ?? Infinity, ) ?? Infinity,
Number( Number(
previousLowest.products[0].productType.member?.localPrice previousLowest.products[0].member?.localPrice?.pricePerNight
?.pricePerNight
) ?? Infinity ) ?? Infinity
)) ))
) { ) {

View File

@@ -9,6 +9,7 @@ import { RatesContext } from "@/contexts/Rates"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { RateTypeEnum } from "@/types/enums/rateType"
import type { InitialState, RatesState } from "@/types/stores/rates" import type { InitialState, RatesState } from "@/types/stores/rates"
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
@@ -27,8 +28,8 @@ function findSelectedRate(
room.roomTypeCode === roomTypeCode && room.roomTypeCode === roomTypeCode &&
room.products.find( room.products.find(
(product) => (product) =>
product.productType.public.rateCode === rateCode || product.public?.rateCode === rateCode ||
product.productType.member?.rateCode === rateCode product.member?.rateCode === rateCode
) )
) )
} }
@@ -87,21 +88,22 @@ export function createRatesStore({
roomConf.roomTypeCode === room.roomTypeCode && roomConf.roomTypeCode === room.roomTypeCode &&
roomConf.products.find( roomConf.products.find(
(product) => (product) =>
product.productType.public.rateCode === room.rateCode || product.public?.rateCode === room.rateCode ||
product.productType.member?.rateCode === room.rateCode product.member?.rateCode === room.rateCode
) )
) )
const product = selectedRoom?.products.find( const product = selectedRoom?.products.find(
(p) => (p) =>
p.productType.public.rateCode === room.rateCode || p.public?.rateCode === room.rateCode ||
p.productType.member?.rateCode === room.rateCode p.member?.rateCode === room.rateCode
) )
if (selectedRoom && product) { if (selectedRoom && product) {
rateSummary[idx] = { rateSummary[idx] = {
features: selectedRoom.features, features: selectedRoom.features,
member: product.productType.member, member: product.member,
public: product.productType.public, public: product.public,
rate: product.rate,
roomType: selectedRoom.roomType, roomType: selectedRoom.roomType,
roomTypeCode: selectedRoom.roomTypeCode, roomTypeCode: selectedRoom.roomTypeCode,
} }
@@ -180,35 +182,48 @@ export function createRatesStore({
return function (selectedRate) { return function (selectedRate) {
return set( return set(
produce((state: RatesState) => { produce((state: RatesState) => {
const memberRate = selectedRate.product.member
const publicRate = selectedRate.product.public
if (!memberRate && !publicRate) {
return
}
state.rooms[idx].selectedRate = selectedRate state.rooms[idx].selectedRate = selectedRate
state.rateSummary[idx] = { state.rateSummary[idx] = {
features: selectedRate.features, features: selectedRate.features,
member: selectedRate.product.productType.member, member: selectedRate.product.member,
package: state.rooms[idx].selectedPackage, package: state.rooms[idx].selectedPackage,
public: selectedRate.product.productType.public, rate: selectedRate.product.rate,
public: selectedRate.product.public,
roomType: selectedRate.roomType, roomType: selectedRate.roomType,
roomTypeCode: selectedRate.roomTypeCode, roomTypeCode: selectedRate.roomTypeCode,
} }
const isBookingCodeRate =
selectedRate.product.public?.rateType !== RateTypeEnum.Regular
const roomNr = idx + 1 const roomNr = idx + 1
const isMainRoom = roomNr + 1
const isMemberRate = const isMemberRate =
isUserLoggedIn && isUserLoggedIn && isMainRoom && memberRate && !isBookingCodeRate
roomNr === 1 &&
selectedRate.product.productType.member
const searchParams = new URLSearchParams(state.searchParams) const searchParams = new URLSearchParams(state.searchParams)
searchParams.set( const counterratecode = isMemberRate
`room[${idx}].counterratecode`, ? (publicRate?.rateCode ?? "")
isMemberRate : (memberRate?.rateCode ?? "")
? selectedRate.product.productType.public.rateCode if (counterratecode) {
: (selectedRate.product.productType.member?.rateCode ?? "") searchParams.set(
) `room[${idx}].counterratecode`,
searchParams.set( counterratecode
`room[${idx}].ratecode`, )
isMemberRate }
? // already checked in isMemberRate
selectedRate.product.productType.member!.rateCode const rateCode = isMemberRate
: selectedRate.product.productType.public.rateCode ? memberRate.rateCode
) : (publicRate?.rateCode ?? "")
if (rateCode) {
searchParams.set(`room[${idx}].ratecode`, rateCode)
}
searchParams.set( searchParams.set(
`room[${idx}].roomtype`, `room[${idx}].roomtype`,
selectedRate.roomTypeCode selectedRate.roomTypeCode
@@ -249,8 +264,8 @@ export function createRatesStore({
const product = selectedRate?.products.find( const product = selectedRate?.products.find(
(prd) => (prd) =>
prd.productType.public.rateCode === room.rateCode || prd.public?.rateCode === room.rateCode ||
prd.productType.member?.rateCode === room.rateCode prd.member?.rateCode === room.rateCode
) )
const selectedPackage = room.packages?.[0] const selectedPackage = room.packages?.[0]
@@ -259,18 +274,18 @@ export function createRatesStore({
bookingRoom: room, bookingRoom: room,
rooms: selectedPackage rooms: selectedPackage
? allRooms.filter((r) => ? allRooms.filter((r) =>
r.features.find((f) => f.code === selectedPackage) r.features.find((f) => f.code === selectedPackage)
) )
: allRooms, : allRooms,
selectedPackage, selectedPackage,
selectedRate: selectedRate:
selectedRate && product selectedRate && product
? { ? {
features: selectedRate.features, features: selectedRate.features,
product, product,
roomType: selectedRate.roomType, roomType: selectedRate.roomType,
roomTypeCode: selectedRate.roomTypeCode, roomTypeCode: selectedRate.roomTypeCode,
} }
: null, : null,
} }
}), }),

View File

@@ -27,6 +27,6 @@ export type JoinScandicFriendsCardProps = {
} }
export type RoomRate = { export type RoomRate = {
publicRate: Product["productType"]["public"] memberRate?: Product["member"]
memberRate?: Product["productType"]["member"] publicRate?: Product["public"]
} }

View File

@@ -15,7 +15,6 @@ export type RoomPriceSchema = z.output<typeof priceSchema>
export type FlexibilityOptionProps = { export type FlexibilityOptionProps = {
features: RoomConfiguration["features"] features: RoomConfiguration["features"]
isSelected: boolean
paymentTerm: string paymentTerm: string
petRoomPackage: RoomPackage | undefined petRoomPackage: RoomPackage | undefined
priceInformation?: Array<string> priceInformation?: Array<string>

View File

@@ -28,20 +28,36 @@ export interface SelectRateSearchParams {
toDate: string toDate: string
} }
export interface Rate { export type Rate = {
features: RoomConfiguration["features"] features: RoomConfiguration["features"]
member?: Product["productType"]["member"]
package?: RoomPackageCodeEnum | undefined package?: RoomPackageCodeEnum | undefined
priceName?: string priceName?: string
priceTerm?: string priceTerm?: string
public: Product["productType"]["public"] rate: "change" | "flex" | "save"
roomRates?: { roomRates?: {
rate: Rate rate: Rate
roomIndex: number roomIndex: number
}[] }[]
roomType: RoomConfiguration["roomType"] roomType: RoomConfiguration["roomType"]
roomTypeCode: RoomConfiguration["roomTypeCode"] roomTypeCode: RoomConfiguration["roomTypeCode"]
} } & (
| {
member?: undefined
public?: undefined
}
| {
member?: never
public: NonNullable<Product["public"]>
}
| {
member: NonNullable<Product["member"]>
public?: never
}
| {
member: NonNullable<Product["member"]>
public: NonNullable<Product["public"]>
}
)
export type RateCode = { export type RateCode = {
publicRateCode: string publicRateCode: string

View File

@@ -33,6 +33,11 @@ export interface InitialRoomData {
roomTypeCode: string roomTypeCode: string
} }
export type RoomStep = {
step: StepEnum
isValid: boolean
}
export interface RoomState { export interface RoomState {
currentStep: StepEnum | null currentStep: StepEnum | null
isComplete: boolean isComplete: boolean
@@ -89,19 +94,3 @@ export type PersistedState = {
booking: SelectRateSearchParams booking: SelectRateSearchParams
rooms: RoomState[] rooms: RoomState[]
} }
export type RoomStep = {
step: StepEnum
isValid: boolean
}
export type RoomStatus = {
isComplete: boolean
currentStep: StepEnum | null
lastCompletedStep: StepEnum | undefined
steps: {
[StepEnum.selectBed]: RoomStep
[StepEnum.breakfast]?: RoomStep
[StepEnum.details]: RoomStep
}
}