diff --git a/packages/booking-flow/lib/bookingFlowConfig/bookingFlowConfigContext.tsx b/packages/booking-flow/lib/bookingFlowConfig/bookingFlowConfigContext.tsx index bf8d2051f..133d716d1 100644 --- a/packages/booking-flow/lib/bookingFlowConfig/bookingFlowConfigContext.tsx +++ b/packages/booking-flow/lib/bookingFlowConfig/bookingFlowConfigContext.tsx @@ -2,8 +2,9 @@ import { createContext, useContext } from "react" +import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" + import type { BookingFlowConfig } from "./bookingFlowConfig" -import type { BookingFlowVariant } from "./bookingFlowVariants" type BookingFlowConfigContextData = BookingFlowConfig @@ -11,18 +12,6 @@ const BookingFlowConfigContext = createContext< BookingFlowConfigContextData | undefined >(undefined) -export const useIsPartner = (variant: BookingFlowVariant) => { - const context = useContext(BookingFlowConfigContext) - - if (!context) { - throw new Error( - "useBookingFlowConfig must be used within a BookingFlowConfigContextProvider. Did you forget to use BookingFlowConfig in the consuming app?" - ) - } - - return context.variant === variant -} - export const useBookingFlowConfig = (): BookingFlowConfigContextData => { const context = useContext(BookingFlowConfigContext) @@ -35,6 +24,19 @@ 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: + throw new Error(`Unknown variant: ${config.variant}`) + } +} + export function BookingFlowConfigContextProvider({ children, config, diff --git a/packages/booking-flow/lib/components/HotelCardListing/index.tsx b/packages/booking-flow/lib/components/HotelCardListing/index.tsx index 951a3db11..6c153226d 100644 --- a/packages/booking-flow/lib/components/HotelCardListing/index.tsx +++ b/packages/booking-flow/lib/components/HotelCardListing/index.tsx @@ -12,6 +12,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 { useGetPointsCurrency } from "../../bookingFlowConfig/bookingFlowConfigContext" import { useIsLoggedIn } from "../../hooks/useIsLoggedIn" import useLang from "../../hooks/useLang" import { mapApiImagesToGalleryImages } from "../../misc/imageGallery" @@ -57,6 +58,7 @@ export default function HotelCardListing({ const { activeHotel, activate, disengage, engage } = useHotelsMapStore() const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 }) const activeCardRef = useRef(null) + const pointsCurrency = useGetPointsCurrency() const sortBy = searchParams.get("sort") ?? DEFAULT_SORT @@ -159,6 +161,7 @@ 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 eec3d9f6c..97ec94329 100644 --- a/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx +++ b/packages/booking-flow/lib/components/ListingHotelCardDialog/index.tsx @@ -16,6 +16,7 @@ import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton import Subtitle from "@scandic-hotels/design-system/Subtitle" import { Typography } from "@scandic-hotels/design-system/Typography" +import { useGetPointsCurrency } from "../../bookingFlowConfig/bookingFlowConfigContext" import { useIsLoggedIn } from "../../hooks/useIsLoggedIn" import useLang from "../../hooks/useLang" @@ -34,6 +35,7 @@ export default function ListingHotelCardDialog({ }: ListingHotelCardProps) { const intl = useIntl() const lang = useLang() + const pointsCurrency = useGetPointsCurrency() const [imageError, setImageError] = useState(false) @@ -153,7 +155,10 @@ export default function ListingHotelCardDialog({ )} {redemptionPrice && ( - + )} {chequePrice && ( 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 9604dbe12..a77c779c3 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,14 +1,16 @@ "use client" import { useIntl } from "react-intl" -import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" 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 { @@ -34,6 +36,7 @@ export default function RedemptionPrice({ price, }: RedemptionPriceProps) { const intl = useIntl() + const pointsCurrency = useGetPointsCurrency() if (!price) { return null @@ -50,7 +53,7 @@ export default function RedemptionPrice({ : null const additionalCurrency = price.currency ?? currency - let averagePricePerNight = `${price.pointsPerNight} ${CurrencyEnum.POINTS}` + let averagePricePerNight = `${price.pointsPerNight} ${pointsCurrency}` if (averageAdditionalPricePerNight) { averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}` } @@ -62,7 +65,7 @@ export default function RedemptionPrice({ value={formatPrice( intl, price.pointsPerStay, - CurrencyEnum.POINTS, + pointsCurrency, additionalPricePerStay, additionalCurrency )} 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 86de56e39..12219b8ec 100644 --- a/packages/booking-flow/lib/components/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx +++ b/packages/booking-flow/lib/components/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx @@ -19,6 +19,7 @@ import { InteractiveMap } from "@scandic-hotels/design-system/Map/InteractiveMap 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" @@ -76,6 +77,7 @@ export function SelectHotelMapContent({ const activeFilters = useHotelFilterStore((state) => state.activeFilters) const setResultCount = useHotelFilterStore((state) => state.setResultCount) + const pointsCurrency = useGetPointsCurrency() const hotelMapStore = useHotelsMapStore() @@ -254,6 +256,7 @@ export function SelectHotelMapContent({ )} { 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 7723a08cf..dabf6fb04 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 @@ -3,6 +3,7 @@ import { useIntl } from "react-intl" 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" @@ -33,6 +34,7 @@ export default function Redemptions({ actions: { selectRate }, selectedRates, } = useSelectRateContext() + const pointsCurrency = useGetPointsCurrency() // TODO: Replace with context value when we have support for dropdown "Show all rates" const selectedFilter = BookingCodeFilterEnum.All as BookingCodeFilterEnum @@ -86,7 +88,7 @@ export default function Redemptions({ price: additionalPrice.toString(), } : undefined, - currency: "PTS", + currency: pointsCurrency ?? "PTS", isDisabled: !r.redemption.hasEnoughPoints, points: r.redemption.localPrice.pointsPerStay.toString(), 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 62d5d87bf..a7e16b3ed 100644 --- a/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx +++ b/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx @@ -6,6 +6,7 @@ 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 { @@ -60,6 +61,7 @@ export default function EnterDetailsProvider({ // rendering the form until that has been done. const [hasInitializedStore, setHasInitializedStore] = useState(false) const storeRef = useRef(undefined) + const pointsCurrency = useGetPointsCurrency() if (!storeRef.current) { const initialData: InitialState = { booking, @@ -99,7 +101,8 @@ export default function EnterDetailsProvider({ searchParamsStr, user, breakfastPackages, - lang + lang, + pointsCurrency ) } diff --git a/packages/booking-flow/lib/contexts/SelectRate/SelectRateContext/index.tsx b/packages/booking-flow/lib/contexts/SelectRate/SelectRateContext/index.tsx index 8846e6e3a..b192878b4 100644 --- a/packages/booking-flow/lib/contexts/SelectRate/SelectRateContext/index.tsx +++ b/packages/booking-flow/lib/contexts/SelectRate/SelectRateContext/index.tsx @@ -18,6 +18,7 @@ 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" @@ -68,6 +69,7 @@ export function SelectRateProvider({ const updateBooking = useUpdateBooking() const isUserLoggedIn = useIsLoggedIn() const intl = useIntl() + const pointsCurrency = useGetPointsCurrency() const [activeRoomIndex, setInternalActiveRoomIndex] = useQueryState( "activeRoomIndex", @@ -233,6 +235,7 @@ export function SelectRateProvider({ roomConfiguration: roomAvailability[ix]?.[0], })), isMember: isUserLoggedIn, + pointsCurrency, }) const getPriceForRoom = useCallback( @@ -253,9 +256,10 @@ export function SelectRateProvider({ ], isMember: isUserLoggedIn && roomIndex === 0, addAdditionalCost: false, + pointsCurrency, }) }, - [selectedRates, roomAvailability, isUserLoggedIn] + [selectedRates, roomAvailability, isUserLoggedIn, pointsCurrency] ) const setActiveRoomIndex = useCallback( diff --git a/packages/booking-flow/lib/contexts/SelectRate/getTotalPrice.ts b/packages/booking-flow/lib/contexts/SelectRate/getTotalPrice.ts index d4f256189..4a488ed4b 100644 --- a/packages/booking-flow/lib/contexts/SelectRate/getTotalPrice.ts +++ b/packages/booking-flow/lib/contexts/SelectRate/getTotalPrice.ts @@ -17,10 +17,12 @@ 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 @@ -45,7 +47,8 @@ export function getTotalPrice({ mainRoom.roomConfiguration?.selectedPackages.filter( (pkg) => "localPrice" in pkg ) ?? null, - addAdditionalCost + addAdditionalCost, + pointsCurrency ) } if ("voucher" in mainRoomRate) { @@ -156,7 +159,8 @@ function calculateTotalPrice( function calculateRedemptionTotalPrice( redemption: RedemptionProduct["redemption"], packages: RoomPackage[] | null, - addAdditonalCost: boolean + addAdditonalCost: boolean, + pointsCurrency?: CurrencyEnum ) { const pkgsSum = addAdditonalCost ? sumPackages(packages) @@ -179,7 +183,7 @@ function calculateRedemptionTotalPrice( local: { additionalPrice, additionalPriceCurrency, - currency: CurrencyEnum.POINTS, + currency: pointsCurrency ?? CurrencyEnum.POINTS, price: redemption.localPrice.pointsPerStay, }, } diff --git a/packages/booking-flow/lib/stores/enter-details/helpers.ts b/packages/booking-flow/lib/stores/enter-details/helpers.ts index 4c331ba8a..688a97221 100644 --- a/packages/booking-flow/lib/stores/enter-details/helpers.ts +++ b/packages/booking-flow/lib/stores/enter-details/helpers.ts @@ -52,7 +52,11 @@ function add(...nums: (number | string | undefined)[]) { }, 0) } -export function getRoomPrice(roomRate: Product, isMember: boolean) { +export function getRoomPrice( + roomRate: Product, + isMember: boolean, + pointsCurrency?: CurrencyEnum +) { if (isMember && "member" in roomRate && roomRate.member) { let publicRate if ( @@ -196,7 +200,7 @@ export function getRoomPrice(roomRate: Product, isMember: boolean) { perNight: { requested: undefined, local: { - currency: CurrencyEnum.POINTS, + currency: pointsCurrency ?? CurrencyEnum.POINTS, price: roomRate.redemption.localPrice.pointsPerStay, additionalPrice: roomRate.redemption.localPrice.additionalPricePerStay, @@ -207,7 +211,7 @@ export function getRoomPrice(roomRate: Product, isMember: boolean) { perStay: { requested: undefined, local: { - currency: CurrencyEnum.POINTS, + currency: pointsCurrency ?? CurrencyEnum.POINTS, price: roomRate.redemption.localPrice.pointsPerStay, additionalPrice: roomRate.redemption.localPrice.additionalPricePerStay, @@ -440,7 +444,11 @@ interface TRoomRedemption extends TRoom { roomRate: RedemptionProduct } -function getRedemptionPrice(rooms: TRoom[], nights: number) { +function getRedemptionPrice( + rooms: TRoom[], + nights: number, + pointsCurrency?: CurrencyEnum +) { return rooms .filter((room): room is TRoomRedemption => "redemption" in room.roomRate) .reduce( @@ -466,7 +474,7 @@ function getRedemptionPrice(rooms: TRoom[], nights: number) { }, { local: { - currency: CurrencyEnum.POINTS, + currency: pointsCurrency ?? CurrencyEnum.POINTS, price: 0, }, requested: undefined, @@ -575,7 +583,8 @@ function getRegularPrice(rooms: TRoom[], isMember: boolean, nights: number) { export function getTotalPrice( rooms: TRoom[], isMember: boolean, - nights: number + nights: number, + pointsCurrency?: CurrencyEnum ) { const hasCorpChqRates = rooms.some( (room) => "corporateCheque" in room.roomRate @@ -586,7 +595,7 @@ export function getTotalPrice( const hasRedemptionRates = rooms.some((room) => "redemption" in room.roomRate) if (hasRedemptionRates) { - return getRedemptionPrice(rooms, nights) + return getRedemptionPrice(rooms, nights, pointsCurrency) } const hasVoucherRates = rooms.some((room) => "voucher" in room.roomRate) diff --git a/packages/booking-flow/lib/stores/enter-details/index.ts b/packages/booking-flow/lib/stores/enter-details/index.ts index 9da99b664..d47ce052d 100644 --- a/packages/booking-flow/lib/stores/enter-details/index.ts +++ b/packages/booking-flow/lib/stores/enter-details/index.ts @@ -16,6 +16,7 @@ import { writeToSessionStorage, } from "./helpers" +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" @@ -43,7 +44,8 @@ export function createDetailsStore( searchParams: string, user: User | null, breakfastPackages: BreakfastPackages, - lang: Lang + lang: Lang, + pointsCurrency?: CurrencyEnum ) { const isMember = !!user const nights = dt(initialState.booking.toDate).diff( @@ -68,14 +70,23 @@ export function createDetailsStore( ...defaultGuestState, phoneNumberCC: getDefaultCountryFromLang(lang), }, - roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0), + roomPrice: getRoomPrice( + room.roomRate, + isMember && idx === 0, + pointsCurrency + ), specialRequest: { comment: "", }, } }) - const initialTotalPrice = getTotalPrice(initialRooms, isMember, nights) + const initialTotalPrice = getTotalPrice( + initialRooms, + isMember, + nights, + pointsCurrency + ) const availableBeds = initialState.rooms.reduce< DetailsState["availableBeds"] @@ -175,7 +186,8 @@ export function createDetailsStore( state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, - nights + nights, + pointsCurrency ) const isAllStepsCompleted = checkRoomProgress( @@ -206,7 +218,8 @@ export function createDetailsStore( } currentRoom.roomPrice = getRoomPrice( currentRoom.roomRate, - isValidMembershipNo || currentRoom.guest.join + isValidMembershipNo || currentRoom.guest.join, + pointsCurrency ) const nights = dt(state.booking.toDate).diff( @@ -217,7 +230,8 @@ export function createDetailsStore( state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, - nights + nights, + pointsCurrency ) writeToSessionStorage({ @@ -240,7 +254,8 @@ export function createDetailsStore( currentRoom.roomPrice = getRoomPrice( currentRoom.roomRate, - join || !!currentRoom.guest.membershipNo + join || !!currentRoom.guest.membershipNo, + pointsCurrency ) const nights = dt(state.booking.toDate).diff( @@ -251,7 +266,8 @@ export function createDetailsStore( state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, - nights + nights, + pointsCurrency ) writeToSessionStorage({ @@ -316,7 +332,8 @@ export function createDetailsStore( currentRoom.roomPrice = getRoomPrice( currentRoom.roomRate, - Boolean(data.join || data.membershipNo || isMemberAndRoomOne) + Boolean(data.join || data.membershipNo || isMemberAndRoomOne), + pointsCurrency ) const nights = dt(state.booking.toDate).diff( @@ -327,7 +344,8 @@ export function createDetailsStore( state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, - nights + nights, + pointsCurrency ) const isAllStepsCompleted = checkRoomProgress( diff --git a/packages/common/constants/currency.ts b/packages/common/constants/currency.ts index a370f39be..cd7ebe87e 100644 --- a/packages/common/constants/currency.ts +++ b/packages/common/constants/currency.ts @@ -10,4 +10,5 @@ export enum CurrencyEnum { Voucher = "Voucher", CC = "CC", Unknown = "Unknown", + EUROBONUS = "EB Points", } 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 48fad03d8..35c88eb5c 100644 --- a/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx +++ b/packages/design-system/lib/components/HotelCard/HotelDialogCard/StandaloneHotelCardDialog/index.tsx @@ -22,6 +22,7 @@ import { FacilityToIcon } from '../../../FacilityToIcon' import { HotelPin } from '../../../Map/types' import { HotelPointsRow } from '../../HotelPointsRow' import styles from './standaloneHotelCardDialog.module.css' +import { CurrencyEnum } from '@scandic-hotels/common/constants/currency' interface StandaloneHotelCardProps { data: HotelPin @@ -29,6 +30,7 @@ interface StandaloneHotelCardProps { isUserLoggedIn: boolean handleClose: () => void onClick?: () => void + pointsCurrency?: CurrencyEnum } export function StandaloneHotelCardDialog({ @@ -37,6 +39,7 @@ export function StandaloneHotelCardDialog({ handleClose, isUserLoggedIn, onClick, + pointsCurrency, }: StandaloneHotelCardProps) { const intl = useIntl() const [imageError, setImageError] = useState(false) @@ -224,7 +227,10 @@ export function StandaloneHotelCardDialog({ )} {redemptionPrice && ( - + )} {shouldShowNotEnoughPoints ? ( diff --git a/packages/design-system/lib/components/HotelCard/HotelPointsRow/index.tsx b/packages/design-system/lib/components/HotelCard/HotelPointsRow/index.tsx index 1514fb59d..22bdea6ae 100644 --- a/packages/design-system/lib/components/HotelCard/HotelPointsRow/index.tsx +++ b/packages/design-system/lib/components/HotelCard/HotelPointsRow/index.tsx @@ -4,16 +4,19 @@ import Caption from '../../Caption' import Subtitle from '../../Subtitle' import styles from './hotelPointsRow.module.css' +import { CurrencyEnum } from '@scandic-hotels/common/constants/currency' export type PointsRowProps = { pointsPerStay: number additionalPricePerStay?: number additionalPriceCurrency?: string + pointsCurrency?: CurrencyEnum } export function HotelPointsRow({ pointsPerStay, additionalPricePerStay, additionalPriceCurrency, + pointsCurrency, }: PointsRowProps) { const intl = useIntl() @@ -23,9 +26,10 @@ export function HotelPointsRow({ {pointsPerStay} - {intl.formatMessage({ - defaultMessage: 'Points', - })} + {pointsCurrency ?? + intl.formatMessage({ + defaultMessage: 'Points', + })} {additionalPricePerStay ? ( <> diff --git a/packages/design-system/lib/components/HotelCard/index.tsx b/packages/design-system/lib/components/HotelCard/index.tsx index 5e9430a7a..f58c3c267 100644 --- a/packages/design-system/lib/components/HotelCard/index.tsx +++ b/packages/design-system/lib/components/HotelCard/index.tsx @@ -106,6 +106,7 @@ export type HotelCardProps = { state?: 'default' | 'active' bookingCode?: string | null isAlternative?: boolean + pointsCurrency?: CurrencyEnum fullPrice: boolean lang: Lang @@ -127,6 +128,7 @@ export const HotelCard = memo( type = 'pageListing', bookingCode = '', isAlternative, + pointsCurrency, images, lang, belowInfoSlot, @@ -307,6 +309,7 @@ export const HotelCard = memo( additionalPriceCurrency={ redemption.localPrice.currency ?? undefined } + pointsCurrency={pointsCurrency} /> ))} 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 f0c9d1a8f..882af713e 100644 --- a/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/index.tsx +++ b/packages/design-system/lib/components/Map/InteractiveMap/HotelListingMapContent/index.tsx @@ -10,6 +10,7 @@ import type { HotelPin as HotelPinType } from '../../types' import styles from './hotelListingMapContent.module.css' import { StandaloneHotelCardDialog } from '../../../HotelCard/HotelDialogCard/StandaloneHotelCardDialog' import { Lang } from '@scandic-hotels/common/constants/language' +import { CurrencyEnum } from '@scandic-hotels/common/constants/currency' export type HotelListingMapContentProps = { hotelPins: HotelPinType[] @@ -17,6 +18,7 @@ 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?: ( @@ -32,6 +34,7 @@ export function HotelListingMapContent({ setHoveredHotel, lang, onClickHotel, + pointsCurrency, }: HotelListingMapContentProps) { const isDesktop = useMediaQuery('(min-width: 900px)') @@ -104,6 +107,7 @@ 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 0f2bad7bf..1b15a7f11 100644 --- a/packages/design-system/lib/components/Map/InteractiveMap/index.tsx +++ b/packages/design-system/lib/components/Map/InteractiveMap/index.tsx @@ -18,6 +18,7 @@ import styles from './interactiveMap.module.css' import { HotelPin, MarkerInfo, PointOfInterest } from '../types' import { Lang } from '@scandic-hotels/common/constants/language' +import { CurrencyEnum } from '@scandic-hotels/common/constants/currency' export type InteractiveMapProps = { lang: Lang @@ -27,6 +28,7 @@ export type InteractiveMapProps = { } activePoi?: string | null hotelPins?: HotelPin[] + pointsCurrency?: CurrencyEnum pointsOfInterest?: PointOfInterest[] markerInfo?: MarkerInfo mapId: string @@ -73,6 +75,7 @@ export function InteractiveMap({ hoveredHotelPin, activeHotelPin, isUserLoggedIn, + pointsCurrency, onClickHotel, onHoverHotelPin, onSetActiveHotelPin, @@ -122,6 +125,7 @@ export function InteractiveMap({ activeHotel={activeHotelPin} hoveredHotel={hoveredHotelPin} onClickHotel={onClickHotel} + pointsCurrency={pointsCurrency} /> )} {pointsOfInterest && markerInfo && ( diff --git a/packages/trpc/lib/routers/hotels/availability/enterDetails.ts b/packages/trpc/lib/routers/hotels/availability/enterDetails.ts index 83d3b20df..09305bc5e 100644 --- a/packages/trpc/lib/routers/hotels/availability/enterDetails.ts +++ b/packages/trpc/lib/routers/hotels/availability/enterDetails.ts @@ -9,7 +9,8 @@ import { SEARCH_TYPE_REDEMPTION } from "../../../constants/booking" import { AvailabilityEnum } from "../../../enums/selectHotel" import { unauthorizedError } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" -import { getVerifiedUser } from "../../user/utils/getVerifiedUser" +import { getRedemptionTokenSafely } from "../../../utils/getRedemptionTokenSafely" +import { getUserPointsBalance } from "../../../utils/getUserPointsBalance" import { baseBookingSchema, baseRoomSchema, selectedRoomSchema } from "../input" import { getHotel } from "../services/getHotel" import { getRoomsAvailability } from "../services/getRoomsAvailability" @@ -37,13 +38,15 @@ export const enterDetails = safeProtectedServiceProcedure .use(async ({ ctx, input, next }) => { if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) { if (ctx.session?.token.access_token) { - const verifiedUser = await getVerifiedUser({ session: ctx.session }) - if (!verifiedUser?.error) { + const pointsValue = await getUserPointsBalance(ctx.session) + const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) + if (pointsValue && token) { return next({ ctx: { - token: ctx.session.token.access_token, - userPoints: verifiedUser?.data.membership?.currentPoints ?? 0, + token: token, + userPoints: pointsValue ?? 0, }, + input, }) } } diff --git a/packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts b/packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts index ce3634182..750c1803a 100644 --- a/packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts +++ b/packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts @@ -7,7 +7,8 @@ import { env } from "../../../../env/server" import { unauthorizedError } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" import { toApiLang } from "../../../utils" -import { getVerifiedUser } from "../../user/utils/getVerifiedUser" +import { getRedemptionTokenSafely } from "../../../utils/getRedemptionTokenSafely" +import { getUserPointsBalance } from "../../../utils/getUserPointsBalance" import { getHotelsAvailabilityByCity } from "../services/getHotelsAvailabilityByCity" export type HotelsAvailabilityInputSchema = z.output< @@ -68,12 +69,13 @@ export const hotelsByCity = safeProtectedServiceProcedure .use(async ({ ctx, input, next }) => { if (input.redemption) { if (ctx.session?.token.access_token) { - const verifiedUser = await getVerifiedUser({ session: ctx.session }) - if (!verifiedUser?.error) { + const pointsValue = await getUserPointsBalance(ctx.session) + const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) + if (pointsValue && token) { return next({ ctx: { - token: ctx.session.token.access_token, - userPoints: verifiedUser?.data.membership?.currentPoints ?? 0, + token: token, + userPoints: pointsValue ?? 0, }, input, }) diff --git a/packages/trpc/lib/routers/hotels/availability/hotelsByHotelIds.ts b/packages/trpc/lib/routers/hotels/availability/hotelsByHotelIds.ts index b936f7283..a4ef8f319 100644 --- a/packages/trpc/lib/routers/hotels/availability/hotelsByHotelIds.ts +++ b/packages/trpc/lib/routers/hotels/availability/hotelsByHotelIds.ts @@ -4,7 +4,8 @@ import { z } from "zod" import { unauthorizedError } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" import { toApiLang } from "../../../utils" -import { getVerifiedUser } from "../../user/utils/getVerifiedUser" +import { getRedemptionTokenSafely } from "../../../utils/getRedemptionTokenSafely" +import { getUserPointsBalance } from "../../../utils/getUserPointsBalance" import { getHotelsAvailabilityByHotelIds } from "../services/getHotelsAvailabilityByHotelIds" export type HotelsByHotelIdsAvailabilityInputSchema = z.output< @@ -65,12 +66,13 @@ export const hotelsByHotelIds = safeProtectedServiceProcedure .use(async ({ ctx, input, next }) => { if (input.redemption) { if (ctx.session?.token.access_token) { - const verifiedUser = await getVerifiedUser({ session: ctx.session }) - if (!verifiedUser?.error) { + const pointsValue = await getUserPointsBalance(ctx.session) + const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) + if (pointsValue && token) { return next({ ctx: { - token: ctx.session.token.access_token, - userPoints: verifiedUser?.data.membership?.currentPoints ?? 0, + token: token, + userPoints: pointsValue ?? 0, }, input, }) diff --git a/packages/trpc/lib/routers/hotels/availability/myStay.ts b/packages/trpc/lib/routers/hotels/availability/myStay.ts index 53650580d..7f91ebc25 100644 --- a/packages/trpc/lib/routers/hotels/availability/myStay.ts +++ b/packages/trpc/lib/routers/hotels/availability/myStay.ts @@ -6,7 +6,8 @@ import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { SEARCH_TYPE_REDEMPTION } from "../../../constants/booking" import { unauthorizedError } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" -import { getVerifiedUser } from "../../user/utils/getVerifiedUser" +import { getRedemptionTokenSafely } from "../../../utils/getRedemptionTokenSafely" +import { getUserPointsBalance } from "../../../utils/getUserPointsBalance" import { baseBookingSchema, baseRoomSchema, selectedRoomSchema } from "../input" import { getRoomsAvailability } from "../services/getRoomsAvailability" import { getSelectedRoomAvailability } from "../utils" @@ -24,13 +25,15 @@ export const myStay = safeProtectedServiceProcedure .use(async ({ ctx, input, next }) => { if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) { if (ctx.session?.token.access_token) { - const verifiedUser = await getVerifiedUser({ session: ctx.session }) - if (!verifiedUser?.error) { + const pointsValue = await getUserPointsBalance(ctx.session) + const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) + if (pointsValue && token) { return next({ ctx: { - token: ctx.session.token.access_token, - userPoints: verifiedUser?.data.membership?.currentPoints ?? 0, + token: token, + userPoints: pointsValue ?? 0, }, + input, }) } } diff --git a/packages/trpc/lib/routers/hotels/availability/selectRate/room.ts b/packages/trpc/lib/routers/hotels/availability/selectRate/room.ts index 014db59b1..98d43f361 100644 --- a/packages/trpc/lib/routers/hotels/availability/selectRate/room.ts +++ b/packages/trpc/lib/routers/hotels/availability/selectRate/room.ts @@ -5,7 +5,8 @@ import { Lang } from "@scandic-hotels/common/constants/language" import { SEARCH_TYPE_REDEMPTION } from "../../../../constants/booking" import { unauthorizedError } from "../../../../errors" import { safeProtectedServiceProcedure } from "../../../../procedures" -import { getVerifiedUser } from "../../../user/utils/getVerifiedUser" +import { getRedemptionTokenSafely } from "../../../../utils/getRedemptionTokenSafely" +import { getUserPointsBalance } from "../../../../utils/getUserPointsBalance" import { baseBookingSchema, baseRoomSchema } from "../../input" import { getRoomsAvailability } from "../../services/getRoomsAvailability" import { mergeRoomTypes } from "../../utils" @@ -22,15 +23,15 @@ export const room = safeProtectedServiceProcedure .use(async ({ ctx, input, next }) => { if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) { if (ctx.session?.token.access_token) { - const verifiedUser = await getVerifiedUser({ - session: ctx.session, - }) - if (!verifiedUser?.error) { + const pointsValue = await getUserPointsBalance(ctx.session) + const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) + if (pointsValue && token) { return next({ ctx: { - token: ctx.session.token.access_token, - userPoints: verifiedUser?.data.membership?.currentPoints ?? 0, + token: token, + userPoints: pointsValue ?? 0, }, + input, }) } } diff --git a/packages/trpc/lib/routers/hotels/availability/selectRate/rooms/index.ts b/packages/trpc/lib/routers/hotels/availability/selectRate/rooms/index.ts index a02d3485e..fc0dc7c14 100644 --- a/packages/trpc/lib/routers/hotels/availability/selectRate/rooms/index.ts +++ b/packages/trpc/lib/routers/hotels/availability/selectRate/rooms/index.ts @@ -3,7 +3,8 @@ import "server-only" import { SEARCH_TYPE_REDEMPTION } from "../../../../../constants/booking" import { unauthorizedError } from "../../../../../errors" import { safeProtectedServiceProcedure } from "../../../../../procedures" -import { getVerifiedUser } from "../../../../user/utils/getVerifiedUser" +import { getRedemptionTokenSafely } from "../../../../../utils/getRedemptionTokenSafely" +import { getUserPointsBalance } from "../../../../../utils/getUserPointsBalance" import { getRoomsAvailability } from "../../../services/getRoomsAvailability" import { mergeRoomTypes } from "../../../utils" import { selectRateRoomsAvailabilityInputSchema } from "./schema" @@ -13,15 +14,15 @@ export const rooms = safeProtectedServiceProcedure .use(async ({ ctx, input, next }) => { if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) { if (ctx.session?.token.access_token) { - const verifiedUser = await getVerifiedUser({ - session: ctx.session, - }) - if (!verifiedUser?.error) { + const pointsValue = await getUserPointsBalance(ctx.session) + const token = getRedemptionTokenSafely(ctx.session, ctx.serviceToken) + if (pointsValue && token) { return next({ ctx: { - token: ctx.session.token.access_token, - userPoints: verifiedUser?.data.membership?.currentPoints ?? 0, + token: token, + userPoints: pointsValue ?? 0, }, + input, }) } } diff --git a/packages/trpc/lib/routers/partners/sas/getEuroBonusProfile.ts b/packages/trpc/lib/routers/partners/sas/getEuroBonusProfile.ts index 840d84519..b3815af7c 100644 --- a/packages/trpc/lib/routers/partners/sas/getEuroBonusProfile.ts +++ b/packages/trpc/lib/routers/partners/sas/getEuroBonusProfile.ts @@ -5,6 +5,8 @@ import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { env } from "../../../../env/server" import { protectedProcedure } from "../../../procedures" +import type { Session } from "next-auth" + const outputSchema = z.object({ eurobonusNumber: z.string(), firstName: z.string().optional(), @@ -46,32 +48,67 @@ const outputSchema = z.object({ const sasLogger = createLogger("SAS") const url = new URL("/api/scandic-partnership/v1/profile", env.SAS_API_ENDPOINT) -export const getEuroBonusProfile = protectedProcedure - .output(outputSchema) - .query(async function ({ ctx }) { - if (ctx.session.token.loginType !== "sas") { - throw new Error( - `Failed to fetch EuroBonus profile, expected loginType to be "sas" but was ${ctx.session.token.loginType}` - ) - } - - const response = await fetch(url, { - headers: { - "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": env.SAS_OCP_APIM, - Authorization: `Bearer ${ctx.session?.token?.access_token}`, +export async function getEuroBonusProfileData(session: Session) { + if (session.token.loginType !== "sas") { + return { + error: { + message: `Failed to fetch EuroBonus profile, expected loginType to be "sas" but was ${session.token.loginType}`, }, - }) + } as const + } - if (!response.ok) { - sasLogger.error( - `Failed to get EuroBonus profile, status: ${response.status}, statusText: ${response.statusText}` - ) - throw new Error("Failed to fetch EuroBonus profile", { - cause: { status: response.status, statusText: response.statusText }, - }) - } + if (!session.token.expires_at || session.token.expires_at < Date.now()) { + return { + error: { + message: "Token expired sas", + }, + } as const + } - const data = await response.json() - return data + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + "Ocp-Apim-Subscription-Key": env.SAS_OCP_APIM, + Authorization: `Bearer ${session?.token?.access_token}`, + }, }) + + if (!response.ok) { + sasLogger.error( + `Failed to get EuroBonus profile, status: ${response.status}, statusText: ${response.statusText}` + ) + return { + error: { + message: "Failed to fetch EuroBonus profile", + cause: { status: response.status, statusText: response.statusText }, + }, + } as const + } + + const responseJson = await response.json() + const data = outputSchema.safeParse(responseJson) + if (!data.success) { + sasLogger.error( + `Failed to parse EuroBonus profile, cause: ${data.error.cause}, message: ${data.error.message}` + ) + return { + error: { + message: `Failed to parse EuroBonus profile: ${data.error.message}`, + cause: { status: response.status, statusText: response.statusText }, + }, + } as const + } + return data +} + +export const getEuroBonusProfile = protectedProcedure.query(async function ({ + ctx, +}) { + const verifiedSasUser = await getEuroBonusProfileData(ctx.session) + if ("error" in verifiedSasUser) { + throw new Error(verifiedSasUser.error?.message, { + cause: verifiedSasUser.error?.cause, + }) + } + return verifiedSasUser.data +}) diff --git a/packages/trpc/lib/routers/user/utils.ts b/packages/trpc/lib/routers/user/utils.ts index 1b45ea9f4..7913e3d21 100644 --- a/packages/trpc/lib/routers/user/utils.ts +++ b/packages/trpc/lib/routers/user/utils.ts @@ -9,7 +9,7 @@ import { creditCardsSchema } from "../../routers/user/output" import { toApiLang } from "../../utils" import { encrypt } from "../../utils/encryption" import { isValidSession } from "../../utils/session" -import { getUserSchema } from "./output" +import { getVerifiedUser } from "./utils/getVerifiedUser" import { type FriendTransaction, getStaysSchema, type Stay } from "./output" import type { Lang } from "@scandic-hotels/common/constants/language" @@ -29,78 +29,6 @@ export async function getMembershipNumber( return verifiedUser.data.membershipNumber } -export const getVerifiedUser = cache( - async ({ - session, - includeExtendedPartnerData, - }: { - session: Session - includeExtendedPartnerData?: boolean - }) => { - const getVerifiedUserCounter = createCounter("user", "getVerifiedUser") - const metricsGetVerifiedUser = getVerifiedUserCounter.init() - - metricsGetVerifiedUser.start() - - const now = Date.now() - if (session.token.expires_at && session.token.expires_at < now) { - metricsGetVerifiedUser.dataError(`Token expired`) - return { error: true, cause: "token_expired" } as const - } - - const apiResponse = await api.get( - api.endpoints.v2.Profile.profile, - { - headers: { - Authorization: `Bearer ${session.token.access_token}`, - }, - }, - includeExtendedPartnerData - ? { includes: "extendedPartnerInformation" } - : {} - ) - - if (!apiResponse.ok) { - await metricsGetVerifiedUser.httpError(apiResponse) - - if (apiResponse.status === 401) { - return { error: true, cause: "unauthorized" } as const - } else if (apiResponse.status === 403) { - return { error: true, cause: "forbidden" } as const - } else if (apiResponse.status === 404) { - return { error: true, cause: "notfound" } as const - } - - return { - error: true, - cause: "unknown", - status: apiResponse.status, - } as const - } - - const apiJson = await apiResponse.json() - if (!apiJson.data?.attributes) { - metricsGetVerifiedUser.dataError( - `Missing data attributes in API response`, - { - data: apiJson, - } - ) - return null - } - const verifiedData = getUserSchema.safeParse(apiJson) - - if (!verifiedData.success) { - metricsGetVerifiedUser.validationError(verifiedData.error) - return null - } - - metricsGetVerifiedUser.success() - - return verifiedData - } -) - export async function getPreviousStays( accessToken: string, limit: number = 10, diff --git a/packages/trpc/lib/utils/getRedemptionTokenSafely.ts b/packages/trpc/lib/utils/getRedemptionTokenSafely.ts new file mode 100644 index 000000000..1caa18f96 --- /dev/null +++ b/packages/trpc/lib/utils/getRedemptionTokenSafely.ts @@ -0,0 +1,23 @@ +import { isValidSession } from "./session" + +import type { Session } from "next-auth" + +export function getRedemptionTokenSafely( + session: Session, + serviceToken: string +): string | undefined { + if (!isValidSession(session)) return undefined + + // ToDo- Get Curity based token when linked user is logged in + // const token = + // session.token.loginType === "sas" + // ? session.token.curity_access_token ?? serviceToken + // : session.token.access_token + + const token = + session.token.loginType === "sas" + ? serviceToken + : session.token.access_token + + return token +} diff --git a/packages/trpc/lib/utils/getUserPointsBalance.ts b/packages/trpc/lib/utils/getUserPointsBalance.ts new file mode 100644 index 000000000..dad3445d3 --- /dev/null +++ b/packages/trpc/lib/utils/getUserPointsBalance.ts @@ -0,0 +1,27 @@ +import { getEuroBonusProfileData } from "../routers/partners/sas/getEuroBonusProfile" +import { getVerifiedUser } from "../routers/user/utils/getVerifiedUser" +import { isValidSession } from "./session" + +import type { Session } from "next-auth" + +export async function getUserPointsBalance( + session: Session | null +): Promise { + if (!isValidSession(session)) return undefined + + const verifiedUser = + session.token.loginType === "sas" + ? await getEuroBonusProfileData(session) + : await getVerifiedUser({ session }) + + if (!verifiedUser || "error" in verifiedUser) { + return undefined + } + + const points = + "points" in verifiedUser.data + ? verifiedUser.data.points.total + : verifiedUser.data.membership?.currentPoints + + return points ?? 0 +}