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

@@ -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": {