fix: always use totalPrice to display roomCharge

This commit is contained in:
Simon Emanuelsson
2025-04-16 12:41:37 +02:00
parent 1f94c581ae
commit 722d4505ba
18 changed files with 312 additions and 864 deletions

View File

@@ -1,398 +0,0 @@
"use client"
import { Fragment } from "react"
import { useIntl } from "react-intl"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import IconChip from "@/components/TempDesignSystem/IconChip"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./priceDetailsTable.module.css"
import type { Price } from "@/types/components/hotelReservation/price"
import { CurrencyEnum } from "@/types/enums/currency"
import type { RoomState } from "@/types/stores/enter-details"
function Row({
label,
value,
bold,
}: {
label: string
value: string
bold?: boolean
}) {
return (
<tr className={styles.row}>
<td>
<Caption type={bold ? "bold" : undefined}>{label}</Caption>
</td>
<td className={styles.price}>
<Caption type={bold ? "bold" : undefined}>{value}</Caption>
</td>
</tr>
)
}
function TableSection({ children }: React.PropsWithChildren) {
return <tbody className={styles.tableSection}>{children}</tbody>
}
function TableSectionHeader({
title,
subtitle,
}: {
title: string
subtitle?: string
}) {
return (
<tr>
<th colSpan={2}>
<Body>{title}</Body>
{subtitle ? <Body>{subtitle}</Body> : null}
</th>
</tr>
)
}
export type Room = Pick<
RoomState["room"],
| "adults"
| "bedType"
| "breakfast"
| "childrenInRoom"
| "roomFeatures"
| "roomRate"
| "roomType"
> & {
guest?: RoomState["room"]["guest"]
}
export interface PriceDetailsTableProps {
bookingCode?: string
fromDate: string
isMember: boolean
rooms: Room[]
toDate: string
totalPrice: Price
vat: number
}
export default function PriceDetailsTable({
bookingCode,
fromDate,
isMember,
rooms,
toDate,
totalPrice,
vat,
}: PriceDetailsTableProps) {
const intl = useIntl()
const lang = useLang()
const diff = dt(toDate).diff(fromDate, "days")
const nights = intl.formatMessage(
{
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: diff }
)
const vatPercentage = vat / 100
const vatAmount = totalPrice.local.price * vatPercentage
const priceExclVat = totalPrice.local.price - vatAmount
const duration = ` ${dt(fromDate).locale(lang).format("ddd, D MMM")}
-
${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})`
const noVatCurrencies = [
CurrencyEnum.CC,
CurrencyEnum.POINTS,
CurrencyEnum.Voucher,
]
return (
<table className={styles.priceDetailsTable}>
{rooms.map((room, idx) => {
const isMainRoom = idx === 0
const getMemberRate =
room.guest?.join ||
room.guest?.membershipNo ||
(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
}
const voucherPrice =
"voucher" in room.roomRate ? room.roomRate.voucher : undefined
const chequePrice =
"corporateCheque" in room.roomRate
? room.roomRate.corporateCheque
: undefined
const redemptionPrice =
"redemption" in room.roomRate ? room.roomRate.redemption : undefined
if (!price && !voucherPrice && !chequePrice && !redemptionPrice) {
return null
}
return (
<Fragment key={idx}>
<TableSection>
{rooms.length > 1 && (
<tr>
<th colSpan={2}>
<Body textTransform="bold">
{intl.formatMessage({
defaultMessage: "Room",
})}
{
/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */
" "
}
{idx + 1}
</Body>
</th>
</tr>
)}
<TableSectionHeader title={room.roomType} subtitle={duration} />
{price && (
<>
<Row
label={intl.formatMessage({
defaultMessage: "Average price per night",
})}
value={formatPrice(
intl,
price.localPrice.pricePerNight,
price.localPrice.currency
)}
/>
{room.roomFeatures
? room.roomFeatures.map((feature) => (
<Row
key={feature.code}
label={feature.description}
value={formatPrice(
intl,
+feature.localPrice.totalPrice,
feature.localPrice.currency
)}
/>
))
: null}
{room.bedType ? (
<Row
label={room.bedType.description}
value={formatPrice(intl, 0, price.localPrice.currency)}
/>
) : null}
<Row
bold
label={intl.formatMessage({
defaultMessage: "Room charge",
})}
value={formatPrice(
intl,
price.localPrice.pricePerStay,
price.localPrice.currency
)}
/>
</>
)}
{voucherPrice && (
<Row
bold
label={intl.formatMessage({
defaultMessage: "Room charge",
})}
value={formatPrice(
intl,
voucherPrice.numberOfVouchers,
CurrencyEnum.Voucher
)}
/>
)}
{chequePrice && (
<Row
bold
label={intl.formatMessage({
defaultMessage: "Room charge",
})}
value={formatPrice(
intl,
chequePrice.localPrice.numberOfCheques,
CurrencyEnum.CC,
chequePrice.localPrice.additionalPricePerStay,
chequePrice.localPrice.currency ?? undefined
)}
/>
)}
{redemptionPrice && (
<Row
bold
label={intl.formatMessage({
defaultMessage: "Room charge",
})}
value={formatPrice(
intl,
redemptionPrice.localPrice.pointsPerStay,
CurrencyEnum.POINTS,
redemptionPrice.localPrice.additionalPricePerStay,
redemptionPrice.localPrice.currency ?? undefined
)}
/>
)}
</TableSection>
{room.breakfast ? (
<TableSection>
<Row
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
},
{ totalAdults: room.adults, totalBreakfasts: diff }
)}
value={formatPrice(
intl,
room.breakfast.localPrice.price * room.adults,
room.breakfast.localPrice.currency
)}
/>
{room.childrenInRoom?.length ? (
<Row
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{
totalChildren: room.childrenInRoom.length,
totalBreakfasts: diff,
}
)}
value={formatPrice(
intl,
0,
room.breakfast.localPrice.currency
)}
/>
) : null}
<Row
bold
label={intl.formatMessage({
defaultMessage: "Breakfast charge",
})}
value={formatPrice(
intl,
room.breakfast.localPrice.price * room.adults * diff,
room.breakfast.localPrice.currency
)}
/>
</TableSection>
) : null}
</Fragment>
)
})}
<TableSection>
<TableSectionHeader
title={intl.formatMessage({
defaultMessage: "Total",
})}
/>
{!noVatCurrencies.includes(totalPrice.local.currency) ? (
<>
<Row
label={intl.formatMessage({
defaultMessage: "Price excluding VAT",
})}
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
/>
<Row
label={intl.formatMessage(
{
defaultMessage: "VAT {vat}%",
},
{ vat }
)}
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
/>
</>
) : null}
<tr className={styles.row}>
<td>
<Body textTransform="bold">
{intl.formatMessage({
defaultMessage: "Price including VAT",
})}
</Body>
</td>
<td className={styles.price}>
<Body textTransform="bold">
{formatPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)}
</Body>
</td>
</tr>
{totalPrice.local.regularPrice ? (
<tr className={styles.row}>
<td></td>
<td className={styles.price}>
<Caption color="uiTextMediumContrast" striked={true}>
{formatPrice(
intl,
totalPrice.local.regularPrice,
totalPrice.local.currency
)}
</Caption>
</td>
</tr>
) : null}
{bookingCode && (
<tr className={styles.row}>
<td colSpan={2} align="left">
<Typography variant="Body/Supporting text (caption)/smRegular">
<IconChip
color="blue"
icon={<DiscountIcon color="Icon/Feedback/Information" />}
>
{intl.formatMessage(
{
defaultMessage: "<strong>Booking code</strong>: {value}",
},
{
value: bookingCode,
strong: (text) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>{text}</strong>
</Typography>
),
}
)}
</IconChip>
</Typography>
</td>
</tr>
)}
</TableSection>
</table>
)
}

