diff --git a/__mocks__/hotelReservation/index.ts b/__mocks__/hotelReservation/index.ts
new file mode 100644
index 000000000..74a78f7be
--- /dev/null
+++ b/__mocks__/hotelReservation/index.ts
@@ -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",
+}
diff --git a/components/HotelReservation/EnterDetails/Summary/Desktop.tsx b/components/HotelReservation/EnterDetails/Summary/Desktop.tsx
index 3c90539b4..fd4e7f8bc 100644
--- a/components/HotelReservation/EnterDetails/Summary/Desktop.tsx
+++ b/components/HotelReservation/EnterDetails/Summary/Desktop.tsx
@@ -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 (
-
+
)
}
diff --git a/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/bottomSheet.module.css b/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/bottomSheet.module.css
index d586effc7..99eb5567f 100644
--- a/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/bottomSheet.module.css
+++ b/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/bottomSheet.module.css
@@ -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);
diff --git a/components/HotelReservation/EnterDetails/Summary/Mobile/index.tsx b/components/HotelReservation/EnterDetails/Summary/Mobile/index.tsx
index 94c7c28af..ec16da6f7 100644
--- a/components/HotelReservation/EnterDetails/Summary/Mobile/index.tsx
+++ b/components/HotelReservation/EnterDetails/Summary/Mobile/index.tsx
@@ -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 (
diff --git a/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/index.tsx b/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/index.tsx
index 7eff059da..196309748 100644
--- a/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/index.tsx
+++ b/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/index.tsx
@@ -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
)}
/>
diff --git a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx
index c7095b1b0..c67c59513 100644
--- a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx
+++ b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx
@@ -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.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 (
@@ -160,171 +100,255 @@ export default function SummaryUI({
-
-
-
- {roomType}
-
- {formatPrice(
- intl,
- roomPrice.perStay.local.price,
- roomPrice.perStay.local.currency
- )}
-
-
-
- {guestsParts.join(", ")}
-
-
{cancellationText}
-
-
- {intl.formatMessage({ id: "Rate details" })}
-
-
+ {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}
- >
-
- {rateDetails?.map((info) => (
-
-
- {info}
-
- ))}
-
-
-
- {packages
- ? packages.map((roomPackage) => (
-
-
-
- {roomPackage.description}
+ const count = acc.get(bedType) ?? 0
+ acc.set(bedType, count + 1)
+ return acc
+ },
+ new Map
([
+ [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 (
+
+
+
+ {rooms.length > 1 ? (
+
+ {intl.formatMessage({ id: "Room" })} {roomNumber}
+
+ ) : null}
+
+ {room.roomType}
+
+ {formatPrice(
+ intl,
+ room.roomPrice.perStay.local.price,
+ room.roomPrice.perStay.local.currency
+ )}
-
-
- {formatPrice(
- intl,
- parseInt(roomPackage.localPrice.price),
- roomPackage.localPrice.currency
- )}
-
-
- ))
- : null}
- {bedType ? (
-
- {bedType.description}
-
-
- {formatPrice(intl, 0, roomPrice.perStay.local.currency)}
-
-
- ) : null}
- {childBedCrib ? (
-
-
-
- {intl.formatMessage(
- { id: "Crib (child) × {count}" },
- { count: childBedCrib }
- )}
-
-
- {intl.formatMessage({ id: "Based on availability" })}
-
-
-
- {formatPrice(intl, 0, roomPrice.perStay.local.currency)}
-
-
- ) : null}
- {childBedExtraBed ? (
-
-
-
- {intl.formatMessage(
- { id: "Extra bed (child) × {count}" },
- {
- count: childBedExtraBed,
- }
- )}
-
-
-
- {formatPrice(intl, 0, roomPrice.perStay.local.currency)}
-
-
- ) : null}
- {breakfastIncluded ? (
-
-
- {intl.formatMessage({ id: "Breakfast included" })}
-
-
- ) : breakfast === false ? (
-
-
- {intl.formatMessage({ id: "No breakfast" })}
-
-
- {formatPrice(intl, 0, roomPrice.perStay.local.currency)}
-
-
- ) : null}
- {breakfast ? (
-
-
- {intl.formatMessage({ id: "Breakfast buffet" })}
-
-
-
- {intl.formatMessage(
- {
- id: "{totalAdults, plural, one {# adult} other {# adults}}",
- },
- { totalAdults: adults }
- )}
-
-
- {formatPrice(
- intl,
- parseInt(breakfast.localPrice.totalPrice),
- breakfast.localPrice.currency
- )}
-
-
- {children?.length ? (
-
- {intl.formatMessage(
- {
- id: "{totalChildren, plural, one {# child} other {# children}}",
- },
- { totalChildren: children.length }
- )}
+ {guestsParts.join(", ")}
-
- {formatPrice(intl, 0, breakfast.localPrice.currency)}
-
+
+ {room.cancellationText}
+
+
+
+ {intl.formatMessage({ id: "Rate details" })}
+
+
+ }
+ title={room.cancellationText}
+ >
+
+ {room.rateDetails?.map((info) => (
+
+
+ {info}
+
+ ))}
+
+
- ) : null}
-
- ) : null}
-
-
+ {packages
+ ? packages.map((roomPackage) => (
+
+
+
+ {roomPackage.description}
+
+
+
+
+ {formatPrice(
+ intl,
+ parseInt(roomPackage.localPrice.price),
+ roomPackage.localPrice.currency
+ )}
+
+
+ ))
+ : null}
+ {room.bedType ? (
+
+
+ {room.bedType.description}
+
+
+
+ {formatPrice(
+ intl,
+ 0,
+ room.roomPrice.perStay.local.currency
+ )}
+
+
+ ) : null}
+ {childBedCrib ? (
+
+
+
+ {intl.formatMessage(
+ { id: "Crib (child) × {count}" },
+ { count: childBedCrib }
+ )}
+
+
+ {intl.formatMessage({ id: "Based on availability" })}
+
+
+
+ {formatPrice(
+ intl,
+ 0,
+ room.roomPrice.perStay.local.currency
+ )}
+
+
+ ) : null}
+ {childBedExtraBed ? (
+
+
+
+ {intl.formatMessage(
+ { id: "Extra bed (child) × {count}" },
+ {
+ count: childBedExtraBed,
+ }
+ )}
+
+
+
+ {formatPrice(
+ intl,
+ 0,
+ room.roomPrice.perStay.local.currency
+ )}
+
+
+ ) : null}
+ {breakfastIncluded ? (
+
+
+ {intl.formatMessage({ id: "Breakfast included" })}
+
+
+ ) : room.breakfast === false ? (
+
+
+ {intl.formatMessage({ id: "No breakfast" })}
+
+
+ {formatPrice(
+ intl,
+ 0,
+ room.roomPrice.perStay.local.currency
+ )}
+
+
+ ) : null}
+ {room.breakfast ? (
+
+
+ {intl.formatMessage({ id: "Breakfast buffet" })}
+
+
+
+ {intl.formatMessage(
+ {
+ id: "{totalAdults, plural, one {# adult} other {# adults}}",
+ },
+ { totalAdults: adults }
+ )}
+
+
+ {formatPrice(
+ intl,
+ parseInt(room.breakfast.localPrice.totalPrice),
+ room.breakfast.localPrice.currency
+ )}
+
+
+ {childrenInRoom?.length ? (
+
+
+ {intl.formatMessage(
+ {
+ id: "{totalChildren, plural, one {# child} other {# children}}",
+ },
+ { totalChildren: childrenInRoom.length }
+ )}
+
+
+ {formatPrice(
+ intl,
+ 0,
+ room.breakfast.localPrice.currency
+ )}
+
+
+ ) : null}
+
+ ) : null}
+
+
+
+ )
+ })}
@@ -334,7 +358,6 @@ export default function SummaryUI({
{ b: (str) =>
{str} }
)}
-
}
>
-
+ {/* // TODO: all rooms needs to be passed to PriceDetails */}
+
-
+
{formatPrice(
intl,
totalPrice.local.price,
@@ -379,7 +403,7 @@ export default function SummaryUI({
- {!showMemberPrice && memberPrice ? (
+ {showSignupPromo && memberPrice ? (
) : null}
diff --git a/components/HotelReservation/EnterDetails/Summary/UI/ui.module.css b/components/HotelReservation/EnterDetails/Summary/UI/ui.module.css
index 58ff5770c..66785ae67 100644
--- a/components/HotelReservation/EnterDetails/Summary/UI/ui.module.css
+++ b/components/HotelReservation/EnterDetails/Summary/UI/ui.module.css
@@ -39,6 +39,7 @@
display: flex;
flex-direction: column;
gap: var(--Spacing-x-one-and-half);
+ overflow-y: auto;
}
.rateDetailsPopover {
diff --git a/components/HotelReservation/EnterDetails/Summary/summary.test.tsx b/components/HotelReservation/EnterDetails/Summary/summary.test.tsx
new file mode 100644
index 000000000..b10c019a1
--- /dev/null
+++ b/components/HotelReservation/EnterDetails/Summary/summary.test.tsx
@@ -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 (
+
+ {children}
+
+ )
+ }
+}
+
+// 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(
+
,
+ {
+ 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(
+
,
+ {
+ 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)
+ })
+})
diff --git a/components/HotelReservation/SignupPromo/Desktop.tsx b/components/HotelReservation/SignupPromo/Desktop.tsx
index b96dfd189..a774e2d5c 100644
--- a/components/HotelReservation/SignupPromo/Desktop.tsx
+++ b/components/HotelReservation/SignupPromo/Desktop.tsx
@@ -22,7 +22,10 @@ export default function SignupPromoDesktop({
const price = formatPrice(intl, amount, currency)
return memberPrice ? (
-
+
{badgeContent && {badgeContent}}
{intl.formatMessage(
diff --git a/components/TempDesignSystem/Form/Date/date.test.tsx b/components/TempDesignSystem/Form/Date/date.test.tsx
index 0b2fd4183..e755f40c4 100644
--- a/components/TempDesignSystem/Form/Date/date.test.tsx
+++ b/components/TempDesignSystem/Form/Date/date.test.tsx
@@ -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
children: React.ReactNode
diff --git a/i18n/index.ts b/i18n/index.ts
index 0a8ebb67d..e3b4cc8f9 100644
--- a/i18n/index.ts
+++ b/i18n/index.ts
@@ -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(
{
defaultLocale: Lang.en,
diff --git a/jest.setup.ts b/jest.setup.ts
index 6d866f13c..31fe0b102 100644
--- a/jest.setup.ts
+++ b/jest.setup.ts
@@ -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("/"),
diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts
index b785621b2..51d11b479 100644
--- a/server/routers/hotels/query.ts
+++ b/server/routers/hotels/query.ts
@@ -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,
diff --git a/stores/enter-details/useEnterDetailsStore.test.tsx b/stores/enter-details/useEnterDetailsStore.test.tsx
index a8db55157..ab3b15cf0 100644
--- a/stores/enter-details/useEnterDetailsStore.test.tsx
+++ b/stores/enter-details/useEnterDetailsStore.test.tsx
@@ -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 (
{
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)
diff --git a/types/components/hotelReservation/enterDetails/breakfast.ts b/types/components/hotelReservation/enterDetails/breakfast.ts
index 283d85028..acb936abd 100644
--- a/types/components/hotelReservation/enterDetails/breakfast.ts
+++ b/types/components/hotelReservation/enterDetails/breakfast.ts
@@ -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 {}
diff --git a/types/components/hotelReservation/enterDetails/details.ts b/types/components/hotelReservation/enterDetails/details.ts
index 3d7fc41c1..786f48570 100644
--- a/types/components/hotelReservation/enterDetails/details.ts
+++ b/types/components/hotelReservation/enterDetails/details.ts
@@ -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
export type SignedInDetailsSchema = z.output
diff --git a/types/components/hotelReservation/summary.ts b/types/components/hotelReservation/summary.ts
index 50fd2ed66..c60194d71 100644
--- a/types/components/hotelReservation/summary.ts
+++ b/types/components/hotelReservation/summary.ts
@@ -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 &
Pick &
@@ -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
+}