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> </Suspense>
</div> </div>
<aside className={styles.summary}> <aside className={styles.summary}>
<MobileSummary isMember={!!user} /> <MobileSummary isUserLoggedIn={!!user} />
<DesktopSummary isMember={!!user} /> <DesktopSummary isUserLoggedIn={!!user} />
</aside> </aside>
</div> </div>
<EnterDetailsTrackingWrapper <EnterDetailsTrackingWrapper

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,19 +61,12 @@ export function MobileSummary() {
return null return null
} }
const totalRegularPrice = selectedRates.totalPrice.local?.regularPrice
? selectedRates.totalPrice.local.regularPrice
: 0
const showStrikeThroughPrice =
totalRegularPrice > selectedRates.totalPrice.local?.price
return ( return (
<div className={styles.wrapper} data-open={isSummaryOpen}> <div className={styles.wrapper} data-open={isSummaryOpen}>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.summaryAccordion}> <div className={styles.summaryAccordion}>
<SummaryContent <SummaryContent
isMember={isUserLoggedIn} isUserLoggedIn={isUserLoggedIn}
toggleSummaryOpen={toggleSummaryOpen} toggleSummaryOpen={toggleSummaryOpen}
/> />
</div> </div>
@@ -106,17 +99,17 @@ export function MobileSummary() {
)} )}
</span> </span>
</Typography> </Typography>
{showDiscounted && {showDiscounted && selectedRates.totalPrice.local?.regularPrice ? (
showStrikeThroughPrice &&
selectedRates.totalPrice.local.regularPrice ? (
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
<s className={styles.strikeThroughRate}> <p>
{formatPrice( <s className={styles.strikeThroughRate}>
intl, {formatPrice(
selectedRates.totalPrice.local.regularPrice, intl,
selectedRates.totalPrice.local.currency selectedRates.totalPrice.local?.regularPrice,
)} selectedRates.totalPrice.local.currency
</s> )}
</s>
</p>
</Typography> </Typography>
) : null} ) : 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, rate,
roomConfiguration: roomAvailability[ix]?.[0], roomConfiguration: roomAvailability[ix]?.[0],
})), })),
useMemberPrices: isUserLoggedIn, isMember: isUserLoggedIn,
}) })
const getPriceForRoom = useCallback( const getPriceForRoom = useCallback(
@@ -249,7 +249,8 @@ export function SelectRateProvider({
selectedRates: [ selectedRates: [
{ rate, roomConfiguration: roomAvailability[roomIndex]?.[0] }, { rate, roomConfiguration: roomAvailability[roomIndex]?.[0] },
], ],
useMemberPrices: isUserLoggedIn, isMember: isUserLoggedIn && roomIndex === 0,
addAdditionalCost: false,
}) })
}, },
[selectedRates, roomAvailability, isUserLoggedIn] [selectedRates, roomAvailability, isUserLoggedIn]

View File

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

View File

