Merged in feat/SW-1379-multiroom-summary (pull request #1198)
Feat/SW-1379 multiroom summary * fix: added early return in hotel query and added missing type annotations * feat(SW-1379): update summary to support multiple rooms and add tests * fix: added check for room number when using isMember for member prices * fix: remove mocked array * fix: minor bug fixes in rate details popup * fix: translation key Approved-by: Pontus Dreij Approved-by: Arvid Norlin
This commit is contained in:
138
__mocks__/hotelReservation/index.ts
Normal file
138
__mocks__/hotelReservation/index.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { BedTypeEnum } from "@/constants/booking"
|
||||||
|
|
||||||
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
|
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
|
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||||
|
import type {
|
||||||
|
DetailsSchema,
|
||||||
|
SignedInDetailsSchema,
|
||||||
|
} from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import { PackageTypeEnum } from "@/types/enums/packages"
|
||||||
|
import type { RoomPrice, RoomRate } from "@/types/stores/enter-details"
|
||||||
|
|
||||||
|
export const booking: SelectRateSearchParams = {
|
||||||
|
city: "Stockholm",
|
||||||
|
hotelId: "811",
|
||||||
|
fromDate: "2030-01-01",
|
||||||
|
toDate: "2030-01-03",
|
||||||
|
rooms: [
|
||||||
|
{
|
||||||
|
adults: 2,
|
||||||
|
roomTypeCode: "",
|
||||||
|
rateCode: "",
|
||||||
|
counterRateCode: "",
|
||||||
|
childrenInRoom: [{ bed: ChildBedMapEnum.IN_EXTRA_BED, age: 5 }],
|
||||||
|
packages: [RoomPackageCodeEnum.PET_ROOM],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const breakfastPackage: BreakfastPackage = {
|
||||||
|
code: "BRF1",
|
||||||
|
description: "Breakfast with reservation",
|
||||||
|
localPrice: { currency: "SEK", price: "99", totalPrice: "99" },
|
||||||
|
requestedPrice: {
|
||||||
|
currency: "EUR",
|
||||||
|
price: "9",
|
||||||
|
totalPrice: "9",
|
||||||
|
},
|
||||||
|
packageType: PackageTypeEnum.BreakfastAdult as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const roomRate: RoomRate = {
|
||||||
|
memberRate: {
|
||||||
|
rateCode: "PLSA2BEU",
|
||||||
|
localPrice: {
|
||||||
|
pricePerNight: 1508,
|
||||||
|
pricePerStay: 1508,
|
||||||
|
currency: "SEK",
|
||||||
|
},
|
||||||
|
requestedPrice: {
|
||||||
|
pricePerNight: 132,
|
||||||
|
pricePerStay: 132,
|
||||||
|
currency: "EUR",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
publicRate: {
|
||||||
|
rateCode: "SAVEEU",
|
||||||
|
localPrice: {
|
||||||
|
pricePerNight: 1525,
|
||||||
|
pricePerStay: 1525,
|
||||||
|
currency: "SEK",
|
||||||
|
},
|
||||||
|
requestedPrice: {
|
||||||
|
pricePerNight: 133,
|
||||||
|
pricePerStay: 133,
|
||||||
|
currency: "EUR",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const roomPrice: RoomPrice = {
|
||||||
|
perNight: {
|
||||||
|
local: {
|
||||||
|
currency: "SEK",
|
||||||
|
price: 1525,
|
||||||
|
},
|
||||||
|
requested: {
|
||||||
|
currency: "EUR",
|
||||||
|
price: 133,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
perStay: {
|
||||||
|
local: {
|
||||||
|
currency: "SEK",
|
||||||
|
price: 1525,
|
||||||
|
},
|
||||||
|
requested: {
|
||||||
|
currency: "EUR",
|
||||||
|
price: 133,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bedType: { [x: string]: BedTypeSelection } = {
|
||||||
|
king: {
|
||||||
|
type: BedTypeEnum.King,
|
||||||
|
description: "King-size bed",
|
||||||
|
value: "SKS",
|
||||||
|
size: {
|
||||||
|
min: 180,
|
||||||
|
max: 200,
|
||||||
|
},
|
||||||
|
extraBed: undefined,
|
||||||
|
},
|
||||||
|
queen: {
|
||||||
|
type: BedTypeEnum.Queen,
|
||||||
|
description: "Queen-size bed",
|
||||||
|
value: "QZ",
|
||||||
|
size: {
|
||||||
|
min: 160,
|
||||||
|
max: 200,
|
||||||
|
},
|
||||||
|
extraBed: undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const guestDetailsNonMember: DetailsSchema = {
|
||||||
|
join: false,
|
||||||
|
countryCode: "SE",
|
||||||
|
email: "tester@testersson.com",
|
||||||
|
firstName: "Test",
|
||||||
|
lastName: "Testersson",
|
||||||
|
phoneNumber: "72727272",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const guestDetailsMember: SignedInDetailsSchema = {
|
||||||
|
join: false,
|
||||||
|
countryCode: "SE",
|
||||||
|
email: "tester@testersson.com",
|
||||||
|
firstName: "Test",
|
||||||
|
lastName: "Testersson",
|
||||||
|
phoneNumber: "72727272",
|
||||||
|
zipCode: "12345",
|
||||||
|
dateOfBirth: "1999-01-01",
|
||||||
|
membershipNo: "12421412211212",
|
||||||
|
}
|
||||||
@@ -1,13 +1,74 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||||
|
|
||||||
import SummaryUI from "./UI"
|
import SummaryUI from "./UI"
|
||||||
|
|
||||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||||
|
import type { DetailsState } from "@/types/stores/enter-details"
|
||||||
|
|
||||||
|
function storeSelector(state: DetailsState) {
|
||||||
|
return {
|
||||||
|
bedType: state.bedType,
|
||||||
|
booking: state.booking,
|
||||||
|
breakfast: state.breakfast,
|
||||||
|
guest: state.guest,
|
||||||
|
packages: state.packages,
|
||||||
|
roomRate: state.roomRate,
|
||||||
|
roomPrice: state.roomPrice,
|
||||||
|
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||||
|
togglePriceDetailsModalOpen: state.actions.togglePriceDetailsModalOpen,
|
||||||
|
totalPrice: state.totalPrice,
|
||||||
|
vat: state.vat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function DesktopSummary(props: SummaryProps) {
|
export default function DesktopSummary(props: SummaryProps) {
|
||||||
|
const {
|
||||||
|
bedType,
|
||||||
|
booking,
|
||||||
|
breakfast,
|
||||||
|
guest,
|
||||||
|
packages,
|
||||||
|
roomPrice,
|
||||||
|
roomRate,
|
||||||
|
toggleSummaryOpen,
|
||||||
|
togglePriceDetailsModalOpen,
|
||||||
|
totalPrice,
|
||||||
|
vat,
|
||||||
|
} = useEnterDetailsStore(storeSelector)
|
||||||
|
|
||||||
|
// TODO: rooms should be part of store
|
||||||
|
const rooms = [
|
||||||
|
{
|
||||||
|
adults: booking.rooms[0].adults,
|
||||||
|
childrenInRoom: booking.rooms[0].childrenInRoom,
|
||||||
|
bedType,
|
||||||
|
breakfast,
|
||||||
|
guest,
|
||||||
|
roomRate,
|
||||||
|
roomPrice,
|
||||||
|
roomType: props.roomType,
|
||||||
|
rateDetails: props.rateDetails,
|
||||||
|
cancellationText: props.cancellationText,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidePanel variant="summary">
|
<SidePanel variant="summary">
|
||||||
<SummaryUI {...props} />
|
<SummaryUI
|
||||||
|
booking={booking}
|
||||||
|
rooms={rooms}
|
||||||
|
isMember={props.isMember}
|
||||||
|
breakfastIncluded={props.breakfastIncluded}
|
||||||
|
packages={packages}
|
||||||
|
totalPrice={totalPrice}
|
||||||
|
vat={vat}
|
||||||
|
toggleSummaryOpen={toggleSummaryOpen}
|
||||||
|
togglePriceDetailsModalOpen={togglePriceDetailsModalOpen}
|
||||||
|
/>
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,11 +49,15 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content,
|
|
||||||
.priceDetailsButton {
|
.priceDetailsButton {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-height: 50dvh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.bottomSheet {
|
.bottomSheet {
|
||||||
padding: var(--Spacing-x2) 0 var(--Spacing-x7);
|
padding: var(--Spacing-x2) 0 var(--Spacing-x7);
|
||||||
|
|||||||
@@ -14,20 +14,67 @@ import type { DetailsState } from "@/types/stores/enter-details"
|
|||||||
|
|
||||||
function storeSelector(state: DetailsState) {
|
function storeSelector(state: DetailsState) {
|
||||||
return {
|
return {
|
||||||
join: state.guest.join,
|
bedType: state.bedType,
|
||||||
membershipNo: state.guest.membershipNo,
|
booking: state.booking,
|
||||||
|
breakfast: state.breakfast,
|
||||||
|
guest: state.guest,
|
||||||
|
packages: state.packages,
|
||||||
|
roomRate: state.roomRate,
|
||||||
|
roomPrice: state.roomPrice,
|
||||||
|
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||||
|
togglePriceDetailsModalOpen: state.actions.togglePriceDetailsModalOpen,
|
||||||
|
totalPrice: state.totalPrice,
|
||||||
|
vat: state.vat,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MobileSummary(props: SummaryProps) {
|
export default function MobileSummary(props: SummaryProps) {
|
||||||
const { join, membershipNo } = useEnterDetailsStore(storeSelector)
|
const {
|
||||||
const showPromo = !props.isMember && !join && !membershipNo
|
bedType,
|
||||||
|
booking,
|
||||||
|
breakfast,
|
||||||
|
guest,
|
||||||
|
packages,
|
||||||
|
roomPrice,
|
||||||
|
roomRate,
|
||||||
|
toggleSummaryOpen,
|
||||||
|
togglePriceDetailsModalOpen,
|
||||||
|
totalPrice,
|
||||||
|
vat,
|
||||||
|
} = useEnterDetailsStore(storeSelector)
|
||||||
|
|
||||||
|
// TODO: rooms should be part of store
|
||||||
|
const rooms = [
|
||||||
|
{
|
||||||
|
adults: booking.rooms[0].adults,
|
||||||
|
childrenInRoom: booking.rooms[0].childrenInRoom,
|
||||||
|
bedType,
|
||||||
|
breakfast,
|
||||||
|
guest,
|
||||||
|
roomRate,
|
||||||
|
roomPrice,
|
||||||
|
roomType: props.roomType,
|
||||||
|
rateDetails: props.rateDetails,
|
||||||
|
cancellationText: props.cancellationText,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const showPromo = !props.isMember && !guest.join && !guest.membershipNo
|
||||||
return (
|
return (
|
||||||
<div className={styles.mobileSummary}>
|
<div className={styles.mobileSummary}>
|
||||||
{showPromo ? <SignupPromoMobile /> : null}
|
{showPromo ? <SignupPromoMobile /> : null}
|
||||||
<SummaryBottomSheet>
|
<SummaryBottomSheet>
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<SummaryUI {...props} />
|
<SummaryUI
|
||||||
|
booking={booking}
|
||||||
|
rooms={rooms}
|
||||||
|
isMember={props.isMember}
|
||||||
|
breakfastIncluded={props.breakfastIncluded}
|
||||||
|
packages={packages}
|
||||||
|
totalPrice={totalPrice}
|
||||||
|
vat={vat}
|
||||||
|
toggleSummaryOpen={toggleSummaryOpen}
|
||||||
|
togglePriceDetailsModalOpen={togglePriceDetailsModalOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SummaryBottomSheet>
|
</SummaryBottomSheet>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default function PriceDetailsTable({
|
|||||||
|
|
||||||
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
|
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||||
const nights = intl.formatMessage(
|
const nights = intl.formatMessage(
|
||||||
{ id: "booking.nights" },
|
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||||
{ totalNights: diff }
|
{ totalNights: diff }
|
||||||
)
|
)
|
||||||
const vatPercentage = vat / 100
|
const vatPercentage = vat / 100
|
||||||
@@ -135,7 +135,7 @@ export default function PriceDetailsTable({
|
|||||||
)}
|
)}
|
||||||
value={formatPrice(
|
value={formatPrice(
|
||||||
intl,
|
intl,
|
||||||
parseInt(breakfast.localPrice.totalPrice),
|
parseInt(breakfast.localPrice.price),
|
||||||
breakfast.localPrice.currency
|
breakfast.localPrice.currency
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import React from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
|
||||||
|
|
||||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||||
import {
|
import {
|
||||||
@@ -26,84 +26,22 @@ import PriceDetailsTable from "../PriceDetailsTable"
|
|||||||
import styles from "./ui.module.css"
|
import styles from "./ui.module.css"
|
||||||
|
|
||||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
import type { SummaryUIProps } from "@/types/components/hotelReservation/summary"
|
||||||
import type { DetailsState } from "@/types/stores/enter-details"
|
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
||||||
|
|
||||||
export function storeSelector(state: DetailsState) {
|
|
||||||
return {
|
|
||||||
bedType: state.bedType,
|
|
||||||
booking: state.booking,
|
|
||||||
breakfast: state.breakfast,
|
|
||||||
join: state.guest.join,
|
|
||||||
membershipNo: state.guest.membershipNo,
|
|
||||||
packages: state.packages,
|
|
||||||
roomRate: state.roomRate,
|
|
||||||
roomPrice: state.roomPrice,
|
|
||||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
|
||||||
togglePriceDetailsModalOpen: state.actions.togglePriceDetailsModalOpen,
|
|
||||||
totalPrice: state.totalPrice,
|
|
||||||
vat: state.vat,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SummaryUI({
|
export default function SummaryUI({
|
||||||
cancellationText,
|
booking,
|
||||||
|
rooms,
|
||||||
|
packages,
|
||||||
|
totalPrice,
|
||||||
isMember,
|
isMember,
|
||||||
rateDetails,
|
|
||||||
roomType,
|
|
||||||
breakfastIncluded,
|
breakfastIncluded,
|
||||||
}: SummaryProps) {
|
toggleSummaryOpen,
|
||||||
|
togglePriceDetailsModalOpen,
|
||||||
|
}: SummaryUIProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
|
|
||||||
const {
|
|
||||||
bedType,
|
|
||||||
booking,
|
|
||||||
breakfast,
|
|
||||||
join,
|
|
||||||
membershipNo,
|
|
||||||
packages,
|
|
||||||
roomPrice,
|
|
||||||
roomRate,
|
|
||||||
toggleSummaryOpen,
|
|
||||||
togglePriceDetailsModalOpen,
|
|
||||||
totalPrice,
|
|
||||||
vat,
|
|
||||||
} = useEnterDetailsStore(storeSelector)
|
|
||||||
|
|
||||||
// TODO: Update for Multiroom later
|
|
||||||
const adults = booking.rooms[0].adults
|
|
||||||
const children = booking.rooms[0].childrenInRoom
|
|
||||||
|
|
||||||
const childrenBeds = children?.reduce(
|
|
||||||
(acc, value) => {
|
|
||||||
const bedType = Number(value.bed)
|
|
||||||
if (bedType === ChildBedMapEnum.IN_ADULTS_BED) {
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
const count = acc.get(bedType) ?? 0
|
|
||||||
acc.set(bedType, count + 1)
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
new Map<ChildBedMapEnum, number>([
|
|
||||||
[ChildBedMapEnum.IN_CRIB, 0],
|
|
||||||
[ChildBedMapEnum.IN_EXTRA_BED, 0],
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
const childBedCrib = childrenBeds?.get(ChildBedMapEnum.IN_CRIB)
|
|
||||||
const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED)
|
|
||||||
|
|
||||||
const memberPrice = roomRate.memberRate
|
|
||||||
? {
|
|
||||||
currency: roomRate.memberRate.localPrice.currency,
|
|
||||||
pricePerNight: roomRate.memberRate.localPrice.pricePerNight,
|
|
||||||
amount: roomRate.memberRate.localPrice.pricePerStay,
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
|
|
||||||
const showMemberPrice = !!(isMember || join || membershipNo) && memberPrice
|
|
||||||
|
|
||||||
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
|
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||||
|
|
||||||
const nights = intl.formatMessage(
|
const nights = intl.formatMessage(
|
||||||
@@ -123,22 +61,24 @@ export default function SummaryUI({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const adultsMsg = intl.formatMessage(
|
function getMemberPrice(roomRate: DetailsProviderProps["roomRate"]) {
|
||||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
return roomRate.memberRate
|
||||||
{ totalAdults: adults }
|
? {
|
||||||
)
|
currency: roomRate.memberRate.localPrice.currency,
|
||||||
|
pricePerNight: roomRate.memberRate.localPrice.pricePerNight,
|
||||||
const guestsParts = [adultsMsg]
|
amount: roomRate.memberRate.localPrice.pricePerStay,
|
||||||
if (children?.length) {
|
}
|
||||||
const childrenMsg = intl.formatMessage(
|
: null
|
||||||
{
|
|
||||||
id: "{totalChildren, plural, one {# child} other {# children}}",
|
|
||||||
},
|
|
||||||
{ totalChildren: children.length }
|
|
||||||
)
|
|
||||||
guestsParts.push(childrenMsg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showSignupPromo =
|
||||||
|
rooms.length === 1 &&
|
||||||
|
rooms
|
||||||
|
.slice(0, 1)
|
||||||
|
.some((r) => !isMember || !r.guest.join || !r.guest.membershipNo)
|
||||||
|
|
||||||
|
const memberPrice = getMemberPrice(rooms[0].roomRate)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.summary}>
|
<section className={styles.summary}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
@@ -160,171 +100,255 @@ export default function SummaryUI({
|
|||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
<Divider color="primaryLightSubtle" />
|
<Divider color="primaryLightSubtle" />
|
||||||
<div className={styles.addOns}>
|
{rooms.map((room, idx) => {
|
||||||
<div>
|
const roomNumber = idx + 1
|
||||||
<div className={styles.entry}>
|
const adults = room.adults
|
||||||
<Body color="uiTextHighContrast">{roomType}</Body>
|
const childrenInRoom = room.childrenInRoom
|
||||||
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
|
||||||
{formatPrice(
|
const childrenBeds = childrenInRoom?.reduce(
|
||||||
intl,
|
(acc, value) => {
|
||||||
roomPrice.perStay.local.price,
|
const bedType = Number(value.bed)
|
||||||
roomPrice.perStay.local.currency
|
if (bedType === ChildBedMapEnum.IN_ADULTS_BED) {
|
||||||
)}
|
return acc
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
|
||||||
{guestsParts.join(", ")}
|
|
||||||
</Caption>
|
|
||||||
<Caption color="uiTextMediumContrast">{cancellationText}</Caption>
|
|
||||||
<Modal
|
|
||||||
trigger={
|
|
||||||
<Button intent="text">
|
|
||||||
<Caption color="burgundy" type="underline">
|
|
||||||
{intl.formatMessage({ id: "Rate details" })}
|
|
||||||
</Caption>
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
title={cancellationText}
|
const count = acc.get(bedType) ?? 0
|
||||||
>
|
acc.set(bedType, count + 1)
|
||||||
<div className={styles.terms}>
|
return acc
|
||||||
{rateDetails?.map((info) => (
|
},
|
||||||
<Body
|
new Map<ChildBedMapEnum, number>([
|
||||||
key={info}
|
[ChildBedMapEnum.IN_CRIB, 0],
|
||||||
color="uiTextHighContrast"
|
[ChildBedMapEnum.IN_EXTRA_BED, 0],
|
||||||
className={styles.termsText}
|
])
|
||||||
>
|
)
|
||||||
<CheckIcon
|
|
||||||
color="uiSemanticSuccess"
|
const childBedCrib = childrenBeds?.get(ChildBedMapEnum.IN_CRIB)
|
||||||
width={20}
|
const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED)
|
||||||
height={20}
|
|
||||||
className={styles.termsIcon}
|
const memberPrice = getMemberPrice(room.roomRate)
|
||||||
></CheckIcon>
|
|
||||||
{info}
|
const isFirstRoomMember = roomNumber === 1 && isMember
|
||||||
</Body>
|
const showMemberPrice =
|
||||||
))}
|
!!(isFirstRoomMember || room.guest.join || room.guest.membershipNo) &&
|
||||||
</div>
|
memberPrice
|
||||||
</Modal>
|
|
||||||
</div>
|
const adultsMsg = intl.formatMessage(
|
||||||
{packages
|
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||||
? packages.map((roomPackage) => (
|
{ totalAdults: adults }
|
||||||
<div className={styles.entry} key={roomPackage.code}>
|
)
|
||||||
<div>
|
|
||||||
<Body color="uiTextHighContrast">
|
const guestsParts = [adultsMsg]
|
||||||
{roomPackage.description}
|
if (childrenInRoom?.length) {
|
||||||
|
const childrenMsg = intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "{totalChildren, plural, one {# child} other {# children}}",
|
||||||
|
},
|
||||||
|
{ totalChildren: childrenInRoom.length }
|
||||||
|
)
|
||||||
|
guestsParts.push(childrenMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={idx}>
|
||||||
|
<div
|
||||||
|
className={styles.addOns}
|
||||||
|
data-testid={`summary-room-${roomNumber}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{rooms.length > 1 ? (
|
||||||
|
<Body textTransform="bold">
|
||||||
|
{intl.formatMessage({ id: "Room" })} {roomNumber}
|
||||||
|
</Body>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
||||||
|
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
room.roomPrice.perStay.local.price,
|
||||||
|
room.roomPrice.perStay.local.currency
|
||||||
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{formatPrice(
|
|
||||||
intl,
|
|
||||||
parseInt(roomPackage.localPrice.price),
|
|
||||||
roomPackage.localPrice.currency
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
{bedType ? (
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<Body color="uiTextHighContrast">{bedType.description}</Body>
|
|
||||||
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{formatPrice(intl, 0, roomPrice.perStay.local.currency)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{childBedCrib ? (
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<div>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: "Crib (child) × {count}" },
|
|
||||||
{ count: childBedCrib }
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
|
||||||
{intl.formatMessage({ id: "Based on availability" })}
|
|
||||||
</Caption>
|
|
||||||
</div>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{formatPrice(intl, 0, roomPrice.perStay.local.currency)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{childBedExtraBed ? (
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<div>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: "Extra bed (child) × {count}" },
|
|
||||||
{
|
|
||||||
count: childBedExtraBed,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{formatPrice(intl, 0, roomPrice.perStay.local.currency)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{breakfastIncluded ? (
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({ id: "Breakfast included" })}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
) : breakfast === false ? (
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({ id: "No breakfast" })}
|
|
||||||
</Body>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{formatPrice(intl, 0, roomPrice.perStay.local.currency)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{breakfast ? (
|
|
||||||
<div>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({ id: "Breakfast buffet" })}
|
|
||||||
</Body>
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "{totalAdults, plural, one {# adult} other {# adults}}",
|
|
||||||
},
|
|
||||||
{ totalAdults: adults }
|
|
||||||
)}
|
|
||||||
</Caption>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{formatPrice(
|
|
||||||
intl,
|
|
||||||
parseInt(breakfast.localPrice.totalPrice),
|
|
||||||
breakfast.localPrice.currency
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
{children?.length ? (
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{intl.formatMessage(
|
{guestsParts.join(", ")}
|
||||||
{
|
|
||||||
id: "{totalChildren, plural, one {# child} other {# children}}",
|
|
||||||
},
|
|
||||||
{ totalChildren: children.length }
|
|
||||||
)}
|
|
||||||
</Caption>
|
</Caption>
|
||||||
<Body color="uiTextHighContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{formatPrice(intl, 0, breakfast.localPrice.currency)}
|
{room.cancellationText}
|
||||||
</Body>
|
</Caption>
|
||||||
|
<Modal
|
||||||
|
trigger={
|
||||||
|
<Button intent="text">
|
||||||
|
<Caption color="burgundy" type="underline">
|
||||||
|
{intl.formatMessage({ id: "Rate details" })}
|
||||||
|
</Caption>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title={room.cancellationText}
|
||||||
|
>
|
||||||
|
<div className={styles.terms}>
|
||||||
|
{room.rateDetails?.map((info) => (
|
||||||
|
<Body
|
||||||
|
key={info}
|
||||||
|
color="uiTextHighContrast"
|
||||||
|
className={styles.termsText}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
color="uiSemanticSuccess"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className={styles.termsIcon}
|
||||||
|
></CheckIcon>
|
||||||
|
{info}
|
||||||
|
</Body>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
{packages
|
||||||
</div>
|
? packages.map((roomPackage) => (
|
||||||
) : null}
|
<div className={styles.entry} key={roomPackage.code}>
|
||||||
</div>
|
<div>
|
||||||
<Divider color="primaryLightSubtle" />
|
<Body color="uiTextHighContrast">
|
||||||
|
{roomPackage.description}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
parseInt(roomPackage.localPrice.price),
|
||||||
|
roomPackage.localPrice.currency
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
{room.bedType ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{room.bedType.description}
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
0,
|
||||||
|
room.roomPrice.perStay.local.currency
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{childBedCrib ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "Crib (child) × {count}" },
|
||||||
|
{ count: childBedCrib }
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Based on availability" })}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
0,
|
||||||
|
room.roomPrice.perStay.local.currency
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{childBedExtraBed ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "Extra bed (child) × {count}" },
|
||||||
|
{
|
||||||
|
count: childBedExtraBed,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
0,
|
||||||
|
room.roomPrice.perStay.local.currency
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{breakfastIncluded ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage({ id: "Breakfast included" })}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : room.breakfast === false ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage({ id: "No breakfast" })}
|
||||||
|
</Body>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
0,
|
||||||
|
room.roomPrice.perStay.local.currency
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{room.breakfast ? (
|
||||||
|
<div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage({ id: "Breakfast buffet" })}
|
||||||
|
</Body>
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "{totalAdults, plural, one {# adult} other {# adults}}",
|
||||||
|
},
|
||||||
|
{ totalAdults: adults }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
parseInt(room.breakfast.localPrice.totalPrice),
|
||||||
|
room.breakfast.localPrice.currency
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
{childrenInRoom?.length ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "{totalChildren, plural, one {# child} other {# children}}",
|
||||||
|
},
|
||||||
|
{ totalChildren: childrenInRoom.length }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
0,
|
||||||
|
room.breakfast.localPrice.currency
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Divider color="primaryLightSubtle" />
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
<div className={styles.total}>
|
<div className={styles.total}>
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
<div>
|
<div>
|
||||||
@@ -334,7 +358,6 @@ export default function SummaryUI({
|
|||||||
{ b: (str) => <b>{str}</b> }
|
{ b: (str) => <b>{str}</b> }
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={intl.formatMessage({ id: "Price details" })}
|
title={intl.formatMessage({ id: "Price details" })}
|
||||||
trigger={
|
trigger={
|
||||||
@@ -350,11 +373,12 @@ export default function SummaryUI({
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PriceDetailsTable roomType={roomType} />
|
{/* // TODO: all rooms needs to be passed to PriceDetails */}
|
||||||
|
<PriceDetailsTable roomType={rooms[0].roomType} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Body textTransform="bold">
|
<Body textTransform="bold" data-testid="total-price">
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
intl,
|
intl,
|
||||||
totalPrice.local.price,
|
totalPrice.local.price,
|
||||||
@@ -379,7 +403,7 @@ export default function SummaryUI({
|
|||||||
</div>
|
</div>
|
||||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||||
</div>
|
</div>
|
||||||
{!showMemberPrice && memberPrice ? (
|
{showSignupPromo && memberPrice ? (
|
||||||
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
|
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--Spacing-x-one-and-half);
|
gap: var(--Spacing-x-one-and-half);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rateDetailsPopover {
|
.rateDetailsPopover {
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { describe, expect, test } from "@jest/globals"
|
||||||
|
import { act, cleanup, render, screen, within } from "@testing-library/react"
|
||||||
|
import { type IntlConfig, IntlProvider } from "react-intl"
|
||||||
|
|
||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
import {
|
||||||
|
bedType,
|
||||||
|
booking,
|
||||||
|
breakfastPackage,
|
||||||
|
guestDetailsMember,
|
||||||
|
guestDetailsNonMember,
|
||||||
|
roomPrice,
|
||||||
|
roomRate,
|
||||||
|
} from "@/__mocks__/hotelReservation"
|
||||||
|
import { initIntl } from "@/i18n"
|
||||||
|
|
||||||
|
import SummaryUI from "./UI"
|
||||||
|
|
||||||
|
import type { PropsWithChildren } from "react"
|
||||||
|
|
||||||
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
|
|
||||||
|
jest.mock("@/lib/api", () => ({
|
||||||
|
fetchRetry: jest.fn((fn) => fn),
|
||||||
|
}))
|
||||||
|
|
||||||
|
function createWrapper(intlConfig: IntlConfig) {
|
||||||
|
return function Wrapper({ children }: PropsWithChildren) {
|
||||||
|
return (
|
||||||
|
<IntlProvider
|
||||||
|
messages={intlConfig.messages}
|
||||||
|
locale={intlConfig.locale}
|
||||||
|
defaultLocale={intlConfig.defaultLocale}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</IntlProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add type definition to this object
|
||||||
|
export const rooms = [
|
||||||
|
{
|
||||||
|
adults: 2,
|
||||||
|
childrenInRoom: [{ bed: ChildBedMapEnum.IN_EXTRA_BED, age: 5 }],
|
||||||
|
bedType: {
|
||||||
|
description: bedType.queen.description,
|
||||||
|
roomTypeCode: bedType.queen.value,
|
||||||
|
},
|
||||||
|
breakfast: breakfastPackage,
|
||||||
|
guest: guestDetailsNonMember,
|
||||||
|
roomRate: roomRate,
|
||||||
|
roomPrice: roomPrice,
|
||||||
|
roomType: "Standard",
|
||||||
|
rateDetails: [],
|
||||||
|
cancellationText: "Non-refundable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
adults: 1,
|
||||||
|
childrenInRoom: [],
|
||||||
|
bedType: {
|
||||||
|
description: bedType.king.description,
|
||||||
|
roomTypeCode: bedType.king.value,
|
||||||
|
},
|
||||||
|
breakfast: undefined,
|
||||||
|
guest: guestDetailsMember,
|
||||||
|
roomRate: roomRate,
|
||||||
|
roomPrice: roomPrice,
|
||||||
|
roomType: "Standard",
|
||||||
|
rateDetails: [],
|
||||||
|
cancellationText: "Non-refundable",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("EnterDetails Summary", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("render with single room correctly", async () => {
|
||||||
|
const intl = await initIntl(Lang.en)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<SummaryUI
|
||||||
|
booking={booking}
|
||||||
|
rooms={rooms.slice(0, 1)}
|
||||||
|
isMember={false}
|
||||||
|
breakfastIncluded={false}
|
||||||
|
packages={[]}
|
||||||
|
totalPrice={{
|
||||||
|
requested: {
|
||||||
|
currency: "EUR",
|
||||||
|
price: 133,
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
currency: "SEK",
|
||||||
|
price: 1500,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
vat={12}
|
||||||
|
toggleSummaryOpen={jest.fn()}
|
||||||
|
togglePriceDetailsModalOpen={jest.fn()}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: createWrapper(intl),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
screen.getByText("2 adults, 1 child")
|
||||||
|
screen.getByText("Standard")
|
||||||
|
screen.getByText("1,525 SEK")
|
||||||
|
screen.getByText(bedType.queen.description)
|
||||||
|
screen.getByText("Breakfast buffet")
|
||||||
|
screen.getByText("1,500 SEK")
|
||||||
|
screen.getByTestId("signup-promo-desktop")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("render with multiple rooms correctly", async () => {
|
||||||
|
const intl = await initIntl(Lang.en)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(
|
||||||
|
<SummaryUI
|
||||||
|
booking={booking}
|
||||||
|
rooms={rooms}
|
||||||
|
isMember={false}
|
||||||
|
breakfastIncluded={false}
|
||||||
|
packages={[]}
|
||||||
|
totalPrice={{
|
||||||
|
requested: {
|
||||||
|
currency: "EUR",
|
||||||
|
price: 133,
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
currency: "SEK",
|
||||||
|
price: 1500,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
vat={12}
|
||||||
|
toggleSummaryOpen={jest.fn()}
|
||||||
|
togglePriceDetailsModalOpen={jest.fn()}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: createWrapper(intl),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const room1 = within(screen.getByTestId("summary-room-1"))
|
||||||
|
room1.getByText("Standard")
|
||||||
|
room1.getByText("2 adults, 1 child")
|
||||||
|
room1.getByText(bedType.queen.description)
|
||||||
|
room1.getByText("Breakfast buffet")
|
||||||
|
|
||||||
|
const room2 = within(screen.getByTestId("summary-room-2"))
|
||||||
|
room2.getByText("Standard")
|
||||||
|
room2.getByText("1 adult")
|
||||||
|
const room2Breakfast = room2.queryByText("Breakfast buffet")
|
||||||
|
expect(room2Breakfast).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
room2.getByText(bedType.king.description)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -22,7 +22,10 @@ export default function SignupPromoDesktop({
|
|||||||
const price = formatPrice(intl, amount, currency)
|
const price = formatPrice(intl, amount, currency)
|
||||||
|
|
||||||
return memberPrice ? (
|
return memberPrice ? (
|
||||||
<div className={styles.memberDiscountBannerDesktop}>
|
<div
|
||||||
|
className={styles.memberDiscountBannerDesktop}
|
||||||
|
data-testid="signup-promo-desktop"
|
||||||
|
>
|
||||||
{badgeContent && <span className={styles.badge}>{badgeContent}</span>}
|
{badgeContent && <span className={styles.badge}>{badgeContent}</span>}
|
||||||
<Footnote color="burgundy">
|
<Footnote color="burgundy">
|
||||||
{intl.formatMessage<React.ReactNode>(
|
{intl.formatMessage<React.ReactNode>(
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import { getLocalizedMonthName } from "@/utils/dateFormatting"
|
|||||||
|
|
||||||
import Date from "./index"
|
import Date from "./index"
|
||||||
|
|
||||||
|
jest.mock("react-intl", () => ({
|
||||||
|
useIntl: () => ({
|
||||||
|
formatMessage: (message: { id: string }) => message.id,
|
||||||
|
formatNumber: (value: number) => value,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
interface FormWrapperProps {
|
interface FormWrapperProps {
|
||||||
defaultValues: Record<string, unknown>
|
defaultValues: Record<string, unknown>
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Lang } from "@/constants/languages"
|
|||||||
|
|
||||||
const cache = createIntlCache()
|
const cache = createIntlCache()
|
||||||
|
|
||||||
async function initIntl(lang: Lang) {
|
export async function initIntl(lang: Lang) {
|
||||||
return createIntl<React.ReactNode>(
|
return createIntl<React.ReactNode>(
|
||||||
{
|
{
|
||||||
defaultLocale: Lang.en,
|
defaultLocale: Lang.en,
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
|
import "@testing-library/jest-dom/jest-globals"
|
||||||
import "@testing-library/jest-dom"
|
import "@testing-library/jest-dom"
|
||||||
|
|
||||||
jest.mock("react-intl", () => ({
|
|
||||||
useIntl: () => ({
|
|
||||||
formatMessage: (message: { id: string }) => message.id,
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock("next/navigation", () => ({
|
jest.mock("next/navigation", () => ({
|
||||||
useRouter: jest.fn(),
|
useRouter: jest.fn(),
|
||||||
usePathname: jest.fn().mockReturnValue("/"),
|
usePathname: jest.fn().mockReturnValue("/"),
|
||||||
|
|||||||
@@ -1129,17 +1129,17 @@ export const hotelQueryRouter = router({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const countries = await getCountries(options, searchParams, ctx.lang)
|
const countries = await getCountries(options, searchParams, ctx.lang)
|
||||||
|
if (!countries) {
|
||||||
let citiesByCountry = null
|
return null
|
||||||
if (countries) {
|
|
||||||
citiesByCountry = await getCitiesByCountry(
|
|
||||||
countries,
|
|
||||||
options,
|
|
||||||
searchParams,
|
|
||||||
ctx.lang
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const citiesByCountry = await getCitiesByCountry(
|
||||||
|
countries,
|
||||||
|
options,
|
||||||
|
searchParams,
|
||||||
|
ctx.lang
|
||||||
|
)
|
||||||
|
|
||||||
const locations = await getLocations(
|
const locations = await getLocations(
|
||||||
ctx.lang,
|
ctx.lang,
|
||||||
options,
|
options,
|
||||||
|
|||||||
@@ -2,15 +2,19 @@ import { describe, expect, test } from "@jest/globals"
|
|||||||
import { act, renderHook } from "@testing-library/react"
|
import { act, renderHook } from "@testing-library/react"
|
||||||
import { type PropsWithChildren } from "react"
|
import { type PropsWithChildren } from "react"
|
||||||
|
|
||||||
import { BedTypeEnum } from "@/constants/booking"
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
import {
|
||||||
|
bedType,
|
||||||
|
booking,
|
||||||
|
breakfastPackage,
|
||||||
|
guestDetailsNonMember,
|
||||||
|
roomRate,
|
||||||
|
} from "@/__mocks__/hotelReservation"
|
||||||
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
||||||
|
|
||||||
import { detailsStorageName, useEnterDetailsStore } from "."
|
import { detailsStorageName, useEnterDetailsStore } from "."
|
||||||
|
|
||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
|
||||||
import { PackageTypeEnum } from "@/types/enums/packages"
|
|
||||||
import { StepEnum } from "@/types/enums/step"
|
import { StepEnum } from "@/types/enums/step"
|
||||||
import type { PersistedState } from "@/types/stores/enter-details"
|
import type { PersistedState } from "@/types/stores/enter-details"
|
||||||
|
|
||||||
@@ -27,100 +31,14 @@ jest.mock("@/lib/api", () => ({
|
|||||||
fetchRetry: jest.fn((fn) => fn),
|
fetchRetry: jest.fn((fn) => fn),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const booking = {
|
|
||||||
hotelId: "123",
|
|
||||||
fromDate: "2100-01-01",
|
|
||||||
toDate: "2100-01-02",
|
|
||||||
rooms: [
|
|
||||||
{
|
|
||||||
adults: 1,
|
|
||||||
roomTypeCode: "SKS",
|
|
||||||
rateCode: "SAVEEU",
|
|
||||||
counterRateCode: "PLSA2BEU",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const bedTypes = [
|
|
||||||
{
|
|
||||||
type: BedTypeEnum.King,
|
|
||||||
description: "King-size bed",
|
|
||||||
value: "SKS",
|
|
||||||
size: {
|
|
||||||
min: 180,
|
|
||||||
max: 200,
|
|
||||||
},
|
|
||||||
roomTypeCode: "SKS",
|
|
||||||
extraBed: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: BedTypeEnum.Queen,
|
|
||||||
description: "Queen-size bed",
|
|
||||||
value: "QZ",
|
|
||||||
size: {
|
|
||||||
min: 160,
|
|
||||||
max: 200,
|
|
||||||
},
|
|
||||||
roomTypeCode: "QZ",
|
|
||||||
extraBed: undefined,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const guest = {
|
|
||||||
countryCode: "SE",
|
|
||||||
dateOfBirth: "",
|
|
||||||
email: "test@test.com",
|
|
||||||
firstName: "Tester",
|
|
||||||
lastName: "Testersson",
|
|
||||||
join: false,
|
|
||||||
membershipNo: "12345678901234",
|
|
||||||
phoneNumber: "+46700000000",
|
|
||||||
zipCode: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
const breakfastPackages = [
|
|
||||||
{
|
|
||||||
code: BreakfastPackageEnum.REGULAR_BREAKFAST,
|
|
||||||
description: "Breakfast with reservation",
|
|
||||||
localPrice: {
|
|
||||||
currency: "SEK",
|
|
||||||
price: "99",
|
|
||||||
totalPrice: "99",
|
|
||||||
},
|
|
||||||
requestedPrice: {
|
|
||||||
currency: "EUR",
|
|
||||||
price: "9",
|
|
||||||
totalPrice: "9",
|
|
||||||
},
|
|
||||||
packageType: PackageTypeEnum.BreakfastAdult as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function Wrapper({ children }: PropsWithChildren) {
|
function Wrapper({ children }: PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<EnterDetailsProvider
|
<EnterDetailsProvider
|
||||||
bedTypes={bedTypes}
|
bedTypes={[bedType.king, bedType.queen]}
|
||||||
booking={booking}
|
booking={booking}
|
||||||
showBreakfastStep={true}
|
showBreakfastStep={true}
|
||||||
packages={null}
|
packages={null}
|
||||||
roomRate={{
|
roomRate={roomRate}
|
||||||
memberRate: {
|
|
||||||
rateCode: "PLSA2BEU",
|
|
||||||
localPrice: {
|
|
||||||
currency: "EUR",
|
|
||||||
pricePerNight: 100,
|
|
||||||
pricePerStay: 200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
publicRate: {
|
|
||||||
rateCode: "SAVEEU",
|
|
||||||
localPrice: {
|
|
||||||
currency: "EUR",
|
|
||||||
pricePerNight: 100,
|
|
||||||
pricePerStay: 200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
searchParamsStr=""
|
searchParamsStr=""
|
||||||
step={StepEnum.selectBed}
|
step={StepEnum.selectBed}
|
||||||
user={null}
|
user={null}
|
||||||
@@ -154,10 +72,13 @@ describe("Enter Details Store", () => {
|
|||||||
|
|
||||||
test("initialize with correct values from sessionStorage", async () => {
|
test("initialize with correct values from sessionStorage", async () => {
|
||||||
const storage: PersistedState = {
|
const storage: PersistedState = {
|
||||||
bedType: bedTypes[1],
|
bedType: {
|
||||||
breakfast: breakfastPackages[0],
|
roomTypeCode: bedType.queen.value,
|
||||||
|
description: bedType.queen.description,
|
||||||
|
},
|
||||||
|
breakfast: breakfastPackage,
|
||||||
booking,
|
booking,
|
||||||
guest,
|
guest: guestDetailsNonMember,
|
||||||
}
|
}
|
||||||
|
|
||||||
window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))
|
window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))
|
||||||
@@ -187,7 +108,10 @@ describe("Enter Details Store", () => {
|
|||||||
expect(result.current.currentStep).toEqual(StepEnum.selectBed)
|
expect(result.current.currentStep).toEqual(StepEnum.selectBed)
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.actions.updateBedType(bedTypes[0])
|
result.current.actions.updateBedType({
|
||||||
|
roomTypeCode: bedType.king.value,
|
||||||
|
description: bedType.king.description,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.current.isValid[StepEnum.selectBed]).toEqual(true)
|
expect(result.current.isValid[StepEnum.selectBed]).toEqual(true)
|
||||||
@@ -195,7 +119,7 @@ describe("Enter Details Store", () => {
|
|||||||
expect(window.location.pathname.slice(1)).toBe(StepEnum.breakfast)
|
expect(window.location.pathname.slice(1)).toBe(StepEnum.breakfast)
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.actions.updateBreakfast(breakfastPackages[0])
|
result.current.actions.updateBreakfast(breakfastPackage)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.current.isValid[StepEnum.breakfast]).toEqual(true)
|
expect(result.current.isValid[StepEnum.breakfast]).toEqual(true)
|
||||||
@@ -203,7 +127,7 @@ describe("Enter Details Store", () => {
|
|||||||
expect(window.location.pathname.slice(1)).toBe(StepEnum.details)
|
expect(window.location.pathname.slice(1)).toBe(StepEnum.details)
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.actions.updateDetails(guest)
|
result.current.actions.updateDetails(guestDetailsNonMember)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.current.isValid[StepEnum.details]).toEqual(true)
|
expect(result.current.isValid[StepEnum.details]).toEqual(true)
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { z } from "zod"
|
import { type z } from "zod"
|
||||||
|
|
||||||
import {
|
import type { breakfastFormSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
|
||||||
|
import type {
|
||||||
breakfastPackageSchema,
|
breakfastPackageSchema,
|
||||||
breakfastPackagesSchema,
|
breakfastPackagesSchema,
|
||||||
} from "@/server/routers/hotels/output"
|
} from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
import { breakfastFormSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
|
|
||||||
|
|
||||||
export interface BreakfastFormSchema
|
export interface BreakfastFormSchema
|
||||||
extends z.output<typeof breakfastFormSchema> {}
|
extends z.output<typeof breakfastFormSchema> {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
import {
|
import type { SafeUser } from "@/types/user"
|
||||||
|
import type {
|
||||||
guestDetailsSchema,
|
guestDetailsSchema,
|
||||||
signedInDetailsSchema,
|
signedInDetailsSchema,
|
||||||
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||||
|
|
||||||
import type { SafeUser } from "@/types/user"
|
|
||||||
|
|
||||||
export type DetailsSchema = z.output<typeof guestDetailsSchema>
|
export type DetailsSchema = z.output<typeof guestDetailsSchema>
|
||||||
export type SignedInDetailsSchema = z.output<typeof signedInDetailsSchema>
|
export type SignedInDetailsSchema = z.output<typeof signedInDetailsSchema>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
||||||
import type { Packages } from "@/types/requests/packages"
|
import type { Packages } from "@/types/requests/packages"
|
||||||
import type { DetailsState, Price } from "@/types/stores/enter-details"
|
import type {
|
||||||
|
DetailsState,
|
||||||
|
Price,
|
||||||
|
RoomPrice,
|
||||||
|
} from "@/types/stores/enter-details"
|
||||||
import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability"
|
import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability"
|
||||||
import type { Child } from "./selectRate/selectRate"
|
import type { BedTypeSchema } from "./enterDetails/bedType"
|
||||||
|
import type { BreakfastPackage } from "./enterDetails/breakfast"
|
||||||
|
import type { DetailsSchema } from "./enterDetails/details"
|
||||||
|
import type { Child, SelectRateSearchParams } from "./selectRate/selectRate"
|
||||||
|
|
||||||
export type RoomsData = Pick<DetailsState, "roomPrice"> &
|
export type RoomsData = Pick<DetailsState, "roomPrice"> &
|
||||||
Pick<RoomAvailability, "cancellationText" | "rateDetails"> &
|
Pick<RoomAvailability, "cancellationText" | "rateDetails"> &
|
||||||
@@ -17,3 +25,26 @@ export interface SummaryProps
|
|||||||
isMember: boolean
|
isMember: boolean
|
||||||
breakfastIncluded: boolean
|
breakfastIncluded: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SummaryUIProps {
|
||||||
|
booking: SelectRateSearchParams
|
||||||
|
rooms: {
|
||||||
|
adults: number
|
||||||
|
childrenInRoom: Child[] | undefined
|
||||||
|
bedType: BedTypeSchema | undefined
|
||||||
|
breakfast: BreakfastPackage | false | undefined
|
||||||
|
guest: DetailsSchema
|
||||||
|
roomRate: DetailsProviderProps["roomRate"]
|
||||||
|
roomPrice: RoomPrice
|
||||||
|
roomType: string
|
||||||
|
rateDetails: string[] | undefined
|
||||||
|
cancellationText: string
|
||||||
|
}[]
|
||||||
|
isMember: boolean
|
||||||
|
breakfastIncluded: boolean
|
||||||
|
packages: Packages | null
|
||||||
|
totalPrice: Price
|
||||||
|
vat: number
|
||||||
|
toggleSummaryOpen: () => void
|
||||||
|
togglePriceDetailsModalOpen: () => void
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user