feat(SW-2116): Use refId instead of confirmationNumber

This commit is contained in:
Michael Zetterberg
2025-05-04 11:11:15 +02:00
parent f681fa7675
commit b910b6a313
59 changed files with 491 additions and 310 deletions

View File

@@ -1,3 +1,5 @@
import { notFound } from "next/navigation"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation" import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation"
@@ -6,9 +8,14 @@ import type { LangParams, PageArgs } from "@/types/params"
export default async function BookingConfirmationPage({ export default async function BookingConfirmationPage({
searchParams, searchParams,
}: PageArgs<LangParams, { confirmationNumber: string }>) { }: PageArgs<LangParams, { RefId?: string }>) {
void getBookingConfirmation(searchParams.confirmationNumber) const refId = searchParams.RefId
return (
<BookingConfirmation confirmationNumber={searchParams.confirmationNumber} /> if (!refId) {
) notFound()
}
void getBookingConfirmation(refId)
return <BookingConfirmation refId={refId} />
} }

View File

@@ -19,8 +19,8 @@ export default async function GuaranteePaymentCallbackPage({
}: PageArgs< }: PageArgs<
LangParams, LangParams,
{ {
status: PaymentCallbackStatusEnum status?: PaymentCallbackStatusEnum
RefId: string RefId?: string
confirmationNumber?: string confirmationNumber?: string
ancillary?: string ancillary?: string
} }
@@ -30,7 +30,7 @@ export default async function GuaranteePaymentCallbackPage({
const status = searchParams.status const status = searchParams.status
const confirmationNumber = searchParams.confirmationNumber const confirmationNumber = searchParams.confirmationNumber
const refId = searchParams.RefId const refId = searchParams.RefId
if (!refId) { if (!status || !confirmationNumber || !refId) {
notFound() notFound()
} }
const isAncillaryFlow = searchParams.ancillary const isAncillaryFlow = searchParams.ancillary
@@ -43,6 +43,7 @@ export default async function GuaranteePaymentCallbackPage({
return ( return (
<GuaranteeCallback <GuaranteeCallback
returnUrl={myStayUrl} returnUrl={myStayUrl}
refId={refId}
confirmationNumber={confirmationNumber} confirmationNumber={confirmationNumber}
lang={lang} lang={lang}
/> />
@@ -54,10 +55,10 @@ export default async function GuaranteePaymentCallbackPage({
let errorMessage = undefined let errorMessage = undefined
if (confirmationNumber) { if (refId) {
try { try {
const bookingStatus = await serverClient().booking.status({ const bookingStatus = await serverClient().booking.status({
confirmationNumber, refId,
}) })
const error = bookingStatus.errors.find((e) => e.errorCode) const error = bookingStatus.errors.find((e) => e.errorCode)

View File

@@ -1,5 +1,6 @@
import { notFound } from "next/navigation"
import { import {
BOOKING_CONFIRMATION_NUMBER,
BookingErrorCodeEnum, BookingErrorCodeEnum,
PaymentCallbackStatusEnum, PaymentCallbackStatusEnum,
} from "@/constants/booking" } from "@/constants/booking"
@@ -8,7 +9,10 @@ import {
details, details,
} from "@/constants/routes/hotelReservation" } from "@/constants/routes/hotelReservation"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import { getBooking } from "@/server/routers/booking/utils"
import { getServiceToken } from "@/server/tokenManager"
import { auth } from "@/auth"
import HandleErrorCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleErrorCallback" import HandleErrorCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleErrorCallback"
import HandleSuccessCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback" import HandleSuccessCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback"
@@ -20,7 +24,7 @@ export default async function PaymentCallbackPage({
}: PageArgs< }: PageArgs<
LangParams, LangParams,
{ {
status: PaymentCallbackStatusEnum status?: PaymentCallbackStatusEnum
confirmationNumber?: string confirmationNumber?: string
hotel?: string hotel?: string
} }
@@ -30,15 +34,42 @@ export default async function PaymentCallbackPage({
const status = searchParams.status const status = searchParams.status
const confirmationNumber = searchParams.confirmationNumber const confirmationNumber = searchParams.confirmationNumber
if (!status || !confirmationNumber) {
notFound()
}
let token = ""
const session = await auth()
if (session) {
token = session.token.access_token
} else {
const serviceToken = await getServiceToken()
if (serviceToken) {
token = serviceToken.access_token
}
}
if (!token) {
notFound()
}
const booking = await getBooking(confirmationNumber, params.lang, token)
if (!booking) {
notFound()
}
const { refId } = booking
if (status === PaymentCallbackStatusEnum.Success && confirmationNumber) { if (status === PaymentCallbackStatusEnum.Success && confirmationNumber) {
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${confirmationNumber}` const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(refId)}`
console.log( console.log(
`[payment-callback] rendering success callback with confirmation number: ${confirmationNumber}` `[payment-callback] rendering success callback with confirmation number: ${confirmationNumber}`
) )
return ( return (
<HandleSuccessCallback <HandleSuccessCallback
confirmationNumber={confirmationNumber} refId={refId}
successRedirectUrl={confirmationUrl} successRedirectUrl={confirmationUrl}
/> />
) )
@@ -49,10 +80,10 @@ export default async function PaymentCallbackPage({
let errorMessage = undefined let errorMessage = undefined
if (confirmationNumber) { if (refId) {
try { try {
const bookingStatus = await serverClient().booking.status({ const bookingStatus = await serverClient().booking.status({
confirmationNumber, refId,
}) })
// TODO: how to handle errors for multiple rooms? // TODO: how to handle errors for multiple rooms?

View File

@@ -14,7 +14,6 @@ import {
getProfileSafely, getProfileSafely,
getSavedPaymentCardsSafely, getSavedPaymentCardsSafely,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
import { decrypt } from "@/server/routers/utils/encryption"
import { auth } from "@/auth" import { auth } from "@/auth"
import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm" import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
@@ -35,6 +34,7 @@ import Image from "@/components/Image"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import MyStayProvider from "@/providers/MyStay" import MyStayProvider from "@/providers/MyStay"
import { parseRefId } from "@/utils/refId"
import { isValidSession } from "@/utils/session" import { isValidSession } from "@/utils/session"
import { getCurrentWebUrl } from "@/utils/url" import { getCurrentWebUrl } from "@/utils/url"
@@ -54,19 +54,18 @@ export default async function MyStay({
notFound() notFound()
} }
const value = decrypt(refId) const { confirmationNumber, lastName } = parseRefId(refId)
if (!value) { if (!confirmationNumber) {
return notFound() return notFound()
} }
const session = await auth() const session = await auth()
const isLoggedIn = isValidSession(session) const isLoggedIn = isValidSession(session)
const [confirmationNumber, lastName] = value.split(",")
const bv = cookies().get("bv")?.value const bv = cookies().get("bv")?.value
let bookingConfirmation let bookingConfirmation
if (isLoggedIn) { if (isLoggedIn) {
bookingConfirmation = await getBookingConfirmation(confirmationNumber) bookingConfirmation = await getBookingConfirmation(refId)
} else if (bv) { } else if (bv) {
const params = new URLSearchParams(bv) const params = new URLSearchParams(bv)
const firstName = params.get("firstName") const firstName = params.get("firstName")
@@ -113,9 +112,7 @@ export default async function MyStay({
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD") const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD") const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
const linkedReservationsPromise = getLinkedReservations({ const linkedReservationsPromise = getLinkedReservations(booking.refId)
rooms: booking.linkedReservations,
})
const ancillariesInput = { const ancillariesInput = {
fromDate, fromDate,
@@ -187,7 +184,7 @@ export default async function MyStay({
breakfastPackages={breakfastPackages} breakfastPackages={breakfastPackages}
lang={params.lang} lang={params.lang}
linkedReservationsPromise={linkedReservationsPromise} linkedReservationsPromise={linkedReservationsPromise}
refId={refId} refId={booking.refId}
roomCategories={roomCategories} roomCategories={roomCategories}
savedCreditCards={savedCreditCards} savedCreditCards={savedCreditCards}
> >
@@ -215,7 +212,6 @@ export default async function MyStay({
packages={breakfastPackages} packages={breakfastPackages}
user={user} user={user}
savedCreditCards={savedCreditCards} savedCreditCards={savedCreditCards}
refId={refId}
/> />
)} )}

View File

@@ -14,7 +14,6 @@ import {
getProfileSafely, getProfileSafely,
getSavedPaymentCardsSafely, getSavedPaymentCardsSafely,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
import { decrypt } from "@/server/routers/utils/encryption"
import { auth } from "@/auth" import { auth } from "@/auth"
import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm" import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
@@ -35,6 +34,7 @@ import Image from "@/components/Image"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import MyStayProvider from "@/providers/MyStay" import MyStayProvider from "@/providers/MyStay"
import { parseRefId } from "@/utils/refId"
import { isValidSession } from "@/utils/session" import { isValidSession } from "@/utils/session"
import { getCurrentWebUrl } from "@/utils/url" import { getCurrentWebUrl } from "@/utils/url"
@@ -54,18 +54,19 @@ export default async function MyStay({
notFound() notFound()
} }
const value = decrypt(refId) const { confirmationNumber, lastName } = parseRefId(refId)
if (!value) {
if (!confirmationNumber) {
return notFound() return notFound()
} }
const session = await auth() const session = await auth()
const isLoggedIn = isValidSession(session) const isLoggedIn = isValidSession(session)
const [confirmationNumber, lastName] = value.split(",")
const bv = cookies().get("bv")?.value const bv = cookies().get("bv")?.value
let bookingConfirmation let bookingConfirmation
if (isLoggedIn) { if (isLoggedIn) {
bookingConfirmation = await getBookingConfirmation(confirmationNumber) bookingConfirmation = await getBookingConfirmation(refId)
} else if (bv) { } else if (bv) {
const params = new URLSearchParams(bv) const params = new URLSearchParams(bv)
const firstName = params.get("firstName") const firstName = params.get("firstName")
@@ -110,9 +111,7 @@ export default async function MyStay({
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD") const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD") const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
const linkedReservationsPromise = getLinkedReservations({ const linkedReservationsPromise = getLinkedReservations(booking.refId)
rooms: booking.linkedReservations,
})
const ancillariesInput = { const ancillariesInput = {
fromDate, fromDate,
@@ -184,7 +183,7 @@ export default async function MyStay({
breakfastPackages={breakfastPackages} breakfastPackages={breakfastPackages}
lang={params.lang} lang={params.lang}
linkedReservationsPromise={linkedReservationsPromise} linkedReservationsPromise={linkedReservationsPromise}
refId={refId} refId={booking.refId}
roomCategories={roomCategories} roomCategories={roomCategories}
savedCreditCards={savedCreditCards} savedCreditCards={savedCreditCards}
> >
@@ -212,7 +211,6 @@ export default async function MyStay({
packages={breakfastPackages} packages={breakfastPackages}
user={user} user={user}
savedCreditCards={savedCreditCards} savedCreditCards={savedCreditCards}
refId={refId}
/> />
)} )}

View File

@@ -52,7 +52,7 @@ export default function Header({
url: hotel.contactInformation.websiteUrl, url: hotel.contactInformation.websiteUrl,
} }
const bookingUrlPath = `${myStay[lang]}?RefId=${refId}` const bookingUrlPath = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}`
return ( return (
<header className={styles.header}> <header className={styles.header}>

View File

@@ -20,13 +20,13 @@ import { CurrencyEnum } from "@/types/enums/currency"
export function LinkedReservation({ export function LinkedReservation({
checkInTime, checkInTime,
checkOutTime, checkOutTime,
confirmationNumber, refId,
roomIndex, roomIndex,
roomNumber, roomNumber,
}: LinkedReservationProps) { }: LinkedReservationProps) {
const lang = useLang() const lang = useLang()
const { data, refetch, isLoading } = trpc.booking.get.useQuery({ const { data, refetch, isLoading } = trpc.booking.get.useQuery({
confirmationNumber, refId,
lang, lang,
}) })
const { const {

View File

@@ -8,12 +8,12 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { BookingStatusEnum } from "@/constants/booking" import { BookingStatusEnum } from "@/constants/booking"
import { trpc } from "@/lib/trpc/client" import { trpc } from "@/lib/trpc/client"
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation" import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import { convertToChildType } from "@/components/HotelReservation/utils/convertToChildType" import { convertToChildType } from "@/components/HotelReservation/utils/convertToChildType"
import { getPriceType } from "@/components/HotelReservation/utils/getPriceType" import { getPriceType } from "@/components/HotelReservation/utils/getPriceType"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek" import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import { getBookedHotelRoom } from "@/utils/booking"
import styles from "./sidePeek.module.css" import styles from "./sidePeek.module.css"

View File

@@ -14,14 +14,13 @@ export default async function Rooms({
checkInTime, checkInTime,
checkOutTime, checkOutTime,
mainRoom, mainRoom,
linkedReservations,
}: BookingConfirmationRoomsProps) { }: BookingConfirmationRoomsProps) {
const intl = await getIntl() const intl = await getIntl()
return ( return (
<section className={styles.rooms}> <section className={styles.rooms}>
<div className={styles.room}> <div className={styles.room}>
{linkedReservations.length ? ( {booking.linkedReservations.length ? (
<Typography variant="Title/Subtitle/md"> <Typography variant="Title/Subtitle/md">
<h2 className={styles.roomTitle}> <h2 className={styles.roomTitle}>
{intl.formatMessage( {intl.formatMessage(
@@ -42,7 +41,7 @@ export default async function Rooms({
/> />
</div> </div>
{linkedReservations.map((reservation, idx) => ( {booking.linkedReservations.map((reservation, idx) => (
<div className={styles.room} key={reservation.confirmationNumber}> <div className={styles.room} key={reservation.confirmationNumber}>
<Typography variant="Title/Subtitle/md"> <Typography variant="Title/Subtitle/md">
<h2 className={styles.roomTitle}> <h2 className={styles.roomTitle}>
@@ -57,7 +56,7 @@ export default async function Rooms({
<LinkedReservation <LinkedReservation
checkInTime={checkInTime} checkInTime={checkInTime}
checkOutTime={checkOutTime} checkOutTime={checkOutTime}
confirmationNumber={reservation.confirmationNumber} refId={reservation.refId}
roomIndex={idx + 1} roomIndex={idx + 1}
roomNumber={idx + 2} roomNumber={idx + 2}
/> />

View File

@@ -1,7 +1,6 @@
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import { encrypt } from "@/server/routers/utils/encryption"
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails" import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails" import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails"
@@ -23,22 +22,20 @@ import styles from "./bookingConfirmation.module.css"
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
export default async function BookingConfirmation({ export default async function BookingConfirmation({
confirmationNumber, refId,
}: BookingConfirmationProps) { }: BookingConfirmationProps) {
const bookingConfirmation = await getBookingConfirmation(confirmationNumber) const bookingConfirmation = await getBookingConfirmation(refId)
if (!bookingConfirmation) { if (!bookingConfirmation) {
return notFound() return notFound()
} }
const { booking, hotel, room, roomCategories } = bookingConfirmation const { booking, hotel, room, roomCategories } = bookingConfirmation
if (!room) { if (!room) {
return notFound() return notFound()
} }
const refId = encrypt(
`${booking.confirmationNumber},${booking.guest.lastName}`
)
const intl = await getIntl() const intl = await getIntl()
return ( return (
<BookingConfirmationProvider <BookingConfirmationProvider
@@ -62,7 +59,6 @@ export default async function BookingConfirmation({
checkInTime={hotel.hotelFacts.checkin.checkInTime} checkInTime={hotel.hotelFacts.checkin.checkInTime}
checkOutTime={hotel.hotelFacts.checkin.checkOutTime} checkOutTime={hotel.hotelFacts.checkin.checkOutTime}
mainRoom={room} mainRoom={room}
linkedReservations={booking.linkedReservations}
/> />
<PaymentDetails /> <PaymentDetails />
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />

View File

@@ -64,6 +64,7 @@ export function mapRoomState(
name: room.name, name: room.name,
packages: booking.packages, packages: booking.packages,
rateDefinition: booking.rateDefinition, rateDefinition: booking.rateDefinition,
refId: booking.refId,
roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"), roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"),
roomPoints: booking.roomPoints, roomPoints: booking.roomPoints,
roomPrice: booking.roomPrice, roomPrice: booking.roomPrice,

View File

@@ -18,12 +18,12 @@ const validBookingStatuses = [
] ]
interface HandleStatusPollingProps { interface HandleStatusPollingProps {
confirmationNumber: string refId: string
successRedirectUrl: string successRedirectUrl: string
} }
export default function HandleSuccessCallback({ export default function HandleSuccessCallback({
confirmationNumber, refId,
successRedirectUrl, successRedirectUrl,
}: HandleStatusPollingProps) { }: HandleStatusPollingProps) {
const router = useRouter() const router = useRouter()
@@ -33,7 +33,7 @@ export default function HandleSuccessCallback({
error, error,
isTimeout, isTimeout,
} = useHandleBookingStatus({ } = useHandleBookingStatus({
confirmationNumber, refId,
expectedStatuses: validBookingStatuses, expectedStatuses: validBookingStatuses,
maxRetries: 10, maxRetries: 10,
retryInterval: 2000, retryInterval: 2000,

View File

@@ -12,7 +12,6 @@ import { Button } from "@scandic-hotels/design-system/Button"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { import {
BOOKING_CONFIRMATION_NUMBER,
BookingStatusEnum, BookingStatusEnum,
PAYMENT_METHOD_TITLES, PAYMENT_METHOD_TITLES,
PaymentMethodEnum, PaymentMethodEnum,
@@ -111,7 +110,7 @@ export default function PaymentClient({
(state) => state.actions.setIsSubmittingDisabled (state) => state.actions.setIsSubmittingDisabled
) )
const [bookingNumber, setBookingNumber] = useState<string>("") const [refId, setRefId] = useState("")
const [isPollingForBookingStatus, setIsPollingForBookingStatus] = const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
useState(false) useState(false)
@@ -156,13 +155,15 @@ export default function PaymentClient({
return return
} }
const mainRoom = result.rooms[0]
if (result.reservationStatus == BookingStatusEnum.BookingCompleted) { if (result.reservationStatus == BookingStatusEnum.BookingCompleted) {
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${result.id}` const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
router.push(confirmationUrl) router.push(confirmationUrl)
return return
} }
setBookingNumber(result.id) setRefId(mainRoom.refId)
const hasPriceChange = result.rooms.some((r) => r.priceChangedMetadata) const hasPriceChange = result.rooms.some((r) => r.priceChangedMetadata)
if (hasPriceChange) { if (hasPriceChange) {
@@ -200,7 +201,7 @@ export default function PaymentClient({
}) })
const bookingStatus = useHandleBookingStatus({ const bookingStatus = useHandleBookingStatus({
confirmationNumber: bookingNumber, refId,
expectedStatuses: [BookingStatusEnum.BookingCompleted], expectedStatuses: [BookingStatusEnum.BookingCompleted],
maxRetries, maxRetries,
retryInterval, retryInterval,
@@ -263,7 +264,8 @@ export default function PaymentClient({
bookingStatus?.data?.reservationStatus === bookingStatus?.data?.reservationStatus ===
BookingStatusEnum.BookingCompleted BookingStatusEnum.BookingCompleted
) { ) {
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${bookingStatus?.data?.id}` const mainRoom = bookingStatus.data.rooms[0]
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
router.push(confirmationUrl) router.push(confirmationUrl)
} else if (bookingStatus.isTimeout) { } else if (bookingStatus.isTimeout) {
handlePaymentError("Timeout") handlePaymentError("Timeout")
@@ -633,9 +635,7 @@ export default function PaymentClient({
: "" : ""
router.push(`${selectRate(lang)}${allSearchParams}`) router.push(`${selectRate(lang)}${allSearchParams}`)
}} }}
onAccept={() => onAccept={() => priceChange.mutate({ refId })}
priceChange.mutate({ confirmationNumber: bookingNumber })
}
/> />
) : null} ) : null}
</section> </section>

View File

@@ -44,7 +44,7 @@ export default function FindMyBooking() {
const values = form.getValues() const values = form.getValues()
const value = new URLSearchParams(values).toString() const value = new URLSearchParams(values).toString()
document.cookie = `bv=${encodeURIComponent(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict` document.cookie = `bv=${encodeURIComponent(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict`
router.push(`${myStay[lang]}?RefId=${result.refId}`) router.push(`${myStay[lang]}?RefId=${encodeURIComponent(result.refId)}`)
}, },
onError: (error) => { onError: (error) => {
console.error("Failed to create ref id", error) console.error("Failed to create ref id", error)

View File

@@ -57,7 +57,6 @@ export default function AddAncillaryFlowModal({
packages, packages,
user, user,
savedCreditCards, savedCreditCards,
refId,
}: AddAncillaryFlowModalProps) { }: AddAncillaryFlowModalProps) {
const { const {
currentStep, currentStep,
@@ -123,7 +122,7 @@ export default function AddAncillaryFlowModal({
const addAncillary = trpc.booking.packages.useMutation() const addAncillary = trpc.booking.packages.useMutation()
const { guaranteeBooking, isLoading, handleGuaranteeError } = const { guaranteeBooking, isLoading, handleGuaranteeError } =
useGuaranteeBooking(booking.confirmationNumber, true, booking.hotelId) useGuaranteeBooking(booking.refId, true, booking.hotelId)
function validateTermsAndConditions(data: AncillaryFormData): boolean { function validateTermsAndConditions(data: AncillaryFormData): boolean {
if (!data.termsAndConditions) { if (!data.termsAndConditions) {
@@ -145,7 +144,7 @@ export default function AddAncillaryFlowModal({
) { ) {
addAncillary.mutate( addAncillary.mutate(
{ {
confirmationNumber: booking.confirmationNumber, refId: booking.refId,
ancillaryComment: data.optionalText, ancillaryComment: data.optionalText,
ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime
? data.deliveryTime ? data.deliveryTime
@@ -176,7 +175,7 @@ export default function AddAncillaryFlowModal({
clearAncillarySessionData() clearAncillarySessionData()
closeModal() closeModal()
utils.booking.get.invalidate({ utils.booking.get.invalidate({
confirmationNumber: booking.confirmationNumber, refId: booking.refId,
}) })
router.refresh() router.refresh()
} else { } else {
@@ -202,7 +201,7 @@ export default function AddAncillaryFlowModal({
selectedAncillary, selectedAncillary,
data.deliveryTime data.deliveryTime
) )
if (booking.confirmationNumber) { if (booking.refId) {
const card = savedCreditCard const card = savedCreditCard
? { ? {
alias: savedCreditCard.alias, alias: savedCreditCard.alias,
@@ -211,12 +210,12 @@ export default function AddAncillaryFlowModal({
} }
: undefined : undefined
guaranteeBooking.mutate({ guaranteeBooking.mutate({
confirmationNumber: booking.confirmationNumber, refId: booking.refId,
language: lang, language: lang,
...(card && { card }), ...(card && { card }),
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}&ancillary=1`, success: `${guaranteeRedirectUrl}?status=success&ancillary=1&RefId=${encodeURIComponent(booking.refId)}`,
error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}&ancillary=1`, error: `${guaranteeRedirectUrl}?status=error&ancillary=1&RefId=${encodeURIComponent(booking.refId)}`,
cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}&ancillary=1`, cancel: `${guaranteeRedirectUrl}?status=cancel&ancillary=1&RefId=${encodeURIComponent(booking.refId)}`,
}) })
} else { } else {
handleGuaranteeError("No confirmation number") handleGuaranteeError("No confirmation number")

View File

@@ -10,12 +10,12 @@ import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
export default function RemoveButton({ export default function RemoveButton({
confirmationNumber, refId,
codes, codes,
title, title,
onSuccess, onSuccess,
}: { }: {
confirmationNumber: string refId: string
codes: string[] codes: string[]
title?: string title?: string
onSuccess: () => void onSuccess: () => void
@@ -51,7 +51,7 @@ export default function RemoveButton({
removePackage.mutate( removePackage.mutate(
{ {
language: lang, language: lang,
confirmationNumber, refId,
codes, codes,
}, },
{ {

View File

@@ -126,7 +126,7 @@ export function AddedAncillaries({
{booking.confirmationNumber && ancillary.code ? ( {booking.confirmationNumber && ancillary.code ? (
<div className={styles.actions}> <div className={styles.actions}>
<RemoveButton <RemoveButton
confirmationNumber={booking.confirmationNumber} refId={booking.refId}
codes={ codes={
ancillary.code === ancillary.code ===
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
@@ -192,7 +192,7 @@ export function AddedAncillaries({
booking.canModifyAncillaries ? ( booking.canModifyAncillaries ? (
<div className={styles.actions}> <div className={styles.actions}>
<RemoveButton <RemoveButton
confirmationNumber={booking.confirmationNumber} refId={booking.refId}
codes={ codes={
ancillary.code === ancillary.code ===
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST

View File

@@ -21,9 +21,11 @@ import type { Lang } from "@/constants/languages"
export default function GuaranteeAncillaryHandler({ export default function GuaranteeAncillaryHandler({
confirmationNumber, confirmationNumber,
returnUrl, returnUrl,
refId,
lang, lang,
}: { }: {
confirmationNumber: string confirmationNumber: string
refId: string
returnUrl: string returnUrl: string
lang: Lang lang: Lang
}) { }) {
@@ -47,7 +49,7 @@ export default function GuaranteeAncillaryHandler({
addAncillary.mutate( addAncillary.mutate(
{ {
confirmationNumber, refId,
ancillaryComment: formData.optionalText, ancillaryComment: formData.optionalText,
ancillaryDeliveryTime: selectedAncillary.requiresDeliveryTime ancillaryDeliveryTime: selectedAncillary.requiresDeliveryTime
? formData.deliveryTime ? formData.deliveryTime
@@ -86,7 +88,7 @@ export default function GuaranteeAncillaryHandler({
}, },
} }
) )
}, [confirmationNumber, returnUrl, addAncillary, lang, router]) }, [confirmationNumber, refId, returnUrl, addAncillary, lang, router])
return <LoadingSpinner /> return <LoadingSpinner />
} }

View File

@@ -83,7 +83,6 @@ export function Ancillaries({
packages, packages,
user, user,
savedCreditCards, savedCreditCards,
refId,
}: AncillariesProps) { }: AncillariesProps) {
const intl = useIntl() const intl = useIntl()
const ancillaries = use(ancillariesPromise) const ancillaries = use(ancillariesPromise)
@@ -221,7 +220,6 @@ export function Ancillaries({
user={user} user={user}
booking={booking} booking={booking}
packages={packages} packages={packages}
refId={refId}
savedCreditCards={savedCreditCards} savedCreditCards={savedCreditCards}
/> />
</AncillaryFlowModalWrapper> </AncillaryFlowModalWrapper>

View File

@@ -31,14 +31,14 @@ import type { SafeUser } from "@/types/user"
import type { Guest } from "@/server/routers/booking/output" import type { Guest } from "@/server/routers/booking/output"
interface GuestDetailsProps { interface GuestDetailsProps {
confirmationNumber: string refId: string
guest: Guest guest: Guest
isCancelled: boolean isCancelled: boolean
user: SafeUser user: SafeUser
} }
export default function GuestDetails({ export default function GuestDetails({
confirmationNumber, refId,
guest, guest,
isCancelled, isCancelled,
user, user,
@@ -74,7 +74,7 @@ export default function GuestDetails({
onSuccess: (data) => { onSuccess: (data) => {
if (data) { if (data) {
utils.booking.get.invalidate({ utils.booking.get.invalidate({
confirmationNumber: data.confirmationNumber, refId: data.refId,
}) })
toast.success( toast.success(
@@ -106,7 +106,7 @@ export default function GuestDetails({
async function onSubmit(data: ModifyContactSchema) { async function onSubmit(data: ModifyContactSchema) {
updateGuest.mutate({ updateGuest.mutate({
confirmationNumber, refId,
guest: { guest: {
email: data.email, email: data.email,
phoneNumber: data.phoneNumber, phoneNumber: data.phoneNumber,

View File

@@ -3,8 +3,6 @@ import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import SkeletonShimmer from "@/components/SkeletonShimmer" import SkeletonShimmer from "@/components/SkeletonShimmer"
import { formatPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
@@ -14,13 +12,14 @@ export default function Points({
isCancelled, isCancelled,
points, points,
price, price,
currencyCode,
}: { }: {
isCancelled: boolean isCancelled: boolean
points: number points: number
price: number price: number
currencyCode: CurrencyEnum
}) { }) {
const intl = useIntl() const intl = useIntl()
const currency = useMyStayStore((state) => state.bookedRoom.currencyCode)
if (!points) { if (!points) {
return <SkeletonShimmer width="100px" /> return <SkeletonShimmer width="100px" />
@@ -31,7 +30,7 @@ export default function Points({
points, points,
CurrencyEnum.POINTS, CurrencyEnum.POINTS,
price, price,
currency currencyCode
) )
return ( return (

View File

@@ -11,7 +11,12 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat
interface PriceTypeProps interface PriceTypeProps
extends Pick< extends Pick<
BookingConfirmation["booking"], BookingConfirmation["booking"],
"cheques" | "rateDefinition" | "roomPoints" | "totalPrice" | "vouchers" | "cheques"
| "currencyCode"
| "rateDefinition"
| "roomPoints"
| "totalPrice"
| "vouchers"
> { > {
formattedTotalPrice: string formattedTotalPrice: string
isCancelled: boolean isCancelled: boolean
@@ -22,6 +27,7 @@ export default function PriceType({
cheques, cheques,
formattedTotalPrice, formattedTotalPrice,
isCancelled, isCancelled,
currencyCode,
priceType, priceType,
rateDefinition, rateDefinition,
roomPoints, roomPoints,
@@ -51,6 +57,7 @@ export default function PriceType({
isCancelled={isCancelled} isCancelled={isCancelled}
points={roomPoints} points={roomPoints}
price={totalPrice} price={totalPrice}
currencyCode={currencyCode}
/> />
) )
case PriceTypeEnum.voucher: case PriceTypeEnum.voucher:

View File

@@ -11,10 +11,10 @@ import {
getBookingConfirmation, getBookingConfirmation,
getProfileSafely, getProfileSafely,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
import { decrypt } from "@/server/routers/utils/encryption"
import { auth } from "@/auth" import { auth } from "@/auth"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { parseRefId } from "@/utils/refId"
import { isValidSession } from "@/utils/session" import { isValidSession } from "@/utils/session"
import AdditionalInfoForm from "../../FindMyBooking/AdditionalInfoForm" import AdditionalInfoForm from "../../FindMyBooking/AdditionalInfoForm"
@@ -32,18 +32,19 @@ import styles from "./receipt.module.css"
import { CurrencyEnum } from "@/types/enums/currency" import { CurrencyEnum } from "@/types/enums/currency"
export async function Receipt({ refId }: { refId: string }) { export async function Receipt({ refId }: { refId: string }) {
const value = decrypt(refId) const { confirmationNumber, lastName } = parseRefId(refId)
if (!value) {
if (!confirmationNumber) {
return notFound() return notFound()
} }
const session = await auth() const session = await auth()
const isLoggedIn = isValidSession(session) const isLoggedIn = isValidSession(session)
const [confirmationNumber, lastName] = value.split(",")
const bv = cookies().get("bv")?.value const bv = cookies().get("bv")?.value
let bookingConfirmation let bookingConfirmation
if (isLoggedIn) { if (isLoggedIn) {
bookingConfirmation = await getBookingConfirmation(confirmationNumber) bookingConfirmation = await getBookingConfirmation(refId)
} else if (bv) { } else if (bv) {
const params = new URLSearchParams(bv) const params = new URLSearchParams(bv)
const firstName = params.get("firstName") const firstName = params.get("firstName")

View File

@@ -29,9 +29,7 @@ export default function CancelStayPriceContainer() {
const { totalAdults, totalChildren } = formRooms.reduce( const { totalAdults, totalChildren } = formRooms.reduce(
(total, formRoom) => { (total, formRoom) => {
if (formRoom.checked) { if (formRoom.checked) {
const room = rooms.find( const room = rooms.find((r) => r.refId === formRoom.refId)
(r) => r.confirmationNumber === formRoom.confirmationNumber
)
if (room) { if (room) {
total.totalAdults = total.totalAdults + room.adults total.totalAdults = total.totalAdults + room.adults
if (room.childrenInRoom.length) { if (room.childrenInRoom.length) {

View File

@@ -57,7 +57,7 @@ export default function FinalConfirmation({
) )
} else { } else {
const cancelledRooms = rooms.filter((r) => const cancelledRooms = rooms.filter((r) =>
variables.confirmationNumbers.includes(r.confirmationNumber) variables.refIds.includes(r.refId)
) )
for (const cancelledRoom of cancelledRooms) { for (const cancelledRoom of cancelledRooms) {
toast.success( toast.success(
@@ -94,11 +94,11 @@ export default function FinalConfirmation({
} }
utils.booking.get.invalidate({ utils.booking.get.invalidate({
confirmationNumber: bookedRoom.confirmationNumber, refId: bookedRoom.refId,
}) })
utils.booking.linkedReservations.invalidate({ utils.booking.linkedReservations.invalidate({
lang, lang,
rooms: bookedRoom.linkedReservations, refId: bookedRoom.refId,
}) })
closeModal() closeModal()
}, },
@@ -113,12 +113,12 @@ export default function FinalConfirmation({
function cancelBooking() { function cancelBooking() {
if (Array.isArray(formRooms)) { if (Array.isArray(formRooms)) {
const confirmationNumbersToCancel = formRooms const refIdsToCancel = formRooms
.filter((r) => r.checked) .filter((r) => r.checked)
.map((r) => r.confirmationNumber) .map((r) => r.refId)
if (confirmationNumbersToCancel.length) { if (refIdsToCancel.length) {
cancelBookingsMutation.mutate({ cancelBookingsMutation.mutate({
confirmationNumbers: confirmationNumbersToCancel, refIds: refIdsToCancel,
language: lang, language: lang,
}) })
} }

View File

@@ -29,7 +29,7 @@ export default function Steps({ closeModal }: StepsProps) {
rooms: rooms.map((room, idx) => ({ rooms: rooms.map((room, idx) => ({
// Single room booking // Single room booking
checked: rooms.length === 1, checked: rooms.length === 1,
confirmationNumber: room.confirmationNumber, refId: room.refId,
id: idx + 1, id: idx + 1,
})), })),
}, },

View File

@@ -57,7 +57,7 @@ export default function Confirmation({
onSuccess: (updatedBooking) => { onSuccess: (updatedBooking) => {
if (updatedBooking) { if (updatedBooking) {
utils.booking.get.invalidate({ utils.booking.get.invalidate({
confirmationNumber: updatedBooking.confirmationNumber, refId: updatedBooking.refId,
}) })
toast.success( toast.success(
@@ -86,7 +86,7 @@ export default function Confirmation({
function handleModifyStay() { function handleModifyStay() {
updateBooking.mutate({ updateBooking.mutate({
confirmationNumber: bookedRoom.confirmationNumber, refId: bookedRoom.refId,
checkInDate, checkInDate,
checkOutDate, checkOutDate,
}) })

View File

@@ -41,7 +41,7 @@ export default function Form() {
confirmationNumber: state.bookedRoom.confirmationNumber, confirmationNumber: state.bookedRoom.confirmationNumber,
currencyCode: state.bookedRoom.currencyCode, currencyCode: state.bookedRoom.currencyCode,
hotelId: state.bookedRoom.hotelId, hotelId: state.bookedRoom.hotelId,
refId: state.refId, refId: state.bookedRoom.refId,
savedCreditCards: state.savedCreditCards, savedCreditCards: state.savedCreditCards,
})) }))
@@ -85,7 +85,7 @@ export default function Form() {
: undefined : undefined
writeGlaToSessionStorage("yes", hotelId) writeGlaToSessionStorage("yes", hotelId)
guaranteeBooking.mutate({ guaranteeBooking.mutate({
confirmationNumber, refId,
language: lang, language: lang,
...(card && { card }), ...(card && { card }),
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`, success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`,

View File

@@ -276,6 +276,7 @@ export default function Room({ booking, roomNr, user }: RoomProps) {
cheques={cheques} cheques={cheques}
formattedTotalPrice={formattedTotalPrice} formattedTotalPrice={formattedTotalPrice}
isCancelled={isCancelled} isCancelled={isCancelled}
currencyCode={currencyCode}
priceType={priceType} priceType={priceType}
rateDefinition={rateDefinition} rateDefinition={rateDefinition}
roomPoints={roomPoints} roomPoints={roomPoints}

View File

@@ -16,6 +16,7 @@ export default function PriceDetails() {
cheques: state.bookedRoom.cheques, cheques: state.bookedRoom.cheques,
formattedTotalPrice: state.totalPrice, formattedTotalPrice: state.totalPrice,
isCancelled: state.bookedRoom.isCancelled, isCancelled: state.bookedRoom.isCancelled,
currencyCode: state.bookedRoom.currencyCode,
priceType: state.bookedRoom.priceType, priceType: state.bookedRoom.priceType,
rateDefinition: state.bookedRoom.rateDefinition, rateDefinition: state.bookedRoom.rateDefinition,
roomPoints: state.bookedRoom.roomPoints, roomPoints: state.bookedRoom.roomPoints,

View File

@@ -23,21 +23,15 @@ interface RoomProps {
} }
export default function SingleRoom({ user }: RoomProps) { export default function SingleRoom({ user }: RoomProps) {
const { const { refId, guest, isCancelled, isMultiRoom, roomName, roomNumber } =
confirmationNumber, useMyStayStore((state) => ({
guest, refId: state.bookedRoom.refId,
isCancelled, guest: state.bookedRoom.guest,
isMultiRoom, isCancelled: state.bookedRoom.isCancelled,
roomName, isMultiRoom: state.rooms.length > 1,
roomNumber, roomName: state.bookedRoom.roomName,
} = useMyStayStore((state) => ({ roomNumber: state.bookedRoom.roomNumber,
confirmationNumber: state.bookedRoom.confirmationNumber, }))
guest: state.bookedRoom.guest,
isCancelled: state.bookedRoom.isCancelled,
isMultiRoom: state.rooms.length > 1,
roomName: state.bookedRoom.roomName,
roomNumber: state.bookedRoom.roomNumber,
}))
if (isMultiRoom) { if (isMultiRoom) {
return null return null
@@ -70,7 +64,7 @@ export default function SingleRoom({ user }: RoomProps) {
<Details /> <Details />
<div className={styles.guestDetailsDesktopWrapper}> <div className={styles.guestDetailsDesktopWrapper}>
<GuestDetails <GuestDetails
confirmationNumber={confirmationNumber} refId={refId}
guest={guest} guest={guest}
isCancelled={isCancelled} isCancelled={isCancelled}
user={user} user={user}
@@ -83,7 +77,7 @@ export default function SingleRoom({ user }: RoomProps) {
<PriceDetails /> <PriceDetails />
<div className={styles.guestDetailsMobileWrapper}> <div className={styles.guestDetailsMobileWrapper}>
<GuestDetails <GuestDetails
confirmationNumber={confirmationNumber} refId={refId}
guest={guest} guest={guest}
isCancelled={isCancelled} isCancelled={isCancelled}
user={user} user={user}

View File

@@ -16,6 +16,7 @@ export default function TotalPrice() {
cheques={bookedRoom.cheques} cheques={bookedRoom.cheques}
formattedTotalPrice={formattedTotalPrice} formattedTotalPrice={formattedTotalPrice}
isCancelled={bookedRoom.isCancelled} isCancelled={bookedRoom.isCancelled}
currencyCode={bookedRoom.currencyCode}
priceType={bookedRoom.priceType} priceType={bookedRoom.priceType}
rateDefinition={bookedRoom.rateDefinition} rateDefinition={bookedRoom.rateDefinition}
roomPoints={bookedRoom.roomPoints} roomPoints={bookedRoom.roomPoints}

View File

@@ -14,18 +14,32 @@ import type { Guest } from "@/server/routers/booking/output"
describe("Access booking", () => { describe("Access booking", () => {
describe("for logged in booking", () => { describe("for logged in booking", () => {
it("should enable access if all is provided", () => { it("should enable access if all is provided", () => {
expect(accessBooking(loggedIn, "Booking", user)).toBe(ACCESS_GRANTED) expect(accessBooking(loggedInGuest, "Booking", authenticatedUser)).toBe(
ACCESS_GRANTED
)
}) })
it("should enable access if all is provided and be case-insensitive", () => { it("should enable access if all is provided and be case-insensitive", () => {
expect(accessBooking(loggedIn, "BoOkInG", user)).toBe(ACCESS_GRANTED) expect(accessBooking(loggedInGuest, "BoOkInG", authenticatedUser)).toBe(
ACCESS_GRANTED
)
}) })
it("should prompt to login", () => { it("should prompt to login without user", () => {
expect(accessBooking(loggedIn, "Booking", null)).toBe(ERROR_UNAUTHORIZED) expect(accessBooking(loggedInGuest, "Booking", null)).toBe(
ERROR_UNAUTHORIZED
)
}) })
it("should deny access", () => { it("should prompt to login if user mismatch", () => {
expect(accessBooking(loggedIn, "NotBooking", user)).toBe(ERROR_NOT_FOUND) expect(
accessBooking(loggedInGuest, "Booking", badAuthenticatedUser)
).toBe(ERROR_UNAUTHORIZED)
})
it("should deny access if refId mismatch", () => {
expect(
accessBooking(loggedInGuest, "NotBooking", authenticatedUser)
).toBe(ERROR_UNAUTHORIZED)
}) })
}) })
describe("for anonymous booking", () => { describe("for anonymous booking", () => {
it("should enable access if all is provided", () => { it("should enable access if all is provided", () => {
const cookieString = new URLSearchParams({ const cookieString = new URLSearchParams({
@@ -34,7 +48,7 @@ describe("Access booking", () => {
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }).toString()
expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe(
ACCESS_GRANTED ACCESS_GRANTED
) )
}) })
@@ -45,7 +59,7 @@ describe("Access booking", () => {
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }).toString()
expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe(
ACCESS_GRANTED ACCESS_GRANTED
) )
}) })
@@ -56,7 +70,7 @@ describe("Access booking", () => {
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }).toString()
expect(accessBooking(loggedOut, "BoOkInG", null, cookieString)).toBe( expect(accessBooking(loggedOutGuest, "BoOkInG", null, cookieString)).toBe(
ACCESS_GRANTED ACCESS_GRANTED
) )
}) })
@@ -67,7 +81,7 @@ describe("Access booking", () => {
lastName: "Booking", lastName: "Booking",
email: "LOGGED+out@scandichotels.com", email: "LOGGED+out@scandichotels.com",
}).toString() }).toString()
expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe(
ACCESS_GRANTED ACCESS_GRANTED
) )
}) })
@@ -78,9 +92,14 @@ describe("Access booking", () => {
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }).toString()
expect(accessBooking(loggedOut, "Booking", user, cookieString)).toBe( expect(
ERROR_FORBIDDEN accessBooking(
) loggedOutGuest,
"Booking",
authenticatedUser,
cookieString
)
).toBe(ERROR_FORBIDDEN)
}) })
it("should prompt for more if first name is missing", () => { it("should prompt for more if first name is missing", () => {
const cookieString = new URLSearchParams({ const cookieString = new URLSearchParams({
@@ -88,7 +107,7 @@ describe("Access booking", () => {
lastName: "Booking", lastName: "Booking",
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
}).toString() }).toString()
expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe(
ERROR_BAD_REQUEST ERROR_BAD_REQUEST
) )
}) })
@@ -98,23 +117,25 @@ describe("Access booking", () => {
firstName: "Anonymous", firstName: "Anonymous",
lastName: "Booking", lastName: "Booking",
}).toString() }).toString()
expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe(
ERROR_BAD_REQUEST ERROR_BAD_REQUEST
) )
}) })
it("should prompt for more if cookie is invalid", () => { it("should prompt for more if cookie is invalid", () => {
const cookieString = new URLSearchParams({}).toString() const cookieString = new URLSearchParams({}).toString()
expect(accessBooking(loggedOut, "Booking", null, cookieString)).toBe( expect(accessBooking(loggedOutGuest, "Booking", null, cookieString)).toBe(
ERROR_BAD_REQUEST ERROR_BAD_REQUEST
) )
}) })
it("should deny access", () => { it("should deny access if refId mismatch", () => {
expect(accessBooking(loggedOut, "NotBooking", null)).toBe(ERROR_NOT_FOUND) expect(accessBooking(loggedOutGuest, "NotBooking", null)).toBe(
ERROR_NOT_FOUND
)
}) })
}) })
}) })
const user: SafeUser = { const authenticatedUser: SafeUser = {
address: { address: {
city: undefined, city: undefined,
country: "Sweden", country: "Sweden",
@@ -124,10 +145,10 @@ const user: SafeUser = {
}, },
dateOfBirth: "", dateOfBirth: "",
email: "", email: "",
firstName: "", firstName: "Authenticated",
language: undefined, language: undefined,
lastName: "", lastName: "Booking",
membershipNumber: "", membershipNumber: "01234567890123",
membership: undefined, membership: undefined,
loyalty: { loyalty: {
memberships: [], memberships: [],
@@ -145,7 +166,38 @@ const user: SafeUser = {
profileId: "", profileId: "",
} }
const loggedOut: Guest = { const badAuthenticatedUser: SafeUser = {
address: {
city: undefined,
country: "Sweden",
countryCode: "SE",
streetAddress: undefined,
zipCode: undefined,
},
dateOfBirth: "",
email: "",
firstName: "Authenticated",
language: undefined,
lastName: `Bad name ${Math.random()}`,
membershipNumber: "0987654321",
membership: undefined,
loyalty: {
memberships: [],
pointExpirations: [],
points: {
earned: 0,
spent: 0,
spendable: 0,
},
tier: "L1",
tierExpires: "",
},
name: "",
phoneNumber: undefined,
profileId: "",
}
const loggedOutGuest: Guest = {
email: "logged+out@scandichotels.com", email: "logged+out@scandichotels.com",
firstName: "Anonymous", firstName: "Anonymous",
lastName: "Booking", lastName: "Booking",
@@ -154,7 +206,7 @@ const loggedOut: Guest = {
countryCode: "SE", countryCode: "SE",
} }
const loggedIn: Guest = { const loggedInGuest: Guest = {
email: "logged+in@scandichotels.com", email: "logged+in@scandichotels.com",
firstName: "Authenticated", firstName: "Authenticated",
lastName: "Booking", lastName: "Booking",

View File

@@ -21,22 +21,20 @@ function accessBooking(
) { ) {
if (guest.membershipNumber) { if (guest.membershipNumber) {
if (user) { if (user) {
if (lastName.toLowerCase() === guest.lastName?.toLowerCase()) { if (
user.membershipNumber === guest.membershipNumber &&
user.lastName.toLowerCase() === lastName.toLowerCase() &&
lastName.toLowerCase() === guest.lastName?.toLowerCase()
) {
return ACCESS_GRANTED return ACCESS_GRANTED
} }
} else {
console.warn(
"Access to booking not granted due to anonymous user attempting accessing to logged in booking"
)
return ERROR_UNAUTHORIZED
} }
return ERROR_UNAUTHORIZED
} }
if (guest.lastName?.toLowerCase() === lastName.toLowerCase()) { if (guest.lastName?.toLowerCase() === lastName.toLowerCase()) {
if (user) { if (user) {
console.warn(
"Access to booking not granted due to logged in user attempting access to anonymous booking"
)
return ERROR_FORBIDDEN return ERROR_FORBIDDEN
} else { } else {
const params = new URLSearchParams(cookie) const params = new URLSearchParams(cookie)
@@ -47,17 +45,11 @@ function accessBooking(
) { ) {
return ACCESS_GRANTED return ACCESS_GRANTED
} else { } else {
console.warn(
"Access to booking not granted due to incorrect cookie values"
)
return ERROR_BAD_REQUEST return ERROR_BAD_REQUEST
} }
} }
} }
console.warn(
"Access to booking not granted due to anonymous user attempting access with incorrect lastname"
)
return ERROR_NOT_FOUND return ERROR_NOT_FOUND
} }

View File

@@ -143,6 +143,7 @@ export function mapRoomDetails({
priceType, priceType,
rate, rate,
rateDefinition: booking.rateDefinition, rateDefinition: booking.rateDefinition,
refId: booking.refId,
reservationStatus: booking.reservationStatus, reservationStatus: booking.reservationStatus,
room, room,
roomName: room?.name ?? "", roomName: room?.name ?? "",

View File

@@ -45,6 +45,7 @@ export type Room = Pick<
| "checkInDate" | "checkInDate"
| "cheques" | "cheques"
| "confirmationNumber" | "confirmationNumber"
| "refId"
| "currencyCode" | "currencyCode"
| "guest" | "guest"
| "rateDefinition" | "rateDefinition"
@@ -87,6 +88,7 @@ export default function BookedRoomSidePeek({
cheques, cheques,
childrenInRoom, childrenInRoom,
confirmationNumber, confirmationNumber,
refId,
currencyCode, currencyCode,
guest, guest,
isCancelled, isCancelled,
@@ -377,6 +379,7 @@ export default function BookedRoomSidePeek({
formattedTotalPrice={formattedTotalPrice} formattedTotalPrice={formattedTotalPrice}
isCancelled={isCancelled} isCancelled={isCancelled}
priceType={priceType} priceType={priceType}
currencyCode={currencyCode}
rateDefinition={rateDefinition} rateDefinition={rateDefinition}
roomPoints={roomPoints} roomPoints={roomPoints}
totalPrice={totalPrice} totalPrice={totalPrice}
@@ -409,7 +412,7 @@ export default function BookedRoomSidePeek({
)} )}
<GuestDetails <GuestDetails
confirmationNumber={confirmationNumber} refId={refId}
guest={guest} guest={guest}
isCancelled={isCancelled} isCancelled={isCancelled}
user={user} user={user}

View File

@@ -39,8 +39,6 @@ export const FamilyAndFriendsCodes = ["D000029555", "D000029271", "D000029195"]
export const REDEMPTION = "redemption" export const REDEMPTION = "redemption"
export const SEARCHTYPE = "searchtype" export const SEARCHTYPE = "searchtype"
export const BOOKING_CONFIRMATION_NUMBER = "confirmationNumber"
export const MEMBERSHIP_FAILED_ERROR = "MembershipFailedError" export const MEMBERSHIP_FAILED_ERROR = "MembershipFailedError"
export enum PaymentMethodEnum { export enum PaymentMethodEnum {

View File

@@ -13,7 +13,7 @@ const maxRetries = 15
const retryInterval = 2000 const retryInterval = 2000
export function useGuaranteeBooking( export function useGuaranteeBooking(
confirmationNumber: string, refId: string,
isAncillaryFlow = false, isAncillaryFlow = false,
hotelId: string hotelId: string
) { ) {
@@ -50,11 +50,12 @@ export function useGuaranteeBooking(
const guaranteeBooking = trpc.booking.guarantee.useMutation({ const guaranteeBooking = trpc.booking.guarantee.useMutation({
onSuccess: (result) => { onSuccess: (result) => {
if (result) { if (result) {
const mainRoom = result.rooms[0]
if (result.reservationStatus == BookingStatusEnum.BookingCompleted) { if (result.reservationStatus == BookingStatusEnum.BookingCompleted) {
utils.booking.get.invalidate({ confirmationNumber }) utils.booking.get.invalidate({ refId: mainRoom.refId })
} else { } else {
setIsPollingForBookingStatus(true) setIsPollingForBookingStatus(true)
utils.booking.status.invalidate({ confirmationNumber }) utils.booking.status.invalidate({ refId: mainRoom.refId })
} }
} else { } else {
handleGuaranteeError() handleGuaranteeError()
@@ -66,7 +67,7 @@ export function useGuaranteeBooking(
}) })
const bookingStatus = useHandleBookingStatus({ const bookingStatus = useHandleBookingStatus({
confirmationNumber, refId,
expectedStatuses: [BookingStatusEnum.BookingCompleted], expectedStatuses: [BookingStatusEnum.BookingCompleted],
maxRetries, maxRetries,
retryInterval, retryInterval,
@@ -76,7 +77,7 @@ export function useGuaranteeBooking(
useEffect(() => { useEffect(() => {
if (bookingStatus?.data?.paymentUrl && isPollingForBookingStatus) { if (bookingStatus?.data?.paymentUrl && isPollingForBookingStatus) {
router.push(bookingStatus.data.paymentUrl) router.push(bookingStatus.data.paymentUrl)
utils.booking.get.invalidate({ confirmationNumber }) utils.booking.get.invalidate({ refId })
setIsPollingForBookingStatus(false) setIsPollingForBookingStatus(false)
} else if (bookingStatus.isTimeout) { } else if (bookingStatus.isTimeout) {
handleGuaranteeError("Timeout") handleGuaranteeError("Timeout")
@@ -87,7 +88,7 @@ export function useGuaranteeBooking(
handleGuaranteeError, handleGuaranteeError,
setIsPollingForBookingStatus, setIsPollingForBookingStatus,
isPollingForBookingStatus, isPollingForBookingStatus,
confirmationNumber, refId,
utils.booking.get, utils.booking.get,
]) ])

View File

@@ -7,13 +7,13 @@ import { trpc } from "@/lib/trpc/client"
import type { BookingStatusEnum } from "@/constants/booking" import type { BookingStatusEnum } from "@/constants/booking"
export function useHandleBookingStatus({ export function useHandleBookingStatus({
confirmationNumber, refId,
expectedStatuses, expectedStatuses,
maxRetries, maxRetries,
retryInterval, retryInterval,
enabled, enabled,
}: { }: {
confirmationNumber: string | null refId: string | null
expectedStatuses: BookingStatusEnum[] expectedStatuses: BookingStatusEnum[]
maxRetries: number maxRetries: number
retryInterval: number retryInterval: number
@@ -22,7 +22,7 @@ export function useHandleBookingStatus({
const retries = useRef(0) const retries = useRef(0)
const query = trpc.booking.status.useQuery( const query = trpc.booking.status.useQuery(
{ confirmationNumber: confirmationNumber ?? "" }, { refId: refId ?? "" },
{ {
enabled, enabled,
refetchInterval: (query) => { refetchInterval: (query) => {

View File

@@ -17,7 +17,6 @@ import type {
HotelInput, HotelInput,
} from "@/types/trpc/routers/hotel/hotel" } from "@/types/trpc/routers/hotel/hotel"
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
import type { LinkedReservationsInput } from "@/server/routers/booking/input"
import type { GetHotelsByCSFilterInput } from "@/server/routers/hotels/input" import type { GetHotelsByCSFilterInput } from "@/server/routers/hotels/input"
import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input" import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input"
@@ -136,8 +135,8 @@ export const getPackages = cache(async function getMemoizedPackages(
}) })
export const getBookingConfirmation = cache( export const getBookingConfirmation = cache(
async function getMemoizedBookingConfirmation(confirmationNumber: string) { async function getMemoizedBookingConfirmation(refId: string) {
return serverClient().booking.get({ confirmationNumber }) return serverClient().booking.get({ refId })
} }
) )
@@ -156,8 +155,10 @@ export const findBooking = cache(async function getMemoizedFindBooking(
}) })
export const getLinkedReservations = cache( export const getLinkedReservations = cache(
async function getMemoizedLinkedReservations(input: LinkedReservationsInput) { async function getMemoizedLinkedReservations(refId: string) {
return serverClient().booking.linkedReservations(input) return serverClient().booking.linkedReservations({
refId,
})
} }
) )

View File

@@ -45,7 +45,7 @@ export default function MyStayProvider({
const { data, error, isFetching, isFetchedAfterMount } = const { data, error, isFetching, isFetchedAfterMount } =
trpc.booking.get.useQuery( trpc.booking.get.useQuery(
{ {
confirmationNumber: bookingConfirmation.booking.confirmationNumber, refId: bookingConfirmation.booking.refId,
lang, lang,
}, },
{ {
@@ -68,7 +68,7 @@ export default function MyStayProvider({
} = trpc.booking.linkedReservations.useQuery( } = trpc.booking.linkedReservations.useQuery(
{ {
lang, lang,
rooms: bookingConfirmation.booking.linkedReservations, refId: bookingConfirmation.booking.refId,
}, },
{ {
initialData: linkedReservationsResponses, initialData: linkedReservationsResponses,

View File

@@ -0,0 +1,46 @@
import { initTRPC } from "@trpc/server"
import { z } from "zod"
import { parseRefId } from "@/utils/refId"
import type { Meta } from "@/types/trpc/meta"
import type { Context } from "../context"
export function createRefIdPlugin() {
const t = initTRPC.context<Context>().meta<Meta>().create()
return {
toConfirmationNumber: t.procedure
.input(
z.object({
refId: z.string(),
})
)
.use(({ input, next }) => {
const { confirmationNumber } = parseRefId(input.refId)
return next({
ctx: {
confirmationNumber,
},
})
}),
toConfirmationNumbers: t.procedure
.input(
z.object({
refIds: z.array(z.string()),
})
)
.use(({ input, next }) => {
const confirmationNumbers = input.refIds.map((refId) => {
const { confirmationNumber } = parseRefId(refId)
return confirmationNumber
})
return next({
ctx: {
confirmationNumbers,
},
})
}),
}
}

View File

@@ -103,7 +103,6 @@ export const createBookingInput = z.object({
}) })
export const addPackageInput = z.object({ export const addPackageInput = z.object({
confirmationNumber: z.string(),
ancillaryComment: z.string(), ancillaryComment: z.string(),
ancillaryDeliveryTime: z.string().nullish(), ancillaryDeliveryTime: z.string().nullish(),
packages: z.array( packages: z.array(
@@ -117,22 +116,15 @@ export const addPackageInput = z.object({
}) })
export const removePackageInput = z.object({ export const removePackageInput = z.object({
confirmationNumber: z.string(),
codes: z.array(z.string()), codes: z.array(z.string()),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]), language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
}) })
export const priceChangeInput = z.object({
confirmationNumber: z.string(),
})
export const cancelBookingsInput = z.object({ export const cancelBookingsInput = z.object({
confirmationNumbers: z.array(z.string()),
language: z.nativeEnum(Lang), language: z.nativeEnum(Lang),
}) })
export const guaranteeBookingInput = z.object({ export const guaranteeBookingInput = z.object({
confirmationNumber: z.string(),
card: z card: z
.object({ .object({
alias: z.string(), alias: z.string(),
@@ -156,7 +148,6 @@ export const createRefIdInput = z.object({
}) })
export const updateBookingInput = z.object({ export const updateBookingInput = z.object({
confirmationNumber: z.string(),
checkInDate: z.string().optional(), checkInDate: z.string().optional(),
checkOutDate: z.string().optional(), checkOutDate: z.string().optional(),
guest: z guest: z
@@ -169,19 +160,13 @@ export const updateBookingInput = z.object({
}) })
// Query // Query
const confirmationNumberInput = z.object({
confirmationNumber: z.string(), export const getBookingInput = z.object({
lang: z.nativeEnum(Lang).optional(), lang: z.nativeEnum(Lang).optional(),
}) })
export const getBookingInput = confirmationNumberInput
export const getLinkedReservationsInput = z.object({ export const getLinkedReservationsInput = z.object({
lang: z.nativeEnum(Lang).optional(), lang: z.nativeEnum(Lang).optional(),
rooms: z.array(
z.object({
confirmationNumber: z.string(),
})
),
}) })
export const findBookingInput = z.object({ export const findBookingInput = z.object({
@@ -194,4 +179,6 @@ export const findBookingInput = z.object({
export type LinkedReservationsInput = z.input<typeof getLinkedReservationsInput> export type LinkedReservationsInput = z.input<typeof getLinkedReservationsInput>
export const getBookingStatusInput = confirmationNumberInput export const getBookingStatusInput = z.object({
lang: z.nativeEnum(Lang).optional(),
})

View File

@@ -1,4 +1,5 @@
import * as api from "@/lib/api" import * as api from "@/lib/api"
import { createRefIdPlugin } from "@/server/plugins/refIdToConfirmationNumber"
import { getMembershipNumber } from "@/server/routers/user/utils" import { getMembershipNumber } from "@/server/routers/user/utils"
import { createCounter } from "@/server/telemetry" import { createCounter } from "@/server/telemetry"
import { router, safeProtectedServiceProcedure } from "@/server/trpc" import { router, safeProtectedServiceProcedure } from "@/server/trpc"
@@ -8,13 +9,14 @@ import {
cancelBookingsInput, cancelBookingsInput,
createBookingInput, createBookingInput,
guaranteeBookingInput, guaranteeBookingInput,
priceChangeInput,
removePackageInput, removePackageInput,
updateBookingInput, updateBookingInput,
} from "./input" } from "./input"
import { bookingConfirmationSchema, createBookingSchema } from "./output" import { bookingConfirmationSchema, createBookingSchema } from "./output"
import { cancelBooking } from "./utils" import { cancelBooking } from "./utils"
const refIdPlugin = createRefIdPlugin()
export const bookingMutationRouter = router({ export const bookingMutationRouter = router({
create: safeProtectedServiceProcedure create: safeProtectedServiceProcedure
.input(createBookingInput) .input(createBookingInput)
@@ -72,9 +74,9 @@ export const bookingMutationRouter = router({
return verifiedData.data return verifiedData.data
}), }),
priceChange: safeProtectedServiceProcedure priceChange: safeProtectedServiceProcedure
.input(priceChangeInput) .concat(refIdPlugin.toConfirmationNumber)
.mutation(async function ({ ctx, input }) { .mutation(async function ({ ctx }) {
const { confirmationNumber } = input const { confirmationNumber } = ctx
const priceChangeCounter = createCounter("trpc.booking", "price-change") const priceChangeCounter = createCounter("trpc.booking", "price-change")
const metricsPriceChange = priceChangeCounter.init({ confirmationNumber }) const metricsPriceChange = priceChangeCounter.init({ confirmationNumber })
@@ -91,7 +93,6 @@ export const bookingMutationRouter = router({
api.endpoints.v1.Booking.priceChange(confirmationNumber), api.endpoints.v1.Booking.priceChange(confirmationNumber),
{ {
headers, headers,
body: input,
} }
) )
@@ -113,9 +114,11 @@ export const bookingMutationRouter = router({
}), }),
cancel: safeProtectedServiceProcedure cancel: safeProtectedServiceProcedure
.input(cancelBookingsInput) .input(cancelBookingsInput)
.concat(refIdPlugin.toConfirmationNumbers)
.mutation(async function ({ ctx, input }) { .mutation(async function ({ ctx, input }) {
const token = ctx.session?.token.access_token ?? ctx.serviceToken const token = ctx.session?.token.access_token ?? ctx.serviceToken
const { confirmationNumbers, language } = input const { confirmationNumbers } = ctx
const { language } = input
const responses = await Promise.allSettled( const responses = await Promise.allSettled(
confirmationNumbers.map((confirmationNumber) => confirmationNumbers.map((confirmationNumber) =>
@@ -144,9 +147,11 @@ export const bookingMutationRouter = router({
}), }),
packages: safeProtectedServiceProcedure packages: safeProtectedServiceProcedure
.input(addPackageInput) .input(addPackageInput)
.concat(refIdPlugin.toConfirmationNumber)
.mutation(async function ({ ctx, input }) { .mutation(async function ({ ctx, input }) {
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
const { confirmationNumber, ...body } = input const { confirmationNumber } = ctx
const { refId, ...body } = input
const addPackageCounter = createCounter("trpc.booking", "package.add") const addPackageCounter = createCounter("trpc.booking", "package.add")
const metricsAddPackage = addPackageCounter.init({ confirmationNumber }) const metricsAddPackage = addPackageCounter.init({ confirmationNumber })
@@ -183,9 +188,11 @@ export const bookingMutationRouter = router({
}), }),
guarantee: safeProtectedServiceProcedure guarantee: safeProtectedServiceProcedure
.input(guaranteeBookingInput) .input(guaranteeBookingInput)
.concat(refIdPlugin.toConfirmationNumber)
.mutation(async function ({ ctx, input }) { .mutation(async function ({ ctx, input }) {
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
const { confirmationNumber, language, ...body } = input const { confirmationNumber } = ctx
const { refId, language, ...body } = input
const guaranteeBookingCounter = createCounter("trpc.booking", "guarantee") const guaranteeBookingCounter = createCounter("trpc.booking", "guarantee")
const metricsGuaranteeBooking = guaranteeBookingCounter.init({ const metricsGuaranteeBooking = guaranteeBookingCounter.init({
@@ -225,9 +232,11 @@ export const bookingMutationRouter = router({
}), }),
update: safeProtectedServiceProcedure update: safeProtectedServiceProcedure
.input(updateBookingInput) .input(updateBookingInput)
.concat(refIdPlugin.toConfirmationNumber)
.mutation(async function ({ ctx, input }) { .mutation(async function ({ ctx, input }) {
const accessToken = ctx.session?.token.access_token || ctx.serviceToken const accessToken = ctx.session?.token.access_token || ctx.serviceToken
const { confirmationNumber, ...body } = input const { confirmationNumber } = ctx
const { refId, ...body } = input
const updateBookingCounter = createCounter("trpc.booking", "update") const updateBookingCounter = createCounter("trpc.booking", "update")
const metricsUpdateBooking = updateBookingCounter.init({ const metricsUpdateBooking = updateBookingCounter.init({
@@ -265,9 +274,11 @@ export const bookingMutationRouter = router({
}), }),
removePackage: safeProtectedServiceProcedure removePackage: safeProtectedServiceProcedure
.input(removePackageInput) .input(removePackageInput)
.concat(refIdPlugin.toConfirmationNumber)
.mutation(async function ({ ctx, input }) { .mutation(async function ({ ctx, input }) {
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
const { confirmationNumber, codes, language } = input const { confirmationNumber } = ctx
const { codes, language } = input
const removePackageCounter = createCounter( const removePackageCounter = createCounter(
"trpc.booking", "trpc.booking",

View File

@@ -2,6 +2,7 @@ import { z } from "zod"
import { BookingStatusEnum, ChildBedTypeEnum } from "@/constants/booking" import { BookingStatusEnum, ChildBedTypeEnum } from "@/constants/booking"
import { calculateRefId } from "@/utils/refId"
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator" import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
import { nullableIntValidator } from "@/utils/zod/numberValidator" import { nullableIntValidator } from "@/utils/zod/numberValidator"
import { import {
@@ -78,7 +79,13 @@ export const createBookingSchema = z
type: d.data.type, type: d.data.type,
reservationStatus: d.data.attributes.reservationStatus, reservationStatus: d.data.attributes.reservationStatus,
paymentUrl: d.data.attributes.paymentUrl, paymentUrl: d.data.attributes.paymentUrl,
rooms: d.data.attributes.rooms, rooms: d.data.attributes.rooms.map((room) => {
const lastName = d.data.attributes.guest?.lastName ?? ""
return {
...room,
refId: calculateRefId(room.confirmationNumber, lastName),
}
}),
errors: d.data.attributes.errors, errors: d.data.attributes.errors,
guest: d.data.attributes.guest, guest: d.data.attributes.guest,
})) }))
@@ -248,6 +255,31 @@ export const bookingConfirmationSchema = z
}) })
.transform(({ data }) => ({ .transform(({ data }) => ({
...data.attributes, ...data.attributes,
refId: calculateRefId(
data.attributes.confirmationNumber,
data.attributes.guest.lastName
),
linkedReservations: data.attributes.linkedReservations.map(
(linkedReservation) => {
/**
* We lazy load linked reservations in the client.
* The problem is that we need to load the reservation in order to
* calculate the refId for the reservation as the refId uses the guest's
* lastname in it. Ideally we should pass a promise to the React
* component that uses `use()` to resolve it. But right now we use tRPC
* in the client. That tRPC endpoint only uses the confirmationNumber
* from the refId. So that means we can pass whatever as the lastname
* here, because it is actually never read. We should change this ASAP.
*/
return {
...linkedReservation,
refId: calculateRefId(
linkedReservation.confirmationNumber,
"" // TODO: Empty lastname here, see comment above
),
}
}
),
packages: data.attributes.packages.filter((p) => p.type !== "Ancillary"), packages: data.attributes.packages.filter((p) => p.type !== "Ancillary"),
ancillaries: data.attributes.packages.filter((p) => p.type === "Ancillary"), ancillaries: data.attributes.packages.filter((p) => p.type === "Ancillary"),
extraBedTypes: data.attributes.childBedPreferences, extraBedTypes: data.attributes.childBedPreferences,

View File

@@ -1,5 +1,6 @@
import * as api from "@/lib/api" import * as api from "@/lib/api"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { createRefIdPlugin } from "@/server/plugins/refIdToConfirmationNumber"
import { createCounter } from "@/server/telemetry" import { createCounter } from "@/server/telemetry"
import { import {
router, router,
@@ -7,8 +8,10 @@ import {
serviceProcedure, serviceProcedure,
} from "@/server/trpc" } from "@/server/trpc"
import { getBookedHotelRoom } from "@/utils/booking"
import { encrypt } from "../../../utils/encryption"
import { getHotel } from "../hotels/utils" import { getHotel } from "../hotels/utils"
import { encrypt } from "../utils/encryption"
import { import {
createRefIdInput, createRefIdInput,
findBookingInput, findBookingInput,
@@ -17,28 +20,31 @@ import {
getLinkedReservationsInput, getLinkedReservationsInput,
} from "./input" } from "./input"
import { createBookingSchema } from "./output" import { createBookingSchema } from "./output"
import { findBooking, getBookedHotelRoom, getBooking } from "./utils" import { findBooking, getBooking } from "./utils"
const refIdPlugin = createRefIdPlugin()
export const bookingQueryRouter = router({ export const bookingQueryRouter = router({
get: safeProtectedServiceProcedure get: safeProtectedServiceProcedure
.input(getBookingInput) .input(getBookingInput)
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, input, next }) => { .use(async ({ ctx, input, next }) => {
const lang = input.lang ?? ctx.lang const lang = input.lang ?? ctx.lang
const token = ctx.session?.token.access_token ?? ctx.serviceToken
return next({ return next({
ctx: { ctx: {
lang, lang,
token,
}, },
}) })
}) })
.query(async function ({ ctx, input: { confirmationNumber } }) { .query(async function ({ ctx }) {
const { confirmationNumber, lang, serviceToken } = ctx
const getBookingCounter = createCounter("trpc.booking", "get") const getBookingCounter = createCounter("trpc.booking", "get")
const metricsGetBooking = getBookingCounter.init({ confirmationNumber }) const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
metricsGetBooking.start() metricsGetBooking.start()
const booking = await getBooking(confirmationNumber, ctx.lang, ctx.token) const booking = await getBooking(confirmationNumber, lang, serviceToken)
if (!booking) { if (!booking) {
metricsGetBooking.dataError( metricsGetBooking.dataError(
@@ -52,9 +58,9 @@ export const bookingQueryRouter = router({
{ {
hotelId: booking.hotelId, hotelId: booking.hotelId,
isCardOnlyPayment: false, isCardOnlyPayment: false,
language: ctx.lang, language: lang,
}, },
ctx.serviceToken serviceToken
) )
if (!hotelData) { if (!hotelData) {
@@ -141,6 +147,7 @@ export const bookingQueryRouter = router({
}), }),
linkedReservations: safeProtectedServiceProcedure linkedReservations: safeProtectedServiceProcedure
.input(getLinkedReservationsInput) .input(getLinkedReservationsInput)
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, input, next }) => { .use(async ({ ctx, input, next }) => {
const lang = input.lang ?? ctx.lang const lang = input.lang ?? ctx.lang
const token = ctx.session?.token.access_token ?? ctx.serviceToken const token = ctx.session?.token.access_token ?? ctx.serviceToken
@@ -151,27 +158,36 @@ export const bookingQueryRouter = router({
}, },
}) })
}) })
.query(async function ({ ctx, input: { rooms } }) { .query(async function ({ ctx }) {
const { confirmationNumber, lang, token } = ctx
const getLinkedReservationsCounter = createCounter( const getLinkedReservationsCounter = createCounter(
"trpc.booking", "trpc.booking",
"linkedReservations" "linkedReservations"
) )
const metricsGetLinkedReservations = getLinkedReservationsCounter.init({ const metricsGetLinkedReservations = getLinkedReservationsCounter.init({
confirmationNumbers: rooms, confirmationNumber,
}) })
metricsGetLinkedReservations.start() metricsGetLinkedReservations.start()
const linkedReservationsResult = await Promise.allSettled( const booking = await getBooking(confirmationNumber, lang, token)
rooms.map((room) =>
getBooking(room.confirmationNumber, ctx.lang, ctx.token) if (!booking) {
return []
}
const linkedReservationsResults = await Promise.allSettled(
booking.linkedReservations.map((linkedReservation) =>
getBooking(linkedReservation.confirmationNumber, lang, token)
) )
) )
const linkedReservations = [] const linkedReservations = []
for (const booking of linkedReservationsResult) { for (const linkedReservationsResult of linkedReservationsResults) {
if (booking.status === "fulfilled") { if (linkedReservationsResult.status === "fulfilled") {
if (booking.value) { if (linkedReservationsResult.value) {
linkedReservations.push(booking.value) linkedReservations.push(linkedReservationsResult.value)
} else { } else {
metricsGetLinkedReservations.dataError( metricsGetLinkedReservations.dataError(
`Unexpected value for linked reservation` `Unexpected value for linked reservation`
@@ -188,44 +204,44 @@ export const bookingQueryRouter = router({
return linkedReservations return linkedReservations
}), }),
status: serviceProcedure.input(getBookingStatusInput).query(async function ({ status: serviceProcedure
ctx, .input(getBookingStatusInput)
input, .concat(refIdPlugin.toConfirmationNumber)
}) { .query(async function ({ ctx }) {
const { confirmationNumber } = input const { confirmationNumber } = ctx
const getBookingStatusCounter = createCounter("trpc.booking", "status") const getBookingStatusCounter = createCounter("trpc.booking", "status")
const metricsGetBookingStatus = getBookingStatusCounter.init({ const metricsGetBookingStatus = getBookingStatusCounter.init({
confirmationNumber, confirmationNumber,
}) })
metricsGetBookingStatus.start() metricsGetBookingStatus.start()
const apiResponse = await api.get( const apiResponse = await api.get(
api.endpoints.v1.Booking.status(confirmationNumber), api.endpoints.v1.Booking.status(confirmationNumber),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,
}, },
}
)
if (!apiResponse.ok) {
await metricsGetBookingStatus.httpError(apiResponse)
throw serverErrorByStatus(apiResponse.status, apiResponse)
} }
)
if (!apiResponse.ok) { const apiJson = await apiResponse.json()
await metricsGetBookingStatus.httpError(apiResponse) const verifiedData = createBookingSchema.safeParse(apiJson)
throw serverErrorByStatus(apiResponse.status, apiResponse) if (!verifiedData.success) {
} metricsGetBookingStatus.validationError(verifiedData.error)
throw badRequestError()
}
const apiJson = await apiResponse.json() metricsGetBookingStatus.success()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsGetBookingStatus.validationError(verifiedData.error)
throw badRequestError()
}
metricsGetBookingStatus.success() return verifiedData.data
}),
return verifiedData.data
}),
createRefId: serviceProcedure createRefId: serviceProcedure
.input(createRefIdInput) .input(createRefIdInput)
.mutation(async function ({ input }) { .mutation(async function ({ input }) {

View File

@@ -5,35 +5,8 @@ import { toApiLang } from "@/server/utils"
import { bookingConfirmationSchema, createBookingSchema } from "./output" import { bookingConfirmationSchema, createBookingSchema } from "./output"
import type { Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
export function getBookedHotelRoom(
rooms: Room[],
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( export async function getBooking(
confirmationNumber: string, confirmationNumber: string,
lang: Lang, lang: Lang,

View File

@@ -4,10 +4,10 @@ import { myStay } from "@/constants/routes/myStay"
import { env } from "@/env/server" import { env } from "@/env/server"
import * as api from "@/lib/api" import * as api from "@/lib/api"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { encrypt } from "@/server/routers/utils/encryption"
import { createCounter } from "@/server/telemetry" import { createCounter } from "@/server/telemetry"
import { cache } from "@/utils/cache" import { cache } from "@/utils/cache"
import { encrypt } from "@/utils/encryption"
import * as maskValue from "@/utils/maskValue" import * as maskValue from "@/utils/maskValue"
import { isValidSession } from "@/utils/session" import { isValidSession } from "@/utils/session"
import { getCurrentWebUrl } from "@/utils/url" import { getCurrentWebUrl } from "@/utils/url"

View File

@@ -3,10 +3,9 @@ import { produce } from "immer"
import { useContext } from "react" import { useContext } from "react"
import { create, useStore } from "zustand" import { create, useStore } from "zustand"
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
import { mapRoomDetails } from "@/components/HotelReservation/MyStay/utils/mapRoomDetails" import { mapRoomDetails } from "@/components/HotelReservation/MyStay/utils/mapRoomDetails"
import { MyStayContext } from "@/contexts/MyStay" import { MyStayContext } from "@/contexts/MyStay"
import { getBookedHotelRoom } from "@/utils/booking"
import { import {
calculateTotalPoints, calculateTotalPoints,

View File

@@ -5,7 +5,7 @@ import type {
} from "@/types/trpc/routers/booking/confirmation" } from "@/types/trpc/routers/booking/confirmation"
export interface BookingConfirmationProps { export interface BookingConfirmationProps {
confirmationNumber: string refId: string
} }
export interface BookingConfirmationRoom extends Room { export interface BookingConfirmationRoom extends Room {

View File

@@ -1,11 +1,5 @@
import type { z } from "zod"
import type { Room } from "@/types/hotel" import type { Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" 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 export interface BookingConfirmationRoomsProps
extends Pick<BookingConfirmation, "booking"> { extends Pick<BookingConfirmation, "booking"> {
@@ -14,5 +8,4 @@ export interface BookingConfirmationRoomsProps
} }
checkInTime: string checkInTime: string
checkOutTime: string checkOutTime: string
linkedReservations: LinkedReservationSchema[]
} }

View File

@@ -1,7 +1,7 @@
export interface LinkedReservationProps { export interface LinkedReservationProps {
checkInTime: string checkInTime: string
checkOutTime: string checkOutTime: string
confirmationNumber: string refId: string
roomIndex: number roomIndex: number
roomNumber: number roomNumber: number
} }

View File

@@ -6,7 +6,7 @@ export const cancelStaySchema = z.object({
rooms: z.array( rooms: z.array(
z.object({ z.object({
checked: z.boolean(), checked: z.boolean(),
confirmationNumber: z.string(), refId: z.string(),
}) })
), ),
}) })

View File

@@ -17,7 +17,6 @@ export interface AncillariesProps extends Pick<BookingConfirmation, "booking"> {
packages: Packages | null packages: Packages | null
user: User | null user: User | null
savedCreditCards: CreditCard[] | null savedCreditCards: CreditCard[] | null
refId: string
} }
export interface AddedAncillariesProps { export interface AddedAncillariesProps {
@@ -41,7 +40,6 @@ export interface AncillaryGridModalProps {
export interface AddAncillaryFlowModalProps export interface AddAncillaryFlowModalProps
extends Pick<BookingConfirmation, "booking"> { extends Pick<BookingConfirmation, "booking"> {
packages: Packages | null packages: Packages | null
refId: string
user: User | null user: User | null
savedCreditCards: CreditCard[] | null savedCreditCards: CreditCard[] | null
} }

View File

@@ -27,6 +27,7 @@ export interface Room {
packages: BookingConfirmation["booking"]["packages"] packages: BookingConfirmation["booking"]["packages"]
formattedRoomCost: string formattedRoomCost: string
rateDefinition: BookingConfirmation["booking"]["rateDefinition"] rateDefinition: BookingConfirmation["booking"]["rateDefinition"]
refId: string
roomFeatures?: PackageSchema[] | null roomFeatures?: PackageSchema[] | null
roomPoints: number roomPoints: number
roomPrice: number roomPrice: number

View File

@@ -30,6 +30,7 @@ export type Room = Pick<
| "linkedReservations" | "linkedReservations"
| "multiRoom" | "multiRoom"
| "rateDefinition" | "rateDefinition"
| "refId"
| "reservationStatus" | "reservationStatus"
| "roomPoints" | "roomPoints"
| "roomTypeCode" | "roomTypeCode"

View File

@@ -0,0 +1,27 @@
import type { Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function getBookedHotelRoom(
rooms: Room[],
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,
}
}

View File

@@ -4,13 +4,11 @@ import crypto from "crypto"
import { env } from "@/env/server" import { env } from "@/env/server"
export { decrypt, encrypt }
const algorithm = "DES-ECB" const algorithm = "DES-ECB"
const encryptionKey = env.BOOKING_ENCRYPTION_KEY const encryptionKey = env.BOOKING_ENCRYPTION_KEY
const bufferKey = Buffer.from(encryptionKey, "utf8") const bufferKey = Buffer.from(encryptionKey, "utf8")
function encrypt(originalString: string) { export function encrypt(originalString: string) {
try { try {
const cipher = crypto.createCipheriv(algorithm, bufferKey, null) const cipher = crypto.createCipheriv(algorithm, bufferKey, null)
cipher.setAutoPadding(false) cipher.setAutoPadding(false)
@@ -32,7 +30,7 @@ function encrypt(originalString: string) {
} }
} }
function decrypt(encryptedString: string) { export function decrypt(encryptedString: string) {
try { try {
const decipher = crypto.createDecipheriv(algorithm, bufferKey, null) const decipher = crypto.createDecipheriv(algorithm, bufferKey, null)
decipher.setAutoPadding(false) decipher.setAutoPadding(false)

View File

@@ -0,0 +1,21 @@
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],
}
}