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 (
{showPromo ? : null}
- +
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 +}