import { parsePhoneNumberFromString } from "libphonenumber-js" import { sumPackages, sumPackagesRequestedPrice, } from "@scandic-hotels/booking-flow/utils/SelectRate" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType" import { logger } from "@scandic-hotels/common/logger" import { detailsStorageName } from "." import type { BreakfastPackage } from "@scandic-hotels/trpc/routers/hotels/schemas/packages" import type { Packages } from "@scandic-hotels/trpc/types/packages" import type { CorporateChequeProduct, PriceProduct, RedemptionProduct, VoucherProduct, } from "@scandic-hotels/trpc/types/roomAvailability" import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details" import type { Price } from "@/types/components/hotelReservation/price" import type { PersistedState, RoomState } from "@/types/stores/enter-details" import type { SafeUser } from "@/types/user" export function extractGuestFromUser(user: NonNullable) { 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, } } export function add(...nums: (number | string | undefined)[]) { return nums.reduce((total: number, num) => { if (typeof num === "undefined") { num = 0 } total = total + parseInt(`${num}`) return total }, 0) } export function getRoomPrice(roomRate: RoomRate, isMember: boolean) { 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: CurrencyEnum.POINTS, price: roomRate.redemption.localPrice.pointsPerStay, additionalPrice: roomRate.redemption.localPrice.additionalPricePerStay, additionalPriceCurrency: roomRate.redemption.localPrice.currency ?? undefined, }, }, perStay: { requested: undefined, local: { currency: 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) } function getAdditionalPrice( total: Price, adults: number, breakfast: BreakfastPackage | false | undefined, nights: number, packages: Packages | null, 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 } } } function getRequestedAdditionalPrice( total: Price, adults: number, breakfast: BreakfastPackage | false | undefined, nights: number, packages: Packages | null, 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 } } } interface TRoom extends Pick< RoomState["room"], "adults" | "breakfast" | "guest" | "roomFeatures" | "roomRate" > {} interface TRoomCorporateCheque extends TRoom { roomRate: CorporateChequeProduct } export function getCorporateChequePrice(rooms: TRoom[], nights: number) { return rooms .filter( (room): room is TRoomCorporateCheque => "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, } ) } interface TRoomVoucher extends TRoom { roomRate: VoucherProduct } export function getVoucherPrice(rooms: TRoom[], nights: number) { return rooms .filter((room): room is TRoomVoucher => "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, } ) } interface TRoomRedemption extends TRoom { roomRate: RedemptionProduct } export function getRedemptionPrice(rooms: TRoom[], nights: number) { return rooms .filter((room): room is TRoomRedemption => "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: CurrencyEnum.POINTS, price: 0, }, requested: undefined, } ) } interface TRoomPriceProduct extends TRoom { roomRate: PriceProduct } export function getRegularPrice( rooms: TRoom[], isMember: boolean, nights: number ) { const totalPrice = rooms .filter( (room): room is TRoomPriceProduct => "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 getMemberRate = isMainRoomAndMember || join const memberRate = "member" in room.roomRate && room.roomRate.member const publicRate = "public" in room.roomRate && room.roomRate.public let rate if (getMemberRate && memberRate) { rate = memberRate } else if (publicRate) { rate = 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 ) } // Legend: // - total.local.price = Total Price = Black price, what the user pays // - total.local.regularPrice = Regular Price = Strikethrough price (could potentially be none) // - total.requested.price = Requested Price = EUR approx price // We sometimes don't get all the required data to calculate the correct strikethrough total. // Therefore we try these different approach to get a number that is close // enough to the real number if all data would've been present. if (getMemberRate && memberRate) { if (publicRate) { // #1 Member price uses public price as strikethrough total.local.regularPrice = add( total.local.regularPrice, publicRate.localPrice.pricePerStay, additionalCost ) } else if (memberRate.localPrice.regularPricePerStay) { // #2 Member price uses member regular price as strikethrough total.local.regularPrice = add( total.local.regularPrice, memberRate.localPrice.regularPricePerStay, additionalCost ) } else { // #3 Member price uses member price as strikethrough // NOTE: If all rooms end up using this, no strikethrough price is shown. total.local.regularPrice = add( total.local.regularPrice, memberRate.localPrice.pricePerStay, additionalCost ) } } else if (publicRate) { if (publicRate.localPrice.regularPricePerStay) { // #1 Public price uses public regular price as strikethrough total.local.regularPrice = add( total.local.regularPrice, publicRate.localPrice.regularPricePerStay, additionalCost ) } else { // #2 Public price uses public price as strikethrough // NOTE: If all rooms end up using this, no strikethrough price is shown. total.local.regularPrice = add( total.local.regularPrice, publicRate.localPrice.pricePerStay, additionalCost ) } } else { // We cannot do anything, too much data is missing. return total } return total }, { 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 ) { 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) } const hasVoucherRates = rooms.some((room) => "voucher" in room.roomRate) if (hasVoucherRates) { return getVoucherPrice(rooms, nights) } return getRegularPrice(rooms, isMember, nights) }