Merged in feat/SW-1076-no-room-availability (pull request #1467)
Feat/SW-1076 no room availability * fix: update booking error codes * feat(SW-1076): handle no room availabilty on enter-details * fix: parse to json in api mutation instead of expecting json * fix: remove 'isComplete' state from sectionAccordion because it was not needed Approved-by: Simon.Emanuelsson
This commit is contained in:
@@ -2,8 +2,8 @@ import { redirect } from "next/navigation"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
BOOKING_CONFIRMATION_NUMBER,
|
BOOKING_CONFIRMATION_NUMBER,
|
||||||
|
BookingErrorCodeEnum,
|
||||||
MEMBERSHIP_FAILED_ERROR,
|
MEMBERSHIP_FAILED_ERROR,
|
||||||
PaymentErrorCodeEnum,
|
|
||||||
} from "@/constants/booking"
|
} from "@/constants/booking"
|
||||||
import {
|
import {
|
||||||
bookingConfirmation,
|
bookingConfirmation,
|
||||||
@@ -70,17 +70,17 @@ export default async function PaymentCallbackPage({
|
|||||||
"errorCode",
|
"errorCode",
|
||||||
error
|
error
|
||||||
? error.errorCode.toString()
|
? error.errorCode.toString()
|
||||||
: PaymentErrorCodeEnum.Failed.toString()
|
: BookingErrorCodeEnum.TransactionFailed
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
console.error(
|
console.error(
|
||||||
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||||
)
|
)
|
||||||
if (status === "cancel") {
|
if (status === "cancel") {
|
||||||
searchObject.set("errorCode", PaymentErrorCodeEnum.Cancelled.toString())
|
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
|
||||||
}
|
}
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
searchObject.set("errorCode", PaymentErrorCodeEnum.Failed.toString())
|
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
|
||||||
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
|
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { notFound } from "next/navigation"
|
import { notFound, redirect } from "next/navigation"
|
||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
|
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||||
import {
|
import {
|
||||||
getBreakfastPackages,
|
getBreakfastPackages,
|
||||||
getHotel,
|
getHotel,
|
||||||
@@ -16,13 +17,17 @@ import RoomOne from "@/components/HotelReservation/EnterDetails/Room/One"
|
|||||||
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
|
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
|
||||||
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
|
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
|
||||||
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
||||||
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
import RoomProvider from "@/providers/Details/RoomProvider"
|
import RoomProvider from "@/providers/Details/RoomProvider"
|
||||||
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
||||||
import { convertSearchParamsToObj } from "@/utils/url"
|
import { convertSearchParamsToObj } from "@/utils/url"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
import type { Room } from "@/types/providers/details/room"
|
import type { Room } from "@/types/providers/details/room"
|
||||||
|
|
||||||
@@ -81,7 +86,8 @@ export default async function DetailsPage({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!roomAvailability) {
|
if (!roomAvailability) {
|
||||||
continue // TODO: handle no room availability
|
// redirect back to select-rate if availability call fails
|
||||||
|
redirect(`${selectRate(lang)}?${selectRoomParams.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
rooms.push({
|
rooms.push({
|
||||||
@@ -98,6 +104,8 @@ export default async function DetailsPage({
|
|||||||
memberRate: roomAvailability?.memberRate,
|
memberRate: roomAvailability?.memberRate,
|
||||||
publicRate: roomAvailability.publicRate,
|
publicRate: roomAvailability.publicRate,
|
||||||
},
|
},
|
||||||
|
isAvailable:
|
||||||
|
roomAvailability.selectedRoom.status === AvailabilityEnum.Available,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,8 +147,11 @@ export default async function DetailsPage({
|
|||||||
// region: hotel?.address.city,
|
// region: hotel?.address.city,
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
const firstRoom = rooms[0]
|
const firstRoom = rooms[0]
|
||||||
const multirooms = rooms.slice(1)
|
const multirooms = rooms.slice(1)
|
||||||
|
const isRoomNotAvailable = rooms.some((room) => !room.isAvailable)
|
||||||
return (
|
return (
|
||||||
<EnterDetailsProvider
|
<EnterDetailsProvider
|
||||||
booking={booking}
|
booking={booking}
|
||||||
@@ -153,6 +164,21 @@ export default async function DetailsPage({
|
|||||||
<main>
|
<main>
|
||||||
<HotelHeader hotelData={hotelData} />
|
<HotelHeader hotelData={hotelData} />
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
{isRoomNotAvailable && (
|
||||||
|
<Alert
|
||||||
|
type={AlertTypeEnum.Alarm}
|
||||||
|
variant="inline"
|
||||||
|
heading={intl.formatMessage({ id: "Room sold out" })}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
id: "Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.",
|
||||||
|
})}
|
||||||
|
link={{
|
||||||
|
title: intl.formatMessage({ id: "Change room" }),
|
||||||
|
url: `${selectRate(lang)}?${selectRoomParams.toString()}`,
|
||||||
|
keepSearchParams: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<RoomProvider idx={0} room={firstRoom}>
|
<RoomProvider idx={0} room={firstRoom}>
|
||||||
<RoomOne user={user} />
|
<RoomOne user={user} />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { FormProvider, useForm } from "react-hook-form"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BookingErrorCodeEnum,
|
||||||
BookingStatusEnum,
|
BookingStatusEnum,
|
||||||
PAYMENT_METHOD_TITLES,
|
PAYMENT_METHOD_TITLES,
|
||||||
PaymentMethodEnum,
|
PaymentMethodEnum,
|
||||||
@@ -107,6 +108,15 @@ export default function PaymentClient({
|
|||||||
const initiateBooking = trpc.booking.create.useMutation({
|
const initiateBooking = trpc.booking.create.useMutation({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
|
if ("error" in result) {
|
||||||
|
if (result.cause === BookingErrorCodeEnum.AvailabilityError) {
|
||||||
|
window.location.reload() // reload to refetch room data because we dont know which room is unavailable
|
||||||
|
} else {
|
||||||
|
handlePaymentError(result.cause)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setBookingNumber(result.id)
|
setBookingNumber(result.id)
|
||||||
|
|
||||||
const priceChange = result.rooms.find(
|
const priceChange = result.rooms.find(
|
||||||
|
|||||||
@@ -25,14 +25,12 @@ export default function SectionAccordion({
|
|||||||
actions: { setStep },
|
actions: { setStep },
|
||||||
currentStep,
|
currentStep,
|
||||||
isActiveRoom,
|
isActiveRoom,
|
||||||
room: { bedType, breakfast },
|
room: { bedType, breakfast, isAvailable },
|
||||||
steps,
|
steps,
|
||||||
} = useRoomContext()
|
} = useRoomContext()
|
||||||
|
|
||||||
const [isComplete, setIsComplete] = useState(false)
|
const isStepComplete = !!(steps[step]?.isValid && isAvailable)
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const isValid = steps[step]?.isValid ?? false
|
|
||||||
|
|
||||||
const [title, setTitle] = useState(label)
|
const [title, setTitle] = useState(label)
|
||||||
|
|
||||||
const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" })
|
const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" })
|
||||||
@@ -54,14 +52,10 @@ export default function SectionAccordion({
|
|||||||
}
|
}
|
||||||
}, [bedType, breakfast, setTitle, step, breakfastTitle, noBreakfastTitle])
|
}, [bedType, breakfast, setTitle, step, breakfastTitle, noBreakfastTitle])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsComplete(isValid)
|
|
||||||
}, [isValid, setIsComplete])
|
|
||||||
|
|
||||||
const accordionRef = useRef<HTMLDivElement>(null)
|
const accordionRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shouldBeOpen = currentStep === step && isActiveRoom
|
const shouldBeOpen = currentStep === step && isActiveRoom && isAvailable
|
||||||
setIsOpen(shouldBeOpen)
|
setIsOpen(shouldBeOpen)
|
||||||
|
|
||||||
// Scroll to this section when it is opened,
|
// Scroll to this section when it is opened,
|
||||||
@@ -91,7 +85,7 @@ export default function SectionAccordion({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentStep, isActiveRoom, setIsOpen, step])
|
}, [currentStep, isActiveRoom, isAvailable, setIsOpen, step])
|
||||||
|
|
||||||
function goToStep() {
|
function goToStep() {
|
||||||
setStep(step)
|
setStep(step)
|
||||||
@@ -103,7 +97,7 @@ export default function SectionAccordion({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const textColor =
|
const textColor =
|
||||||
isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled"
|
isStepComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled"
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.accordion}
|
className={styles.accordion}
|
||||||
@@ -112,8 +106,8 @@ export default function SectionAccordion({
|
|||||||
ref={accordionRef}
|
ref={accordionRef}
|
||||||
>
|
>
|
||||||
<div className={styles.iconWrapper}>
|
<div className={styles.iconWrapper}>
|
||||||
<div className={styles.circle} data-checked={isComplete}>
|
<div className={styles.circle} data-checked={isStepComplete}>
|
||||||
{isComplete ? (
|
{isStepComplete ? (
|
||||||
<CheckIcon color="white" height="16" width="16" />
|
<CheckIcon color="white" height="16" width="16" />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -121,7 +115,7 @@ export default function SectionAccordion({
|
|||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<button
|
<button
|
||||||
onClick={isOpen ? close : goToStep}
|
onClick={isOpen ? close : goToStep}
|
||||||
disabled={!isComplete}
|
disabled={!isStepComplete}
|
||||||
className={styles.modifyButton}
|
className={styles.modifyButton}
|
||||||
>
|
>
|
||||||
<Footnote
|
<Footnote
|
||||||
@@ -136,7 +130,7 @@ export default function SectionAccordion({
|
|||||||
<Subtitle className={styles.selection} type="two" color={textColor}>
|
<Subtitle className={styles.selection} type="two" color={textColor}>
|
||||||
{title}
|
{title}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
{isComplete && (
|
{isStepComplete && (
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={`${styles.button} ${isOpen ? styles.buttonOpen : ""}`}
|
className={`${styles.button} ${isOpen ? styles.buttonOpen : ""}`}
|
||||||
color="burgundy"
|
color="burgundy"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
|||||||
|
|
||||||
import { CheckIcon, EditIcon } from "@/components/Icons"
|
import { CheckIcon, EditIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { useRoomContext } from "@/contexts/Details/Room"
|
import { useRoomContext } from "@/contexts/Details/Room"
|
||||||
@@ -39,7 +40,7 @@ export default function SelectedRoom() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper} data-available={room.isAvailable}>
|
||||||
<div className={styles.iconWrapper}>
|
<div className={styles.iconWrapper}>
|
||||||
<div className={styles.circle}>
|
<div className={styles.circle}>
|
||||||
<CheckIcon color="white" height="16" width="16" />
|
<CheckIcon color="white" height="16" width="16" />
|
||||||
@@ -74,13 +75,15 @@ export default function SelectedRoom() {
|
|||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Button
|
<Button
|
||||||
variant="icon"
|
variant="icon"
|
||||||
|
intent="text"
|
||||||
size="small"
|
size="small"
|
||||||
color="burgundy"
|
|
||||||
onClick={changeRoom}
|
onClick={changeRoom}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
<EditIcon color="burgundy" />
|
<EditIcon color="burgundy" />
|
||||||
{intl.formatMessage({ id: "Change room" })}
|
<Caption color="burgundy" type="bold">
|
||||||
|
{intl.formatMessage({ id: "Change room" })}
|
||||||
|
</Caption>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{room.roomTypeCode && (
|
{room.roomTypeCode && (
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
gap: var(--Spacing-x-one-and-half);
|
gap: var(--Spacing-x-one-and-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper[data-available="false"] .title,
|
||||||
|
.wrapper[data-available="false"] .description,
|
||||||
|
.wrapper[data-available="false"] .details {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
@@ -52,6 +59,10 @@
|
|||||||
background-color: var(--UI-Input-Controls-Fill-Selected);
|
background-color: var(--UI-Input-Controls-Fill-Selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper[data-available="false"] .circle {
|
||||||
|
background-color: var(--Base-Surface-Subtle-Hover);
|
||||||
|
}
|
||||||
|
|
||||||
.rate {
|
.rate {
|
||||||
color: var(--UI-Text-Placeholder);
|
color: var(--UI-Text-Placeholder);
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ export default function SummaryUI({
|
|||||||
totalPrice.local.currency
|
totalPrice.local.currency
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
{totalPrice.local.regularPrice && (
|
{totalPrice.local.regularPrice ? (
|
||||||
<Caption color="uiTextMediumContrast" striked={true}>
|
<Caption color="uiTextMediumContrast" striked={true}>
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
intl,
|
intl,
|
||||||
@@ -389,7 +389,7 @@ export default function SummaryUI({
|
|||||||
totalPrice.local.currency
|
totalPrice.local.currency
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
)}
|
) : null}
|
||||||
{totalPrice.requested && (
|
{totalPrice.requested && (
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const rooms: RoomState[] = [
|
|||||||
roomRate: roomRate,
|
roomRate: roomRate,
|
||||||
roomType: "Standard",
|
roomType: "Standard",
|
||||||
roomTypeCode: "QS",
|
roomTypeCode: "QS",
|
||||||
|
isAvailable: true,
|
||||||
},
|
},
|
||||||
steps: {
|
steps: {
|
||||||
[StepEnum.selectBed]: {
|
[StepEnum.selectBed]: {
|
||||||
@@ -100,6 +101,7 @@ const rooms: RoomState[] = [
|
|||||||
roomRate: roomRate,
|
roomRate: roomRate,
|
||||||
roomType: "Standard",
|
roomType: "Standard",
|
||||||
roomTypeCode: "QS",
|
roomTypeCode: "QS",
|
||||||
|
isAvailable: true,
|
||||||
},
|
},
|
||||||
steps: {
|
steps: {
|
||||||
[StepEnum.selectBed]: {
|
[StepEnum.selectBed]: {
|
||||||
|
|||||||
@@ -64,10 +64,16 @@ export enum PaymentMethodEnum {
|
|||||||
discover = "discover",
|
discover = "discover",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PaymentErrorCodeEnum {
|
export enum BookingErrorCodeEnum {
|
||||||
Abandoned = 5,
|
InternalError = "InternalError",
|
||||||
Cancelled = 6,
|
ReservationError = "ReservationError",
|
||||||
Failed = 7,
|
AvailabilityError = "AvailabilityError",
|
||||||
|
BookingStatusNotFound = "BookingStatusNotFound",
|
||||||
|
TransactionAbandoned = "TransactionAbandoned",
|
||||||
|
TransactionCancelled = "TransactionCancelled",
|
||||||
|
TransactionFailed = "TransactionFailed",
|
||||||
|
BookingStateError = "BookingStateError",
|
||||||
|
MembershipFailedError = "MembershipFailedError",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PAYMENT_METHOD_TITLES: Record<
|
export const PAYMENT_METHOD_TITLES: Record<
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|||||||
import { useCallback, useEffect } from "react"
|
import { useCallback, useEffect } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { PaymentErrorCodeEnum } from "@/constants/booking"
|
import { BookingErrorCodeEnum } from "@/constants/booking"
|
||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
@@ -19,9 +19,9 @@ export function usePaymentFailedToast() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const getErrorMessage = useCallback(
|
const getErrorMessage = useCallback(
|
||||||
(errorCode: PaymentErrorCodeEnum) => {
|
(errorCode: string | null) => {
|
||||||
switch (errorCode) {
|
switch (errorCode) {
|
||||||
case PaymentErrorCodeEnum.Cancelled:
|
case BookingErrorCodeEnum.TransactionCancelled:
|
||||||
return intl.formatMessage({
|
return intl.formatMessage({
|
||||||
id: "You have now cancelled your payment.",
|
id: "You have now cancelled your payment.",
|
||||||
})
|
})
|
||||||
@@ -34,8 +34,7 @@ export function usePaymentFailedToast() {
|
|||||||
[intl]
|
[intl]
|
||||||
)
|
)
|
||||||
|
|
||||||
const errorCodeString = searchParams.get("errorCode")
|
const errorCode = searchParams.get("errorCode")
|
||||||
const errorCode = Number(errorCodeString) as PaymentErrorCodeEnum
|
|
||||||
const errorMessage = getErrorMessage(errorCode)
|
const errorMessage = getErrorMessage(errorCode)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,7 +43,9 @@ export function usePaymentFailedToast() {
|
|||||||
// setTimeout is needed to show toasts on page load: https://sonner.emilkowal.ski/toast#render-toast-on-page-load
|
// setTimeout is needed to show toasts on page load: https://sonner.emilkowal.ski/toast#render-toast-on-page-load
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const toastType =
|
const toastType =
|
||||||
errorCode === PaymentErrorCodeEnum.Cancelled ? "warning" : "error"
|
errorCode === BookingErrorCodeEnum.TransactionCancelled
|
||||||
|
? "warning"
|
||||||
|
: "error"
|
||||||
|
|
||||||
toast[toastType](errorMessage)
|
toast[toastType](errorMessage)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -575,6 +575,7 @@
|
|||||||
"Room charge": "Værelsesafgift",
|
"Room charge": "Værelsesafgift",
|
||||||
"Room details": "Room details",
|
"Room details": "Room details",
|
||||||
"Room facilities": "Værelsesfaciliteter",
|
"Room facilities": "Værelsesfaciliteter",
|
||||||
|
"Room sold out": "Værelse solgt ud",
|
||||||
"Room total": "Værelse total",
|
"Room total": "Værelse total",
|
||||||
"Room {roomIndex}": "Værelse {roomIndex}",
|
"Room {roomIndex}": "Værelse {roomIndex}",
|
||||||
"Rooms": "Værelser",
|
"Rooms": "Værelser",
|
||||||
@@ -686,6 +687,7 @@
|
|||||||
"Type of bed": "Sengtype",
|
"Type of bed": "Sengtype",
|
||||||
"Type of room": "Værelsestype",
|
"Type of room": "Værelsestype",
|
||||||
"U-shape": "U-form",
|
"U-shape": "U-form",
|
||||||
|
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Desværre er et af de værelser, du har valgt, solgt ud. Vælg et andet værelse for at fortsætte.",
|
||||||
"Unlink accounts": "Unlink accounts",
|
"Unlink accounts": "Unlink accounts",
|
||||||
"Unpaid": "Ikke betalt",
|
"Unpaid": "Ikke betalt",
|
||||||
"Until {time}, {date}": "Indtil {time} den {date}",
|
"Until {time}, {date}": "Indtil {time} den {date}",
|
||||||
|
|||||||
@@ -574,6 +574,7 @@
|
|||||||
"Room charge": "Zimmerpreis",
|
"Room charge": "Zimmerpreis",
|
||||||
"Room details": "Room details",
|
"Room details": "Room details",
|
||||||
"Room facilities": "Zimmerausstattung",
|
"Room facilities": "Zimmerausstattung",
|
||||||
|
"Room sold out": "Zimmer verkauft",
|
||||||
"Room total": "Zimmer total",
|
"Room total": "Zimmer total",
|
||||||
"Room {roomIndex}": "Zimmer {roomIndex}",
|
"Room {roomIndex}": "Zimmer {roomIndex}",
|
||||||
"Rooms": "Räume",
|
"Rooms": "Räume",
|
||||||
@@ -684,6 +685,7 @@
|
|||||||
"Type of bed": "Bettentyp",
|
"Type of bed": "Bettentyp",
|
||||||
"Type of room": "Zimmerart",
|
"Type of room": "Zimmerart",
|
||||||
"U-shape": "U-shape",
|
"U-shape": "U-shape",
|
||||||
|
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Leider ist eines der von Ihnen ausgewählten Zimmer verkauft. Bitte wählen Sie ein anderes Zimmer, um fortzufahren.",
|
||||||
"Unlink accounts": "Unlink accounts",
|
"Unlink accounts": "Unlink accounts",
|
||||||
"Unpaid": "Nicht bezahlt",
|
"Unpaid": "Nicht bezahlt",
|
||||||
"Until {time}, {date}": "Bis {time} am {date}",
|
"Until {time}, {date}": "Bis {time} am {date}",
|
||||||
|
|||||||
@@ -581,6 +581,7 @@
|
|||||||
"Room charge": "Room charge",
|
"Room charge": "Room charge",
|
||||||
"Room details": "Room details",
|
"Room details": "Room details",
|
||||||
"Room facilities": "Room facilities",
|
"Room facilities": "Room facilities",
|
||||||
|
"Room sold out": "Room sold out",
|
||||||
"Room total": "Room total",
|
"Room total": "Room total",
|
||||||
"Room {roomIndex}": "Room {roomIndex}",
|
"Room {roomIndex}": "Room {roomIndex}",
|
||||||
"Rooms": "Rooms",
|
"Rooms": "Rooms",
|
||||||
@@ -692,6 +693,7 @@
|
|||||||
"Type of bed": "Type of bed",
|
"Type of bed": "Type of bed",
|
||||||
"Type of room": "Type of room",
|
"Type of room": "Type of room",
|
||||||
"U-shape": "U-shape",
|
"U-shape": "U-shape",
|
||||||
|
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.",
|
||||||
"Unlink accounts": "Unlink accounts",
|
"Unlink accounts": "Unlink accounts",
|
||||||
"Unpaid": "Unpaid",
|
"Unpaid": "Unpaid",
|
||||||
"Until {time}, {date}": "Until {time}, {date}",
|
"Until {time}, {date}": "Until {time}, {date}",
|
||||||
|
|||||||
@@ -573,6 +573,7 @@
|
|||||||
"Room charge": "Huonemaksu",
|
"Room charge": "Huonemaksu",
|
||||||
"Room details": "Room details",
|
"Room details": "Room details",
|
||||||
"Room facilities": "Huoneen varustelu",
|
"Room facilities": "Huoneen varustelu",
|
||||||
|
"Room sold out": "Huone slutsattu",
|
||||||
"Room total": "Huoneen kokonaishinta",
|
"Room total": "Huoneen kokonaishinta",
|
||||||
"Room {roomIndex}": "Huone {roomIndex}",
|
"Room {roomIndex}": "Huone {roomIndex}",
|
||||||
"Rooms": "Huoneet",
|
"Rooms": "Huoneet",
|
||||||
@@ -684,6 +685,7 @@
|
|||||||
"Type of bed": "Vuodetyyppi",
|
"Type of bed": "Vuodetyyppi",
|
||||||
"Type of room": "Huonetyyppi",
|
"Type of room": "Huonetyyppi",
|
||||||
"U-shape": "U-muoto",
|
"U-shape": "U-muoto",
|
||||||
|
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Valitettavasti valitsemasi huone on loppuunmyyty. Valitse toinen huone jatkaaksesi.",
|
||||||
"Unlink accounts": "Unlink accounts",
|
"Unlink accounts": "Unlink accounts",
|
||||||
"Unpaid": "Maksettaa",
|
"Unpaid": "Maksettaa",
|
||||||
"Until {time}, {date}": "Asti {time}, {date}",
|
"Until {time}, {date}": "Asti {time}, {date}",
|
||||||
|
|||||||
@@ -572,6 +572,7 @@
|
|||||||
"Room charge": "Rumspris",
|
"Room charge": "Rumspris",
|
||||||
"Room details": "Room details",
|
"Room details": "Room details",
|
||||||
"Room facilities": "Rumfaciliteter",
|
"Room facilities": "Rumfaciliteter",
|
||||||
|
"Room sold out": "Rum slutsålt",
|
||||||
"Room total": "Rum total",
|
"Room total": "Rum total",
|
||||||
"Room {roomIndex}": "Rum {roomIndex}",
|
"Room {roomIndex}": "Rum {roomIndex}",
|
||||||
"Rooms": "Rum",
|
"Rooms": "Rum",
|
||||||
@@ -682,6 +683,7 @@
|
|||||||
"Type of bed": "Sängtyp",
|
"Type of bed": "Sängtyp",
|
||||||
"Type of room": "Rumstyp",
|
"Type of room": "Rumstyp",
|
||||||
"U-shape": "U-form",
|
"U-shape": "U-form",
|
||||||
|
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Tyvärr, ett av de rum du valde är slutsålt. Vänligen välj ett annat rum för att fortsätta.",
|
||||||
"Unlink accounts": "Unlink accounts",
|
"Unlink accounts": "Unlink accounts",
|
||||||
"Unpaid": "Ej betalt",
|
"Unpaid": "Ej betalt",
|
||||||
"Until {time}, {date}": "Tills {time} den {date}",
|
"Until {time}, {date}": "Tills {time} den {date}",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
"react-international-phone": "^4.2.6",
|
"react-international-phone": "^4.2.6",
|
||||||
"react-intl": "^6.6.8",
|
"react-intl": "^6.6.8",
|
||||||
"react-to-print": "^3.0.2",
|
"react-to-print": "^3.0.2",
|
||||||
|
"secure-json-parse": "^4.0.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.7.0",
|
"sonner": "^1.7.0",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default function EnterDetailsProvider({
|
|||||||
rooms: rooms
|
rooms: rooms
|
||||||
.filter((r) => r.bedTypes?.length) // TODO: how to handle room without bedtypes?
|
.filter((r) => r.bedTypes?.length) // TODO: how to handle room without bedtypes?
|
||||||
.map((room) => ({
|
.map((room) => ({
|
||||||
|
isAvailable: room.isAvailable,
|
||||||
breakfastIncluded: !!room.breakfastIncluded,
|
breakfastIncluded: !!room.breakfastIncluded,
|
||||||
cancellationText: room.cancellationText,
|
cancellationText: room.cancellationText,
|
||||||
rateDetails: room.rateDetails,
|
rateDetails: room.rateDetails,
|
||||||
@@ -86,6 +87,10 @@ export default function EnterDetailsProvider({
|
|||||||
// since store is readonly
|
// since store is readonly
|
||||||
const currentRoom = deepmerge({}, store.rooms[idx])
|
const currentRoom = deepmerge({}, store.rooms[idx])
|
||||||
|
|
||||||
|
if (!currentRoom.room.isAvailable) {
|
||||||
|
return currentRoom
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentRoom.room.bedType && storedRoom.room.bedType) {
|
if (!currentRoom.room.bedType && storedRoom.room.bedType) {
|
||||||
const sameBed = currentRoom.room.bedTypes.find(
|
const sameBed = currentRoom.room.bedTypes.find(
|
||||||
(bedType) => bedType.value === storedRoom.room.bedType?.roomTypeCode
|
(bedType) => bedType.value === storedRoom.room.bedType?.roomTypeCode
|
||||||
@@ -134,7 +139,9 @@ export default function EnterDetailsProvider({
|
|||||||
return currentRoom
|
return currentRoom
|
||||||
})
|
})
|
||||||
|
|
||||||
const canProceedToPayment = updatedRooms.every((room) => room.isComplete)
|
const canProceedToPayment = updatedRooms.every(
|
||||||
|
(room) => room.isComplete && room.room.isAvailable
|
||||||
|
)
|
||||||
|
|
||||||
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||||
const currency = (updatedRooms[0].room.roomRate.publicRate?.localPrice
|
const currency = (updatedRooms[0].room.roomRate.publicRate?.localPrice
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { metrics } from "@opentelemetry/api"
|
import { metrics } from "@opentelemetry/api"
|
||||||
|
import sjson from "secure-json-parse"
|
||||||
|
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
import { getVerifiedUser } from "@/server/routers/user/query"
|
import { getVerifiedUser } from "@/server/routers/user/query"
|
||||||
@@ -11,8 +12,8 @@ import {
|
|||||||
cancelBookingInput,
|
cancelBookingInput,
|
||||||
createBookingInput,
|
createBookingInput,
|
||||||
priceChangeInput,
|
priceChangeInput,
|
||||||
updateBookingInput,
|
|
||||||
removePackageInput,
|
removePackageInput,
|
||||||
|
updateBookingInput,
|
||||||
} from "./input"
|
} from "./input"
|
||||||
import { createBookingSchema } from "./output"
|
import { createBookingSchema } from "./output"
|
||||||
|
|
||||||
@@ -135,10 +136,17 @@ export const bookingMutationRouter = router({
|
|||||||
error: {
|
error: {
|
||||||
status: apiResponse.status,
|
status: apiResponse.status,
|
||||||
statusText: apiResponse.statusText,
|
statusText: apiResponse.statusText,
|
||||||
error: text,
|
text,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const apiJson = sjson.safeParse(text)
|
||||||
|
if ("errors" in apiJson && apiJson.errors.length) {
|
||||||
|
const error = apiJson.errors[0]
|
||||||
|
return { error: true, cause: error.code } as const
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ import {
|
|||||||
} from "./utils"
|
} from "./utils"
|
||||||
|
|
||||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
|
||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
@@ -595,7 +594,6 @@ export const hotelQueryRouter = router({
|
|||||||
bookingCode,
|
bookingCode,
|
||||||
rateCode,
|
rateCode,
|
||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
packageCodes,
|
|
||||||
} = input
|
} = input
|
||||||
|
|
||||||
const params: Record<string, string | number | undefined> = {
|
const params: Record<string, string | number | undefined> = {
|
||||||
@@ -691,27 +689,11 @@ export const hotelQueryRouter = router({
|
|||||||
ctx.serviceToken
|
ctx.serviceToken
|
||||||
)
|
)
|
||||||
|
|
||||||
const availableRooms =
|
const rooms = validateAvailabilityData.data.roomConfigurations
|
||||||
validateAvailabilityData.data.roomConfigurations.filter((room) => {
|
const selectedRoom = rooms.find(
|
||||||
if (packageCodes) {
|
|
||||||
return (
|
|
||||||
room.status === AvailabilityEnum.Available &&
|
|
||||||
room.features.some(
|
|
||||||
(feature) =>
|
|
||||||
packageCodes.includes(feature.code) && feature.inventory > 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return room.status === AvailabilityEnum.Available
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedRoom = availableRooms.find(
|
|
||||||
(room) => room.roomTypeCode === roomTypeCode
|
(room) => room.roomTypeCode === roomTypeCode
|
||||||
)
|
)
|
||||||
|
|
||||||
const availableRoomsInCategory = availableRooms.filter(
|
|
||||||
(room) => room.roomType === selectedRoom?.roomType
|
|
||||||
)
|
|
||||||
if (!selectedRoom) {
|
if (!selectedRoom) {
|
||||||
metrics.selectedRoomAvailability.fail.add(1, {
|
metrics.selectedRoomAvailability.fail.add(1, {
|
||||||
hotelId,
|
hotelId,
|
||||||
@@ -720,6 +702,7 @@ export const hotelQueryRouter = router({
|
|||||||
adults,
|
adults,
|
||||||
children,
|
children,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
|
roomTypeCode,
|
||||||
error_type: "not_found",
|
error_type: "not_found",
|
||||||
error: `Couldn't find selected room with input: ${roomTypeCode}`,
|
error: `Couldn't find selected room with input: ${roomTypeCode}`,
|
||||||
})
|
})
|
||||||
@@ -727,6 +710,10 @@ export const hotelQueryRouter = router({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const availableRoomsInCategory = rooms.filter(
|
||||||
|
(room) => room.roomType === selectedRoom?.roomType
|
||||||
|
)
|
||||||
|
|
||||||
const rateTypes = selectedRoom.products.find(
|
const rateTypes = selectedRoom.products.find(
|
||||||
(rate) =>
|
(rate) =>
|
||||||
rate.public?.rateCode === rateCode ||
|
rate.public?.rateCode === rateCode ||
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ export function extractGuestFromUser(user: NonNullable<SafeUser>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function checkIsSameBooking(
|
export function checkIsSameBooking(
|
||||||
prev: SelectRateSearchParams,
|
prev: SelectRateSearchParams & { errorCode?: string },
|
||||||
next: SelectRateSearchParams
|
next: SelectRateSearchParams & { errorCode?: string }
|
||||||
) {
|
) {
|
||||||
const { rooms: prevRooms, ...prevBooking } = prev
|
const { rooms: prevRooms, errorCode: prevErrorCode, ...prevBooking } = prev
|
||||||
|
|
||||||
const prevRoomsWithoutRateCodes = prevRooms.map(
|
const prevRoomsWithoutRateCodes = prevRooms.map(
|
||||||
({ rateCode, counterRateCode, roomTypeCode, ...room }) => room
|
({ rateCode, counterRateCode, roomTypeCode, ...room }) => room
|
||||||
)
|
)
|
||||||
const { rooms: nextRooms, ...nextBooking } = next
|
const { rooms: nextRooms, errorCode: nextErrorCode, ...nextBooking } = next
|
||||||
|
|
||||||
const nextRoomsWithoutRateCodes = nextRooms.map(
|
const nextRoomsWithoutRateCodes = nextRooms.map(
|
||||||
({ rateCode, counterRateCode, roomTypeCode, ...room }) => room
|
({ rateCode, counterRateCode, roomTypeCode, ...room }) => room
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface Room {
|
|||||||
roomRate: RoomRate
|
roomRate: RoomRate
|
||||||
roomType: string
|
roomType: string
|
||||||
roomTypeCode: string
|
roomTypeCode: string
|
||||||
|
isAvailable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomProviderProps extends React.PropsWithChildren {
|
export interface RoomProviderProps extends React.PropsWithChildren {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
import type { Packages } from "../requests/packages"
|
import type { Packages } from "../requests/packages"
|
||||||
|
|
||||||
export interface InitialRoomData {
|
export interface InitialRoomData {
|
||||||
|
isAvailable: boolean
|
||||||
bedType?: BedTypeSchema // used when there is only one bedtype to preselect it
|
bedType?: BedTypeSchema // used when there is only one bedtype to preselect it
|
||||||
bedTypes: BedTypeSelection[]
|
bedTypes: BedTypeSelection[]
|
||||||
breakfastIncluded: boolean
|
breakfastIncluded: boolean
|
||||||
|
|||||||
@@ -6147,6 +6147,7 @@ __metadata:
|
|||||||
react-intl: "npm:^6.6.8"
|
react-intl: "npm:^6.6.8"
|
||||||
react-to-print: "npm:^3.0.2"
|
react-to-print: "npm:^3.0.2"
|
||||||
schema-dts: "npm:^1.1.2"
|
schema-dts: "npm:^1.1.2"
|
||||||
|
secure-json-parse: "npm:^4.0.0"
|
||||||
server-only: "npm:^0.0.1"
|
server-only: "npm:^0.0.1"
|
||||||
slugify: "npm:^1.6.6"
|
slugify: "npm:^1.6.6"
|
||||||
sonner: "npm:^1.7.0"
|
sonner: "npm:^1.7.0"
|
||||||
@@ -18487,6 +18488,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"secure-json-parse@npm:^4.0.0":
|
||||||
|
version: 4.0.0
|
||||||
|
resolution: "secure-json-parse@npm:4.0.0"
|
||||||
|
checksum: 10c0/1a298cf00e1de91e833cee5eb406d6e77fb2f7eca9bef3902047d49e7f5d3e6c21b5de61ff73466c831e716430bfe87d732a6e645a7dabb5f1e8a8e4d3e15eb4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"semver@npm:^5.6.0":
|
"semver@npm:^5.6.0":
|
||||||
version: 5.7.2
|
version: 5.7.2
|
||||||
resolution: "semver@npm:5.7.2"
|
resolution: "semver@npm:5.7.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user