View File

@@ -48,13 +48,13 @@ export default function SummaryUI({
const intl = useIntl()
const lang = useLang()
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
const nights = intl.formatMessage(
const nightsMsg = intl.formatMessage(
{
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: diff }
{ totalNights: nights }
)
function handleToggleSummary() {
@@ -95,7 +95,7 @@ export default function SummaryUI({
? totalPrice.requested.currency === totalPrice.local.currency
: false
const priceDetailsRooms = mapToPrice(rooms, isMember)
const priceDetailsRooms = mapToPrice(rooms, isMember, nights)
return (
<section className={styles.summary}>
@@ -113,7 +113,7 @@ export default function SummaryUI({
size={15}
/>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nights})
{dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nightsMsg})
</Body>
<Button
onPress={handleToggleSummary}
@@ -399,7 +399,7 @@ export default function SummaryUI({
<Body color="uiTextHighContrast">
{formatPrice(
intl,
room.breakfast.localPrice.price * adults * diff,
room.breakfast.localPrice.price * adults * nights,
room.breakfast.localPrice.currency
)}
</Body>

View File

@@ -1,12 +1,37 @@
import { sumPackages } from "@/components/HotelReservation/utils"
import type { RoomState } from "@/types/stores/enter-details"
export function mapToPrice(rooms: RoomState[], isMember: boolean) {
export function mapToPrice(
rooms: RoomState[],
isMember: boolean,
nights: number
) {
return rooms
.filter((room) => room && room.room.roomRate)
.map(({ room }, idx) => {
const isMainRoom = idx === 0
const pkgsSum = sumPackages(room.roomFeatures)
if ("corporateCheque" in room.roomRate) {
if (
room.roomRate.corporateCheque.localPrice.additionalPricePerStay ||
pkgsSum.price
) {
return {
...room,
packages: room.roomFeatures,
price: {
corporateCheque: {
...room.roomRate.corporateCheque.localPrice,
additionalPricePerStay:
(room.roomRate.corporateCheque.localPrice
.additionalPricePerStay || 0) + pkgsSum.price,
},
},
}
}
return {
...room,
packages: room.roomFeatures,
@@ -17,6 +42,23 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
}
if ("redemption" in room.roomRate) {
if (
room.roomRate.redemption.localPrice.additionalPricePerStay ||
pkgsSum.price
) {
return {
...room,
packages: room.roomFeatures,
price: {
redemption: {
...room.roomRate.redemption.localPrice,
additionalPricePerStay:
(room.roomRate.redemption.localPrice.additionalPricePerStay ||
0) + pkgsSum.price,
},
},
}
}
return {
...room,
packages: room.roomFeatures,
@@ -39,6 +81,23 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
const isMemberRate = !!(room.guest.join || room.guest.membershipNo)
if ((isMember && isMainRoom) || isMemberRate) {
if ("member" in room.roomRate && room.roomRate.member) {
if (pkgsSum.price) {
return {
...room,
packages: room.roomFeatures,
price: {
regular: {
...room.roomRate.member.localPrice,
pricePerNight:
room.roomRate.member.localPrice.pricePerNight +
pkgsSum.price / nights,
pricePerStay:
room.roomRate.member.localPrice.pricePerStay +
pkgsSum.price,
},
},
}
}
return {
...room,
packages: room.roomFeatures,
@@ -50,6 +109,22 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
}
if ("public" in room.roomRate && room.roomRate.public) {
if (pkgsSum.price) {
return {
...room,
packages: room.roomFeatures,
price: {
regular: {
...room.roomRate.public.localPrice,
pricePerNight:
room.roomRate.public.localPrice.pricePerNight +
pkgsSum.price / nights,
pricePerStay:
room.roomRate.public.localPrice.pricePerStay + pkgsSum.price,
},
},
}
}
return {
...room,
packages: room.roomFeatures,