Merged in fix/refactor-currency-display (pull request #3434)

fix(SW-3616): Handle EuroBonus point type everywhere

* Add tests to formatPrice

* formatPrice

* More work replacing config with api points type

* More work replacing config with api points type

* More fixing with currency

* maybe actually fixed it

* Fix MyStay

* Clean up

* Fix comments

* Merge branch 'master' into fix/refactor-currency-display

* Fix calculateTotalPrice for EB points + SF points + cash


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2026-01-15 09:32:17 +00:00
parent c61ddaf94d
commit 16fbdb7ae0
59 changed files with 729 additions and 282 deletions

View File

@@ -9,7 +9,6 @@ import { Typography } from "../../../Typography"
import { HotelCardDialogImage } from "../../HotelCardDialogImage"
import { NoPriceAvailableCard } from "../../NoPriceAvailableCard"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { Lang } from "@scandic-hotels/common/constants/language"
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { useUrlWithSearchParam } from "@scandic-hotels/common/hooks/useUrlWithSearchParam"
@@ -26,7 +25,6 @@ interface StandaloneHotelCardProps {
isUserLoggedIn: boolean
handleClose: () => void
onClick?: () => void
pointsCurrency?: CurrencyEnum
}
export function StandaloneHotelCardDialog({
@@ -35,7 +33,6 @@ export function StandaloneHotelCardDialog({
handleClose,
isUserLoggedIn,
onClick,
pointsCurrency,
}: StandaloneHotelCardProps) {
const intl = useIntl()
const [imageError, setImageError] = useState(false)
@@ -45,6 +42,7 @@ export function StandaloneHotelCardDialog({
publicPrice,
memberPrice,
redemptionPrice,
pointsType,
voucherPrice,
currency,
amenities,
@@ -183,7 +181,7 @@ export function StandaloneHotelCardDialog({
{redemptionPrice ? (
<HotelPointsRow
pointsPerStay={redemptionPrice}
pointsCurrency={pointsCurrency}
pointsType={pointsType}
/>
) : null}
</div>

View File

@@ -3,34 +3,37 @@ import { useIntl } from "react-intl"
import { RoomPrice } from "../../HotelCard/RoomPrice"
import { Typography } from "../../Typography"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import styles from "./hotelPointsRow.module.css"
import { PointType } from "@scandic-hotels/common/constants/pointType"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { getCurrencyText } from "../../currency-utils"
export type PointsRowProps = {
pointsPerStay: number
additionalPricePerStay?: number
additionalPriceCurrency?: string
pointsCurrency?: CurrencyEnum
pointsType: PointType | null
}
export function HotelPointsRow({
pointsPerStay,
additionalPricePerStay,
additionalPriceCurrency,
pointsCurrency,
pointsType,
}: PointsRowProps) {
const intl = useIntl()
const currency = getCurrencyText(
intl,
CurrencyEnum.POINTS,
pointsPerStay,
pointsType
)
return (
<RoomPrice
className={styles.roomPrice}
price={pointsPerStay}
currency={
pointsCurrency ??
intl.formatMessage({
id: "common.points",
defaultMessage: "Points",
})
}
currency={currency}
includePerNight={false}
>
{additionalPricePerStay ? (

View File

@@ -36,6 +36,7 @@ import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
import { BookingCodeChip } from "../BookingCodeChip"
import { FakeButton } from "../FakeButton"
import { TripAdvisorChip } from "../TripAdvisorChip"
import { PointType } from "@scandic-hotels/common/constants/pointType"
type Price = {
pricePerStay: number
@@ -96,6 +97,7 @@ export type HotelCardProps = {
additionalPricePerStay: number
pointsPerStay: number
currency: CurrencyEnum | null | undefined
pointsType?: PointType | null
}
}[]
}
@@ -108,7 +110,6 @@ export type HotelCardProps = {
bookingCode?: string | null
isAlternative?: boolean
isPartnerBrand: boolean
pointsCurrency?: CurrencyEnum
fullPrice: boolean
isCampaignWithBookingCode: boolean
lang: Lang
@@ -133,7 +134,6 @@ export const HotelCardComponent = memo(
bookingCode = "",
isAlternative,
isPartnerBrand,
pointsCurrency,
images,
lang,
belowInfoSlot,
@@ -358,7 +358,7 @@ export const HotelCardComponent = memo(
additionalPriceCurrency={
redemption.localPrice.currency ?? undefined
}
pointsCurrency={pointsCurrency}
pointsType={redemption.localPrice.pointsType ?? null}
/>
))}
</div>

View File

@@ -7,6 +7,7 @@ import { Typography } from "../../../../Typography"
import HotelMarker from "../../../Markers/HotelMarker"
import styles from "./hotelPin.module.css"
import { PointType } from "@scandic-hotels/common/constants/pointType"
interface HotelPinProps {
isActive: boolean
@@ -14,6 +15,7 @@ interface HotelPinProps {
currency: string
hotelAdditionalPrice?: number
hotelAdditionalCurrency?: string
pointsType?: PointType | null
}
const NOT_AVAILABLE = "-"
export function HotelPin({
@@ -22,6 +24,7 @@ export function HotelPin({
currency,
hotelAdditionalPrice,
hotelAdditionalCurrency,
pointsType,
}: HotelPinProps) {
const intl = useIntl()
const isNotAvailable = !hotelPrice
@@ -51,7 +54,8 @@ export function HotelPin({
hotelPrice,
currency,
hotelAdditionalPrice,
hotelAdditionalCurrency
hotelAdditionalCurrency,
pointsType
)}
</p>
</Typography>

View File

@@ -5,13 +5,13 @@ import {
} from "@vis.gl/react-google-maps"
import { useMediaQuery } from "usehooks-ts"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { Lang } from "@scandic-hotels/common/constants/language"
import { useIntl } from "react-intl"
import { StandaloneHotelCardDialog } from "../../../HotelCard/HotelDialogCard/StandaloneHotelCardDialog"
import type { HotelPin as HotelPinType } from "../../types"
import styles from "./hotelListingMapContent.module.css"
import { HotelPin } from "./HotelPin"
import { getCurrencyText } from "../../../currency-utils"
export type HotelListingMapContentProps = {
hotelPins: HotelPinType[]
@@ -19,7 +19,6 @@ export type HotelListingMapContentProps = {
hoveredHotel?: string | null
lang: Lang
isUserLoggedIn: boolean
pointsCurrency?: CurrencyEnum
onClickHotel?: (hotelId: string) => void
setActiveHotel?: (args: { hotelName: string; hotelId: string } | null) => void
setHoveredHotel?: (
@@ -35,7 +34,6 @@ export function HotelListingMapContent({
setHoveredHotel,
lang,
onClickHotel,
pointsCurrency,
}: HotelListingMapContentProps) {
const intl = useIntl()
const isDesktop = useMediaQuery("(min-width: 900px)")
@@ -65,10 +63,12 @@ export function HotelListingMapContent({
null
const pinCurrency = pin.redemptionPrice
? intl.formatMessage({
id: "common.points",
defaultMessage: "Points",
})
? getCurrencyText(
intl,
pin.currency,
pin.redemptionPrice,
pin.pointsType
)
: pin.currency
const hotelAdditionalPrice = pin.chequePrice
@@ -116,7 +116,6 @@ export function HotelListingMapContent({
onClick={() => {
onClickHotel?.(pin.operaId)
}}
pointsCurrency={pointsCurrency}
/>
</InfoWindow>
)}

View File

@@ -15,7 +15,6 @@ import PoiMapMarkers from "./PoiMapMarkers"
import styles from "./interactiveMap.module.css"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { Lang } from "@scandic-hotels/common/constants/language"
import { HotelPin, MarkerInfo, PointOfInterest } from "../types"
@@ -27,7 +26,6 @@ export type InteractiveMapProps = {
}
activePoi?: string | null
hotelPins?: HotelPin[]
pointsCurrency?: CurrencyEnum
pointsOfInterest?: PointOfInterest[]
markerInfo?: MarkerInfo
mapId: string
@@ -74,7 +72,6 @@ export function InteractiveMap({
hoveredHotelPin,
activeHotelPin,
isUserLoggedIn,
pointsCurrency,
onClickHotel,
onHoverHotelPin,
onSetActiveHotelPin,
@@ -124,7 +121,6 @@ export function InteractiveMap({
activeHotel={activeHotelPin}
hoveredHotel={hoveredHotelPin}
onClickHotel={onClickHotel}
pointsCurrency={pointsCurrency}
/>
)}
{pointsOfInterest && markerInfo && (

View File

@@ -1,5 +1,6 @@
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { HotelPin } from "../types"
import { PointType } from "@scandic-hotels/common/constants/pointType"
export const hotelPins: HotelPin[] = [
{
@@ -15,6 +16,7 @@ export const hotelPins: HotelPin[] = [
voucherPrice: null,
rateType: "Regular",
currency: "SEK",
pointsType: PointType.SCANDIC,
amenities: [
{
filter: "Hotel facilities",
@@ -90,6 +92,7 @@ export const hotelPins: HotelPin[] = [
voucherPrice: null,
rateType: "Regular",
currency: "SEK",
pointsType: PointType.SCANDIC,
amenities: [
{
filter: "Hotel facilities",
@@ -168,6 +171,7 @@ export const hotelPins: HotelPin[] = [
voucherPrice: null,
rateType: "Regular",
currency: "CC",
pointsType: PointType.SCANDIC,
amenities: [
{
filter: "Hotel facilities",
@@ -242,6 +246,7 @@ export const hotelPins: HotelPin[] = [
voucherPrice: null,
rateType: "Regular",
currency: "Points",
pointsType: PointType.SCANDIC,
amenities: [
{
filter: "None",
@@ -316,6 +321,7 @@ export const hotelPins: HotelPin[] = [
voucherPrice: 1,
rateType: "Regular",
currency: "Voucher",
pointsType: PointType.SCANDIC,
amenities: [
{
filter: "Hotel facilities",

View File

@@ -1,6 +1,7 @@
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
import { HotelType } from "@scandic-hotels/common/constants/hotelType"
import { PointType } from "@scandic-hotels/common/constants/pointType"
export type HotelPin = {
bookingCode?: string | null
@@ -17,6 +18,7 @@ export type HotelPin = {
publicPrice: number | null
memberPrice: number | null
redemptionPrice: number | null
pointsType: PointType | null
voucherPrice: number | null
rateType: string | null
currency: string

View File

@@ -1,6 +1,8 @@
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
import PointsRateCard from "."
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { PointType } from "@scandic-hotels/common/constants/pointType"
const meta: Meta<typeof PointsRateCard> = {
title: "Product Components/RateCard/Points",
@@ -36,25 +38,25 @@ export const Default: Story = {
bannerText: "Reward night ∙ Breakfast included",
rates: [
{
points: "20000",
currency: "PTS",
points: 20000,
currency: CurrencyEnum.POINTS,
rateCode: "REDNIGHT7",
},
{
points: "15000",
currency: "PTS",
points: 15000,
currency: CurrencyEnum.POINTS,
additionalPrice: {
price: "250",
currency: "EUR",
currency: CurrencyEnum.EUR,
},
rateCode: "REDNIGHT7A",
},
{
points: "10000",
currency: "PTS",
points: 10000,
currency: CurrencyEnum.POINTS,
additionalPrice: {
price: "500",
currency: "EUR",
currency: CurrencyEnum.EUR,
},
rateCode: "REDNIGHT7B",
},
@@ -77,27 +79,27 @@ export const WithDisabledRates: Story = {
bannerText: "Reward night ∙ Breakfast included",
rates: [
{
points: "20000",
currency: "PTS",
points: 20000,
currency: CurrencyEnum.POINTS,
isDisabled: true,
rateCode: "REDNIGHT7",
},
{
points: "15000",
currency: "PTS",
points: 15000,
currency: CurrencyEnum.POINTS,
isDisabled: true,
additionalPrice: {
price: "250",
currency: "EUR",
currency: CurrencyEnum.EUR,
},
rateCode: "REDNIGHT7A",
},
{
points: "10000",
currency: "PTS",
points: 10000,
currency: CurrencyEnum.POINTS,
additionalPrice: {
price: "500",
currency: "EUR",
currency: CurrencyEnum.EUR,
},
rateCode: "REDNIGHT7B",
},
@@ -120,25 +122,25 @@ export const NotEnoughPoints: Story = {
bannerText: "Reward night ∙ Breakfast included",
rates: [
{
points: "20000",
currency: "PTS",
points: 20000,
currency: CurrencyEnum.POINTS,
rateCode: "REDNIGHT7",
},
{
points: "15000",
currency: "PTS",
points: 15000,
currency: CurrencyEnum.POINTS,
additionalPrice: {
price: "250",
currency: "EUR",
currency: CurrencyEnum.EUR,
},
rateCode: "REDNIGHT7A",
},
{
points: "10000",
currency: "PTS",
points: 10000,
currency: CurrencyEnum.POINTS,
additionalPrice: {
price: "500",
currency: "EUR",
currency: CurrencyEnum.EUR,
},
rateCode: "REDNIGHT7B",
},
@@ -155,3 +157,47 @@ export const NotEnoughPoints: Story = {
],
},
}
export const WithEuroBonusPoints: Story = {
args: {
rateTitle: "FREE CANCELLATION",
paymentTerm: "PAY LATER",
bannerText: "Reward night ∙ Breakfast included",
rates: [
{
points: 20000,
currency: CurrencyEnum.POINTS,
rateCode: "REDNIGHT7",
pointsType: PointType.EUROBONUS,
},
{
points: 15000,
currency: CurrencyEnum.POINTS,
additionalPrice: {
price: "250",
currency: CurrencyEnum.EUR,
},
rateCode: "REDNIGHT7A",
pointsType: PointType.EUROBONUS,
},
{
points: 10000,
currency: CurrencyEnum.POINTS,
additionalPrice: {
price: "500",
currency: CurrencyEnum.EUR,
},
rateCode: "REDNIGHT7B",
pointsType: PointType.EUROBONUS,
},
],
selectedRate: undefined,
onRateSelect: (value) => console.log(value),
rateTermDetails: [
{
title: "Rate definition 1",
terms: ["term 1", "term 2", "term 3"],
},
],
},
}

View File

@@ -10,6 +10,7 @@ import Modal from "../Modal"
import styles from "../rate-card.module.css"
import { variants } from "../variants"
import { MaterialIcon } from "../../Icons/MaterialIcon"
import { getCurrencyText } from "../../currency-utils"
interface PointsRateCardProps {
rateTitle: string
@@ -120,7 +121,7 @@ export default function PointsRateCard({
<Typography variant="Body/Supporting text (caption)/smBold">
<span>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{`${rate.currency} ${rate.additionalPrice ? " + " : ""}`}
{`${getCurrencyText(intl, rate.currency, rate.points, rate.pointsType)} ${rate.additionalPrice ? " + " : ""}`}
</span>
</Typography>
</p>

View File

@@ -1,3 +1,5 @@
import { PointType } from "@scandic-hotels/common/constants/pointType"
export type Rate = {
label?: string
price: string
@@ -6,7 +8,8 @@ export type Rate = {
export type RatePointsOption = {
rateCode: string
points: string
points: number
pointsType?: PointType | null
currency: string
isDisabled?: boolean
additionalPrice?: AdditionalPrice

View File

@@ -0,0 +1,47 @@
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { PointType } from "@scandic-hotels/common/constants/pointType"
import { logger } from "@scandic-hotels/common/logger"
import { IntlShape } from "react-intl"
export function getCurrencyText(
intl: IntlShape,
currency: string,
price: number,
pointsType?: PointType | null
) {
if (currency !== CurrencyEnum.POINTS) return currency
if (!pointsType) return currency
switch (pointsType) {
case PointType.SCANDIC: {
return intl.formatMessage(
{
id: "price.numberOfScandicPoints",
defaultMessage:
"{numberOfScandicPoints, plural, one {Point} other {Points}}",
},
{
numberOfScandicPoints: price,
}
)
}
case PointType.EUROBONUS: {
return intl.formatMessage(
{
id: "price.numberOfEuroBonusPoints",
defaultMessage:
"{numberOfEuroBonusPoints, plural, one {EB Point} other {EB Points}}",
},
{
numberOfEuroBonusPoints: price,
}
)
}
default: {
const _exhaustiveCheck: never = pointsType
void _exhaustiveCheck
logger.warn(`Unknown point type provided: ${pointsType}`)
return currency
}
}
}