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

@@ -36,10 +36,6 @@ export default function PriceDetails() {
return total
}
const pkgsSum =
room.roomFeatures?.reduce((total, pkg) => total + pkg.totalPrice, 0) ??
0
if (room.cheques) {
// CorporateCheque Booking
total.local.currency = CurrencyEnum.CC
@@ -54,7 +50,7 @@ export default function PriceDetails() {
total.local.price = total.local.price + room.vouchers
} else {
// Price Booking
total.local.price = total.local.price + room.roomPrice + pkgsSum
total.local.price = total.local.price + room.totalPrice
}
if (
@@ -62,7 +58,7 @@ export default function PriceDetails() {
room.roomPrice
) {
total.local.additionalPrice =
(total.local.additionalPrice || 0) + room.roomPrice + pkgsSum
(total.local.additionalPrice || 0) + room.totalPrice
total.local.additionalPriceCurrency = currency
}

View File

@@ -4,6 +4,8 @@ import {
} from "@/server/routers/hotels/schemas/packages"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { PackageTypeEnum } from "@/types/enums/packages"
import type { Package } from "@/types/requests/packages"
import type { Room } from "@/types/stores/booking-confirmation"
@@ -15,16 +17,20 @@ export function mapToPrice(rooms: (Room | null)[], nights: number) {
if (room.cheques) {
price = {
corporateCheque: {
additionalPricePerStay: room.roomPrice ? room.roomPrice : undefined,
currency: room.roomPrice ? room.currencyCode : undefined,
additionalPricePerStay: room.totalPrice
? room.totalPrice
: undefined,
currency: room.totalPrice ? room.currencyCode : undefined,
numberOfCheques: room.cheques,
},
}
} else if (room.roomPoints) {
price = {
redemption: {
additionalPricePerStay: room.roomPrice ? room.roomPrice : undefined,
currency: room.roomPrice ? room.currencyCode : undefined,
additionalPricePerStay: room.totalPrice
? room.totalPrice
: undefined,
currency: room.totalPrice ? room.currencyCode : undefined,
pointsPerNight: room.roomPoints / nights,
pointsPerStay: room.roomPoints,
},
@@ -40,7 +46,7 @@ export function mapToPrice(rooms: (Room | null)[], nights: number) {
regular: {
currency: room.currencyCode,
pricePerNight: room.roomPrice / nights,
pricePerStay: room.roomPrice,
pricePerStay: room.totalPrice,
},
}
}
@@ -53,7 +59,10 @@ export function mapToPrice(rooms: (Room | null)[], nights: number) {
price: room.breakfast?.unitPrice,
totalPrice: room.breakfast?.totalPrice,
},
packageType: room.breakfast?.type,
packageType:
room.breakfast?.code === BreakfastPackageEnum.REGULAR_BREAKFAST
? PackageTypeEnum.BreakfastAdult
: "",
requestedPrice: {
currency: room.breakfast?.currency,
price: room.breakfast?.unitPrice,

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,

View File

@@ -0,0 +1,40 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import { formatPrice } from "@/utils/numberFormatting"
import { CurrencyEnum } from "@/types/enums/currency"
export default function Cheques({
cheques,
price,
}: {
cheques: number
price: number
}) {
const intl = useIntl()
const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode)
if (!cheques) {
return <SkeletonShimmer width={"100px"} />
}
const totalPrice = formatPrice(
intl,
cheques,
CurrencyEnum.CC,
price,
currencyCode
)
return (
<Typography variant="Title/Subtitle/lg">
<p>{totalPrice}</p>
</Typography>
)
}

View File

@@ -16,8 +16,7 @@ import IconChip from "@/components/TempDesignSystem/IconChip"
import useLang from "@/hooks/useLang"
import { IconForFeatureCode } from "../../utils"
import Points from "../Points"
import Price from "../Price"
import PriceType from "../PriceType"
import { hasModifiableRate } from "../utils"
import { hasBreakfastPackageFromBookingFlow } from "../utils/hasBreakfastPackage"
import { mapRoomDetails } from "../utils/mapRoomDetails"
@@ -119,17 +118,18 @@ export default function MultiRoom({
const {
adults,
checkInDate,
cheques,
childrenAges,
confirmationNumber,
cancellationNumber,
hotelId,
roomPoints,
roomPrice,
packages,
rateDefinition,
isCancelled,
priceType,
vouchers,
totalPrice,
} = multiRoom
const fromDate = dt(checkInDate).locale(lang)
@@ -349,26 +349,15 @@ export default function MultiRoom({
})}
</p>
</Typography>
{priceType === "points" ? (
<Points points={roomPoints} variant="Body/Paragraph/mdBold" />
) : priceType === "voucher" ? (
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage(
{
defaultMessage: "{count} voucher",
},
{ count: vouchers }
)}
</p>
</Typography>
) : (
<Price
price={isCancelled ? 0 : roomPrice.perStay.local.price}
variant="Body/Paragraph/mdBold"
isMember={rateDefinition.isMemberRate}
/>
)}
<PriceType
cheques={cheques}
isCancelled={isCancelled}
priceType={priceType}
rateDefinition={rateDefinition}
roomPoints={roomPoints}
totalPrice={totalPrice}
vouchers={vouchers}
/>
</div>
</div>
</div>

View File

@@ -1,330 +0,0 @@
"use client"
import { Fragment } from "react"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
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 type { Room } from "@/stores/my-stay/myStayRoomDetailsStore"
function Row({
label,
value,
bold,
}: {
label: string
value: string
bold?: boolean
}) {
return (
<tr className={styles.row}>
<td>
<Typography
variant={
bold
? "Body/Supporting text (caption)/smBold"
: "Body/Supporting text (caption)/smRegular"
}
>
<span>{label}</span>
</Typography>
</td>
<td className={styles.price}>
<Typography
variant={
bold
? "Body/Supporting text (caption)/smBold"
: "Body/Supporting text (caption)/smRegular"
}
>
<span>{value}</span>
</Typography>
</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}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{title}</span>
</Typography>
</th>
</tr>
{subtitle && (
<tr>
<th colSpan={2}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{subtitle}</span>
</Typography>
</th>
</tr>
)}
</>
)
}
export type RoomPriceDetails = Pick<
Room,
| "adults"
| "bedType"
| "breakfast"
| "childrenInRoom"
| "roomPrice"
| "roomName"
| "packages"
| "isCancelled"
> & {
guest?: Room["guest"]
}
export interface PriceDetailsTableProps {
bookingCode?: string | null
fromDate: string
bookedRoom: RoomPriceDetails
linkedReservationRooms: RoomPriceDetails[]
toDate: string
totalPrice: Price
vat: number
}
export default function PriceDetailsTable({
bookingCode,
fromDate,
bookedRoom,
linkedReservationRooms,
toDate,
totalPrice,
vat,
}: PriceDetailsTableProps) {
const intl = useIntl()
const lang = useLang()
const rooms = [bookedRoom, ...linkedReservationRooms].filter(
(room) => !room.isCancelled
)
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})`
return (
<table className={styles.priceDetailsTable}>
{rooms.map((room, idx) => {
const totalAdultsMsg = intl.formatMessage(
{
defaultMessage:
"{totalAdults, plural, one {# adult} other {# adults}}",
},
{
totalAdults: room.adults,
}
)
const totalChildrenMsg = intl.formatMessage(
{
defaultMessage:
"{totalChildren, plural, one {# child} other {# children}}",
},
{
totalChildren: room.childrenInRoom.length,
}
)
const guestsCountMsg = [totalAdultsMsg]
if (room.childrenInRoom.length) {
guestsCountMsg.push(totalChildrenMsg)
}
return (
<Fragment key={idx}>
<TableSection>
{rooms.length > 1 && (
<tr>
<td>
<Typography variant="Body/Paragraph/mdBold">
<span>
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: idx + 1 }
)}
</span>
</Typography>
</td>
</tr>
)}
<TableSectionHeader title={room.roomName} subtitle={duration} />
<Row
label={intl.formatMessage({
defaultMessage: "Average price per night",
})}
value={formatPrice(
intl,
room.roomPrice.perNight.local.price,
room.roomPrice.perNight.local.currency
)}
/>
{room.packages
? room.packages.map((feature) => (
<Row
key={feature.code}
label={feature.description}
value={formatPrice(
intl,
+feature.localPrice.totalPrice,
feature.localPrice.currency
)}
/>
))
: null}
<Row
bold
label={intl.formatMessage({
defaultMessage: "Room charge",
})}
value={formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency
)}
/>
</TableSection>
{room.breakfast ? (
<TableSection>
<Row
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({guestsCount}) x {totalBreakfasts}",
},
{
guestsCount: guestsCountMsg.join(", "),
totalBreakfasts: diff,
}
)}
value={formatPrice(
intl,
room.breakfast.localPrice.totalPrice,
room.breakfast.localPrice.currency
)}
/>
<Row
bold
label={intl.formatMessage({
defaultMessage: "Breakfast charge",
})}
value={formatPrice(
intl,
room.breakfast.localPrice.totalPrice,
room.breakfast.localPrice.currency
)}
/>
</TableSection>
) : null}
</Fragment>
)
})}
<TableSection>
<TableSectionHeader
title={intl.formatMessage({
defaultMessage: "Total",
})}
/>
<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)}
/>
<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
)}
</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>
)}
{bookingCode && totalPrice.local.regularPrice && (
<tr className={styles.row}>
<td>
<MaterialIcon icon="sell" />
{bookingCode}
</td>
<td></td>
</tr>
)}
</TableSection>
</table>
)
}

View File

@@ -1,7 +1,5 @@
import { dt } from "@/lib/dt"
import { sumPackages } from "@/components/HotelReservation/utils"
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import type { Price } from "@/types/components/hotelReservation/price"
import { CurrencyEnum } from "@/types/enums/currency"
@@ -12,7 +10,7 @@ export function mapToPrice(room: Room) {
case PriceTypeEnum.cheque:
return {
corporateCheque: {
additionalPricePerStay: room.roomPrice.perStay.local.price,
additionalPricePerStay: room.totalPrice,
currency: room.roomPrice.perStay.local.currency,
numberOfCheques: room.cheques,
},
@@ -21,8 +19,8 @@ export function mapToPrice(room: Room) {
return {
regular: {
currency: room.currencyCode,
pricePerNight: room.roomPrice.perNight,
pricePerStay: room.roomPrice.perStay,
pricePerNight: room.roomPrice.perNight.local.price,
pricePerStay: room.totalPrice,
},
}
case PriceTypeEnum.points:
@@ -31,8 +29,8 @@ export function mapToPrice(room: Room) {
.diff(dt(room.checkInDate).startOf("day"), "days")
return {
redemption: {
additionalPricePerStay: room.roomPrice.perStay.local.price,
currency: room.roomPrice.perStay.local.currency,
additionalPricePerStay: room.totalPrice,
currency: room.currencyCode,
pointsPerNight: room.roomPoints / nights,
pointsPerStay: room.roomPoints,
},
@@ -51,11 +49,6 @@ export function mapToPrice(room: Room) {
export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) {
return rooms.reduce<Price>(
(total, room) => {
const pkgsSum = sumPackages(room.packages)
let breakfastPrice = 0
if (room.breakfast && !room.rateDefinition.breakfastIncluded) {
breakfastPrice = room.breakfast.localPrice.totalPrice
}
switch (room.priceType) {
case PriceTypeEnum.cheque:
{
@@ -65,11 +58,7 @@ export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) {
break
case PriceTypeEnum.money:
{
total.local.price =
total.local.price +
room.roomPrice.perStay.local.price +
pkgsSum.price +
breakfastPrice
total.local.price = total.local.price + room.totalPrice
if (!total.local.currency) {
total.local.currency = room.currencyCode
@@ -93,17 +82,14 @@ export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) {
case PriceTypeEnum.points:
case PriceTypeEnum.voucher:
{
if (room.roomPrice.perStay.local.price || pkgsSum) {
if (room.totalPrice) {
total.local.additionalPrice =
room.roomPrice.perStay.local.price +
pkgsSum.price +
breakfastPrice
(total.local.additionalPrice || 0) + room.totalPrice
}
if (!total.local.additionalPriceCurrency) {
if (room.roomPrice.perStay.local.currency) {
total.local.additionalPriceCurrency =
room.roomPrice.perStay.local.currency
if (room.currencyCode) {
total.local.additionalPriceCurrency = room.currencyCode
} else {
total.local.additionalPriceCurrency = currency
}

View File

@@ -0,0 +1,62 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import Cheques from "./Cheques"
import Points from "./Points"
import Price from "./Price"
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface PriceTypeProps
extends Pick<
BookingConfirmation["booking"],
"cheques" | "rateDefinition" | "roomPoints" | "totalPrice" | "vouchers"
> {
isCancelled: boolean
priceType: PriceTypeEnum
}
export default function PriceType({
cheques,
isCancelled,
priceType,
rateDefinition,
roomPoints,
totalPrice,
vouchers,
}: PriceTypeProps) {
const intl = useIntl()
switch (priceType) {
case PriceTypeEnum.cheque:
return <Cheques cheques={cheques} price={isCancelled ? 0 : totalPrice} />
case PriceTypeEnum.money:
return (
<Price
isMember={rateDefinition.isMemberRate}
price={isCancelled ? 0 : totalPrice}
variant="Title/Subtitle/lg"
/>
)
case PriceTypeEnum.points:
return <Points points={roomPoints} variant="Title/Subtitle/lg" />
case PriceTypeEnum.voucher:
return (
<Typography variant="Title/Subtitle/lg">
<p>
{intl.formatMessage(
{
defaultMessage: "{count} voucher",
},
{ count: vouchers }
)}
</p>
</Typography>
)
default:
return null
}
}

View File

@@ -17,9 +17,8 @@ import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import GuestDetails from "../GuestDetails"
import Points from "../Points"
import Price from "../Price"
import PriceDetails from "../PriceDetails"
import PriceType from "../PriceType"
import { hasModifiableRate } from "../utils"
import ToggleSidePeek from "./ToggleSidePeek"
@@ -58,16 +57,17 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
const {
adults,
bookingCode,
breakfast,
cheques,
childrenAges,
confirmationNumber,
bookingCode,
roomPrice,
roomPoints,
breakfast,
packages,
rateDefinition,
isCancelled,
packages,
priceType,
rateDefinition,
roomPoints,
totalPrice,
vouchers,
} = bookedRoom
@@ -419,26 +419,15 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
})}
</p>
</Typography>
{priceType === "points" ? (
<Points points={roomPoints} variant="Title/Subtitle/lg" />
) : priceType === "voucher" ? (
<Typography variant="Title/Subtitle/lg">
<p>
{intl.formatMessage(
{
defaultMessage: "{count} voucher",
},
{ count: vouchers }
)}
</p>
</Typography>
) : (
<Price
price={isCancelled ? 0 : roomPrice.perStay.local.price}
variant="Title/Subtitle/lg"
isMember={rateDefinition.isMemberRate}
/>
)}
<PriceType
cheques={cheques}
isCancelled={isCancelled}
priceType={priceType}
rateDefinition={rateDefinition}
roomPoints={roomPoints}
totalPrice={totalPrice}
vouchers={vouchers}
/>
</div>
</div>
</div>

View File

@@ -1,12 +1,12 @@
import type { PriceType } from "@/types/components/hotelReservation/myStay/myStay"
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
export function getPriceType(
cheques: number,
points: number,
vouchers: number
): PriceType {
if (points > 0) return "points"
if (vouchers > 0) return "voucher"
if (cheques > 0) return "cheque"
return "money"
): PriceTypeEnum {
if (points > 0) return PriceTypeEnum.points
if (vouchers > 0) return PriceTypeEnum.voucher
if (cheques > 0) return PriceTypeEnum.cheque
return PriceTypeEnum.money
}

View File

@@ -153,6 +153,9 @@ export function mapRoomDetails({
requested: undefined,
},
},
totalPriceExVat: booking.totalPriceExVat,
totalPrice: booking.totalPrice,
vatAmount: booking.vatAmount,
breakfast,
priceType,
}

View File

@@ -1,7 +1,6 @@
"use client"
import { useIntl } from "react-intl"
import { sumPackages } from "@/components/HotelReservation/utils"
import { formatPrice } from "@/utils/numberFormatting"
import BoldRow from "../Bold"
@@ -43,21 +42,14 @@ export default function CorporateChequePrice({
defaultMessage: "Average price per night",
})
const pkgsSum = sumPackages(packages)
const roomAdditionalPrice = price.additionalPricePerStay
let additionalPricePerStay
if (roomAdditionalPrice) {
additionalPricePerStay = roomAdditionalPrice + pkgsSum.price
} else if (pkgsSum.price) {
additionalPricePerStay = pkgsSum.price
}
const additionalPricePerStay = price.additionalPricePerStay
const averageChequesPerNight = price.numberOfCheques / nights
const averageAdditionalPricePerNight = roomAdditionalPrice
? Math.ceil(roomAdditionalPrice / nights)
const averageAdditionalPricePerNight = additionalPricePerStay
? Math.ceil(additionalPricePerStay / nights)
: null
const additionalCurrency = price.currency ?? pkgsSum.currency
const additionalCurrency = price.currency ?? currency
let averagePricePerNight = `${averageChequesPerNight} ${CurrencyEnum.CC}`
if (averageAdditionalPricePerNight) {
averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}`

View File

@@ -1,7 +1,6 @@
"use client"
import { useIntl } from "react-intl"
import { sumPackages } from "@/components/HotelReservation/utils"
import { formatPrice } from "@/utils/numberFormatting"
import BoldRow from "../Bold"
@@ -43,21 +42,14 @@ export default function RedemptionPrice({
const averagePriceTitle = intl.formatMessage({
defaultMessage: "Average price per night",
})
const pkgsSum = sumPackages(packages)
const roomAdditionalPrice = price.additionalPricePerStay
let additionalPricePerStay
if (roomAdditionalPrice) {
additionalPricePerStay = roomAdditionalPrice + pkgsSum.price
} else if (pkgsSum.price) {
additionalPricePerStay = pkgsSum.price
}
const additionalPricePerStay = price.additionalPricePerStay
const averageAdditionalPricePerNight = roomAdditionalPrice
? Math.ceil(roomAdditionalPrice / nights)
const averageAdditionalPricePerNight = additionalPricePerStay
? Math.ceil(additionalPricePerStay / nights)
: null
const additionalCurrency = price.currency ?? pkgsSum.currency
const additionalCurrency = price.currency ?? currency
let averagePricePerNight = `${price.pointsPerNight} ${CurrencyEnum.POINTS}`
if (averageAdditionalPricePerNight) {
averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}`

View File

@@ -1,7 +1,6 @@
"use client"
import { useIntl } from "react-intl"
import { sumPackages } from "@/components/HotelReservation/utils"
import { formatPrice } from "@/utils/numberFormatting"
import BoldRow from "../Bold"
@@ -45,13 +44,7 @@ export default function RegularPrice({
price.currency
)
const pkgs = sumPackages(packages)
const roomCharge = formatPrice(
intl,
price.pricePerStay + pkgs.price,
price.currency
)
const roomCharge = formatPrice(intl, price.pricePerStay, price.currency)
return (
<>

View File

@@ -273,7 +273,7 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
}
export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
return roomRates.reduce<Price>(
const totalPrice = roomRates.reduce<Price>(
(total, roomRate, idx) => {
const isMainRoom = idx === 0
let rate
@@ -320,6 +320,52 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
requested: undefined,
}
)
if (totalPrice.local.regularPrice) {
const totalPriceWithRegularPrice = roomRates.reduce(
(total, roomRate, idx) => {
const isMainRoom = idx === 0
let rate
if (isMainRoom && isMember && "member" in roomRate && roomRate.member) {
rate = roomRate.member
} else if ("public" in roomRate && roomRate.public) {
rate = roomRate.public
}
if (!rate) {
return total
}
if (rate.localPrice.regularPricePerStay) {
total.local.regularPrice =
total.local.regularPrice + rate.localPrice.regularPricePerStay
} else {
total.local.regularPrice =
total.local.regularPrice + rate.localPrice.pricePerStay
}
return total
},
{
...totalPrice,
local: {
...totalPrice.local,
regularPrice: 0,
},
}
)
if (
totalPriceWithRegularPrice.local.price ===
totalPriceWithRegularPrice.local.regularPrice
) {
totalPriceWithRegularPrice.local.regularPrice = 0
}
return totalPriceWithRegularPrice
}
return totalPrice
}
export function calculateVoucherPrice(roomRates: RoomRate[]) {

View File

@@ -3,7 +3,7 @@ import { create } from "zustand"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details"
import type { PriceType } from "@/types/components/hotelReservation/myStay/myStay"
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import { CurrencyEnum } from "@/types/enums/currency"
import type { Packages } from "@/types/requests/packages"
@@ -33,6 +33,9 @@ export type Room = Pick<
| "currencyCode"
| "vatPercentage"
| "roomPoints"
| "totalPrice"
| "totalPriceExVat"
| "vatAmount"
> & {
roomName: string
roomNumber: number | null
@@ -45,7 +48,7 @@ export type Room = Pick<
roomPrice: RoomPrice
breakfast: BreakfastPackage | null
mainRoom: boolean
priceType: PriceType
priceType: PriceTypeEnum
}
interface MyStayRoomDetailsState {
@@ -118,6 +121,7 @@ export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
vatPercentage: 0,
vatAmount: 0,
totalPriceExVat: 0,
totalPrice: 0,
createDateTime: new Date(),
canChangeDate: false,
multiRoom: false,
@@ -136,7 +140,7 @@ export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
breakfast: null,
linkedReservations: [],
isCancelable: false,
priceType: "money",
priceType: PriceTypeEnum.money,
},
linkedReservationRooms: [],
actions: {