fix: make sure calculations in booking flow are correct

This commit is contained in:
Simon Emanuelsson
2025-04-02 15:49:59 +02:00
committed by Michael Zetterberg
parent 3e0f503314
commit a222ecfc5c
28 changed files with 309 additions and 276 deletions

View File

@@ -0,0 +1,3 @@
export default function ConfirmedBookingSlot() {
return null
}

View File

@@ -75,6 +75,9 @@ export default function PriceDetailsModal() {
return null return null
} }
const checkInDate = dt(fromDate).format("YYYY-MM-DD")
const checkOutDate = dt(toDate).format("YYYY-MM-DD")
const bookingTotal = rooms.reduce( const bookingTotal = rooms.reduce(
(acc, room) => { (acc, room) => {
if (room) { if (room) {
@@ -89,7 +92,7 @@ export default function PriceDetailsModal() {
{ price: 0, priceExVat: 0, vatAmount: 0 } { price: 0, priceExVat: 0, vatAmount: 0 }
) )
const diff = dt(toDate).diff(fromDate, "days") const diff = dt(checkOutDate).diff(checkInDate, "days")
const nights = intl.formatMessage( const nights = intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" }, { id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: diff } { totalNights: diff }

View File

@@ -163,6 +163,7 @@ export default function ReceiptRoom({
</Body> </Body>
</div> </div>
) : null} ) : null}
{room.breakfast || room.breakfastIncluded ? (
<div className={styles.entry}> <div className={styles.entry}>
<Body>{intl.formatMessage({ id: "Breakfast buffet" })}</Body> <Body>{intl.formatMessage({ id: "Breakfast buffet" })}</Body>
{(room.rateDefinition.breakfastIncluded ?? room.breakfastIncluded) ? ( {(room.rateDefinition.breakfastIncluded ?? room.breakfastIncluded) ? (
@@ -172,12 +173,13 @@ export default function ReceiptRoom({
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
{formatPrice( {formatPrice(
intl, intl,
room.breakfast.totalPrice * room.adults, room.breakfast.totalPrice,
room.breakfast.currency room.breakfast.currency
)} )}
</Body> </Body>
) : null} ) : null}
</div> </div>
) : null}
</article> </article>
) )
} }

View File

@@ -11,6 +11,7 @@ import {
type TrackingSDKPaymentInfo, type TrackingSDKPaymentInfo,
} from "@/types/components/tracking" } from "@/types/components/tracking"
import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { RateEnum } from "@/types/enums/rate"
import type { Room } from "@/types/stores/booking-confirmation" import type { Room } from "@/types/stores/booking-confirmation"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability" import type { RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability"
@@ -19,11 +20,11 @@ import type { Lang } from "@/constants/languages"
function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) { function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) {
switch (cancellationRule) { switch (cancellationRule) {
case "CancellableBefore6PM": case "CancellableBefore6PM":
return "flex" return RateEnum.flex
case "Changeable": case "Changeable":
return "change" return RateEnum.change
case "NotCancellable": case "NotCancellable":
return "save" return RateEnum.save
default: default:
return "-" return "-"
} }

View File

@@ -42,13 +42,11 @@ export default function Breakfast() {
: undefined : undefined
const methods = useForm<BreakfastFormSchema>({ const methods = useForm<BreakfastFormSchema>({
defaultValues: breakfastSelection
? { breakfast: breakfastSelection }
: undefined,
criteriaMode: "all", criteriaMode: "all",
mode: "all", mode: "all",
resolver: zodResolver(breakfastFormSchema), resolver: zodResolver(breakfastFormSchema),
reValidateMode: "onChange", reValidateMode: "onChange",
values: breakfastSelection ? { breakfast: breakfastSelection } : undefined,
}) })
const onSubmit = useCallback( const onSubmit = useCallback(

View File

@@ -28,6 +28,14 @@ import styles from "./ui.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 { EnterDetailsSummaryProps } from "@/types/components/hotelReservation/summary" import type { EnterDetailsSummaryProps } from "@/types/components/hotelReservation/summary"
import { CurrencyEnum } from "@/types/enums/currency"
const notDisplayableCurrencies = [
CurrencyEnum.CC,
CurrencyEnum.POINTS,
CurrencyEnum.Voucher,
CurrencyEnum.Unknown,
]
export default function SummaryUI({ export default function SummaryUI({
booking, booking,
@@ -81,6 +89,10 @@ export default function SummaryUI({
"redemption" in roomOneRoomRate || "redemption" in roomOneRoomRate ||
"voucher" in roomOneRoomRate "voucher" in roomOneRoomRate
const isSameCurrency = totalPrice.requested
? totalPrice.requested.currency === totalPrice.local.currency
: false
return ( return (
<section className={styles.summary}> <section className={styles.summary}>
<header className={styles.header}> <header className={styles.header}>
@@ -160,6 +172,10 @@ export default function SummaryUI({
guestsParts.push(childrenMsg) guestsParts.push(childrenMsg)
} }
const hideBedCurrency = notDisplayableCurrencies.includes(
room.roomPrice.perStay.local.currency
)
return ( return (
<Fragment key={idx}> <Fragment key={idx}>
<div <div
@@ -262,7 +278,9 @@ export default function SummaryUI({
{formatPrice( {formatPrice(
intl, intl,
0, 0,
room.roomPrice.perStay.local.currency hideBedCurrency
? ""
: room.roomPrice.perStay.local.currency
)} )}
</Body> </Body>
</div> </div>
@@ -418,7 +436,7 @@ export default function SummaryUI({
)} )}
</Caption> </Caption>
) : null} ) : null}
{totalPrice.requested && !isSpecialRate && ( {totalPrice.requested && !isSpecialRate && !isSameCurrency && (
<Caption color="uiTextMediumContrast"> <Caption color="uiTextMediumContrast">
{intl.formatMessage( {intl.formatMessage(
{ id: "Approx. {value}" }, { id: "Approx. {value}" },

View File

@@ -31,7 +31,7 @@ import styles from "./rateSummary.module.css"
import type { Price } from "@/types/components/hotelReservation/price" import type { Price } from "@/types/components/hotelReservation/price"
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" import { RateEnum } from "@/types/enums/rate"
import { RateTypeEnum } from "@/types/enums/rateType" import { RateTypeEnum } from "@/types/enums/rateType"
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
@@ -111,13 +111,13 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
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(rate: Rate["rate"]) { function getRateDetails(rate: RateEnum) {
switch (rate) { switch (rate) {
case "change": case RateEnum.change:
return `${freeBooking}, ${payNow}` return `${freeBooking}, ${payNow}`
case "flex": case RateEnum.flex:
return `${freeCancelation}, ${payLater}` return `${freeCancelation}, ${payLater}`
case "save": case RateEnum.save:
default: default:
return `${nonRefundable}, ${payNow}` return `${nonRefundable}, ${payNow}`
} }
@@ -243,19 +243,29 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
total, total,
{ features, packages: roomPackages, product } { features, packages: roomPackages, product }
) => { ) => {
if (!("member" in product) || !product.member) { const memberExists =
"member" in product && product.member
const publicExists =
"public" in product && product.public
if (!memberExists) {
if (!publicExists) {
return total return total
} }
const memberPrice = }
product.member.localPrice.pricePerStay
if (!memberPrice) { const price =
product.member?.localPrice.pricePerStay ||
product.public?.localPrice.pricePerStay
if (!price) {
return total return total
} }
const hasSelectedPetRoom = roomPackages.includes( const hasSelectedPetRoom = roomPackages.includes(
RoomPackageCodeEnum.PET_ROOM RoomPackageCodeEnum.PET_ROOM
) )
if (!hasSelectedPetRoom) { if (!hasSelectedPetRoom) {
return total + memberPrice return total + price
} }
const isPetRoom = features.find( const isPetRoom = features.find(
(feature) => (feature) =>
@@ -265,7 +275,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
isPetRoom && petRoomPackage isPetRoom && petRoomPackage
? Number(petRoomPackage.localPrice.totalPrice) ? Number(petRoomPackage.localPrice.totalPrice)
: 0 : 0
return total + memberPrice + petRoomPrice return total + price + petRoomPrice
}, },
0 0
), ),

View File

@@ -34,24 +34,32 @@ export function calculateTotalPrice(
const isPetRoom = room.features.find( const isPetRoom = room.features.find(
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
) )
let petRoomPrice = 0 let petRoomPriceLocal = 0
if ( if (
petRoomPackage && petRoomPackage &&
isPetRoom && isPetRoom &&
room.packages.includes(RoomPackageCodeEnum.PET_ROOM) room.packages.includes(RoomPackageCodeEnum.PET_ROOM)
) { ) {
petRoomPrice = Number(petRoomPackage.localPrice.totalPrice) petRoomPriceLocal = Number(petRoomPackage.localPrice.totalPrice)
}
let petRoomPriceRequested = 0
if (
petRoomPackage &&
isPetRoom &&
room.packages.includes(RoomPackageCodeEnum.PET_ROOM)
) {
petRoomPriceRequested = Number(petRoomPackage.requestedPrice.totalPrice)
} }
total.local.currency = rate.localPrice.currency total.local.currency = rate.localPrice.currency
total.local.price = total.local.price =
total.local.price + rate.localPrice.pricePerStay + petRoomPrice total.local.price + rate.localPrice.pricePerStay + petRoomPriceLocal
if (rate.localPrice.regularPricePerStay) { if (rate.localPrice.regularPricePerStay) {
total.local.regularPrice = total.local.regularPrice =
(total.local.regularPrice || 0) + (total.local.regularPrice || 0) +
rate.localPrice.regularPricePerStay + rate.localPrice.regularPricePerStay +
petRoomPrice petRoomPriceLocal
} }
if (rate.requestedPrice) { if (rate.requestedPrice) {
@@ -69,13 +77,13 @@ export function calculateTotalPrice(
total.requested.price = total.requested.price =
total.requested.price + total.requested.price +
rate.requestedPrice.pricePerStay + rate.requestedPrice.pricePerStay +
petRoomPrice petRoomPriceRequested
if (rate.requestedPrice.regularPricePerStay) { if (rate.requestedPrice.regularPricePerStay) {
total.requested.regularPrice = total.requested.regularPrice =
(total.requested.regularPrice || 0) + (total.requested.regularPrice || 0) +
rate.requestedPrice.regularPricePerStay + rate.requestedPrice.regularPricePerStay +
petRoomPrice petRoomPriceRequested
} }
} }

View File

@@ -15,8 +15,8 @@ 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"
import { CurrencyEnum } from "@/types/enums/currency" import { CurrencyEnum } from "@/types/enums/currency"
import { RateEnum } from "@/types/enums/rate"
export default function SelectedRoomPanel() { export default function SelectedRoomPanel() {
const intl = useIntl() const intl = useIntl()
@@ -43,13 +43,13 @@ export default function SelectedRoomPanel() {
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 getRateTitle(rate: Rate["rate"]) { function getRateTitle(rate: RateEnum) {
switch (rate) { switch (rate) {
case "change": case RateEnum.change:
return `${freeBooking}, ${payNow}` return `${freeBooking}, ${payNow}`
case "flex": case RateEnum.flex:
return `${freeCancelation}, ${payLater}` return `${freeCancelation}, ${payLater}`
case "save": case RateEnum.save:
default: default:
return `${nonRefundable}, ${payNow}` return `${nonRefundable}, ${payNow}`
} }

View File

@@ -27,5 +27,5 @@ export function getBreakfastMessage(
return msgs.notIncluded return msgs.notIncluded
} }
return msgs.noSelection return msgs.notIncluded
} }

View File

@@ -4,8 +4,6 @@ import { useIntl } from "react-intl"
import CampaignRateCard from "@scandic-hotels/design-system/CampaignRateCard" import CampaignRateCard from "@scandic-hotels/design-system/CampaignRateCard"
import NoRateAvailableCard from "@scandic-hotels/design-system/NoRateAvailableCard" import NoRateAvailableCard from "@scandic-hotels/design-system/NoRateAvailableCard"
import { useRatesStore } from "@/stores/select-rate"
import { useRoomContext } from "@/contexts/SelectRate/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import useRateTitles from "@/hooks/booking/useRateTitles" import useRateTitles from "@/hooks/booking/useRateTitles"
@@ -30,7 +28,6 @@ export default function Campaign({
const intl = useIntl() const intl = useIntl()
const { roomAvailability, roomNr, selectedFilter, selectedRate } = const { roomAvailability, roomNr, selectedFilter, selectedRate } =
useRoomContext() useRoomContext()
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
const rateTitles = useRateTitles() const rateTitles = useRateTitles()
let isCampaignRate = false let isCampaignRate = false
@@ -85,11 +82,11 @@ export default function Campaign({
) )
let bannerText = intl.formatMessage({ id: "Campaign" }) let bannerText = intl.formatMessage({ id: "Campaign" })
if (bookingCode) { if (product.bookingCode) {
bannerText = bookingCode bannerText = product.bookingCode
} }
if (product.rateDefinition?.breakfastIncluded) { if (product.rateDefinition.breakfastIncluded) {
bannerText = `${bannerText}${intl.formatMessage({ id: "Breakfast included" })}` bannerText = `${bannerText}${intl.formatMessage({ id: "Breakfast included" })}`
} else { } else {
bannerText = `${bannerText}${intl.formatMessage({ id: "Breakfast excluded" })}` bannerText = `${bannerText}${intl.formatMessage({ id: "Breakfast excluded" })}`

View File

@@ -38,7 +38,7 @@ export default function Code({
return code.map((product) => { return code.map((product) => {
let bannerText = "" let bannerText = ""
if (product.breakfastIncluded) { if (product.rateDefinition.breakfastIncluded) {
bannerText = `${bookingCode}${intl.formatMessage({ id: "Breakfast included" })}` bannerText = `${bookingCode}${intl.formatMessage({ id: "Breakfast included" })}`
} else { } else {
bannerText = `${bookingCode}${intl.formatMessage({ id: "Breakfast excluded" })}` bannerText = `${bookingCode}${intl.formatMessage({ id: "Breakfast excluded" })}`
@@ -141,7 +141,8 @@ export default function Code({
petRoomPackage petRoomPackage
) )
const comparisonRate = regularPricePerNight.totalPrice const comparisonRate =
+regularPricePerNight.totalPrice > +pricePerNight.totalPrice
? { ? {
price: regularPricePerNight.totalPrice, price: regularPricePerNight.totalPrice,
unit: localPrice.currency, unit: localPrice.currency,

View File

@@ -72,7 +72,7 @@ export default function Redemptions({
const notEnoughPoints = rates.every((rate) => rate.isDisabled) const notEnoughPoints = rates.every((rate) => rate.isDisabled)
const firstRedemption = redemptions[0] const firstRedemption = redemptions[0]
const bannerText = firstRedemption.breakfastIncluded const bannerText = firstRedemption.rateDefinition.breakfastIncluded
? `${rewardNight}${breakfastIncluded}` ? `${rewardNight}${breakfastIncluded}`
: `${rewardNight}${breakfastExcluded}` : `${rewardNight}${breakfastExcluded}`

View File

@@ -114,7 +114,7 @@ export default function Regular({
unit: `${standard!.localPrice.currency}/${night}`, unit: `${standard!.localPrice.currency}/${night}`,
} }
if (standardPricePerNight.totalRequestedPrice) { if (standardPricePerNight.totalRequestedPrice && !isUserLoggedIn) {
approximateStandardRatePrice = standardPricePerNight.totalRequestedPrice approximateStandardRatePrice = standardPricePerNight.totalRequestedPrice
} }
} }

View File

@@ -10,34 +10,29 @@ export function isSelectedPriceProduct(
selectedRate: SelectedRate | null, selectedRate: SelectedRate | null,
roomTypeCode: string roomTypeCode: string
) { ) {
if (!selectedRate) { if (!selectedRate || roomTypeCode !== selectedRate.roomTypeCode) {
return false return false
} }
const { member, public: standard } = product const { member, public: standard } = product
let selectedRateMember: PriceProduct["member"] = null let isSelected = false
if ("member" in selectedRate.product) { if (
selectedRateMember = selectedRate.product.member "member" in selectedRate.product &&
selectedRate.product.member &&
member
) {
isSelected = selectedRate.product.member.rateCode === member.rateCode
} }
let selectedRatePublic: PriceProduct["public"] = null if (
if ("public" in selectedRate.product) { "public" in selectedRate.product &&
selectedRatePublic = selectedRate.product.public selectedRate.product.public &&
standard
) {
isSelected = selectedRate.product.public.rateCode === standard.rateCode
} }
const selectedRateIsMember = return isSelected
member &&
selectedRateMember &&
member.rateCode === selectedRateMember.rateCode
const selectedRateIsPublic =
standard &&
selectedRatePublic &&
standard.rateCode === selectedRatePublic.rateCode
return !!(
(selectedRateIsMember || selectedRateIsPublic) &&
selectedRate.roomTypeCode === roomTypeCode
)
} }
export function isSelectedCorporateCheque( export function isSelectedCorporateCheque(

View File

@@ -42,6 +42,15 @@ export default function RoomProvider({
const dontShowRegularRates = const dontShowRegularRates =
hasRedemptionRates || hasCorporateChequeOrVoucherRates hasRedemptionRates || hasCorporateChequeOrVoucherRates
// Since input would be the same on single room as already
// done in useRoomsAvailability hook, data is already present
// and thus runs the appendRegularRates updater resulting in
// duplicate data
const enabled = !!(
booking.bookingCode &&
selectedFilter === BookingCodeFilterEnum.All &&
!dontShowRegularRates
)
// Extra query needed to fetch regular rates upon user // Extra query needed to fetch regular rates upon user
// selecting to view all rates. // selecting to view all rates.
// TODO: Setup route to handle singular availability call // TODO: Setup route to handle singular availability call
@@ -58,22 +67,18 @@ export default function RoomProvider({
roomStayStartDate: booking.fromDate, roomStayStartDate: booking.fromDate,
}, },
{ {
enabled: !!( enabled,
booking.bookingCode &&
selectedFilter === BookingCodeFilterEnum.All &&
!dontShowRegularRates
),
} }
) )
useEffect(() => { useEffect(() => {
if (isFetched && !isFetching && data?.length) { if (isFetched && !isFetching && data?.length && enabled) {
const regularRates = data[0] const regularRates = data[0]
if ("roomConfigurations" in regularRates) { if ("roomConfigurations" in regularRates) {
appendRegularRates(regularRates.roomConfigurations) appendRegularRates(regularRates.roomConfigurations)
} }
} }
}, [appendRegularRates, data, isFetched, isFetching]) }, [appendRegularRates, data, enabled, isFetched, isFetching])
return ( return (
<RoomContext.Provider <RoomContext.Provider

View File

@@ -30,6 +30,7 @@ import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
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 { RateEnum } from "@/types/enums/rate"
import { RateTypeEnum } from "@/types/enums/rateType" import { RateTypeEnum } from "@/types/enums/rateType"
import type { import type {
AdditionalData, AdditionalData,
@@ -112,11 +113,11 @@ export const hotelsAvailabilitySchema = z.object({
function getRate(rate: RateDefinition) { function getRate(rate: RateDefinition) {
switch (rate.cancellationRule) { switch (rate.cancellationRule) {
case "CancellableBefore6PM": case "CancellableBefore6PM":
return "flex" return RateEnum.flex
case "Changeable": case "Changeable":
return "change" return RateEnum.change
case "NotCancellable": case "NotCancellable":
return "save" return RateEnum.save
default: default:
console.info( console.info(
`Unknown cancellationRule [${rate.cancellationRule}]. This should never happen!` `Unknown cancellationRule [${rate.cancellationRule}]. This should never happen!`
@@ -276,7 +277,6 @@ export const roomsAvailabilitySchema = z
} else { } else {
product.bookingCode = undefined product.bookingCode = undefined
} }
product.breakfastIncluded = rateDefinition.breakfastIncluded
product.rate = rate product.rate = rate
product.rateDefinition = rateDefinition product.rateDefinition = rateDefinition
@@ -292,7 +292,9 @@ export const roomsAvailabilitySchema = z
if ("corporateCheque" in product) { if ("corporateCheque" in product) {
const rateDetails = getRateDetails(product) const rateDetails = getRateDetails(product)
if (rateDetails) { if (rateDetails) {
breakfastIncluded.push(rateDetails.breakfastIncluded) breakfastIncluded.push(
rateDetails.rateDefinition.breakfastIncluded
)
room.code.push({ room.code.push({
...rateDetails, ...rateDetails,
corporateCheque: product.corporateCheque, corporateCheque: product.corporateCheque,
@@ -304,7 +306,9 @@ export const roomsAvailabilitySchema = z
if ("voucher" in product) { if ("voucher" in product) {
const rateDetails = getRateDetails(product) const rateDetails = getRateDetails(product)
if (rateDetails) { if (rateDetails) {
breakfastIncluded.push(rateDetails.breakfastIncluded) breakfastIncluded.push(
rateDetails.rateDefinition.breakfastIncluded
)
room.code.push({ room.code.push({
...rateDetails, ...rateDetails,
voucher: product.voucher, voucher: product.voucher,
@@ -319,7 +323,9 @@ export const roomsAvailabilitySchema = z
for (const redemption of product) { for (const redemption of product) {
const rateDetails = getRateDetails(redemption) const rateDetails = getRateDetails(redemption)
if (rateDetails) { if (rateDetails) {
breakfastIncluded.push(rateDetails.breakfastIncluded) breakfastIncluded.push(
rateDetails.rateDefinition.breakfastIncluded
)
room.redemptions.push({ room.redemptions.push({
...redemption, ...redemption,
...rateDetails, ...rateDetails,
@@ -336,24 +342,26 @@ export const roomsAvailabilitySchema = z
) { ) {
const memberRate = product.member const memberRate = product.member
const publicRate = product.public const publicRate = product.public
const rateCode = publicRate?.rateCode ?? memberRate?.rateCode
const rateDetails = getRateDetails(product) const rateDetails = getRateDetails(product)
const rateDetailsMember = getRateDetails({ const rateDetailsMember = getRateDetails({
...product, ...product,
public: null, public: null,
}) })
if (rateDetails && rateCode) { if (rateDetails) {
if (publicRate) {
breakfastIncluded.push(
rateDetails.rateDefinition.breakfastIncluded
)
}
if (rateDetailsMember) { if (rateDetailsMember) {
breakfastIncludedMember.push( breakfastIncludedMember.push(
rateDetailsMember.breakfastIncluded rateDetailsMember.rateDefinition.breakfastIncluded
) )
rateDetails.rateDefinitionMember = rateDetails.rateDefinitionMember =
rateDetailsMember.rateDefinition rateDetailsMember.rateDefinition
} }
const rateDefinition = findRateDefintion(rateCode) switch (rateDetails.rateDefinition.rateType) {
if (rateDefinition) {
switch (rateDefinition.rateType) {
case RateTypeEnum.PublicPromotion: case RateTypeEnum.PublicPromotion:
room.campaign.push({ room.campaign.push({
...rateDetails, ...rateDetails,
@@ -375,7 +383,6 @@ export const roomsAvailabilitySchema = z
public: publicRate, public: publicRate,
}) })
} }
}
continue continue
} }
} }

View File

@@ -56,7 +56,10 @@ export const breakfastPackageSchema = z.object({
description: z.string(), description: z.string(),
localPrice: packagePriceSchema, localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema, requestedPrice: packagePriceSchema,
packageType: z.literal(PackageTypeEnum.BreakfastAdult), packageType: z.enum([
PackageTypeEnum.BreakfastAdult,
PackageTypeEnum.BreakfastChildren,
]),
}) })
export const ancillaryPackageSchema = z.object({ export const ancillaryPackageSchema = z.object({

View File

@@ -8,16 +8,16 @@ import {
} from "../productTypePrice" } from "../productTypePrice"
import { rateDefinitionSchema } from "./rateDefinition" import { rateDefinitionSchema } from "./rateDefinition"
import { RateEnum } from "@/types/enums/rate"
const baseProductSchema = z.object({ const baseProductSchema = z.object({
// transform empty string to undefined // transform empty string to undefined
bookingCode: z bookingCode: z
.string() .string()
.optional() .optional()
.transform((val) => val), .transform((val) => val),
// Is breakfast included on product
breakfastIncluded: z.boolean().default(false),
// Used to set the rate that we use to chose titles etc. // Used to set the rate that we use to chose titles etc.
rate: z.enum(["change", "flex", "save"]).default("save"), rate: z.nativeEnum(RateEnum).default(RateEnum.save),
rateDefinition: rateDefinitionSchema.optional().transform((val) => rateDefinition: rateDefinitionSchema.optional().transform((val) =>
val val
? val ? val
@@ -42,7 +42,6 @@ const baseProductSchema = z.object({
function mapBaseProduct(baseProduct: typeof baseProductSchema._type) { function mapBaseProduct(baseProduct: typeof baseProductSchema._type) {
return { return {
bookingCode: baseProduct.bookingCode, bookingCode: baseProduct.bookingCode,
breakfastIncluded: baseProduct.breakfastIncluded,
rate: baseProduct.rate, rate: baseProduct.rate,
rateDefinition: baseProduct.rateDefinition, rateDefinition: baseProduct.rateDefinition,
rateDefinitionMember: baseProduct.rateDefinitionMember, rateDefinitionMember: baseProduct.rateDefinitionMember,
@@ -97,14 +96,12 @@ export const redemptionsProduct = z
data.map( data.map(
({ ({
bookingCode, bookingCode,
breakfastIncluded,
rate, rate,
rateDefinition, rateDefinition,
rateDefinitionMember, rateDefinitionMember,
...redemption ...redemption
}) => ({ }) => ({
bookingCode, bookingCode,
breakfastIncluded,
rate, rate,
rateDefinition, rateDefinition,
rateDefinitionMember, rateDefinitionMember,

View File

@@ -430,7 +430,7 @@ export function calcTotalPrice(
? (room.breakfast.localPrice?.price ?? 0) ? (room.breakfast.localPrice?.price ?? 0)
: 0 : 0
const roomFeaturesTotal = room.roomFeatures?.reduce( const roomFeaturesTotal = (room.roomFeatures || []).reduce(
(total, pkg) => { (total, pkg) => {
if (pkg.requestedPrice.totalPrice) { if (pkg.requestedPrice.totalPrice) {
total.requestedPrice = add( total.requestedPrice = add(
@@ -445,45 +445,72 @@ export function calcTotalPrice(
{ local: 0, requestedPrice: 0 } { local: 0, requestedPrice: 0 }
) )
const result: Price = { if (roomPrice.perStay.requested) {
requested: roomPrice.perStay.requested if (!acc.requested) {
? { acc.requested = {
currency: roomPrice.perStay.requested.currency, currency: roomPrice.perStay.requested.currency,
price: add( price: 0,
acc.requested?.price ?? 0,
roomPrice.perStay.requested.price,
breakfastRequestedPrice * room.adults * nights
),
} }
: undefined,
local: {
currency: roomPrice.perStay.local.currency,
price: add(
acc.local.price,
roomPrice.perStay.local.price,
breakfastLocalPrice * room.adults * nights,
roomFeaturesTotal?.local ?? 0
),
regularPrice: add(
acc.local.regularPrice,
roomPrice.perStay.local.regularPrice,
breakfastLocalPrice * room.adults * nights,
roomFeaturesTotal?.requestedPrice ?? 0
),
additionalPrice: add(
acc.local.additionalPrice,
roomPrice.perStay.local.additionalPrice,
breakfastLocalPrice * room.adults * nights,
roomFeaturesTotal?.local ?? 0
),
additionalPriceCurrency: roomPrice.perStay.local
.additionalPriceCurrency
? roomPrice.perStay.local.additionalPriceCurrency
: undefined,
},
} }
return result acc.requested.price = add(
acc.requested.price,
roomPrice.perStay.requested.price,
breakfastRequestedPrice * room.adults * nights
)
// TODO: Come back and verify on CC, PTS, Voucher
if (roomPrice.perStay.requested.additionalPrice) {
acc.requested.additionalPrice = add(
acc.requested.additionalPrice,
roomPrice.perStay.requested.additionalPrice
)
}
if (
roomPrice.perStay.requested.additionalPriceCurrency &&
!acc.requested.additionalPriceCurrency
) {
acc.requested.additionalPriceCurrency =
roomPrice.perStay.requested.additionalPriceCurrency
}
}
const breakfastLocalTotalPrice =
breakfastLocalPrice * room.adults * nights
acc.local.price = add(
acc.local.price,
roomPrice.perStay.local.price,
breakfastLocalTotalPrice,
roomFeaturesTotal.local
)
if (roomPrice.perStay.local.regularPrice) {
acc.local.regularPrice = add(
acc.local.regularPrice,
roomPrice.perStay.local.regularPrice,
breakfastLocalTotalPrice,
roomFeaturesTotal.local
)
}
if (roomPrice.perStay.local.additionalPrice) {
acc.local.additionalPrice = add(
acc.local.additionalPrice,
roomPrice.perStay.local.additionalPrice
)
}
if (
roomPrice.perStay.local.additionalPriceCurrency &&
!acc.local.additionalPriceCurrency
) {
acc.local.additionalPriceCurrency =
roomPrice.perStay.local.additionalPriceCurrency
}
return acc
}, },
{ {
requested: undefined, requested: undefined,

View File

@@ -168,100 +168,19 @@ export function createDetailsStore(
currentRoom.steps[StepEnum.breakfast].isValid = true currentRoom.steps[StepEnum.breakfast].isValid = true
} }
const currentTotalPriceRequested = state.totalPrice.requested currentRoom.room.breakfast = breakfast
let stateTotalRequestedPrice = 0
if (currentTotalPriceRequested) {
stateTotalRequestedPrice =
currentTotalPriceRequested.price ?? 0
}
const stateTotalLocalPrice = state.totalPrice.local.price
const stateTotalLocalRegularPrice =
state.totalPrice.local.regularPrice
const addToTotalPrice =
(currentRoom.room.breakfast === undefined ||
currentRoom.room.breakfast === false) &&
!!breakfast
const subtractFromTotalPrice =
currentRoom.room.breakfast && breakfast === false
const nights = dt(state.booking.toDate).diff( const nights = dt(state.booking.toDate).diff(
state.booking.fromDate, state.booking.fromDate,
"days" "days"
) )
if (addToTotalPrice) { state.totalPrice = calcTotalPrice(
const breakfastTotalRequestedPrice = state.rooms,
breakfast.requestedPrice.price * currentRoom.room.roomPrice.perStay.local.currency,
currentRoom.room.adults * isMember,
nights nights
const breakfastTotalPrice = )
breakfast.localPrice.price *
currentRoom.room.adults *
nights
state.totalPrice = {
requested: state.totalPrice.requested && {
currency: state.totalPrice.requested.currency,
price:
stateTotalRequestedPrice + breakfastTotalRequestedPrice,
},
local: {
currency: breakfast.localPrice.currency,
price: stateTotalLocalPrice ?? 0 + breakfastTotalPrice,
regularPrice: stateTotalLocalRegularPrice
? stateTotalLocalRegularPrice + breakfastTotalPrice
: undefined,
},
}
}
if (subtractFromTotalPrice) {
let currency = state.totalPrice.local.currency
let currentBreakfastTotalPrice = 0
let currentBreakfastTotalRequestedPrice = 0
if (currentRoom.room.breakfast) {
currentBreakfastTotalPrice =
currentRoom.room.breakfast.localPrice.price *
currentRoom.room.adults *
nights
currentBreakfastTotalRequestedPrice =
currentRoom.room.breakfast.requestedPrice.totalPrice *
currentRoom.room.adults *
nights
currency = currentRoom.room.breakfast.localPrice.currency
}
let requestedPrice =
stateTotalRequestedPrice -
currentBreakfastTotalRequestedPrice
if (requestedPrice < 0) {
requestedPrice = 0
}
let localPrice =
stateTotalLocalPrice - currentBreakfastTotalPrice
if (localPrice < 0) {
localPrice = 0
}
let regularPrice = stateTotalLocalRegularPrice
? stateTotalLocalRegularPrice - currentBreakfastTotalPrice
: undefined
state.totalPrice = {
requested: state.totalPrice.requested && {
currency: state.totalPrice.requested.currency,
price: requestedPrice,
},
local: {
currency,
price: localPrice,
regularPrice,
},
}
}
currentRoom.room.breakfast = breakfast
const isAllStepsCompleted = checkRoomProgress( const isAllStepsCompleted = checkRoomProgress(
state.rooms[idx].steps state.rooms[idx].steps

View File

@@ -5,7 +5,11 @@ import type {
RoomConfiguration, RoomConfiguration,
} from "@/types/trpc/routers/hotel/roomAvailability" } from "@/types/trpc/routers/hotel/roomAvailability"
export function findProduct(rateCode: string, product: Product) { export function findProduct(
rateCode: string,
product: Product,
counterRateCode = ""
) {
if ("corporateCheque" in product) { if ("corporateCheque" in product) {
return product.corporateCheque.rateCode === rateCode return product.corporateCheque.rateCode === rateCode
} }
@@ -18,21 +22,35 @@ export function findProduct(rateCode: string, product: Product) {
return product.voucher.rateCode === rateCode return product.voucher.rateCode === rateCode
} }
if ("public" in product && product.public) { const memberExists = "member" in product
return product.public.rateCode === rateCode const publicExists = "public" in product
const isRegularRate = memberExists && publicExists
if (isRegularRate) {
let isProduct = false
if (product.member) {
isProduct =
product.member.rateCode === rateCode ||
product.member.rateCode === counterRateCode
} }
if (product.public) {
if ("member" in product && product.member) { isProduct =
return product.member.rateCode === rateCode product.public.rateCode === rateCode ||
product.public.rateCode === counterRateCode
}
return isProduct
} }
return null return null
} }
export function findProductInRoom(rateCode: string, room: RoomConfiguration) { export function findProductInRoom(
rateCode: string,
room: RoomConfiguration,
counterRateCode = ""
) {
if (room.campaign.length) { if (room.campaign.length) {
const campaignProduct = room.campaign.find((product) => const campaignProduct = room.campaign.find((product) =>
findProduct(rateCode, product) findProduct(rateCode, product, counterRateCode)
) )
if (campaignProduct) { if (campaignProduct) {
return campaignProduct return campaignProduct
@@ -40,7 +58,7 @@ export function findProductInRoom(rateCode: string, room: RoomConfiguration) {
} }
if (room.code.length) { if (room.code.length) {
const codeProduct = room.code.find((product) => const codeProduct = room.code.find((product) =>
findProduct(rateCode, product) findProduct(rateCode, product, counterRateCode)
) )
if (codeProduct) { if (codeProduct) {
return codeProduct return codeProduct
@@ -56,7 +74,7 @@ export function findProductInRoom(rateCode: string, room: RoomConfiguration) {
} }
if (room.regular.length) { if (room.regular.length) {
const regularProduct = room.regular.find((product) => const regularProduct = room.regular.find((product) =>
findProduct(rateCode, product) findProduct(rateCode, product, counterRateCode)
) )
if (regularProduct) { if (regularProduct) {
return regularProduct return regularProduct
@@ -66,6 +84,7 @@ export function findProductInRoom(rateCode: string, room: RoomConfiguration) {
export function findSelectedRate( export function findSelectedRate(
rateCode: string, rateCode: string,
counterRateCode: string,
roomTypeCode: string, roomTypeCode: string,
rooms: RoomConfiguration[] | AvailabilityError rooms: RoomConfiguration[] | AvailabilityError
) { ) {
@@ -76,7 +95,7 @@ export function findSelectedRate(
if (room.roomTypeCode !== roomTypeCode) { if (room.roomTypeCode !== roomTypeCode) {
return false return false
} }
return findProductInRoom(rateCode, room) return findProductInRoom(rateCode, room, counterRateCode)
}) })
} }

View File

@@ -71,6 +71,7 @@ export function createRatesStore({
const roomConfiguration = roomConfigurations?.[idx] const roomConfiguration = roomConfigurations?.[idx]
const selectedRoom = findSelectedRate( const selectedRoom = findSelectedRate(
room.rateCode, room.rateCode,
room.counterRateCode,
room.roomTypeCode, room.roomTypeCode,
roomConfiguration roomConfiguration
) )
@@ -79,7 +80,11 @@ export function createRatesStore({
continue continue
} }
const product = findProductInRoom(room.rateCode, selectedRoom) const product = findProductInRoom(
room.rateCode,
selectedRoom,
room.counterRateCode
)
if (product) { if (product) {
rateSummary[idx] = { rateSummary[idx] = {
features: selectedRoom.features, features: selectedRoom.features,
@@ -121,13 +126,18 @@ export function createRatesStore({
const selectedRate = const selectedRate =
findSelectedRate( findSelectedRate(
room.rateCode, room.rateCode,
room.counterRateCode,
room.roomTypeCode, room.roomTypeCode,
roomConfiguration roomConfiguration
) ?? null ) ?? null
let product = null let product = null
if (selectedRate) { if (selectedRate) {
product = findProductInRoom(room.rateCode, selectedRate) product = findProductInRoom(
room.rateCode,
selectedRate,
room.counterRateCode
)
} }
// Since features are fetched async based on query string, we need to read from query string to apply correct filtering // Since features are fetched async based on query string, we need to read from query string to apply correct filtering

View File

@@ -1,3 +1,4 @@
import type { RateEnum } from "@/types/enums/rate"
import type { import type {
Product, Product,
RoomConfiguration, RoomConfiguration,
@@ -35,7 +36,7 @@ export type Rate = {
priceName?: string priceName?: string
priceTerm?: string priceTerm?: string
product: Product product: Product
rate: "change" | "flex" | "save" rate: RateEnum
roomRates?: { roomRates?: {
rate: Rate rate: Rate
roomIndex: number roomIndex: number

View File

@@ -1,5 +1,6 @@
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
import type { MembershipLevel } from "@/constants/membershipLevels" import type { MembershipLevel } from "@/constants/membershipLevels"
import type { RateEnum } from "../enums/rate"
export enum TrackingChannelEnum { export enum TrackingChannelEnum {
"scandic-friends" = "scandic-friends", "scandic-friends" = "scandic-friends",
@@ -54,7 +55,7 @@ export type TrackingSDKUserData =
export type TrackingSDKHotelInfo = { export type TrackingSDKHotelInfo = {
ageOfChildren?: string // "10", "2,5,10" ageOfChildren?: string // "10", "2,5,10"
ancillaries?: Ancillary[] ancillaries?: Ancillary[]
analyticsRateCode?: "flex" | "change" | "save" | string analyticsRateCode?: RateEnum | string
arrivalDate?: string arrivalDate?: string
availableResults?: number // Number of hotels to choose from after a city search availableResults?: number // Number of hotels to choose from after a city search
bedType?: string bedType?: string

View File

@@ -0,0 +1,5 @@
export enum RateEnum {
change = "change",
flex = "flex",
save = "save",
}

View File

@@ -1,5 +1,6 @@
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details" import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
import type { RateEnum } from "@/types/enums/rate"
import type { Packages } from "@/types/requests/packages" import type { Packages } from "@/types/requests/packages"
export interface Room { export interface Room {
@@ -10,7 +11,7 @@ export interface Room {
mustBeGuaranteed: boolean mustBeGuaranteed: boolean
memberMustBeGuaranteed?: boolean memberMustBeGuaranteed?: boolean
packages: Packages | null packages: Packages | null
rate: "change" | "flex" | "save" rate: RateEnum
rateDefinitionTitle: string rateDefinitionTitle: string
rateDetails: string[] rateDetails: string[]
rateTitle?: string rateTitle?: string

View File

@@ -120,7 +120,9 @@ export default function CodeRateCard({
<div className={`${styles.rateRow} ${styles.comparisonRate}`}> <div className={`${styles.rateRow} ${styles.comparisonRate}`}>
<Typography variant="Title/Subtitle/md"> <Typography variant="Title/Subtitle/md">
<p> <p>
<span className={styles.strikethrough}>{rate.price}</span>{' '} <span className={styles.strikethrough}>
{comparisonRate.price}
</span>{' '}
<Typography variant="Body/Supporting text (caption)/smBold"> <Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.strikethrough}> <span className={styles.strikethrough}>
{comparisonRate.unit} {comparisonRate.unit}