diff --git a/apps/scandic-web/components/Forms/Edit/Profile/FormContent/index.tsx b/apps/scandic-web/components/Forms/Edit/Profile/FormContent/index.tsx index 916dc4e5c..1e06c4c0b 100644 --- a/apps/scandic-web/components/Forms/Edit/Profile/FormContent/index.tsx +++ b/apps/scandic-web/components/Forms/Edit/Profile/FormContent/index.tsx @@ -9,10 +9,10 @@ import DateSelect from "@scandic-hotels/design-system/Form/Date" import { FormInput } from "@scandic-hotels/design-system/Form/FormInput" import Phone from "@scandic-hotels/design-system/Form/Phone" import { FormSelect } from "@scandic-hotels/design-system/Form/Select" +import { PasswordInput } from "@scandic-hotels/design-system/PasswordInput" import { Typography } from "@scandic-hotels/design-system/Typography" import { getLocalizedLanguageOptions } from "@/constants/languages" -import { PasswordInput } from "@scandic-hotels/design-system/PasswordInput" import useLang from "@/hooks/useLang" import { getFormattedCountryList } from "@/utils/countries" diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts index cc2af5e1c..1e41850a5 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts @@ -34,9 +34,10 @@ export function mapToPrice(room: Room) { return { redemption: { additionalPricePerStay: room.roomPrice.perStay.local.price, - currency: room.currencyCode, + currency: CurrencyEnum.POINTS, pointsPerNight: room.roomPoints / nights, pointsPerStay: room.roomPoints, + pointsType: room.roomPointType, }, } case PriceTypeEnum.voucher: @@ -71,16 +72,6 @@ export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) { break case PriceTypeEnum.points: { - if ( - room.roomPoints && - room.roomPointType && - room.roomPointType !== "Scandic" - ) { - total.local.currency = - roomPointTypeToCurrencyMap[room.roomPointType] - total.local.price = total.local.price + room.roomPoints - break - } total.local.currency = CurrencyEnum.POINTS total.local.price = total.local.price + room.totalPoints } @@ -95,6 +86,17 @@ export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) { case PriceTypeEnum.cheque: case PriceTypeEnum.points: { + if ( + room.roomPoints && + room.roomPointType && + room.roomPointType !== "Scandic" + ) { + total.local.currency = CurrencyEnum.POINTS + total.local.price = total.local.price + room.roomPoints + total.local.pointsType = room.roomPointType + break + } + if (room.totalPrice) { total.local.additionalPrice = (total.local.additionalPrice || 0) + room.totalPrice @@ -140,16 +142,9 @@ export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) { local: { currency, price: 0, + pointsType: undefined, }, requested: undefined, } ) } - -const roomPointTypeToCurrencyMap: Record< - NonNullable, - CurrencyEnum -> = { - Scandic: CurrencyEnum.POINTS, - EuroBonus: CurrencyEnum.EUROBONUS, -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx index 826233e09..1f5595722 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx @@ -9,16 +9,15 @@ import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConf import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" -interface PriceTypeProps - extends Pick< - BookingConfirmation["booking"], - | "cheques" - | "currencyCode" - | "rateDefinition" - | "totalPoints" - | "totalPrice" - | "vouchers" - > { +interface PriceTypeProps extends Pick< + BookingConfirmation["booking"], + | "cheques" + | "currencyCode" + | "rateDefinition" + | "totalPoints" + | "totalPrice" + | "vouchers" +> { formattedTotalPrice: string isCancelled: boolean priceType: PriceTypeEnum diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx index 07c6e7a39..e62038129 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx @@ -10,7 +10,6 @@ export default function TotalPrice() { const { bookedRoom, totalPrice } = useMyStayStore((state) => ({ bookedRoom: state.bookedRoom, totalPrice: state.totalPrice, - rooms: state.rooms, })) return ( + createIntl({ locale, messages: {}, onError: () => {} }, cache) + describe("calculateTotalPrice", () => { const baseRoom: Parameters[0][0] = { totalPrice: 0, isCancelled: false, cheques: 0, - roomPoints: 0, roomPointType: null, totalPoints: 0, + roomPoints: 0, vouchers: 0, } - const mockIntlSimple = { - formatMessage: vi.fn(({}, values) => { - if (values?.numberOfVouchers === 1) return "Voucher" - return "Vouchers" - }), - formatNumber: vi.fn((num) => String(num)), - } as any - vi.mock("@scandic-hotels/common/utils/numberFormatting", () => ({ formatPrice: (_intl: any, price: number, currency: string) => `${price} ${currency}`, @@ -40,7 +38,7 @@ describe("calculateTotalPrice", () => { const result = calculateTotalPrice( rooms, "SEK" as any, - mockIntlSimple, + createTestIntl(), false ) expect(result).toBe("1500 SEK") @@ -60,7 +58,7 @@ describe("calculateTotalPrice", () => { const result = calculateTotalPrice( rooms, "SEK" as any, - mockIntlSimple, + createTestIntl(), false ) expect(result).toBe("1000 SEK") @@ -70,7 +68,7 @@ describe("calculateTotalPrice", () => { const result = calculateTotalPrice( [{ ...baseRoom, vouchers: 2, totalPrice: -1, isCancelled: false }], "SEK" as any, - mockIntlSimple, + createTestIntl(), false ) expect(result).toContain("2 Vouchers") @@ -84,19 +82,17 @@ describe("calculateTotalPrice", () => { totalPrice: 100, isCancelled: false, totalPoints: 0, - roomPoints: 0, }, { ...baseRoom, totalPrice: 0, totalPoints: 20000, - roomPoints: 20000, roomPointType: "Scandic", isCancelled: false, }, ], "SEK" as any, - mockIntlSimple, + createTestIntl(), false ) @@ -109,7 +105,7 @@ describe("calculateTotalPrice", () => { const result = calculateTotalPrice( rooms, "SEK" as any, - mockIntlSimple, + createTestIntl(), false ) expect(result).toContain("2 CC") @@ -131,7 +127,7 @@ describe("calculateTotalPrice", () => { }, ], "SEK" as any, - mockIntlSimple, + createTestIntl(), false ) expect(result).toMatch(/1 Voucher \+ 500 SEK/) @@ -151,10 +147,10 @@ describe("calculateTotalPrice", () => { }, ], "SEK" as any, - mockIntlSimple, + createTestIntl(), false ) - expect(result).toMatch(/1 EuroBonus \+ 500 SEK/) + expect(result).toMatch(/1 EB Point \+ 500 SEK/) }) it("should combine Eurobonus points, Scandic Friends points and Cash", () => { @@ -172,9 +168,9 @@ describe("calculateTotalPrice", () => { }, ], "SEK" as any, - mockIntlSimple, + createTestIntl(), false ) - expect(result).toMatch(/500 Points \+ 1 EuroBonus \+ 500 SEK/) + expect(result).toMatch(/1 EB Point \+ 500 Points \+ 500 SEK/) }) }) diff --git a/apps/scandic-web/stores/my-stay/helpers.ts b/apps/scandic-web/stores/my-stay/helpers.ts index 1f52aa81c..3a4422d0b 100644 --- a/apps/scandic-web/stores/my-stay/helpers.ts +++ b/apps/scandic-web/stores/my-stay/helpers.ts @@ -1,4 +1,5 @@ import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" +import { PointType } from "@scandic-hotels/common/constants/pointType" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import type { IntlShape } from "react-intl" @@ -10,8 +11,8 @@ export function calculateTotalPrice( Room, | "cheques" | "vouchers" - | "roomPoints" | "roomPointType" + | "roomPoints" | "totalPoints" | "totalPrice" | "isCancelled" @@ -34,18 +35,19 @@ export function calculateTotalPrice( total.vouchers = total.vouchers + room.vouchers } + // If roomPointType isn't Scandic, these should be counted as partnerPoints. + // If they're not Scandic points, we can ignore them on roomPoints as they + // are included in totalPoints, which in turn never contain partner points. if ( room.roomPoints && room.roomPointType && room.roomPointType !== "Scandic" ) { total.partnerPoints = total.partnerPoints + room.roomPoints - total.partnerPointsCurrency = room.roomPointType ?? null } if (room.totalPoints) { - total.scandicFriendsPoints = - total.scandicFriendsPoints + room.totalPoints + total.scandicPoints = total.scandicPoints + room.totalPoints } // room.totalPrice is a negative value when @@ -59,9 +61,8 @@ export function calculateTotalPrice( { cash: 0, cheques: 0, - scandicFriendsPoints: 0, + scandicPoints: 0, partnerPoints: 0, - partnerPointsCurrency: null as Room["roomPointType"], vouchers: 0, } ) @@ -86,12 +87,24 @@ export function calculateTotalPrice( priceParts.push(`${totals.cheques} ${CurrencyEnum.CC}`) } - if (totals.scandicFriendsPoints) { - priceParts.push(`${totals.scandicFriendsPoints} ${CurrencyEnum.POINTS}`) + if (totals.partnerPoints) { + // We can assume that all rooms has the same point type + const roomPointType = rooms[0]?.roomPointType || PointType.SCANDIC + const currencyText = getPointsCurrencyText( + totals.partnerPoints, + roomPointType, + intl + ) + priceParts.push(`${totals.partnerPoints} ${currencyText}`) } - if (totals.partnerPoints) { - priceParts.push(`${totals.partnerPoints} ${totals.partnerPointsCurrency}`) + if (totals.scandicPoints) { + const currencyText = getPointsCurrencyText( + totals.scandicPoints, + PointType.SCANDIC, + intl + ) + priceParts.push(`${totals.scandicPoints} ${currencyText}`) } if (totals.cash) { @@ -102,18 +115,43 @@ export function calculateTotalPrice( return priceParts.join(" + ") } -export function calculateTotalPoints( - rooms: Room[], - allRoomsAreCancelled: boolean -) { - return rooms.reduce((total, room) => { - if (!allRoomsAreCancelled && room.isCancelled) { - return total - } - return total + room.totalPoints - }, 0) -} - export function isAllRoomsCancelled(rooms: Room[]) { return !rooms.some((room) => room.isCancelled === false) } + +function getPointsCurrencyText( + points: number, + pointsType: PointType, + intl: IntlShape +) { + switch (pointsType) { + case PointType.SCANDIC: { + return intl.formatMessage( + { + id: "price.numberOfScandicPoints", + defaultMessage: + "{numberOfScandicPoints, plural, one {Point} other {Points}}", + }, + { + numberOfScandicPoints: points, + } + ) + } + case PointType.EUROBONUS: { + return intl.formatMessage( + { + id: "price.numberOfEuroBonusPoints", + defaultMessage: + "{numberOfEuroBonusPoints, plural, one {EB Point} other {EB Points}}", + }, + { + numberOfEuroBonusPoints: points, + } + ) + } + default: { + const _exhaustiveCheck: never = pointsType + return "Points" + } + } +} diff --git a/apps/scandic-web/stores/my-stay/index.ts b/apps/scandic-web/stores/my-stay/index.ts index 84fcc0d3d..0ad918b2d 100644 --- a/apps/scandic-web/stores/my-stay/index.ts +++ b/apps/scandic-web/stores/my-stay/index.ts @@ -8,11 +8,7 @@ import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers" import { mapRoomDetails } from "@/components/HotelReservation/MyStay/utils/mapRoomDetails" import { MyStayContext } from "@/contexts/MyStay" -import { - calculateTotalPoints, - calculateTotalPrice, - isAllRoomsCancelled, -} from "./helpers" +import { calculateTotalPrice, isAllRoomsCancelled } from "./helpers" import type { InitialState, MyStayState } from "@/types/stores/my-stay" @@ -56,8 +52,6 @@ export function createMyStayStore({ const allRoomsAreCancelled = isAllRoomsCancelled(mappedRooms) - const totalPoints = calculateTotalPoints(mappedRooms, allRoomsAreCancelled) - const totalPrice = calculateTotalPrice( mappedRooms, bookedRoom.currencyCode, @@ -80,7 +74,6 @@ export function createMyStayStore({ refId, rooms: mappedRooms, savedCreditCards, - totalPoints, totalPrice, isPastBooking, diff --git a/apps/scandic-web/types/components/hotelReservation/price.ts b/apps/scandic-web/types/components/hotelReservation/price.ts index 8ad792728..0995a8821 100644 --- a/apps/scandic-web/types/components/hotelReservation/price.ts +++ b/apps/scandic-web/types/components/hotelReservation/price.ts @@ -1,4 +1,5 @@ import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency" +import type { PointType } from "@scandic-hotels/common/constants/pointType" interface TPrice { additionalPrice?: number @@ -6,6 +7,7 @@ interface TPrice { currency: CurrencyEnum price: number regularPrice?: number + pointsType?: PointType | null } export interface Price { diff --git a/apps/scandic-web/types/stores/my-stay.ts b/apps/scandic-web/types/stores/my-stay.ts index 215f6ed1e..36c841324 100644 --- a/apps/scandic-web/types/stores/my-stay.ts +++ b/apps/scandic-web/types/stores/my-stay.ts @@ -57,7 +57,6 @@ export interface MyStayState { refId: string rooms: Room[] savedCreditCards: CreditCard[] | null - totalPoints: number totalPrice: string isPastBooking: boolean } diff --git a/packages/booking-flow/lib/bookingFlowConfig/bookingFlowConfigContext.tsx b/packages/booking-flow/lib/bookingFlowConfig/bookingFlowConfigContext.tsx index 9958bf377..437fcc64d 100644 --- a/packages/booking-flow/lib/bookingFlowConfig/bookingFlowConfigContext.tsx +++ b/packages/booking-flow/lib/bookingFlowConfig/bookingFlowConfigContext.tsx @@ -2,8 +2,6 @@ import { createContext, useContext } from "react" -import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" - import type { BookingFlowConfig } from "./bookingFlowConfig" type BookingFlowConfigContextData = BookingFlowConfig @@ -24,20 +22,6 @@ export const useBookingFlowConfig = (): BookingFlowConfigContextData => { return context } -export const useGetPointsCurrency = () => { - const config = useBookingFlowConfig() - - switch (config.variant) { - case "scandic": - return CurrencyEnum.POINTS - case "partner-sas": - return CurrencyEnum.EUROBONUS - default: - const _exhaustiveCheck: never = config.variant - throw new Error(`Unknown variant: ${config.variant}`) - } -} - export function BookingFlowConfigContextProvider({ children, config, diff --git a/packages/booking-flow/lib/components/BookingConfirmation/Receipt/index.tsx b/packages/booking-flow/lib/components/BookingConfirmation/Receipt/index.tsx index ccb92a7b8..d4acb7075 100644 --- a/packages/booking-flow/lib/components/BookingConfirmation/Receipt/index.tsx +++ b/packages/booking-flow/lib/components/BookingConfirmation/Receipt/index.tsx @@ -11,7 +11,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography" import useLang from "../../../hooks/useLang" import { useBookingConfirmationStore } from "../../../stores/booking-confirmation" -import { ReceiptRoom as Room } from "./Room" +import { ReceiptRoom } from "./Room" import TotalPrice from "./TotalPrice" import styles from "./receipt.module.css" @@ -69,7 +69,7 @@ export function Receipt() { {filteredRooms.map((room, idx) => ( - diff --git a/packages/booking-flow/lib/components/HotelCardDialogListing/utils.ts b/packages/booking-flow/lib/components/HotelCardDialogListing/utils.ts index 3c0eeb94c..63fb3ceae 100644 --- a/packages/booking-flow/lib/components/HotelCardDialogListing/utils.ts +++ b/packages/booking-flow/lib/components/HotelCardDialogListing/utils.ts @@ -1,3 +1,4 @@ +import type { PointType } from "@scandic-hotels/common/constants/pointType" import type { imageSchema } from "@scandic-hotels/trpc/routers/hotels/schemas/image" import type { ProductTypeCheque } from "@scandic-hotels/trpc/types/availability" import type { Amenities } from "@scandic-hotels/trpc/types/hotel" @@ -19,6 +20,7 @@ export type HotelPin = { publicPrice: number | null memberPrice: number | null redemptionPrice: number | null + pointsType: PointType | null voucherPrice: number | null rateType: string | null currency: string @@ -59,6 +61,7 @@ export function getHotelPins( publicPrice: productType?.public?.localPrice.pricePerNight ?? null, memberPrice: productType?.member?.localPrice.pricePerNight ?? null, redemptionPrice: redemptionRate?.localPrice.pointsPerStay ?? null, + pointsType: redemptionRate?.localPrice.pointsType ?? null, voucherPrice: voucherPrice ?? null, rateType: productType?.public?.rateType ?? productType?.member?.rateType ?? null, diff --git a/packages/booking-flow/lib/components/HotelCardListing/index.tsx b/packages/booking-flow/lib/components/HotelCardListing/index.tsx index cc9b49b87..2d372b3b5 100644 --- a/packages/booking-flow/lib/components/HotelCardListing/index.tsx +++ b/packages/booking-flow/lib/components/HotelCardListing/index.tsx @@ -13,10 +13,7 @@ import { useScrollToTop } from "@scandic-hotels/common/hooks/useScrollToTop" import { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton" import { HotelCard } from "@scandic-hotels/design-system/HotelCard" -import { - useBookingFlowConfig, - useGetPointsCurrency, -} from "../../bookingFlowConfig/bookingFlowConfigContext" +import { useBookingFlowConfig } from "../../bookingFlowConfig/bookingFlowConfigContext" import { useIsLoggedIn } from "../../hooks/useIsLoggedIn" import useLang from "../../hooks/useLang" import { mapApiImagesToGalleryImages } from "../../misc/imageGallery" @@ -65,7 +62,6 @@ export default function HotelCardListing({ const { activeHotel, activate, disengage, engage } = useHotelsMapStore() const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 }) const activeCardRef = useRef(null) - const pointsCurrency = useGetPointsCurrency() const config = useBookingFlowConfig() const sortBy = searchParams.get("sort") ?? DEFAULT_SORT @@ -183,7 +179,6 @@ export default function HotelCardListing({ tripAdvisor: hotel.hotel.ratings?.tripAdvisor.rating, }, }} - pointsCurrency={pointsCurrency} lang={lang} fullPrice={!hotel.availability.bookingCode} prices={ diff --git a/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx b/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx index b13513719..72c92a288 100644 --- a/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx +++ b/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx @@ -14,7 +14,6 @@ import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton import Link from "@scandic-hotels/design-system/OldDSLink" import { Typography } from "@scandic-hotels/design-system/Typography" -import { useGetPointsCurrency } from "../../bookingFlowConfig/bookingFlowConfigContext" import { useIsLoggedIn } from "../../hooks/useIsLoggedIn" import useLang from "../../hooks/useLang" @@ -33,7 +32,6 @@ export default function ListingHotelCardDialog({ }: ListingHotelCardProps) { const intl = useIntl() const lang = useLang() - const pointsCurrency = useGetPointsCurrency() const [imageError, setImageError] = useState(false) @@ -49,6 +47,7 @@ export default function ListingHotelCardDialog({ ratings, operaId, redemptionPrice, + pointsType, chequePrice, voucherPrice, hasEnoughPoints, @@ -183,7 +182,7 @@ export default function ListingHotelCardDialog({ {redemptionPrice && ( )} {chequePrice && ( diff --git a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Large.tsx b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Large.tsx index 71d1bdf4d..d4f8aca4b 100644 --- a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Large.tsx +++ b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Large.tsx @@ -26,7 +26,8 @@ export default function LargeRow({ price.local.price, price.local.currency, price.local.additionalPrice, - price.local.additionalPriceCurrency + price.local.additionalPriceCurrency, + price.local.pointsType ) const regularPrice = price.local.regularPrice ? formatPrice( @@ -34,7 +35,8 @@ export default function LargeRow({ price.local.regularPrice, price.local.currency, price.local.additionalPrice, - price.local.additionalPriceCurrency + price.local.additionalPriceCurrency, + price.local.pointsType ) : null diff --git a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Price/Redemption.tsx b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Price/Redemption.tsx index e9307708e..6e05762af 100644 --- a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Price/Redemption.tsx +++ b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Price/Redemption.tsx @@ -1,16 +1,15 @@ "use client" -import { useIntl } from "react-intl" +import { type IntlShape, useIntl } from "react-intl" +import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" +import { PointType } from "@scandic-hotels/common/constants/pointType" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" -import { useGetPointsCurrency } from "../../../../../bookingFlowConfig/bookingFlowConfigContext" import BoldRow from "../Bold" import RegularRow from "../Regular" import BedTypeRow from "./BedType" import PackagesRow from "./Packages" -import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency" - import type { SharedPriceRowProps } from "./price" export interface RedemptionPriceType { @@ -19,11 +18,12 @@ export interface RedemptionPriceType { currency?: CurrencyEnum pointsPerNight: number pointsPerStay: number + pointsType: PointType } } interface RedemptionPriceProps extends SharedPriceRowProps { - currency: string + currency: CurrencyEnum nights: number price: RedemptionPriceType["redemption"] } @@ -36,7 +36,6 @@ export default function RedemptionPrice({ price, }: RedemptionPriceProps) { const intl = useIntl() - const pointsCurrency = useGetPointsCurrency() if (!price) { return null @@ -53,10 +52,17 @@ export default function RedemptionPrice({ ? Math.ceil(additionalPricePerStay / nights) : null - const additionalCurrency = price.currency ?? currency - let averagePricePerNight = `${price.pointsPerNight} ${pointsCurrency}` + const actualCurrency = price.currency || currency + const formattedCurrency = getCurrencyText( + price.pointsPerStay, + actualCurrency, + price.pointsType, + intl + ) + + let averagePricePerNight = `${price.pointsPerNight} ${formattedCurrency}` if (averageAdditionalPricePerNight) { - averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}` + averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${formattedCurrency}` } return ( @@ -69,9 +75,10 @@ export default function RedemptionPrice({ value={formatPrice( intl, price.pointsPerStay, - pointsCurrency, + currency, additionalPricePerStay, - additionalCurrency + formattedCurrency, + price.pointsType )} /> {nights > 1 ? ( @@ -82,3 +89,47 @@ export default function RedemptionPrice({ ) } + +function getCurrencyText( + points: number, + currency: CurrencyEnum | undefined, + pointsType: PointType, + intl: IntlShape +) { + if (!currency) return currency + + if (currency === CurrencyEnum.POINTS) { + switch (pointsType) { + case PointType.SCANDIC: { + return intl.formatMessage( + { + id: "price.numberOfScandicPoints", + defaultMessage: + "{numberOfScandicPoints, plural, one {Point} other {Points}}", + }, + { + numberOfScandicPoints: points, + } + ) + } + case PointType.EUROBONUS: { + return intl.formatMessage( + { + id: "price.numberOfEuroBonusPoints", + defaultMessage: + "{numberOfEuroBonusPoints, plural, one {EB Point} other {EB Points}}", + }, + { + numberOfEuroBonusPoints: points, + } + ) + } + default: { + const _exhaustiveCheck: never = pointsType + return currency + } + } + } + + return currency +} diff --git a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx index d38f41987..8dc92dcb8 100644 --- a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx +++ b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx @@ -17,7 +17,6 @@ interface VatProps { const noVatCurrencies = [ CurrencyEnum.CC, CurrencyEnum.POINTS, - CurrencyEnum.EUROBONUS, CurrencyEnum.Voucher, CurrencyEnum.Unknown, ] diff --git a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/index.tsx b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/index.tsx index c91d3b76c..e41405b0f 100644 --- a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/index.tsx +++ b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/index.tsx @@ -119,7 +119,7 @@ export default function PriceDetailsTable({ return ( {rooms.map((room, idx) => { - let currency = "" + let currency: CurrencyEnum = defaultCurrency let chequePrice: CorporateChequePriceType["corporateCheque"] | undefined if ("corporateCheque" in room.price && room.price.corporateCheque) { chequePrice = room.price.corporateCheque @@ -151,10 +151,6 @@ export default function PriceDetailsTable({ voucherPrice = room.price.voucher } - if (!currency) { - currency = defaultCurrency - } - if (!price && !voucherPrice && !chequePrice && !redemptionPrice) { return null } diff --git a/packages/booking-flow/lib/components/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx b/packages/booking-flow/lib/components/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx index 09c970665..b6abd010c 100644 --- a/packages/booking-flow/lib/components/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx +++ b/packages/booking-flow/lib/components/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx @@ -19,7 +19,6 @@ import Link from "@scandic-hotels/design-system/OldDSLink" import { Typography } from "@scandic-hotels/design-system/Typography" import { trackEvent } from "@scandic-hotels/tracking/base" -import { useGetPointsCurrency } from "../../../../bookingFlowConfig/bookingFlowConfigContext" import { useIsLoggedIn } from "../../../../hooks/useIsLoggedIn" import useLang from "../../../../hooks/useLang" import { mapApiImagesToGalleryImages } from "../../../../misc/imageGallery" @@ -80,7 +79,6 @@ export function SelectHotelMapContent({ const setResultCount = useHotelResultCountStore( (state) => state.setResultCount ) - const pointsCurrency = useGetPointsCurrency() const hotelMapStore = useHotelsMapStore() @@ -266,7 +264,6 @@ export function SelectHotelMapContent({ )} { diff --git a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/DesktopSummary.tsx b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/DesktopSummary.tsx index 88482daac..6d10c47fe 100644 --- a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/DesktopSummary.tsx +++ b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/DesktopSummary.tsx @@ -167,7 +167,8 @@ export function DesktopSummary({ selectedRates.totalPrice.local.price, selectedRates.totalPrice.local.currency, selectedRates.totalPrice.local.additionalPrice, - selectedRates.totalPrice.local.additionalPriceCurrency + selectedRates.totalPrice.local.additionalPriceCurrency, + selectedRates.totalPrice.local.pointsType )}

@@ -203,7 +204,8 @@ export function DesktopSummary({ selectedRates.totalPrice.requested.currency, selectedRates.totalPrice.requested.additionalPrice, selectedRates.totalPrice.requested - .additionalPriceCurrency + .additionalPriceCurrency, + selectedRates.totalPrice.local.pointsType ), } )} diff --git a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/Content/index.tsx b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/Content/index.tsx index 9ad6a6263..c141e6fff 100644 --- a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/Content/index.tsx +++ b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/Content/index.tsx @@ -195,7 +195,8 @@ export default function SummaryContent({ selectedRates.totalPrice.local.price, selectedRates.totalPrice.local.currency, selectedRates.totalPrice.local.additionalPrice, - selectedRates.totalPrice.local.additionalPriceCurrency + selectedRates.totalPrice.local.additionalPriceCurrency, + selectedRates.totalPrice.local.pointsType )} diff --git a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/Room/index.tsx b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/Room/index.tsx index eb9e2fbc9..b99725560 100644 --- a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/Room/index.tsx +++ b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/Room/index.tsx @@ -137,7 +137,8 @@ export default function Room({ room.roomPrice.perStay.local.price, room.roomPrice.perStay.local.currency, room.roomPrice.perStay.local.additionalPrice, - room.roomPrice.perStay.local.additionalPriceCurrency + room.roomPrice.perStay.local.additionalPriceCurrency, + room.roomPrice.perStay.local.pointsType )}

