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 SummaryUI from "./UI"
|
||||
|
||||
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) {
|
||||
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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,11 +49,15 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.content,
|
||||
.priceDetailsButton {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-height: 50dvh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.bottomSheet {
|
||||
padding: var(--Spacing-x2) 0 var(--Spacing-x7);
|
||||
|
||||
@@ -14,20 +14,67 @@ import type { DetailsState } from "@/types/stores/enter-details"
|
||||
|
||||
function storeSelector(state: DetailsState) {
|
||||
return {
|
||||
join: state.guest.join,
|
||||
membershipNo: state.guest.membershipNo,
|
||||
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 MobileSummary(props: SummaryProps) {
|
||||
const { join, membershipNo } = useEnterDetailsStore(storeSelector)
|
||||
const showPromo = !props.isMember && !join && !membershipNo
|
||||
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,
|
||||
},
|
||||
]
|
||||
const showPromo = !props.isMember && !guest.join && !guest.membershipNo
|
||||
return (
|
||||
<div className={styles.mobileSummary}>
|
||||
{showPromo ? <SignupPromoMobile /> : null}
|
||||
<SummaryBottomSheet>
|
||||
<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>
|
||||
</SummaryBottomSheet>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function PriceDetailsTable({
|
||||
|
||||
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||
const nights = intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: diff }
|
||||
)
|
||||
const vatPercentage = vat / 100
|
||||
@@ -135,7 +135,7 @@ export default function PriceDetailsTable({
|
||||
)}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(breakfast.localPrice.totalPrice),
|
||||
parseInt(breakfast.localPrice.price),
|
||||
breakfast.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||
import {
|
||||
@@ -26,84 +26,22 @@ import PriceDetailsTable from "../PriceDetailsTable"
|
||||
import styles from "./ui.module.css"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
import type { DetailsState } from "@/types/stores/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,
|
||||
}
|
||||
}
|
||||
import type { SummaryUIProps } from "@/types/components/hotelReservation/summary"
|
||||
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
||||
|
||||
export default function SummaryUI({
|
||||
cancellationText,
|
||||
booking,
|
||||
rooms,
|
||||
packages,
|
||||
totalPrice,
|
||||
isMember,
|
||||
rateDetails,
|
||||
roomType,
|
||||
breakfastIncluded,
|
||||
}: SummaryProps) {
|
||||
toggleSummaryOpen,
|
||||
togglePriceDetailsModalOpen,
|
||||
}: SummaryUIProps) {
|
||||
const intl = useIntl()
|
||||
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 nights = intl.formatMessage(
|
||||
@@ -123,22 +61,24 @@ export default function SummaryUI({
|
||||
}
|
||||
}
|
||||
|
||||
const adultsMsg = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: adults }
|
||||
)
|
||||
|
||||
const guestsParts = [adultsMsg]
|
||||
if (children?.length) {
|
||||
const childrenMsg = intl.formatMessage(
|
||||
{
|
||||
id: "{totalChildren, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{ totalChildren: children.length }
|
||||
)
|
||||
guestsParts.push(childrenMsg)
|
||||
function getMemberPrice(roomRate: DetailsProviderProps["roomRate"]) {
|
||||
return roomRate.memberRate
|
||||
? {
|
||||
currency: roomRate.memberRate.localPrice.currency,
|
||||
pricePerNight: roomRate.memberRate.localPrice.pricePerNight,
|
||||
amount: roomRate.memberRate.localPrice.pricePerStay,
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className={styles.summary}>
|
||||
<header className={styles.header}>
|
||||
@@ -160,171 +100,255 @@ export default function SummaryUI({
|
||||
</Button>
|
||||
</header>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.addOns}>
|
||||
<div>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{roomType}</Body>
|
||||
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
||||
{formatPrice(
|
||||
intl,
|
||||
roomPrice.perStay.local.price,
|
||||
roomPrice.perStay.local.currency
|
||||
)}
|
||||
</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>
|
||||
{rooms.map((room, idx) => {
|
||||
const roomNumber = idx + 1
|
||||
const adults = room.adults
|
||||
const childrenInRoom = room.childrenInRoom
|
||||
|
||||
const childrenBeds = childrenInRoom?.reduce(
|
||||
(acc, value) => {
|
||||
const bedType = Number(value.bed)
|
||||
if (bedType === ChildBedMapEnum.IN_ADULTS_BED) {
|
||||
return acc
|
||||
}
|
||||
title={cancellationText}
|
||||
>
|
||||
<div className={styles.terms}>
|
||||
{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>
|
||||
{packages
|
||||
? packages.map((roomPackage) => (
|
||||
<div className={styles.entry} key={roomPackage.code}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{roomPackage.description}
|
||||
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 = getMemberPrice(room.roomRate)
|
||||
|
||||
const isFirstRoomMember = roomNumber === 1 && isMember
|
||||
const showMemberPrice =
|
||||
!!(isFirstRoomMember || room.guest.join || room.guest.membershipNo) &&
|
||||
memberPrice
|
||||
|
||||
const adultsMsg = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: adults }
|
||||
)
|
||||
|
||||
const guestsParts = [adultsMsg]
|
||||
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>
|
||||
</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">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "{totalChildren, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{ totalChildren: children.length }
|
||||
)}
|
||||
{guestsParts.join(", ")}
|
||||
</Caption>
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, 0, breakfast.localPrice.currency)}
|
||||
</Body>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{room.cancellationText}
|
||||
</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>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
{packages
|
||||
? packages.map((roomPackage) => (
|
||||
<div className={styles.entry} key={roomPackage.code}>
|
||||
<div>
|
||||
<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.entry}>
|
||||
<div>
|
||||
@@ -334,7 +358,6 @@ export default function SummaryUI({
|
||||
{ b: (str) => <b>{str}</b> }
|
||||
)}
|
||||
</Body>
|
||||
|
||||
<Modal
|
||||
title={intl.formatMessage({ id: "Price details" })}
|
||||
trigger={
|
||||
@@ -350,11 +373,12 @@ export default function SummaryUI({
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PriceDetailsTable roomType={roomType} />
|
||||
{/* // TODO: all rooms needs to be passed to PriceDetails */}
|
||||
<PriceDetailsTable roomType={rooms[0].roomType} />
|
||||
</Modal>
|
||||
</div>
|
||||
<div>
|
||||
<Body textTransform="bold">
|
||||
<Body textTransform="bold" data-testid="total-price">
|
||||
{formatPrice(
|
||||
intl,
|
||||
totalPrice.local.price,
|
||||
@@ -379,7 +403,7 @@ export default function SummaryUI({
|
||||
</div>
|
||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||
</div>
|
||||
{!showMemberPrice && memberPrice ? (
|
||||
{showSignupPromo && memberPrice ? (
|
||||
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.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)
|
||||
|
||||
return memberPrice ? (
|
||||
<div className={styles.memberDiscountBannerDesktop}>
|
||||
<div
|
||||
className={styles.memberDiscountBannerDesktop}
|
||||
data-testid="signup-promo-desktop"
|
||||
>
|
||||
{badgeContent && <span className={styles.badge}>{badgeContent}</span>}
|
||||
<Footnote color="burgundy">
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
|
||||
@@ -10,6 +10,13 @@ import { getLocalizedMonthName } from "@/utils/dateFormatting"
|
||||
|
||||
import Date from "./index"
|
||||
|
||||
jest.mock("react-intl", () => ({
|
||||
useIntl: () => ({
|
||||
formatMessage: (message: { id: string }) => message.id,
|
||||
formatNumber: (value: number) => value,
|
||||
}),
|
||||
}))
|
||||
|
||||
interface FormWrapperProps {
|
||||
defaultValues: Record<string, unknown>
|
||||
children: React.ReactNode
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Lang } from "@/constants/languages"
|
||||
|
||||
const cache = createIntlCache()
|
||||
|
||||
async function initIntl(lang: Lang) {
|
||||
export async function initIntl(lang: Lang) {
|
||||
return createIntl<React.ReactNode>(
|
||||
{
|
||||
defaultLocale: Lang.en,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import "@testing-library/jest-dom/jest-globals"
|
||||
import "@testing-library/jest-dom"
|
||||
|
||||
jest.mock("react-intl", () => ({
|
||||
useIntl: () => ({
|
||||
formatMessage: (message: { id: string }) => message.id,
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock("next/navigation", () => ({
|
||||
useRouter: jest.fn(),
|
||||
usePathname: jest.fn().mockReturnValue("/"),
|
||||
|
||||
@@ -1129,17 +1129,17 @@ export const hotelQueryRouter = router({
|
||||
}
|
||||
|
||||
const countries = await getCountries(options, searchParams, ctx.lang)
|
||||
|
||||
let citiesByCountry = null
|
||||
if (countries) {
|
||||
citiesByCountry = await getCitiesByCountry(
|
||||
countries,
|
||||
options,
|
||||
searchParams,
|
||||
ctx.lang
|
||||
)
|
||||
if (!countries) {
|
||||
return null
|
||||
}
|
||||
|
||||
const citiesByCountry = await getCitiesByCountry(
|
||||
countries,
|
||||
options,
|
||||
searchParams,
|
||||
ctx.lang
|
||||
)
|
||||
|
||||
const locations = await getLocations(
|
||||
ctx.lang,
|
||||
options,
|
||||
|
||||
@@ -2,15 +2,19 @@ import { describe, expect, test } from "@jest/globals"
|
||||
import { act, renderHook } from "@testing-library/react"
|
||||
import { type PropsWithChildren } from "react"
|
||||
|
||||
import { BedTypeEnum } from "@/constants/booking"
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
import {
|
||||
bedType,
|
||||
booking,
|
||||
breakfastPackage,
|
||||
guestDetailsNonMember,
|
||||
roomRate,
|
||||
} from "@/__mocks__/hotelReservation"
|
||||
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
||||
|
||||
import { detailsStorageName, useEnterDetailsStore } from "."
|
||||
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import { PackageTypeEnum } from "@/types/enums/packages"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { PersistedState } from "@/types/stores/enter-details"
|
||||
|
||||
@@ -27,100 +31,14 @@ jest.mock("@/lib/api", () => ({
|
||||
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) {
|
||||
return (
|
||||
<EnterDetailsProvider
|
||||
bedTypes={bedTypes}
|
||||
bedTypes={[bedType.king, bedType.queen]}
|
||||
booking={booking}
|
||||
showBreakfastStep={true}
|
||||
packages={null}
|
||||
roomRate={{
|
||||
memberRate: {
|
||||
rateCode: "PLSA2BEU",
|
||||
localPrice: {
|
||||
currency: "EUR",
|
||||
pricePerNight: 100,
|
||||
pricePerStay: 200,
|
||||
},
|
||||
},
|
||||
publicRate: {
|
||||
rateCode: "SAVEEU",
|
||||
localPrice: {
|
||||
currency: "EUR",
|
||||
pricePerNight: 100,
|
||||
pricePerStay: 200,
|
||||
},
|
||||
},
|
||||
}}
|
||||
roomRate={roomRate}
|
||||
searchParamsStr=""
|
||||
step={StepEnum.selectBed}
|
||||
user={null}
|
||||
@@ -154,10 +72,13 @@ describe("Enter Details Store", () => {
|
||||
|
||||
test("initialize with correct values from sessionStorage", async () => {
|
||||
const storage: PersistedState = {
|
||||
bedType: bedTypes[1],
|
||||
breakfast: breakfastPackages[0],
|
||||
bedType: {
|
||||
roomTypeCode: bedType.queen.value,
|
||||
description: bedType.queen.description,
|
||||
},
|
||||
breakfast: breakfastPackage,
|
||||
booking,
|
||||
guest,
|
||||
guest: guestDetailsNonMember,
|
||||
}
|
||||
|
||||
window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))
|
||||
@@ -187,7 +108,10 @@ describe("Enter Details Store", () => {
|
||||
expect(result.current.currentStep).toEqual(StepEnum.selectBed)
|
||||
|
||||
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)
|
||||
@@ -195,7 +119,7 @@ describe("Enter Details Store", () => {
|
||||
expect(window.location.pathname.slice(1)).toBe(StepEnum.breakfast)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateBreakfast(breakfastPackages[0])
|
||||
result.current.actions.updateBreakfast(breakfastPackage)
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateDetails(guest)
|
||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
||||
})
|
||||
|
||||
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,
|
||||
breakfastPackagesSchema,
|
||||
} from "@/server/routers/hotels/output"
|
||||
|
||||
import { breakfastFormSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
|
||||
|
||||
export interface 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,
|
||||
signedInDetailsSchema,
|
||||
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||
|
||||
import type { SafeUser } from "@/types/user"
|
||||
|
||||
export type DetailsSchema = z.output<typeof guestDetailsSchema>
|
||||
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 { 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 { 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"> &
|
||||
Pick<RoomAvailability, "cancellationText" | "rateDetails"> &
|
||||
@@ -17,3 +25,26 @@ export interface SummaryProps
|
||||
isMember: 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