diff --git a/apps/scandic-web/__mocks__/hotelReservation/index.ts b/apps/scandic-web/__mocks__/hotelReservation/index.ts index 8b3c621e9..44249401d 100644 --- a/apps/scandic-web/__mocks__/hotelReservation/index.ts +++ b/apps/scandic-web/__mocks__/hotelReservation/index.ts @@ -64,8 +64,6 @@ export const roomRate: RoomRate = { pricePerStay: 132, currency: CurrencyEnum.EUR, }, - oldRateCode: "", - rate: "", }, publicRate: { rateCode: "SAVEEU", @@ -79,8 +77,6 @@ export const roomRate: RoomRate = { pricePerStay: 133, currency: CurrencyEnum.EUR, }, - oldRateCode: "", - rate: "", }, } diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx index f57bd6fcc..b41ace619 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx @@ -84,7 +84,7 @@ export default function Details({ user }: DetailsProps) { id={`${formID}-room-${roomNr}`} onSubmit={methods.handleSubmit(onSubmit)} > - {user ? null : } + {user || !memberRate ? null : }
- {showSignupPromo && memberPrice ? ( + {showSignupPromo && memberPrice && !isMember ? ( ) : null} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx index 6881d6675..a874479ac 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx @@ -25,6 +25,7 @@ import styles from "./summary.module.css" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details" import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary" +import { RateTypeEnum } from "@/types/enums/rateType" export default function Summary({ booking, @@ -56,6 +57,11 @@ export default function Summary({ const memberPrice = getMemberPrice(rooms[0].roomRate) + const containsBookingCodeRate = rooms.find( + (room) => room.roomRate.publicRate?.rateType !== RateTypeEnum.Regular + ) + const showDiscounted = containsBookingCodeRate || isMember + return (
@@ -103,6 +109,9 @@ export default function Summary({ const memberPrice = getMemberPrice(room.roomRate) const showMemberPrice = !!(isMember && memberPrice && roomNumber === 1) + const isBookingCodeRate = + room.roomRate.publicRate?.rateType !== RateTypeEnum.Regular + const showDiscounted = isBookingCodeRate || showMemberPrice const adultsMsg = intl.formatMessage( { id: "{totalAdults, plural, one {# adult} other {# adults}}" }, @@ -134,7 +143,7 @@ export default function Summary({ ) : null}
{room.roomType} - + {formatPrice( intl, room.roomPrice.perStay.local.price, @@ -249,7 +258,11 @@ export default function Summary({ />
- + {formatPrice( intl, totalPrice.local.price, diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx index 203b1af88..0d8117363 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx @@ -14,6 +14,7 @@ import Summary from "./Summary" import styles from "./mobileSummary.module.css" import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" +import { RateTypeEnum } from "@/types/enums/rateType" export default function MobileSummary({ isAllRoomsSelected, @@ -71,19 +72,24 @@ export default function MobileSummary({ roomPrice: { perNight: { local: { - price: room.public.localPrice.pricePerNight, - currency: room.public.localPrice.currency, + price: (room.public?.localPrice.pricePerNight || + room.member?.localPrice.pricePerNight)!, + currency: (room.public?.localPrice.currency || + room.member?.localPrice.currency)!, }, requested: undefined, }, perStay: { local: { - price: room.public.localPrice.pricePerStay, - currency: room.public.localPrice.currency, + price: (room.public?.localPrice.pricePerStay || + room.member?.localPrice.pricePerStay)!, + currency: (room.public?.localPrice.currency || + room.member?.localPrice.currency)!, }, requested: undefined, }, - currency: room.public.localPrice.currency, + currency: (room.public?.localPrice.currency || + room.member?.localPrice.currency)!, }, roomRate: { ...room.public, @@ -91,13 +97,23 @@ export default function MobileSummary({ publicRate: room.public, }, rateDetails: rateDefinitions.find( - (rate) => rate.rateCode === room.public.rateCode + (rate) => + rate.rateCode === room.public?.rateCode || + rate.rateCode === room.member?.rateCode )?.generalTerms, cancellationText: - rateDefinitions.find((rate) => rate.rateCode === room.public.rateCode) - ?.cancellationText ?? "", + rateDefinitions.find( + (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 (
@@ -122,7 +138,7 @@ export default function MobileSummary({ className={styles.priceDetailsButton} > {intl.formatMessage({ id: "Total price" })} - + {formatPrice( intl, totalPriceToShow.local.price, diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx index 0a440a429..8f8e16384 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx @@ -6,7 +6,6 @@ import { useIntl } from "react-intl" import { dt } from "@/lib/dt" import { useRatesStore } from "@/stores/select-rate" -import { getRates } from "@/components/HotelReservation/SelectRate/utils" import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop" import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile" 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 { 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) { const { @@ -86,19 +87,13 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { const hasMemberRates = rateSummary.some((room) => room.member) const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn - const rates = getRates(roomsAvailability.rateDefinitions) - const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) const freeBooking = intl.formatMessage({ id: "Free rebooking" }) const payLater = intl.formatMessage({ id: "Pay later" }) const payNow = intl.formatMessage({ id: "Pay now" }) - function getRateDetails(rateCode: string) { - const rate = Object.keys(rates).find((k) => - rates[k as keyof typeof rates].find((a) => a.rateCode === rateCode) - ) - + function getRateDetails(rate: Rate["rate"]) { switch (rate) { case "change": return `${freeBooking}, ${payNow}` @@ -122,6 +117,11 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { return null } + const isBookingCodeRate = rateSummary.some( + (rate) => rate.public?.rateType !== RateTypeEnum.Regular + ) + const showDiscounted = isUserLoggedIn || isBookingCodeRate + const totalPriceToShow = calculateTotalPrice( rateSummary, isUserLoggedIn, @@ -134,7 +134,6 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
{rateSummary.map((room, index) => { - const isMainRoom = index + 1 === 1 return (
{rateSummary.length > 1 ? ( @@ -147,11 +146,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { {room.roomType} - {getRateDetails( - isUserLoggedIn && room.member && isMainRoom - ? room.member?.rateCode - : room.public.rateCode - )} + {getRateDetails(room.rate)} ) : ( @@ -160,11 +155,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { {room.roomType} - {getRateDetails( - isUserLoggedIn && room.member && isMainRoom - ? room.member?.rateCode - : room.public.rateCode - )} + {getRateDetails(room.rate)} )} @@ -206,9 +197,8 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { : 0 return total + memberPrice + petRoomPrice }, 0), - currency: - rateSummary[0].member?.localPrice.currency ?? - rateSummary[0].public.localPrice.currency, + currency: (rateSummary[0].member?.localPrice.currency ?? + rateSummary[0].public?.localPrice.currency)!, }} />
@@ -225,7 +215,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
{formatPrice( @@ -253,7 +243,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { {intl.formatMessage({ id: "Total price" })} - + {formatPrice( intl, totalPriceToShow.local.price, diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts index 9f9836091..3db07ee98 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts @@ -12,10 +12,15 @@ export const calculateTotalPrice = ( ) => { return selectedRateSummary.reduce( (total, room, idx) => { - const priceToUse = + const rate = isUserLoggedIn && room.member && idx + 1 === 1 ? room.member : room.public + + if (!rate) { + return total + } + const isPetRoom = room.features.find( (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM ) @@ -31,18 +36,16 @@ export const calculateTotalPrice = ( return { local: { - currency: priceToUse.localPrice.currency, + currency: rate.localPrice.currency, price: - total.local.price + - priceToUse.localPrice.pricePerStay + - petRoomPrice, + total.local.price + rate.localPrice.pricePerStay + petRoomPrice, }, - requested: priceToUse.requestedPrice + requested: rate.requestedPrice ? { - currency: priceToUse.requestedPrice.currency, + currency: rate.requestedPrice.currency, price: (total.requested?.price ?? 0) + - priceToUse.requestedPrice.pricePerStay + + rate.requestedPrice.pricePerStay + petRoomPrice, } : undefined, @@ -50,7 +53,8 @@ export const calculateTotalPrice = ( }, { local: { - currency: selectedRateSummary[0].public.localPrice.currency, + currency: (selectedRateSummary[0].public?.localPrice.currency || + selectedRateSummary[0].member?.localPrice.currency)!, price: 0, }, requested: undefined, diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx index 4b988b79b..365bc0c5f 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx @@ -3,7 +3,6 @@ import { useIntl } from "react-intl" import { useRatesStore } from "@/stores/select-rate" -import { getRates } from "@/components/HotelReservation/SelectRate/utils" import { EditIcon } from "@/components/Icons" import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" @@ -15,9 +14,11 @@ import { useRoomContext } from "@/contexts/SelectRate/Room" import styles from "./selectedRoomPanel.module.css" +import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" + export default function SelectedRoomPanel() { const intl = useIntl() - const { isUserLoggedIn, rateDefinitions, roomCategories } = useRatesStore( + const { isUserLoggedIn, roomCategories } = useRatesStore( (state) => ({ isUserLoggedIn: state.isUserLoggedIn, rateDefinitions: state.roomsAvailability?.rateDefinitions, @@ -37,23 +38,13 @@ export default function SelectedRoomPanel() { ) )?.images - if (!rateDefinitions) { - return null - } - - const rates = getRates(rateDefinitions) - const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) const freeBooking = intl.formatMessage({ id: "Free rebooking" }) const payLater = intl.formatMessage({ id: "Pay later" }) const payNow = intl.formatMessage({ id: "Pay now" }) - function getRateDetails(rateCode: string) { - const rate = Object.keys(rates).find((k) => - rates[k as keyof typeof rates].find((a) => a.rateCode === rateCode) - ) - + function getRateTitle(rate: Rate["rate"]) { switch (rate) { case "change": return `${freeBooking}, ${payNow}` @@ -65,10 +56,14 @@ export default function SelectedRoomPanel() { } } - const rate = - isUserLoggedIn && isMainRoom && selectedRate?.product.productType.member - ? selectedRate?.product.productType.member - : selectedRate?.product.productType.public + if (!selectedRate) { + return null + } + + const selectedProduct = + isUserLoggedIn && isMainRoom && selectedRate.product?.member + ? selectedRate.product?.member + : selectedRate.product?.public return (
@@ -80,20 +75,21 @@ export default function SelectedRoomPanel() { )} - {selectedRate?.roomType} + {selectedRate.roomType} - {rate?.rateCode ? getRateDetails(rate.rateCode) : null} + {getRateTitle(selectedRate.product.rate)} - {rate?.localPrice.pricePerNight} {rate?.localPrice.currency}/ + {selectedProduct?.localPrice.pricePerNight}{" "} + {selectedProduct?.localPrice.currency}/ {intl.formatMessage({ id: "night" })}
{images?.[0]?.imageSizes?.tiny ? ( {selectedRate?.roomType
@@ -67,9 +72,26 @@ export default function FlexibilityOption({ ) } - const { public: publicPrice, member: memberPrice } = product.productType - const rate = - isUserLoggedIn && isMainRoom && memberPrice ? memberPrice : publicPrice + const productMember = product.member + const selectedRateMember = selectedRate?.product.member + 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 (
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx index 72d5442d8..c16484f5d 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx @@ -7,7 +7,6 @@ import { useIntl } from "react-intl" import { useRatesStore } from "@/stores/select-rate" import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek" -import { getRates } from "@/components/HotelReservation/SelectRate/utils" import { getIconForFeatureCode } from "@/components/HotelReservation/utils" import { ErrorCircleIcon, PriceTagIcon } from "@/components/Icons" import ImageGallery from "@/components/ImageGallery" @@ -91,7 +90,11 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { rateDefinitions: state.roomsAvailability?.rateDefinitions, roomCategories: state.roomCategories, })) - const { isMainRoom, roomNr, selectedPackage, selectedRate } = useRoomContext() + const { isMainRoom, roomNr, selectedPackage } = useRoomContext() + + if (!rateDefinitions) { + return null + } const classNames = cardVariants({ availability: @@ -119,12 +122,6 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { roomNr ) - if (!rateDefinitions) { - return null - } - - const rates = getRates(rateDefinitions) - const petRoomPackageSelected = (selectedPackage === RoomPackageCodeEnum.PET_ROOM && petRoomPackage) || undefined @@ -144,93 +141,19 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { const payLater = intl.formatMessage({ id: "Pay later" }) const payNow = intl.formatMessage({ id: "Pay now" }) - function getRate(rateCode: string) { + function getRateTitle(rateCode: Product["rate"]) { switch (rateCode) { case "change": - return { - isFlex: false, - notAvailable: false, - title: freeBooking, - } + return freeBooking case "flex": - return { - isFlex: true, - notAvailable: false, - title: freeCancelation, - } + return freeCancelation case "save": - return { - isFlex: false, - notAvailable: false, - title: nonRefundable, - } + return nonRefundable default: - throw new Error( - `Unknown key for rate, should be "change", "flex", "save", but got ${rateCode}` - ) + return "" } } - 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 * 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[] ) { return rateDefinitions.find((rateDefinition) => - isUserLoggedIn && product.productType.member - ? rateDefinition.rateCode === product.productType.member.rateCode - : rateDefinition.rateCode === product.productType.public.rateCode + isUserLoggedIn && product.member && isMainRoom + ? rateDefinition.rateCode === product.member?.rateCode + : rateDefinition.rateCode === product.public?.rateCode ) } const isBookingCodeRate = bookingCode && roomConfiguration.products.every((item) => { - return item.productType.public.rateType !== RateTypeEnum.Regular + return item.public?.rateType !== RateTypeEnum.Regular }) return ( @@ -294,9 +217,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { {occupancy.max === occupancy.min ? intl.formatMessage( - { id: "guests.plural" }, - { guests: occupancy.max } - ) + { id: "guests.plural" }, + { guests: occupancy.max } + ) : intl.formatMessage({ id: "guests.span" }, occupancy)} )} @@ -349,33 +272,25 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { ) : null} {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 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 ( state.booking.rooms.length === 1 && !!state.rateSummary.length + ) const searchParams = useSearchParams() const bookingCode = searchParams.get("bookingCode") const intl = useIntl() @@ -41,8 +46,7 @@ export default function RoomSelectionPanel() { (room) => room.status === AvailabilityEnum.Available && room.products.some( - (product) => - product.productType.public.rateType === RateTypeEnum.Regular + (product) => product.public?.rateType === RateTypeEnum.Regular ) ) @@ -56,8 +60,7 @@ export default function RoomSelectionPanel() { (room) => room.status === AvailabilityEnum.Available && room.products.some( - (product) => - product.productType.public.rateType !== RateTypeEnum.Regular + (product) => product.public?.rateType !== RateTypeEnum.Regular ) ) @@ -71,22 +74,44 @@ export default function RoomSelectionPanel() { (room) => room.status === AvailabilityEnum.Available && room.products.every( - (product) => - product.productType.public.rateType !== RateTypeEnum.Regular + (product) => product.public?.rateType !== RateTypeEnum.Regular ) ) const regularRateRooms = rooms.filter( (room) => room.status === AvailabilityEnum.Available && room.products.every( - (product) => - product.productType.public.rateType === RateTypeEnum.Regular + (product) => product.public?.rateType === RateTypeEnum.Regular ) ) // Show booking code filter when both of the booking code rates or regular rates are available const showBookingCodeFilter = 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 ( <> {noAvailableRooms || (bookingCode && !isBookingCodeRatesAvailable) ? ( @@ -117,27 +142,27 @@ export default function RoomSelectionPanel() { {showBookingCodeFilter ? : null}
    - {/* Show either Booking code filtered rooms or all the rooms */} - {showAllRooms - ? rooms.map((roomConfiguration) => ( + {/* Show either Booking code filtered rooms or all the rooms */} + {showAllRooms + ? rooms.map((roomConfiguration) => ( + + )) + : activeCodeFilter === BookingCodeFilterEnum.Discounted + ? bookingCodeDiscountedRooms.map((roomConfiguration) => ( )) - : activeCodeFilter === BookingCodeFilterEnum.Discounted - ? bookingCodeDiscountedRooms.map((roomConfiguration) => ( - - )) - : regularRateRooms.map((roomConfiguration) => ( - - ))} + : regularRateRooms.map((roomConfiguration) => ( + + ))}
) diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx index fe29e6040..94d6d268d 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx @@ -30,10 +30,14 @@ export default function Rooms() { useEffect(() => { const pricesWithCurrencies = visibleRooms.flatMap((room) => - room.products.map((product) => ({ - price: product.productType.public.localPrice.pricePerNight, - currency: product.productType.public.localPrice.currency, - })) + room.products + .filter((product) => product.member || product.public) + .map((product) => ({ + currency: (product.public?.localPrice.currency || + product.member?.localPrice.currency)!, + price: (product.public?.localPrice.pricePerNight || + product.member?.localPrice.pricePerNight)!, + })) ) const lowestPrice = pricesWithCurrencies.reduce( (minPrice, { price }) => Math.min(minPrice, price), diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts index abab1f575..e319e0558 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts +++ b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts @@ -33,22 +33,6 @@ export function combineRoomAvailabilities( }, 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( uniqueAdultsCount: number[], hotelId: number, diff --git a/apps/scandic-web/providers/EnterDetailsProvider.tsx b/apps/scandic-web/providers/EnterDetailsProvider.tsx index de80533d7..61e12cb7d 100644 --- a/apps/scandic-web/providers/EnterDetailsProvider.tsx +++ b/apps/scandic-web/providers/EnterDetailsProvider.tsx @@ -1,19 +1,23 @@ "use client" +import deepmerge from "deepmerge" import { useEffect, useRef } from "react" import { dt } from "@/lib/dt" import { createDetailsStore } from "@/stores/enter-details" import { calcTotalPrice, - checkIsSameBedTypes, checkIsSameBooking as checkIsSameBooking, clearSessionStorage, readFromSessionStorage, + writeToSessionStorage, } 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 type { DetailsStore } from "@/types/contexts/enter-details" +import { StepEnum } from "@/types/enums/step" import type { DetailsProviderProps } from "@/types/providers/enter-details" import type { InitialState } from "@/types/stores/enter-details" @@ -71,46 +75,80 @@ export default function EnterDetailsProvider({ return } + const store = storeRef.current?.getState() + if (!store) { + return + } + const updatedRooms = storedValues.rooms.map((storedRoom, idx) => { - const currentRoom = booking.rooms[idx] - const room = rooms[idx] + // Need to create a deep new copy + // since store is readonly + const currentRoom = deepmerge({}, store.rooms[idx]) - if (!storedRoom.room?.bedType) { - return storedRoom - } - - const isSameBedTypes = checkIsSameBedTypes( - storedRoom.room.bedType.roomTypeCode, - currentRoom.roomTypeCode - ) - if (isSameBedTypes) { - return storedRoom - } - - if (room?.bedTypes?.length === 1 && room.bedTypes[0]) { - return { - ...storedRoom, - bedType: { - roomTypeCode: room.bedTypes[0].value, - description: room.bedTypes[0].description, - }, + if (!currentRoom.room.bedType && storedRoom.room.bedType) { + const sameBed = currentRoom.room.bedTypes.find( + (bedType) => bedType.value === storedRoom.room.bedType?.roomTypeCode + ) + if (sameBed) { + currentRoom.room.bedType = { + description: sameBed.description, + roomTypeCode: sameBed.value, + } + currentRoom.steps[StepEnum.selectBed].isValid = true } } - // Remove bed type selection if bedtypes change - return { - ...storedRoom, - bedType: undefined, + if ( + currentRoom.steps[StepEnum.breakfast] && + currentRoom.room.breakfast === 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 nights = dt(booking.toDate).diff(booking.fromDate, "days") - const currency = - updatedRooms[0].room.roomRate.publicRate.localPrice.currency + const currency = (updatedRooms[0].room.roomRate.publicRate?.localPrice + .currency || + updatedRooms[0].room.roomRate.memberRate?.localPrice.currency)! const totalPrice = calcTotalPrice(updatedRooms, currency, !!user, nights) + const activeRoom = updatedRooms.findIndex((room) => !room.isComplete) + + writeToSessionStorage({ + activeRoom, + booking, + rooms: updatedRooms, + }) + storeRef.current?.setState({ activeRoom: storedValues.activeRoom, canProceedToPayment, diff --git a/apps/scandic-web/server/routers/booking/input.ts b/apps/scandic-web/server/routers/booking/input.ts index c30bcb13d..560619ccd 100644 --- a/apps/scandic-web/server/routers/booking/input.ts +++ b/apps/scandic-web/server/routers/booking/input.ts @@ -44,11 +44,8 @@ const roomsSchema = z.array( accessibility: z.boolean(), }), roomPrice: z.object({ - publicPrice: z.number().or(z.string().transform((val) => Number(val))), - memberPrice: z - .number() - .or(z.string().transform((val) => Number(val))) - .optional(), + memberPrice: z.number().nullish(), + publicPrice: z.number().nullish(), }), }) ) diff --git a/apps/scandic-web/server/routers/hotels/output.ts b/apps/scandic-web/server/routers/hotels/output.ts index 17384c3b8..9c4474c85 100644 --- a/apps/scandic-web/server/routers/hotels/output.ts +++ b/apps/scandic-web/server/routers/hotels/output.ts @@ -105,7 +105,7 @@ function everyRateHasBreakfastIncluded( userType: "member" | "public" ) { const rateDefinition = rateDefinitions.find( - (rd) => rd.rateCode === product.productType[userType]?.rateCode + (rd) => rd.rateCode === product[userType]?.rateCode ) if (!rateDefinition) { return false @@ -113,10 +113,7 @@ function everyRateHasBreakfastIncluded( return rateDefinition.breakfastIncluded } -function getRate(rate: RateDefinition | undefined) { - if (!rate) { - return null - } +function getRate(rate: RateDefinition) { switch (rate.cancellationRule) { case "CancellableBefore6PM": return "flex" @@ -156,77 +153,79 @@ export const roomsAvailabilitySchema = z type: z.string().optional(), }), }) - .transform((o) => { - const cancellationRuleLookup = o.data.attributes.rateDefinitions.reduce( - (acc, val) => { - // @ts-expect-error - index of cancellationRule TS - acc[val.rateCode] = cancellationRules[val.cancellationRule] - return acc - }, - {} - ) + .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 + }, {}) - o.data.attributes.roomConfigurations = - o.data.attributes.roomConfigurations.map((room) => { + attributes.roomConfigurations = attributes.roomConfigurations.map( + (room) => { if (room.products.length) { room.breakfastIncludedInAllRatesMember = room.products.every( (product) => - everyRateHasBreakfastIncluded( - product, - o.data.attributes.rateDefinitions, - "member" - ) + everyRateHasBreakfastIncluded(product, rateDefinitions, "member") ) room.breakfastIncludedInAllRatesPublic = room.products.every( (product) => - everyRateHasBreakfastIncluded( - product, - o.data.attributes.rateDefinitions, - "public" - ) + everyRateHasBreakfastIncluded(product, rateDefinitions, "public") ) room.products = room.products.map((product) => { - const publicRateDefinition = o.data.attributes.rateDefinitions.find( - (rate) => - product.productType.public.rateCode - ? rate.rateCode === product.productType.public.rateCode - : rate.rateCode === product.productType.public.oldRateCode - ) - const publicRate = getRate(publicRateDefinition) - const memberRateDefinition = o.data.attributes.rateDefinitions.find( - (rate) => - product.productType.member?.rateCode - ? rate.rateCode === product.productType.member?.rateCode - : rate.rateCode === product.productType.member?.oldRateCode - ) - const memberRate = getRate(memberRateDefinition) - - if (publicRate) { - product.productType.public.rate = publicRate + 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 + } + } + } } - 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 }) + + // 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 o.data.attributes + return attributes }) export const ratesSchema = z.array(rateSchema) diff --git a/apps/scandic-web/server/routers/hotels/query.ts b/apps/scandic-web/server/routers/hotels/query.ts index 5fd9b95e3..915471cfa 100644 --- a/apps/scandic-web/server/routers/hotels/query.ts +++ b/apps/scandic-web/server/routers/hotels/query.ts @@ -535,7 +535,6 @@ export const hotelQueryRouter = router({ } const apiJson = await apiResponse.json() - const validateAvailabilityData = roomsAvailabilitySchema.safeParse(apiJson) @@ -710,8 +709,8 @@ export const hotelQueryRouter = router({ const rateTypes = selectedRoom.products.find( (rate) => - rate.productType.public?.rateCode === rateCode || - rate.productType.member?.rateCode === rateCode + rate.public?.rateCode === rateCode || + rate.member?.rateCode === rateCode ) if (!rateTypes) { @@ -728,7 +727,7 @@ export const hotelQueryRouter = router({ console.error("No matching rate found") return null } - const rates = rateTypes.productType + const rates = rateTypes const rateDefinition = validateAvailabilityData.data.rateDefinitions.find( @@ -786,7 +785,7 @@ export const hotelQueryRouter = router({ mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed, breakfastIncluded: !!rateDefinition?.breakfastIncluded, memberRate: rates?.member, - publicRate: rates.public, + publicRate: rates?.public, bedTypes, } }), diff --git a/apps/scandic-web/server/routers/hotels/schemas/productTypePrice.ts b/apps/scandic-web/server/routers/hotels/schemas/productTypePrice.ts index a5a0a4a10..266cfe92e 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/productTypePrice.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/productTypePrice.ts @@ -15,10 +15,4 @@ export const productTypePriceSchema = z.object({ rateCode: z.string(), rateType: z.string().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(""), }) diff --git a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts index 9c5ed5ae7..2f35b11aa 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts @@ -1,11 +1,9 @@ -import deepmerge from "deepmerge" import { z } from "zod" import { productSchema } from "./product" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { RateTypeEnum } from "@/types/enums/rateType" export const roomConfigurationSchema = z .object({ @@ -31,60 +29,15 @@ export const roomConfigurationSchema = z }) .transform((data) => { 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 - * since we cannot as of now (31 january) guarantee the flow with missing rateCodes. - * This rule applies to regular rates (Save, Change and Flex) - * Exception Booking code rate - * - * TODO: (Maybe) notify somewhere that this happened + * Just guaranteeing that if all products all miss + * both public and member rateCode that status is + * set to `NotAvailable` */ - const allProductsMissAtLeastOneRateCode = data.products.every( - ({ productType }) => - (!productType.public.rateCode || !productType.member?.rateCode) && - productType.public.rateType === RateTypeEnum.Regular + const allProductsMissBothRateCodes = data.products.every( + (product) => !product.public?.rateCode && !product.member?.rateCode ) - if (allProductsMissAtLeastOneRateCode) { + if (allProductsMissBothRateCodes) { data.status = AvailabilityEnum.NotAvailable } } diff --git a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts index 94ff02ab9..5a944710d 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts @@ -2,20 +2,19 @@ import { z } from "zod" import { productTypePriceSchema } from "../productTypePrice" -import { CurrencyEnum } from "@/types/enums/currency" - -export const productSchema = z.object({ - productType: z.object({ - member: productTypePriceSchema.optional(), - public: productTypePriceSchema.default({ - localPrice: { - currency: CurrencyEnum.SEK, - pricePerNight: 0, - pricePerStay: 0, - }, - rateCode: "", - rateType: "", - requestedPrice: undefined, +export const productSchema = z + .object({ + // Is product flex rate + isFlex: z.boolean().default(false), + productType: z.object({ + member: productTypePriceSchema.optional(), + public: productTypePriceSchema.optional(), }), - }), -}) + // 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, + })) diff --git a/apps/scandic-web/stores/enter-details/helpers.ts b/apps/scandic-web/stores/enter-details/helpers.ts index 9a296ef85..e14648f10 100644 --- a/apps/scandic-web/stores/enter-details/helpers.ts +++ b/apps/scandic-web/stores/enter-details/helpers.ts @@ -25,13 +25,6 @@ export function extractGuestFromUser(user: NonNullable) { } } -export function checkIsSameBedTypes( - storedBedTypes: string, - bedTypesData: string -) { - return storedBedTypes === bedTypesData -} - export function checkIsSameBooking( prev: SelectRateSearchParams, next: SelectRateSearchParams @@ -111,28 +104,34 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) { } } - return { - perNight: { - requested: roomRate.publicRate.requestedPrice && { - currency: roomRate.publicRate.requestedPrice.currency, - price: roomRate.publicRate.requestedPrice.pricePerNight, + if (roomRate.publicRate) { + return { + perNight: { + requested: roomRate.publicRate.requestedPrice && { + currency: roomRate.publicRate.requestedPrice.currency, + price: roomRate.publicRate.requestedPrice.pricePerNight, + }, + local: { + currency: roomRate.publicRate.localPrice.currency, + price: roomRate.publicRate.localPrice.pricePerNight, + }, }, - local: { - currency: roomRate.publicRate.localPrice.currency, - price: roomRate.publicRate.localPrice.pricePerNight, + 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, + }, }, - }, - 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 = { @@ -149,6 +148,10 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) { ? roomRate.memberRate : roomRate.publicRate + if (!rate) { + return total + } + return { requested: rate.requestedPrice ? { @@ -168,7 +171,8 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) { { requested: undefined, local: { - currency: roomRates[0].publicRate.localPrice.currency, + currency: (roomRates[0].publicRate?.localPrice.currency || + roomRates[0].memberRate?.localPrice.currency)!, price: 0, }, } @@ -191,6 +195,10 @@ export function calcTotalPrice( isFirstRoomAndMember || join ) + if (!roomPrice) { + return acc + } + const breakfastRequestedPrice = room.breakfast ? parseInt(room.breakfast.requestedPrice?.price ?? 0) : 0 diff --git a/apps/scandic-web/stores/enter-details/index.ts b/apps/scandic-web/stores/enter-details/index.ts index 00bb9b3a5..b0ed817ed 100644 --- a/apps/scandic-web/stores/enter-details/index.ts +++ b/apps/scandic-web/stores/enter-details/index.ts @@ -25,7 +25,6 @@ import type { DetailsState, InitialState, RoomState, - RoomStatus, } from "@/types/stores/enter-details" import type { SafeUser } from "@/types/user" @@ -74,7 +73,7 @@ export function createDetailsStore( }) const rooms: RoomState[] = initialState.rooms.map((room, idx) => { - const steps: RoomStatus["steps"] = { + const steps: RoomState["steps"] = { [StepEnum.selectBed]: { step: StepEnum.selectBed, isValid: !!room.bedType, @@ -225,6 +224,13 @@ export function createDetailsStore( state.rooms[idx].steps[StepEnum.selectBed].isValid = true 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) writeToSessionStorage({ @@ -331,6 +337,13 @@ export function createDetailsStore( currentRoom.room.breakfast = breakfast + const isAllStepsCompleted = checkRoomProgress( + state.rooms[idx].steps + ) + if (isAllStepsCompleted) { + state.rooms[idx].isComplete = true + } + handleStepProgression(currentRoom, state) writeToSessionStorage({ diff --git a/apps/scandic-web/stores/select-rate/helper.ts b/apps/scandic-web/stores/select-rate/helper.ts index adf2c28c5..70e886953 100644 --- a/apps/scandic-web/stores/select-rate/helper.ts +++ b/apps/scandic-web/stores/select-rate/helper.ts @@ -1,70 +1,6 @@ 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" -interface CalculateRoomSummaryParams { - availablePackages: RoomPackages - getFilteredRooms: (roomIndex: number) => RoomConfiguration[] - roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }> - selectedPackagesByRoom: Record - 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. */ @@ -119,12 +55,11 @@ export function filterDuplicateRoomTypesByLowestPrice( if (previousRoom) { products.forEach((product) => { - const { productType } = product - const publicProduct = productType.public || { + const publicProduct = product?.public || { requestedPrice: null, localPrice: null, } - const memberProduct = productType.member || { + const memberProduct = product?.member || { requestedPrice: null, localPrice: null, } @@ -154,34 +89,28 @@ export function filterDuplicateRoomTypesByLowestPrice( currentRequestedPrice < Math.min( Number( - previousLowest.products[0].productType.public.requestedPrice - ?.pricePerNight + previousLowest.products[0].public?.requestedPrice?.pricePerNight ) ?? Infinity, Number( - previousLowest.products[0].productType.member?.requestedPrice - ?.pricePerNight + previousLowest.products[0].member?.requestedPrice?.pricePerNight ) ?? Infinity ) || (currentRequestedPrice === Math.min( Number( - previousLowest.products[0].productType.public.requestedPrice - ?.pricePerNight + previousLowest.products[0].public?.requestedPrice?.pricePerNight ) ?? Infinity, Number( - previousLowest.products[0].productType.member?.requestedPrice - ?.pricePerNight + previousLowest.products[0].member?.requestedPrice?.pricePerNight ) ?? Infinity ) && currentLocalPrice < Math.min( Number( - previousLowest.products[0].productType.public.localPrice - ?.pricePerNight + previousLowest.products[0].public?.localPrice?.pricePerNight ) ?? Infinity, Number( - previousLowest.products[0].productType.member?.localPrice - ?.pricePerNight + previousLowest.products[0].member?.localPrice?.pricePerNight ) ?? Infinity )) ) { diff --git a/apps/scandic-web/stores/select-rate/index.ts b/apps/scandic-web/stores/select-rate/index.ts index 058ee2c6c..1ef70d082 100644 --- a/apps/scandic-web/stores/select-rate/index.ts +++ b/apps/scandic-web/stores/select-rate/index.ts @@ -9,6 +9,7 @@ import { RatesContext } from "@/contexts/Rates" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { RateTypeEnum } from "@/types/enums/rateType" import type { InitialState, RatesState } from "@/types/stores/rates" import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" @@ -27,8 +28,8 @@ function findSelectedRate( room.roomTypeCode === roomTypeCode && room.products.find( (product) => - product.productType.public.rateCode === rateCode || - product.productType.member?.rateCode === rateCode + product.public?.rateCode === rateCode || + product.member?.rateCode === rateCode ) ) } @@ -87,21 +88,22 @@ export function createRatesStore({ roomConf.roomTypeCode === room.roomTypeCode && roomConf.products.find( (product) => - product.productType.public.rateCode === room.rateCode || - product.productType.member?.rateCode === room.rateCode + product.public?.rateCode === room.rateCode || + product.member?.rateCode === room.rateCode ) ) const product = selectedRoom?.products.find( (p) => - p.productType.public.rateCode === room.rateCode || - p.productType.member?.rateCode === room.rateCode + p.public?.rateCode === room.rateCode || + p.member?.rateCode === room.rateCode ) if (selectedRoom && product) { rateSummary[idx] = { features: selectedRoom.features, - member: product.productType.member, - public: product.productType.public, + member: product.member, + public: product.public, + rate: product.rate, roomType: selectedRoom.roomType, roomTypeCode: selectedRoom.roomTypeCode, } @@ -180,35 +182,48 @@ export function createRatesStore({ return function (selectedRate) { return set( produce((state: RatesState) => { + const memberRate = selectedRate.product.member + const publicRate = selectedRate.product.public + if (!memberRate && !publicRate) { + return + } + state.rooms[idx].selectedRate = selectedRate state.rateSummary[idx] = { features: selectedRate.features, - member: selectedRate.product.productType.member, + member: selectedRate.product.member, package: state.rooms[idx].selectedPackage, - public: selectedRate.product.productType.public, + rate: selectedRate.product.rate, + public: selectedRate.product.public, roomType: selectedRate.roomType, roomTypeCode: selectedRate.roomTypeCode, } + const isBookingCodeRate = + selectedRate.product.public?.rateType !== RateTypeEnum.Regular + const roomNr = idx + 1 + const isMainRoom = roomNr + 1 const isMemberRate = - isUserLoggedIn && - roomNr === 1 && - selectedRate.product.productType.member + isUserLoggedIn && isMainRoom && memberRate && !isBookingCodeRate const searchParams = new URLSearchParams(state.searchParams) - searchParams.set( - `room[${idx}].counterratecode`, - isMemberRate - ? selectedRate.product.productType.public.rateCode - : (selectedRate.product.productType.member?.rateCode ?? "") - ) - searchParams.set( - `room[${idx}].ratecode`, - isMemberRate - ? // already checked in isMemberRate - selectedRate.product.productType.member!.rateCode - : selectedRate.product.productType.public.rateCode - ) + const counterratecode = isMemberRate + ? (publicRate?.rateCode ?? "") + : (memberRate?.rateCode ?? "") + if (counterratecode) { + searchParams.set( + `room[${idx}].counterratecode`, + counterratecode + ) + } + + const rateCode = isMemberRate + ? memberRate.rateCode + : (publicRate?.rateCode ?? "") + if (rateCode) { + searchParams.set(`room[${idx}].ratecode`, rateCode) + } + searchParams.set( `room[${idx}].roomtype`, selectedRate.roomTypeCode @@ -249,8 +264,8 @@ export function createRatesStore({ const product = selectedRate?.products.find( (prd) => - prd.productType.public.rateCode === room.rateCode || - prd.productType.member?.rateCode === room.rateCode + prd.public?.rateCode === room.rateCode || + prd.member?.rateCode === room.rateCode ) const selectedPackage = room.packages?.[0] @@ -259,18 +274,18 @@ export function createRatesStore({ bookingRoom: room, rooms: selectedPackage ? allRooms.filter((r) => - r.features.find((f) => f.code === selectedPackage) - ) + r.features.find((f) => f.code === selectedPackage) + ) : allRooms, selectedPackage, selectedRate: selectedRate && product ? { - features: selectedRate.features, - product, - roomType: selectedRate.roomType, - roomTypeCode: selectedRate.roomTypeCode, - } + features: selectedRate.features, + product, + roomType: selectedRate.roomType, + roomTypeCode: selectedRate.roomTypeCode, + } : null, } }), diff --git a/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts b/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts index a8827cd7e..3549298f7 100644 --- a/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts +++ b/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts @@ -27,6 +27,6 @@ export type JoinScandicFriendsCardProps = { } export type RoomRate = { - publicRate: Product["productType"]["public"] - memberRate?: Product["productType"]["member"] + memberRate?: Product["member"] + publicRate?: Product["public"] } diff --git a/apps/scandic-web/types/components/hotelReservation/selectRate/flexibilityOption.ts b/apps/scandic-web/types/components/hotelReservation/selectRate/flexibilityOption.ts index f12d30e5a..714c5aeef 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -15,7 +15,6 @@ export type RoomPriceSchema = z.output export type FlexibilityOptionProps = { features: RoomConfiguration["features"] - isSelected: boolean paymentTerm: string petRoomPackage: RoomPackage | undefined priceInformation?: Array diff --git a/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts b/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts index d68d41c74..2458d77a8 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts @@ -28,20 +28,36 @@ export interface SelectRateSearchParams { toDate: string } -export interface Rate { +export type Rate = { features: RoomConfiguration["features"] - member?: Product["productType"]["member"] package?: RoomPackageCodeEnum | undefined priceName?: string priceTerm?: string - public: Product["productType"]["public"] + rate: "change" | "flex" | "save" roomRates?: { rate: Rate roomIndex: number }[] roomType: RoomConfiguration["roomType"] roomTypeCode: RoomConfiguration["roomTypeCode"] -} +} & ( + | { + member?: undefined + public?: undefined + } + | { + member?: never + public: NonNullable + } + | { + member: NonNullable + public?: never + } + | { + member: NonNullable + public: NonNullable + } +) export type RateCode = { publicRateCode: string diff --git a/apps/scandic-web/types/stores/enter-details.ts b/apps/scandic-web/types/stores/enter-details.ts index 120a32b52..c153f5829 100644 --- a/apps/scandic-web/types/stores/enter-details.ts +++ b/apps/scandic-web/types/stores/enter-details.ts @@ -33,6 +33,11 @@ export interface InitialRoomData { roomTypeCode: string } +export type RoomStep = { + step: StepEnum + isValid: boolean +} + export interface RoomState { currentStep: StepEnum | null isComplete: boolean @@ -89,19 +94,3 @@ export type PersistedState = { booking: SelectRateSearchParams 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 - } -}