feat: add multiroom signup

This commit is contained in:
Simon Emanuelsson
2025-02-17 15:10:48 +01:00
parent 95917e5e4f
commit 92c5566c59
78 changed files with 2035 additions and 1545 deletions

View File

@@ -0,0 +1,657 @@
// import { describe, expect, test } from "@jest/globals"
// import { act, renderHook, waitFor } from "@testing-library/react"
// import { type PropsWithChildren } from "react"
// import { BedTypeEnum } from "@/constants/booking"
// import { Lang } from "@/constants/languages"
// import {
// bedType,
// booking,
// breakfastPackage,
// guestDetailsMember,
// guestDetailsNonMember,
// roomPrice,
// roomRate,
// } from "@/__mocks__/hotelReservation"
// import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
// import { detailsStorageName, useEnterDetailsStore } from "."
// import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
// import type { BreakfastPackages } from "@/types/components/hotelReservation/enterDetails/breakfast"
// import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
// import { PackageTypeEnum } from "@/types/enums/packages"
// import { StepEnum } from "@/types/enums/step"
// import type { PersistedState } from "@/types/stores/enter-details"
// jest.mock("react", () => ({
// ...jest.requireActual("react"),
// cache: jest.fn(),
// }))
// jest.mock("@/server/utils", () => ({
// toLang: () => Lang.en,
// }))
// jest.mock("@/lib/api", () => ({
// fetchRetry: jest.fn((fn) => fn),
// }))
// interface CreateWrapperParams {
// bedTypes?: BedTypeSelection[]
// bookingParams?: SelectRateSearchParams
// breakfastIncluded?: boolean
// breakfastPackages?: BreakfastPackages | null
// mustBeGuaranteed?: boolean
// }
// function createWrapper(params: Partial<CreateWrapperParams> = {}) {
// const {
// breakfastIncluded = false,
// breakfastPackages = null,
// mustBeGuaranteed = false,
// bookingParams = booking,
// bedTypes = [bedType.king, bedType.queen],
// } = params
// return function Wrapper({ children }: PropsWithChildren) {
// return (
// <EnterDetailsProvider
// booking={bookingParams}
// breakfastPackages={breakfastPackages}
// rooms={[
// {
// bedTypes,
// packages: null,
// mustBeGuaranteed,
// breakfastIncluded,
// cancellationText: "",
// rateDetails: [],
// roomType: "Standard",
// roomTypeCode: "QS",
// roomRate: roomRate,
// },
// {
// bedTypes,
// packages: null,
// mustBeGuaranteed,
// breakfastIncluded,
// cancellationText: "",
// rateDetails: [],
// roomType: "Standard",
// roomTypeCode: "QS",
// roomRate: roomRate,
// },
// ]}
// searchParamsStr=""
// user={null}
// vat={0}
// >
// {children}
// </EnterDetailsProvider>
// )
// }
// }
// describe("Enter Details Store", () => {
// beforeEach(() => {
// window.sessionStorage.clear()
// })
// describe("initial state", () => {
// test("initialize with correct default values", () => {
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper(),
// }
// )
// const state = result.current
// expect(state.booking).toEqual(booking)
// // room 1
// const room1 = result.current.rooms[0]
// expect(room1.currentStep).toBe(StepEnum.selectBed)
// expect(room1.room.roomPrice.perNight.local.price).toEqual(
// roomRate.publicRate.localPrice.pricePerNight
// )
// expect(room1.room.bedType).toEqual(undefined)
// expect(Object.values(room1.room.guest).every((value) => value === ""))
// // room 2
// const room2 = result.current.rooms[1]
// expect(room2.currentStep).toBe(null)
// expect(room2.room.roomPrice.perNight.local.price).toEqual(
// room2.room.roomRate.publicRate.localPrice.pricePerNight
// )
// expect(room2.room.bedType).toEqual(undefined)
// expect(Object.values(room2.room.guest).every((value) => value === ""))
// })
// test("initialize with correct values from session storage", () => {
// const storage: PersistedState = {
// activeRoom: 0,
// booking: booking,
// rooms: [
// {
// currentStep: StepEnum.selectBed,
// isComplete: false,
// room: {
// adults: 1,
// bedType: {
// description: bedType.king.description,
// roomTypeCode: bedType.king.value,
// },
// bedTypes: [
// {
// description: bedType.king.description,
// extraBed: undefined,
// size: {
// min: 100,
// max: 120,
// },
// type: BedTypeEnum.King,
// value: bedType.king.value,
// },
// ],
// breakfastIncluded: false,
// breakfast: breakfastPackage,
// cancellationText: "Non-refundable",
// childrenInRoom: [],
// guest: guestDetailsNonMember,
// rateDetails: [],
// roomFeatures: null,
// roomPrice: roomPrice,
// roomRate: roomRate,
// roomType: "Classic Double",
// roomTypeCode: "QS",
// },
// steps: {
// [StepEnum.selectBed]: {
// step: StepEnum.selectBed,
// isValid: true,
// },
// [StepEnum.breakfast]: {
// step: StepEnum.breakfast,
// isValid: true,
// },
// [StepEnum.details]: {
// step: StepEnum.details,
// isValid: true,
// },
// },
// },
// ],
// }
// window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper(),
// }
// )
// expect(result.current.booking).toEqual(storage.booking)
// expect(result.current.rooms[0]).toEqual(storage.rooms[0])
// })
// })
// test("add bedtype and proceed to next step", async () => {
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper(),
// }
// )
// let room1 = result.current.rooms[0]
// expect(room1.currentStep).toEqual(StepEnum.selectBed)
// const selectedBedType = {
// roomTypeCode: bedType.king.value,
// description: bedType.king.description,
// }
// await act(async () => {
// result.current.actions.updateBedType(0)(selectedBedType)
// })
// room1 = result.current.rooms[0]
// expect(room1.steps[StepEnum.selectBed].isValid).toEqual(true)
// expect(room1.room.bedType).toEqual(selectedBedType)
// expect(room1.currentStep).toEqual(StepEnum.breakfast)
// })
// test("complete step and navigate to next step", async () => {
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper(),
// }
// )
// // Room 1
// expect(result.current.activeRoom).toEqual(0)
// let room1 = result.current.rooms[0]
// expect(room1.currentStep).toEqual(StepEnum.selectBed)
// await act(async () => {
// result.current.actions.updateBedType(0)({
// roomTypeCode: bedType.king.value,
// description: bedType.king.description,
// })
// })
// room1 = result.current.rooms[0]
// expect(room1.steps[StepEnum.selectBed].isValid).toEqual(true)
// expect(room1.currentStep).toEqual(StepEnum.breakfast)
// await act(async () => {
// result.current.actions.updateBreakfast(0)(breakfastPackage)
// })
// room1 = result.current.rooms[0]
// expect(room1.steps[StepEnum.breakfast]?.isValid).toEqual(true)
// expect(room1.currentStep).toEqual(StepEnum.details)
// await act(async () => {
// result.current.actions.updateDetails(0)(guestDetailsNonMember)
// })
// expect(result.current.canProceedToPayment).toBe(false)
// // Room 2
// expect(result.current.activeRoom).toEqual(1)
// let room2 = result.current.rooms[1]
// expect(room2.currentStep).toEqual(StepEnum.selectBed)
// await act(async () => {
// const selectedBedType = {
// roomTypeCode: bedType.king.value,
// description: bedType.king.description,
// }
// result.current.actions.updateBedType(1)(selectedBedType)
// })
// room2 = result.current.rooms[1]
// expect(room2.steps[StepEnum.selectBed].isValid).toEqual(true)
// expect(room2.currentStep).toEqual(StepEnum.breakfast)
// await act(async () => {
// result.current.actions.updateBreakfast(1)(breakfastPackage)
// })
// room2 = result.current.rooms[1]
// expect(room2.steps[StepEnum.breakfast]?.isValid).toEqual(true)
// expect(room2.currentStep).toEqual(StepEnum.details)
// await act(async () => {
// result.current.actions.updateDetails(1)(guestDetailsNonMember)
// })
// expect(result.current.canProceedToPayment).toBe(true)
// })
// test("all steps needs to be completed before going to next room", async () => {
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper(),
// }
// )
// await act(async () => {
// result.current.actions.updateDetails(0)(guestDetailsNonMember)
// })
// expect(result.current.activeRoom).toEqual(1)
// await act(async () => {
// result.current.actions.setStep(StepEnum.breakfast)
// })
// expect(result.current.activeRoom).toEqual(1)
// })
// test("can go back and modify room 1 after completion", async () => {
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper(),
// }
// )
// await act(async () => {
// result.current.actions.updateBedType(0)({
// roomTypeCode: bedType.king.value,
// description: bedType.king.description,
// })
// result.current.actions.updateBreakfast(0)(breakfastPackage)
// result.current.actions.updateDetails(0)(guestDetailsNonMember)
// })
// // now we are at room 2
// expect(result.current.activeRoom).toEqual(1)
// await act(async () => {
// result.current.actions.setStep(StepEnum.breakfast) // click "modify"
// })
// expect(result.current.activeRoom).toEqual(1)
// await act(async () => {
// result.current.actions.updateBreakfast(1)(breakfastPackage)
// })
// // going back to room 2
// expect(result.current.activeRoom).toEqual(1)
// })
// test("breakfast step should be hidden when breakfast is included", async () => {
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper({ breakfastPackages: null }),
// }
// )
// const room1 = result.current.rooms[0]
// expect(Object.keys(room1.steps)).not.toContain(StepEnum.breakfast)
// const room2 = result.current.rooms[1]
// expect(Object.keys(room2.steps)).not.toContain(StepEnum.breakfast)
// })
// test("select bed step should be skipped when there is only one bedtype", async () => {
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper({
// bedTypes: [bedType.queen],
// breakfastPackages: [
// {
// code: "TEST",
// description: "Description",
// localPrice: {
// currency: "SEK",
// price: "100",
// totalPrice: "100",
// },
// requestedPrice: {
// currency: "SEK",
// price: "100",
// totalPrice: "100",
// },
// packageType: PackageTypeEnum.BreakfastAdult,
// },
// ],
// }),
// }
// )
// const room1 = result.current.rooms[0]
// expect(room1.steps[StepEnum.selectBed].isValid).toEqual(true)
// expect(room1.currentStep).toEqual(StepEnum.breakfast)
// const room2 = result.current.rooms[1]
// expect(room2.steps[StepEnum.selectBed].isValid).toEqual(true)
// expect(room2.currentStep).toEqual(null)
// })
// describe("price calculation", () => {
// test("total price should be set properly", async () => {
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper(),
// }
// )
// const publicRate = roomRate.publicRate.localPrice.pricePerStay
// const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
// const initialTotalPrice = publicRate * result.current.rooms.length
// expect(result.current.totalPrice.local.price).toEqual(initialTotalPrice)
// // room 1
// await act(async () => {
// result.current.actions.updateBedType(0)({
// roomTypeCode: bedType.king.value,
// description: bedType.king.description,
// })
// result.current.actions.updateBreakfast(0)(breakfastPackage)
// })
// let expectedTotalPrice =
// initialTotalPrice + Number(breakfastPackage.localPrice.price)
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
// await act(async () => {
// result.current.actions.updateDetails(0)(guestDetailsMember)
// })
// expectedTotalPrice =
// memberRate + publicRate + Number(breakfastPackage.localPrice.price)
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
// // room 2
// await act(async () => {
// result.current.actions.updateBedType(1)({
// roomTypeCode: bedType.king.value,
// description: bedType.king.description,
// })
// result.current.actions.updateBreakfast(1)(breakfastPackage)
// })
// expectedTotalPrice =
// memberRate + publicRate + Number(breakfastPackage.localPrice.price) * 2
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
// await act(async () => {
// result.current.actions.updateDetails(1)(guestDetailsNonMember)
// })
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
// })
// test("room price should be set properly", async () => {
// const { result } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper(),
// }
// )
// const publicRate = roomRate.publicRate.localPrice.pricePerStay
// const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
// let room1 = result.current.rooms[0]
// expect(room1.room.roomPrice.perStay.local.price).toEqual(publicRate)
// let room2 = result.current.rooms[0]
// expect(room2.room.roomPrice.perStay.local.price).toEqual(publicRate)
// await act(async () => {
// result.current.actions.updateDetails(0)(guestDetailsMember)
// })
// room1 = result.current.rooms[0]
// expect(room1.room.roomPrice.perStay.local.price).toEqual(memberRate)
// })
// })
// describe("change room", () => {
// test("changing to room with new bedtypes requires selecting bed again", async () => {
// const { result: firstRun } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
// }
// )
// const selectedBedType = {
// roomTypeCode: bedType.king.value,
// description: bedType.king.description,
// }
// // add bedtype
// await act(async () => {
// firstRun.current.actions.updateBedType(0)(selectedBedType)
// })
// await act(async () => {
// firstRun.current.actions.updateBreakfast(0)(false) // 'no breakfast' selected
// })
// await act(async () => {
// firstRun.current.actions.updateDetails(0)(guestDetailsNonMember)
// })
// const updatedBooking = {
// ...booking,
// rooms: booking.rooms.map((r) => ({
// ...r,
// roomTypeCode: "NEW",
// })),
// }
// // render again to change the bedtypes
// const { result: secondRun } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper({
// bookingParams: updatedBooking,
// bedTypes: [bedType.single, bedType.queen],
// }),
// }
// )
// await waitFor(() => {
// const secondRunRoom = secondRun.current.rooms[0]
// // bed type should be unset since the bed types have changed
// expect(secondRunRoom.room.bedType).toEqual(undefined)
// // bed step should be unselected
// expect(secondRunRoom.currentStep).toBe(StepEnum.selectBed)
// expect(secondRunRoom.steps[StepEnum.selectBed].isValid).toBe(false)
// // other steps should still be selected
// expect(secondRunRoom.room.breakfast).toBe(false)
// expect(secondRunRoom.steps[StepEnum.breakfast]?.isValid).toBe(true)
// expect(secondRunRoom.room.guest).toEqual(guestDetailsNonMember)
// expect(secondRunRoom.steps[StepEnum.details].isValid).toBe(true)
// })
// })
// test("changing to room with single bedtype option should skip step", async () => {
// const { result: firstRun } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
// }
// )
// const selectedBedType = {
// roomTypeCode: bedType.king.value,
// description: bedType.king.description,
// }
// // add bedtype
// await act(async () => {
// firstRun.current.actions.updateBedType(0)(selectedBedType)
// })
// await act(async () => {
// firstRun.current.actions.updateBreakfast(0)(breakfastPackage)
// })
// const updatedBooking = {
// ...booking,
// rooms: booking.rooms.map((r) => ({
// ...r,
// roomTypeCode: "NEW",
// })),
// }
// // render again to change the bedtypes
// const { result: secondRun } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper({
// bookingParams: updatedBooking,
// bedTypes: [bedType.queen],
// }),
// }
// )
// await waitFor(() => {
// const secondRunRoom = secondRun.current.rooms[0]
// expect(secondRunRoom.room.bedType).toEqual({
// roomTypeCode: bedType.queen.value,
// description: bedType.queen.description,
// })
// expect(secondRunRoom.steps[StepEnum.selectBed].isValid).toBe(true)
// expect(secondRunRoom.steps[StepEnum.breakfast]?.isValid).toBe(true)
// expect(secondRunRoom.steps[StepEnum.details].isValid).toBe(false)
// expect(secondRunRoom.currentStep).toBe(StepEnum.details)
// })
// })
// test("if booking has changed, stored values should be discarded", async () => {
// const { result: firstRun } = renderHook(
// () => useEnterDetailsStore((state) => state),
// {
// wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
// }
// )
// const selectedBedType = {
// roomTypeCode: bedType.king.value,
// description: bedType.king.description,
// }
// // add bedtype
// await act(async () => {
// firstRun.current.actions.updateBedType(0)(selectedBedType)
// })
// await act(async () => {
// firstRun.current.actions.updateBreakfast(0)(breakfastPackage)
// })
// const updatedBooking = {
// ...booking,
// hotelId: "0001",
// fromDate: "2030-01-01",
// toDate: "2030-01-02",
// }
// renderHook(() => useEnterDetailsStore((state) => state), {
// wrapper: createWrapper({
// bookingParams: updatedBooking,
// bedTypes: [bedType.queen],
// }),
// })
// await waitFor(() => {
// const storageItem = window.sessionStorage.getItem(detailsStorageName)
// expect(storageItem).toBe(null)
// })
// })
// })
// })

View File

@@ -10,8 +10,6 @@ import type {
DetailsState,
PersistedState,
RoomState,
RoomStatus,
RoomStep,
} from "@/types/stores/enter-details"
import type { SafeUser } from "@/types/user"
@@ -160,7 +158,7 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
rate.requestedPrice.pricePerStay
),
}
: undefined,
: total.requested,
local: {
currency: rate.localPrice.currency,
price: add(total.local.price ?? 0, rate.localPrice.pricePerStay),
@@ -179,11 +177,12 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
export function calcTotalPrice(
rooms: RoomState[],
totalPrice: Price,
isMember: boolean
currency: Price["local"]["currency"],
isMember: boolean,
nights: number
) {
return rooms.reduce<Price>(
(acc, room, index) => {
(acc, { room }, index) => {
const isFirstRoomAndMember = index === 0 && isMember
const join = Boolean(room.guest.join || room.guest.membershipNo)
@@ -193,10 +192,10 @@ export function calcTotalPrice(
)
const breakfastRequestedPrice = room.breakfast
? room.breakfast.requestedPrice?.totalPrice ?? 0
? parseInt(room.breakfast.requestedPrice?.price ?? 0)
: 0
const breakfastLocalPrice = room.breakfast
? room.breakfast.localPrice?.totalPrice ?? 0
? parseInt(room.breakfast.localPrice?.price ?? 0)
: 0
const roomFeaturesTotal = room.roomFeatures?.reduce((total, pkg) => {
@@ -213,7 +212,7 @@ export function calcTotalPrice(
price: add(
acc.requested?.price ?? 0,
roomPrice.perStay.requested.price,
breakfastRequestedPrice
breakfastRequestedPrice * room.adults * nights
),
}
: undefined,
@@ -222,7 +221,7 @@ export function calcTotalPrice(
price: add(
acc.local.price,
roomPrice.perStay.local.price,
breakfastLocalPrice,
breakfastLocalPrice * room.adults * nights,
roomFeaturesTotal
),
},
@@ -232,57 +231,40 @@ export function calcTotalPrice(
},
{
requested: undefined,
local: { currency: totalPrice.local.currency, price: 0 },
local: { currency, price: 0 },
}
)
}
export const selectRoomStatus = (state: DetailsState, index?: number) =>
state.bookingProgress.roomStatuses[
index ?? state.bookingProgress.currentRoomIndex
]
export const selectRoom = (state: DetailsState, index?: number) =>
state.rooms[index ?? state.bookingProgress.currentRoomIndex]
export const selectRoomSteps = (state: DetailsState, index?: number) =>
state.bookingProgress.roomStatuses[
index ?? state.bookingProgress.currentRoomIndex
].steps
export const selectPreviousSteps = (
state: DetailsState,
index?: number
): {
[StepEnum.selectBed]?: RoomStep
[StepEnum.breakfast]?: RoomStep
[StepEnum.details]?: RoomStep
} => {
const roomStatus =
state.bookingProgress.roomStatuses[
index ?? state.bookingProgress.currentRoomIndex
]
const stepKeys = Object.keys(roomStatus.steps)
const currentStepIndex = stepKeys.indexOf(`${roomStatus.currentStep}`)
return Object.entries(roomStatus.steps)
.slice(0, currentStepIndex)
.reduce((acc, [key, value]) => {
return { ...acc, [key]: value }
}, {})
export function getFirstInteractiveStepOfRoom(room: RoomState["room"]) {
if (!room.bedType) {
return StepEnum.selectBed
}
if (room.breakfast !== false) {
return StepEnum.breakfast
}
return StepEnum.details
}
export const selectNextStep = (roomStatus: RoomStatus) => {
if (roomStatus.currentStep === null) {
export function findNextInvalidStep(roomState: RoomState) {
return (
Object.values(roomState.steps).find((stp) => !stp.isValid)?.step ??
getFirstInteractiveStepOfRoom(roomState.room)
)
}
export const selectNextStep = (room: RoomState) => {
if (room.currentStep === null) {
throw new Error("getNextStep: currentStep is null")
}
if (!roomStatus.steps[roomStatus.currentStep]?.isValid) {
return roomStatus.currentStep
if (!room.steps[room.currentStep]?.isValid) {
return room.currentStep
}
const stepsArray = Object.values(roomStatus.steps)
const stepsArray = Object.values(room.steps)
const currentIndex = stepsArray.findIndex(
(step) => step?.step === roomStatus.currentStep
(step) => step?.step === room.currentStep
)
if (currentIndex === stepsArray.length - 1) {
return null
@@ -295,52 +277,27 @@ export const selectNextStep = (roomStatus: RoomStatus) => {
return nextInvalidStep?.step ?? null
}
export const selectBookingProgress = (state: DetailsState) =>
state.bookingProgress
export const checkBookingProgress = (state: DetailsState) => {
return state.bookingProgress.roomStatuses.every((r) => r.isComplete)
}
export const checkRoomProgress = (state: DetailsState) => {
const steps = selectRoomSteps(state)
export const checkRoomProgress = (steps: RoomState["steps"]) => {
return Object.values(steps)
.filter(Boolean)
.every((step) => step.isValid)
}
export function handleStepProgression(state: DetailsState) {
const isAllRoomsCompleted = checkBookingProgress(state)
export function handleStepProgression(room: RoomState, state: DetailsState) {
const isAllRoomsCompleted = state.rooms.every((r) => r.isComplete)
if (isAllRoomsCompleted) {
const roomStatus = selectRoomStatus(state)
roomStatus.currentStep = null
state.bookingProgress.canProceedToPayment = true
return
}
room.currentStep = null
state.canProceedToPayment = true
} else if (room.isComplete) {
room.currentStep = null
const nextRoomIndex = state.rooms.findIndex((r) => !r.isComplete)
state.activeRoom = nextRoomIndex
const roomStatus = selectRoomStatus(state)
if (roomStatus.isComplete) {
const nextRoomIndex = state.bookingProgress.roomStatuses.findIndex(
(room) => !room.isComplete
)
roomStatus.lastCompletedStep = roomStatus.currentStep ?? undefined
roomStatus.currentStep = null
const nextRoomStatus = selectRoomStatus(state, nextRoomIndex)
nextRoomStatus.currentStep =
Object.values(nextRoomStatus.steps).find((step) => !step.isValid)?.step ??
StepEnum.selectBed
const nextStep = selectNextStep(nextRoomStatus)
nextRoomStatus.currentStep = nextStep
state.bookingProgress.currentRoomIndex = nextRoomIndex
return
}
const nextStep = selectNextStep(roomStatus)
if (nextStep !== null && roomStatus.currentStep !== null) {
roomStatus.lastCompletedStep = roomStatus.currentStep
roomStatus.currentStep = nextStep
return
const nextRoom = state.rooms[nextRoomIndex]
const nextStep = selectNextStep(nextRoom)
nextRoom.currentStep = nextStep
} else if (selectNextStep(room)) {
room.currentStep = selectNextStep(room)
}
}
@@ -357,11 +314,7 @@ export function readFromSessionStorage(): PersistedState | undefined {
const parsedData = JSON.parse(storedData) as PersistedState
if (
!parsedData.booking ||
!parsedData.rooms ||
!parsedData.bookingProgress
) {
if (!parsedData.booking || !parsedData.rooms) {
return undefined
}

View File

@@ -3,6 +3,8 @@ import { produce } from "immer"
import { useContext } from "react"
import { create, useStore } from "zustand"
import { dt } from "@/lib/dt"
import { DetailsContext } from "@/contexts/Details"
import {
@@ -10,22 +12,20 @@ import {
calcTotalPrice,
checkRoomProgress,
extractGuestFromUser,
findNextInvalidStep,
getRoomPrice,
getTotalPrice,
handleStepProgression,
selectPreviousSteps,
selectRoom,
selectRoomStatus,
writeToSessionStorage,
} from "./helpers"
import type { BreakfastPackages } from "@/types/components/hotelReservation/enterDetails/breakfast"
import { StepEnum } from "@/types/enums/step"
import type {
DetailsState,
InitialState,
RoomState,
RoomStatus,
RoomStep,
} from "@/types/stores/enter-details"
import type { SafeUser } from "@/types/user"
@@ -46,7 +46,8 @@ export const detailsStorageName = "rooms-details-storage"
export function createDetailsStore(
initialState: InitialState,
searchParams: string,
user: SafeUser
user: SafeUser,
breakfastPackages: BreakfastPackages | null
) {
const isMember = !!user
@@ -73,21 +74,6 @@ export function createDetailsStore(
})
const rooms: RoomState[] = initialState.rooms.map((room, idx) => {
return {
...room,
adults: initialState.booking.rooms[idx].adults,
childrenInRoom: initialState.booking.rooms[idx].childrenInRoom,
bedType: room.bedType,
breakfast:
initialState.breakfast === false ? initialState.breakfast : undefined,
guest: isMember
? deepmerge(defaultGuestState, extractGuestFromUser(user))
: defaultGuestState,
roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0),
}
})
const roomStatuses: RoomStatus[] = initialState.rooms.map((room, idx) => {
const steps: RoomStatus["steps"] = {
[StepEnum.selectBed]: {
step: StepEnum.selectBed,
@@ -103,69 +89,112 @@ export function createDetailsStore(
},
}
if (initialState.breakfast === false) {
if (room.breakfastIncluded || !breakfastPackages?.length) {
delete steps[StepEnum.breakfast]
}
const currentStep =
idx === 0
? Object.values(steps).find((step) => !step.isValid)?.step ??
StepEnum.selectBed
: null
Object.values(steps).find((step) => !step.isValid)?.step ??
StepEnum.selectBed
return {
room: {
...room,
adults: initialState.booking.rooms[idx].adults,
childrenInRoom: initialState.booking.rooms[idx].childrenInRoom,
bedType: room.bedType,
breakfast:
!breakfastPackages?.length || room.breakfastIncluded
? false
: undefined,
guest:
isMember && idx === 0
? deepmerge(defaultGuestState, extractGuestFromUser(user))
: defaultGuestState,
roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0),
},
currentStep,
isComplete: false,
currentStep: currentStep,
lastCompletedStep: undefined,
steps,
}
})
return create<DetailsState>()((set, get) => ({
searchParamString: searchParams,
return create<DetailsState>()((set) => ({
activeRoom: 0,
booking: initialState.booking,
breakfast:
initialState.breakfast === false ? initialState.breakfast : undefined,
breakfastPackages,
canProceedToPayment: false,
isSubmittingDisabled: false,
isSummaryOpen: false,
isPriceDetailsModalOpen: false,
lastRoom: initialState.booking.rooms.length - 1,
rooms,
searchParamString: searchParams,
totalPrice: initialTotalPrice,
vat: initialState.vat,
rooms,
bookingProgress: {
currentRoomIndex: 0,
roomStatuses,
canProceedToPayment: false,
},
actions: {
setStep(step: StepEnum | null, roomIndex?: number) {
if (step === null) {
return
}
return set(
produce((state: DetailsState) => {
const currentRoomIndex =
roomIndex ?? state.bookingProgress.currentRoomIndex
const previousSteps = selectPreviousSteps(state, roomIndex)
const arePreviousStepsCompleted = Object.values(
previousSteps
).every((step: RoomStep) => step.isValid)
const arePreviousRoomsCompleted = state.bookingProgress.roomStatuses
.slice(0, currentRoomIndex)
.every((room) => room.isComplete)
const roomStatus = selectRoomStatus(state, roomIndex)
if (arePreviousRoomsCompleted && arePreviousStepsCompleted) {
roomStatus.currentStep = step
if (roomIndex !== undefined) {
state.bookingProgress.currentRoomIndex = roomIndex
setStep(idx) {
return function (step) {
return set(
produce((state: DetailsState) => {
const isSameRoom = idx === state.activeRoom
const room = state.rooms[idx]
if (isSameRoom) {
// Closed same accordion as was open
if (step === room.currentStep) {
if (room.isComplete) {
// Room is complete, move to next room or payment
const nextRoomIdx = state.rooms.findIndex(
(r) => !r.isComplete
)
state.activeRoom = nextRoomIdx
// Done, proceed to payment
if (nextRoomIdx === -1) {
room.currentStep = null
} else {
const nextRoom = state.rooms[nextRoomIdx]
const nextInvalidStep = findNextInvalidStep(nextRoom)
nextRoom.currentStep = nextInvalidStep
}
} else {
room.currentStep = findNextInvalidStep(room)
}
} else {
if (room.steps[step]?.isValid) {
room.currentStep = step
} else {
room.currentStep = findNextInvalidStep(room)
}
}
} else {
const arePreviousRoomsCompleted = state.rooms
.slice(0, idx)
.every((room) => room.isComplete)
if (arePreviousRoomsCompleted) {
state.activeRoom = idx
if (room.steps[step]?.isValid) {
room.currentStep = step
} else {
room.currentStep = findNextInvalidStep(room)
}
} else {
const firstIncompleteRoom = state.rooms.findIndex(
(r) => !r.isComplete
)
state.activeRoom = firstIncompleteRoom
if (firstIncompleteRoom === -1) {
// All rooms are done, proceed to payment
room.currentStep = null
} else {
const nextRoom = state.rooms[firstIncompleteRoom]
nextRoom.currentStep = findNextInvalidStep(nextRoom)
}
}
}
}
})
)
})
)
}
},
setIsSubmittingDisabled(isSubmittingDisabled) {
return set(
@@ -189,170 +218,240 @@ export function createDetailsStore(
})
)
},
togglePriceDetailsModalOpen() {
return set(
produce((state: DetailsState) => {
state.isPriceDetailsModalOpen = !state.isPriceDetailsModalOpen
})
)
},
updateBedType(bedType) {
return set(
produce((state: DetailsState) => {
const roomStatus = selectRoomStatus(state)
roomStatus.steps[StepEnum.selectBed].isValid = true
updateBedType(idx) {
return function (bedType) {
return set(
produce((state: DetailsState) => {
state.rooms[idx].steps[StepEnum.selectBed].isValid = true
state.rooms[idx].room.bedType = bedType
const room = selectRoom(state)
room.bedType = bedType
handleStepProgression(state.rooms[idx], state)
handleStepProgression(state)
writeToSessionStorage({
booking: state.booking,
rooms: state.rooms,
bookingProgress: state.bookingProgress,
writeToSessionStorage({
activeRoom: state.activeRoom,
booking: state.booking,
rooms: state.rooms,
})
})
})
)
)
}
},
updateBreakfast(breakfast) {
return set(
produce((state: DetailsState) => {
const roomStatus = selectRoomStatus(state)
if (roomStatus.steps[StepEnum.breakfast]) {
roomStatus.steps[StepEnum.breakfast].isValid = true
}
updateBreakfast(idx) {
return function (breakfast) {
return set(
produce((state: DetailsState) => {
const currentRoom = state.rooms[idx]
if (currentRoom.steps[StepEnum.breakfast]) {
currentRoom.steps[StepEnum.breakfast].isValid = true
}
const stateTotalRequestedPrice =
state.totalPrice.requested?.price || 0
const currentTotalPriceRequested = state.totalPrice.requested
let stateTotalRequestedPrice = 0
if (currentTotalPriceRequested) {
stateTotalRequestedPrice = currentTotalPriceRequested.price
}
const stateTotalLocalPrice = state.totalPrice.local.price
const stateTotalLocalPrice = state.totalPrice.local.price
const addToTotalPrice =
(state.breakfast === undefined || state.breakfast === false) &&
!!breakfast
const addToTotalPrice =
(currentRoom.room.breakfast === undefined ||
currentRoom.room.breakfast === false) &&
!!breakfast
const subtractFromTotalPrice =
(state.breakfast === undefined || state.breakfast) &&
breakfast === false
const subtractFromTotalPrice =
currentRoom.room.breakfast && breakfast === false
if (addToTotalPrice) {
const breakfastTotalRequestedPrice = parseInt(
breakfast.requestedPrice.totalPrice
)
const breakfastTotalPrice = parseInt(
breakfast.localPrice.totalPrice
const nights = dt(state.booking.toDate).diff(
state.booking.fromDate,
"days"
)
state.totalPrice = {
requested: state.totalPrice.requested && {
currency: state.totalPrice.requested.currency,
price:
stateTotalRequestedPrice + breakfastTotalRequestedPrice,
},
local: {
currency: breakfast.localPrice.currency,
price: stateTotalLocalPrice + breakfastTotalPrice,
},
}
}
if (subtractFromTotalPrice) {
let currency = state.totalPrice.local.currency
let currentBreakfastTotalPrice = 0
let currentBreakfastTotalRequestedPrice = 0
if (state.breakfast) {
currentBreakfastTotalPrice = parseInt(
state.breakfast.localPrice.totalPrice
)
currentBreakfastTotalRequestedPrice = parseInt(
state.breakfast.requestedPrice.totalPrice
)
currency = state.breakfast.localPrice.currency
if (addToTotalPrice) {
const breakfastTotalRequestedPrice =
parseInt(breakfast.requestedPrice.price) *
currentRoom.room.adults *
nights
const breakfastTotalPrice =
parseInt(breakfast.localPrice.price) *
currentRoom.room.adults *
nights
state.totalPrice = {
requested: state.totalPrice.requested && {
currency: state.totalPrice.requested.currency,
price:
stateTotalRequestedPrice + breakfastTotalRequestedPrice,
},
local: {
currency: breakfast.localPrice.currency,
price: stateTotalLocalPrice + breakfastTotalPrice,
},
}
}
let requestedPrice =
stateTotalRequestedPrice - currentBreakfastTotalRequestedPrice
if (requestedPrice < 0) {
requestedPrice = 0
}
let localPrice = stateTotalLocalPrice - currentBreakfastTotalPrice
if (localPrice < 0) {
localPrice = 0
if (subtractFromTotalPrice) {
let currency = state.totalPrice.local.currency
let currentBreakfastTotalPrice = 0
let currentBreakfastTotalRequestedPrice = 0
if (currentRoom.room.breakfast) {
currentBreakfastTotalPrice =
parseInt(currentRoom.room.breakfast.localPrice.price) *
currentRoom.room.adults *
nights
currentBreakfastTotalRequestedPrice =
parseInt(
currentRoom.room.breakfast.requestedPrice.totalPrice
) *
currentRoom.room.adults *
nights
currency = currentRoom.room.breakfast.localPrice.currency
}
let requestedPrice =
stateTotalRequestedPrice - currentBreakfastTotalRequestedPrice
if (requestedPrice < 0) {
requestedPrice = 0
}
let localPrice =
stateTotalLocalPrice - currentBreakfastTotalPrice
if (localPrice < 0) {
localPrice = 0
}
state.totalPrice = {
requested: state.totalPrice.requested && {
currency: state.totalPrice.requested.currency,
price: requestedPrice,
},
local: {
currency,
price: localPrice,
},
}
}
state.totalPrice = {
requested: state.totalPrice.requested && {
currency: state.totalPrice.requested.currency,
price: requestedPrice,
},
local: {
currency,
price: localPrice,
},
}
}
currentRoom.room.breakfast = breakfast
const room = selectRoom(state)
room.breakfast = breakfast
handleStepProgression(currentRoom, state)
handleStepProgression(state)
writeToSessionStorage({
booking: state.booking,
rooms: state.rooms,
bookingProgress: state.bookingProgress,
writeToSessionStorage({
activeRoom: state.activeRoom,
booking: state.booking,
rooms: state.rooms,
})
})
})
)
)
}
},
updateDetails(data) {
return set(
produce((state: DetailsState) => {
const roomStatus = selectRoomStatus(state)
roomStatus.steps[StepEnum.details].isValid = true
updateDetails(idx) {
return function (data) {
return set(
produce((state: DetailsState) => {
state.rooms[idx].steps[StepEnum.details].isValid = true
const room = selectRoom(state)
room.guest.countryCode = data.countryCode
room.guest.dateOfBirth = data.dateOfBirth
room.guest.email = data.email
room.guest.firstName = data.firstName
room.guest.join = data.join
room.guest.lastName = data.lastName
state.rooms[idx].room.guest.countryCode = data.countryCode
state.rooms[idx].room.guest.dateOfBirth = data.dateOfBirth
state.rooms[idx].room.guest.email = data.email
state.rooms[idx].room.guest.firstName = data.firstName
state.rooms[idx].room.guest.join = data.join
state.rooms[idx].room.guest.lastName = data.lastName
if (data.join) {
room.guest.membershipNo = undefined
} else {
room.guest.membershipNo = data.membershipNo
}
room.guest.phoneNumber = data.phoneNumber
room.guest.zipCode = data.zipCode
if (data.join) {
state.rooms[idx].room.guest.membershipNo = undefined
} else {
state.rooms[idx].room.guest.membershipNo = data.membershipNo
}
state.rooms[idx].room.guest.phoneNumber = data.phoneNumber
state.rooms[idx].room.guest.zipCode = data.zipCode
room.roomPrice = getRoomPrice(
room.roomRate,
Boolean(data.join || data.membershipNo || isMember)
)
state.rooms[idx].room.roomPrice = getRoomPrice(
state.rooms[idx].room.roomRate,
Boolean(data.join || data.membershipNo || isMember)
)
state.totalPrice = calcTotalPrice(
state.rooms,
state.totalPrice,
isMember
)
const nights = dt(state.booking.toDate).diff(
state.booking.fromDate,
"days"
)
const isAllStepsCompleted = checkRoomProgress(state)
if (isAllStepsCompleted) {
roomStatus.isComplete = true
}
state.totalPrice = calcTotalPrice(
state.rooms,
state.totalPrice.local.currency,
isMember,
nights
)
handleStepProgression(state)
const isAllStepsCompleted = checkRoomProgress(
state.rooms[idx].steps
)
if (isAllStepsCompleted) {
state.rooms[idx].isComplete = true
}
writeToSessionStorage({
booking: state.booking,
rooms: state.rooms,
bookingProgress: state.bookingProgress,
handleStepProgression(state.rooms[idx], state)
writeToSessionStorage({
activeRoom: state.activeRoom,
booking: state.booking,
rooms: state.rooms,
})
})
})
)
)
}
},
updateMultiroomDetails(idx) {
return function (data) {
return set(
produce((state: DetailsState) => {
state.rooms[idx].steps[StepEnum.details].isValid = true
state.rooms[idx].room.guest.countryCode = data.countryCode
state.rooms[idx].room.guest.email = data.email
state.rooms[idx].room.guest.firstName = data.firstName
state.rooms[idx].room.guest.join = data.join
state.rooms[idx].room.guest.lastName = data.lastName
if (data.join) {
state.rooms[idx].room.guest.membershipNo = undefined
} else {
state.rooms[idx].room.guest.membershipNo = data.membershipNo
}
state.rooms[idx].room.guest.phoneNumber = data.phoneNumber
const getMemberPrice = Boolean(data.join || data.membershipNo)
state.rooms[idx].room.roomPrice = getRoomPrice(
state.rooms[idx].room.roomRate,
getMemberPrice
)
const nights = dt(state.booking.toDate).diff(
state.booking.fromDate,
"days"
)
state.totalPrice = calcTotalPrice(
state.rooms,
state.totalPrice.local.currency,
getMemberPrice,
nights
)
const isAllStepsCompleted = checkRoomProgress(
state.rooms[idx].steps
)
if (isAllStepsCompleted) {
state.rooms[idx].isComplete = true
}
handleStepProgression(state.rooms[idx], state)
writeToSessionStorage({
activeRoom: state.activeRoom,
booking: state.booking,
rooms: state.rooms,
})
})
)
}
},
updateSeachParamString(searchParamString) {
return set(

View File

@@ -1,633 +0,0 @@
import { describe, expect, test } from "@jest/globals"
import { act, renderHook, waitFor } from "@testing-library/react"
import { type PropsWithChildren } from "react"
import { Lang } from "@/constants/languages"
import {
bedType,
booking,
breakfastPackage,
guestDetailsMember,
guestDetailsNonMember,
roomPrice,
roomRate,
} from "@/__mocks__/hotelReservation"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
import { selectRoom, selectRoomStatus } from "./helpers"
import { detailsStorageName, useEnterDetailsStore } from "."
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { StepEnum } from "@/types/enums/step"
import type { PersistedState } from "@/types/stores/enter-details"
jest.mock("react", () => ({
...jest.requireActual("react"),
cache: jest.fn(),
}))
jest.mock("@/server/utils", () => ({
toLang: () => Lang.en,
}))
jest.mock("@/lib/api", () => ({
fetchRetry: jest.fn((fn) => fn),
}))
interface CreateWrapperParams {
showBreakfastStep?: boolean
breakfastIncluded?: boolean
mustBeGuaranteed?: boolean
bookingParams?: SelectRateSearchParams
bedTypes?: BedTypeSelection[]
}
function createWrapper(params: Partial<CreateWrapperParams> = {}) {
const {
showBreakfastStep = true,
breakfastIncluded = false,
mustBeGuaranteed = false,
bookingParams = booking,
bedTypes = [bedType.king, bedType.queen],
} = params
return function Wrapper({ children }: PropsWithChildren) {
return (
<EnterDetailsProvider
booking={bookingParams}
showBreakfastStep={showBreakfastStep}
roomsData={[
{
bedTypes,
packages: null,
mustBeGuaranteed,
breakfastIncluded,
cancellationText: "",
rateDetails: [],
roomType: "Standard",
roomRate: roomRate,
},
{
bedTypes,
packages: null,
mustBeGuaranteed,
breakfastIncluded,
cancellationText: "",
rateDetails: [],
roomType: "Standard",
roomRate: roomRate,
},
]}
searchParamsStr=""
user={null}
vat={0}
>
{children}
</EnterDetailsProvider>
)
}
}
describe("Enter Details Store", () => {
beforeEach(() => {
window.sessionStorage.clear()
})
describe("initial state", () => {
test("initialize with correct default values", () => {
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper(),
}
)
const state = result.current
expect(state.booking).toEqual(booking)
expect(state.breakfast).toEqual(undefined)
// room 1
const room1Status = selectRoomStatus(result.current, 0)
const room1 = selectRoom(result.current, 0)
expect(room1Status.currentStep).toBe(StepEnum.selectBed)
expect(room1.roomPrice.perNight.local.price).toEqual(
roomRate.publicRate.localPrice.pricePerNight
)
expect(room1.bedType).toEqual(undefined)
expect(Object.values(room1.guest).every((value) => value === ""))
// room 2
const room2Status = selectRoomStatus(result.current, 1)
const room2 = selectRoom(result.current, 1)
expect(room2Status.currentStep).toBe(null)
expect(room2.roomPrice.perNight.local.price).toEqual(
room2.roomRate.publicRate.localPrice.pricePerNight
)
expect(room2.bedType).toEqual(undefined)
expect(Object.values(room2.guest).every((value) => value === ""))
})
test("initialize with correct values from session storage", () => {
const storage: PersistedState = {
booking: booking,
bookingProgress: {
currentRoomIndex: 0,
canProceedToPayment: true,
roomStatuses: [
{
isComplete: false,
currentStep: StepEnum.selectBed,
lastCompletedStep: undefined,
steps: {
[StepEnum.selectBed]: {
step: StepEnum.selectBed,
isValid: true,
},
[StepEnum.breakfast]: {
step: StepEnum.breakfast,
isValid: true,
},
[StepEnum.details]: {
step: StepEnum.details,
isValid: true,
},
},
},
],
},
rooms: [
{
roomFeatures: null,
roomRate: roomRate,
roomType: "Classic Double",
cancellationText: "Non-refundable",
rateDetails: [],
bedType: {
roomTypeCode: bedType.king.value,
description: bedType.king.description,
},
adults: 1,
childrenInRoom: [],
breakfast: breakfastPackage,
guest: guestDetailsNonMember,
roomPrice: roomPrice,
},
],
}
window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper(),
}
)
expect(result.current.booking).toEqual(storage.booking)
expect(result.current.rooms[0]).toEqual(storage.rooms[0])
expect(result.current.bookingProgress).toEqual(storage.bookingProgress)
})
})
test("add bedtype and proceed to next step", async () => {
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper(),
}
)
let roomStatus = selectRoomStatus(result.current)
expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)
const selectedBedType = {
roomTypeCode: bedType.king.value,
description: bedType.king.description,
}
await act(async () => {
result.current.actions.updateBedType(selectedBedType)
})
roomStatus = selectRoomStatus(result.current)
const room = selectRoom(result.current)
expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
expect(room.bedType).toEqual(selectedBedType)
expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
})
test("complete step and navigate to next step", async () => {
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper(),
}
)
// Room 1
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
let roomStatus = selectRoomStatus(result.current)
expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)
await act(async () => {
result.current.actions.updateBedType({
roomTypeCode: bedType.king.value,
description: bedType.king.description,
})
})
roomStatus = selectRoomStatus(result.current)
expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
await act(async () => {
result.current.actions.updateBreakfast(breakfastPackage)
})
roomStatus = selectRoomStatus(result.current)
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true)
expect(roomStatus.currentStep).toEqual(StepEnum.details)
await act(async () => {
result.current.actions.updateDetails(guestDetailsNonMember)
})
expect(result.current.bookingProgress.canProceedToPayment).toBe(false)
// Room 2
expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
roomStatus = selectRoomStatus(result.current)
expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)
await act(async () => {
const selectedBedType = {
roomTypeCode: bedType.king.value,
description: bedType.king.description,
}
result.current.actions.updateBedType(selectedBedType)
})
roomStatus = selectRoomStatus(result.current)
expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
await act(async () => {
result.current.actions.updateBreakfast(breakfastPackage)
})
roomStatus = selectRoomStatus(result.current)
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true)
expect(roomStatus.currentStep).toEqual(StepEnum.details)
await act(async () => {
result.current.actions.updateDetails(guestDetailsNonMember)
})
expect(result.current.bookingProgress.canProceedToPayment).toBe(true)
})
test("all steps needs to be completed before going to next room", async () => {
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper(),
}
)
await act(async () => {
result.current.actions.updateDetails(guestDetailsNonMember)
})
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
await act(async () => {
result.current.actions.setStep(StepEnum.breakfast, 1)
})
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
})
test("can go back and modify room 1 after completion", async () => {
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper(),
}
)
await act(async () => {
result.current.actions.updateBedType({
roomTypeCode: bedType.king.value,
description: bedType.king.description,
})
result.current.actions.updateBreakfast(breakfastPackage)
result.current.actions.updateDetails(guestDetailsNonMember)
})
// now we are at room 2
expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
await act(async () => {
result.current.actions.setStep(StepEnum.breakfast, 0) // click "modify"
})
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
await act(async () => {
result.current.actions.updateBreakfast(breakfastPackage)
})
// going back to room 2
expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
})
test("breakfast step should be hidden when breakfast is included", async () => {
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper({ showBreakfastStep: false }),
}
)
const room1Status = selectRoomStatus(result.current, 0)
expect(Object.keys(room1Status.steps)).not.toContain(StepEnum.breakfast)
const room2Status = selectRoomStatus(result.current, 1)
expect(Object.keys(room2Status.steps)).not.toContain(StepEnum.breakfast)
})
test("select bed step should be skipped when there is only one bedtype", async () => {
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper({ bedTypes: [bedType.queen] }),
}
)
const room1Status = selectRoomStatus(result.current, 0)
expect(room1Status.steps[StepEnum.selectBed].isValid).toEqual(true)
expect(room1Status.currentStep).toEqual(StepEnum.breakfast)
const room2Status = selectRoomStatus(result.current, 1)
expect(room2Status.steps[StepEnum.selectBed].isValid).toEqual(true)
expect(room2Status.currentStep).toEqual(null)
})
describe("price calculation", () => {
test("total price should be set properly", async () => {
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper(),
}
)
const publicRate = roomRate.publicRate.localPrice.pricePerStay
const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
const initialTotalPrice = publicRate * result.current.rooms.length
expect(result.current.totalPrice.local.price).toEqual(initialTotalPrice)
// room 1
await act(async () => {
result.current.actions.updateBedType({
roomTypeCode: bedType.king.value,
description: bedType.king.description,
})
result.current.actions.updateBreakfast(breakfastPackage)
})
let expectedTotalPrice =
initialTotalPrice + Number(breakfastPackage.localPrice.price)
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
await act(async () => {
result.current.actions.updateDetails(guestDetailsMember)
})
expectedTotalPrice =
memberRate + publicRate + Number(breakfastPackage.localPrice.price)
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
// room 2
await act(async () => {
result.current.actions.updateBedType({
roomTypeCode: bedType.king.value,
description: bedType.king.description,
})
result.current.actions.updateBreakfast(breakfastPackage)
})
expectedTotalPrice =
memberRate + publicRate + Number(breakfastPackage.localPrice.price) * 2
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
await act(async () => {
result.current.actions.updateDetails(guestDetailsNonMember)
})
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
})
test("room price should be set properly", async () => {
const { result } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper(),
}
)
const publicRate = roomRate.publicRate.localPrice.pricePerStay
const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
let room1 = selectRoom(result.current, 0)
expect(room1.roomPrice.perStay.local.price).toEqual(publicRate)
let room2 = selectRoom(result.current, 0)
expect(room2.roomPrice.perStay.local.price).toEqual(publicRate)
await act(async () => {
result.current.actions.updateDetails(guestDetailsMember)
})
room1 = selectRoom(result.current, 0)
expect(room1.roomPrice.perStay.local.price).toEqual(memberRate)
})
})
describe("change room", () => {
test("changing to room with new bedtypes requires selecting bed again", async () => {
const { result: firstRun } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
}
)
const selectedBedType = {
roomTypeCode: bedType.king.value,
description: bedType.king.description,
}
// add bedtype
await act(async () => {
firstRun.current.actions.updateBedType(selectedBedType)
})
await act(async () => {
firstRun.current.actions.updateBreakfast(false) // 'no breakfast' selected
})
await act(async () => {
firstRun.current.actions.updateDetails(guestDetailsNonMember)
})
const updatedBooking = {
...booking,
rooms: booking.rooms.map((r) => ({
...r,
roomTypeCode: "NEW",
})),
}
// render again to change the bedtypes
const { result: secondRun } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper({
bookingParams: updatedBooking,
bedTypes: [bedType.single, bedType.queen],
}),
}
)
await waitFor(() => {
const room = selectRoom(secondRun.current, 0)
const roomStatus = selectRoomStatus(secondRun.current, 0)
// bed type should be unset since the bed types have changed
expect(room.bedType).toEqual(undefined)
// bed step should be unselected
expect(roomStatus.currentStep).toBe(StepEnum.selectBed)
expect(roomStatus.steps[StepEnum.selectBed].isValid).toBe(false)
// other steps should still be selected
expect(room.breakfast).toBe(false)
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toBe(true)
expect(room.guest).toEqual(guestDetailsNonMember)
expect(roomStatus.steps[StepEnum.details].isValid).toBe(true)
})
})
test("changing to room with single bedtype option should skip step", async () => {
const { result: firstRun } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
}
)
const selectedBedType = {
roomTypeCode: bedType.king.value,
description: bedType.king.description,
}
// add bedtype
await act(async () => {
firstRun.current.actions.updateBedType(selectedBedType)
})
await act(async () => {
firstRun.current.actions.updateBreakfast(breakfastPackage)
})
const updatedBooking = {
...booking,
rooms: booking.rooms.map((r) => ({
...r,
roomTypeCode: "NEW",
})),
}
// render again to change the bedtypes
const { result: secondRun } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper({
bookingParams: updatedBooking,
bedTypes: [bedType.queen],
}),
}
)
await waitFor(() => {
const room = selectRoom(secondRun.current, 0)
const roomStatus = selectRoomStatus(secondRun.current, 0)
expect(room.bedType).toEqual({
roomTypeCode: bedType.queen.value,
description: bedType.queen.description,
})
expect(roomStatus.steps[StepEnum.selectBed].isValid).toBe(true)
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toBe(true)
expect(roomStatus.steps[StepEnum.details].isValid).toBe(false)
expect(roomStatus.currentStep).toBe(StepEnum.details)
})
})
test("if booking has changed, stored values should be discarded", async () => {
const { result: firstRun } = renderHook(
() => useEnterDetailsStore((state) => state),
{
wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
}
)
const selectedBedType = {
roomTypeCode: bedType.king.value,
description: bedType.king.description,
}
// add bedtype
await act(async () => {
firstRun.current.actions.updateBedType(selectedBedType)
})
await act(async () => {
firstRun.current.actions.updateBreakfast(breakfastPackage)
})
const updatedBooking = {
...booking,
hotelId: "0001",
fromDate: "2030-01-01",
toDate: "2030-01-02",
}
renderHook(() => useEnterDetailsStore((state) => state), {
wrapper: createWrapper({
bookingParams: updatedBooking,
bedTypes: [bedType.queen],
}),
})
await waitFor(() => {
const storageItem = window.sessionStorage.getItem(detailsStorageName)
expect(storageItem).toBe(null)
})
})
})
})

View File

@@ -200,7 +200,7 @@ export function createRatesStore({
`room[${idx}].counterratecode`,
isMemberRate
? selectedRate.product.productType.public.rateCode
: selectedRate.product.productType.member?.rateCode ?? ""
: (selectedRate.product.productType.member?.rateCode ?? "")
)
searchParams.set(
`room[${idx}].ratecode`,
@@ -236,6 +236,7 @@ export function createRatesStore({
booking,
filterOptions,
hotelType,
isUserLoggedIn,
packages,
pathname,
petRoomPackage: packages.find(

View File

@@ -1,104 +0,0 @@
import { create } from "zustand"
import { calculateRoomSummary } from "./helper"
import type {
RoomPackageCodeEnum,
RoomPackages,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type {
Child,
Rate,
RateCode,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
export interface RateSummaryParams {
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
availablePackages: RoomPackages
roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }>
selectedPackagesByRoom: Record<number, RoomPackageCodeEnum[]>
}
interface RateSelectionState {
selectedRates: (RateCode | undefined)[]
rateSummary: (Rate | null)[]
isPriceDetailsModalOpen: boolean
isSummaryOpen: boolean
guestsInRooms: { adults: number; children?: Child[] }[]
modifyRateIndex: number | null
modifyRate: (index: number) => void
closeModifyRate: () => void
selectRate: (index: number, rate: RateCode | undefined) => void
initializeRates: (count: number) => void
calculateRateSummary: ({
getFilteredRooms,
availablePackages,
roomCategories,
}: RateSummaryParams) => void
getSelectedRateSummary: () => Rate[]
togglePriceDetailsModalOpen: () => void
toggleSummaryOpen: () => void
setGuestsInRooms: (index: number, adults: number, children?: Child[]) => void
}
export const useRateSelectionStore = create<RateSelectionState>((set, get) => ({
selectedRates: [],
rateSummary: [],
isPriceDetailsModalOpen: false,
isSummaryOpen: false,
guestsInRooms: [{ adults: 1 }],
modifyRateIndex: null,
modifyRate: (index) => set({ modifyRateIndex: index }),
closeModifyRate: () => set({ modifyRateIndex: null }),
selectRate: (index, rate) => {
set((state) => {
const newRates = [...state.selectedRates]
newRates[index] = rate
return {
selectedRates: newRates,
}
})
},
initializeRates: (count) =>
set({ selectedRates: new Array(count).fill(undefined) }),
calculateRateSummary: (params) => {
const { selectedRates } = get()
const summaries = selectedRates.map((selectedRate, roomIndex) => {
if (!selectedRate) return null
return calculateRoomSummary({
selectedRate,
roomIndex,
...params,
})
})
set({ rateSummary: summaries })
},
getSelectedRateSummary: () => {
const { rateSummary } = get()
return rateSummary.filter((summary): summary is Rate => summary !== null)
},
togglePriceDetailsModalOpen: () => {
set((state) => ({
isPriceDetailsModalOpen: !state.isPriceDetailsModalOpen,
}))
},
toggleSummaryOpen: () => {
set((state) => ({
isSummaryOpen: !state.isSummaryOpen,
}))
},
setGuestsInRooms: (index, adults, children) => {
set((state) => ({
guestsInRooms: [
...state.guestsInRooms.slice(0, index),
{ adults, children },
...state.guestsInRooms.slice(index + 1),
],
}))
},
}))