feat(SW-1717): rewrite select-rate to show all variants of rate-cards

This commit is contained in:
Simon Emanuelsson
2025-03-25 11:25:44 +01:00
committed by Michael Zetterberg
parent adde77eaa9
commit ebaea78fb3
118 changed files with 4601 additions and 4374 deletions

View File

@@ -97,11 +97,19 @@ export default function PriceDetailsTable({
return (
<table className={styles.priceDetailsTable}>
{rooms.map((room, idx) => {
const getMemberRate = idx === 0 && isMember
const price =
getMemberRate && room.roomRate.memberRate
? room.roomRate.memberRate
: room.roomRate.publicRate
const isMainRoom = idx === 0
const getMemberRate = isMainRoom && isMember
let price
if (
getMemberRate &&
"member" in room.roomRate &&
room.roomRate.member
) {
price = room.roomRate.member
} else if ("public" in room.roomRate && room.roomRate.public) {
price = room.roomRate.public
}
if (!price) {
return null
}

View File

@@ -20,6 +20,7 @@ import {
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import { isBookingCodeRate } from "./isBookingCodeRate"
import PriceDetailsTable from "./PriceDetailsTable"
import styles from "./summary.module.css"
@@ -27,7 +28,6 @@ import styles from "./summary.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary"
import { RateTypeEnum } from "@/types/enums/rateType"
export default function Summary({
booking,
@@ -48,19 +48,21 @@ export default function Summary({
)
function getMemberPrice(roomRate: RoomRate) {
return roomRate?.memberRate
? {
currency: roomRate.memberRate.localPrice.currency ?? "",
pricePerNight: roomRate.memberRate.localPrice.pricePerNight ?? 0,
amount: roomRate.memberRate.localPrice.pricePerStay ?? 0,
}
: null
if ("member" in roomRate && roomRate.member) {
return {
amount: roomRate.member.localPrice.pricePerStay,
currency: roomRate.member.localPrice.currency,
pricePerNight: roomRate.member.localPrice.pricePerNight,
}
}
return null
}
const memberPrice = getMemberPrice(rooms[0].roomRate)
const containsBookingCodeRate = rooms.find(
(room) => room.roomRate.publicRate?.rateType !== RateTypeEnum.Regular
const containsBookingCodeRate = rooms.find((r) =>
isBookingCodeRate(r.roomRate)
)
const showDiscounted = containsBookingCodeRate || isMember
@@ -119,9 +121,8 @@ export default function Summary({
const memberPrice = getMemberPrice(room.roomRate)
const showMemberPrice = !!(isMember && memberPrice && roomNumber === 1)
const isBookingCodeRate =
room.roomRate.publicRate?.rateType !== RateTypeEnum.Regular
const showDiscounted = isBookingCodeRate || showMemberPrice
const showDiscounted =
isBookingCodeRate(room.roomRate) || showMemberPrice
const adultsMsg = intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },

View File

@@ -9,12 +9,13 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPriceWithAdditionalPrice } from "@/utils/numberFormatting"
import { isBookingCodeRate } from "./isBookingCodeRate"
import { mapRate } from "./mapRate"
import Summary from "./Summary"
import styles from "./mobileSummary.module.css"
import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
import { RateTypeEnum } from "@/types/enums/rateType"
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
export default function MobileSummary({
@@ -69,54 +70,12 @@ export default function MobileSummary({
return null
}
const rateDefinitions = roomRateDefinitions.rateDefinitions
const rooms = rateSummary.map((room, index) =>
mapRate(room, index, bookingRooms)
)
const rooms = rateSummary.map((room, index) => ({
adults: bookingRooms[index].adults,
childrenInRoom: bookingRooms[index].childrenInRoom ?? undefined,
roomType: room.roomType,
roomPrice: {
perNight: {
local: {
price: (room.public?.localPrice.pricePerNight ||
room.member?.localPrice.pricePerNight)!,
currency: (room.public?.localPrice.currency ||
room.member?.localPrice.currency)!,
},
requested: undefined,
},
perStay: {
local: {
price: (room.public?.localPrice.pricePerStay ||
room.member?.localPrice.pricePerStay)!,
currency: (room.public?.localPrice.currency ||
room.member?.localPrice.currency)!,
},
requested: undefined,
},
currency: (room.public?.localPrice.currency ||
room.member?.localPrice.currency)!,
},
roomRate: {
...room.public,
memberRate: room.member,
publicRate: room.public,
},
rateDetails: rateDefinitions.find(
(rate) =>
rate.rateCode === room.public?.rateCode ||
rate.rateCode === room.member?.rateCode
)?.generalTerms,
cancellationText:
rateDefinitions.find(
(rate) =>
rate.rateCode === room.public?.rateCode ||
rate.rateCode === room.member?.rateCode
)?.cancellationText ?? "",
}))
const containsBookingCodeRate = rateSummary.find(
(rate) => rate.public?.rateType !== RateTypeEnum.Regular
const containsBookingCodeRate = rateSummary.find((r) =>
isBookingCodeRate(r.product)
)
const showDiscounted = containsBookingCodeRate || isUserLoggedIn

View File

@@ -0,0 +1,20 @@
import { RateTypeEnum } from "@/types/enums/rateType"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
export function isBookingCodeRate(product: Product) {
if (
"corporateCheque" in product ||
"redemption" in product ||
"voucher" in product
) {
return true
} else {
if (product.public) {
return product.public.rateType !== RateTypeEnum.Regular
}
if (product.member) {
return product.member.rateType !== RateTypeEnum.Regular
}
return false
}
}

View File

@@ -0,0 +1,87 @@
import type {
Rate,
Room,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import { CurrencyEnum } from "@/types/enums/currency"
export function mapRate(room: Rate, index: number, bookingRooms: Room[]) {
const rate = {
adults: bookingRooms[index].adults,
cancellationText: room.product.rateDefinition?.cancellationText ?? "",
childrenInRoom: bookingRooms[index].childrenInRoom ?? undefined,
rateDetails: room.product.rateDefinition?.generalTerms,
roomPrice: {
currency: CurrencyEnum.Unknown,
perNight: {
local: {
currency: CurrencyEnum.Unknown,
price: 0,
},
requested: undefined,
},
perStay: {
local: {
currency: CurrencyEnum.Unknown,
price: 0,
},
requested: undefined,
},
},
roomRate: room.product,
roomType: room.roomType,
}
if ("corporateCheque" in room.product) {
rate.roomPrice.currency = CurrencyEnum.CC
rate.roomPrice.perNight.local = {
currency: CurrencyEnum.CC,
price: room.product.corporateCheque.localPrice.additionalPricePerStay,
}
rate.roomPrice.perStay.local = {
currency: CurrencyEnum.CC,
price: room.product.corporateCheque.localPrice.additionalPricePerStay,
}
} else if ("redemption" in room.product) {
rate.roomPrice.currency = CurrencyEnum.POINTS
rate.roomPrice.perNight.local = {
currency: CurrencyEnum.POINTS,
price: room.product.redemption.localPrice.pointsPerNight,
}
rate.roomPrice.perStay.local = {
currency: CurrencyEnum.POINTS,
price: room.product.redemption.localPrice.pointsPerStay,
}
} else if ("voucher" in room.product) {
rate.roomPrice.currency = CurrencyEnum.Voucher
rate.roomPrice.perNight.local = {
currency: CurrencyEnum.Voucher,
price: room.product.voucher.numberOfVouchers,
}
rate.roomPrice.perStay.local = {
currency: CurrencyEnum.Voucher,
price: room.product.voucher.numberOfVouchers,
}
} else {
const currency =
room.product.public?.localPrice.currency ||
room.product.member?.localPrice.currency ||
CurrencyEnum.Unknown
rate.roomPrice.currency = currency
rate.roomPrice.perNight.local = {
currency,
price:
room.product.public?.localPrice.pricePerNight ||
room.product.member?.localPrice.pricePerNight ||
0,
}
rate.roomPrice.perStay.local = {
currency,
price:
room.product.public?.localPrice.pricePerStay ||
room.product.member?.localPrice.pricePerStay ||
0,
}
}
return rate
}

View File

@@ -21,17 +21,15 @@ import {
import MobileSummary from "./MobileSummary"
import {
calculateChequePrice,
calculateCorporateChequePrice,
calculateRedemptionTotalPrice,
calculateTotalPrice,
calculateVoucherPrice,
} from "./utils"
import styles from "./rateSummary.module.css"
import {
PointsPriceSchema,
type Price,
} from "@/types/components/hotelReservation/price"
import type { Price } from "@/types/components/hotelReservation/price"
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
@@ -40,7 +38,6 @@ import { RateTypeEnum } from "@/types/enums/rateType"
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
const {
bookingCode,
isRedemption,
bookingRooms,
dates,
petRoomPackage,
@@ -105,7 +102,9 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
const totalRoomsRequired = bookingRooms.length
const isAllRoomsSelected = rateSummary.length === totalRoomsRequired
const hasMemberRates = rateSummary.some((room) => room.member)
const hasMemberRates = rateSummary.some(
(room) => "member" in room.product && room.product.member
)
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
@@ -139,21 +138,31 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
}
const isBookingCodeRate = rateSummary.some(
(rate) => rate.public?.rateType !== RateTypeEnum.Regular
(rate) =>
"public" in rate.product &&
rate.product.public?.rateType !== RateTypeEnum.Regular
)
const isVoucherRate = rateSummary.some((rate) => "voucher" in rate.product)
const isCorporateChequeRate = rateSummary.some(
(rate) => "corporateCheque" in rate.product
)
const isVoucherRate = rateSummary.some((rate) => rate.voucher)
const isChequeRate = rateSummary.some((rate) => rate.bonusCheque)
const showDiscounted =
isUserLoggedIn || isBookingCodeRate || isVoucherRate || isChequeRate
isUserLoggedIn ||
isBookingCodeRate ||
isVoucherRate ||
isCorporateChequeRate
const mainRoomProduct = rateSummary[0]
let totalPriceToShow: Price
if (isVoucherRate) {
totalPriceToShow = calculateVoucherPrice(rateSummary)
} else if (isChequeRate) {
totalPriceToShow = calculateChequePrice(rateSummary)
} else if (rateSummary[0].redemption) {
if ("redemption" in mainRoomProduct.product) {
// In case of reward night (redemption) only single room booking is supported by business rules
totalPriceToShow = PointsPriceSchema.parse(rateSummary[0].redemption)
totalPriceToShow = calculateRedemptionTotalPrice(
mainRoomProduct.product.redemption
)
} else if ("voucher" in mainRoomProduct.product) {
totalPriceToShow = calculateVoucherPrice(rateSummary)
} else if ("corporateCheque" in mainRoomProduct.product) {
totalPriceToShow = calculateCorporateChequePrice(rateSummary)
} else {
totalPriceToShow = calculateTotalPrice(
rateSummary,
@@ -162,40 +171,53 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
)
}
let mainRoomCurrency = ""
if (
"member" in mainRoomProduct.product &&
mainRoomProduct.product.member?.localPrice
) {
mainRoomCurrency = mainRoomProduct.product.member.localPrice.currency
}
if (
!mainRoomCurrency &&
"public" in mainRoomProduct.product &&
mainRoomProduct.product.public?.localPrice
) {
mainRoomCurrency = mainRoomProduct.product.public.localPrice.currency
}
return (
<form action={`details?${params}`} method="GET" onSubmit={handleSubmit}>
<div className={styles.summary}>
<div className={styles.content}>
<div className={styles.summaryText}>
{rateSummary.map((room, index) => {
return (
<div key={index} className={styles.roomSummary}>
{rateSummary.length > 1 ? (
<>
<Subtitle color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Room {roomIndex}" },
{ roomIndex: index + 1 }
)}
</Subtitle>
<Body color="uiTextMediumContrast">{room.roomType}</Body>
<Caption color="uiTextMediumContrast">
{getRateDetails(room.rate)}
</Caption>
</>
) : (
<>
<Subtitle color="uiTextHighContrast">
{room.roomType}
</Subtitle>
<Body color="uiTextMediumContrast">
{getRateDetails(room.rate)}
</Body>
</>
)}
</div>
)
})}
{rateSummary.map((room, index) => (
<div key={index} className={styles.roomSummary}>
{rateSummary.length > 1 ? (
<>
<Subtitle color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Room {roomIndex}" },
{ roomIndex: index + 1 }
)}
</Subtitle>
<Body color="uiTextMediumContrast">{room.roomType}</Body>
<Caption color="uiTextMediumContrast">
{getRateDetails(room.rate)}
</Caption>
</>
) : (
<>
<Subtitle color="uiTextHighContrast">
{room.roomType}
</Subtitle>
<Body color="uiTextMediumContrast">
{getRateDetails(room.rate)}
</Body>
</>
)}
</div>
))}
{/* Render unselected rooms */}
{Array.from({
length: totalRoomsRequired - rateSummary.length,
@@ -218,28 +240,34 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
<div className={styles.promoContainer}>
<SignupPromoDesktop
memberPrice={{
amount: rateSummary.reduce((total, room) => {
const memberPrice = room.member?.localPrice.pricePerStay
if (!memberPrice) {
return total
}
const hasSelectedPetRoom =
room.package === RoomPackageCodeEnum.PET_ROOM
if (!hasSelectedPetRoom) {
return total + memberPrice
}
const isPetRoom = room.features.find(
(feature) =>
feature.code === RoomPackageCodeEnum.PET_ROOM
)
const petRoomPrice =
isPetRoom && petRoomPackage
? Number(petRoomPackage.localPrice.totalPrice)
: 0
return total + memberPrice + petRoomPrice
}, 0),
currency: (rateSummary[0].member?.localPrice.currency ??
rateSummary[0].public?.localPrice.currency)!,
amount: rateSummary.reduce(
(total, { features, package: roomPackage, product }) => {
if (!("member" in product) || !product.member) {
return total
}
const memberPrice =
product.member.localPrice.pricePerStay
if (!memberPrice) {
return total
}
const hasSelectedPetRoom =
roomPackage === RoomPackageCodeEnum.PET_ROOM
if (!hasSelectedPetRoom) {
return total + memberPrice
}
const isPetRoom = features.find(
(feature) =>
feature.code === RoomPackageCodeEnum.PET_ROOM
)
const petRoomPrice =
isPetRoom && petRoomPackage
? Number(petRoomPackage.localPrice.totalPrice)
: 0
return total + memberPrice + petRoomPrice
},
0
),
currency: mainRoomCurrency,
}}
/>
</div>

View File

@@ -5,18 +5,27 @@ import {
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
import { CurrencyEnum } from "@/types/enums/currency"
import type { RedemptionProduct } from "@/types/trpc/routers/hotel/roomAvailability"
export const calculateTotalPrice = (
export function calculateTotalPrice(
selectedRateSummary: Rate[],
isUserLoggedIn: boolean,
petRoomPackage: RoomPackage | undefined
) => {
) {
return selectedRateSummary.reduce<Price>(
(total, room, idx) => {
const rate =
isUserLoggedIn && room.member && idx + 1 === 1
? room.member
: room.public
if (!("member" in room.product) || !("public" in room.product)) {
return total
}
const roomNr = idx + 1
const isMainRoom = roomNr === 1
let rate
if (isUserLoggedIn && isMainRoom && room.product.member) {
rate = room.product.member
} else if (room.product.public) {
rate = room.product.public
}
if (!rate) {
return total
@@ -25,7 +34,6 @@ export const calculateTotalPrice = (
const isPetRoom = room.features.find(
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
)
let petRoomPrice = 0
if (
petRoomPackage &&
@@ -35,33 +43,47 @@ export const calculateTotalPrice = (
petRoomPrice = Number(petRoomPackage.localPrice.totalPrice)
}
const regularPrice = rate.localPrice.regularPricePerStay
? (total.local.regularPrice || 0) +
(rate.localPrice.regularPricePerStay || 0)
: undefined
total.local.currency = rate.localPrice.currency
total.local.price =
total.local.price + rate.localPrice.pricePerStay + petRoomPrice
return {
local: {
currency: rate.localPrice.currency,
price:
total.local.price + rate.localPrice.pricePerStay + petRoomPrice,
regularPrice,
},
requested: rate.requestedPrice
? {
currency: rate.requestedPrice.currency,
price:
(total.requested?.price ?? 0) +
rate.requestedPrice.pricePerStay +
petRoomPrice,
}
: undefined,
if (rate.localPrice.regularPricePerStay) {
total.local.regularPrice =
(total.local.regularPrice || 0) +
rate.localPrice.regularPricePerStay +
petRoomPrice
}
if (rate.requestedPrice) {
if (!total.requested) {
total.requested = {
currency: rate.requestedPrice.currency,
price: 0,
}
}
if (!total.requested.currency) {
total.requested.currency = rate.requestedPrice.currency
}
total.requested.price =
total.requested.price +
rate.requestedPrice.pricePerStay +
petRoomPrice
if (rate.requestedPrice.regularPricePerStay) {
total.requested.regularPrice =
(total.requested.regularPrice || 0) +
rate.requestedPrice.regularPricePerStay +
petRoomPrice
}
}
return total
},
{
local: {
currency: (selectedRateSummary[0].public?.localPrice.currency ||
selectedRateSummary[0].member?.localPrice.currency)!,
currency: "",
price: 0,
regularPrice: undefined,
},
@@ -70,15 +92,32 @@ export const calculateTotalPrice = (
)
}
export const calculateVoucherPrice = (selectedRateSummary: Rate[]) => {
export function calculateRedemptionTotalPrice(
redemption: RedemptionProduct["redemption"]
) {
return {
local: {
additionalPrice: redemption.localPrice.additionalPricePerStay
? redemption.localPrice.additionalPricePerStay
: undefined,
additionalPriceCurrency: redemption.localPrice.currency
? redemption.localPrice.currency
: undefined,
currency: "PTS",
price: redemption.localPrice.pointsPerStay,
},
}
}
export function calculateVoucherPrice(selectedRateSummary: Rate[]) {
return selectedRateSummary.reduce<Price>(
(total, room) => {
const rate = room.voucher
if (!rate) {
if (!("voucher" in room.product)) {
return total
}
const rate = room.product.voucher
return <Price>{
return {
local: {
currency: total.local.currency,
price: total.local.price + rate.numberOfVouchers,
@@ -96,49 +135,47 @@ export const calculateVoucherPrice = (selectedRateSummary: Rate[]) => {
)
}
export const calculateChequePrice = (selectedRateSummary: Rate[]) => {
export function calculateCorporateChequePrice(selectedRateSummary: Rate[]) {
return selectedRateSummary.reduce<Price>(
(total, room) => {
const rate = room.bonusCheque
if (!rate) {
if (!("corporateCheque" in room.product)) {
return total
}
const rate = room.product.corporateCheque
const price = total.local.price + rate.localPrice.numberOfBonusCheques
const additionalPrice =
rate.localPrice.numberOfBonusCheques &&
(total.local.additionalPrice ?? 0) +
(rate.localPrice.additionalPricePerStay ?? 0)
const additionalPriceCurrency = (rate.localPrice.numberOfBonusCheques &&
rate.localPrice.currency)!
const requestedPrice = rate.requestedPrice?.numberOfBonusCheques
? (total.requested?.price ?? 0) +
rate.requestedPrice?.numberOfBonusCheques
: total.requested?.price
const requestedAdditionalPrice =
rate.requestedPrice?.additionalPricePerStay &&
(total.requested?.additionalPrice ?? 0) +
(rate.requestedPrice?.additionalPricePerStay ?? 0)
return <Price>{
local: {
currency: CurrencyEnum.CC,
price,
additionalPrice,
additionalPriceCurrency,
},
requested: rate.requestedPrice
? {
currency: CurrencyEnum.CC,
price: requestedPrice,
additionalPrice: requestedAdditionalPrice,
additionalPriceCurrency: rate.requestedPrice?.currency,
}
: undefined,
total.local.price = total.local.price + rate.localPrice.numberOfCheques
if (rate.localPrice.additionalPricePerStay) {
total.local.additionalPrice =
(total.local.additionalPrice || 0) +
rate.localPrice.additionalPricePerStay
}
if (rate.localPrice.currency) {
total.local.additionalPriceCurrency = rate.localPrice.currency
}
if (rate.requestedPrice) {
if (!total.requested) {
total.requested = {
currency: CurrencyEnum.CC,
price: 0,
}
}
total.requested.price =
total.requested.price + rate.requestedPrice.numberOfCheques
if (rate.requestedPrice.additionalPricePerStay) {
total.requested.additionalPrice =
(total.requested.additionalPrice || 0) +
rate.requestedPrice.additionalPricePerStay
}
if (rate.requestedPrice.currency) {
total.requested.additionalPriceCurrency = rate.requestedPrice.currency
}
}
return total
},
{
local: {