Merged in feat/SW-2079-update-booking-page-to-show-points- (pull request #1683)

feat: SW-2079 Show points in confirmation page

* feat: SW-2079 Show points in confirmation page

* feat: SW-2079 Optimized code

* feat: SW-2079 Updated Body to Typography

* feat: SW-2079 Multi-room total cost display

* feat: SW-2079 Add reward nights condition rate title

* feat: SW-2079 Removed extra checks

* feat: SW-2079 Optimmized formatPrice function

* feat: SW-2079 Typo fix


Approved-by: Christian Andolf
This commit is contained in:
Hrishikesh Vaipurkar
2025-04-04 09:39:55 +00:00
parent e30b8a0be3
commit ae1010bfce
24 changed files with 325 additions and 181 deletions

View File

@@ -7,24 +7,20 @@ import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import SkeletonShimmer from "@/components/SkeletonShimmer" import SkeletonShimmer from "@/components/SkeletonShimmer"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./paymentDetails.module.css" import styles from "./paymentDetails.module.css"
export default function PaymentDetails() { export default function PaymentDetails() {
const intl = useIntl() const intl = useIntl()
const { rooms, currencyCode } = useBookingConfirmationStore((state) => ({ const { rooms, formattedTotalCost } = useBookingConfirmationStore(
(state) => ({
rooms: state.rooms, rooms: state.rooms,
currencyCode: state.currencyCode, formattedTotalCost: state.formattedTotalCost,
})) })
)
const hasAllRoomsLoaded = rooms.every((room) => room) const hasAllRoomsLoaded = rooms.every((room) => room)
const grandTotal = rooms.reduce((acc, room) => {
const reservationTotalPrice = room?.totalPrice || 0
return acc + reservationTotalPrice
}, 0)
return ( return (
<div className={styles.details}> <div className={styles.details}>
<Subtitle color="uiTextHighContrast" type="two"> <Subtitle color="uiTextHighContrast" type="two">
@@ -33,11 +29,12 @@ export default function PaymentDetails() {
<div className={styles.payment}> <div className={styles.payment}>
{hasAllRoomsLoaded ? ( {hasAllRoomsLoaded ? (
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
{`${intl.formatMessage({ id: "Total cost" })}: ${formatPrice( {intl.formatMessage(
intl, { id: "Total cost: {amount}" },
grandTotal, {
currencyCode amount: formattedTotalCost,
)}`} }
)}
</Body> </Body>
) : ( ) : (
<SkeletonShimmer width={"100%"} /> <SkeletonShimmer width={"100%"} />

View File

@@ -3,6 +3,7 @@ import React from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons" import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation" import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
@@ -61,14 +62,24 @@ function TableSectionHeader({
export default function PriceDetailsModal() { export default function PriceDetailsModal() {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const { rooms, currencyCode, vat, fromDate, toDate, bookingCode } = const {
useBookingConfirmationStore((state) => ({ rooms,
currencyCode,
vat,
fromDate,
toDate,
bookingCode,
isVatCurrency,
formattedTotalCost,
} = useBookingConfirmationStore((state) => ({
rooms: state.rooms, rooms: state.rooms,
currencyCode: state.currencyCode, currencyCode: state.currencyCode,
vat: state.vat, vat: state.vat,
fromDate: state.fromDate, fromDate: state.fromDate,
toDate: state.toDate, toDate: state.toDate,
bookingCode: state.bookingCode, bookingCode: state.bookingCode,
isVatCurrency: state.isVatCurrency,
formattedTotalCost: state.formattedTotalCost,
})) }))
if (!rooms[0]) { if (!rooms[0]) {
@@ -149,7 +160,7 @@ export default function PriceDetailsModal() {
<Row <Row
bold bold
label={intl.formatMessage({ id: "Room charge" })} label={intl.formatMessage({ id: "Room charge" })}
value={formatPrice(intl, room.roomPrice, currencyCode)} value={room.formattedTotalCost}
/> />
</TableSection> </TableSection>
@@ -200,6 +211,8 @@ export default function PriceDetailsModal() {
})} })}
<TableSection> <TableSection>
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} /> <TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
{isVatCurrency ? (
<>
<Row <Row
label={intl.formatMessage({ id: "Price excluding VAT" })} label={intl.formatMessage({ id: "Price excluding VAT" })}
value={formatPrice(intl, bookingTotal.priceExVat, currencyCode)} value={formatPrice(intl, bookingTotal.priceExVat, currencyCode)}
@@ -208,16 +221,18 @@ export default function PriceDetailsModal() {
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })} label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
value={formatPrice(intl, bookingTotal.vatAmount, currencyCode)} value={formatPrice(intl, bookingTotal.vatAmount, currencyCode)}
/> />
</>
) : null}
<tr className={styles.row}> <tr className={styles.row}>
<td> <td>
<Body textTransform="bold"> <Typography variant="Body/Paragraph/mdBold">
{intl.formatMessage({ id: "Price including VAT" })} <span>{intl.formatMessage({ id: "Price including VAT" })}</span>
</Body> </Typography>
</td> </td>
<td className={styles.price}> <td className={styles.price}>
<Body textTransform="bold"> <Typography variant="Body/Paragraph/mdBold">
{formatPrice(intl, bookingTotal.price, currencyCode)} <span>{formattedTotalCost}</span>
</Body> </Typography>
</td> </td>
</tr> </tr>
{bookingCode && ( {bookingCode && (

View File

@@ -3,6 +3,7 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons" import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { CancellationRuleEnum, ChildBedTypeEnum } from "@/constants/booking" import { CancellationRuleEnum, ChildBedTypeEnum } from "@/constants/booking"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation" import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
@@ -10,8 +11,6 @@ import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import Modal from "@/components/Modal" import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link" import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { formatPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
import RoomSkeletonLoader from "./RoomSkeletonLoader" import RoomSkeletonLoader from "./RoomSkeletonLoader"
@@ -45,29 +44,37 @@ export default function ReceiptRoom({
return ( return (
<article className={styles.room}> <article className={styles.room}>
<header className={styles.roomHeader}> <header className={styles.roomHeader}>
<Body color="uiTextHighContrast">{room.name}</Body> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>{room.name}</p>
</Typography>
{room.rateDefinition.isMemberRate ? ( {room.rateDefinition.isMemberRate ? (
<div className={styles.memberPrice}> <div className={styles.memberPrice}>
<Body color="red"> <Typography variant="Body/Paragraph/mdRegular">
{formatPrice(intl, room.roomPrice, currencyCode)} <p className={styles.red}>{room.formattedTotalCost}</p>
</Body> </Typography>
</div> </div>
) : ( ) : (
<Body color="uiTextHighContrast"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{formatPrice(intl, room.roomPrice, currencyCode)} {formatPrice(intl, room.roomPrice, currencyCode)}
</Body> </p>
</Typography>
)} )}
<Caption color="uiTextMediumContrast"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextMediumContrast}>
{intl.formatMessage( {intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" }, { id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{ {
totalAdults: room.adults, totalAdults: room.adults,
} }
)} )}
</Caption> </p>
<Caption color="uiTextMediumContrast"> </Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextMediumContrast}>
{room.rateDefinition.cancellationText} {room.rateDefinition.cancellationText}
</Caption> </p>
</Typography>
<Modal <Modal
trigger={ trigger={
<Button intent="text" className={styles.termsLink}> <Button intent="text" className={styles.termsLink}>
@@ -83,7 +90,11 @@ export default function ReceiptRoom({
</Link> </Link>
</Button> </Button>
} }
title={room.rateDefinition.cancellationText || ""} title={
(room.roomPoints
? room.rateDefinition.title
: room.rateDefinition.cancellationText) || ""
}
subtitle={ subtitle={
room.rateDefinition.cancellationRule === room.rateDefinition.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM CancellationRuleEnum.CancellableBefore6PM
@@ -93,11 +104,12 @@ export default function ReceiptRoom({
> >
<div className={styles.terms}> <div className={styles.terms}>
{room.rateDefinition.generalTerms?.map((info) => ( {room.rateDefinition.generalTerms?.map((info) => (
<Body <Typography
key={info} key={info}
color="uiTextHighContrast"
className={styles.termsText} className={styles.termsText}
variant="Body/Paragraph/mdRegular"
> >
<span>
<MaterialIcon <MaterialIcon
icon="check" icon="check"
color="Icon/Feedback/Success" color="Icon/Feedback/Success"
@@ -105,7 +117,8 @@ export default function ReceiptRoom({
className={styles.termsIcon} className={styles.termsIcon}
/> />
{info} {info}
</Body> </span>
</Typography>
))} ))}
</div> </div>
</Modal> </Modal>
@@ -114,70 +127,98 @@ export default function ReceiptRoom({
? room.roomFeatures.map((feature) => ( ? room.roomFeatures.map((feature) => (
<div className={styles.entry} key={feature.code}> <div className={styles.entry} key={feature.code}>
<div> <div>
<Body color="uiTextHighContrast">{feature.description}</Body> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{feature.description}
</p>
</Typography>
</div> </div>
<Body color="uiTextHighContrast"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{formatPrice(intl, feature.totalPrice, feature.currency)} {formatPrice(intl, feature.totalPrice, feature.currency)}
</Body> </p>
</Typography>
</div> </div>
)) ))
: null} : null}
<div className={styles.entry}> <div className={styles.entry}>
<Body color="uiTextHighContrast">{room.bedDescription}</Body> <Typography variant="Body/Paragraph/mdRegular">
<Body color="uiTextHighContrast"> <p className={styles.uiTextHighContrast}>{room.bedDescription}</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{formatPrice(intl, 0, currencyCode)} {formatPrice(intl, 0, currencyCode)}
</Body> </p>
</Typography>
</div> </div>
{childBedCrib ? ( {childBedCrib ? (
<div className={styles.entry}> <div className={styles.entry}>
<div> <div>
<Body color="uiTextHighContrast"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{intl.formatMessage( {intl.formatMessage(
{ id: "Crib (child) × {count}" }, { id: "Crib (child) × {count}" },
{ count: childBedCrib.quantity } { count: childBedCrib.quantity }
)} )}
</Body> </p>
<Caption color="uiTextMediumContrast"> </Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p className={styles.uiTextHighContrast}>
{intl.formatMessage({ id: "Based on availability" })} {intl.formatMessage({ id: "Based on availability" })}
</Caption> </p>
</Typography>
</div> </div>
<Body color="uiTextHighContrast"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{formatPrice(intl, 0, currencyCode)} {formatPrice(intl, 0, currencyCode)}
</Body> </p>
</Typography>
</div> </div>
) : null} ) : null}
{childBedExtraBed ? ( {childBedExtraBed ? (
<div className={styles.entry}> <div className={styles.entry}>
<div> <div>
<Body color="uiTextHighContrast"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{intl.formatMessage( {intl.formatMessage(
{ id: "Extra bed (child) × {count}" }, { id: "Extra bed (child) × {count}" },
{ {
count: childBedExtraBed.quantity, count: childBedExtraBed.quantity,
} }
)} )}
</Body> </p>
</Typography>
</div> </div>
<Body color="uiTextHighContrast"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{formatPrice(intl, 0, currencyCode)} {formatPrice(intl, 0, currencyCode)}
</Body> </p>
</Typography>
</div> </div>
) : null} ) : null}
{room.breakfast || breakfastIncluded ? ( {room.breakfast || breakfastIncluded ? (
<div className={styles.entry}> <div className={styles.entry}>
<Body>{intl.formatMessage({ id: "Breakfast buffet" })}</Body> <Typography variant="Body/Paragraph/mdRegular">
<p>{intl.formatMessage({ id: "Breakfast buffet" })}</p>
</Typography>
{breakfastIncluded ? ( {breakfastIncluded ? (
<Body color="red">{intl.formatMessage({ id: "Included" })}</Body> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.red}>
{intl.formatMessage({ id: "Included" })}
</p>
</Typography>
) : null} ) : null}
{room.breakfast && !breakfastIncluded ? ( {room.breakfast && !breakfastIncluded ? (
<Body color="uiTextHighContrast"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.uiTextHighContrast}>
{formatPrice( {formatPrice(
intl, intl,
room.breakfast.totalPrice, room.breakfast.totalPrice,
room.breakfast.currency room.breakfast.currency
)} )}
</Body> </p>
</Typography>
) : null} ) : null}
</div> </div>
) : null} ) : null}

View File

@@ -40,3 +40,15 @@
.terms .termsIcon { .terms .termsIcon {
padding-right: var(--Spacing-x1); padding-right: var(--Spacing-x1);
} }
.red {
color: var(--Scandic-Brand-Scandic-Red);
}
.uiTextHighContrast {
color: var(--UI-Text-High-contrast);
}
.uiTextMediumContrast {
color: var(--UI-Text-Medium-contrast);
}

View File

@@ -2,12 +2,12 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation" import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import SkeletonShimmer from "@/components/SkeletonShimmer" import SkeletonShimmer from "@/components/SkeletonShimmer"
import Divider from "@/components/TempDesignSystem/Divider" import Divider from "@/components/TempDesignSystem/Divider"
import Body from "@/components/TempDesignSystem/Text/Body"
import { formatPrice } from "@/utils/numberFormatting"
import PriceDetailsModal from "../../PriceDetailsModal" import PriceDetailsModal from "../../PriceDetailsModal"
@@ -15,29 +15,27 @@ import styles from "./totalPrice.module.css"
export default function TotalPrice() { export default function TotalPrice() {
const intl = useIntl() const intl = useIntl()
const { rooms, currencyCode } = useBookingConfirmationStore((state) => ({ const { rooms, formattedTotalCost } = useBookingConfirmationStore(
(state) => ({
rooms: state.rooms, rooms: state.rooms,
currencyCode: state.currencyCode, formattedTotalCost: state.formattedTotalCost,
})) })
)
const hasAllRoomsLoaded = rooms.every((room) => room) const hasAllRoomsLoaded = rooms.every((room) => room)
const grandTotal = rooms.reduce((acc, room) => {
const reservationTotalPrice = room?.totalPrice || 0
return acc + reservationTotalPrice
}, 0)
return ( return (
<> <>
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />
<div className={styles.price}> <div className={styles.price}>
<div className={styles.entry}> <div className={styles.entry}>
<Body textTransform="bold"> <Typography variant="Body/Paragraph/mdBold">
{intl.formatMessage({ id: "Total price" })} <p>{intl.formatMessage({ id: "Total price" })}</p>
</Body> </Typography>
{hasAllRoomsLoaded ? ( {hasAllRoomsLoaded ? (
<Body textTransform="bold"> <Typography variant="Body/Paragraph/mdBold">
{formatPrice(intl, grandTotal, currencyCode)} <p>{formattedTotalCost}</p>
</Body> </Typography>
) : ( ) : (
<SkeletonShimmer width={"25%"} /> <SkeletonShimmer width={"25%"} />
)} )}

View File

@@ -1,11 +1,13 @@
"use client" "use client"
import { useEffect } from "react" import { useEffect } from "react"
import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client" import { trpc } from "@/lib/trpc/client"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation" import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import { mapRoomState } from "../../utils" import { mapRoomState } from "../../utils"
import Room from "../Room" import Room from "../Room"
@@ -25,14 +27,36 @@ export function LinkedReservation({
confirmationNumber, confirmationNumber,
lang, lang,
}) })
const setRoom = useBookingConfirmationStore((state) => state.actions.setRoom) const { setRoom, setFormattedTotalCost, currencyCode, totalBookingPrice } =
useBookingConfirmationStore((state) => ({
setRoom: state.actions.setRoom,
setFormattedTotalCost: state.actions.setFormattedTotalCost,
currencyCode: state.currencyCode,
totalBookingPrice: state.totalBookingPrice,
}))
const intl = useIntl()
useEffect(() => { useEffect(() => {
if (data?.room) { if (data?.room) {
const roomData = mapRoomState(data.booking, data.room) const roomData = mapRoomState(data.booking, data.room, intl)
setRoom(roomData, roomIndex) setRoom(roomData, roomIndex)
const formattedTotalCost = formatPrice(
intl,
totalBookingPrice,
currencyCode
)
setFormattedTotalCost(formattedTotalCost)
} }
}, [data, roomIndex, setRoom]) }, [
data,
roomIndex,
setRoom,
intl,
totalBookingPrice,
currencyCode,
setFormattedTotalCost,
])
if (isLoading) { if (isLoading) {
return <LinkedReservationCardSkeleton /> return <LinkedReservationCardSkeleton />

View File

@@ -10,6 +10,7 @@ import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms" import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
import SidePanel from "@/components/HotelReservation/SidePanel" import SidePanel from "@/components/HotelReservation/SidePanel"
import Divider from "@/components/TempDesignSystem/Divider" import Divider from "@/components/TempDesignSystem/Divider"
import { getIntl } from "@/i18n"
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider" import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
import Alerts from "./Alerts" import Alerts from "./Alerts"
@@ -38,6 +39,7 @@ export default async function BookingConfirmation({
`${booking.confirmationNumber},${booking.guest.lastName}` `${booking.confirmationNumber},${booking.guest.lastName}`
) )
const intl = await getIntl()
return ( return (
<BookingConfirmationProvider <BookingConfirmationProvider
bookingCode={booking.bookingCode} bookingCode={booking.bookingCode}
@@ -45,7 +47,7 @@ export default async function BookingConfirmation({
fromDate={booking.checkInDate} fromDate={booking.checkInDate}
toDate={booking.checkOutDate} toDate={booking.checkOutDate}
rooms={[ rooms={[
mapRoomState(booking, room), mapRoomState(booking, room, intl),
// null represents "known but not yet fetched rooms" and is used to render placeholders correctly // null represents "known but not yet fetched rooms" and is used to render placeholders correctly
...Array(booking.linkedReservations.length).fill(null), ...Array(booking.linkedReservations.length).fill(null),
]} ]}

View File

@@ -1,10 +1,16 @@
import { formatPrice } from "@/utils/numberFormatting"
import type { IntlShape } from "react-intl"
import type { BookingConfirmationRoom } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" import type { BookingConfirmationRoom } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { CurrencyEnum } from "@/types/enums/currency"
import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation" import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation"
export function mapRoomState( export function mapRoomState(
booking: BookingConfirmationSchema, booking: BookingConfirmationSchema,
room: BookingConfirmationRoom room: BookingConfirmationRoom,
intl: IntlShape
) { ) {
const breakfast = booking.packages.find( const breakfast = booking.packages.find(
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
@@ -13,6 +19,21 @@ export function mapRoomState(
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST (pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
) )
let formattedTotalCost = formatPrice(
intl,
booking.totalPrice,
booking.currencyCode
)
if (booking.roomPoints) {
formattedTotalCost = formatPrice(
intl,
booking.roomPoints,
CurrencyEnum.POINTS,
booking.totalPrice,
booking.currencyCode
)
}
return { return {
adults: booking.adults, adults: booking.adults,
bedDescription: room.bedType.description, bedDescription: room.bedType.description,
@@ -23,11 +44,13 @@ export function mapRoomState(
childBedPreferences: booking.childBedPreferences, childBedPreferences: booking.childBedPreferences,
confirmationNumber: booking.confirmationNumber, confirmationNumber: booking.confirmationNumber,
currencyCode: booking.currencyCode, currencyCode: booking.currencyCode,
formattedTotalCost,
fromDate: booking.checkInDate, fromDate: booking.checkInDate,
name: room.name, name: room.name,
packages: booking.packages, packages: booking.packages,
rateDefinition: booking.rateDefinition, rateDefinition: booking.rateDefinition,
roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"), roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"),
roomPoints: booking.roomPoints,
roomPrice: booking.roomPrice, roomPrice: booking.roomPrice,
roomTypeCode: booking.roomTypeCode, roomTypeCode: booking.roomTypeCode,
toDate: booking.checkOutDate, toDate: booking.checkOutDate,

View File

@@ -9,7 +9,7 @@ import { formId } from "@/components/HotelReservation/EnterDetails/Payment/Payme
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPriceWithAdditionalPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
import styles from "./bottomSheet.module.css" import styles from "./bottomSheet.module.css"
@@ -57,7 +57,7 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
> >
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption> <Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
<Subtitle> <Subtitle>
{formatPriceWithAdditionalPrice( {formatPrice(
intl, intl,
totalPrice.local.price, totalPrice.local.price,
totalPrice.local.currency, totalPrice.local.currency,

View File

@@ -16,10 +16,7 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { import { formatPrice } from "@/utils/numberFormatting"
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import PriceDetailsTable from "./PriceDetailsTable" import PriceDetailsTable from "./PriceDetailsTable"
@@ -202,7 +199,7 @@ export default function SummaryUI({
memberPrice.amount, memberPrice.amount,
memberPrice.currency memberPrice.currency
) )
: formatPriceWithAdditionalPrice( : formatPrice(
intl, intl,
room.roomPrice.perStay.local.price, room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency, room.roomPrice.perStay.local.currency,
@@ -419,7 +416,7 @@ export default function SummaryUI({
</div> </div>
<div> <div>
<Body textTransform="bold" data-testid="total-price"> <Body textTransform="bold" data-testid="total-price">
{formatPriceWithAdditionalPrice( {formatPrice(
intl, intl,
totalPrice.local.price, totalPrice.local.price,
totalPrice.local.currency, totalPrice.local.currency,
@@ -444,9 +441,7 @@ export default function SummaryUI({
value: formatPrice( value: formatPrice(
intl, intl,
totalPrice.requested.price, totalPrice.requested.price,
totalPrice.requested.currency, totalPrice.requested.currency
totalPrice.requested.additionalPrice,
totalPrice.requested.additionalPriceCurrency
), ),
} }
)} )}

View File

@@ -15,10 +15,7 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { import { formatPrice } from "@/utils/numberFormatting"
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import { isBookingCodeRate } from "./isBookingCodeRate" import { isBookingCodeRate } from "./isBookingCodeRate"
import PriceDetailsTable from "./PriceDetailsTable" import PriceDetailsTable from "./PriceDetailsTable"
@@ -160,7 +157,7 @@ export default function Summary({
<div className={styles.entry}> <div className={styles.entry}>
<Body color="uiTextHighContrast">{room.roomType}</Body> <Body color="uiTextHighContrast">{room.roomType}</Body>
<Body color={showDiscounted ? "red" : "uiTextHighContrast"}> <Body color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPriceWithAdditionalPrice( {formatPrice(
intl, intl,
room.roomPrice.perStay.local.price, room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency, room.roomPrice.perStay.local.currency,
@@ -280,7 +277,7 @@ export default function Summary({
textTransform="bold" textTransform="bold"
data-testid="total-price" data-testid="total-price"
> >
{formatPriceWithAdditionalPrice( {formatPrice(
intl, intl,
totalPrice.local.price, totalPrice.local.price,
totalPrice.local.currency, totalPrice.local.currency,

View File

@@ -7,7 +7,7 @@ import { useRatesStore } from "@/stores/select-rate"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPriceWithAdditionalPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
import { isBookingCodeRate } from "./isBookingCodeRate" import { isBookingCodeRate } from "./isBookingCodeRate"
import { mapRate } from "./mapRate" import { mapRate } from "./mapRate"
@@ -107,7 +107,7 @@ export default function MobileSummary({
color={showDiscounted ? "red" : "uiTextHighContrast"} color={showDiscounted ? "red" : "uiTextHighContrast"}
className={styles.wrappedText} className={styles.wrappedText}
> >
{formatPriceWithAdditionalPrice( {formatPrice(
intl, intl,
totalPriceToShow.local.price, totalPriceToShow.local.price,
totalPriceToShow.local.currency, totalPriceToShow.local.currency,

View File

@@ -13,10 +13,7 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { import { formatPrice } from "@/utils/numberFormatting"
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import MobileSummary from "./MobileSummary" import MobileSummary from "./MobileSummary"
import { import {
@@ -299,7 +296,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
color={showDiscounted ? "red" : "uiTextHighContrast"} color={showDiscounted ? "red" : "uiTextHighContrast"}
textAlign="right" textAlign="right"
> >
{formatPriceWithAdditionalPrice( {formatPrice(
intl, intl,
totalPriceToShow.local.price, totalPriceToShow.local.price,
totalPriceToShow.local.currency, totalPriceToShow.local.currency,
@@ -342,7 +339,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
{intl.formatMessage({ id: "Total price" })} {intl.formatMessage({ id: "Total price" })}
</Caption> </Caption>
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}> <Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPriceWithAdditionalPrice( {formatPrice(
intl, intl,
totalPriceToShow.local.price, totalPriceToShow.local.price,
totalPriceToShow.local.currency, totalPriceToShow.local.currency,

View File

@@ -806,7 +806,7 @@
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.",
"Too many failed attempts.": "Too many failed attempts.", "Too many failed attempts.": "Too many failed attempts.",
"Total": "Total", "Total": "Total",
"Total cost": "Total cost", "Total cost: {amount}": "Total cost: {amount}",
"Total paid": "Total betalt", "Total paid": "Total betalt",
"Total points": "Samlet antal point", "Total points": "Samlet antal point",
"Total price": "Samlet pris", "Total price": "Samlet pris",

View File

@@ -804,7 +804,7 @@
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.",
"Too many failed attempts.": "Too many failed attempts.", "Too many failed attempts.": "Too many failed attempts.",
"Total": "Gesamt", "Total": "Gesamt",
"Total cost": "Total cost", "Total cost: {amount}": "Total cost: {amount}",
"Total paid": "Gesamt bezahlt", "Total paid": "Gesamt bezahlt",
"Total points": "Gesamtpunktzahl", "Total points": "Gesamtpunktzahl",
"Total price": "Gesamtpreis", "Total price": "Gesamtpreis",

View File

@@ -806,7 +806,7 @@
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.",
"Too many failed attempts.": "Too many failed attempts.", "Too many failed attempts.": "Too many failed attempts.",
"Total": "Total", "Total": "Total",
"Total cost": "Total cost", "Total cost: {amount}": "Total cost: {amount}",
"Total paid": "Total paid", "Total paid": "Total paid",
"Total points": "Total points", "Total points": "Total points",
"Total price": "Total price", "Total price": "Total price",

View File

@@ -804,7 +804,7 @@
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.",
"Too many failed attempts.": "Too many failed attempts.", "Too many failed attempts.": "Too many failed attempts.",
"Total": "Kokonais", "Total": "Kokonais",
"Total cost": "Total cost", "Total cost: {amount}": "Total cost: {amount}",
"Total paid": "Kokonais maksamasi", "Total paid": "Kokonais maksamasi",
"Total points": "Kokonaispisteet", "Total points": "Kokonaispisteet",
"Total price": "Kokonaishinta", "Total price": "Kokonaishinta",

View File

@@ -801,7 +801,7 @@
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.",
"Too many failed attempts.": "Too many failed attempts.", "Too many failed attempts.": "Too many failed attempts.",
"Total": "Total", "Total": "Total",
"Total cost": "Total cost", "Total cost: {amount}": "Total cost: {amount}",
"Total incl VAT": "Sum inkl mva", "Total incl VAT": "Sum inkl mva",
"Total paid": "Total betalt", "Total paid": "Total betalt",
"Total points": "Totale poeng", "Total points": "Totale poeng",
@@ -877,7 +877,7 @@
"When": "Når", "When": "Når",
"When guaranteeing your booking with a credit card, we will hold the booking until 07:00 the day after check-in.": "Når vi garanterer bestillingen med kredittkort, vil vi holde bestillingen til kl. 07.00 dagen etter innsjekking.", "When guaranteeing your booking with a credit card, we will hold the booking until 07:00 the day after check-in.": "Når vi garanterer bestillingen med kredittkort, vil vi holde bestillingen til kl. 07.00 dagen etter innsjekking.",
"When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din reservasjon, vil vi holde reservasjonen til 07:00 til dagen etter check-in. Dette vil gi deg som gjest tilføjet fleksibilitet for check-in-tider.", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din reservasjon, vil vi holde reservasjonen til 07:00 til dagen etter check-in. Dette vil gi deg som gjest tilføjet fleksibilitet for check-in-tider.",
"When you confirm the booking the room will be guaranteed for late arrival. If you fail to arrive without cancelling in advance or if you cancel after 18:00 local time, you will be charged for one reward night.": "", "When you confirm the booking the room will be guaranteed for late arrival. If you fail to arrive without cancelling in advance or if you cancel after 18:00 local time, you will be charged for one reward night.": "When you confirm the booking the room will be guaranteed for late arrival. If you fail to arrive without cancelling in advance or if you cancel after 18:00 local time, you will be charged for one reward night.",
"Where should you go next?": "Hvor ønsker du å reise neste gang?", "Where should you go next?": "Hvor ønsker du å reise neste gang?",
"Where to?": "Hvor skal du?", "Where to?": "Hvor skal du?",
"Which room class suits you the best?": "Hvilken romklasse passer deg best?", "Which room class suits you the best?": "Hvilken romklasse passer deg best?",

View File

@@ -802,7 +802,7 @@
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.",
"Too many failed attempts.": "Too many failed attempts.", "Too many failed attempts.": "Too many failed attempts.",
"Total": "Totalt", "Total": "Totalt",
"Total cost": "Total cost", "Total cost: {amount}": "Total cost: {amount}",
"Total incl VAT": "Totalt inkl moms", "Total incl VAT": "Totalt inkl moms",
"Total paid": "Total betalt", "Total paid": "Total betalt",
"Total points": "Poäng totalt", "Total points": "Poäng totalt",

View File

@@ -1,12 +1,15 @@
"use client" "use client"
import { useRef } from "react" import { useRef } from "react"
import { useIntl } from "react-intl"
import { createBookingConfirmationStore } from "@/stores/booking-confirmation" import { createBookingConfirmationStore } from "@/stores/booking-confirmation"
import { BookingConfirmationContext } from "@/contexts/BookingConfirmation" import { BookingConfirmationContext } from "@/contexts/BookingConfirmation"
import { formatPrice } from "@/utils/numberFormatting"
import type { BookingConfirmationStore } from "@/types/contexts/booking-confirmation" import type { BookingConfirmationStore } from "@/types/contexts/booking-confirmation"
import { CurrencyEnum } from "@/types/enums/currency"
import type { BookingConfirmationProviderProps } from "@/types/providers/booking-confirmation" import type { BookingConfirmationProviderProps } from "@/types/providers/booking-confirmation"
export default function BookingConfirmationProvider({ export default function BookingConfirmationProvider({
@@ -18,9 +21,29 @@ export default function BookingConfirmationProvider({
rooms, rooms,
vat, vat,
}: BookingConfirmationProviderProps) { }: BookingConfirmationProviderProps) {
const intl = useIntl()
const storeRef = useRef<BookingConfirmationStore>() const storeRef = useRef<BookingConfirmationStore>()
if (!storeRef.current) { if (!storeRef.current) {
const totalBookingPrice = rooms.reduce((acc, room) => {
const reservationTotalPrice = room?.totalPrice || 0
return acc + reservationTotalPrice
}, 0)
let formattedTotalCost = formatPrice(intl, totalBookingPrice, currencyCode)
const totalBookingPoints = rooms.reduce((acc, room) => {
return acc + (room?.roomPoints ?? 0)
}, 0)
let isVatCurrency = true
if (totalBookingPoints) {
isVatCurrency = false
formattedTotalCost = formatPrice(
intl,
totalBookingPoints,
CurrencyEnum.POINTS,
totalBookingPrice,
currencyCode
)
}
const initialData = { const initialData = {
bookingCode, bookingCode,
currencyCode, currencyCode,
@@ -28,6 +51,9 @@ export default function BookingConfirmationProvider({
toDate, toDate,
rooms, rooms,
vat, vat,
isVatCurrency,
formattedTotalCost,
totalBookingPrice,
} }
storeRef.current = createBookingConfirmationStore(initialData) storeRef.current = createBookingConfirmationStore(initialData)

View File

@@ -16,13 +16,24 @@ export function createBookingConfirmationStore(initialState: InitialState) {
fromDate: initialState.fromDate, fromDate: initialState.fromDate,
toDate: initialState.toDate, toDate: initialState.toDate,
vat: initialState.vat, vat: initialState.vat,
formattedTotalCost: initialState.formattedTotalCost,
isVatCurrency: initialState.isVatCurrency,
totalBookingPrice: initialState.totalBookingPrice,
actions: { actions: {
setRoom: (room, idx) => { setRoom: (room, idx) => {
set((state) => { set((state) => {
const rooms = [...state.rooms] const rooms = [...state.rooms]
rooms[idx] = room rooms[idx] = room
const totalBookingPrice = rooms.reduce((acc, room) => {
return acc + (room?.totalPrice ?? 0)
}, 0)
return { rooms } return { rooms, totalBookingPrice }
})
},
setFormattedTotalCost: (updatedFormattedTotalCost: string) => {
set((state) => {
return { ...state, formattedTotalCost: updatedFormattedTotalCost }
}) })
}, },
}, },

View File

@@ -57,7 +57,7 @@ export const useMyStayTotalPriceStore = create<MyStayTotalPriceState>(
}, 0) }, 0)
const totalPoints = newRooms.reduce((sum, r) => { const totalPoints = newRooms.reduce((sum, r) => {
return sum + r.roomPoints return sum + (r.roomPoints ?? 0)
}, 0) }, 0)
return { return {

View File

@@ -22,8 +22,10 @@ export interface Room {
fromDate: Date fromDate: Date
name: string name: string
packages: BookingConfirmation["booking"]["packages"] packages: BookingConfirmation["booking"]["packages"]
formattedTotalCost: string
rateDefinition: BookingConfirmation["booking"]["rateDefinition"] rateDefinition: BookingConfirmation["booking"]["rateDefinition"]
roomFeatures?: PackageSchema[] | null roomFeatures?: PackageSchema[] | null
roomPoints: number
roomPrice: number roomPrice: number
roomTypeCode: string | null roomTypeCode: string | null
toDate: Date toDate: Date
@@ -39,14 +41,23 @@ export interface InitialState {
toDate: Date toDate: Date
currencyCode: string currencyCode: string
vat: number vat: number
isVatCurrency: boolean
formattedTotalCost: string
totalBookingPrice: number
} }
export interface BookingConfirmationState { export interface BookingConfirmationState {
bookingCode: string | null bookingCode: string | null
isVatCurrency: boolean
rooms: (Room | null)[] rooms: (Room | null)[]
currencyCode: string currencyCode: string
vat: number vat: number
fromDate: Date fromDate: Date
toDate: Date toDate: Date
actions: { setRoom: (room: Room, idx: number) => void } formattedTotalCost: string | null
totalBookingPrice: number
actions: {
setRoom: (room: Room, idx: number) => void
setFormattedTotalCost: (updatedFormattedTotalCost: string) => void
}
} }

View File

@@ -14,6 +14,8 @@ export function getSingleDecimal(n: Number | string) {
* @param intl - react-intl object * @param intl - react-intl object
* @param price - number to be formatted * @param price - number to be formatted
* @param currency - currency code * @param currency - currency code
* @param additionalPrice - number (obtained in reward nights and Corporate cheque scenarios)
* @param additionalPriceCurrency - currency code (obtained in reward nights and Corporate cheque scenarios)
* @returns localized and formatted number in string type with currency * @returns localized and formatted number in string type with currency
*/ */
export function formatPrice( export function formatPrice(
@@ -26,21 +28,14 @@ export function formatPrice(
const localizedPrice = intl.formatNumber(price, { const localizedPrice = intl.formatNumber(price, {
minimumFractionDigits: 0, minimumFractionDigits: 0,
}) })
return `${localizedPrice} ${currency} ${additionalPrice ? "+ " + additionalPrice + " " + additionalPriceCurrency : ""}`
let formattedAdditionalPrice: string = ""
if (additionalPrice && additionalPriceCurrency) {
const localizedAdditionalPrice = intl.formatNumber(additionalPrice, {
minimumFractionDigits: 0,
})
formattedAdditionalPrice = ` ${localizedAdditionalPrice} ${additionalPriceCurrency}`
} }
// This will handle redemption and bonus cheque (corporate cheque) scneario with partial payments return `${localizedPrice} ${currency}${formattedAdditionalPrice}`
export function formatPriceWithAdditionalPrice(
intl: IntlShape,
points: number,
pointsCurrency: string,
additionalPrice?: number,
additionalPriceCurrency?: string
) {
const formattedAdditionalPrice =
additionalPrice && additionalPriceCurrency
? `+ ${formatPrice(intl, additionalPrice, additionalPriceCurrency)}`
: ""
return `${formatPrice(intl, points, pointsCurrency)} ${formattedAdditionalPrice}`
} }