{showDiscounted && room.roomPrice.perStay.local.regularPrice ? ( diff --git a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx index 4c4aacb90..42acd6895 100644 --- a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx +++ b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/MobileSummary/index.tsx @@ -100,7 +100,8 @@ export function MobileSummary() { selectedRates.totalPrice.local.price, selectedRates.totalPrice.local.currency, selectedRates.totalPrice.local.additionalPrice, - selectedRates.totalPrice.local.additionalPriceCurrency + selectedRates.totalPrice.local.additionalPriceCurrency, + selectedRates.totalPrice.local.pointsType )} diff --git a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Redemptions.tsx b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Redemptions.tsx index 6770a0207..7a8ce709b 100644 --- a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Redemptions.tsx +++ b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Redemptions.tsx @@ -1,9 +1,9 @@ "use client" import { useIntl } from "react-intl" +import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import PointsRateCard from "@scandic-hotels/design-system/PointsRateCard" -import { useGetPointsCurrency } from "../../../../../../../bookingFlowConfig/bookingFlowConfigContext" import { useSelectRateContext } from "../../../../../../../contexts/SelectRate/SelectRateContext" import { BookingCodeFilterEnum } from "../../../../../../../stores/bookingCode-filter" import { sumPackages } from "../../../../../../../utils/SelectRate" @@ -35,7 +35,6 @@ export default function Redemptions({ selectedRates, } = useSelectRateContext() const roomNr = roomIndex + 1 - const pointsCurrency = useGetPointsCurrency() // TODO: Replace with context value when we have support for dropdown "Show all rates" const selectedFilter = BookingCodeFilterEnum.All as BookingCodeFilterEnum @@ -92,9 +91,10 @@ export default function Redemptions({ price: additionalPrice.toString(), } : undefined, - currency: pointsCurrency ?? "PTS", + currency: CurrencyEnum.POINTS, isDisabled: !r.redemption.hasEnoughPoints, - points: r.redemption.localPrice.pointsPerStay.toString(), + points: r.redemption.localPrice.pointsPerStay, + pointsType: r.redemption.localPrice.pointsType, rateCode: r.redemption.rateCode, } }) diff --git a/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx b/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx index 2fac5fafb..d6bb4308a 100644 --- a/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx +++ b/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx @@ -6,7 +6,6 @@ import { createContext, useEffect, useRef, useState } from "react" import { dt } from "@scandic-hotels/common/dt" import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner" -import { useGetPointsCurrency } from "../../bookingFlowConfig/bookingFlowConfigContext" import { getMultiroomDetailsSchema } from "../../components/EnterDetails/Details/Multiroom/schema" import { guestDetailsSchema } from "../../components/EnterDetails/Details/RoomOne/schema" import { @@ -65,7 +64,6 @@ export default function EnterDetailsProvider({ // rendering the form until that has been done. const [hasInitializedStore, setHasInitializedStore] = useState(false) const storeRef = useRef(undefined) - const pointsCurrency = useGetPointsCurrency() // eslint-disable-next-line react-hooks/refs if (!storeRef.current) { const initialData: InitialState = { @@ -108,8 +106,7 @@ export default function EnterDetailsProvider({ searchParamsStr, user, breakfastPackages, - lang, - pointsCurrency + lang ) } diff --git a/packages/booking-flow/lib/contexts/SelectRate/SelectRateContext/index.tsx b/packages/booking-flow/lib/contexts/SelectRate/SelectRateContext/index.tsx index 2f79c3ed8..9dcaba399 100644 --- a/packages/booking-flow/lib/contexts/SelectRate/SelectRateContext/index.tsx +++ b/packages/booking-flow/lib/contexts/SelectRate/SelectRateContext/index.tsx @@ -12,7 +12,6 @@ import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel" import { selectRateRoomsAvailabilityInputSchema } from "@scandic-hotels/trpc/routers/hotels/availability/selectRate/rooms/schema" -import { useGetPointsCurrency } from "../../../bookingFlowConfig/bookingFlowConfigContext" import { useIsLoggedIn } from "../../../hooks/useIsLoggedIn" import useLang from "../../../hooks/useLang" import { BookingCodeFilterEnum } from "../../../stores/bookingCode-filter" @@ -63,7 +62,6 @@ export function SelectRateProvider({ const updateBooking = useUpdateBooking() const isUserLoggedIn = useIsLoggedIn() const intl = useIntl() - const pointsCurrency = useGetPointsCurrency() const [activeRoomIndex, setInternalActiveRoomIndex] = useQueryState( "activeRoomIndex", @@ -228,7 +226,6 @@ export function SelectRateProvider({ roomConfiguration: roomAvailability[ix]?.[0], })), isMember: isUserLoggedIn, - pointsCurrency, }) const getPriceForRoom = useCallback( @@ -249,10 +246,9 @@ export function SelectRateProvider({ ], isMember: isUserLoggedIn && roomIndex === 0, addAdditionalCost: false, - pointsCurrency, }) }, - [selectedRates, roomAvailability, isUserLoggedIn, pointsCurrency] + [selectedRates, roomAvailability, isUserLoggedIn] ) const setActiveRoomIndex = useCallback( diff --git a/packages/booking-flow/lib/contexts/SelectRate/getTotalPrice.ts b/packages/booking-flow/lib/contexts/SelectRate/getTotalPrice.ts index 30e2a2398..c31d7f102 100644 --- a/packages/booking-flow/lib/contexts/SelectRate/getTotalPrice.ts +++ b/packages/booking-flow/lib/contexts/SelectRate/getTotalPrice.ts @@ -17,12 +17,10 @@ export function getTotalPrice({ selectedRates, isMember, addAdditionalCost = true, - pointsCurrency, }: { selectedRates: Array isMember: boolean addAdditionalCost?: boolean - pointsCurrency?: CurrencyEnum }): Price | null { const mainRoom = selectedRates[0] const mainRoomRate = mainRoom?.rate @@ -47,8 +45,7 @@ export function getTotalPrice({ mainRoom.roomConfiguration?.selectedPackages.filter( (pkg) => "localPrice" in pkg ) ?? null, - addAdditionalCost, - pointsCurrency + addAdditionalCost ) } if ("voucher" in mainRoomRate) { @@ -159,8 +156,7 @@ function calculateTotalPrice( function calculateRedemptionTotalPrice( redemption: RedemptionProduct["redemption"], packages: RoomPackage[] | null, - addAdditonalCost: boolean, - pointsCurrency?: CurrencyEnum + addAdditonalCost: boolean ) { const pkgsSum = addAdditonalCost ? sumPackages(packages) @@ -183,8 +179,9 @@ function calculateRedemptionTotalPrice( local: { additionalPrice, additionalPriceCurrency, - currency: pointsCurrency ?? CurrencyEnum.POINTS, + currency: CurrencyEnum.POINTS, price: redemption.localPrice.pointsPerStay, + pointsType: redemption.localPrice.pointsType, }, } } diff --git a/packages/booking-flow/lib/providers/BookingConfirmationProvider.tsx b/packages/booking-flow/lib/providers/BookingConfirmationProvider.tsx index ac63bcc39..b85aa48a2 100644 --- a/packages/booking-flow/lib/providers/BookingConfirmationProvider.tsx +++ b/packages/booking-flow/lib/providers/BookingConfirmationProvider.tsx @@ -45,13 +45,16 @@ export function BookingConfirmationProvider({ let isVatCurrency = true if (totalBookingPoints) { + // We can assume all rooms have the same point type + const pointsType = rooms?.[0]?.pointsType isVatCurrency = false formattedTotalCost = formatPrice( intl, totalBookingPoints, CurrencyEnum.POINTS, totalBookingPrice, - currencyCode + currencyCode, + pointsType ) } else if (totalBookingCheques) { isVatCurrency = false diff --git a/packages/booking-flow/lib/stores/enter-details/index.ts b/packages/booking-flow/lib/stores/enter-details/index.ts index d2a1b5a36..25a63f202 100644 --- a/packages/booking-flow/lib/stores/enter-details/index.ts +++ b/packages/booking-flow/lib/stores/enter-details/index.ts @@ -15,7 +15,6 @@ import { } from "./helpers" import { getRoomPrice, getTotalPrice } from "./priceCalculations" -import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import type { Lang } from "@scandic-hotels/common/constants/language" import type { BreakfastPackages } from "@scandic-hotels/trpc/routers/hotels/output" import type { User } from "@scandic-hotels/trpc/types/user" @@ -41,8 +40,7 @@ export function createDetailsStore( searchParams: string, user: User | null, breakfastPackages: BreakfastPackages, - lang: Lang, - pointsCurrency?: CurrencyEnum + lang: Lang ) { const isMember = !!user const nights = dt(initialState.booking.toDate).diff( @@ -67,23 +65,14 @@ export function createDetailsStore( ...defaultGuestState, phoneNumberCC: getDefaultCountryFromLang(lang), }, - roomPrice: getRoomPrice( - room.roomRate, - isMember && idx === 0, - pointsCurrency - ), + roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0), specialRequest: { comment: "", }, } }) - const initialTotalPrice = getTotalPrice( - initialRooms, - isMember, - nights, - pointsCurrency - ) + const initialTotalPrice = getTotalPrice(initialRooms, isMember, nights) const availableBeds = initialState.rooms.reduce< DetailsState["availableBeds"] @@ -184,8 +173,7 @@ export function createDetailsStore( state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, - nights, - pointsCurrency + nights ) const isAllStepsCompleted = checkRoomProgress( @@ -216,8 +204,7 @@ export function createDetailsStore( } currentRoom.roomPrice = getRoomPrice( currentRoom.roomRate, - isValidMembershipNo || currentRoom.guest.join, - pointsCurrency + isValidMembershipNo || currentRoom.guest.join ) const nights = dt(state.booking.toDate).diff( @@ -228,8 +215,7 @@ export function createDetailsStore( state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, - nights, - pointsCurrency + nights ) writeToSessionStorage({ @@ -252,8 +238,7 @@ export function createDetailsStore( currentRoom.roomPrice = getRoomPrice( currentRoom.roomRate, - join || !!currentRoom.guest.membershipNo, - pointsCurrency + join || !!currentRoom.guest.membershipNo ) const nights = dt(state.booking.toDate).diff( @@ -264,8 +249,7 @@ export function createDetailsStore( state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, - nights, - pointsCurrency + nights ) writeToSessionStorage({ @@ -330,8 +314,7 @@ export function createDetailsStore( currentRoom.roomPrice = getRoomPrice( currentRoom.roomRate, - Boolean(data.join || data.membershipNo || isMemberAndRoomOne), - pointsCurrency + Boolean(data.join || data.membershipNo || isMemberAndRoomOne) ) const nights = dt(state.booking.toDate).diff( @@ -342,8 +325,7 @@ export function createDetailsStore( state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, - nights, - pointsCurrency + nights ) const isAllStepsCompleted = checkRoomProgress( diff --git a/packages/booking-flow/lib/stores/enter-details/priceCalculations.test.ts b/packages/booking-flow/lib/stores/enter-details/priceCalculations.test.ts index de04ad939..59348ac62 100644 --- a/packages/booking-flow/lib/stores/enter-details/priceCalculations.test.ts +++ b/packages/booking-flow/lib/stores/enter-details/priceCalculations.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" +import { PointType } from "@scandic-hotels/common/constants/pointType" import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" import { @@ -998,16 +999,24 @@ describe("getRedemptionPrice", () => { const result = getRedemptionPrice([], 1) expect(result).toEqual({ - local: { price: 0, currency: CurrencyEnum.POINTS }, + local: { + price: 0, + currency: CurrencyEnum.POINTS, + pointsType: PointType.SCANDIC, + }, requested: undefined, }) }) it("returns price 0 and set currency when rooms are empty", () => { - const result = getRedemptionPrice([], 1, CurrencyEnum.EUROBONUS) + const result = getRedemptionPrice([], 1) expect(result).toEqual({ - local: { price: 0, currency: CurrencyEnum.EUROBONUS }, + local: { + price: 0, + currency: CurrencyEnum.POINTS, + pointsType: PointType.SCANDIC, + }, requested: undefined, }) }) @@ -1026,6 +1035,7 @@ describe("getRedemptionPrice", () => { pointsPerStay: 100, currency: CurrencyEnum.POINTS, additionalPricePerStay: 0, + pointsType: PointType.SCANDIC, }, }, }, @@ -1040,6 +1050,7 @@ describe("getRedemptionPrice", () => { currency: CurrencyEnum.POINTS, additionalPrice: 0, additionalPriceCurrency: CurrencyEnum.POINTS, + pointsType: PointType.SCANDIC, }, requested: undefined, }) @@ -1059,6 +1070,7 @@ describe("getRedemptionPrice", () => { pointsPerStay: 100, currency: CurrencyEnum.POINTS, additionalPricePerStay: 0, + pointsType: PointType.SCANDIC, }, }, }, @@ -1073,6 +1085,7 @@ describe("getRedemptionPrice", () => { currency: CurrencyEnum.POINTS, additionalPrice: 0, additionalPriceCurrency: CurrencyEnum.POINTS, + pointsType: PointType.SCANDIC, }, requested: undefined, }) @@ -1092,6 +1105,7 @@ describe("getRedemptionPrice", () => { pointsPerStay: 100, currency: CurrencyEnum.POINTS, additionalPricePerStay: 0, + pointsType: PointType.SCANDIC, }, }, }, @@ -1106,6 +1120,7 @@ describe("getRedemptionPrice", () => { pointsPerStay: 150, currency: CurrencyEnum.POINTS, additionalPricePerStay: 0, + pointsType: PointType.SCANDIC, }, }, }, @@ -1120,6 +1135,7 @@ describe("getRedemptionPrice", () => { currency: CurrencyEnum.POINTS, additionalPrice: 0, additionalPriceCurrency: CurrencyEnum.POINTS, + pointsType: PointType.SCANDIC, }, requested: undefined, }) @@ -1144,6 +1160,7 @@ describe("getRedemptionPrice", () => { pointsPerStay: 100, currency: CurrencyEnum.POINTS, additionalPricePerStay: 0, + pointsType: PointType.SCANDIC, }, }, }, @@ -1158,6 +1175,7 @@ describe("getRedemptionPrice", () => { currency: CurrencyEnum.POINTS, additionalPrice: 33, additionalPriceCurrency: CurrencyEnum.POINTS, + pointsType: PointType.SCANDIC, }, requested: undefined, }) @@ -1818,6 +1836,7 @@ describe("getTotalPrice", () => { pointsPerStay: 100, currency: CurrencyEnum.POINTS, additionalPricePerStay: 0, + pointsType: PointType.SCANDIC, }, }, }, @@ -1856,6 +1875,7 @@ describe("getTotalPrice", () => { currency: CurrencyEnum.POINTS, additionalPrice: 0, additionalPriceCurrency: CurrencyEnum.POINTS, + pointsType: PointType.SCANDIC, }, requested: undefined, }) diff --git a/packages/booking-flow/lib/stores/enter-details/priceCalculations.ts b/packages/booking-flow/lib/stores/enter-details/priceCalculations.ts index c105a12c8..17832bd8c 100644 --- a/packages/booking-flow/lib/stores/enter-details/priceCalculations.ts +++ b/packages/booking-flow/lib/stores/enter-details/priceCalculations.ts @@ -1,4 +1,5 @@ import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" +import { PointType } from "@scandic-hotels/common/constants/pointType" import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType" import { logger } from "@scandic-hotels/common/logger" @@ -19,11 +20,7 @@ function add(...nums: (number | string | undefined)[]) { }, 0) } -export function getRoomPrice( - roomRate: Product, - isMember: boolean, - pointsCurrency?: CurrencyEnum -) { +export function getRoomPrice(roomRate: Product, isMember: boolean) { if (isMember && "member" in roomRate && roomRate.member) { let publicRate if ( @@ -167,23 +164,25 @@ export function getRoomPrice( perNight: { requested: undefined, local: { - currency: pointsCurrency ?? CurrencyEnum.POINTS, + currency: CurrencyEnum.POINTS, price: roomRate.redemption.localPrice.pointsPerStay, additionalPrice: roomRate.redemption.localPrice.additionalPricePerStay, additionalPriceCurrency: roomRate.redemption.localPrice.currency ?? undefined, + pointsType: roomRate.redemption.localPrice.pointsType, }, }, perStay: { requested: undefined, local: { - currency: pointsCurrency ?? CurrencyEnum.POINTS, + currency: CurrencyEnum.POINTS, price: roomRate.redemption.localPrice.pointsPerStay, additionalPrice: roomRate.redemption.localPrice.additionalPricePerStay, additionalPriceCurrency: roomRate.redemption.localPrice.currency ?? undefined, + pointsType: roomRate.redemption.localPrice.pointsType, }, }, } @@ -410,17 +409,18 @@ type RedemptionRoom = BasePriceCalculationRoom & { localPrice: { pointsPerStay: number additionalPricePerStay: number + pointsType: PointType currency?: CurrencyEnum } } } } -export function getRedemptionPrice( - rooms: RedemptionRoom[], - nights: number, - pointsCurrency?: CurrencyEnum -) { +export function getRedemptionPrice(rooms: RedemptionRoom[], nights: number) { + // We can assume that all rooms have the same pointsType + const pointsType = + rooms[0]?.roomRate.redemption.localPrice.pointsType || PointType.SCANDIC + return rooms.reduce( (total, room) => { const redemption = room.roomRate.redemption @@ -444,8 +444,9 @@ export function getRedemptionPrice( }, { local: { - currency: pointsCurrency ?? CurrencyEnum.POINTS, + currency: CurrencyEnum.POINTS, price: 0, + pointsType, }, requested: undefined, } @@ -580,8 +581,7 @@ export function getTotalPrice( | VoucherRoom )[], isMember: boolean, - nights: number, - pointsCurrency?: CurrencyEnum + nights: number ) { const corporateChequeRooms = rooms.filter( (x): x is CorporateCheckRoom => "corporateCheque" in x.roomRate @@ -594,7 +594,7 @@ export function getTotalPrice( (x): x is RedemptionRoom => "redemption" in x.roomRate ) if (redemptionRooms.length > 0) { - return getRedemptionPrice(redemptionRooms, nights, pointsCurrency) + return getRedemptionPrice(redemptionRooms, nights) } const voucherRooms = rooms.filter( diff --git a/packages/booking-flow/lib/types/price.ts b/packages/booking-flow/lib/types/price.ts index 8376abfcf..6b09ca12f 100644 --- a/packages/booking-flow/lib/types/price.ts +++ b/packages/booking-flow/lib/types/price.ts @@ -1,4 +1,5 @@ import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency" +import type { PointType } from "@scandic-hotels/common/constants/pointType" interface TPrice { additionalPrice?: number @@ -6,6 +7,7 @@ interface TPrice { currency: CurrencyEnum price: number regularPrice?: number + pointsType?: PointType | null } // TODO after migration, check this type for duplicates and maybe move to better location diff --git a/packages/booking-flow/lib/types/stores/booking-confirmation.ts b/packages/booking-flow/lib/types/stores/booking-confirmation.ts index 6eeb2d96c..89809e044 100644 --- a/packages/booking-flow/lib/types/stores/booking-confirmation.ts +++ b/packages/booking-flow/lib/types/stores/booking-confirmation.ts @@ -1,4 +1,5 @@ import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency" +import type { PointType } from "@scandic-hotels/common/constants/pointType" import type { ChildBedTypeEnum } from "@scandic-hotels/trpc/enums/childBedTypeEnum" import type { BookingConfirmation, @@ -32,6 +33,7 @@ export interface Room { refId: string roomFeatures?: PackageSchema[] | null roomPoints: number + pointsType: PointType | null roomPrice: number roomTypeCode: string | null toDate: string diff --git a/packages/common/constants/currency.ts b/packages/common/constants/currency.ts index cd7ebe87e..5117dbaf4 100644 --- a/packages/common/constants/currency.ts +++ b/packages/common/constants/currency.ts @@ -6,9 +6,8 @@ export enum CurrencyEnum { NOK = "NOK", PLN = "PLN", SEK = "SEK", - POINTS = "Points", Voucher = "Voucher", CC = "CC", Unknown = "Unknown", - EUROBONUS = "EB Points", + POINTS = "Points", } diff --git a/packages/common/constants/pointType.ts b/packages/common/constants/pointType.ts new file mode 100644 index 000000000..fed39c606 --- /dev/null +++ b/packages/common/constants/pointType.ts @@ -0,0 +1,7 @@ +export const PointType = { + SCANDIC: "Scandic", + EUROBONUS: "EuroBonus", +} as const +export const pointTypes = Object.values(PointType) + +export type PointType = (typeof PointType)[keyof typeof PointType] diff --git a/packages/common/package.json b/packages/common/package.json index 4b30128c1..d6b77d206 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -25,6 +25,7 @@ "./constants/membershipLevels": "./constants/membershipLevels.ts", "./constants/paymentCallbackStatusEnum": "./constants/paymentCallbackStatusEnum.ts", "./constants/paymentMethod": "./constants/paymentMethod.ts", + "./constants/pointType": "./constants/pointType.ts", "./constants/rate": "./constants/rate.ts", "./constants/rateType": "./constants/rateType.ts", "./constants/routes/*": "./constants/routes/*.ts", diff --git a/packages/common/utils/numberFormatting.test.ts b/packages/common/utils/numberFormatting.test.ts new file mode 100644 index 000000000..3da7106c2 --- /dev/null +++ b/packages/common/utils/numberFormatting.test.ts @@ -0,0 +1,200 @@ +import { createIntl, createIntlCache } from "react-intl" +import { describe, expect, it } from "vitest" + +import { CurrencyEnum } from "../constants/currency" +import { PointType } from "../constants/pointType" +import { formatPrice } from "./numberFormatting" + +const cache = createIntlCache() + +const createTestIntl = (locale: string = "en-US") => + createIntl({ locale, messages: {}, onError: () => {} }, cache) + +describe("formatPrice", () => { + it("should format price with currency", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 1000, CurrencyEnum.SEK) + + expect(result).toBe("1,000 SEK") + }) + + it("should format price with decimal values", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 99.5, CurrencyEnum.EUR) + + expect(result).toBe("99.5 EUR") + }) + + it("should format price with additional price and currency", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 500, CurrencyEnum.NOK, 100, "SEK") + + expect(result).toBe("500 NOK + 100 SEK") + }) + + it("should not include additional price when only additionalPrice is provided without currency", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 500, CurrencyEnum.DKK, 100, undefined) + + expect(result).toBe("500 DKK") + }) + + it("should not include additional price when only additionalPriceCurrency is provided without amount", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 500, CurrencyEnum.PLN, undefined, "SEK") + + expect(result).toBe("500 PLN") + }) + + it("should format Voucher currency with plural form", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 2, CurrencyEnum.Voucher) + + expect(result).toBe("2 Vouchers") + }) + + it("should format single Voucher correctly", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 1, CurrencyEnum.Voucher) + + expect(result).toBe("1 Voucher") + }) + + it("should handle string currency codes", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 250, "USD") + + expect(result).toBe("250 USD") + }) + + it("should handle zero price", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 0, CurrencyEnum.SEK) + + expect(result).toBe("0 SEK") + }) + + it("should handle large numbers", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 1000000, CurrencyEnum.EUR) + + expect(result).toBe("1,000,000 EUR") + }) + + it("should format numbers according to locale", () => { + const intl = createTestIntl("sv-SE") + + const result = formatPrice(intl, 1000, CurrencyEnum.SEK) + + // Swedish locale uses non-breaking space as thousands separator + expect(result).toBe("1\u00a0000 SEK") + }) + + it("should format POINTS currency without pointType", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice(intl, 5000, CurrencyEnum.POINTS) + + expect(result).toBe("5,000 Points") + }) + + it("should format POINTS currency with Scandic pointType and plural", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice( + intl, + 5000, + CurrencyEnum.POINTS, + undefined, + undefined, + PointType.SCANDIC + ) + + expect(result).toBe("5,000 Points") + }) + + it("should format POINTS currency with Scandic pointType and singular", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice( + intl, + 1, + CurrencyEnum.POINTS, + undefined, + undefined, + PointType.SCANDIC + ) + + expect(result).toBe("1 Point") + }) + + it("should format POINTS currency with EuroBonus pointType and plural", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice( + intl, + 10000, + CurrencyEnum.POINTS, + undefined, + undefined, + PointType.EUROBONUS + ) + + expect(result).toBe("10,000 EB Points") + }) + + it("should format POINTS currency with EuroBonus pointType and singular", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice( + intl, + 1, + CurrencyEnum.POINTS, + undefined, + undefined, + PointType.EUROBONUS + ) + + expect(result).toBe("1 EB Point") + }) + + it("should format POINTS with additional price and Scandic pointType", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice( + intl, + 5000, + CurrencyEnum.POINTS, + 100, + "SEK", + PointType.SCANDIC + ) + + expect(result).toBe("5,000 Points + 100 SEK") + }) + + it("should format POINTS with additional price and EuroBonus pointType", () => { + const intl = createTestIntl("en-US") + + const result = formatPrice( + intl, + 5000, + CurrencyEnum.POINTS, + 100, + "SEK", + PointType.EUROBONUS + ) + + expect(result).toBe("5,000 EB Points + 100 SEK") + }) +}) diff --git a/packages/common/utils/numberFormatting.ts b/packages/common/utils/numberFormatting.ts index 798591223..dfef99759 100644 --- a/packages/common/utils/numberFormatting.ts +++ b/packages/common/utils/numberFormatting.ts @@ -1,4 +1,6 @@ import { CurrencyEnum } from "../constants/currency" +import { PointType } from "../constants/pointType" +import { logger } from "../logger" import type { IntlShape } from "react-intl" @@ -18,6 +20,7 @@ export function getSingleDecimal(n: Number | string) { * @param currency - currency code * @param additionalPrice - number (obtained in reward nights and Corporate cheque scenarios) * @param additionalPriceCurrency - currency code (obtained in reward nights and Corporate cheque scenarios) + * @param pointsType - type of points when currency is points * @returns localized and formatted number in string type with currency */ export function formatPrice( @@ -25,7 +28,8 @@ export function formatPrice( price: number, currency: string | CurrencyEnum, additionalPrice?: number, - additionalPriceCurrency?: string + additionalPriceCurrency?: string, + pointsType?: PointType | null ) { const localizedPrice = intl.formatNumber(price, { minimumFractionDigits: 0, @@ -41,19 +45,66 @@ export function formatPrice( formattedAdditionalPrice = ` + ${localizedAdditionalPrice} ${additionalPriceCurrency}` } - const currencyText = - currency === CurrencyEnum.Voucher - ? intl.formatMessage( - { - id: "price.numberOfVouchers", - defaultMessage: - "{numberOfVouchers, plural, one {Voucher} other {Vouchers}}", - }, - { - numberOfVouchers: price, - } - ) - : currency + const currencyText = getCurrencyText(intl, price, currency, pointsType) return `${localizedPrice} ${currencyText}${formattedAdditionalPrice}` } + +function getCurrencyText( + intl: IntlShape, + price: number, + currency: string | CurrencyEnum, + pointsType?: PointType | null +) { + if (currency === CurrencyEnum.Voucher) { + return intl.formatMessage( + { + id: "price.numberOfVouchers", + defaultMessage: + "{numberOfVouchers, plural, one {Voucher} other {Vouchers}}", + }, + { + numberOfVouchers: price, + } + ) + } + + if (currency === CurrencyEnum.POINTS) { + 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 + } + } + } + + return currency +} diff --git a/packages/design-system/eslint.config.mjs b/packages/design-system/eslint.config.mjs index d192a2038..9c871044e 100644 --- a/packages/design-system/eslint.config.mjs +++ b/packages/design-system/eslint.config.mjs @@ -51,6 +51,14 @@ export default defineConfig([ }, }, rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], "import/no-relative-packages": "error", "import/no-extraneous-dependencies": [ "error", diff --git a/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx b/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx index 320af8c17..5965069ad 100644 --- a/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx +++ b/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx @@ -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 ? ( ) : null} diff --git a/packages/design-system/lib/components/HotelCard/HotelPointsRow/index.tsx b/packages/design-system/lib/components/HotelCard/HotelPointsRow/index.tsx index cd3e234fc..bfc0039b8 100644 --- a/packages/design-system/lib/components/HotelCard/HotelPointsRow/index.tsx +++ b/packages/design-system/lib/components/HotelCard/HotelPointsRow/index.tsx @@ -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 ( {additionalPricePerStay ? ( diff --git a/packages/design-system/lib/components/HotelCard/index.tsx b/packages/design-system/lib/components/HotelCard/index.tsx index 0edc8cd29..edc8ed792 100644 --- a/packages/design-system/lib/components/HotelCard/index.tsx +++ b/packages/design-system/lib/components/HotelCard/index.tsx @@ -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} /> ))} diff --git a/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/HotelPin/index.tsx b/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/HotelPin/index.tsx index 49bc1f2ff..ceb64ac70 100644 --- a/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/HotelPin/index.tsx +++ b/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/HotelPin/index.tsx @@ -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 )}

diff --git a/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/index.tsx b/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/index.tsx index 55c390dfe..379d51a33 100644 --- a/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/index.tsx +++ b/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/index.tsx @@ -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} /> )} diff --git a/packages/design-system/lib/components/Map/InteractiveMap/index.tsx b/packages/design-system/lib/components/Map/InteractiveMap/index.tsx index 14aee0970..1664142e3 100644 --- a/packages/design-system/lib/components/Map/InteractiveMap/index.tsx +++ b/packages/design-system/lib/components/Map/InteractiveMap/index.tsx @@ -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 && ( diff --git a/packages/design-system/lib/components/Map/InteractiveMap/storybookData.ts b/packages/design-system/lib/components/Map/InteractiveMap/storybookData.ts index 49dd430dc..29c1e03bf 100644 --- a/packages/design-system/lib/components/Map/InteractiveMap/storybookData.ts +++ b/packages/design-system/lib/components/Map/InteractiveMap/storybookData.ts @@ -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", diff --git a/packages/design-system/lib/components/Map/types.ts b/packages/design-system/lib/components/Map/types.ts index ba53e877c..dc34f5fdc 100644 --- a/packages/design-system/lib/components/Map/types.ts +++ b/packages/design-system/lib/components/Map/types.ts @@ -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 diff --git a/packages/design-system/lib/components/RateCard/Points/Points.stories.tsx b/packages/design-system/lib/components/RateCard/Points/Points.stories.tsx index 9ada1641a..a54e762e4 100644 --- a/packages/design-system/lib/components/RateCard/Points/Points.stories.tsx +++ b/packages/design-system/lib/components/RateCard/Points/Points.stories.tsx @@ -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 = { 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"], + }, + ], + }, +} diff --git a/packages/design-system/lib/components/RateCard/Points/index.tsx b/packages/design-system/lib/components/RateCard/Points/index.tsx index 2ca25d94f..b3dbdf86a 100644 --- a/packages/design-system/lib/components/RateCard/Points/index.tsx +++ b/packages/design-system/lib/components/RateCard/Points/index.tsx @@ -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({ {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${rate.currency} ${rate.additionalPrice ? " + " : ""}`} + {`${getCurrencyText(intl, rate.currency, rate.points, rate.pointsType)} ${rate.additionalPrice ? " + " : ""}`}

diff --git a/packages/design-system/lib/components/RateCard/types.ts b/packages/design-system/lib/components/RateCard/types.ts index ba1a51938..c26198bc1 100644 --- a/packages/design-system/lib/components/RateCard/types.ts +++ b/packages/design-system/lib/components/RateCard/types.ts @@ -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 diff --git a/packages/design-system/lib/components/currency-utils.ts b/packages/design-system/lib/components/currency-utils.ts new file mode 100644 index 000000000..daaea464a --- /dev/null +++ b/packages/design-system/lib/components/currency-utils.ts @@ -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 + } + } +} diff --git a/packages/trpc/lib/routers/booking/output.ts b/packages/trpc/lib/routers/booking/output.ts index ec3ff9ff8..ef8674fc1 100644 --- a/packages/trpc/lib/routers/booking/output.ts +++ b/packages/trpc/lib/routers/booking/output.ts @@ -176,7 +176,11 @@ export const bookingConfirmationSchema = z rateDefinition: rateDefinitionSchema, reservationStatus: z.string().nullable().default(""), roomPoints: z.number(), - roomPointType: z.nullable(z.enum(["Scandic", "EuroBonus"])).catch(null), + roomPointType: z + .enum(["Scandic", "EuroBonus"]) + .nullable() + .default(null) + .catch(null), roomPrice: z.number(), roomTypeCode: z.string().default(""), totalPoints: z.number(), diff --git a/packages/trpc/lib/routers/hotels/schemas/productTypePrice.ts b/packages/trpc/lib/routers/hotels/schemas/productTypePrice.ts index 1cd390969..44ea767e1 100644 --- a/packages/trpc/lib/routers/hotels/schemas/productTypePrice.ts +++ b/packages/trpc/lib/routers/hotels/schemas/productTypePrice.ts @@ -1,6 +1,10 @@ import { z } from "zod" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" +import { + PointType, + pointTypes, +} from "@scandic-hotels/common/constants/pointType" import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType" import { nullableNumberValidator } from "@scandic-hotels/common/utils/zod/numberValidator" import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator" @@ -22,6 +26,10 @@ export const redemptionSchema = z.object({ currency: z.nativeEnum(CurrencyEnum).nullish(), pointsPerNight: nullableNumberValidator, pointsPerStay: nullableNumberValidator, + pointsType: z + .enum(pointTypes as [PointType, ...PointType[]]) + .nullish() + .catch(PointType.SCANDIC), }) export const priceSchema = z.object({ diff --git a/packages/trpc/package.json b/packages/trpc/package.json index e81b56856..3be1df627 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -79,6 +79,7 @@ "@types/react": "19.2.7", "@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/parser": "^8.32.0", + "@typescript/native-preview": "^7.0.0-dev.20251104.1", "dotenv": "^16.5.0", "eslint": "^9", "eslint-plugin-import": "^2.31.0", diff --git a/yarn.lock b/yarn.lock index 03b223aa1..27eff744c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5363,6 +5363,7 @@ __metadata: "@types/react": "npm:19.2.7" "@typescript-eslint/eslint-plugin": "npm:^8.32.0" "@typescript-eslint/parser": "npm:^8.32.0" + "@typescript/native-preview": "npm:^7.0.0-dev.20251104.1" dayjs: "npm:^1.11.13" deepmerge: "npm:^4.3.1" dotenv: "npm:^16.5.0"