import { parsePhoneNumberFromString } from "libphonenumber-js" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType" import { logger } from "@scandic-hotels/common/logger" import { calculateRegularPrice } from "../../utils/calculateRegularPrice" import { sumPackages, sumPackagesRequestedPrice } from "../../utils/SelectRate" import { detailsStorageName } from "." import type { Product } from "@scandic-hotels/trpc/types/roomAvailability" import type { User } from "@scandic-hotels/trpc/types/user" import type { Price } from "../../types/price" import type { PersistedState, RoomState } from "./types" export function extractGuestFromUser(user: User) { let phoneNumberCC = "" if (user.phoneNumber) { const parsedPhoneNumber = parsePhoneNumberFromString(user.phoneNumber) if (parsedPhoneNumber?.country) { phoneNumberCC = parsedPhoneNumber.country.toLowerCase() } } return { countryCode: user.address?.countryCode?.toString(), email: user.email, firstName: user.firstName, lastName: user.lastName, join: false, membershipNo: user.membership?.membershipNumber, phoneNumber: user.phoneNumber ?? "", phoneNumberCC, } } function add(...nums: (number | string | undefined)[]) { return nums.reduce((total: number, num) => { if (typeof num === "undefined") { num = 0 } total = total + parseInt(`${num}`) return total }, 0) } export function getRoomPrice( roomRate: Product, isMember: boolean, pointsCurrency?: CurrencyEnum ) { if (isMember && "member" in roomRate && roomRate.member) { let publicRate if ( "public" in roomRate && roomRate.public?.rateType === RateTypeEnum.Regular ) { publicRate = roomRate.public } return { perNight: { requested: roomRate.member.requestedPrice ? { currency: roomRate.member.requestedPrice.currency, price: roomRate.member.requestedPrice.pricePerNight, } : undefined, local: { currency: roomRate.member.localPrice.currency, price: roomRate.member.localPrice.pricePerNight, regularPrice: publicRate?.localPrice.pricePerStay || roomRate.member.localPrice.regularPricePerNight, }, }, perStay: { requested: roomRate.member.requestedPrice ? { currency: roomRate.member.requestedPrice.currency, price: roomRate.member.requestedPrice.pricePerStay, } : undefined, local: { currency: roomRate.member.localPrice.currency, price: roomRate.member.localPrice.pricePerStay, regularPrice: publicRate?.localPrice.pricePerStay || roomRate.member.localPrice.regularPricePerStay, }, }, } } if ("public" in roomRate && roomRate.public) { return { perNight: { requested: roomRate.public.requestedPrice ? { currency: roomRate.public.requestedPrice.currency, price: roomRate.public.requestedPrice.pricePerNight, } : undefined, local: { currency: roomRate.public.localPrice.currency, price: roomRate.public.localPrice.pricePerNight, regularPrice: roomRate.public.localPrice.regularPricePerNight, }, }, perStay: { requested: roomRate.public.requestedPrice ? { currency: roomRate.public.requestedPrice.currency, price: roomRate.public.requestedPrice.pricePerStay, } : undefined, local: { currency: roomRate.public.localPrice.currency, price: roomRate.public.localPrice.pricePerStay, regularPrice: roomRate.public.localPrice.regularPricePerStay, }, }, } } if ("corporateCheque" in roomRate) { return { perNight: { requested: roomRate.corporateCheque.requestedPrice ? { currency: CurrencyEnum.CC, price: roomRate.corporateCheque.requestedPrice.numberOfCheques, additionalPrice: roomRate.corporateCheque.requestedPrice.additionalPricePerStay, additionalPriceCurrency: roomRate.corporateCheque.requestedPrice.currency ?? undefined, } : undefined, local: { currency: CurrencyEnum.CC, price: roomRate.corporateCheque.localPrice.numberOfCheques, additionalPrice: roomRate.corporateCheque.localPrice.additionalPricePerStay, additionalPriceCurrency: roomRate.corporateCheque.localPrice.currency ?? undefined, }, }, perStay: { requested: roomRate.corporateCheque.requestedPrice ? { currency: CurrencyEnum.CC, price: roomRate.corporateCheque.requestedPrice.numberOfCheques, additionalPrice: roomRate.corporateCheque.requestedPrice.additionalPricePerStay, additionalPriceCurrency: roomRate.corporateCheque.requestedPrice.currency ?? undefined, } : undefined, local: { currency: CurrencyEnum.CC, price: roomRate.corporateCheque.localPrice.numberOfCheques, additionalPrice: roomRate.corporateCheque.localPrice.additionalPricePerStay, additionalPriceCurrency: roomRate.corporateCheque.localPrice.currency ?? undefined, }, }, } } if ("voucher" in roomRate) { return { perNight: { requested: undefined, local: { currency: CurrencyEnum.Voucher, price: roomRate.voucher.numberOfVouchers, }, }, perStay: { requested: undefined, local: { currency: CurrencyEnum.Voucher, price: roomRate.voucher.numberOfVouchers, }, }, } } if ("redemption" in roomRate) { return { // ToDo Handle perNight as undefined perNight: { requested: undefined, local: { currency: pointsCurrency ?? CurrencyEnum.POINTS, price: roomRate.redemption.localPrice.pointsPerStay, additionalPrice: roomRate.redemption.localPrice.additionalPricePerStay, additionalPriceCurrency: roomRate.redemption.localPrice.currency ?? undefined, }, }, perStay: { requested: undefined, local: { currency: pointsCurrency ?? CurrencyEnum.POINTS, price: roomRate.redemption.localPrice.pointsPerStay, additionalPrice: roomRate.redemption.localPrice.additionalPricePerStay, additionalPriceCurrency: roomRate.redemption.localPrice.currency ?? undefined, }, }, } } throw new Error( `Unable to calculate RoomPrice since user is neither a member or memberRate is missing, or publicRate is missing` ) } export const checkRoomProgress = (steps: RoomState["steps"]) => { return Object.values(steps) .filter(Boolean) .every((step) => step.isValid) } export function readFromSessionStorage(): PersistedState | undefined { if (typeof window === "undefined") { return undefined } try { const storedData = sessionStorage.getItem(detailsStorageName) if (!storedData) { return undefined } const parsedData = JSON.parse(storedData) as PersistedState if (!parsedData.booking || !parsedData.rooms) { return undefined } return parsedData } catch (error) { logger.error("Error reading from session storage:", error) return undefined } } export function writeToSessionStorage(state: PersistedState) { if (typeof window === "undefined") { return } try { sessionStorage.setItem(detailsStorageName, JSON.stringify(state)) } catch (error) { logger.error("Error writing to session storage:", error) } } export function clearSessionStorage() { if (typeof window === "undefined") { return } sessionStorage.removeItem(detailsStorageName) } export function getAdditionalPrice( total: { local: { additionalPrice?: number additionalPriceCurrency?: CurrencyEnum } }, adults: number, breakfast: | { localPrice: { price: number; currency?: CurrencyEnum } } | false | undefined, nights: number, packages: | { localPrice: { totalPrice: number; currency?: CurrencyEnum } }[] | null | undefined, additionalPrice = 0, additionalPriceCurrency?: CurrencyEnum | null | undefined ) { const breakfastLocalPrice = (breakfast ? breakfast.localPrice.price : 0) * nights * adults const pkgsSum = sumPackages(packages || []) total.local.additionalPrice = add( total.local.additionalPrice, additionalPrice, breakfastLocalPrice, pkgsSum.price ) if (!total.local.additionalPriceCurrency) { if (additionalPriceCurrency) { total.local.additionalPriceCurrency = additionalPriceCurrency } else if (breakfast && breakfast.localPrice.currency) { total.local.additionalPriceCurrency = breakfast.localPrice.currency } else if (pkgsSum.currency) { total.local.additionalPriceCurrency = pkgsSum.currency } } } export function getRequestedAdditionalPrice( total: Price, adults: number, breakfast: | { requestedPrice?: { price: number; currency?: CurrencyEnum } } | false | undefined, nights: number, packages: | { requestedPrice: { totalPrice: number; currency?: CurrencyEnum } }[] | null | undefined, cheques: number, additionalPrice = 0, additionalPriceCurrency: CurrencyEnum | null | undefined ) { if (!total.requested) { total.requested = { currency: CurrencyEnum.CC, price: 0, } } total.requested.price = add(total.requested.price, cheques) const breakfastRequestedPrice = (breakfast ? breakfast.requestedPrice?.price || 0 : 0) * nights * adults const pkgsSumRequested = sumPackagesRequestedPrice(packages) total.requested.additionalPrice = add( total.requested.additionalPrice, additionalPrice, breakfastRequestedPrice, pkgsSumRequested.price ) if (!total.requested.additionalPriceCurrency) { if (additionalPriceCurrency) { total.requested.additionalPriceCurrency = additionalPriceCurrency } else if (pkgsSumRequested.currency) { total.requested.additionalPriceCurrency = pkgsSumRequested.currency } else if (breakfast && breakfast.requestedPrice) { total.requested.additionalPriceCurrency = breakfast.requestedPrice.currency } } } type TRoom = Pick< RoomState["room"], "adults" | "breakfast" | "guest" | "roomFeatures" | "roomRate" > type CorporateCheckRoom = PriceCalculationRoom & { roomRate: { corporateCheque: { localPrice: { numberOfCheques: number additionalPricePerStay: number currency?: CurrencyEnum } requestedPrice?: { numberOfCheques: number additionalPricePerStay: number currency?: CurrencyEnum } } } } export function getCorporateChequePrice( rooms: PriceCalculationRoom[], nights: number ) { return rooms .filter( (room): room is CorporateCheckRoom => "corporateCheque" in room.roomRate ) .reduce( (total, room) => { const corporateCheque = room.roomRate.corporateCheque total.local.price = add( total.local.price, corporateCheque.localPrice.numberOfCheques ) getAdditionalPrice( total, room.adults, room.breakfast, nights, room.roomFeatures, corporateCheque.localPrice.additionalPricePerStay, corporateCheque.localPrice.currency ) if (corporateCheque.requestedPrice) { getRequestedAdditionalPrice( total, room.adults, room.breakfast, nights, room.roomFeatures, corporateCheque.requestedPrice.numberOfCheques, corporateCheque.requestedPrice?.additionalPricePerStay, corporateCheque.requestedPrice?.currency ) } return total }, { local: { currency: CurrencyEnum.CC, price: 0, }, requested: undefined, } ) } type VoucherRoom = PriceCalculationRoom & { roomRate: { voucher: { numberOfVouchers: number } } } export function getVoucherPrice(rooms: PriceCalculationRoom[], nights: number) { return rooms .filter((room): room is VoucherRoom => "voucher" in room.roomRate) .reduce( (total, room) => { const voucher = room.roomRate.voucher total.local.price = add(total.local.price, voucher.numberOfVouchers) getAdditionalPrice( total, room.adults, room.breakfast, nights, room.roomFeatures ) return total }, { local: { currency: CurrencyEnum.Voucher, price: 0, }, requested: undefined, } ) } type PriceCalculationRoom = { adults: number breakfast: | { localPrice: { price: number; currency?: CurrencyEnum } requestedPrice?: { price: number; currency?: CurrencyEnum } } | false | undefined roomFeatures: | { localPrice: { totalPrice: number; currency?: CurrencyEnum } requestedPrice: { totalPrice: number; currency?: CurrencyEnum } }[] | null | undefined // We don't care about roomRate unless it's RedemptionProduct roomRate: object } type RedemptionRoom = PriceCalculationRoom & { roomRate: { redemption: { localPrice: { pointsPerStay: number additionalPricePerStay: number currency?: CurrencyEnum } } } } export function getRedemptionPrice( rooms: PriceCalculationRoom[], nights: number, pointsCurrency?: CurrencyEnum ) { return rooms .filter((room): room is RedemptionRoom => "redemption" in room.roomRate) .reduce( (total, room) => { const redemption = room.roomRate.redemption total.local.price = add( total.local.price, redemption.localPrice.pointsPerStay ) getAdditionalPrice( total, room.adults, room.breakfast, nights, room.roomFeatures, redemption.localPrice.additionalPricePerStay, redemption.localPrice.currency ) return total }, { local: { currency: pointsCurrency ?? CurrencyEnum.POINTS, price: 0, }, requested: undefined, } ) } type RegularPriceCalculationRoom = PriceCalculationRoom & { guest: { join: boolean membershipNo?: string | null } } type RegularRoomLocalPrice = { price: number currency: CurrencyEnum pricePerStay: number pricePerNight: number regularPricePerStay: number } type RegularRoomRequestedPrice = { price: number currency: CurrencyEnum pricePerStay: number } type GetRegularPriceRoom = RegularPriceCalculationRoom & { roomRate: { member: { localPrice: RegularRoomLocalPrice requestedPrice: RegularRoomRequestedPrice } public: { localPrice: RegularRoomLocalPrice requestedPrice: RegularRoomRequestedPrice } } } export function getRegularPrice( rooms: RegularPriceCalculationRoom[], isMember: boolean, nights: number ) { const totalPrice = rooms .filter( (room): room is GetRegularPriceRoom => "member" in room.roomRate || "public" in room.roomRate ) .reduce( (total, room, idx) => { const isMainRoomAndMember = idx === 0 && isMember const join = Boolean(room.guest.join || room.guest.membershipNo) const memberRate = "member" in room.roomRate && room.roomRate.member const publicRate = "public" in room.roomRate && room.roomRate.public const useMemberRate = (isMainRoomAndMember || join) && memberRate const rate = useMemberRate ? memberRate : publicRate if (!rate) { return total } const breakfastLocalPrice = (room.breakfast ? room.breakfast.localPrice.price || 0 : 0) * nights * room.adults const pkgsSum = sumPackages(room.roomFeatures || []) const additionalCost = breakfastLocalPrice + pkgsSum.price total.local.currency = rate.localPrice.currency total.local.price = add( total.local.price, rate.localPrice.pricePerStay, additionalCost ) if (rate.requestedPrice) { if (!total.requested) { total.requested = { currency: rate.requestedPrice.currency, price: 0, } } const breakfastRequestedPrice = (room.breakfast ? (room.breakfast.requestedPrice?.price ?? 0) : 0) * nights * room.adults const pkgsSumRequested = sumPackagesRequestedPrice(room.roomFeatures) total.requested.price = add( total.requested.price, rate.requestedPrice.pricePerStay, breakfastRequestedPrice, pkgsSumRequested.price ) } return calculateRegularPrice({ total, useMemberRate: !!useMemberRate, regularMemberPrice: memberRate ? { pricePerStay: memberRate.localPrice.pricePerNight, regularPricePerStay: memberRate.localPrice.regularPricePerStay, } : undefined, regularPublicPrice: publicRate ? { pricePerStay: publicRate.localPrice.pricePerNight, regularPricePerStay: publicRate.localPrice.regularPricePerStay, } : undefined, additionalCost, }) }, { local: { currency: CurrencyEnum.Unknown, price: 0, regularPrice: 0, }, requested: undefined, } ) if ( totalPrice.local.regularPrice && totalPrice.local.price >= totalPrice.local.regularPrice ) { totalPrice.local.regularPrice = 0 } return totalPrice } export function getTotalPrice( rooms: TRoom[], isMember: boolean, nights: number, pointsCurrency?: CurrencyEnum ) { const hasCorpChqRates = rooms.some( (room) => "corporateCheque" in room.roomRate ) if (hasCorpChqRates) { return getCorporateChequePrice(rooms, nights) } const hasRedemptionRates = rooms.some((room) => "redemption" in room.roomRate) if (hasRedemptionRates) { return getRedemptionPrice(rooms, nights, pointsCurrency) } const hasVoucherRates = rooms.some((room) => "voucher" in room.roomRate) if (hasVoucherRates) { return getVoucherPrice(rooms, nights) } return getRegularPrice(rooms, isMember, nights) }