Merged in feat/SW-1078-rate-terms-scenarios (pull request #1500)
feat(SW-1078): mixed rate scenario * feat(SW-1078): mixed rate scenario * fix: change from css string modification to array join * refactor: split out big reduce function into smaller parts * fix: minor fixes and improvments * fix: added room index map to localization string Approved-by: Christian Andolf
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import React from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import {
|
||||
calculateTotalRoomPrice,
|
||||
hasFlexibleRate,
|
||||
hasPrepaidRate,
|
||||
} from "../helpers"
|
||||
|
||||
import styles from "./mixedRatePaymentBreakdown.module.css"
|
||||
|
||||
import type { RoomState } from "@/types/stores/enter-details"
|
||||
|
||||
type PaymentBreakdownState = {
|
||||
roomsWithPrepaidRate: number[]
|
||||
roomsWithFlexRate: number[]
|
||||
payNowPrice: number
|
||||
payNowComparisonPrice: number
|
||||
payAtCheckInPrice: number
|
||||
payAtCheckInComparisonPrice: number
|
||||
}
|
||||
|
||||
interface MixedRatePaymentBreakdownProps {
|
||||
rooms: RoomState[]
|
||||
currency: string
|
||||
}
|
||||
|
||||
export default function MixedRatePaymentBreakdown({
|
||||
rooms,
|
||||
currency,
|
||||
}: MixedRatePaymentBreakdownProps) {
|
||||
const intl = useIntl()
|
||||
const payNowTitle = intl.formatMessage({ id: "Pay now" })
|
||||
const payAtCheckInTitle = intl.formatMessage({ id: "Pay at check-in" })
|
||||
|
||||
const initialState: PaymentBreakdownState = {
|
||||
roomsWithPrepaidRate: [],
|
||||
roomsWithFlexRate: [],
|
||||
payNowPrice: 0,
|
||||
payNowComparisonPrice: 0,
|
||||
payAtCheckInPrice: 0,
|
||||
payAtCheckInComparisonPrice: 0,
|
||||
}
|
||||
|
||||
const {
|
||||
roomsWithPrepaidRate,
|
||||
roomsWithFlexRate,
|
||||
payNowPrice,
|
||||
payNowComparisonPrice,
|
||||
payAtCheckInPrice,
|
||||
payAtCheckInComparisonPrice,
|
||||
} = rooms.reduce((acc, room, idx) => {
|
||||
if (hasPrepaidRate(room)) {
|
||||
acc.roomsWithPrepaidRate.push(idx)
|
||||
const { totalPrice, comparisonPrice } = calculateTotalRoomPrice(room)
|
||||
acc.payNowPrice += totalPrice
|
||||
acc.payNowComparisonPrice += comparisonPrice
|
||||
}
|
||||
if (hasFlexibleRate(room)) {
|
||||
acc.roomsWithFlexRate.push(idx)
|
||||
const { totalPrice, comparisonPrice } = calculateTotalRoomPrice(room)
|
||||
acc.payAtCheckInPrice += totalPrice
|
||||
acc.payAtCheckInComparisonPrice += comparisonPrice
|
||||
}
|
||||
return acc
|
||||
}, initialState)
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<PaymentCard
|
||||
title={payNowTitle}
|
||||
price={payNowPrice}
|
||||
comparisonPrice={payNowComparisonPrice}
|
||||
currency={currency}
|
||||
roomIndexes={roomsWithPrepaidRate}
|
||||
/>
|
||||
<PaymentCard
|
||||
title={payAtCheckInTitle}
|
||||
price={payAtCheckInPrice}
|
||||
comparisonPrice={payAtCheckInComparisonPrice}
|
||||
currency={currency}
|
||||
roomIndexes={roomsWithFlexRate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PaymentCardProps {
|
||||
title: string
|
||||
price: number
|
||||
comparisonPrice: number
|
||||
currency: string
|
||||
roomIndexes: number[]
|
||||
}
|
||||
|
||||
function PaymentCard({
|
||||
title,
|
||||
price,
|
||||
comparisonPrice,
|
||||
currency,
|
||||
roomIndexes,
|
||||
}: PaymentCardProps) {
|
||||
const intl = useIntl()
|
||||
const isMemberRateApplied = price < comparisonPrice
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<Caption
|
||||
type="bold"
|
||||
textTransform="uppercase"
|
||||
className={styles.cardTitle}
|
||||
>
|
||||
{title}{" "}
|
||||
<span>
|
||||
/{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "Room {roomIndex}" },
|
||||
{
|
||||
roomIndex: roomIndexes.map((idx) => idx + 1).join(" & "),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Caption>
|
||||
<Body textTransform="bold" className={styles.priceItem}>
|
||||
{formatPrice(intl, price, currency)}
|
||||
{isMemberRateApplied && comparisonPrice ? (
|
||||
<span>{formatPrice(intl, comparisonPrice, currency)}</span>
|
||||
) : null}
|
||||
</Body>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
.container {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
background-color: var(--Scandic-Blue-00);
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.cardTitle > span {
|
||||
color: var(--UI-Text-Placeholder);
|
||||
}
|
||||
|
||||
.card.inactive {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.priceItem {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.priceItem > span {
|
||||
font-weight: 400;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
@@ -38,6 +38,8 @@ import { trackPaymentEvent } from "@/utils/tracking"
|
||||
import { bedTypeMap } from "../../utils"
|
||||
import PriceChangeDialog from "../PriceChangeDialog"
|
||||
import GuaranteeDetails from "./GuaranteeDetails"
|
||||
import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers"
|
||||
import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown"
|
||||
import PaymentOption from "./PaymentOption"
|
||||
import { type PaymentFormData, paymentSchema } from "./schema"
|
||||
|
||||
@@ -51,14 +53,9 @@ const retryInterval = 2000
|
||||
|
||||
export const formId = "submit-booking"
|
||||
|
||||
function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||
return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum)
|
||||
}
|
||||
|
||||
export default function PaymentClient({
|
||||
otherPaymentOptions,
|
||||
savedCreditCards,
|
||||
mustBeGuaranteed,
|
||||
}: PaymentClientProps) {
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
@@ -90,6 +87,11 @@ export default function PaymentClient({
|
||||
|
||||
const { toDate, fromDate, hotelId } = booking
|
||||
|
||||
const mustBeGuaranteed = rooms.every((r) => r.room.mustBeGuaranteed)
|
||||
const hasPrepaidRates = rooms.some(hasPrepaidRate)
|
||||
const hasFlexRates = rooms.some(hasFlexibleRate)
|
||||
const hasMixedRates = hasPrepaidRates && hasFlexRates
|
||||
|
||||
usePaymentFailedToast()
|
||||
|
||||
const methods = useForm<PaymentFormData>({
|
||||
@@ -351,6 +353,15 @@ export default function PaymentClient({
|
||||
<GuaranteeDetails />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{hasMixedRates ? (
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.",
|
||||
})}
|
||||
</Body>
|
||||
) : null}
|
||||
|
||||
{savedCreditCards?.length ? (
|
||||
<section className={styles.section}>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
@@ -374,6 +385,7 @@ export default function PaymentClient({
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className={styles.section}>
|
||||
{savedCreditCards?.length ? (
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
@@ -399,7 +411,14 @@ export default function PaymentClient({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{hasMixedRates ? (
|
||||
<MixedRatePaymentBreakdown
|
||||
rooms={rooms}
|
||||
currency={totalPrice.local.currency}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CancellationRuleEnum, PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
import type { RoomState } from "@/types/stores/enter-details"
|
||||
|
||||
export function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||
return Object.values<string>(PaymentMethodEnum).includes(value)
|
||||
}
|
||||
|
||||
export function hasFlexibleRate({ room }: RoomState): boolean {
|
||||
return room.cancellationRule === CancellationRuleEnum.CancellableBefore6PM
|
||||
}
|
||||
|
||||
export function hasPrepaidRate({ room }: RoomState): boolean {
|
||||
return room.cancellationRule !== CancellationRuleEnum.CancellableBefore6PM
|
||||
}
|
||||
|
||||
export function calculateTotalRoomPrice({ room }: RoomState) {
|
||||
let totalPrice = room.roomPrice.perStay.local.price
|
||||
|
||||
if (room.breakfast) {
|
||||
totalPrice += Number(room.breakfast.localPrice.totalPrice) * room.adults
|
||||
}
|
||||
|
||||
if (room.roomFeatures) {
|
||||
room.roomFeatures.forEach((pkg) => {
|
||||
totalPrice += Number(pkg.localPrice.price)
|
||||
})
|
||||
}
|
||||
|
||||
let comparisonPrice = totalPrice
|
||||
|
||||
const isMember = room.guest.join || room.guest.membershipNo
|
||||
if (isMember) {
|
||||
const publicPrice = room.roomRate.publicRate?.localPrice.pricePerStay ?? 0
|
||||
const memberPrice = room.roomRate.memberRate?.localPrice.pricePerStay ?? 0
|
||||
const diff = publicPrice - memberPrice
|
||||
comparisonPrice = totalPrice + diff
|
||||
}
|
||||
|
||||
return {
|
||||
totalPrice,
|
||||
comparisonPrice,
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import type { PaymentProps } from "@/types/components/hotelReservation/enterDeta
|
||||
|
||||
export default async function Payment({
|
||||
otherPaymentOptions,
|
||||
mustBeGuaranteed,
|
||||
supportedCards,
|
||||
}: PaymentProps) {
|
||||
const savedCreditCards = await getSavedPaymentCardsSafely({
|
||||
@@ -17,7 +16,6 @@ export default async function Payment({
|
||||
<PaymentClient
|
||||
otherPaymentOptions={otherPaymentOptions}
|
||||
savedCreditCards={savedCreditCards}
|
||||
mustBeGuaranteed={mustBeGuaranteed}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ const rooms: RoomState[] = [
|
||||
bedTypes: [],
|
||||
breakfast: breakfastPackage,
|
||||
breakfastIncluded: false,
|
||||
cancellationRule: "",
|
||||
cancellationText: "Non-refundable",
|
||||
childrenInRoom: [{ bed: ChildBedMapEnum.IN_EXTRA_BED, age: 5 }],
|
||||
guest: guestDetailsNonMember,
|
||||
@@ -64,6 +65,7 @@ const rooms: RoomState[] = [
|
||||
roomType: "Standard",
|
||||
roomTypeCode: "QS",
|
||||
isAvailable: true,
|
||||
mustBeGuaranteed: false,
|
||||
},
|
||||
steps: {
|
||||
[StepEnum.selectBed]: {
|
||||
@@ -92,6 +94,7 @@ const rooms: RoomState[] = [
|
||||
bedTypes: [],
|
||||
breakfast: undefined,
|
||||
breakfastIncluded: false,
|
||||
cancellationRule: "",
|
||||
cancellationText: "Non-refundable",
|
||||
childrenInRoom: [],
|
||||
guest: guestDetailsMember,
|
||||
@@ -102,6 +105,7 @@ const rooms: RoomState[] = [
|
||||
roomType: "Standard",
|
||||
roomTypeCode: "QS",
|
||||
isAvailable: true,
|
||||
mustBeGuaranteed: false,
|
||||
},
|
||||
steps: {
|
||||
[StepEnum.selectBed]: {
|
||||
|
||||
Reference in New Issue
Block a user