650 lines
19 KiB
TypeScript
650 lines
19 KiB
TypeScript
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<SafeUser>) {
|
|
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<Price>(
|
|
(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<Price>(
|
|
(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<Price>(
|
|
(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<Price>(
|
|
(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)
|
|
}
|