Merged in revert-pr-1925 (pull request #1927)
Revert "Feat/sw 2323 find booking (pull request #1925)" Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -1,84 +1,14 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Alerts from "@/components/HotelReservation/BookingConfirmation/Alerts"
|
||||
import Header from "@/components/HotelReservation/BookingConfirmation/Header"
|
||||
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
|
||||
import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails"
|
||||
import Promos from "@/components/HotelReservation/BookingConfirmation/Promos"
|
||||
import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
|
||||
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
||||
import Tracking from "@/components/HotelReservation/BookingConfirmation/Tracking"
|
||||
import { mapRoomState } from "@/components/HotelReservation/BookingConfirmation/utils"
|
||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import { getIntl } from "@/i18n"
|
||||
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function BookingConfirmationPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, { RefId?: string }>) {
|
||||
const refId = searchParams.RefId
|
||||
|
||||
if (!refId) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const bookingConfirmation = await getBookingConfirmation(refId, params.lang)
|
||||
|
||||
if (!bookingConfirmation) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { booking, hotelData, room } = bookingConfirmation
|
||||
const { hotel } = hotelData
|
||||
|
||||
const intl = await getIntl()
|
||||
|
||||
}: PageArgs<LangParams, { confirmationNumber: string }>) {
|
||||
void getBookingConfirmation(searchParams.confirmationNumber)
|
||||
return (
|
||||
<BookingConfirmationProvider
|
||||
bookingCode={booking.bookingCode}
|
||||
currencyCode={booking.currencyCode}
|
||||
fromDate={booking.checkInDate}
|
||||
toDate={booking.checkOutDate}
|
||||
rooms={[
|
||||
mapRoomState(booking, room, intl),
|
||||
// null represents "known but not yet fetched rooms" and is used to render placeholders correctly
|
||||
...Array(booking.linkedReservations.length).fill(null),
|
||||
]}
|
||||
vat={booking.vatPercentage}
|
||||
>
|
||||
<main className={styles.main}>
|
||||
<Header booking={booking} hotel={hotel} refId={refId} />
|
||||
<div className={styles.booking}>
|
||||
<Alerts booking={booking} />
|
||||
<Rooms
|
||||
booking={booking}
|
||||
checkInTime={hotel.hotelFacts.checkin.checkInTime}
|
||||
checkOutTime={hotel.hotelFacts.checkin.checkOutTime}
|
||||
mainRoom={room}
|
||||
/>
|
||||
<PaymentDetails />
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<HotelDetails hotel={hotel} />
|
||||
<Promos refId={refId} hotelId={hotel.operaId} />
|
||||
<div className={styles.mobileReceipt}>
|
||||
<Receipt />
|
||||
</div>
|
||||
</div>
|
||||
<aside className={styles.aside}>
|
||||
<SidePanel variant="receipt">
|
||||
<Receipt />
|
||||
</SidePanel>
|
||||
</aside>
|
||||
</main>
|
||||
<Tracking bookingConfirmation={bookingConfirmation} />
|
||||
</BookingConfirmationProvider>
|
||||
<BookingConfirmation confirmationNumber={searchParams.confirmationNumber} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import {
|
||||
} from "@/constants/booking"
|
||||
import { myStay } from "@/constants/routes/myStay"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { createCounter } from "@/server/telemetry"
|
||||
|
||||
import GuaranteeCallback from "@/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback"
|
||||
import TrackGuarantee from "@/components/HotelReservation/MyStay/TrackGuarantee"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
@@ -21,56 +19,45 @@ export default async function GuaranteePaymentCallbackPage({
|
||||
}: PageArgs<
|
||||
LangParams,
|
||||
{
|
||||
status?: PaymentCallbackStatusEnum
|
||||
RefId?: string
|
||||
status: PaymentCallbackStatusEnum
|
||||
RefId: string
|
||||
confirmationNumber?: string
|
||||
ancillary?: string
|
||||
}
|
||||
>) {
|
||||
console.log(`[gla-payment-callback] callback started`)
|
||||
const lang = params.lang
|
||||
const status = searchParams.status
|
||||
const refId = searchParams.RefId
|
||||
const confirmationNumber = searchParams.confirmationNumber
|
||||
const isAncillaryFlow = searchParams.ancillary
|
||||
|
||||
setLang(params.lang)
|
||||
|
||||
if (!status || !confirmationNumber || !refId) {
|
||||
const refId = searchParams.RefId
|
||||
if (!refId) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const glaSuccessCounter = createCounter("gla", "success")
|
||||
const metricsGlaSuccess = glaSuccessCounter.init({
|
||||
confirmationNumber,
|
||||
})
|
||||
|
||||
metricsGlaSuccess.start()
|
||||
const isAncillaryFlow = searchParams.ancillary
|
||||
|
||||
const myStayUrl = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}`
|
||||
const searchObject = new URLSearchParams()
|
||||
|
||||
if (status === PaymentCallbackStatusEnum.Success && confirmationNumber) {
|
||||
if (isAncillaryFlow) {
|
||||
return (
|
||||
<GuaranteeCallback
|
||||
returnUrl={myStayUrl}
|
||||
refId={refId}
|
||||
confirmationNumber={confirmationNumber}
|
||||
lang={lang}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`[gla-payment-callback] redirecting to: ${myStayUrl}`)
|
||||
return <TrackGuarantee status={status} redirectUrl={myStayUrl} />
|
||||
}
|
||||
|
||||
let errorMessage = undefined
|
||||
|
||||
if (confirmationNumber) {
|
||||
const searchObject = new URLSearchParams()
|
||||
|
||||
try {
|
||||
const bookingStatus = await serverClient().booking.status({
|
||||
refId,
|
||||
confirmationNumber,
|
||||
})
|
||||
|
||||
const error = bookingStatus.errors.find((e) => e.errorCode)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import {
|
||||
BookingErrorCodeEnum,
|
||||
PaymentCallbackStatusEnum,
|
||||
} from "@/constants/booking"
|
||||
import { details } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import HandleErrorCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleErrorCallback"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function PaymentCallbackCancelPage({
|
||||
params,
|
||||
}: PageArgs<LangParams>) {
|
||||
console.log(`[payment-callback] cancel callback started`)
|
||||
const lang = params.lang
|
||||
|
||||
const returnUrl = details(lang)
|
||||
const searchObject = new URLSearchParams()
|
||||
|
||||
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
|
||||
|
||||
return (
|
||||
<HandleErrorCallback
|
||||
returnUrl={returnUrl.toString()}
|
||||
searchObject={searchObject}
|
||||
status={PaymentCallbackStatusEnum.Cancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
BookingErrorCodeEnum,
|
||||
PaymentCallbackStatusEnum,
|
||||
} from "@/constants/booking"
|
||||
import { details } from "@/constants/routes/hotelReservation"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import HandleErrorCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleErrorCallback"
|
||||
import { calculateRefId } from "@/utils/refId"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function PaymentCallbackErrorPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<
|
||||
LangParams,
|
||||
{
|
||||
confirmationNumber?: string
|
||||
}
|
||||
>) {
|
||||
console.log(`[payment-callback] error callback started`)
|
||||
|
||||
const lang = params.lang
|
||||
const confirmationNumber = searchParams.confirmationNumber
|
||||
|
||||
const returnUrl = details(lang)
|
||||
const searchObject = new URLSearchParams()
|
||||
|
||||
let errorMessage = undefined
|
||||
|
||||
if (confirmationNumber) {
|
||||
const refId = calculateRefId(confirmationNumber, "")
|
||||
|
||||
try {
|
||||
const bookingStatus = await serverClient().booking.confirmationError({
|
||||
refId,
|
||||
})
|
||||
|
||||
// TODO: how to handle errors for multiple rooms?
|
||||
const error = bookingStatus.errors.find((e) => e.errorCode)
|
||||
|
||||
errorMessage =
|
||||
error?.description ??
|
||||
`No error message found for booking ${confirmationNumber}`
|
||||
|
||||
searchObject.set(
|
||||
"errorCode",
|
||||
error
|
||||
? error.errorCode.toString()
|
||||
: BookingErrorCodeEnum.TransactionFailed
|
||||
)
|
||||
} catch {
|
||||
console.error(
|
||||
`[payment-callback] failed to get booking status for ${confirmationNumber}`
|
||||
)
|
||||
|
||||
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
|
||||
errorMessage = `Failed to get booking status for ${confirmationNumber}`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<HandleErrorCallback
|
||||
returnUrl={returnUrl.toString()}
|
||||
searchObject={searchObject}
|
||||
status={PaymentCallbackStatusEnum.Error}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
BookingErrorCodeEnum,
|
||||
PaymentCallbackStatusEnum,
|
||||
} from "@/constants/booking"
|
||||
import {
|
||||
bookingConfirmation,
|
||||
details,
|
||||
} from "@/constants/routes/hotelReservation"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import HandleErrorCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleErrorCallback"
|
||||
import HandleSuccessCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function PaymentCallbackPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<
|
||||
LangParams,
|
||||
{
|
||||
status: PaymentCallbackStatusEnum
|
||||
confirmationNumber?: string
|
||||
hotel?: string
|
||||
}
|
||||
>) {
|
||||
console.log(`[payment-callback] callback started`)
|
||||
const lang = params.lang
|
||||
const status = searchParams.status
|
||||
const confirmationNumber = searchParams.confirmationNumber
|
||||
|
||||
if (status === PaymentCallbackStatusEnum.Success && confirmationNumber) {
|
||||
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${confirmationNumber}`
|
||||
console.log(
|
||||
`[payment-callback] rendering success callback with confirmation number: ${confirmationNumber}`
|
||||
)
|
||||
|
||||
return (
|
||||
<HandleSuccessCallback
|
||||
confirmationNumber={confirmationNumber}
|
||||
successRedirectUrl={confirmationUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const returnUrl = details(lang)
|
||||
const searchObject = new URLSearchParams()
|
||||
|
||||
let errorMessage = undefined
|
||||
|
||||
if (confirmationNumber) {
|
||||
try {
|
||||
const bookingStatus = await serverClient().booking.status({
|
||||
confirmationNumber,
|
||||
})
|
||||
|
||||
// TODO: how to handle errors for multiple rooms?
|
||||
const error = bookingStatus.errors.find((e) => e.errorCode)
|
||||
|
||||
errorMessage =
|
||||
error?.description ??
|
||||
`No error message found for booking ${confirmationNumber}, status: ${status}`
|
||||
|
||||
searchObject.set(
|
||||
"errorCode",
|
||||
error
|
||||
? error.errorCode.toString()
|
||||
: BookingErrorCodeEnum.TransactionFailed
|
||||
)
|
||||
} catch {
|
||||
console.error(
|
||||
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||
)
|
||||
if (status === PaymentCallbackStatusEnum.Cancel) {
|
||||
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
|
||||
}
|
||||
if (status === PaymentCallbackStatusEnum.Error) {
|
||||
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
|
||||
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<HandleErrorCallback
|
||||
returnUrl={returnUrl.toString()}
|
||||
searchObject={searchObject}
|
||||
status={status}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { bookingConfirmation } from "@/constants/routes/hotelReservation"
|
||||
import { createCounter } from "@/server/telemetry"
|
||||
|
||||
import HandleSuccessCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
import { calculateRefId } from "@/utils/refId"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function PaymentCallbackSuccessPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<
|
||||
LangParams,
|
||||
{
|
||||
confirmationNumber?: string
|
||||
}
|
||||
>) {
|
||||
const confirmationNumber = searchParams.confirmationNumber
|
||||
|
||||
setLang(params.lang)
|
||||
|
||||
if (!confirmationNumber) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const paymentSuccessCounter = createCounter("payment", "success")
|
||||
const metricsPaymentSuccess = paymentSuccessCounter.init({
|
||||
confirmationNumber,
|
||||
})
|
||||
|
||||
metricsPaymentSuccess.start()
|
||||
|
||||
const refId = calculateRefId(confirmationNumber, "")
|
||||
|
||||
return (
|
||||
<HandleSuccessCallback
|
||||
refId={refId}
|
||||
successRedirectUrl={bookingConfirmation(params.lang)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { env } from "@/env/server"
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
findBooking,
|
||||
getAncillaryPackages,
|
||||
getBookingConfirmation,
|
||||
getLinkedReservations,
|
||||
@@ -14,8 +13,8 @@ import {
|
||||
getProfileSafely,
|
||||
getSavedPaymentCardsSafely,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
import { decrypt } from "@/server/routers/utils/encryption"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
|
||||
import accessBooking, {
|
||||
ACCESS_GRANTED,
|
||||
@@ -33,8 +32,6 @@ import Image from "@/components/Image"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
import MyStayProvider from "@/providers/MyStay"
|
||||
import { parseRefId } from "@/utils/refId"
|
||||
import { isValidSession } from "@/utils/session"
|
||||
import { getCurrentWebUrl } from "@/utils/url"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
@@ -47,48 +44,27 @@ export default async function MyStay({
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, { RefId?: string }>) {
|
||||
setLang(params.lang)
|
||||
|
||||
const refId = searchParams.RefId
|
||||
|
||||
if (!refId) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const session = await auth()
|
||||
const isLoggedIn = isValidSession(session)
|
||||
const { confirmationNumber, lastName } = parseRefId(refId)
|
||||
const bv = cookies().get("bv")?.value
|
||||
let bookingConfirmation
|
||||
if (isLoggedIn) {
|
||||
bookingConfirmation = await getBookingConfirmation(refId, params.lang)
|
||||
} else if (bv) {
|
||||
const params = new URLSearchParams(bv)
|
||||
const firstName = params.get("firstName")
|
||||
const email = params.get("email")
|
||||
|
||||
if (firstName && email) {
|
||||
bookingConfirmation = await findBooking(
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
firstName,
|
||||
email
|
||||
)
|
||||
} else {
|
||||
return <RenderAdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
}
|
||||
} else {
|
||||
return <RenderAdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
const value = decrypt(refId)
|
||||
if (!value) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const [confirmationNumber, lastName] = value.split(",")
|
||||
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotelData } = bookingConfirmation
|
||||
const { hotel } = hotelData
|
||||
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
|
||||
|
||||
const user = await getProfileSafely()
|
||||
|
||||
const bv = cookies().get("bv")?.value
|
||||
const intl = await getIntl()
|
||||
|
||||
const access = accessBooking(booking.guest, lastName, user, bv)
|
||||
@@ -98,7 +74,9 @@ export default async function MyStay({
|
||||
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
|
||||
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
|
||||
|
||||
const linkedReservationsPromise = getLinkedReservations(refId, params.lang)
|
||||
const linkedReservationsPromise = getLinkedReservations({
|
||||
rooms: booking.linkedReservations,
|
||||
})
|
||||
|
||||
const packagesInput = {
|
||||
adults: booking.adults,
|
||||
@@ -143,7 +121,7 @@ export default async function MyStay({
|
||||
|
||||
const imageSrc =
|
||||
hotel.hotelContent.images.imageSizes.large ??
|
||||
hotelData.additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
||||
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
||||
hotel.galleryImages[0]?.imageSizes.large
|
||||
|
||||
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
|
||||
@@ -160,7 +138,7 @@ export default async function MyStay({
|
||||
lang={params.lang}
|
||||
linkedReservationsPromise={linkedReservationsPromise}
|
||||
refId={refId}
|
||||
roomCategories={hotelData.roomCategories}
|
||||
roomCategories={roomCategories}
|
||||
savedCreditCards={savedCreditCards}
|
||||
>
|
||||
<main className={styles.main}>
|
||||
@@ -219,7 +197,10 @@ export default async function MyStay({
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.form}>
|
||||
<AdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
<AdditionalInfoForm
|
||||
confirmationNumber={confirmationNumber}
|
||||
lastName={lastName}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
@@ -251,19 +232,3 @@ export default async function MyStay({
|
||||
|
||||
return notFound()
|
||||
}
|
||||
|
||||
function RenderAdditionalInfoForm({
|
||||
refId,
|
||||
lastName,
|
||||
}: {
|
||||
refId: string
|
||||
lastName: string
|
||||
}) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.form}>
|
||||
<AdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton"
|
||||
@@ -1,215 +1,20 @@
|
||||
import { cookies } from "next/headers"
|
||||
import { notFound } from "next/navigation"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import ScandicLogoIcon from "@scandic-hotels/design-system/Icons/ScandicLogoIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton"
|
||||
import { Receipt } from "@/components/HotelReservation/MyStay/Receipt"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
findBooking,
|
||||
getAncillaryPackages,
|
||||
getBookingConfirmation,
|
||||
getProfileSafely,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
|
||||
import accessBooking, {
|
||||
ACCESS_GRANTED,
|
||||
ERROR_BAD_REQUEST,
|
||||
ERROR_UNAUTHORIZED,
|
||||
} from "@/components/HotelReservation/MyStay/accessBooking"
|
||||
import Footer from "@/components/HotelReservation/MyStay/Receipt/Footer"
|
||||
import Specification from "@/components/HotelReservation/MyStay/Receipt/Specification"
|
||||
import Total from "@/components/HotelReservation/MyStay/Receipt/Total"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { parseRefId } from "@/utils/refId"
|
||||
import { isValidSession } from "@/utils/session"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function ReceiptPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, { RefId?: string }>) {
|
||||
const refId = searchParams.RefId
|
||||
|
||||
if (!refId) {
|
||||
if (!searchParams.RefId) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const session = await auth()
|
||||
const isLoggedIn = isValidSession(session)
|
||||
const { confirmationNumber, lastName } = parseRefId(refId)
|
||||
|
||||
const bv = cookies().get("bv")?.value
|
||||
let bookingConfirmation
|
||||
if (isLoggedIn) {
|
||||
bookingConfirmation = await getBookingConfirmation(refId, params.lang)
|
||||
} else if (bv) {
|
||||
const params = new URLSearchParams(bv)
|
||||
const firstName = params.get("firstName")
|
||||
const email = params.get("email")
|
||||
|
||||
if (firstName && email) {
|
||||
bookingConfirmation = await findBooking(
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
firstName,
|
||||
email
|
||||
)
|
||||
} else {
|
||||
return <RenderAdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
}
|
||||
} else {
|
||||
return <RenderAdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
}
|
||||
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotelData, room } = bookingConfirmation
|
||||
const { hotel } = hotelData
|
||||
|
||||
const intl = await getIntl()
|
||||
const user = await getProfileSafely()
|
||||
|
||||
const access = accessBooking(booking.guest, lastName, user, bv)
|
||||
|
||||
if (access === ACCESS_GRANTED) {
|
||||
const ancillaryPackages = await getAncillaryPackages({
|
||||
fromDate: dt(booking.checkInDate).format("YYYY-MM-DD"),
|
||||
hotelId: hotel.operaId,
|
||||
toDate: dt(booking.checkOutDate).format("YYYY-MM-DD"),
|
||||
})
|
||||
|
||||
const currency =
|
||||
booking.currencyCode !== CurrencyEnum.POINTS
|
||||
? booking.currencyCode
|
||||
: (booking.ancillaries.find((a) => a.currency !== CurrencyEnum.POINTS)
|
||||
?.currency ??
|
||||
booking.packages.find((p) => p.currency !== CurrencyEnum.POINTS)
|
||||
?.currency)
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<ScandicLogoIcon width="89px" height="19px" color="Icon/Accent" />
|
||||
<div className={styles.addresses}>
|
||||
<div>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div>{hotel.name}</div>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{`${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city}`}
|
||||
</div>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={`${styles.tertiary} ${styles.addressMargin}`}>
|
||||
{hotel.contactInformation.email}
|
||||
</div>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={styles.tertiary}>
|
||||
{hotel.contactInformation.phoneNumber}
|
||||
</div>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.rightColumn}>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<div>{`${booking.guest.firstName} ${booking.guest.lastName}`}</div>
|
||||
</Typography>
|
||||
{booking.guest.membershipNumber && (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<div>{`${intl.formatMessage({
|
||||
defaultMessage: "Member",
|
||||
})} ${booking.guest.membershipNumber}`}</div>
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={`${styles.tertiary} ${styles.addressMargin}`}>
|
||||
{booking.guest.email}
|
||||
</div>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={styles.tertiary}>
|
||||
{booking.guest.phoneNumber}
|
||||
</div>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Total booking={booking} currency={currency} />
|
||||
<Specification
|
||||
ancillaryPackages={ancillaryPackages}
|
||||
booking={booking}
|
||||
currency={currency}
|
||||
/>
|
||||
|
||||
<hr className={styles.divider} />
|
||||
|
||||
<Footer booking={booking} room={room} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (access === ERROR_BAD_REQUEST) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.form}>
|
||||
<AdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (access === ERROR_UNAUTHORIZED) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.logIn}>
|
||||
<Typography variant="Title/md">
|
||||
<h1>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "You need to be logged in to view your booking",
|
||||
})}
|
||||
</h1>
|
||||
</Typography>
|
||||
<Typography variant="Body/Lead text">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
defaultMessage:
|
||||
"And you need to be logged in with the same member account that made the booking.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return notFound()
|
||||
}
|
||||
|
||||
function RenderAdditionalInfoForm({
|
||||
refId,
|
||||
lastName,
|
||||
}: {
|
||||
refId: string
|
||||
lastName: string
|
||||
}) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.form}>
|
||||
<AdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
</div>
|
||||
</main>
|
||||
<Suspense fallback={<MyStaySkeleton />}>
|
||||
<Receipt refId={searchParams.RefId} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { env } from "@/env/server"
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
findBooking,
|
||||
getAncillaryPackages,
|
||||
getBookingConfirmation,
|
||||
getLinkedReservations,
|
||||
@@ -14,8 +13,8 @@ import {
|
||||
getProfileSafely,
|
||||
getSavedPaymentCardsSafely,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
import { decrypt } from "@/server/routers/utils/encryption"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
|
||||
import accessBooking, {
|
||||
ACCESS_GRANTED,
|
||||
@@ -33,8 +32,6 @@ import Image from "@/components/Image"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
import MyStayProvider from "@/providers/MyStay"
|
||||
import { parseRefId } from "@/utils/refId"
|
||||
import { isValidSession } from "@/utils/session"
|
||||
import { getCurrentWebUrl } from "@/utils/url"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
@@ -47,47 +44,27 @@ export default async function MyStay({
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, { RefId?: string }>) {
|
||||
setLang(params.lang)
|
||||
|
||||
const refId = searchParams.RefId
|
||||
|
||||
if (!refId) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const session = await auth()
|
||||
const isLoggedIn = isValidSession(session)
|
||||
const { confirmationNumber, lastName } = parseRefId(refId)
|
||||
|
||||
const bv = cookies().get("bv")?.value
|
||||
let bookingConfirmation
|
||||
if (isLoggedIn) {
|
||||
bookingConfirmation = await getBookingConfirmation(refId, params.lang)
|
||||
} else if (bv) {
|
||||
const params = new URLSearchParams(bv)
|
||||
const firstName = params.get("firstName")
|
||||
const email = params.get("email")
|
||||
|
||||
if (firstName && email) {
|
||||
bookingConfirmation = await findBooking(
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
firstName,
|
||||
email
|
||||
)
|
||||
} else {
|
||||
return <RenderAdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
}
|
||||
} else {
|
||||
return <RenderAdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
const value = decrypt(refId)
|
||||
if (!value) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const [confirmationNumber, lastName] = value.split(",")
|
||||
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotelData } = bookingConfirmation
|
||||
const { hotel } = hotelData
|
||||
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
|
||||
|
||||
const user = await getProfileSafely()
|
||||
const bv = cookies().get("bv")?.value
|
||||
const intl = await getIntl()
|
||||
|
||||
const access = accessBooking(booking.guest, lastName, user, bv)
|
||||
@@ -97,7 +74,9 @@ export default async function MyStay({
|
||||
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
|
||||
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
|
||||
|
||||
const linkedReservationsPromise = getLinkedReservations(refId, params.lang)
|
||||
const linkedReservationsPromise = getLinkedReservations({
|
||||
rooms: booking.linkedReservations,
|
||||
})
|
||||
|
||||
const packagesInput = {
|
||||
adults: booking.adults,
|
||||
@@ -119,9 +98,9 @@ export default async function MyStay({
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
const breakfastIncluded = booking.rateDefinition.breakfastIncluded
|
||||
const shouldFetchBreakfastPackages =
|
||||
const alreadyHasABreakfastSelection =
|
||||
!hasBreakfastPackage && !breakfastIncluded
|
||||
if (shouldFetchBreakfastPackages) {
|
||||
if (alreadyHasABreakfastSelection) {
|
||||
void getPackages(packagesInput)
|
||||
}
|
||||
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
|
||||
@@ -133,7 +112,7 @@ export default async function MyStay({
|
||||
})
|
||||
|
||||
let breakfastPackages = null
|
||||
if (shouldFetchBreakfastPackages) {
|
||||
if (alreadyHasABreakfastSelection) {
|
||||
breakfastPackages = await getPackages(packagesInput)
|
||||
}
|
||||
const savedCreditCards = await getSavedPaymentCardsSafely(
|
||||
@@ -142,7 +121,7 @@ export default async function MyStay({
|
||||
|
||||
const imageSrc =
|
||||
hotel.hotelContent.images.imageSizes.large ??
|
||||
hotelData.additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
||||
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
||||
hotel.galleryImages[0]?.imageSizes.large
|
||||
|
||||
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
|
||||
@@ -159,7 +138,7 @@ export default async function MyStay({
|
||||
lang={params.lang}
|
||||
linkedReservationsPromise={linkedReservationsPromise}
|
||||
refId={refId}
|
||||
roomCategories={hotelData.roomCategories}
|
||||
roomCategories={roomCategories}
|
||||
savedCreditCards={savedCreditCards}
|
||||
>
|
||||
<main className={styles.main}>
|
||||
@@ -218,7 +197,10 @@ export default async function MyStay({
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.form}>
|
||||
<AdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
<AdditionalInfoForm
|
||||
confirmationNumber={confirmationNumber}
|
||||
lastName={lastName}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
@@ -250,19 +232,3 @@ export default async function MyStay({
|
||||
|
||||
return notFound()
|
||||
}
|
||||
|
||||
function RenderAdditionalInfoForm({
|
||||
refId,
|
||||
lastName,
|
||||
}: {
|
||||
refId: string
|
||||
lastName: string
|
||||
}) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.form}>
|
||||
<AdditionalInfoForm refId={refId} lastName={lastName} />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client"
|
||||
|
||||
import { createEvent } from "ics"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
|
||||
@@ -19,26 +19,3 @@
|
||||
padding-top: var(--Spacing-x9);
|
||||
}
|
||||
}
|
||||
|
||||
.booking {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x5);
|
||||
grid-area: booking;
|
||||
padding-bottom: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.mobileReceipt {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: grid;
|
||||
grid-area: receipt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
import { useRef } from "react"
|
||||
|
||||
import Header from "@/components/HotelReservation/BookingConfirmation/Header"
|
||||
|
||||
import styles from "./confirmation.module.css"
|
||||
|
||||
import type { ConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default function Confirmation({
|
||||
booking,
|
||||
hotel,
|
||||
children,
|
||||
refId,
|
||||
}: React.PropsWithChildren<ConfirmationProps>) {
|
||||
const mainRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
return (
|
||||
<main className={styles.main} ref={mainRef}>
|
||||
<Header booking={booking} hotel={hotel} mainRef={mainRef} refId={refId} />
|
||||
{children}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
|
||||
import { myStay } from "@/constants/routes/myStay"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import type { ManageBookingProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/manageBooking"
|
||||
|
||||
export default function ManageBooking({ refId }: ManageBookingProps) {
|
||||
export default function ManageBooking({ bookingUrl }: ManageBookingProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const bookingUrl = `${myStay[lang]}?RefId=${refId}`
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { myStay } from "@/constants/routes/myStay"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import AddToCalendar from "../../AddToCalendar"
|
||||
import AddToCalendarButton from "./Actions/AddToCalendarButton"
|
||||
// import DownloadInvoice from "./Actions/DownloadInvoice"
|
||||
import { generateDateTime } from "./Actions/helpers"
|
||||
import ManageBooking from "./Actions/ManageBooking"
|
||||
|
||||
@@ -18,9 +22,11 @@ import type { BookingConfirmationHeaderProps } from "@/types/components/hotelRes
|
||||
export default function Header({
|
||||
booking,
|
||||
hotel,
|
||||
// mainRef,
|
||||
refId,
|
||||
}: BookingConfirmationHeaderProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const text = intl.formatMessage({
|
||||
defaultMessage:
|
||||
@@ -46,6 +52,8 @@ export default function Header({
|
||||
url: hotel.contactInformation.websiteUrl,
|
||||
}
|
||||
|
||||
const bookingUrlPath = `${myStay[lang]}?RefId=${refId}`
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<hgroup className={styles.hgroup}>
|
||||
@@ -66,7 +74,9 @@ export default function Header({
|
||||
hotelName={hotel.name}
|
||||
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
|
||||
/>
|
||||
<ManageBooking refId={refId} />
|
||||
<ManageBooking bookingUrl={bookingUrlPath} />
|
||||
{/* Download Invoice will be added later (currently available on My Stay) */}
|
||||
{/* <DownloadInvoice mainRef={mainRef} /> */}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { myStay } from "@/constants/routes/myStay"
|
||||
import { homeHrefs } from "@/constants/homeHrefs"
|
||||
import { myBooking } from "@/constants/myBooking"
|
||||
import { env } from "@/env/client"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
@@ -12,17 +13,22 @@ import styles from "./promos.module.css"
|
||||
|
||||
import type { PromosProps } from "@/types/components/hotelReservation/bookingConfirmation/promos"
|
||||
|
||||
export default function Promos({ refId, hotelId }: PromosProps) {
|
||||
export default function Promos({
|
||||
confirmationNumber,
|
||||
hotelId,
|
||||
lastName,
|
||||
}: PromosProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const homeUrl = homeHrefs[env.NEXT_PUBLIC_NODE_ENV][lang]
|
||||
const myBookingUrl = myBooking[env.NEXT_PUBLIC_NODE_ENV][lang]
|
||||
return (
|
||||
<div className={styles.promos}>
|
||||
<Promo
|
||||
buttonText={intl.formatMessage({
|
||||
defaultMessage: "View and buy add-ons",
|
||||
})}
|
||||
href={`${myStay[lang]}?RefId=${refId}`}
|
||||
href={`${myBookingUrl}?bookingId=${confirmationNumber}&lastName=${lastName}`}
|
||||
text={intl.formatMessage({
|
||||
defaultMessage:
|
||||
"Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
||||
@@ -35,7 +41,7 @@ export default function Promos({ refId, hotelId }: PromosProps) {
|
||||
buttonText={intl.formatMessage({
|
||||
defaultMessage: "Book another stay",
|
||||
})}
|
||||
href={`/${lang}?hotel=${hotelId}`}
|
||||
href={`${homeUrl}?hotel=${hotelId}`}
|
||||
text={intl.formatMessage({
|
||||
defaultMessage:
|
||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
|
||||
@@ -20,16 +20,14 @@ import { CurrencyEnum } from "@/types/enums/currency"
|
||||
export function LinkedReservation({
|
||||
checkInTime,
|
||||
checkOutTime,
|
||||
refId,
|
||||
confirmationNumber,
|
||||
roomIndex,
|
||||
}: LinkedReservationProps) {
|
||||
const lang = useLang()
|
||||
|
||||
const { data, refetch, isLoading } = trpc.booking.confirmation.useQuery({
|
||||
refId,
|
||||
const { data, refetch, isLoading } = trpc.booking.get.useQuery({
|
||||
confirmationNumber,
|
||||
lang,
|
||||
})
|
||||
|
||||
const {
|
||||
setRoom,
|
||||
setFormattedTotalCost,
|
||||
@@ -43,7 +41,6 @@ export function LinkedReservation({
|
||||
totalBookingPrice: state.totalBookingPrice,
|
||||
totalBookingCheques: state.totalBookingCheques,
|
||||
}))
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -82,20 +79,13 @@ export function LinkedReservation({
|
||||
return <Retry handleRefetch={refetch} />
|
||||
}
|
||||
|
||||
const { booking, room } = data
|
||||
|
||||
return (
|
||||
<Room
|
||||
checkInDate={booking.checkInDate}
|
||||
checkOutDate={booking.checkOutDate}
|
||||
booking={data.booking}
|
||||
checkInTime={checkInTime}
|
||||
checkOutTime={checkOutTime}
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
guaranteeInfo={booking.guaranteeInfo}
|
||||
guest={booking.guest}
|
||||
img={room.images[0]}
|
||||
rateDefinition={booking.rateDefinition}
|
||||
roomName={room.name}
|
||||
img={data.room.images[0]}
|
||||
roomName={data.room.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,28 +20,24 @@ import styles from "./room.module.css"
|
||||
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room"
|
||||
|
||||
export default function Room({
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
booking,
|
||||
checkInTime,
|
||||
checkOutTime,
|
||||
confirmationNumber,
|
||||
guaranteeInfo,
|
||||
guest,
|
||||
img,
|
||||
rateDefinition,
|
||||
roomName,
|
||||
}: RoomProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const guestName = `${guest.firstName} ${guest.lastName}`
|
||||
const fromDate = dt(checkInDate).locale(lang)
|
||||
const toDate = dt(checkOutDate).locale(lang)
|
||||
const guestName = `${booking.guest.firstName} ${booking.guest.lastName}`
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
|
||||
const isFlexBooking =
|
||||
rateDefinition.cancellationRule ===
|
||||
booking.rateDefinition.cancellationRule ===
|
||||
CancellationRuleEnum.CancellableBefore6PM
|
||||
const isChangeBooking =
|
||||
rateDefinition.cancellationRule === CancellationRuleEnum.Changeable
|
||||
booking.rateDefinition.cancellationRule === CancellationRuleEnum.Changeable
|
||||
return (
|
||||
<article className={styles.room}>
|
||||
<header className={styles.header}>
|
||||
@@ -51,11 +47,11 @@ export default function Room({
|
||||
{
|
||||
defaultMessage: "Booking number {value}",
|
||||
},
|
||||
{ value: confirmationNumber }
|
||||
{ value: booking.confirmationNumber }
|
||||
)}
|
||||
</h2>
|
||||
</Typography>
|
||||
{rateDefinition.isMemberRate ? (
|
||||
{booking.rateDefinition.isMemberRate ? (
|
||||
<div className={styles.benefits}>
|
||||
<>
|
||||
<MaterialIcon
|
||||
@@ -71,7 +67,7 @@ export default function Room({
|
||||
</>
|
||||
</div>
|
||||
) : null}
|
||||
{guaranteeInfo && (
|
||||
{booking.guaranteeInfo && (
|
||||
<div className={styles.benefits}>
|
||||
<MaterialIcon
|
||||
icon="check_circle"
|
||||
@@ -172,7 +168,7 @@ export default function Room({
|
||||
})}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{rateDefinition.cancellationText}
|
||||
{booking.rateDefinition.cancellationText}
|
||||
</Body>
|
||||
</li>
|
||||
{isFlexBooking || isChangeBooking ? (
|
||||
@@ -200,23 +196,25 @@ export default function Room({
|
||||
})}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{guestName}</Body>
|
||||
{guest.membershipNumber ? (
|
||||
{booking.guest.membershipNumber ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Friend no. {value}",
|
||||
},
|
||||
{
|
||||
value: guest.membershipNumber,
|
||||
value: booking.guest.membershipNumber,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
{guest.phoneNumber ? (
|
||||
<Body color="uiTextHighContrast">{guest.phoneNumber}</Body>
|
||||
{booking.guest.phoneNumber ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.guest.phoneNumber}
|
||||
</Body>
|
||||
) : null}
|
||||
{guest.email ? (
|
||||
<Body color="uiTextHighContrast">{guest.email}</Body>
|
||||
{booking.guest.email ? (
|
||||
<Body color="uiTextHighContrast">{booking.guest.email}</Body>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,56 +9,55 @@ import styles from "./rooms.module.css"
|
||||
|
||||
import type { BookingConfirmationRoomsProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms"
|
||||
|
||||
async function RoomTitle({ nr }: { nr: number }) {
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h2 className={styles.roomTitle}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Room {roomIndex}",
|
||||
},
|
||||
{ roomIndex: nr }
|
||||
)}
|
||||
</h2>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function Rooms({
|
||||
booking,
|
||||
checkInTime,
|
||||
checkOutTime,
|
||||
mainRoom,
|
||||
linkedReservations,
|
||||
}: BookingConfirmationRoomsProps) {
|
||||
const { linkedReservations } = booking
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<section className={styles.rooms}>
|
||||
<div className={styles.room}>
|
||||
{linkedReservations.length ? <RoomTitle nr={1} /> : null}
|
||||
{linkedReservations.length ? (
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h2 className={styles.roomTitle}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Room {roomIndex}",
|
||||
},
|
||||
{ roomIndex: 1 }
|
||||
)}
|
||||
</h2>
|
||||
</Typography>
|
||||
) : null}
|
||||
<Room
|
||||
checkInDate={booking.checkInDate}
|
||||
checkOutDate={booking.checkOutDate}
|
||||
booking={booking}
|
||||
checkInTime={checkInTime}
|
||||
checkOutTime={checkOutTime}
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
guaranteeInfo={booking.guaranteeInfo}
|
||||
guest={booking.guest}
|
||||
img={mainRoom.images[0]}
|
||||
rateDefinition={booking.rateDefinition}
|
||||
roomName={mainRoom.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{linkedReservations.map((reservation, idx) => (
|
||||
<div className={styles.room} key={reservation.confirmationNumber}>
|
||||
<RoomTitle nr={idx + 2} />
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h2 className={styles.roomTitle}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Room {roomIndex}",
|
||||
},
|
||||
{ roomIndex: idx + 2 }
|
||||
)}
|
||||
</h2>
|
||||
</Typography>
|
||||
<LinkedReservation
|
||||
checkInTime={checkInTime}
|
||||
checkOutTime={checkOutTime}
|
||||
refId={reservation.refId}
|
||||
confirmationNumber={reservation.confirmationNumber}
|
||||
roomIndex={idx + 1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Tracking({
|
||||
getTracking(
|
||||
lang,
|
||||
bookingConfirmation.booking,
|
||||
bookingConfirmation.hotelData.hotel,
|
||||
bookingConfirmation.hotel,
|
||||
rooms
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ function mapAncillaryPackage(
|
||||
export function getTracking(
|
||||
lang: Lang,
|
||||
booking: BookingConfirmation["booking"],
|
||||
hotel: BookingConfirmation["hotelData"]["hotel"],
|
||||
hotel: BookingConfirmation["hotel"],
|
||||
rooms: Room[]
|
||||
) {
|
||||
const arrivalDate = new Date(booking.checkInDate)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
.booking {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x5);
|
||||
grid-area: booking;
|
||||
padding-bottom: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.mobileReceipt {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: grid;
|
||||
grid-area: receipt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
import { encrypt } from "@/server/routers/utils/encryption"
|
||||
|
||||
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
|
||||
import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails"
|
||||
import Promos from "@/components/HotelReservation/BookingConfirmation/Promos"
|
||||
import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
|
||||
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import { getIntl } from "@/i18n"
|
||||
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
|
||||
|
||||
import Alerts from "./Alerts"
|
||||
import Confirmation from "./Confirmation"
|
||||
import Tracking from "./Tracking"
|
||||
import { mapRoomState } from "./utils"
|
||||
|
||||
import styles from "./bookingConfirmation.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default async function BookingConfirmation({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
const { booking, hotel, room } = bookingConfirmation
|
||||
if (!room) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const refId = encrypt(
|
||||
`${booking.confirmationNumber},${booking.guest.lastName}`
|
||||
)
|
||||
|
||||
const intl = await getIntl()
|
||||
return (
|
||||
<BookingConfirmationProvider
|
||||
bookingCode={booking.bookingCode}
|
||||
currencyCode={booking.currencyCode}
|
||||
fromDate={booking.checkInDate}
|
||||
toDate={booking.checkOutDate}
|
||||
rooms={[
|
||||
mapRoomState(booking, room, intl),
|
||||
// null represents "known but not yet fetched rooms" and is used to render placeholders correctly
|
||||
...Array(booking.linkedReservations.length).fill(null),
|
||||
]}
|
||||
vat={booking.vatPercentage}
|
||||
>
|
||||
<Confirmation booking={booking} hotel={hotel} room={room} refId={refId}>
|
||||
<div className={styles.booking}>
|
||||
<Alerts booking={booking} />
|
||||
<Rooms
|
||||
booking={booking}
|
||||
checkInTime={hotel.hotelFacts.checkin.checkInTime}
|
||||
checkOutTime={hotel.hotelFacts.checkin.checkOutTime}
|
||||
mainRoom={room}
|
||||
linkedReservations={booking.linkedReservations}
|
||||
/>
|
||||
<PaymentDetails />
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<HotelDetails hotel={hotel} />
|
||||
<Promos
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
hotelId={hotel.operaId}
|
||||
lastName={booking.guest.lastName}
|
||||
/>
|
||||
<div className={styles.mobileReceipt}>
|
||||
<Receipt />
|
||||
</div>
|
||||
</div>
|
||||
<aside className={styles.aside}>
|
||||
<SidePanel variant="receipt">
|
||||
<Receipt />
|
||||
</SidePanel>
|
||||
</aside>
|
||||
</Confirmation>
|
||||
<Tracking bookingConfirmation={bookingConfirmation} />
|
||||
</BookingConfirmationProvider>
|
||||
)
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import type { IntlShape } from "react-intl"
|
||||
import type { BookingConfirmationRoom } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import type { BookingSchema } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export function mapRoomState(
|
||||
booking: BookingSchema,
|
||||
booking: BookingConfirmationSchema,
|
||||
room: BookingConfirmationRoom,
|
||||
intl: IntlShape
|
||||
) {
|
||||
|
||||
@@ -18,12 +18,12 @@ const validBookingStatuses = [
|
||||
]
|
||||
|
||||
interface HandleStatusPollingProps {
|
||||
refId: string
|
||||
confirmationNumber: string
|
||||
successRedirectUrl: string
|
||||
}
|
||||
|
||||
export default function HandleSuccessCallback({
|
||||
refId,
|
||||
confirmationNumber,
|
||||
successRedirectUrl,
|
||||
}: HandleStatusPollingProps) {
|
||||
const router = useRouter()
|
||||
@@ -33,7 +33,7 @@ export default function HandleSuccessCallback({
|
||||
error,
|
||||
isTimeout,
|
||||
} = useHandleBookingStatus({
|
||||
refId,
|
||||
confirmationNumber,
|
||||
expectedStatuses: validBookingStatuses,
|
||||
maxRetries: 10,
|
||||
retryInterval: 2000,
|
||||
@@ -70,9 +70,9 @@ export default function HandleSuccessCallback({
|
||||
? `&errorCode=${membershipFailedError.errorCode}`
|
||||
: ""
|
||||
|
||||
router.replace(`${successRedirectUrl}?RefId=${refId}${errorParam}`)
|
||||
router.replace(`${successRedirectUrl}${errorParam}`)
|
||||
}
|
||||
}, [bookingStatus, refId, router, successRedirectUrl])
|
||||
}, [bookingStatus, successRedirectUrl, router])
|
||||
|
||||
if (isTimeout || error) {
|
||||
return <TimeoutSpinner />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { Label } from "react-aria-components"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
@@ -10,6 +10,7 @@ import { useIntl } from "react-intl"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import {
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
BookingStatusEnum,
|
||||
PAYMENT_METHOD_TITLES,
|
||||
PaymentMethodEnum,
|
||||
@@ -29,6 +30,7 @@ import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions"
|
||||
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import useStickyPosition from "@/hooks/useStickyPosition"
|
||||
import { trackPaymentEvent } from "@/utils/tracking"
|
||||
@@ -99,7 +101,7 @@ export default function PaymentClient({
|
||||
(state) => state.actions.setIsSubmittingDisabled
|
||||
)
|
||||
|
||||
const [refId, setRefId] = useState<string>("")
|
||||
const [bookingNumber, setBookingNumber] = useState<string>("")
|
||||
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
||||
useState(false)
|
||||
|
||||
@@ -144,14 +146,13 @@ export default function PaymentClient({
|
||||
return
|
||||
}
|
||||
|
||||
const mainRoom = result.rooms[0]
|
||||
if (result.reservationStatus == BookingStatusEnum.BookingCompleted) {
|
||||
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${mainRoom.refId}`
|
||||
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${result.id}`
|
||||
router.push(confirmationUrl)
|
||||
return
|
||||
}
|
||||
|
||||
setRefId(mainRoom.refId)
|
||||
setBookingNumber(result.id)
|
||||
|
||||
const hasPriceChange = result.rooms.some((r) => r.priceChangedMetadata)
|
||||
if (hasPriceChange) {
|
||||
@@ -173,8 +174,8 @@ export default function PaymentClient({
|
||||
})
|
||||
|
||||
const priceChange = trpc.booking.priceChange.useMutation({
|
||||
onSuccess: (confirmationNumber) => {
|
||||
if (confirmationNumber) {
|
||||
onSuccess: (result) => {
|
||||
if (result?.id) {
|
||||
setIsPollingForBookingStatus(true)
|
||||
} else {
|
||||
handlePaymentError("No confirmation number")
|
||||
@@ -188,39 +189,13 @@ export default function PaymentClient({
|
||||
},
|
||||
})
|
||||
|
||||
// Replaced useHandleBookingStatus with logic specifically used here, since the hook would need
|
||||
// to handle different parameters based on use case
|
||||
const retries = useRef(0)
|
||||
|
||||
const bookingStatus = trpc.booking.confirmationCompleted.useQuery(
|
||||
{
|
||||
refId,
|
||||
lang,
|
||||
},
|
||||
{
|
||||
enabled: isPollingForBookingStatus,
|
||||
refetchInterval: (query) => {
|
||||
retries.current = query.state.dataUpdateCount
|
||||
|
||||
if (query.state.error || query.state.dataUpdateCount >= maxRetries) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
query.state.data?.reservationStatus ===
|
||||
BookingStatusEnum.BookingCompleted
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return retryInterval
|
||||
},
|
||||
refetchIntervalInBackground: true,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
retry: false,
|
||||
}
|
||||
)
|
||||
const bookingStatus = useHandleBookingStatus({
|
||||
confirmationNumber: bookingNumber,
|
||||
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
||||
maxRetries,
|
||||
retryInterval,
|
||||
enabled: isPollingForBookingStatus,
|
||||
})
|
||||
|
||||
const handlePaymentError = useCallback(
|
||||
(errorMessage: string) => {
|
||||
@@ -270,12 +245,18 @@ export default function PaymentClient({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (bookingStatus?.data?.redirectUrl) {
|
||||
router.push(bookingStatus.data.redirectUrl)
|
||||
} else if (retries.current >= maxRetries) {
|
||||
if (bookingStatus?.data?.paymentUrl) {
|
||||
router.push(bookingStatus.data.paymentUrl)
|
||||
} else if (
|
||||
bookingStatus?.data?.reservationStatus ===
|
||||
BookingStatusEnum.BookingCompleted
|
||||
) {
|
||||
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${bookingStatus?.data?.id}`
|
||||
router.push(confirmationUrl)
|
||||
} else if (bookingStatus.isTimeout) {
|
||||
handlePaymentError("Timeout")
|
||||
}
|
||||
}, [bookingStatus, router, handlePaymentError])
|
||||
}, [bookingStatus, router, intl, lang, handlePaymentError])
|
||||
|
||||
useEffect(() => {
|
||||
setIsSubmittingDisabled(
|
||||
@@ -477,7 +458,7 @@ export default function PaymentClient({
|
||||
initiateBooking.isPending ||
|
||||
(isPollingForBookingStatus &&
|
||||
!bookingStatus.data?.paymentUrl &&
|
||||
retries.current < maxRetries)
|
||||
!bookingStatus.isTimeout)
|
||||
) {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -639,7 +620,9 @@ export default function PaymentClient({
|
||||
: ""
|
||||
router.push(`${selectRate(lang)}${allSearchParams}`)
|
||||
}}
|
||||
onAccept={() => priceChange.mutate({ refId })}
|
||||
onAccept={() =>
|
||||
priceChange.mutate({ confirmationNumber: bookingNumber })
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
import styles from "./findMyBooking.module.css"
|
||||
|
||||
export default function AdditionalInfoForm({
|
||||
refId,
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
}: {
|
||||
refId: string
|
||||
confirmationNumber: string
|
||||
lastName: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
@@ -37,7 +37,7 @@ export default function AdditionalInfoForm({
|
||||
const values = form.getValues()
|
||||
const value = new URLSearchParams({
|
||||
...values,
|
||||
RefId: refId,
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
}).toString()
|
||||
document.cookie = `bv=${encodeURIComponent(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict`
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function AddAncillaryFlowModal({
|
||||
) {
|
||||
addAncillary.mutate(
|
||||
{
|
||||
refId,
|
||||
confirmationNumber: booking.confirmationNumber,
|
||||
ancillaryComment: data.optionalText,
|
||||
ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime
|
||||
? data.deliveryTime
|
||||
@@ -175,8 +175,8 @@ export default function AddAncillaryFlowModal({
|
||||
)
|
||||
clearAncillarySessionData()
|
||||
closeModal()
|
||||
utils.booking.confirmation.invalidate({
|
||||
refId: booking.refId,
|
||||
utils.booking.get.invalidate({
|
||||
confirmationNumber: booking.confirmationNumber,
|
||||
})
|
||||
router.refresh()
|
||||
} else {
|
||||
@@ -211,7 +211,7 @@ export default function AddAncillaryFlowModal({
|
||||
}
|
||||
: undefined
|
||||
guaranteeBooking.mutate({
|
||||
refId,
|
||||
confirmationNumber: booking.confirmationNumber,
|
||||
language: lang,
|
||||
...(card && { card }),
|
||||
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}&ancillary=1`,
|
||||
|
||||
@@ -10,12 +10,12 @@ import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
export default function RemoveButton({
|
||||
refId,
|
||||
confirmationNumber,
|
||||
codes,
|
||||
title,
|
||||
onSuccess,
|
||||
}: {
|
||||
refId: string
|
||||
confirmationNumber: string
|
||||
codes: string[]
|
||||
title?: string
|
||||
onSuccess: () => void
|
||||
@@ -51,7 +51,7 @@ export default function RemoveButton({
|
||||
removePackage.mutate(
|
||||
{
|
||||
language: lang,
|
||||
refId,
|
||||
confirmationNumber,
|
||||
codes,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,7 +25,6 @@ import type {
|
||||
export function AddedAncillaries({
|
||||
ancillaries,
|
||||
booking,
|
||||
refId,
|
||||
}: AddedAncillariesProps) {
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
@@ -127,7 +126,7 @@ export function AddedAncillaries({
|
||||
{booking.confirmationNumber && ancillary.code ? (
|
||||
<div className={styles.actions}>
|
||||
<RemoveButton
|
||||
refId={refId}
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
codes={
|
||||
ancillary.code ===
|
||||
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
|
||||
@@ -193,7 +192,7 @@ export function AddedAncillaries({
|
||||
booking.canModifyAncillaries ? (
|
||||
<div className={styles.actions}>
|
||||
<RemoveButton
|
||||
refId={refId}
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
codes={
|
||||
ancillary.code ===
|
||||
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
|
||||
|
||||
@@ -20,12 +20,10 @@ import type { Lang } from "@/constants/languages"
|
||||
|
||||
export default function GuaranteeAncillaryHandler({
|
||||
confirmationNumber,
|
||||
refId,
|
||||
returnUrl,
|
||||
lang,
|
||||
}: {
|
||||
confirmationNumber: string
|
||||
refId: string
|
||||
returnUrl: string
|
||||
lang: Lang
|
||||
}) {
|
||||
@@ -49,7 +47,7 @@ export default function GuaranteeAncillaryHandler({
|
||||
|
||||
addAncillary.mutate(
|
||||
{
|
||||
refId,
|
||||
confirmationNumber,
|
||||
ancillaryComment: formData.optionalText,
|
||||
ancillaryDeliveryTime: selectedAncillary.requiresDeliveryTime
|
||||
? formData.deliveryTime
|
||||
@@ -88,7 +86,7 @@ export default function GuaranteeAncillaryHandler({
|
||||
},
|
||||
}
|
||||
)
|
||||
}, [confirmationNumber, refId, returnUrl, addAncillary, lang, router])
|
||||
}, [confirmationNumber, returnUrl, addAncillary, lang, router])
|
||||
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
@@ -213,11 +213,7 @@ export function Ancillaries({
|
||||
</>
|
||||
)}
|
||||
|
||||
<AddedAncillaries
|
||||
booking={booking}
|
||||
ancillaries={uniqueAncillaries}
|
||||
refId={refId}
|
||||
/>
|
||||
<AddedAncillaries booking={booking} ancillaries={uniqueAncillaries} />
|
||||
|
||||
<AncillaryFlowModalWrapper>
|
||||
<AddAncillaryFlowModal
|
||||
|
||||
@@ -64,10 +64,10 @@ export default function Details({ booking, user }: DetailsProps) {
|
||||
|
||||
const updateGuest = trpc.booking.update.useMutation({
|
||||
onMutate: () => setIsLoading(true),
|
||||
onSuccess: (refId) => {
|
||||
if (refId) {
|
||||
utils.booking.confirmation.invalidate({
|
||||
refId,
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
utils.booking.get.invalidate({
|
||||
confirmationNumber: data.confirmationNumber,
|
||||
})
|
||||
|
||||
toast.success(
|
||||
@@ -99,7 +99,7 @@ export default function Details({ booking, user }: DetailsProps) {
|
||||
|
||||
async function onSubmit(data: ModifyContactSchema) {
|
||||
updateGuest.mutate({
|
||||
refId: booking.refId,
|
||||
confirmationNumber: booking.confirmationNumber,
|
||||
guest: {
|
||||
email: data.email,
|
||||
phoneNumber: data.phoneNumber,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import Details from "./Details"
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { cookies } from "next/headers"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import ScandicLogoIcon from "@scandic-hotels/design-system/Icons/ScandicLogoIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
getAncillaryPackages,
|
||||
getBookingConfirmation,
|
||||
getProfileSafely,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
import { decrypt } from "@/server/routers/utils/encryption"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import AdditionalInfoForm from "../../FindMyBooking/AdditionalInfoForm"
|
||||
import accessBooking, {
|
||||
ACCESS_GRANTED,
|
||||
ERROR_BAD_REQUEST,
|
||||
ERROR_UNAUTHORIZED,
|
||||
} from "../accessBooking"
|
||||
import Footer from "./Footer"
|
||||
import Specification from "./Specification"
|
||||
import Total from "./Total"
|
||||
|
||||
import styles from "./receipt.module.css"
|
||||
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
|
||||
export async function Receipt({ refId }: { refId: string }) {
|
||||
const value = decrypt(refId)
|
||||
if (!value) {
|
||||
return notFound()
|
||||
}
|
||||
const [confirmationNumber, lastName] = value.split(",")
|
||||
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotel, room } = bookingConfirmation
|
||||
const user = await getProfileSafely()
|
||||
const bv = cookies().get("bv")?.value
|
||||
const intl = await getIntl()
|
||||
|
||||
const access = accessBooking(booking.guest, lastName, user, bv)
|
||||
|
||||
if (access === ACCESS_GRANTED) {
|
||||
const ancillaryPackages = await getAncillaryPackages({
|
||||
fromDate: dt(booking.checkInDate).format("YYYY-MM-DD"),
|
||||
hotelId: hotel.operaId,
|
||||
toDate: dt(booking.checkOutDate).format("YYYY-MM-DD"),
|
||||
})
|
||||
|
||||
const currency =
|
||||
booking.currencyCode !== CurrencyEnum.POINTS
|
||||
? booking.currencyCode
|
||||
: (booking.ancillaries.find((a) => a.currency !== CurrencyEnum.POINTS)
|
||||
?.currency ??
|
||||
booking.packages.find((p) => p.currency !== CurrencyEnum.POINTS)
|
||||
?.currency)
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<ScandicLogoIcon width="89px" height="19px" color="Icon/Accent" />
|
||||
<div className={styles.addresses}>
|
||||
<div>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div>{hotel.name}</div>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{`${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city}`}
|
||||
</div>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={`${styles.tertiary} ${styles.addressMargin}`}>
|
||||
{hotel.contactInformation.email}
|
||||
</div>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={styles.tertiary}>
|
||||
{hotel.contactInformation.phoneNumber}
|
||||
</div>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.rightColumn}>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<div>{`${booking.guest.firstName} ${booking.guest.lastName}`}</div>
|
||||
</Typography>
|
||||
{booking.guest.membershipNumber && (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<div>{`${intl.formatMessage({
|
||||
defaultMessage: "Member",
|
||||
})} ${booking.guest.membershipNumber}`}</div>
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={`${styles.tertiary} ${styles.addressMargin}`}>
|
||||
{booking.guest.email}
|
||||
</div>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={styles.tertiary}>
|
||||
{booking.guest.phoneNumber}
|
||||
</div>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Total booking={booking} currency={currency} />
|
||||
<Specification
|
||||
ancillaryPackages={ancillaryPackages}
|
||||
booking={booking}
|
||||
currency={currency}
|
||||
/>
|
||||
|
||||
<hr className={styles.divider} />
|
||||
|
||||
<Footer booking={booking} room={room} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (access === ERROR_BAD_REQUEST) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.form}>
|
||||
<AdditionalInfoForm
|
||||
confirmationNumber={confirmationNumber}
|
||||
lastName={lastName}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (access === ERROR_UNAUTHORIZED) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.logIn}>
|
||||
<Typography variant="Title/md">
|
||||
<h1>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "You need to be logged in to view your booking",
|
||||
})}
|
||||
</h1>
|
||||
</Typography>
|
||||
<Typography variant="Body/Lead text">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
defaultMessage:
|
||||
"And you need to be logged in with the same member account that made the booking.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return notFound()
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export default function FinalConfirmation({
|
||||
)
|
||||
} else {
|
||||
const cancelledRooms = rooms.filter((r) =>
|
||||
variables.refIds.includes(r.refId)
|
||||
variables.confirmationNumbers.includes(r.confirmationNumber)
|
||||
)
|
||||
for (const cancelledRoom of cancelledRooms) {
|
||||
toast.success(
|
||||
@@ -93,16 +93,13 @@ export default function FinalConfirmation({
|
||||
)
|
||||
}
|
||||
|
||||
utils.booking.confirmation.invalidate({
|
||||
refId: bookedRoom.refId,
|
||||
lang,
|
||||
utils.booking.get.invalidate({
|
||||
confirmationNumber: bookedRoom.confirmationNumber,
|
||||
})
|
||||
|
||||
utils.booking.linkedReservations.invalidate({
|
||||
refId: bookedRoom.refId,
|
||||
lang,
|
||||
rooms: bookedRoom.linkedReservations,
|
||||
})
|
||||
|
||||
closeModal()
|
||||
},
|
||||
onError() {
|
||||
@@ -116,13 +113,13 @@ export default function FinalConfirmation({
|
||||
|
||||
function cancelBooking() {
|
||||
if (Array.isArray(formRooms)) {
|
||||
const refIdsToCancel = formRooms
|
||||
const confirmationNumbersToCancel = formRooms
|
||||
.filter((r) => r.checked)
|
||||
.map((r) => r.confirmationNumber)
|
||||
if (refIdsToCancel.length) {
|
||||
if (confirmationNumbersToCancel.length) {
|
||||
cancelBookingsMutation.mutate({
|
||||
refIds: refIdsToCancel,
|
||||
lang,
|
||||
confirmationNumbers: confirmationNumbersToCancel,
|
||||
language: lang,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -54,10 +54,10 @@ export default function Confirmation({
|
||||
)
|
||||
|
||||
const updateBooking = trpc.booking.update.useMutation({
|
||||
onSuccess: (refId) => {
|
||||
if (refId) {
|
||||
utils.booking.confirmation.invalidate({
|
||||
refId,
|
||||
onSuccess: (updatedBooking) => {
|
||||
if (updatedBooking) {
|
||||
utils.booking.get.invalidate({
|
||||
confirmationNumber: updatedBooking.confirmationNumber,
|
||||
})
|
||||
|
||||
toast.success(
|
||||
@@ -86,7 +86,7 @@ export default function Confirmation({
|
||||
|
||||
function handleModifyStay() {
|
||||
updateBooking.mutate({
|
||||
refId: bookedRoom.refId,
|
||||
confirmationNumber: bookedRoom.confirmationNumber,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
})
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function Form() {
|
||||
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}`
|
||||
|
||||
const { guaranteeBooking, isLoading, handleGuaranteeError } =
|
||||
useGuaranteeBooking(refId, false, hotelId)
|
||||
useGuaranteeBooking(confirmationNumber, false, hotelId)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -85,7 +85,7 @@ export default function Form() {
|
||||
: undefined
|
||||
writeGlaToSessionStorage("yes", hotelId)
|
||||
guaranteeBooking.mutate({
|
||||
refId,
|
||||
confirmationNumber,
|
||||
language: lang,
|
||||
...(card && { card }),
|
||||
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`,
|
||||
|
||||
@@ -143,7 +143,6 @@ export function mapRoomDetails({
|
||||
priceType,
|
||||
rate,
|
||||
rateDefinition: booking.rateDefinition,
|
||||
refId: booking.refId,
|
||||
reservationStatus: booking.reservationStatus,
|
||||
room,
|
||||
roomName: room?.name ?? "",
|
||||
|
||||
@@ -37,6 +37,8 @@ export enum ChildBedTypeEnum {
|
||||
export const REDEMPTION = "redemption"
|
||||
export const SEARCHTYPE = "searchtype"
|
||||
|
||||
export const BOOKING_CONFIRMATION_NUMBER = "confirmationNumber"
|
||||
|
||||
export const MEMBERSHIP_FAILED_ERROR = "MembershipFailedError"
|
||||
|
||||
export enum PaymentMethodEnum {
|
||||
|
||||
@@ -13,7 +13,7 @@ const maxRetries = 15
|
||||
const retryInterval = 2000
|
||||
|
||||
export function useGuaranteeBooking(
|
||||
refId: string,
|
||||
confirmationNumber: string,
|
||||
isAncillaryFlow = false,
|
||||
hotelId: string
|
||||
) {
|
||||
@@ -51,10 +51,10 @@ export function useGuaranteeBooking(
|
||||
onSuccess: (result) => {
|
||||
if (result) {
|
||||
if (result.reservationStatus == BookingStatusEnum.BookingCompleted) {
|
||||
utils.booking.confirmation.invalidate({ refId })
|
||||
utils.booking.get.invalidate({ confirmationNumber })
|
||||
} else {
|
||||
setIsPollingForBookingStatus(true)
|
||||
utils.booking.status.invalidate({ refId })
|
||||
utils.booking.status.invalidate({ confirmationNumber })
|
||||
}
|
||||
} else {
|
||||
handleGuaranteeError()
|
||||
@@ -66,7 +66,7 @@ export function useGuaranteeBooking(
|
||||
})
|
||||
|
||||
const bookingStatus = useHandleBookingStatus({
|
||||
refId,
|
||||
confirmationNumber,
|
||||
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
||||
maxRetries,
|
||||
retryInterval,
|
||||
@@ -76,7 +76,7 @@ export function useGuaranteeBooking(
|
||||
useEffect(() => {
|
||||
if (bookingStatus?.data?.paymentUrl && isPollingForBookingStatus) {
|
||||
router.push(bookingStatus.data.paymentUrl)
|
||||
utils.booking.confirmation.invalidate({ refId })
|
||||
utils.booking.get.invalidate({ confirmationNumber })
|
||||
setIsPollingForBookingStatus(false)
|
||||
} else if (bookingStatus.isTimeout) {
|
||||
handleGuaranteeError("Timeout")
|
||||
@@ -87,8 +87,8 @@ export function useGuaranteeBooking(
|
||||
handleGuaranteeError,
|
||||
setIsPollingForBookingStatus,
|
||||
isPollingForBookingStatus,
|
||||
refId,
|
||||
utils.booking.confirmation,
|
||||
confirmationNumber,
|
||||
utils.booking.get,
|
||||
])
|
||||
|
||||
const isLoading =
|
||||
|
||||
@@ -7,13 +7,13 @@ import { trpc } from "@/lib/trpc/client"
|
||||
import type { BookingStatusEnum } from "@/constants/booking"
|
||||
|
||||
export function useHandleBookingStatus({
|
||||
refId,
|
||||
confirmationNumber,
|
||||
expectedStatuses,
|
||||
maxRetries,
|
||||
retryInterval,
|
||||
enabled,
|
||||
}: {
|
||||
refId: string
|
||||
confirmationNumber: string | null
|
||||
expectedStatuses: BookingStatusEnum[]
|
||||
maxRetries: number
|
||||
retryInterval: number
|
||||
@@ -22,7 +22,7 @@ export function useHandleBookingStatus({
|
||||
const retries = useRef(0)
|
||||
|
||||
const query = trpc.booking.status.useQuery(
|
||||
{ refId },
|
||||
{ confirmationNumber: confirmationNumber ?? "" },
|
||||
{
|
||||
enabled,
|
||||
refetchInterval: (query) => {
|
||||
|
||||
@@ -60,9 +60,6 @@ export namespace endpoints {
|
||||
export function booking(confirmationNumber: string) {
|
||||
return `${bookings}/${confirmationNumber}`
|
||||
}
|
||||
export function find(confirmationNumber: string) {
|
||||
return `${bookings}/${confirmationNumber}/find`
|
||||
}
|
||||
export function cancel(confirmationNumber: string) {
|
||||
return `${bookings}/${confirmationNumber}/cancel`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getHotel as _getHotel } from "@/server/routers/hotels/utils"
|
||||
import { isDefined } from "@/server/utils"
|
||||
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
@@ -18,6 +17,7 @@ import type {
|
||||
HotelInput,
|
||||
} from "@/types/trpc/routers/hotel/hotel"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { LinkedReservationsInput } from "@/server/routers/booking/input"
|
||||
import type { GetHotelsByCSFilterInput } from "@/server/routers/hotels/input"
|
||||
import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input"
|
||||
|
||||
@@ -136,34 +136,14 @@ export const getPackages = cache(async function getMemoizedPackages(
|
||||
})
|
||||
|
||||
export const getBookingConfirmation = cache(
|
||||
async function getMemoizedBookingConfirmation(refId: string, lang: Lang) {
|
||||
return serverClient().booking.confirmation({
|
||||
refId,
|
||||
lang,
|
||||
})
|
||||
async function getMemoizedBookingConfirmation(confirmationNumber: string) {
|
||||
return serverClient().booking.get({ confirmationNumber })
|
||||
}
|
||||
)
|
||||
|
||||
export const findBooking = cache(async function getMemoizedFindBooking(
|
||||
confirmationNumber: string,
|
||||
lastName: string,
|
||||
firstName: string,
|
||||
email: string
|
||||
) {
|
||||
return serverClient().booking.findBooking({
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
firstName,
|
||||
email,
|
||||
})
|
||||
})
|
||||
|
||||
export const getLinkedReservations = cache(
|
||||
async function getMemoizedLinkedReservations(refId: string, lang: Lang) {
|
||||
return serverClient().booking.linkedReservations({
|
||||
refId,
|
||||
lang,
|
||||
})
|
||||
async function getMemoizedLinkedReservations(input: LinkedReservationsInput) {
|
||||
return serverClient().booking.linkedReservations(input)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -285,6 +285,11 @@ const nextConfig = {
|
||||
source: `${myPages.sv}/:path*`,
|
||||
destination: `/sv/my-pages/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/:lang/hotelreservation/payment-callback/:status",
|
||||
destination:
|
||||
"/:lang/hotelreservation/payment-callback?status=:status",
|
||||
},
|
||||
// Find my booking
|
||||
{
|
||||
source: findMyBooking.en,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { notFound } from "next/navigation"
|
||||
import { use, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { type RouterOutput, trpc } from "@/lib/trpc/client"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { createMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton"
|
||||
@@ -13,7 +12,10 @@ import { MyStayContext } from "@/contexts/MyStay"
|
||||
import type { Packages } from "@/types/components/myPages/myStay/ancillaries"
|
||||
import type { MyStayStore } from "@/types/contexts/my-stay"
|
||||
import type { RoomCategories } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type {
|
||||
BookingConfirmation,
|
||||
BookingConfirmationSchema,
|
||||
} from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { CreditCard } from "@/types/user"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
@@ -21,9 +23,7 @@ interface MyStayProviderProps {
|
||||
bookingConfirmation: BookingConfirmation
|
||||
breakfastPackages: Packages | null
|
||||
lang: Lang
|
||||
linkedReservationsPromise: Promise<
|
||||
RouterOutput["booking"]["linkedReservations"]
|
||||
>
|
||||
linkedReservationsPromise: Promise<BookingConfirmationSchema[]>
|
||||
refId: string
|
||||
roomCategories: RoomCategories
|
||||
savedCreditCards: CreditCard[] | null
|
||||
@@ -39,14 +39,13 @@ export default function MyStayProvider({
|
||||
roomCategories,
|
||||
savedCreditCards,
|
||||
}: React.PropsWithChildren<MyStayProviderProps>) {
|
||||
const storeRef = useRef<MyStayStore>()
|
||||
const intl = useIntl()
|
||||
|
||||
const storeRef = useRef<MyStayStore>()
|
||||
|
||||
const { data, error, isFetching, isFetchedAfterMount } =
|
||||
trpc.booking.confirmation.useQuery(
|
||||
trpc.booking.get.useQuery(
|
||||
{
|
||||
refId,
|
||||
confirmationNumber: bookingConfirmation.booking.confirmationNumber,
|
||||
lang,
|
||||
},
|
||||
{
|
||||
@@ -69,7 +68,7 @@ export default function MyStayProvider({
|
||||
} = trpc.booking.linkedReservations.useQuery(
|
||||
{
|
||||
lang,
|
||||
refId,
|
||||
rooms: bookingConfirmation.booking.linkedReservations,
|
||||
},
|
||||
{
|
||||
initialData: linkedReservationsResponses,
|
||||
@@ -86,16 +85,15 @@ export default function MyStayProvider({
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const rooms = [data.booking].concat(linkedReservations ?? [])
|
||||
const rooms = [data.booking, ...linkedReservations]
|
||||
|
||||
const hasInvalidatedQueryAndRefetched =
|
||||
(isFetchedAfterMount && data) ||
|
||||
(linkedReservationsIsFetchedAfterMount && linkedReservations)
|
||||
|
||||
if (!storeRef.current || hasInvalidatedQueryAndRefetched) {
|
||||
storeRef.current = createMyStayStore({
|
||||
breakfastPackages,
|
||||
hotel: bookingConfirmation.hotelData.hotel,
|
||||
hotel: bookingConfirmation.hotel,
|
||||
intl,
|
||||
refId,
|
||||
roomCategories,
|
||||
|
||||
@@ -103,7 +103,7 @@ export const createBookingInput = z.object({
|
||||
})
|
||||
|
||||
export const addPackageInput = z.object({
|
||||
refId: z.string(),
|
||||
confirmationNumber: z.string(),
|
||||
ancillaryComment: z.string(),
|
||||
ancillaryDeliveryTime: z.string().nullish(),
|
||||
packages: z.array(
|
||||
@@ -117,22 +117,22 @@ export const addPackageInput = z.object({
|
||||
})
|
||||
|
||||
export const removePackageInput = z.object({
|
||||
refId: z.string(),
|
||||
confirmationNumber: z.string(),
|
||||
codes: z.array(z.string()),
|
||||
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
|
||||
})
|
||||
|
||||
export const priceChangeInput = z.object({
|
||||
refId: z.string(),
|
||||
confirmationNumber: z.string(),
|
||||
})
|
||||
|
||||
export const cancelBookingsInput = z.object({
|
||||
refIds: z.array(z.string()),
|
||||
lang: z.nativeEnum(Lang),
|
||||
confirmationNumbers: z.array(z.string()),
|
||||
language: z.nativeEnum(Lang),
|
||||
})
|
||||
|
||||
export const guaranteeBookingInput = z.object({
|
||||
refId: z.string(),
|
||||
confirmationNumber: z.string(),
|
||||
card: z
|
||||
.object({
|
||||
alias: z.string(),
|
||||
@@ -156,7 +156,7 @@ export const createRefIdInput = z.object({
|
||||
})
|
||||
|
||||
export const updateBookingInput = z.object({
|
||||
refId: z.string(),
|
||||
confirmationNumber: z.string(),
|
||||
checkInDate: z.string().optional(),
|
||||
checkOutDate: z.string().optional(),
|
||||
guest: z
|
||||
@@ -168,35 +168,22 @@ export const updateBookingInput = z.object({
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const bookingConfirmationInput = z.object({
|
||||
refId: z.string(),
|
||||
lang: z.nativeEnum(Lang).optional(),
|
||||
})
|
||||
|
||||
export const getLinkedReservationsInput = z.object({
|
||||
refId: z.string(),
|
||||
lang: z.nativeEnum(Lang).optional(),
|
||||
})
|
||||
|
||||
export const findBookingInput = z.object({
|
||||
// Query
|
||||
const confirmationNumberInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
email: z.string(),
|
||||
lang: z.nativeEnum(Lang).optional(),
|
||||
})
|
||||
|
||||
export const getBookingInput = confirmationNumberInput
|
||||
export const getLinkedReservationsInput = z.object({
|
||||
lang: z.nativeEnum(Lang).optional(),
|
||||
rooms: z.array(
|
||||
z.object({
|
||||
confirmationNumber: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export type LinkedReservationsInput = z.input<typeof getLinkedReservationsInput>
|
||||
|
||||
export const getBookingStatusInput = z.object({
|
||||
refId: z.string(),
|
||||
})
|
||||
|
||||
export const getBookingConfirmationErrorInput = z.object({
|
||||
refId: z.string(),
|
||||
})
|
||||
|
||||
export const getConfirmationCompletedInput = z.object({
|
||||
refId: z.string(),
|
||||
lang: z.nativeEnum(Lang),
|
||||
})
|
||||
export const getBookingStatusInput = confirmationNumberInput
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import * as api from "@/lib/api"
|
||||
import { getMembershipNumber } from "@/server/routers/user/utils"
|
||||
import { createCounter } from "@/server/telemetry"
|
||||
import { getUserOrServiceToken } from "@/server/tokenManager"
|
||||
import { router, safeProtectedServiceProcedure } from "@/server/trpc"
|
||||
|
||||
import { parseRefId } from "@/utils/refId"
|
||||
|
||||
import {
|
||||
addPackageInput,
|
||||
cancelBookingsInput,
|
||||
@@ -15,7 +12,7 @@ import {
|
||||
removePackageInput,
|
||||
updateBookingInput,
|
||||
} from "./input"
|
||||
import { bookingSchema, createBookingSchema } from "./output"
|
||||
import { bookingConfirmationSchema, createBookingSchema } from "./output"
|
||||
import { cancelBooking } from "./utils"
|
||||
|
||||
export const bookingMutationRouter = router({
|
||||
@@ -76,17 +73,8 @@ export const bookingMutationRouter = router({
|
||||
}),
|
||||
priceChange: safeProtectedServiceProcedure
|
||||
.input(priceChangeInput)
|
||||
.use(async ({ input, next }) => {
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
confirmationNumber,
|
||||
},
|
||||
})
|
||||
})
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const { confirmationNumber } = ctx
|
||||
const { confirmationNumber } = input
|
||||
|
||||
const priceChangeCounter = createCounter("trpc.booking", "price-change")
|
||||
const metricsPriceChange = priceChangeCounter.init({ confirmationNumber })
|
||||
@@ -121,29 +109,17 @@ export const bookingMutationRouter = router({
|
||||
|
||||
metricsPriceChange.success()
|
||||
|
||||
return verifiedData.data.id
|
||||
return verifiedData.data
|
||||
}),
|
||||
cancel: safeProtectedServiceProcedure
|
||||
.input(cancelBookingsInput)
|
||||
.use(async ({ input, next }) => {
|
||||
const confirmationNumbers = input.refIds.map((refId) => {
|
||||
const { confirmationNumber } = parseRefId(refId)
|
||||
return confirmationNumber
|
||||
})
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
confirmationNumbers,
|
||||
},
|
||||
})
|
||||
})
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const { confirmationNumbers } = ctx
|
||||
const { lang } = input
|
||||
const token = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
const { confirmationNumbers, language } = input
|
||||
|
||||
const responses = await Promise.allSettled(
|
||||
confirmationNumbers.map((confirmationNumber) =>
|
||||
cancelBooking(confirmationNumber, lang)
|
||||
cancelBooking(confirmationNumber, language, token)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -168,19 +144,10 @@ export const bookingMutationRouter = router({
|
||||
}),
|
||||
packages: safeProtectedServiceProcedure
|
||||
.input(addPackageInput)
|
||||
.use(async ({ input, next }) => {
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
confirmationNumber,
|
||||
},
|
||||
})
|
||||
})
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
const { refId, ...body } = input
|
||||
const { confirmationNumber } = ctx
|
||||
const { confirmationNumber, ...body } = input
|
||||
|
||||
const addPackageCounter = createCounter("trpc.booking", "package.add")
|
||||
const metricsAddPackage = addPackageCounter.init({ confirmationNumber })
|
||||
|
||||
@@ -216,19 +183,10 @@ export const bookingMutationRouter = router({
|
||||
}),
|
||||
guarantee: safeProtectedServiceProcedure
|
||||
.input(guaranteeBookingInput)
|
||||
.use(async ({ input, next }) => {
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
confirmationNumber,
|
||||
},
|
||||
})
|
||||
})
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
const { refId, language, ...body } = input
|
||||
const { confirmationNumber } = ctx
|
||||
const { confirmationNumber, language, ...body } = input
|
||||
|
||||
const guaranteeBookingCounter = createCounter("trpc.booking", "guarantee")
|
||||
const metricsGuaranteeBooking = guaranteeBookingCounter.init({
|
||||
confirmationNumber,
|
||||
@@ -267,16 +225,10 @@ export const bookingMutationRouter = router({
|
||||
}),
|
||||
update: safeProtectedServiceProcedure
|
||||
.input(updateBookingInput)
|
||||
.use(async ({ input, next }) => {
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
return next({
|
||||
ctx: {
|
||||
confirmationNumber,
|
||||
},
|
||||
})
|
||||
})
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const { confirmationNumber } = ctx
|
||||
const accessToken = ctx.session?.token.access_token || ctx.serviceToken
|
||||
const { confirmationNumber, ...body } = input
|
||||
|
||||
const updateBookingCounter = createCounter("trpc.booking", "update")
|
||||
const metricsUpdateBooking = updateBookingCounter.init({
|
||||
confirmationNumber,
|
||||
@@ -284,17 +236,12 @@ export const bookingMutationRouter = router({
|
||||
|
||||
metricsUpdateBooking.start()
|
||||
|
||||
const token = getUserOrServiceToken()
|
||||
const apiResponse = await api.put(
|
||||
api.endpoints.v1.Booking.booking(confirmationNumber),
|
||||
{
|
||||
body: {
|
||||
checkInDate: input.checkInDate,
|
||||
checkOutDate: input.checkOutDate,
|
||||
guest: input.guest,
|
||||
},
|
||||
body,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -306,7 +253,7 @@ export const bookingMutationRouter = router({
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
|
||||
const verifiedData = bookingSchema.safeParse(apiJson)
|
||||
const verifiedData = bookingConfirmationSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
metricsUpdateBooking.validationError(verifiedData.error)
|
||||
return null
|
||||
@@ -314,23 +261,14 @@ export const bookingMutationRouter = router({
|
||||
|
||||
metricsUpdateBooking.success()
|
||||
|
||||
return verifiedData.data.refId
|
||||
return verifiedData.data
|
||||
}),
|
||||
removePackage: safeProtectedServiceProcedure
|
||||
.input(removePackageInput)
|
||||
.use(async ({ input, next }) => {
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
confirmationNumber,
|
||||
},
|
||||
})
|
||||
})
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
const { codes, language } = input
|
||||
const { confirmationNumber } = ctx
|
||||
const { confirmationNumber, codes, language } = input
|
||||
|
||||
const removePackageCounter = createCounter(
|
||||
"trpc.booking",
|
||||
"package.remove"
|
||||
|
||||
@@ -2,7 +2,6 @@ import { z } from "zod"
|
||||
|
||||
import { BookingStatusEnum, ChildBedTypeEnum } from "@/constants/booking"
|
||||
|
||||
import { calculateRefId } from "@/utils/refId"
|
||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||
import { nullableIntValidator } from "@/utils/zod/numberValidator"
|
||||
import {
|
||||
@@ -79,13 +78,7 @@ export const createBookingSchema = z
|
||||
type: d.data.type,
|
||||
reservationStatus: d.data.attributes.reservationStatus,
|
||||
paymentUrl: d.data.attributes.paymentUrl,
|
||||
rooms: d.data.attributes.rooms.map((room) => {
|
||||
const lastName = d.data.attributes.guest?.lastName || ""
|
||||
return {
|
||||
...room,
|
||||
refId: calculateRefId(room.confirmationNumber, lastName),
|
||||
}
|
||||
}),
|
||||
rooms: d.data.attributes.rooms,
|
||||
errors: d.data.attributes.errors,
|
||||
guest: d.data.attributes.guest,
|
||||
}))
|
||||
@@ -202,7 +195,7 @@ const linksSchema = z.object({
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
export const bookingSchema = z
|
||||
export const bookingConfirmationSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
@@ -255,19 +248,6 @@ export const bookingSchema = z
|
||||
})
|
||||
.transform(({ data }) => ({
|
||||
...data.attributes,
|
||||
refId: calculateRefId(
|
||||
data.attributes.confirmationNumber,
|
||||
data.attributes.guest.lastName
|
||||
),
|
||||
linkedReservations: data.attributes.linkedReservations.map(
|
||||
(linkedReservation) => {
|
||||
const lastName = data.attributes.guest.lastName
|
||||
return {
|
||||
...linkedReservation,
|
||||
refId: calculateRefId(linkedReservation.confirmationNumber, lastName),
|
||||
}
|
||||
}
|
||||
),
|
||||
packages: data.attributes.packages.filter((p) => p.type !== "Ancillary"),
|
||||
ancillaries: data.attributes.packages.filter((p) => p.type === "Ancillary"),
|
||||
extraBedTypes: data.attributes.childBedPreferences,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { BookingStatusEnum } from "@/constants/booking"
|
||||
import { bookingConfirmation } from "@/constants/routes/hotelReservation"
|
||||
import * as api from "@/lib/api"
|
||||
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
|
||||
import { createCounter } from "@/server/telemetry"
|
||||
@@ -8,47 +6,38 @@ import {
|
||||
safeProtectedServiceProcedure,
|
||||
serviceProcedure,
|
||||
} from "@/server/trpc"
|
||||
import { getBookedHotelRoom } from "@/stores/my-stay"
|
||||
|
||||
import { calculateRefId, parseRefId } from "@/utils/refId"
|
||||
|
||||
import { getHotel } from "../hotels/utils"
|
||||
import { encrypt } from "../utils/encryption"
|
||||
import {
|
||||
bookingConfirmationInput,
|
||||
createRefIdInput,
|
||||
findBookingInput,
|
||||
getBookingConfirmationErrorInput,
|
||||
getBookingInput,
|
||||
getBookingStatusInput,
|
||||
getConfirmationCompletedInput,
|
||||
getLinkedReservationsInput,
|
||||
} from "./input"
|
||||
import { createBookingSchema } from "./output"
|
||||
import { findBooking, getBooking, getLinkedReservations } from "./utils"
|
||||
|
||||
import type { BookingSchema } from "@/types/trpc/routers/booking/confirmation"
|
||||
import { getBookedHotelRoom, getBooking } from "./utils"
|
||||
|
||||
export const bookingQueryRouter = router({
|
||||
confirmation: safeProtectedServiceProcedure
|
||||
.input(bookingConfirmationInput)
|
||||
get: safeProtectedServiceProcedure
|
||||
.input(getBookingInput)
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
const lang = input.lang ?? ctx.lang
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
const token = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
return next({
|
||||
ctx: {
|
||||
lang,
|
||||
confirmationNumber,
|
||||
token,
|
||||
},
|
||||
})
|
||||
})
|
||||
.query(async function ({
|
||||
ctx: { confirmationNumber, lang, serviceToken },
|
||||
}) {
|
||||
.query(async function ({ ctx, input: { confirmationNumber } }) {
|
||||
const getBookingCounter = createCounter("trpc.booking", "get")
|
||||
const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
|
||||
|
||||
metricsGetBooking.start()
|
||||
|
||||
const booking = await getBooking(confirmationNumber, lang)
|
||||
const booking = await getBooking(confirmationNumber, ctx.lang, ctx.token)
|
||||
|
||||
if (!booking) {
|
||||
metricsGetBooking.dataError(
|
||||
@@ -58,80 +47,6 @@ export const bookingQueryRouter = router({
|
||||
return null
|
||||
}
|
||||
|
||||
const hotelData = await getHotel(
|
||||
{
|
||||
hotelId: booking.hotelId,
|
||||
isCardOnlyPayment: false,
|
||||
language: lang,
|
||||
},
|
||||
serviceToken
|
||||
)
|
||||
|
||||
if (!hotelData) {
|
||||
metricsGetBooking.dataError(
|
||||
`Failed to get hotel data for ${booking.hotelId}`,
|
||||
{
|
||||
hotelId: booking.hotelId,
|
||||
}
|
||||
)
|
||||
|
||||
throw serverErrorByStatus(404)
|
||||
}
|
||||
|
||||
const room = getBookedHotelRoom(
|
||||
hotelData.roomCategories,
|
||||
booking.roomTypeCode
|
||||
)
|
||||
|
||||
if (!room) {
|
||||
metricsGetBooking.dataError(
|
||||
`Failed to extract booked room ${booking.roomTypeCode} from room categories for ${booking.hotelId}`,
|
||||
{
|
||||
roomTypeCode: booking.roomTypeCode,
|
||||
hotelId: booking.hotelId,
|
||||
}
|
||||
)
|
||||
|
||||
throw serverErrorByStatus(404)
|
||||
}
|
||||
|
||||
metricsGetBooking.success()
|
||||
|
||||
return {
|
||||
hotelData,
|
||||
booking,
|
||||
room,
|
||||
}
|
||||
}),
|
||||
findBooking: safeProtectedServiceProcedure
|
||||
.input(findBookingInput)
|
||||
|
||||
.query(async function ({
|
||||
ctx,
|
||||
input: { confirmationNumber, lastName, firstName, email },
|
||||
}) {
|
||||
const findBookingCounter = createCounter("trpc.booking", "findBooking")
|
||||
const metricsFindBooking = findBookingCounter.init({ confirmationNumber })
|
||||
|
||||
metricsFindBooking.start()
|
||||
|
||||
const booking = await findBooking(
|
||||
confirmationNumber,
|
||||
ctx.lang,
|
||||
ctx.serviceToken,
|
||||
lastName,
|
||||
firstName,
|
||||
email
|
||||
)
|
||||
|
||||
if (!booking) {
|
||||
metricsFindBooking.dataError(
|
||||
`Fail to find booking data for ${confirmationNumber}`,
|
||||
{ confirmationNumber }
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const hotelData = await getHotel(
|
||||
{
|
||||
hotelId: booking.hotelId,
|
||||
@@ -142,8 +57,8 @@ export const bookingQueryRouter = router({
|
||||
)
|
||||
|
||||
if (!hotelData) {
|
||||
metricsFindBooking.dataError(
|
||||
`Failed to find hotel data for ${booking.hotelId}`,
|
||||
metricsGetBooking.dataError(
|
||||
`Failed to get hotel data for ${booking.hotelId}`,
|
||||
{
|
||||
hotelId: booking.hotelId,
|
||||
}
|
||||
@@ -152,248 +67,109 @@ export const bookingQueryRouter = router({
|
||||
throw serverErrorByStatus(404)
|
||||
}
|
||||
|
||||
const room = getBookedHotelRoom(
|
||||
hotelData.roomCategories,
|
||||
booking.roomTypeCode
|
||||
)
|
||||
|
||||
if (!room) {
|
||||
metricsFindBooking.dataError(
|
||||
`Failed to extract booked room ${booking.roomTypeCode} from room categories for ${booking.hotelId}`,
|
||||
{
|
||||
roomTypeCode: booking.roomTypeCode,
|
||||
hotelId: booking.hotelId,
|
||||
}
|
||||
)
|
||||
|
||||
throw serverErrorByStatus(404)
|
||||
}
|
||||
|
||||
metricsFindBooking.success()
|
||||
metricsGetBooking.success()
|
||||
|
||||
return {
|
||||
hotelData,
|
||||
...hotelData,
|
||||
booking,
|
||||
room,
|
||||
room: getBookedHotelRoom(
|
||||
hotelData.roomCategories,
|
||||
booking.roomTypeCode
|
||||
),
|
||||
}
|
||||
}),
|
||||
linkedReservations: safeProtectedServiceProcedure
|
||||
.input(getLinkedReservationsInput)
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
const lang = input.lang ?? ctx.lang
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
|
||||
const token = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
return next({
|
||||
ctx: {
|
||||
lang,
|
||||
confirmationNumber,
|
||||
token,
|
||||
},
|
||||
})
|
||||
})
|
||||
.query(async function ({ ctx: { confirmationNumber, lang } }) {
|
||||
const linkedReservationsCounter = createCounter(
|
||||
.query(async function ({ ctx, input: { rooms } }) {
|
||||
const getLinkedReservationsCounter = createCounter(
|
||||
"trpc.booking",
|
||||
"linkedReservations"
|
||||
)
|
||||
const metricsLinkedReservations = linkedReservationsCounter.init({
|
||||
confirmationNumber,
|
||||
const metricsGetLinkedReservations = getLinkedReservationsCounter.init({
|
||||
confirmationNumbers: rooms,
|
||||
})
|
||||
|
||||
metricsLinkedReservations.start()
|
||||
metricsGetLinkedReservations.start()
|
||||
|
||||
const linkedReservations = await getLinkedReservations(
|
||||
confirmationNumber,
|
||||
lang
|
||||
const linkedReservationsResult = await Promise.allSettled(
|
||||
rooms.map((room) =>
|
||||
getBooking(room.confirmationNumber, ctx.lang, ctx.token)
|
||||
)
|
||||
)
|
||||
|
||||
if (!linkedReservations) {
|
||||
metricsLinkedReservations.noDataError()
|
||||
return null
|
||||
}
|
||||
|
||||
const validLinkedReservations = linkedReservations.reduce<
|
||||
BookingSchema[]
|
||||
>((acc, linkedReservation) => {
|
||||
if ("error" in linkedReservation) {
|
||||
metricsLinkedReservations.dataError(
|
||||
`Failed to get linked reservations ${linkedReservation.confirmationNumber}`,
|
||||
{
|
||||
linkedReservationConfirmationNumber:
|
||||
linkedReservation.confirmationNumber,
|
||||
}
|
||||
const linkedReservations = []
|
||||
for (const booking of linkedReservationsResult) {
|
||||
if (booking.status === "fulfilled") {
|
||||
if (booking.value) {
|
||||
linkedReservations.push(booking.value)
|
||||
} else {
|
||||
metricsGetLinkedReservations.dataError(
|
||||
`Unexpected value for linked reservation`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
metricsGetLinkedReservations.dataError(
|
||||
`Failed to get linked reservation`
|
||||
)
|
||||
return acc
|
||||
}
|
||||
}
|
||||
|
||||
acc.push(linkedReservation)
|
||||
return acc
|
||||
}, [])
|
||||
metricsGetLinkedReservations.success()
|
||||
|
||||
metricsLinkedReservations.success()
|
||||
|
||||
return validLinkedReservations
|
||||
return linkedReservations
|
||||
}),
|
||||
status: serviceProcedure
|
||||
.input(getBookingStatusInput)
|
||||
.use(async ({ input, next }) => {
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
status: serviceProcedure.input(getBookingStatusInput).query(async function ({
|
||||
ctx,
|
||||
input,
|
||||
}) {
|
||||
const { confirmationNumber } = input
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
confirmationNumber,
|
||||
},
|
||||
})
|
||||
const getBookingStatusCounter = createCounter("trpc.booking", "status")
|
||||
const metricsGetBookingStatus = getBookingStatusCounter.init({
|
||||
confirmationNumber,
|
||||
})
|
||||
.query(async function ({ ctx: { confirmationNumber, serviceToken } }) {
|
||||
const getBookingStatusCounter = createCounter("trpc.booking", "status")
|
||||
const metricsGetBookingStatus = getBookingStatusCounter.init({
|
||||
confirmationNumber,
|
||||
})
|
||||
|
||||
metricsGetBookingStatus.start()
|
||||
metricsGetBookingStatus.start()
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Booking.status(confirmationNumber),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
await metricsGetBookingStatus.httpError(apiResponse)
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
metricsGetBookingStatus.validationError(verifiedData.error)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
metricsGetBookingStatus.success()
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
|
||||
confirmationCompleted: serviceProcedure
|
||||
.input(getConfirmationCompletedInput)
|
||||
.use(async ({ input, next }) => {
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
confirmationNumber,
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Booking.status(confirmationNumber),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
})
|
||||
})
|
||||
.query(async function ({ ctx, input }) {
|
||||
const { confirmationNumber } = ctx
|
||||
|
||||
const confirmationCompletedCounter = createCounter(
|
||||
"trpc.booking",
|
||||
"confirmationCompleted"
|
||||
)
|
||||
const metricsConfirmationCompleted = confirmationCompletedCounter.init({
|
||||
confirmationNumber,
|
||||
})
|
||||
|
||||
metricsConfirmationCompleted.start()
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Booking.status(confirmationNumber),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
await metricsConfirmationCompleted.httpError(apiResponse)
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
)
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
metricsConfirmationCompleted.validationError(verifiedData.error)
|
||||
throw badRequestError()
|
||||
}
|
||||
if (!apiResponse.ok) {
|
||||
await metricsGetBookingStatus.httpError(apiResponse)
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
const confirmationUrl =
|
||||
verifiedData.data.reservationStatus ===
|
||||
BookingStatusEnum.BookingCompleted
|
||||
? `${bookingConfirmation(input.lang)}?RefId=${verifiedData.data.rooms[0].refId}`
|
||||
: ""
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
metricsGetBookingStatus.validationError(verifiedData.error)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
const result = {
|
||||
...verifiedData.data,
|
||||
redirectUrl: verifiedData.data.paymentUrl || confirmationUrl,
|
||||
}
|
||||
|
||||
metricsConfirmationCompleted.success()
|
||||
|
||||
return result
|
||||
}),
|
||||
|
||||
confirmationError: serviceProcedure
|
||||
.input(getBookingConfirmationErrorInput)
|
||||
.use(async ({ input, next }) => {
|
||||
const { confirmationNumber } = parseRefId(input.refId)
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
confirmationNumber,
|
||||
},
|
||||
})
|
||||
})
|
||||
.query(async function ({ ctx }) {
|
||||
const { confirmationNumber } = ctx
|
||||
|
||||
const confirmationErrorCounter = createCounter(
|
||||
"trpc.booking",
|
||||
"confirmationError"
|
||||
)
|
||||
const metricsConfirmationError = confirmationErrorCounter.init({
|
||||
confirmationNumber,
|
||||
})
|
||||
|
||||
metricsConfirmationError.start()
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Booking.status(confirmationNumber),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
await metricsConfirmationError.httpError(apiResponse)
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
metricsConfirmationError.validationError(verifiedData.error)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
metricsConfirmationError.success()
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
metricsGetBookingStatus.success()
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
createRefId: serviceProcedure
|
||||
.input(createRefIdInput)
|
||||
.mutation(async function ({ input }) {
|
||||
const { confirmationNumber, lastName } = input
|
||||
const encryptedRefId = calculateRefId(confirmationNumber, lastName)
|
||||
const encryptedRefId = encrypt(`${confirmationNumber},${lastName}`)
|
||||
|
||||
if (!encryptedRefId) {
|
||||
throw serverErrorByStatus(422, "Was not able to encrypt ref id")
|
||||
|
||||
@@ -1,194 +1,55 @@
|
||||
import * as api from "@/lib/api"
|
||||
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
|
||||
import { createCounter } from "@/server/telemetry"
|
||||
import { getUserOrServiceToken } from "@/server/tokenManager"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { bookingConfirmationSchema, createBookingSchema } from "./output"
|
||||
|
||||
import { bookingSchema, createBookingSchema } from "./output"
|
||||
|
||||
import type { BookingSchema } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { Room } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
export async function getBooking(confirmationNumber: string, lang: Lang) {
|
||||
export function getBookedHotelRoom(
|
||||
rooms: Room[] | undefined,
|
||||
roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"]
|
||||
) {
|
||||
if (!rooms?.length || !roomTypeCode) {
|
||||
return null
|
||||
}
|
||||
const room = rooms?.find((r) => {
|
||||
return r.roomTypes.find((roomType) => roomType.code === roomTypeCode)
|
||||
})
|
||||
if (!room) {
|
||||
return null
|
||||
}
|
||||
const bedType = room.roomTypes.find(
|
||||
(roomType) => roomType.code === roomTypeCode
|
||||
)
|
||||
if (!bedType) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...room,
|
||||
bedType,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBooking(
|
||||
confirmationNumber: string,
|
||||
lang: Lang,
|
||||
token: string
|
||||
) {
|
||||
const getBookingCounter = createCounter("booking", "get")
|
||||
const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
|
||||
|
||||
metricsGetBooking.start()
|
||||
|
||||
const cacheKey = `${lang}:booking:${confirmationNumber}`
|
||||
const cache = await getCacheClient()
|
||||
|
||||
const result: BookingSchema | null = await cache.cacheOrGet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const token = getUserOrServiceToken()
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Booking.booking(confirmationNumber),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
{ language: toApiLang(lang) }
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
await metricsGetBooking.httpError(apiResponse)
|
||||
|
||||
// If the booking is not found, return null.
|
||||
// This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them.
|
||||
if (apiResponse.status === 400) {
|
||||
return null
|
||||
}
|
||||
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const booking = bookingSchema.safeParse(apiJson)
|
||||
if (!booking.success) {
|
||||
metricsGetBooking.validationError(booking.error)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
return booking.data
|
||||
},
|
||||
"1h"
|
||||
)
|
||||
|
||||
metricsGetBooking.success()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export async function getBookings(confirmationNumbers: string[], lang: Lang) {
|
||||
const results = await Promise.allSettled(
|
||||
confirmationNumbers.map((confirmationNumber) => {
|
||||
return getBooking(confirmationNumber, lang)
|
||||
})
|
||||
)
|
||||
|
||||
return results.map((result) => {
|
||||
if (result.status === "fulfilled" && result.value) {
|
||||
return result.value
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
export async function getLinkedReservations(
|
||||
confirmationNumber: string,
|
||||
lang: Lang
|
||||
) {
|
||||
const booking = await getBooking(confirmationNumber, lang)
|
||||
|
||||
if (!booking) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (booking.linkedReservations.length > 0) {
|
||||
const confirmationNumbers = booking.linkedReservations.map(
|
||||
(linkedReservation) => {
|
||||
return linkedReservation.confirmationNumber
|
||||
}
|
||||
)
|
||||
|
||||
const bookings = await getBookings(confirmationNumbers, lang)
|
||||
|
||||
const linkedReservations = bookings.map((booking, i) => {
|
||||
if (booking === null) {
|
||||
return {
|
||||
confirmationNumber: confirmationNumbers[i],
|
||||
error: true,
|
||||
} as const
|
||||
}
|
||||
return booking
|
||||
})
|
||||
|
||||
return linkedReservations
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export async function cancelBooking(confirmationNumber: string, lang: Lang) {
|
||||
const cancelBookingCounter = createCounter("booking", "cancel")
|
||||
const metricsCancelBooking = cancelBookingCounter.init({
|
||||
confirmationNumber,
|
||||
lang,
|
||||
})
|
||||
|
||||
metricsCancelBooking.start()
|
||||
|
||||
const token = getUserOrServiceToken()
|
||||
const headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
|
||||
const booking = await getBooking(confirmationNumber, lang)
|
||||
if (!booking) {
|
||||
metricsCancelBooking.noDataError({ confirmationNumber })
|
||||
return null
|
||||
}
|
||||
const { firstName, lastName, email } = booking.guest
|
||||
const apiResponse = await api.remove(
|
||||
api.endpoints.v1.Booking.cancel(confirmationNumber),
|
||||
{
|
||||
headers,
|
||||
body: { firstName, lastName, email },
|
||||
},
|
||||
{ language: toApiLang(lang) }
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
await metricsCancelBooking.httpError(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
metricsCancelBooking.validationError(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
metricsCancelBooking.success()
|
||||
|
||||
return verifiedData.data
|
||||
}
|
||||
|
||||
export async function findBooking(
|
||||
confirmationNumber: string,
|
||||
lang: Lang,
|
||||
token: string,
|
||||
lastName?: string,
|
||||
firstName?: string,
|
||||
email?: string
|
||||
) {
|
||||
const findBookingCounter = createCounter("booking", "find")
|
||||
const metricsGetBooking = findBookingCounter.init({
|
||||
confirmationNumber,
|
||||
lastName,
|
||||
firstName,
|
||||
email,
|
||||
})
|
||||
|
||||
metricsGetBooking.start()
|
||||
|
||||
const apiResponse = await api.post(
|
||||
api.endpoints.v1.Booking.find(confirmationNumber),
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Booking.booking(confirmationNumber),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: {
|
||||
lastName,
|
||||
firstName,
|
||||
email,
|
||||
},
|
||||
},
|
||||
{ language: toApiLang(lang) }
|
||||
)
|
||||
@@ -206,7 +67,7 @@ export async function findBooking(
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const booking = bookingSchema.safeParse(apiJson)
|
||||
const booking = bookingConfirmationSchema.safeParse(apiJson)
|
||||
if (!booking.success) {
|
||||
metricsGetBooking.validationError(booking.error)
|
||||
throw badRequestError()
|
||||
@@ -216,3 +77,52 @@ export async function findBooking(
|
||||
|
||||
return booking.data
|
||||
}
|
||||
|
||||
export async function cancelBooking(
|
||||
confirmationNumber: string,
|
||||
language: Lang,
|
||||
token: string
|
||||
) {
|
||||
const cancelBookingCounter = createCounter("booking", "cancel")
|
||||
const metricsCancelBooking = cancelBookingCounter.init({
|
||||
confirmationNumber,
|
||||
language,
|
||||
})
|
||||
|
||||
metricsCancelBooking.start()
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
|
||||
const booking = await getBooking(confirmationNumber, language, token)
|
||||
if (!booking) {
|
||||
metricsCancelBooking.noDataError({ confirmationNumber })
|
||||
return null
|
||||
}
|
||||
const { firstName, lastName, email } = booking.guest
|
||||
const apiResponse = await api.remove(
|
||||
api.endpoints.v1.Booking.cancel(confirmationNumber),
|
||||
{
|
||||
headers,
|
||||
body: { firstName, lastName, email },
|
||||
},
|
||||
{ language: toApiLang(language) }
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
await metricsCancelBooking.httpError(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
metricsCancelBooking.validationError(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
metricsCancelBooking.success()
|
||||
|
||||
return verifiedData.data
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { myStay } from "@/constants/routes/myStay"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { encrypt } from "@/server/routers/utils/encryption"
|
||||
import { createCounter } from "@/server/telemetry"
|
||||
|
||||
import { cache } from "@/utils/cache"
|
||||
import { encrypt } from "@/utils/encryption"
|
||||
import * as maskValue from "@/utils/maskValue"
|
||||
import { isValidSession } from "@/utils/session"
|
||||
import { getCurrentWebUrl } from "@/utils/url"
|
||||
|
||||
@@ -3,9 +3,7 @@ import { trace, type Tracer } from "@opentelemetry/api"
|
||||
import { env } from "@/env/server"
|
||||
import { createCounter } from "@/server/telemetry"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { isValidSession } from "@/utils/session"
|
||||
|
||||
import type { ServiceTokenResponse } from "@/types/tokens"
|
||||
|
||||
@@ -119,12 +117,3 @@ async function fetchServiceToken(scopes: string[]) {
|
||||
function getServiceTokenCacheKey(scopes: string[]): string {
|
||||
return `serviceToken:${scopes.join(",")}`
|
||||
}
|
||||
|
||||
export async function getUserOrServiceToken() {
|
||||
const serviceToken = await getServiceToken()
|
||||
const session = await auth()
|
||||
|
||||
return isValidSession(session)
|
||||
? session.token.access_token
|
||||
: serviceToken.access_token
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { produce } from "immer"
|
||||
import { useContext } from "react"
|
||||
import { create, useStore } from "zustand"
|
||||
|
||||
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
|
||||
|
||||
import { mapRoomDetails } from "@/components/HotelReservation/MyStay/utils/mapRoomDetails"
|
||||
import { MyStayContext } from "@/contexts/MyStay"
|
||||
|
||||
@@ -12,34 +14,7 @@ import {
|
||||
isAllRoomsCancelled,
|
||||
} from "./helpers"
|
||||
|
||||
import type { Room } from "@/types/hotel"
|
||||
import type { InitialState, MyStayState } from "@/types/stores/my-stay"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export function getBookedHotelRoom(
|
||||
rooms: Room[] | undefined,
|
||||
roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"]
|
||||
) {
|
||||
if (!rooms?.length || !roomTypeCode) {
|
||||
return null
|
||||
}
|
||||
const room = rooms?.find((r) => {
|
||||
return r.roomTypes.find((roomType) => roomType.code === roomTypeCode)
|
||||
})
|
||||
if (!room) {
|
||||
return null
|
||||
}
|
||||
const bedType = room.roomTypes.find(
|
||||
(roomType) => roomType.code === roomTypeCode
|
||||
)
|
||||
if (!bedType) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...room,
|
||||
bedType,
|
||||
}
|
||||
}
|
||||
|
||||
export function createMyStayStore({
|
||||
breakfastPackages,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { EventAttributes } from "ics"
|
||||
|
||||
import type { RouterOutput } from "@/lib/trpc/client"
|
||||
|
||||
export interface AddToCalendarProps {
|
||||
checkInDate: Date
|
||||
checkInDate: NonNullable<
|
||||
RouterOutput["booking"]["get"]
|
||||
>["booking"]["checkInDate"]
|
||||
event: EventAttributes
|
||||
hotelName: string
|
||||
hotelName: NonNullable<RouterOutput["booking"]["get"]>["hotel"]["name"]
|
||||
renderButton: (onPress: () => Promise<void>) => React.ReactNode
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface ManageBookingProps {
|
||||
refId: string
|
||||
bookingUrl: string
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import type { Room } from "@/types/hotel"
|
||||
import type {
|
||||
BookingConfirmation,
|
||||
BookingSchema,
|
||||
BookingConfirmationSchema,
|
||||
} from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface BookingConfirmationProps {
|
||||
confirmationNumber: string
|
||||
}
|
||||
|
||||
export interface BookingConfirmationRoom extends Room {
|
||||
bedType: Room["roomTypes"][number]
|
||||
}
|
||||
|
||||
export interface ConfirmationProps
|
||||
extends Pick<BookingConfirmation, "booking" | "hotelData"> {
|
||||
extends Pick<BookingConfirmation, "booking" | "hotel"> {
|
||||
room: BookingConfirmationRoom
|
||||
refId: string
|
||||
}
|
||||
|
||||
export interface BookingConfirmationAlertsProps {
|
||||
booking: BookingSchema
|
||||
booking: BookingConfirmationSchema
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { MutableRefObject } from "react"
|
||||
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface BookingConfirmationHeaderProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: BookingConfirmation["hotelData"]["hotel"]
|
||||
export interface BookingConfirmationHeaderProps
|
||||
extends Pick<BookingConfirmation, "booking" | "hotel"> {
|
||||
mainRef: MutableRefObject<HTMLElement | null>
|
||||
refId: string
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface BookingConfirmationHotelDetailsProps {
|
||||
hotel: BookingConfirmation["hotelData"]["hotel"]
|
||||
hotel: BookingConfirmation["hotel"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export interface PromosProps {
|
||||
hotelId: string
|
||||
refId: string
|
||||
}
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface PromosProps
|
||||
extends Pick<
|
||||
BookingConfirmation["booking"],
|
||||
"confirmationNumber" | "hotelId"
|
||||
>,
|
||||
Pick<BookingConfirmation["booking"]["guest"], "lastName"> {}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { z } from "zod"
|
||||
|
||||
import type { Room } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { linkedReservationSchema } from "@/server/routers/booking/output"
|
||||
|
||||
export interface LinkedReservationSchema
|
||||
extends z.output<typeof linkedReservationSchema> {}
|
||||
|
||||
export interface BookingConfirmationRoomsProps
|
||||
extends Pick<BookingConfirmation, "booking"> {
|
||||
@@ -8,4 +14,5 @@ export interface BookingConfirmationRoomsProps
|
||||
}
|
||||
checkInTime: string
|
||||
checkOutTime: string
|
||||
linkedReservations: LinkedReservationSchema[]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface LinkedReservationProps {
|
||||
checkInTime: string
|
||||
checkOutTime: string
|
||||
refId: string
|
||||
confirmationNumber: string
|
||||
roomIndex: number
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface RoomProps {
|
||||
checkInDate: BookingConfirmation["booking"]["checkInDate"]
|
||||
checkOutDate: BookingConfirmation["booking"]["checkOutDate"]
|
||||
booking: BookingConfirmation["booking"]
|
||||
checkInTime: string
|
||||
checkOutTime: string
|
||||
confirmationNumber: string
|
||||
guest: BookingConfirmation["booking"]["guest"]
|
||||
guaranteeInfo: BookingConfirmation["booking"]["guaranteeInfo"]
|
||||
img: NonNullable<BookingConfirmation["room"]>["images"][number]
|
||||
rateDefinition: BookingConfirmation["booking"]["rateDefinition"]
|
||||
roomName: NonNullable<BookingConfirmation["room"]>["name"]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export interface AncillariesProps extends Pick<BookingConfirmation, "booking"> {
|
||||
export interface AddedAncillariesProps {
|
||||
ancillaries: Ancillary["ancillaryContent"][number][] | null
|
||||
booking: BookingConfirmation["booking"]
|
||||
refId: string
|
||||
}
|
||||
|
||||
export interface AncillaryProps {
|
||||
|
||||
@@ -30,7 +30,6 @@ export type Room = Pick<
|
||||
| "linkedReservations"
|
||||
| "multiRoom"
|
||||
| "rateDefinition"
|
||||
| "refId"
|
||||
| "reservationStatus"
|
||||
| "roomPoints"
|
||||
| "roomTypeCode"
|
||||
|
||||
@@ -2,20 +2,20 @@ import type { z } from "zod"
|
||||
|
||||
import type { HotelData, Room } from "@/types/hotel"
|
||||
import type {
|
||||
bookingSchema,
|
||||
bookingConfirmationSchema,
|
||||
packageSchema,
|
||||
} from "@/server/routers/booking/output"
|
||||
|
||||
export interface BookingSchema extends z.output<typeof bookingSchema> {}
|
||||
export interface BookingConfirmationSchema
|
||||
extends z.output<typeof bookingConfirmationSchema> {}
|
||||
|
||||
export interface PackageSchema extends z.output<typeof packageSchema> {}
|
||||
|
||||
export interface BookingConfirmationRoom extends Room {
|
||||
bedType: Room["roomTypes"][number]
|
||||
}
|
||||
|
||||
export interface BookingConfirmation {
|
||||
booking: BookingSchema
|
||||
hotelData: HotelData
|
||||
room: BookingConfirmationRoom
|
||||
export interface BookingConfirmation extends HotelData {
|
||||
booking: BookingConfirmationSchema
|
||||
room:
|
||||
| (Room & {
|
||||
bedType: Room["roomTypes"][number]
|
||||
})
|
||||
| null
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import "server-only"
|
||||
|
||||
import { decrypt, encrypt } from "./encryption"
|
||||
|
||||
export function calculateRefId(confirmationNumber: string, lastName: string) {
|
||||
const encryptedRefId = encrypt(`${confirmationNumber},${lastName}`)
|
||||
|
||||
return encryptedRefId
|
||||
}
|
||||
|
||||
export function parseRefId(refId: string) {
|
||||
const data = decrypt(refId)
|
||||
const parts = data.split(",")
|
||||
if (parts.length !== 2) {
|
||||
throw new Error("Invalid refId format")
|
||||
}
|
||||
return {
|
||||
confirmationNumber: parts[0],
|
||||
lastName: parts[1],
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user