Merged in fix/SW-3198-prices-select-rate (pull request #2763)

fix(SW-3198): fix striketrhough/regular prices, the same in enter details as select rate

* fix(SW-3198): fix striketrhough/regular prices, the same in enter details as select rate

* fix(SW-3198): remove additonalcost if calculating cost per room

* fix(SW-3198): include bookingcode in specialrate

* fix(SW-3198): remove console log

* fix(SW-3198): add or operator

* fix(SW-3198): capture total return value

* fix(SW-3198): rename and move function


Approved-by: Joakim Jäderberg
Approved-by: Hrishikesh Vaipurkar
This commit is contained in:
Bianca Widstam
2025-09-05 14:02:47 +00:00
parent a87cef91d4
commit bba4e24569
19 changed files with 290 additions and 236 deletions

View File

@@ -150,8 +150,8 @@ export default async function DetailsPage(
</Suspense>
</div>
<aside className={styles.summary}>
<MobileSummary isMember={!!user} />
<DesktopSummary isMember={!!user} />
<MobileSummary isUserLoggedIn={!!user} />
<DesktopSummary isUserLoggedIn={!!user} />
</aside>
</div>
<EnterDetailsTrackingWrapper

View File

@@ -8,7 +8,7 @@ import SummaryUI from "./UI"
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
export default function DesktopSummary({ isMember }: SummaryProps) {
export default function DesktopSummary({ isUserLoggedIn }: SummaryProps) {
const toggleSummaryOpen = useEnterDetailsStore(
(state) => state.actions.toggleSummaryOpen
)
@@ -27,7 +27,7 @@ export default function DesktopSummary({ isMember }: SummaryProps) {
<SummaryUI
booking={booking}
rooms={rooms}
isMember={isMember}
isUserLoggedIn={isUserLoggedIn}
totalPrice={totalPrice}
vat={vat}
toggleSummaryOpen={toggleSummaryOpen}

View File

@@ -20,12 +20,12 @@ import styles from "./bottomSheet.module.css"
interface SummaryBottomSheetProps
extends PropsWithChildren<{
isMember: boolean
isUserLoggedIn: boolean
}> {}
export default function SummaryBottomSheet({
children,
isMember,
isUserLoggedIn,
}: SummaryBottomSheetProps) {
const intl = useIntl()
const scrollY = useRef(0)
@@ -68,7 +68,7 @@ export default function SummaryBottomSheet({
const containsBookingCodeRate = rooms.find(
(r) => r && isBookingCodeRate(r.room.roomRate)
)
const showDiscounted = containsBookingCodeRate || isMember
const showDiscounted = containsBookingCodeRate || isUserLoggedIn
return (
<div className={styles.wrapper} data-open={isSummaryOpen}>
@@ -103,13 +103,15 @@ export default function SummaryBottomSheet({
</Typography>
{showDiscounted && totalPrice.local.regularPrice ? (
<Typography variant="Body/Paragraph/mdRegular">
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
totalPrice.local.regularPrice,
totalPrice.local.currency
)}
</s>
<p>
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
totalPrice.local.regularPrice,
totalPrice.local.currency
)}
</s>
</p>
</Typography>
) : null}

View File

@@ -11,7 +11,7 @@ import styles from "./mobile.module.css"
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
export default function MobileSummary({ isMember }: SummaryProps) {
export default function MobileSummary({ isUserLoggedIn }: SummaryProps) {
const { isSummaryOpen, toggleSummaryOpen } = useEnterDetailsStore(
(state) => ({
isSummaryOpen: state.isSummaryOpen,
@@ -29,7 +29,7 @@ export default function MobileSummary({ isMember }: SummaryProps) {
}))
const showPromo =
!isMember &&
!isUserLoggedIn &&
rooms.length === 1 &&
!rooms[0].room.guest.join &&
!rooms[0].room.guest.membershipNo
@@ -51,12 +51,12 @@ export default function MobileSummary({ isMember }: SummaryProps) {
/>
)}
<SummaryBottomSheet isMember={isMember}>
<SummaryBottomSheet isUserLoggedIn={isUserLoggedIn}>
<div className={styles.wrapper}>
<SummaryUI
booking={booking}
rooms={rooms}
isMember={isMember}
isUserLoggedIn={isUserLoggedIn}
totalPrice={totalPrice}
vat={vat}
toggleSummaryOpen={toggleSummaryOpen}

View File

@@ -12,7 +12,6 @@ import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import { getFeatureDescription } from "@/components/HotelReservation/utils/getRoomFeatureDescription"
import { getMemberPrice, getPublicPrice } from "../utils"
import Breakfast from "./Breakfast"
import styles from "./room.module.css"
@@ -23,8 +22,7 @@ interface RoomProps {
room: RoomType
roomNumber: number
roomCount: number
isMember: boolean
isSpecialRate: boolean
isUserLoggedIn: boolean
nightsCount: number
defaultCurrency: CurrencyEnum
}
@@ -33,8 +31,7 @@ export default function Room({
room,
roomNumber,
roomCount,
isMember,
isSpecialRate,
isUserLoggedIn,
nightsCount,
defaultCurrency,
}: RoomProps) {
@@ -61,16 +58,23 @@ export default function Room({
const childBedCrib = childrenBeds?.get(ChildBedMapEnum.IN_CRIB)
const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED)
const memberPrice = getMemberPrice(room.roomRate)
const publicPrice = getPublicPrice(room.roomRate)
const isFirstRoomMember = roomNumber === 1 && isMember
const isFirstRoomMember = roomNumber === 1 && isUserLoggedIn
const isOrWillBecomeMember = !!(
room.guest.join ||
room.guest.membershipNo ||
isFirstRoomMember
)
const showMemberPrice = !!(isOrWillBecomeMember && memberPrice)
const showMemberPrice = !!(
isOrWillBecomeMember &&
"member" in room.roomRate &&
room.roomRate.member
)
const isSpecialRate =
"corporateCheque" in room.roomRate ||
"redemption" in room.roomRate ||
"voucher" in room.roomRate ||
room.roomRate.bookingCode ||
room.roomRate.rateDefinition.isCampaignRate
const showDiscounted = isSpecialRate || showMemberPrice
const adultsMsg = intl.formatMessage(
@@ -94,7 +98,7 @@ export default function Room({
let rateDetails = room.rateDetails
if (room.memberRateDetails) {
if (isMember || room.guest.join) {
if (showMemberPrice) {
rateDetails = room.memberRateDetails
}
}
@@ -102,15 +106,13 @@ export default function Room({
const guests = guestsParts.join(", ")
const zeroPrice = formatPrice(intl, 0, defaultCurrency)
let price = showMemberPrice
? formatPrice(intl, memberPrice.amount, memberPrice.currency)
: formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)
let price = formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)
let currency: string = room.roomPrice.perStay.local.currency
const isVoucher = "voucher" in room.roomRate
@@ -164,12 +166,12 @@ export default function Room({
>
{price}
</p>
{showDiscounted && publicPrice ? (
{showDiscounted && room.roomPrice.perStay.local.regularPrice ? (
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
publicPrice.amount,
publicPrice.currency
room.roomPrice.perStay.local.regularPrice,
currency
)}
</s>
) : null}

View File

@@ -32,7 +32,7 @@ export default function SummaryUI({
booking,
rooms,
totalPrice,
isMember,
isUserLoggedIn,
vat,
toggleSummaryOpen,
defaultCurrency,
@@ -59,7 +59,7 @@ export default function SummaryUI({
const roomOneGuest = rooms[0].room.guest
const showSignupPromo =
rooms.length === 1 &&
!isMember &&
!isUserLoggedIn &&
!roomOneGuest.membershipNo &&
!roomOneGuest.join
@@ -67,13 +67,8 @@ export default function SummaryUI({
const roomOneRoomRate = rooms[0].room.roomRate
const isVoucherRate = "voucher" in roomOneRoomRate
// In case of Redemption, voucher and Corporate cheque do not show approx price
const isSpecialRate =
"corporateCheque" in roomOneRoomRate ||
"redemption" in roomOneRoomRate ||
isVoucherRate
const priceDetailsRooms = mapToPrice(rooms, isMember)
const priceDetailsRooms = mapToPrice(rooms, isUserLoggedIn)
const isAllCampaignRate = rooms.every(
(room) => room.room.roomRate.rateDefinition.isCampaignRate
)
@@ -83,7 +78,7 @@ export default function SummaryUI({
const containsBookingCodeRate = rooms.find(
(r) => r && isBookingCodeRate(r.room.roomRate)
)
const showDiscounted = containsBookingCodeRate || isMember
const showDiscounted = containsBookingCodeRate || isUserLoggedIn
const totalCurrency = isVoucherRate
? CurrencyEnum.Voucher
@@ -127,8 +122,7 @@ export default function SummaryUI({
room={room}
roomNumber={idx + 1}
roomCount={rooms.length}
isMember={isMember}
isSpecialRate={isSpecialRate}
isUserLoggedIn={isUserLoggedIn}
nightsCount={nights}
/>
))}
@@ -192,13 +186,15 @@ export default function SummaryUI({
</Typography>
{showDiscounted && totalPrice.local.regularPrice ? (
<Typography variant="Body/Paragraph/mdRegular">
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
totalPrice.local.regularPrice,
totalPrice.local.currency
)}
</s>
<p>
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
totalPrice.local.regularPrice,
totalPrice.local.currency
)}
</s>
</p>
</Typography>
) : null}
</div>
@@ -223,7 +219,7 @@ export default function SummaryUI({
alignCenter
/>
<Divider className={styles.bottomDivider} color="Border/Divider/Subtle" />
{showSignupPromo && roomOneMemberPrice && !isMember ? (
{showSignupPromo && roomOneMemberPrice && !isUserLoggedIn ? (
<SignupPromoDesktop
memberPrice={roomOneMemberPrice}
badgeContent={"✌️"}

View File

@@ -1,5 +1,6 @@
import { parsePhoneNumberFromString } from "libphonenumber-js"
import { calculateRegularPrice } from "@scandic-hotels/booking-flow/utils/calculateRegularPrice"
import {
sumPackages,
sumPackagesRequestedPrice,
@@ -495,17 +496,11 @@ export function getRegularPrice(
(total, room, idx) => {
const isMainRoomAndMember = idx === 0 && isMember
const join = Boolean(room.guest.join || room.guest.membershipNo)
const getMemberRate = isMainRoomAndMember || join
const memberRate = "member" in room.roomRate && room.roomRate.member
const publicRate = "public" in room.roomRate && room.roomRate.public
const useMemberRate = (isMainRoomAndMember || join) && memberRate
let rate
if (getMemberRate && memberRate) {
rate = memberRate
} else if (publicRate) {
rate = publicRate
}
const rate = useMemberRate ? memberRate : publicRate
if (!rate) {
return total
@@ -547,61 +542,23 @@ export function getRegularPrice(
)
}
// Legend:
// - total.local.price = Total Price = Black price, what the user pays
// - total.local.regularPrice = Regular Price = Strikethrough price (could potentially be none)
// - total.requested.price = Requested Price = EUR approx price
// We sometimes don't get all the required data to calculate the correct strikethrough total.
// Therefore we try these different approach to get a number that is close
// enough to the real number if all data would've been present.
if (getMemberRate && memberRate) {
if (publicRate) {
// #1 Member price uses public price as strikethrough
total.local.regularPrice = add(
total.local.regularPrice,
publicRate.localPrice.pricePerStay,
additionalCost
)
} else if (memberRate.localPrice.regularPricePerStay) {
// #2 Member price uses member regular price as strikethrough
total.local.regularPrice = add(
total.local.regularPrice,
memberRate.localPrice.regularPricePerStay,
additionalCost
)
} else {
// #3 Member price uses member price as strikethrough
// NOTE: If all rooms end up using this, no strikethrough price is shown.
total.local.regularPrice = add(
total.local.regularPrice,
memberRate.localPrice.pricePerStay,
additionalCost
)
}
} else if (publicRate) {
if (publicRate.localPrice.regularPricePerStay) {
// #1 Public price uses public regular price as strikethrough
total.local.regularPrice = add(
total.local.regularPrice,
publicRate.localPrice.regularPricePerStay,
additionalCost
)
} else {
// #2 Public price uses public price as strikethrough
// NOTE: If all rooms end up using this, no strikethrough price is shown.
total.local.regularPrice = add(
total.local.regularPrice,
publicRate.localPrice.pricePerStay,
additionalCost
)
}
} else {
// We cannot do anything, too much data is missing.
return total
}
return total
return calculateRegularPrice({
total,
useMemberRate: !!useMemberRate,
regularMemberPrice: memberRate
? {
pricePerStay: memberRate.localPrice.pricePerNight,
regularPricePerStay: memberRate.localPrice.regularPricePerStay,
}
: undefined,
regularPublicPrice: publicRate
? {
pricePerStay: publicRate.localPrice.pricePerNight,
regularPricePerStay: publicRate.localPrice.regularPricePerStay,
}
: undefined,
additionalCost,
})
},
{
local: {

View File

@@ -22,12 +22,12 @@ export type RoomsData = {
}
export interface SummaryProps {
isMember: boolean
isUserLoggedIn: boolean
}
export interface EnterDetailsSummaryProps {
booking: DetailsBooking
isMember: boolean
isUserLoggedIn: boolean
totalPrice: Price
vat: number
rooms: RoomState[]

View File

@@ -27,7 +27,9 @@ export default function BoldRow({
<td className={styles.price}>
{isDiscounted && regularValue ? (
<Typography variant="Body/Supporting text (caption)/smRegular">
<s className={styles.strikeThroughRate}>{regularValue}</s>
<p>
<s className={styles.strikeThroughRate}>{regularValue}</s>
</p>
</Typography>
) : null}
<Typography variant="Body/Supporting text (caption)/smBold">

View File

@@ -52,7 +52,9 @@ export default function LargeRow({
{isDiscounted && regularPrice ? (
<>
<Typography variant="Body/Paragraph/mdRegular">
<s className={styles.strikeThroughRate}>{regularPrice}</s>
<p>
<s className={styles.strikeThroughRate}>{regularPrice}</s>
</p>
</Typography>
</>
) : null}

View File

@@ -24,13 +24,13 @@ import styles from "./summaryContent.module.css"
import type { Price } from "../../../../../../contexts/SelectRate/getTotalPrice"
export type SelectRateSummaryProps = {
isMember: boolean
isUserLoggedIn: boolean
bookingCode?: string
toggleSummaryOpen: () => void
}
export default function SummaryContent({
isMember,
isUserLoggedIn,
toggleSummaryOpen,
}: SelectRateSummaryProps) {
const { selectedRates, input } = useSelectRateContext()
@@ -61,7 +61,7 @@ export default function SummaryContent({
return null
}
const showDiscounted = containsBookingCodeRate || isMember
const showDiscounted = containsBookingCodeRate || isUserLoggedIn
const totalRegularPrice = selectedRates?.totalPrice?.local?.regularPrice
? selectedRates.totalPrice.local.regularPrice
: 0
@@ -117,7 +117,7 @@ export default function SummaryContent({
<Room
key={idx}
room={mapToRoom({
isMember,
isUserLoggedIn,
rate: room,
input,
idx,
@@ -126,7 +126,7 @@ export default function SummaryContent({
})}
roomNumber={idx + 1}
roomCount={selectedRates.rates.length}
isMember={isMember}
isMember={isUserLoggedIn && idx === 0}
/>
)
})}
@@ -192,13 +192,15 @@ export default function SummaryContent({
showStrikeThroughPrice &&
selectedRates.totalPrice.local.regularPrice ? (
<Typography variant="Body/Paragraph/mdRegular">
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
selectedRates.totalPrice.local.regularPrice,
selectedRates.totalPrice.local.currency
)}
</s>
<p>
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
selectedRates.totalPrice.local.regularPrice,
selectedRates.totalPrice.local.currency
)}
</s>
</p>
</Typography>
) : null}
</div>
@@ -217,7 +219,7 @@ export default function SummaryContent({
}
const mapped = mapToRoom({
isMember,
isUserLoggedIn,
rate: room,
input,
idx,
@@ -231,17 +233,26 @@ export default function SummaryContent({
) {
switch (room.type) {
case "regular":
const memberLocalPrice = room.member?.localPrice
? {
...room.member.localPrice,
regularPricePerStay:
room.public?.localPrice?.pricePerStay ||
room.member.localPrice.regularPricePerStay,
}
: undefined
return {
regular:
isMember && memberLocalPrice
? memberLocalPrice
: room.public?.localPrice,
}
case "campaign":
return {
regular: isMember
? (room.member?.localPrice ?? room.public?.localPrice)
: room.public?.localPrice,
}
case "campaign":
return {
campaign: isMember
? (room.member ?? room.public)
: room.public,
}
case "redemption":
return {
redemption: room.redemption,
@@ -259,10 +270,19 @@ export default function SummaryContent({
}
}
if ("public" in room) {
const memberLocalPrice = room.member?.localPrice
? {
...room.member.localPrice,
regularPricePerStay:
room.public?.localPrice?.pricePerStay ||
room.member.localPrice.regularPricePerStay,
}
: undefined
return {
regular: isMember
? (room.member?.localPrice ?? room.public?.localPrice)
: room.public?.localPrice,
regular:
isMember && memberLocalPrice
? memberLocalPrice
: room.public?.localPrice,
}
}
}
@@ -271,7 +291,7 @@ export default function SummaryContent({
}
}
const p = getPrice(room!, isMember)
const p = getPrice(room!, isUserLoggedIn && idx === 0)
return {
...mapped,
@@ -293,7 +313,7 @@ export default function SummaryContent({
vat={selectedRates.vat}
/>
</div>
{!isMember && memberPrice ? (
{!isUserLoggedIn && memberPrice ? (
<SignupPromoDesktop
memberPrice={{
amount: memberPrice.localPrice.pricePerStay,
@@ -307,14 +327,14 @@ export default function SummaryContent({
}
function mapToRoom({
isMember,
isUserLoggedIn,
rate,
input,
idx,
getPriceForRoom,
rateTitles,
}: {
isMember: boolean
isUserLoggedIn: boolean
rate: NonNullable<
ReturnType<typeof useSelectRateContext>["selectedRates"]["rates"][number]
>
@@ -323,6 +343,7 @@ function mapToRoom({
getPriceForRoom: (roomIndex: number) => Price | null
rateTitles: ReturnType<typeof useRateTitles>
}) {
const useMemberPrice = isUserLoggedIn && idx === 0
return {
adults: input.data?.booking.rooms[idx].adults || 0,
childrenInRoom: input.data?.booking.rooms[idx].childrenInRoom,
@@ -335,7 +356,7 @@ function mapToRoom({
local: { price: -1, currency: CurrencyEnum.Unknown },
},
},
rateDetails: isMember
rateDetails: useMemberPrice
? (rate.rateDefinitionMember?.generalTerms ??
rate.rateDefinition.generalTerms)
: rate.rateDefinition.generalTerms,

View File

@@ -10,7 +10,6 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import { isBookingCodeRate } from "../../utils"
import { getMemberPrice } from "../utils"
import styles from "./room.module.css"
@@ -68,9 +67,7 @@ export default function Room({
const childBedCrib = childrenBeds?.get(ChildBedMapEnum.IN_CRIB)
const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED)
const memberPrice = getMemberPrice(room.roomRate)
const showMemberPrice = !!(isMember && memberPrice && roomNumber === 1)
const showDiscounted = isBookingCodeRate(room.roomRate) || showMemberPrice
const showDiscounted = isBookingCodeRate(room.roomRate) || isMember
const adultsMsg = intl.formatMessage(
{
@@ -130,25 +127,19 @@ export default function Room({
[styles.discounted]: showDiscounted,
})}
>
{showMemberPrice
? formatPrice(
intl,
memberPrice.amount,
memberPrice.currency
)
: formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)}
{formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)}
</p>
{showDiscounted && room.roomPrice.perStay.local.price ? (
{showDiscounted && room.roomPrice.perStay.local.regularPrice ? (
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.regularPrice,
room.roomPrice.perStay.local.currency
)}
</s>

View File

@@ -61,19 +61,12 @@ export function MobileSummary() {
return null
}
const totalRegularPrice = selectedRates.totalPrice.local?.regularPrice
? selectedRates.totalPrice.local.regularPrice
: 0
const showStrikeThroughPrice =
totalRegularPrice > selectedRates.totalPrice.local?.price
return (
<div className={styles.wrapper} data-open={isSummaryOpen}>
<div className={styles.content}>
<div className={styles.summaryAccordion}>
<SummaryContent
isMember={isUserLoggedIn}
isUserLoggedIn={isUserLoggedIn}
toggleSummaryOpen={toggleSummaryOpen}
/>
</div>
@@ -106,17 +99,17 @@ export function MobileSummary() {
)}
</span>
</Typography>
{showDiscounted &&
showStrikeThroughPrice &&
selectedRates.totalPrice.local.regularPrice ? (
{showDiscounted && selectedRates.totalPrice.local?.regularPrice ? (
<Typography variant="Body/Paragraph/mdRegular">
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
selectedRates.totalPrice.local.regularPrice,
selectedRates.totalPrice.local.currency
)}
</s>
<p>
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
selectedRates.totalPrice.local?.regularPrice,
selectedRates.totalPrice.local.currency
)}
</s>
</p>
</Typography>
) : null}

View File

@@ -1,13 +0,0 @@
import type { Product } from "@scandic-hotels/trpc/types/roomAvailability"
export function getMemberPrice(roomRate: Product) {
if ("member" in roomRate && roomRate.member) {
return {
amount: roomRate.member.localPrice.pricePerStay,
currency: roomRate.member.localPrice.currency,
pricePerNight: roomRate.member.localPrice.pricePerNight,
}
}
return null
}

View File

@@ -230,7 +230,7 @@ export function SelectRateProvider({
rate,
roomConfiguration: roomAvailability[ix]?.[0],
})),
useMemberPrices: isUserLoggedIn,
isMember: isUserLoggedIn,
})
const getPriceForRoom = useCallback(
@@ -249,7 +249,8 @@ export function SelectRateProvider({
selectedRates: [
{ rate, roomConfiguration: roomAvailability[roomIndex]?.[0] },
],
useMemberPrices: isUserLoggedIn,
isMember: isUserLoggedIn && roomIndex === 0,
addAdditionalCost: false,
})
},
[selectedRates, roomAvailability, isUserLoggedIn]

View File

@@ -6,7 +6,7 @@ describe("getTotalPrice", () => {
it("should return null when no rates are selected", () => {
const result = getTotalPrice({
selectedRates: [],
useMemberPrices: false,
isMember: false,
})
expect(result).toEqual({

View File

@@ -1,6 +1,7 @@
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { sumPackages } from "../../utils/SelectRate"
import { calculateRegularPrice } from "../../utils/calculateRegularPrice"
import { sumPackages, sumPackagesRequestedPrice } from "../../utils/SelectRate"
import type { RedemptionProduct } from "@scandic-hotels/trpc/types/roomAvailability"
@@ -26,10 +27,12 @@ type SelectedRate = {
export function getTotalPrice({
selectedRates,
useMemberPrices,
isMember,
addAdditionalCost = true,
}: {
selectedRates: Array<SelectedRate | null>
useMemberPrices: boolean
isMember: boolean
addAdditionalCost?: boolean
}): Price | null {
const mainRoom = selectedRates[0]
const mainRoomRate = mainRoom?.rate
@@ -42,7 +45,7 @@ export function getTotalPrice({
}
if (!mainRoomRate) {
return calculateTotalPrice(summaryArray, useMemberPrices)
return calculateTotalPrice(summaryArray, isMember, addAdditionalCost)
}
// In case of reward night (redemption) or voucher only single room booking is supported by business rules
@@ -59,14 +62,15 @@ export function getTotalPrice({
return voucherPrice
}
return calculateTotalPrice(summaryArray, useMemberPrices)
return calculateTotalPrice(summaryArray, isMember, addAdditionalCost)
}
function calculateTotalPrice(
selectedRateSummary: OneLevelNonNullable<SelectedRate>[],
useMemberPrices: boolean
isMember: boolean,
addAdditionalCost: boolean
) {
return selectedRateSummary.reduce<Price>(
const totalPrice = selectedRateSummary.reduce<Price>(
(total, room, idx) => {
if (!room.rate || !("member" in room.rate) || !("public" in room.rate)) {
return total
@@ -75,34 +79,25 @@ function calculateTotalPrice(
const roomNr = idx + 1
const isMainRoom = roomNr === 1
const useMemberRate = isMainRoom && useMemberPrices && room.rate.member
const useMemberRate = isMainRoom && isMember && room.rate.member
const rate = useMemberRate ? room.rate.member : room.rate.public
const publicRate = room.rate.public
const memberRate = room.rate.member
if (!rate) {
return total
}
const packagesPrice = room.roomConfiguration?.selectedPackages.reduce(
(total, pkg) => {
total.local = total.local + pkg.localPrice.totalPrice
if (pkg.requestedPrice.totalPrice) {
total.requested = total.requested + pkg.requestedPrice.totalPrice
}
return total
},
{ local: 0, requested: 0 }
)
const packagesPrice = addAdditionalCost
? sumPackages(room.roomConfiguration?.selectedPackages)
: { price: 0, currency: undefined }
const packagesRequestedPrice = addAdditionalCost
? sumPackagesRequestedPrice(room.roomConfiguration?.selectedPackages)
: { price: 0, currency: undefined }
total.local.currency = rate.localPrice.currency
total.local.price =
total.local.price + rate.localPrice.pricePerStay + packagesPrice.local
if (rate.localPrice.regularPricePerStay) {
total.local.regularPrice =
(total.local.regularPrice || 0) +
rate.localPrice.regularPricePerStay +
packagesPrice.local
}
total.local.price + rate.localPrice.pricePerStay + packagesPrice.price
if (rate.requestedPrice) {
if (!total.requested) {
@@ -119,17 +114,33 @@ function calculateTotalPrice(
total.requested.price =
total.requested.price +
rate.requestedPrice.pricePerStay +
packagesPrice.requested
packagesRequestedPrice.price
if (rate.requestedPrice.regularPricePerStay) {
total.requested.regularPrice =
(total.requested.regularPrice || 0) +
rate.requestedPrice.regularPricePerStay +
packagesPrice.requested
packagesRequestedPrice.price
}
}
return calculateRegularPrice({
total,
useMemberRate: !!useMemberRate,
regularMemberPrice: memberRate
? {
pricePerStay: memberRate.localPrice.pricePerNight,
regularPricePerStay: memberRate.localPrice.regularPricePerStay,
}
: undefined,
regularPublicPrice: publicRate
? {
pricePerStay: publicRate.localPrice.pricePerNight,
regularPricePerStay: publicRate.localPrice.regularPricePerStay,
}
: undefined,
return total
additionalCost: packagesPrice.price,
})
},
{
local: {
@@ -140,6 +151,15 @@ function calculateTotalPrice(
requested: undefined,
}
)
if (
totalPrice.local.regularPrice &&
totalPrice.local.price >= totalPrice.local.regularPrice
) {
totalPrice.local.regularPrice = undefined
}
return totalPrice
}
function calculateRedemptionTotalPrice(
@@ -196,6 +216,7 @@ function calculateVoucherPrice(
local: {
currency: CurrencyEnum.Voucher,
price: 0,
regularPrice: undefined,
},
requested: undefined,
}

View File

@@ -0,0 +1,78 @@
import type { Price } from "../types/price"
type RegularPrice = {
pricePerStay: number
regularPricePerStay?: number
}
// Helper function to calculate regular/strikethrough price
export function calculateRegularPrice({
total,
useMemberRate,
regularMemberPrice,
regularPublicPrice,
additionalCost = 0,
}: {
total: Price
useMemberRate: boolean
regularMemberPrice: RegularPrice | undefined
regularPublicPrice: RegularPrice | undefined
additionalCost?: number
}) {
if (
!total ||
(!useMemberRate && !regularPublicPrice) ||
(useMemberRate && !regularMemberPrice)
) {
return total
}
let basePrice = 0
// Legend:
// - total.local.price = Total Price = Black price, what the user pays
// - total.local.regularPrice = Regular Price = Strikethrough price (could potentially be none)
// - total.requested.price = Requested Price = EUR approx price
// We sometimes don't get all the required data to calculate the correct strikethrough total.
// Therefore we try these different approach to get a number that is close
// enough to the real number if all data would've been present.
if (useMemberRate && regularMemberPrice) {
if (regularPublicPrice) {
// #1 Member price uses public price as strikethrough
basePrice = regularPublicPrice.pricePerStay
} else if (regularMemberPrice.regularPricePerStay) {
// #2 Member price uses member regular price as strikethrough
basePrice = regularMemberPrice.regularPricePerStay
} else {
// #3 Member price uses member price as strikethrough
basePrice = regularMemberPrice.pricePerStay
}
} else if (regularPublicPrice) {
if (regularPublicPrice.regularPricePerStay) {
// #1 Public price uses public regular price as strikethrough
basePrice = regularPublicPrice.regularPricePerStay
} else {
// #2 Public price uses public price as strikethrough
basePrice = regularPublicPrice.pricePerStay
}
}
total.local.regularPrice = add(
total.local.regularPrice,
basePrice,
additionalCost
)
return total
}
//copied from enter-details/helpers.ts
export function add(...nums: (number | string | undefined)[]) {
return nums.reduce((total: number, num) => {
if (typeof num === "undefined") {
num = 0
}
total = total + parseInt(`${num}`)
return total
}, 0)
}

View File

@@ -62,6 +62,7 @@
"./utils/isSameBooking": "./lib/utils/isSameBooking.ts",
"./utils/url": "./lib/utils/url.ts",
"./utils/SelectRate": "./lib/utils/SelectRate/index.tsx",
"./utils/calculateRegularPrice": "./lib/utils/calculateRegularPrice.ts",
"./utils/nuqs": "./lib/utils/nuqs.ts"
},
"dependencies": {