diff --git a/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx b/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx index ced6af8ec..f8c83fe56 100644 --- a/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx +++ b/packages/booking-flow/lib/contexts/EnterDetails/EnterDetailsContext.tsx @@ -16,10 +16,10 @@ import { import { EnterDetailsStepEnum } from "../../stores/enter-details/enterDetailsStep" import { clearSessionStorage, - getTotalPrice, readFromSessionStorage, writeToSessionStorage, } from "../../stores/enter-details/helpers" +import { getTotalPrice } from "../../stores/enter-details/priceCalculations" import { isSameBooking } from "../../utils/isSameBooking" import type { Lang } from "@scandic-hotels/common/constants/language" diff --git a/packages/booking-flow/lib/stores/enter-details/helpers.ts b/packages/booking-flow/lib/stores/enter-details/helpers.ts index b2c331058..c38af4946 100644 --- a/packages/booking-flow/lib/stores/enter-details/helpers.ts +++ b/packages/booking-flow/lib/stores/enter-details/helpers.ts @@ -1,17 +1,11 @@ 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) { @@ -34,191 +28,6 @@ export function extractGuestFromUser(user: User) { } } -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) @@ -267,430 +76,3 @@ export function clearSessionStorage() { } 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 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 -} - -type TRoom = Pick< - RoomState["room"], - "adults" | "breakfast" | "guest" | "roomFeatures" | "roomRate" -> - -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) -} diff --git a/packages/booking-flow/lib/stores/enter-details/index.ts b/packages/booking-flow/lib/stores/enter-details/index.ts index 00e15282c..39d832b87 100644 --- a/packages/booking-flow/lib/stores/enter-details/index.ts +++ b/packages/booking-flow/lib/stores/enter-details/index.ts @@ -11,10 +11,9 @@ import { EnterDetailsStepEnum } from "./enterDetailsStep" import { checkRoomProgress, extractGuestFromUser, - getRoomPrice, - getTotalPrice, writeToSessionStorage, } 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" diff --git a/packages/booking-flow/lib/stores/enter-details/helpers.test.ts b/packages/booking-flow/lib/stores/enter-details/priceCalculations.test.ts similarity index 88% rename from packages/booking-flow/lib/stores/enter-details/helpers.test.ts rename to packages/booking-flow/lib/stores/enter-details/priceCalculations.test.ts index 99e40ed98..21533d488 100644 --- a/packages/booking-flow/lib/stores/enter-details/helpers.test.ts +++ b/packages/booking-flow/lib/stores/enter-details/priceCalculations.test.ts @@ -11,7 +11,7 @@ import { getRequestedAdditionalPrice, getTotalPrice, getVoucherPrice, -} from "./helpers" +} from "./priceCalculations" type GetAdditionalPriceParams = Parameters describe("getAdditionalPrice", () => { @@ -920,48 +920,6 @@ describe("getCorporateChequePrice", () => { }) }) - it("does not return price for rooms without corporateCheque", () => { - const nights = 2 - const rooms: GetCorporateChequePriceParams[0] = [ - { - adults: 1, - breakfast: false, - roomFeatures: [], - roomRate: { - public: { - localPrice: { - pricePerStay: 500, - }, - }, - }, - }, - { - adults: 1, - breakfast: false, - roomFeatures: [], - roomRate: { - corporateCheque: { - localPrice: { - numberOfCheques: 3, - additionalPricePerStay: 0, - }, - }, - }, - }, - ] - - const result = getCorporateChequePrice(rooms, nights) - - expect(result).toEqual({ - local: { - price: 3, - currency: CurrencyEnum.CC, - additionalPrice: 0, - }, - requested: undefined, - }) - }) - it("calculates combined price with breakfast and room features", () => { const nights = 3 const rooms: GetCorporateChequePriceParams[0] = [ @@ -1167,49 +1125,6 @@ describe("getRedemptionPrice", () => { }) }) - it("does not return price for room without redemption", () => { - const nights = 3 - const result = getRedemptionPrice( - [ - { - adults: 1, - breakfast: false, - roomFeatures: [], - roomRate: { - public: { - price: 150, - }, - }, - }, - { - adults: 1, - breakfast: false, - roomFeatures: [], - roomRate: { - redemption: { - localPrice: { - pointsPerStay: 150, - currency: CurrencyEnum.POINTS, - additionalPricePerStay: 0, - }, - }, - }, - }, - ], - nights - ) - - expect(result).toEqual({ - local: { - price: 150, - currency: CurrencyEnum.POINTS, - additionalPrice: 0, - additionalPriceCurrency: CurrencyEnum.POINTS, - }, - requested: undefined, - }) - }) - it("returns price and additionalPrice for single room with room features", () => { const nights = 2 const result = getRedemptionPrice( @@ -1352,48 +1267,6 @@ describe("getVoucherPrice", () => { requested: undefined, }) }) - - it("does not return price for room without voucher", () => { - const nights = 3 - const result = getVoucherPrice( - [ - { - adults: 1, - breakfast: false, - roomFeatures: [], - roomRate: { - redemption: { - localPrice: { - pointsPerStay: 150, - currency: CurrencyEnum.POINTS, - additionalPricePerStay: 0, - }, - }, - }, - }, - { - adults: 1, - breakfast: false, - roomFeatures: [], - roomRate: { - voucher: { - numberOfVouchers: 2, - }, - }, - }, - ], - nights - ) - - expect(result).toEqual({ - local: { - price: 2, - currency: CurrencyEnum.Voucher, - additionalPrice: 0, - }, - requested: undefined, - }) - }) }) describe("getRegularPrice", () => { @@ -1412,34 +1285,6 @@ describe("getRegularPrice", () => { }) }) - it("returns price 0 for rooms without public or member rate", () => { - const isMember = false - const nights = 1 - const guest = { join: false } - const result = getRegularPrice( - [ - { - adults: 1, - breakfast: false, - guest, - roomFeatures: [], - roomRate: {}, - }, - ], - isMember, - nights - ) - - expect(result).toEqual({ - local: { - currency: CurrencyEnum.Unknown, - price: 0, - regularPrice: 0, - }, - requested: undefined, - }) - }) - it("calculates regular price for non-member", () => { const isMember = false const nights = 2 @@ -1455,6 +1300,7 @@ describe("getRegularPrice", () => { public: { localPrice: { pricePerNight: 100, + pricePerStay: 0, regularPricePerStay: 100, currency: CurrencyEnum.SEK, }, @@ -1462,6 +1308,7 @@ describe("getRegularPrice", () => { member: { localPrice: { pricePerNight: 50, + pricePerStay: 0, regularPricePerStay: 50, currency: CurrencyEnum.SEK, }, @@ -1498,6 +1345,7 @@ describe("getRegularPrice", () => { public: { localPrice: { pricePerNight: 100, + pricePerStay: 0, regularPricePerStay: 100, currency: CurrencyEnum.SEK, }, @@ -1505,6 +1353,7 @@ describe("getRegularPrice", () => { member: { localPrice: { pricePerNight: 50, + pricePerStay: 0, regularPricePerStay: 50, currency: CurrencyEnum.SEK, }, @@ -1541,10 +1390,12 @@ describe("getRegularPrice", () => { member: { localPrice: { pricePerNight: 50, + pricePerStay: 0, regularPricePerStay: 50, currency: CurrencyEnum.SEK, }, }, + public: null, }, }, ], @@ -1577,10 +1428,12 @@ describe("getRegularPrice", () => { public: { localPrice: { pricePerNight: 50, + pricePerStay: 0, regularPricePerStay: 50, currency: CurrencyEnum.SEK, }, }, + member: null, }, }, ], @@ -1613,6 +1466,7 @@ describe("getRegularPrice", () => { public: { localPrice: { pricePerNight: 100, + pricePerStay: 0, regularPricePerStay: 100, currency: CurrencyEnum.SEK, }, @@ -1620,6 +1474,7 @@ describe("getRegularPrice", () => { member: { localPrice: { pricePerNight: 50, + pricePerStay: 0, regularPricePerStay: 50, currency: CurrencyEnum.SEK, }, @@ -1665,6 +1520,7 @@ describe("getRegularPrice", () => { public: { localPrice: { pricePerNight: 100, + pricePerStay: 0, regularPricePerStay: 100, currency: CurrencyEnum.SEK, }, @@ -1672,6 +1528,7 @@ describe("getRegularPrice", () => { member: { localPrice: { pricePerNight: 50, + pricePerStay: 0, regularPricePerStay: 50, currency: CurrencyEnum.SEK, }, @@ -1713,10 +1570,12 @@ describe("getRegularPrice", () => { public: { localPrice: { pricePerNight: 0, + pricePerStay: 0, regularPricePerStay: 0, currency: CurrencyEnum.SEK, }, }, + member: null, }, }, ], @@ -1757,15 +1616,16 @@ describe("getRegularPrice", () => { public: { localPrice: { pricePerNight: 100, + pricePerStay: 0, regularPricePerStay: 100, currency: CurrencyEnum.SEK, }, requestedPrice: { currency: CurrencyEnum.EUR, - pricePerNight: 10, - regularPricePerStay: 10, + pricePerStay: 0, }, }, + member: null, }, }, ], @@ -1800,6 +1660,7 @@ describe("getRegularPrice", () => { public: { localPrice: { pricePerNight: 100, + pricePerStay: 0, regularPricePerStay: 100, currency: CurrencyEnum.SEK, }, @@ -1807,6 +1668,7 @@ describe("getRegularPrice", () => { member: { localPrice: { pricePerNight: 50, + pricePerStay: 0, regularPricePerStay: 50, currency: CurrencyEnum.SEK, }, @@ -1822,6 +1684,7 @@ describe("getRegularPrice", () => { public: { localPrice: { pricePerNight: 75, + pricePerStay: 0, regularPricePerStay: 75, currency: CurrencyEnum.SEK, }, @@ -1829,6 +1692,7 @@ describe("getRegularPrice", () => { member: { localPrice: { pricePerNight: 30, + pricePerStay: 0, regularPricePerStay: 30, currency: CurrencyEnum.SEK, }, @@ -1849,6 +1713,36 @@ describe("getRegularPrice", () => { requested: undefined, }) }) + + it("returns unknown price for room without public and member rate", () => { + const isMember = false + const nights = 1 + const result = getRegularPrice( + [ + { + adults: 1, + breakfast: false, + roomFeatures: [], + guest: { join: false }, + roomRate: { + public: null, + member: null, + }, + }, + ], + isMember, + nights + ) + + expect(result).toEqual({ + local: { + currency: CurrencyEnum.Unknown, + price: 0, + regularPrice: 0, + }, + requested: undefined, + }) + }) }) type TotalPriceRooms = Parameters[0] @@ -1868,15 +1762,8 @@ describe("getTotalPrice", () => { numberOfCheques: 3, additionalPricePerStay: 0, }, - rateCode: {} as never, - rateType: {} as never, }, - bookingCode: {} as never, - rateDefinition: {} as never, - rate: {} as never, - rateDefinitionMember: {} as never, }, - guest: {} as never, }, { adults: 1, @@ -1886,33 +1773,21 @@ describe("getTotalPrice", () => { public: { localPrice: { pricePerStay: 500, + pricePerNight: 500, + regularPricePerStay: 500, currency: CurrencyEnum.SEK, - omnibusPricePerNight: {} as never, - pricePerNight: {} as never, - regularPricePerNight: {} as never, - regularPricePerStay: {} as never, }, - rateCode: {} as never, - rateType: {} as never, }, member: { localPrice: { pricePerNight: 30, + pricePerStay: 30, regularPricePerStay: 30, currency: CurrencyEnum.SEK, - omnibusPricePerNight: {} as never, - pricePerStay: {} as never, - regularPricePerNight: {} as never, }, - rateCode: {} as never, - rateType: {} as never, }, - bookingCode: {} as never, - rateDefinition: {} as never, - rate: {} as never, - rateDefinitionMember: {} as never, }, - guest: {} as never, + guest: { join: false }, }, ] @@ -1941,20 +1816,11 @@ describe("getTotalPrice", () => { redemption: { localPrice: { pointsPerStay: 100, - pointsPerNight: 100, currency: CurrencyEnum.POINTS, additionalPricePerStay: 0, }, - hasEnoughPoints: {} as never, - rateCode: {} as never, - rateType: {} as never, }, - bookingCode: {} as never, - rateDefinition: {} as never, - rate: {} as never, - rateDefinitionMember: {} as never, }, - guest: {} as never, }, { adults: 1, @@ -1964,33 +1830,21 @@ describe("getTotalPrice", () => { public: { localPrice: { pricePerStay: 500, + pricePerNight: 500, + regularPricePerStay: 500, currency: CurrencyEnum.SEK, - omnibusPricePerNight: {} as never, - pricePerNight: {} as never, - regularPricePerNight: {} as never, - regularPricePerStay: {} as never, }, - rateCode: {} as never, - rateType: {} as never, }, member: { localPrice: { pricePerNight: 30, + pricePerStay: 30, regularPricePerStay: 30, currency: CurrencyEnum.SEK, - omnibusPricePerNight: {} as never, - pricePerStay: {} as never, - regularPricePerNight: {} as never, }, - rateCode: {} as never, - rateType: {} as never, }, - bookingCode: {} as never, - rateDefinition: {} as never, - rate: {} as never, - rateDefinitionMember: {} as never, }, - guest: {} as never, + guest: { join: false }, }, ] @@ -2018,15 +1872,8 @@ describe("getTotalPrice", () => { roomRate: { voucher: { numberOfVouchers: 1, - rateCode: {} as never, - rateType: {} as never, }, - bookingCode: {} as never, - rateDefinition: {} as never, - rate: {} as never, - rateDefinitionMember: {} as never, }, - guest: {} as never, }, { adults: 1, @@ -2036,33 +1883,21 @@ describe("getTotalPrice", () => { public: { localPrice: { pricePerStay: 500, + pricePerNight: 500, + regularPricePerStay: 500, currency: CurrencyEnum.SEK, - omnibusPricePerNight: {} as never, - pricePerNight: {} as never, - regularPricePerNight: {} as never, - regularPricePerStay: {} as never, }, - rateCode: {} as never, - rateType: {} as never, }, member: { localPrice: { pricePerNight: 30, + pricePerStay: 30, regularPricePerStay: 30, currency: CurrencyEnum.SEK, - omnibusPricePerNight: {} as never, - pricePerStay: {} as never, - regularPricePerNight: {} as never, }, - rateCode: {} as never, - rateType: {} as never, }, - bookingCode: {} as never, - rateDefinition: {} as never, - rate: {} as never, - rateDefinitionMember: {} as never, }, - guest: {} as never, + guest: { join: false }, }, ] @@ -2092,31 +1927,19 @@ describe("getTotalPrice", () => { pricePerStay: 500, currency: CurrencyEnum.SEK, regularPricePerStay: 500, - omnibusPricePerNight: {} as never, - pricePerNight: {} as never, - regularPricePerNight: {} as never, + pricePerNight: 500, }, - rateCode: {} as never, - rateType: {} as never, }, member: { localPrice: { pricePerStay: 30, currency: CurrencyEnum.SEK, regularPricePerStay: 30, - omnibusPricePerNight: {} as never, - pricePerNight: {} as never, - regularPricePerNight: {} as never, + pricePerNight: 30, }, - rateCode: {} as never, - rateType: {} as never, }, - bookingCode: {} as never, - rateDefinition: {} as never, - rate: {} as never, - rateDefinitionMember: {} as never, }, - guest: { join: false } as never, + guest: { join: false }, }, ] @@ -2131,4 +1954,64 @@ describe("getTotalPrice", () => { requested: undefined, }) }) + + it("returns regular price for room with only public rate", () => { + const nights = 1 + const isMember = false + const rooms: TotalPriceRooms = [ + { + adults: 1, + breakfast: false, + roomFeatures: [], + roomRate: { + public: { + localPrice: { + pricePerStay: 500, + currency: CurrencyEnum.SEK, + regularPricePerStay: 500, + pricePerNight: 500, + }, + }, + member: null, + }, + guest: { join: false }, + }, + ] + + const totalPrice = getTotalPrice(rooms, isMember, nights) + + expect(totalPrice).toEqual({ + local: { + price: 500, + regularPrice: 0, + currency: CurrencyEnum.SEK, + }, + requested: undefined, + }) + }) + + it("returns unknown price for unknown room type", () => { + const nights = 1 + const isMember = false + const rooms: TotalPriceRooms = [ + { + adults: 1, + breakfast: false, + roomFeatures: [], + roomRate: {}, + guest: { join: false }, + }, + ] + + const totalPrice = getTotalPrice(rooms, isMember, nights) + + expect(totalPrice).toEqual({ + local: { + price: 0, + currency: CurrencyEnum.Unknown, + regularPrice: 0, + }, + requested: undefined, + }) + }) }) diff --git a/packages/booking-flow/lib/stores/enter-details/priceCalculations.ts b/packages/booking-flow/lib/stores/enter-details/priceCalculations.ts new file mode 100644 index 000000000..aa6b9b445 --- /dev/null +++ b/packages/booking-flow/lib/stores/enter-details/priceCalculations.ts @@ -0,0 +1,626 @@ +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 type { Product } from "@scandic-hotels/trpc/types/roomAvailability" + +import type { Price } from "../../types/price" + +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 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 BasePriceCalculationRoom = { + 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 what type of roomRate it is yet + roomRate: object +} + +type CorporateCheckRoom = BasePriceCalculationRoom & { + roomRate: { + corporateCheque: { + localPrice: { + numberOfCheques: number + additionalPricePerStay: number + currency?: CurrencyEnum + } + requestedPrice?: { + numberOfCheques: number + additionalPricePerStay: number + currency?: CurrencyEnum + } + } + } +} +export function getCorporateChequePrice( + rooms: CorporateCheckRoom[], + nights: number +) { + return rooms.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 = BasePriceCalculationRoom & { + roomRate: { + voucher: { + numberOfVouchers: number + } + } +} +export function getVoucherPrice(rooms: VoucherRoom[], nights: number) { + return rooms.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 RedemptionRoom = BasePriceCalculationRoom & { + roomRate: { + redemption: { + localPrice: { + pointsPerStay: number + additionalPricePerStay: number + currency?: CurrencyEnum + } + } + } +} + +export function getRedemptionPrice( + rooms: RedemptionRoom[], + nights: number, + pointsCurrency?: CurrencyEnum +) { + return rooms.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 = BasePriceCalculationRoom & { + guest: { + join: boolean + membershipNo?: string | null + } + roomRate: { + member?: { + localPrice: RegularRoomLocalPrice + requestedPrice?: RegularRoomRequestedPrice | null + } | null + public?: { + localPrice: RegularRoomLocalPrice + requestedPrice?: RegularRoomRequestedPrice | null + } | null + } +} +type RegularRoomLocalPrice = { + currency: CurrencyEnum + pricePerStay: number + pricePerNight: number + regularPricePerStay: number +} +type RegularRoomRequestedPrice = { + currency: CurrencyEnum + pricePerStay: number +} + +export function getRegularPrice( + rooms: RegularPriceCalculationRoom[], + isMember: boolean, + nights: number +) { + const totalPrice = rooms.reduce( + (total, room, idx) => { + const isMainRoomAndMember = idx === 0 && isMember + const join = Boolean(room.guest.join || room.guest.membershipNo) + const memberRate = room.roomRate.member + const publicRate = 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: ( + | RegularPriceCalculationRoom + | CorporateCheckRoom + | RedemptionRoom + | VoucherRoom + )[], + isMember: boolean, + nights: number, + pointsCurrency?: CurrencyEnum +) { + const corporateChequeRooms = rooms.filter( + (x): x is CorporateCheckRoom => "corporateCheque" in x.roomRate + ) + if (corporateChequeRooms.length > 0) { + return getCorporateChequePrice(corporateChequeRooms, nights) + } + + const redemptionRooms = rooms.filter( + (x): x is RedemptionRoom => "redemption" in x.roomRate + ) + if (redemptionRooms.length > 0) { + return getRedemptionPrice(redemptionRooms, nights, pointsCurrency) + } + + const voucherRooms = rooms.filter( + (x): x is VoucherRoom => "voucher" in x.roomRate + ) + if (voucherRooms.length > 0) { + return getVoucherPrice(voucherRooms, nights) + } + + const regularRooms = rooms.filter( + (x): x is RegularPriceCalculationRoom => + "member" in x.roomRate || "public" in x.roomRate + ) + if (regularRooms.length > 0) { + return getRegularPrice(regularRooms, isMember, nights) + } + + logger.warn( + "Unable to determine room type for price calculation, return zero price" + ) + return { + local: { + currency: CurrencyEnum.Unknown, + price: 0, + regularPrice: 0, + }, + requested: undefined, + } +}