feat(SW-2116): RefId instead of confirmationNumber

This commit is contained in:
Arvid Norlin
2025-04-25 13:44:49 +02:00
committed by Michael Zetterberg
parent 7eeb0bbcac
commit 74d37dad93
61 changed files with 1032 additions and 843 deletions

View File

@@ -1,4 +1,5 @@
"use client"
import { createEvent } from "ics"
import { useIntl } from "react-intl"

View File

@@ -1,21 +0,0 @@
.main {
background-color: var(--Base-Surface-Primary-light-Normal);
display: grid;
gap: var(--Spacing-x5);
grid-template-areas: "header" "booking";
margin: 0 auto;
min-height: 100dvh;
padding-top: var(--Spacing-x5);
width: var(--max-width-page);
}
@media screen and (min-width: 1367px) {
.main {
grid-template-areas:
"header receipt"
"booking receipt";
grid-template-columns: 1fr 340px;
grid-template-rows: auto 1fr;
padding-top: var(--Spacing-x9);
}
}

View File

@@ -1,24 +0,0 @@
"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>
)
}

View File

@@ -1,15 +1,22 @@
"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({ bookingUrl }: ManageBookingProps) {
export default function ManageBooking({ refId }: ManageBookingProps) {
const intl = useIntl()
const lang = useLang()
const bookingUrl = `${myStay[lang]}?RefId=${refId}`
return (
<Button

View File

@@ -1,15 +1,11 @@
"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"
@@ -22,11 +18,9 @@ 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:
@@ -52,8 +46,6 @@ export default function Header({
url: hotel.contactInformation.websiteUrl,
}
const bookingUrlPath = `${myStay[lang]}?RefId=${refId}`
return (
<header className={styles.header}>
<hgroup className={styles.hgroup}>
@@ -74,9 +66,7 @@ export default function Header({
hotelName={hotel.name}
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
/>
<ManageBooking bookingUrl={bookingUrlPath} />
{/* Download Invoice will be added later (currently available on My Stay) */}
{/* <DownloadInvoice mainRef={mainRef} /> */}
<ManageBooking refId={refId} />
</div>
</header>
)

View File

@@ -1,9 +1,8 @@
"use client"
import { useIntl } from "react-intl"
import { homeHrefs } from "@/constants/homeHrefs"
import { myBooking } from "@/constants/myBooking"
import { env } from "@/env/client"
import { myStay } from "@/constants/routes/myStay"
import useLang from "@/hooks/useLang"
@@ -13,22 +12,17 @@ import styles from "./promos.module.css"
import type { PromosProps } from "@/types/components/hotelReservation/bookingConfirmation/promos"
export default function Promos({
confirmationNumber,
hotelId,
lastName,
}: PromosProps) {
export default function Promos({ refId, hotelId }: 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={`${myBookingUrl}?bookingId=${confirmationNumber}&lastName=${lastName}`}
href={`${myStay[lang]}?RefId=${refId}`}
text={intl.formatMessage({
defaultMessage:
"Discover the little extra touches to make your upcoming stay even more unforgettable.",
@@ -41,7 +35,7 @@ export default function Promos({
buttonText={intl.formatMessage({
defaultMessage: "Book another stay",
})}
href={`${homeUrl}?hotel=${hotelId}`}
href={`/${lang}?hotel=${hotelId}`}
text={intl.formatMessage({
defaultMessage:
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",

View File

@@ -20,14 +20,16 @@ import { CurrencyEnum } from "@/types/enums/currency"
export function LinkedReservation({
checkInTime,
checkOutTime,
confirmationNumber,
refId,
roomIndex,
}: LinkedReservationProps) {
const lang = useLang()
const { data, refetch, isLoading } = trpc.booking.get.useQuery({
confirmationNumber,
const { data, refetch, isLoading } = trpc.booking.confirmation.useQuery({
refId,
lang,
})
const {
setRoom,
setFormattedTotalCost,
@@ -41,6 +43,7 @@ export function LinkedReservation({
totalBookingPrice: state.totalBookingPrice,
totalBookingCheques: state.totalBookingCheques,
}))
const intl = useIntl()
useEffect(() => {
@@ -78,7 +81,9 @@ export function LinkedReservation({
if (!data?.room) {
return <Retry handleRefetch={refetch} />
}
const { booking, room } = data
return (
<Room
checkInDate={booking.checkInDate}
@@ -86,8 +91,8 @@ export function LinkedReservation({
checkInTime={checkInTime}
checkOutTime={checkOutTime}
confirmationNumber={booking.confirmationNumber}
guest={booking.guest}
guaranteeInfo={booking.guaranteeInfo}
guest={booking.guest}
img={room.images[0]}
rateDefinition={booking.rateDefinition}
roomName={room.name}

View File

@@ -9,30 +9,35 @@ 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 intl = await getIntl()
const { linkedReservations } = booking
return (
<section className={styles.rooms}>
<div className={styles.room}>
{linkedReservations.length ? (
<Typography variant="Title/Subtitle/md">
<h2 className={styles.roomTitle}>
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: 1 }
)}
</h2>
</Typography>
) : null}
{linkedReservations.length ? <RoomTitle nr={1} /> : null}
<Room
checkInDate={booking.checkInDate}
checkOutDate={booking.checkOutDate}
@@ -49,20 +54,11 @@ export default async function Rooms({
{linkedReservations.map((reservation, idx) => (
<div className={styles.room} key={reservation.confirmationNumber}>
<Typography variant="Title/Subtitle/md">
<h2 className={styles.roomTitle}>
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: idx + 2 }
)}
</h2>
</Typography>
<RoomTitle nr={idx + 2} />
<LinkedReservation
checkInTime={checkInTime}
checkOutTime={checkOutTime}
confirmationNumber={reservation.confirmationNumber}
refId={reservation.refId}
roomIndex={idx + 1}
/>
</div>

View File

@@ -27,7 +27,7 @@ export default function Tracking({
getTracking(
lang,
bookingConfirmation.booking,
bookingConfirmation.hotel,
bookingConfirmation.hotelData.hotel,
rooms
)

View File

@@ -67,7 +67,7 @@ function mapAncillaryPackage(
export function getTracking(
lang: Lang,
booking: BookingConfirmation["booking"],
hotel: BookingConfirmation["hotel"],
hotel: BookingConfirmation["hotelData"]["hotel"],
rooms: Room[]
) {
const arrivalDate = new Date(booking.checkInDate)

View File

@@ -1,22 +0,0 @@
.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;
}
}

View File

@@ -1,83 +0,0 @@
import { notFound } from "next/navigation"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
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 { encrypt } from "@/utils/encryption"
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 refId={refId} hotelId={hotel.operaId} />
<div className={styles.mobileReceipt}>
<Receipt />
</div>
</div>
<aside className={styles.aside}>
<SidePanel variant="receipt">
<Receipt />
</SidePanel>
</aside>
</Confirmation>
<Tracking bookingConfirmation={bookingConfirmation} />
</BookingConfirmationProvider>
)
}

View File

@@ -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 { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation"
import type { BookingSchema } from "@/types/trpc/routers/booking/confirmation"
export function mapRoomState(
booking: BookingConfirmationSchema,
booking: BookingSchema,
room: BookingConfirmationRoom,
intl: IntlShape
) {

View File

@@ -18,12 +18,12 @@ const validBookingStatuses = [
]
interface HandleStatusPollingProps {
confirmationNumber: string
refId: string
successRedirectUrl: string
}
export default function HandleSuccessCallback({
confirmationNumber,
refId,
successRedirectUrl,
}: HandleStatusPollingProps) {
const router = useRouter()
@@ -33,7 +33,7 @@ export default function HandleSuccessCallback({
error,
isTimeout,
} = useHandleBookingStatus({
confirmationNumber,
refId,
expectedStatuses: validBookingStatuses,
maxRetries: 10,
retryInterval: 2000,
@@ -70,9 +70,9 @@ export default function HandleSuccessCallback({
? `&errorCode=${membershipFailedError.errorCode}`
: ""
router.replace(`${successRedirectUrl}${errorParam}`)
router.replace(`${successRedirectUrl}?RefId=${refId}${errorParam}`)
}
}, [bookingStatus, successRedirectUrl, router])
}, [bookingStatus, refId, router, successRedirectUrl])
if (isTimeout || error) {
return <TimeoutSpinner />

View File

@@ -2,7 +2,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { Label } from "react-aria-components"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
@@ -10,7 +10,6 @@ import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import {
BOOKING_CONFIRMATION_NUMBER,
BookingStatusEnum,
PAYMENT_METHOD_TITLES,
PaymentMethodEnum,
@@ -30,7 +29,6 @@ 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"
@@ -101,7 +99,7 @@ export default function PaymentClient({
(state) => state.actions.setIsSubmittingDisabled
)
const [bookingNumber, setBookingNumber] = useState<string>("")
const [refId, setRefId] = useState<string>("")
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
useState(false)
@@ -146,13 +144,14 @@ export default function PaymentClient({
return
}
const mainRoom = result.rooms[0]
if (result.reservationStatus == BookingStatusEnum.BookingCompleted) {
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${result.id}`
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${mainRoom.refId}`
router.push(confirmationUrl)
return
}
setBookingNumber(result.id)
setRefId(mainRoom.refId)
const hasPriceChange = result.rooms.some((r) => r.priceChangedMetadata)
if (hasPriceChange) {
@@ -174,8 +173,8 @@ export default function PaymentClient({
})
const priceChange = trpc.booking.priceChange.useMutation({
onSuccess: (result) => {
if (result?.id) {
onSuccess: (confirmationNumber) => {
if (confirmationNumber) {
setIsPollingForBookingStatus(true)
} else {
handlePaymentError("No confirmation number")
@@ -189,13 +188,39 @@ export default function PaymentClient({
},
})
const bookingStatus = useHandleBookingStatus({
confirmationNumber: bookingNumber,
expectedStatuses: [BookingStatusEnum.BookingCompleted],
maxRetries,
retryInterval,
enabled: isPollingForBookingStatus,
})
// 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 handlePaymentError = useCallback(
(errorMessage: string) => {
@@ -245,18 +270,12 @@ export default function PaymentClient({
)
useEffect(() => {
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) {
if (bookingStatus?.data?.redirectUrl) {
router.push(bookingStatus.data.redirectUrl)
} else if (retries.current >= maxRetries) {
handlePaymentError("Timeout")
}
}, [bookingStatus, router, intl, lang, handlePaymentError])
}, [bookingStatus, router, handlePaymentError])
useEffect(() => {
setIsSubmittingDisabled(
@@ -458,7 +477,7 @@ export default function PaymentClient({
initiateBooking.isPending ||
(isPollingForBookingStatus &&
!bookingStatus.data?.paymentUrl &&
!bookingStatus.isTimeout)
retries.current < maxRetries)
) {
return <LoadingSpinner />
}
@@ -620,9 +639,7 @@ export default function PaymentClient({
: ""
router.push(`${selectRate(lang)}${allSearchParams}`)
}}
onAccept={() =>
priceChange.mutate({ confirmationNumber: bookingNumber })
}
onAccept={() => priceChange.mutate({ refId })}
/>
) : null}
</section>

View File

@@ -18,10 +18,10 @@ import {
import styles from "./findMyBooking.module.css"
export default function AdditionalInfoForm({
confirmationNumber,
refId,
lastName,
}: {
confirmationNumber: string
refId: string
lastName: string
}) {
const router = useRouter()
@@ -37,7 +37,7 @@ export default function AdditionalInfoForm({
const values = form.getValues()
const value = new URLSearchParams({
...values,
confirmationNumber,
RefId: refId,
lastName,
}).toString()
document.cookie = `bv=${encodeURIComponent(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict`

View File

@@ -145,7 +145,7 @@ export default function AddAncillaryFlowModal({
) {
addAncillary.mutate(
{
confirmationNumber: booking.confirmationNumber,
refId,
ancillaryComment: data.optionalText,
ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime
? data.deliveryTime
@@ -175,8 +175,8 @@ export default function AddAncillaryFlowModal({
)
clearAncillarySessionData()
closeModal()
utils.booking.get.invalidate({
confirmationNumber: booking.confirmationNumber,
utils.booking.confirmation.invalidate({
refId: booking.refId,
})
router.refresh()
} else {
@@ -211,7 +211,7 @@ export default function AddAncillaryFlowModal({
}
: undefined
guaranteeBooking.mutate({
confirmationNumber: booking.confirmationNumber,
refId,
language: lang,
...(card && { card }),
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}&ancillary=1`,

View File

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

View File

@@ -25,6 +25,7 @@ import type {
export function AddedAncillaries({
ancillaries,
booking,
refId,
}: AddedAncillariesProps) {
const intl = useIntl()
const router = useRouter()
@@ -126,7 +127,7 @@ export function AddedAncillaries({
{booking.confirmationNumber && ancillary.code ? (
<div className={styles.actions}>
<RemoveButton
confirmationNumber={booking.confirmationNumber}
refId={refId}
codes={
ancillary.code ===
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
@@ -192,7 +193,7 @@ export function AddedAncillaries({
booking.canModifyAncillaries ? (
<div className={styles.actions}>
<RemoveButton
confirmationNumber={booking.confirmationNumber}
refId={refId}
codes={
ancillary.code ===
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST

View File

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

View File

@@ -213,7 +213,11 @@ export function Ancillaries({
</>
)}
<AddedAncillaries booking={booking} ancillaries={uniqueAncillaries} />
<AddedAncillaries
booking={booking}
ancillaries={uniqueAncillaries}
refId={refId}
/>
<AncillaryFlowModalWrapper>
<AddAncillaryFlowModal

View File

@@ -64,10 +64,10 @@ export default function Details({ booking, user }: DetailsProps) {
const updateGuest = trpc.booking.update.useMutation({
onMutate: () => setIsLoading(true),
onSuccess: (data) => {
if (data) {
utils.booking.get.invalidate({
confirmationNumber: data.confirmationNumber,
onSuccess: (refId) => {
if (refId) {
utils.booking.confirmation.invalidate({
refId,
})
toast.success(
@@ -99,7 +99,7 @@ export default function Details({ booking, user }: DetailsProps) {
async function onSubmit(data: ModifyContactSchema) {
updateGuest.mutate({
confirmationNumber: booking.confirmationNumber,
refId: booking.refId,
guest: {
email: data.email,
phoneNumber: data.phoneNumber,

View File

@@ -1,4 +1,5 @@
"use client"
import { useMyStayStore } from "@/stores/my-stay"
import Details from "./Details"

View File

@@ -1,169 +0,0 @@
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 { getIntl } from "@/i18n"
import { decrypt } from "@/utils/encryption"
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
// send refId here?
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()
}

View File

@@ -1,45 +0,0 @@
.main {
background-color: var(--Base-Surface-Primary-light-Normal);
display: flex;
padding: var(--Spacing-x5) var(--Spacing-x4);
flex-direction: column;
gap: var(--Spacing-x5);
}
.addresses {
display: flex;
justify-content: space-between;
margin-top: var(--Spacing-x2);
}
.rightColumn {
text-align: right;
}
.addressMargin {
margin-top: var(--Spacing-x-half);
}
.tertiary {
color: var(--Text-Tertiary);
}
.divider {
color: var(--Border-Divider-Accent);
}
.form {
max-width: 640px;
margin-left: auto;
margin-right: auto;
padding: var(--Spacing-x5) 0;
}
.logIn {
padding: var(--Spacing-x9) var(--Spacing-x2);
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
align-items: center;
color: var(--Scandic-Grey-100);
}

View File

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

View File

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

View File

@@ -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(confirmationNumber, false, hotelId)
useGuaranteeBooking(refId, false, hotelId)
if (isLoading) {
return (
@@ -85,7 +85,7 @@ export default function Form() {
: undefined
writeGlaToSessionStorage("yes", hotelId)
guaranteeBooking.mutate({
confirmationNumber,
refId,
language: lang,
...(card && { card }),
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`,

View File

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