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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
import SidePanel from "@/components/HotelReservation/SidePanel"
import Divider from "@/components/TempDesignSystem/Divider"
import { getIntl } from "@/i18n"
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
import Alerts from "./Alerts"
@@ -38,6 +39,7 @@ export default async function BookingConfirmation({
`${booking.confirmationNumber},${booking.guest.lastName}`
)
const intl = await getIntl()
return (
<BookingConfirmationProvider
bookingCode={booking.bookingCode}
@@ -45,7 +47,7 @@ export default async function BookingConfirmation({
fromDate={booking.checkInDate}
toDate={booking.checkOutDate}
rooms={[
mapRoomState(booking, room),
mapRoomState(booking, room, intl),
// null represents "known but not yet fetched rooms" and is used to render placeholders correctly
...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 { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { CurrencyEnum } from "@/types/enums/currency"
import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation"
export function mapRoomState(
booking: BookingConfirmationSchema,
room: BookingConfirmationRoom
room: BookingConfirmationRoom,
intl: IntlShape
) {
const breakfast = booking.packages.find(
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
@@ -13,6 +19,21 @@ export function mapRoomState(
(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 {
adults: booking.adults,
bedDescription: room.bedType.description,
@@ -23,11 +44,13 @@ export function mapRoomState(
childBedPreferences: booking.childBedPreferences,
confirmationNumber: booking.confirmationNumber,
currencyCode: booking.currencyCode,
formattedTotalCost,
fromDate: booking.checkInDate,
name: room.name,
packages: booking.packages,
rateDefinition: booking.rateDefinition,
roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"),
roomPoints: booking.roomPoints,
roomPrice: booking.roomPrice,
roomTypeCode: booking.roomTypeCode,
toDate: booking.checkOutDate,

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,10 +13,7 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import {
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import { formatPrice } from "@/utils/numberFormatting"
import MobileSummary from "./MobileSummary"
import {
@@ -299,7 +296,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
color={showDiscounted ? "red" : "uiTextHighContrast"}
textAlign="right"
>
{formatPriceWithAdditionalPrice(
{formatPrice(
intl,
totalPriceToShow.local.price,
totalPriceToShow.local.currency,
@@ -342,7 +339,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
{intl.formatMessage({ id: "Total price" })}
</Caption>
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPriceWithAdditionalPrice(
{formatPrice(
intl,
totalPriceToShow.local.price,
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.",
"Too many failed attempts.": "Too many failed attempts.",
"Total": "Total",
"Total cost": "Total cost",
"Total cost: {amount}": "Total cost: {amount}",
"Total paid": "Total betalt",
"Total points": "Samlet antal point",
"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.",
"Too many failed attempts.": "Too many failed attempts.",
"Total": "Gesamt",
"Total cost": "Total cost",
"Total cost: {amount}": "Total cost: {amount}",
"Total paid": "Gesamt bezahlt",
"Total points": "Gesamtpunktzahl",
"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.",
"Too many failed attempts.": "Too many failed attempts.",
"Total": "Total",
"Total cost": "Total cost",
"Total cost: {amount}": "Total cost: {amount}",
"Total paid": "Total paid",
"Total points": "Total points",
"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ä.",
"Too many failed attempts.": "Too many failed attempts.",
"Total": "Kokonais",
"Total cost": "Total cost",
"Total cost: {amount}": "Total cost: {amount}",
"Total paid": "Kokonais maksamasi",
"Total points": "Kokonaispisteet",
"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.",
"Too many failed attempts.": "Too many failed attempts.",
"Total": "Total",
"Total cost": "Total cost",
"Total cost: {amount}": "Total cost: {amount}",
"Total incl VAT": "Sum inkl mva",
"Total paid": "Total betalt",
"Total points": "Totale poeng",
@@ -877,7 +877,7 @@
"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, 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 to?": "Hvor skal du?",
"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.",
"Too many failed attempts.": "Too many failed attempts.",
"Total": "Totalt",
"Total cost": "Total cost",
"Total cost: {amount}": "Total cost: {amount}",
"Total incl VAT": "Totalt inkl moms",
"Total paid": "Total betalt",
"Total points": "Poäng totalt",

View File

@@ -1,12 +1,15 @@
"use client"
import { useRef } from "react"
import { useIntl } from "react-intl"
import { createBookingConfirmationStore } from "@/stores/booking-confirmation"
import { BookingConfirmationContext } from "@/contexts/BookingConfirmation"
import { formatPrice } from "@/utils/numberFormatting"
import type { BookingConfirmationStore } from "@/types/contexts/booking-confirmation"
import { CurrencyEnum } from "@/types/enums/currency"
import type { BookingConfirmationProviderProps } from "@/types/providers/booking-confirmation"
export default function BookingConfirmationProvider({
@@ -18,9 +21,29 @@ export default function BookingConfirmationProvider({
rooms,
vat,
}: BookingConfirmationProviderProps) {
const intl = useIntl()
const storeRef = useRef<BookingConfirmationStore>()
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 = {
bookingCode,
currencyCode,
@@ -28,6 +51,9 @@ export default function BookingConfirmationProvider({
toDate,
rooms,
vat,
isVatCurrency,
formattedTotalCost,
totalBookingPrice,
}
storeRef.current = createBookingConfirmationStore(initialData)

View File

@@ -16,13 +16,24 @@ export function createBookingConfirmationStore(initialState: InitialState) {
fromDate: initialState.fromDate,
toDate: initialState.toDate,
vat: initialState.vat,
formattedTotalCost: initialState.formattedTotalCost,
isVatCurrency: initialState.isVatCurrency,
totalBookingPrice: initialState.totalBookingPrice,
actions: {
setRoom: (room, idx) => {
set((state) => {
const rooms = [...state.rooms]
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)
const totalPoints = newRooms.reduce((sum, r) => {
return sum + r.roomPoints
return sum + (r.roomPoints ?? 0)
}, 0)
return {

View File

@@ -22,8 +22,10 @@ export interface Room {
fromDate: Date
name: string
packages: BookingConfirmation["booking"]["packages"]
formattedTotalCost: string
rateDefinition: BookingConfirmation["booking"]["rateDefinition"]
roomFeatures?: PackageSchema[] | null
roomPoints: number
roomPrice: number
roomTypeCode: string | null
toDate: Date
@@ -39,14 +41,23 @@ export interface InitialState {
toDate: Date
currencyCode: string
vat: number
isVatCurrency: boolean
formattedTotalCost: string
totalBookingPrice: number
}
export interface BookingConfirmationState {
bookingCode: string | null
isVatCurrency: boolean
rooms: (Room | null)[]
currencyCode: string
vat: number
fromDate: 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 price - number to be formatted
* @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
*/
export function formatPrice(
@@ -26,21 +28,14 @@ export function formatPrice(
const localizedPrice = intl.formatNumber(price, {
minimumFractionDigits: 0,
})
return `${localizedPrice} ${currency} ${additionalPrice ? "+ " + additionalPrice + " " + additionalPriceCurrency : ""}`
}
// This will handle redemption and bonus cheque (corporate cheque) scneario with partial payments
export function formatPriceWithAdditionalPrice(
intl: IntlShape,
points: number,
pointsCurrency: string,
additionalPrice?: number,
additionalPriceCurrency?: string
) {
const formattedAdditionalPrice =
additionalPrice && additionalPriceCurrency
? `+ ${formatPrice(intl, additionalPrice, additionalPriceCurrency)}`
: ""
let formattedAdditionalPrice: string = ""
if (additionalPrice && additionalPriceCurrency) {
const localizedAdditionalPrice = intl.formatNumber(additionalPrice, {
minimumFractionDigits: 0,
})
formattedAdditionalPrice = ` ${localizedAdditionalPrice} ${additionalPriceCurrency}`
}
return `${formatPrice(intl, points, pointsCurrency)} ${formattedAdditionalPrice}`
return `${localizedPrice} ${currency}${formattedAdditionalPrice}`
}