@@ -1,6 +1,7 @@
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" 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" import type { RedemptionProduct } from "@scandic-hotels/trpc/types/roomAvailability"
@@ -26,10 +27,12 @@ type SelectedRate = {
export function getTotalPrice({ export function getTotalPrice({
selectedRates, selectedRates,
useMemberPrices, isMember,
addAdditionalCost = true,
}: { }: {
selectedRates: Array<SelectedRate | null> selectedRates: Array<SelectedRate | null>
useMemberPrices: boolean isMember: boolean
addAdditionalCost?: boolean
}): Price | null { }): Price | null {
const mainRoom = selectedRates[0] const mainRoom = selectedRates[0]
const mainRoomRate = mainRoom?.rate const mainRoomRate = mainRoom?.rate
@@ -42,7 +45,7 @@ export function getTotalPrice({
} }
if (!mainRoomRate) { 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 // 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 voucherPrice
} }
return calculateTotalPrice(summaryArray, useMemberPrices) return calculateTotalPrice(summaryArray, isMember, addAdditionalCost)
} }
function calculateTotalPrice( function calculateTotalPrice(
selectedRateSummary: OneLevelNonNullable<SelectedRate>[], selectedRateSummary: OneLevelNonNullable<SelectedRate>[],
useMemberPrices: boolean isMember: boolean,
addAdditionalCost: boolean
) { ) {
return selectedRateSummary.reduce<Price>( const totalPrice = selectedRateSummary.reduce<Price>(
(total, room, idx) => { (total, room, idx) => {
if (!room.rate || !("member" in room.rate) || !("public" in room.rate)) { if (!room.rate || !("member" in room.rate) || !("public" in room.rate)) {
return total return total
@@ -75,34 +79,25 @@ function calculateTotalPrice(
const roomNr = idx + 1 const roomNr = idx + 1
const isMainRoom = roomNr === 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 rate = useMemberRate ? room.rate.member : room.rate.public
const publicRate = room.rate.public
const memberRate = room.rate.member
if (!rate) { if (!rate) {
return total return total
} }
const packagesPrice = room.roomConfiguration?.selectedPackages.reduce( const packagesPrice = addAdditionalCost
(total, pkg) => { ? sumPackages(room.roomConfiguration?.selectedPackages)
total.local = total.local + pkg.localPrice.totalPrice : { price: 0, currency: undefined }
if (pkg.requestedPrice.totalPrice) { const packagesRequestedPrice = addAdditionalCost
total.requested = total.requested + pkg.requestedPrice.totalPrice ? sumPackagesRequestedPrice(room.roomConfiguration?.selectedPackages)
} : { price: 0, currency: undefined }
return total
},
{ local: 0, requested: 0 }
)
total.local.currency = rate.localPrice.currency total.local.currency = rate.localPrice.currency
total.local.price = total.local.price =
total.local.price + rate.localPrice.pricePerStay + packagesPrice.local total.local.price + rate.localPrice.pricePerStay + packagesPrice.price
if (rate.localPrice.regularPricePerStay) {
total.local.regularPrice =
(total.local.regularPrice || 0) +
rate.localPrice.regularPricePerStay +
packagesPrice.local
}
if (rate.requestedPrice) { if (rate.requestedPrice) {
if (!total.requested) { if (!total.requested) {
@@ -119,17 +114,33 @@ function calculateTotalPrice(
total.requested.price = total.requested.price =
total.requested.price + total.requested.price +
rate.requestedPrice.pricePerStay + rate.requestedPrice.pricePerStay +
packagesPrice.requested packagesRequestedPrice.price
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 +
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: { local: {
@@ -140,6 +151,15 @@ function calculateTotalPrice(
requested: undefined, requested: undefined,
} }
) )
if (
totalPrice.local.regularPrice &&
totalPrice.local.price >= totalPrice.local.regularPrice
) {
totalPrice.local.regularPrice = undefined
}
return totalPrice
} }
function calculateRedemptionTotalPrice( function calculateRedemptionTotalPrice(
@@ -196,6 +216,7 @@ function calculateVoucherPrice(
local: { local: {
currency: CurrencyEnum.Voucher, currency: CurrencyEnum.Voucher,
price: 0, price: 0,
regularPrice: undefined,
}, },
requested: 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/isSameBooking": "./lib/utils/isSameBooking.ts",
"./utils/url": "./lib/utils/url.ts", "./utils/url": "./lib/utils/url.ts",
"./utils/SelectRate": "./lib/utils/SelectRate/index.tsx", "./utils/SelectRate": "./lib/utils/SelectRate/index.tsx",
"./utils/calculateRegularPrice": "./lib/utils/calculateRegularPrice.ts",
"./utils/nuqs": "./lib/utils/nuqs.ts" "./utils/nuqs": "./lib/utils/nuqs.ts"
}, },
"dependencies": { "dependencies": {