Merged in SW-3459-setup-booking-confirmation-page-in-sas (pull request #2794)
Setup booking-confirmation page in SAS * Setup booking-confirmation page in SAS move booking-confirmation tracking to booking-flow * remove unused param * Add test cards to documentation * Fix payment callback page to use correct status Approved-by: Anton Gunnarsson Approved-by: Hrishikesh Vaipurkar
This commit is contained in:
@@ -48,4 +48,5 @@ For more details see the respective apps and packages' README files.
|
||||
## More documentation
|
||||
|
||||
- [Icons](./docs/icons.md)
|
||||
- [Payment](./docs/payment.md)
|
||||
- [Translations (i18n)](./docs/translations.md)
|
||||
@@ -1,4 +1,22 @@
|
||||
export default async function BookingConfirmationPage() {
|
||||
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
return <div>booking-confirmation</div>
|
||||
import { BookingConfirmationPage as BookingConfirmationPagePrimitive } from "@scandic-hotels/booking-flow/pages/BookingConfirmationPage"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function BookingConfirmationPage(
|
||||
props: PageArgs<LangParams>
|
||||
) {
|
||||
const searchParams = await props.searchParams
|
||||
const lang = await getLang()
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<BookingConfirmationPagePrimitive
|
||||
intl={intl}
|
||||
lang={lang}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { PaymentCallbackPage as PaymentCallbackPagePrimitive } from "@scandic-hotels/booking-flow/pages/PaymentCallbackPage"
|
||||
import { logger } from "@scandic-hotels/common/logger"
|
||||
|
||||
import type { PaymentCallbackStatusEnum } from "@scandic-hotels/common/constants/paymentCallbackStatusEnum"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function PaymentCallbackPage(props: PageArgs<LangParams>) {
|
||||
export default async function PaymentCallbackPage(
|
||||
props: PageArgs<LangParams & { status: string }>
|
||||
) {
|
||||
const searchParams = await props.searchParams
|
||||
const params = await props.params
|
||||
logger.debug(`[payment-callback] callback started`)
|
||||
@@ -21,6 +25,7 @@ export default async function PaymentCallbackPage(props: PageArgs<LangParams>) {
|
||||
lang={lang}
|
||||
userAccessToken={userAccessToken}
|
||||
searchParams={searchParams}
|
||||
status={params.status as PaymentCallbackStatusEnum}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "PORT=3001 NEXT_PUBLIC_PORT=3001 next dev",
|
||||
"dev": "NODE_OPTIONS=--openssl-legacy-provider PORT=3001 NEXT_PUBLIC_PORT=3001 next dev",
|
||||
"build": "next build",
|
||||
"start": "node .next/standalone/server.js",
|
||||
"lint": "next lint --max-warnings 0 && tsc --noEmit",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BookingConfirmationPage as BookingConfirmationPagePrimitive } from "@scandic-hotels/booking-flow/pages/BookingConfirmationPage"
|
||||
|
||||
import Tracking from "@/components/HotelReservation/BookingConfirmation/Tracking"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
@@ -18,12 +17,6 @@ export default async function BookingConfirmationPage(
|
||||
intl={intl}
|
||||
lang={lang}
|
||||
searchParams={searchParams}
|
||||
renderTracking={(props) => (
|
||||
<Tracking
|
||||
bookingConfirmation={props.bookingConfirmation}
|
||||
refId={props.refId}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ export default async function PaymentCallbackPage(
|
||||
lang={lang}
|
||||
userAccessToken={userAccessToken}
|
||||
searchParams={searchParams}
|
||||
// TODO refactor this route to get this from params instead of rewriting in next.config
|
||||
status={searchParams.status as PaymentCallbackStatusEnum}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
13
docs/payment.md
Normal file
13
docs/payment.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Payment
|
||||
|
||||
## Testing
|
||||
|
||||
| Type | Card number | CVV |
|
||||
| ---- | --------------------- | --- |
|
||||
| Visa | `4111 1111 1111 1111` | 123 |
|
||||
| Visa | `4242 4242 4242 4242` | 123 |
|
||||
|
||||
- Expiration date needs to be in the future
|
||||
- For OTP
|
||||
- ✅ 4000 for success
|
||||
- ❌ 4001 for failure
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
||||
|
||||
import { clearPaymentInfoSessionStorage } from "../../../components/EnterDetails/Payment/helpers"
|
||||
import useLang from "../../../hooks/useLang"
|
||||
import { useSearchHistory } from "../../../hooks/useSearchHistory"
|
||||
import { useBookingConfirmationStore } from "../../../stores/booking-confirmation"
|
||||
import { getTracking } from "./tracking"
|
||||
|
||||
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
|
||||
|
||||
import type { Room } from "../../../types/stores/booking-confirmation"
|
||||
|
||||
export default function BookingConfirmationTracking({
|
||||
bookingConfirmation,
|
||||
refId,
|
||||
}: {
|
||||
bookingConfirmation: BookingConfirmation
|
||||
refId: string
|
||||
}) {
|
||||
const lang = useLang()
|
||||
const bookingRooms = useBookingConfirmationStore((state) => state.rooms)
|
||||
|
||||
const [loadedBookingConfirmationRefId] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return sessionStorage.getItem("loadedBookingConfirmationRefId")
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem("loadedBookingConfirmationRefId", refId)
|
||||
}, [refId])
|
||||
|
||||
const searchHistory = useSearchHistory()
|
||||
const searchTerm = searchHistory.searchHistory[0]?.name
|
||||
|
||||
let trackingData = null
|
||||
|
||||
if (bookingRooms.every(Boolean)) {
|
||||
const rooms = bookingRooms.filter((room): room is Room => !!room)
|
||||
trackingData = getTracking(
|
||||
lang,
|
||||
bookingConfirmation.booking,
|
||||
bookingConfirmation.hotel,
|
||||
rooms,
|
||||
searchTerm
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (trackingData?.paymentInfo) {
|
||||
clearPaymentInfoSessionStorage()
|
||||
}
|
||||
}, [trackingData])
|
||||
|
||||
if (!trackingData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { hotelsTrackingData, pageTrackingData, paymentInfo, ancillaries } =
|
||||
trackingData
|
||||
|
||||
return (
|
||||
<TrackingSDK
|
||||
pageData={pageTrackingData}
|
||||
hotelInfo={
|
||||
loadedBookingConfirmationRefId === refId
|
||||
? undefined
|
||||
: hotelsTrackingData
|
||||
}
|
||||
paymentInfo={
|
||||
loadedBookingConfirmationRefId === refId ? undefined : paymentInfo
|
||||
}
|
||||
ancillaries={
|
||||
loadedBookingConfirmationRefId === refId ? undefined : ancillaries
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import { createHash } from "crypto"
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
|
||||
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { RateEnum } from "@scandic-hotels/common/constants/rate"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKAncillaries,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
type TrackingSDKPaymentInfo,
|
||||
} from "@scandic-hotels/tracking/types"
|
||||
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
|
||||
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
||||
|
||||
import { readPaymentInfoFromSessionStorage } from "../../../components/EnterDetails/Payment/helpers"
|
||||
import { invertedBedTypeMap } from "../../../utils/SelectRate"
|
||||
import { getSpecialRoomType } from "../../../utils/specialRoomType"
|
||||
|
||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
|
||||
import type { RateDefinition } from "@scandic-hotels/trpc/types/roomAvailability"
|
||||
|
||||
import type { Room } from "../../../types/stores/booking-confirmation"
|
||||
|
||||
function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) {
|
||||
switch (cancellationRule) {
|
||||
case "CancellableBefore6PM":
|
||||
return RateEnum.flex
|
||||
case "Changeable":
|
||||
return RateEnum.change
|
||||
case "NotCancellable":
|
||||
return RateEnum.save
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
function mapAncillaryPackage(
|
||||
ancillaryPackage: BookingConfirmation["booking"]["packages"][number],
|
||||
operaId: string
|
||||
) {
|
||||
const isPoints = ancillaryPackage.currency === CurrencyEnum.POINTS
|
||||
return {
|
||||
hotelid: operaId,
|
||||
productCategory: "", // TODO: Add category
|
||||
productId: ancillaryPackage.code,
|
||||
productName: ancillaryPackage.description,
|
||||
productPoints: isPoints ? ancillaryPackage.totalPrice : 0,
|
||||
productPrice: isPoints ? 0 : ancillaryPackage.totalPrice,
|
||||
productType:
|
||||
ancillaryPackage.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
? "food"
|
||||
: "room preference",
|
||||
productUnits: ancillaryPackage.totalUnit,
|
||||
productDeliveryTime: "",
|
||||
}
|
||||
}
|
||||
|
||||
export function getTracking(
|
||||
lang: Lang,
|
||||
booking: BookingConfirmation["booking"],
|
||||
hotel: BookingConfirmation["hotel"],
|
||||
rooms: Room[],
|
||||
searchTerm?: string
|
||||
) {
|
||||
const arrivalDate = new Date(booking.checkInDate)
|
||||
const departureDate = new Date(booking.checkOutDate)
|
||||
const paymentInfoSessionData = readPaymentInfoFromSessionStorage()
|
||||
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
channel: TrackingChannelEnum.hotelreservation,
|
||||
domainLanguage: lang,
|
||||
pageId: "booking-confirmation",
|
||||
pageName: `hotelreservation|confirmation`,
|
||||
pageType: "confirmation",
|
||||
siteSections: `hotelreservation|confirmation`,
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const noOfAdults = rooms.map((r) => r.adults).join(",")
|
||||
const noOfChildren = rooms.map((r) => r.childrenAges?.length ?? 0).join(",")
|
||||
const noOfRooms = rooms.length
|
||||
const isFlexBooking =
|
||||
booking.rateDefinition.cancellationRule ===
|
||||
CancellationRuleEnum.CancellableBefore6PM
|
||||
const isGuaranteedFlexBooking = booking.guaranteeInfo && isFlexBooking
|
||||
|
||||
const ancillaries: TrackingSDKAncillaries = rooms
|
||||
.flatMap((r) => r.packages)
|
||||
.filter(
|
||||
(p) =>
|
||||
p.code === RoomPackageCodeEnum.PET_ROOM ||
|
||||
p.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
.map((pkg) => mapAncillaryPackage(pkg, hotel.operaId))
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
ageOfChildren: rooms.map((r) => r.childrenAges?.join(",") ?? "").join("|"),
|
||||
analyticsRateCode: rooms
|
||||
.map((r) => getRate(r.rateDefinition.cancellationRule))
|
||||
.join("|"),
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
bedType: rooms
|
||||
.map((r) => r.bedType)
|
||||
.join(",")
|
||||
.toLowerCase(),
|
||||
bnr: rooms.map((r) => r.confirmationNumber).join(","),
|
||||
bookingCode: rooms.map((room) => room.bookingCode ?? "n/a").join(", "),
|
||||
bookingCodeAvailability: booking.bookingCode
|
||||
? rooms.map((room) => (room.bookingCode ? "true" : "false")).join(", ")
|
||||
: undefined,
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
breakfastOption: rooms
|
||||
.map((r) => {
|
||||
if (r.breakfastIncluded || r.breakfast) {
|
||||
return "breakfast buffet"
|
||||
}
|
||||
return "no breakfast"
|
||||
})
|
||||
.join(","),
|
||||
childBedPreference: rooms
|
||||
.map(
|
||||
(r) =>
|
||||
r.childBedPreferences
|
||||
.map((cbp) =>
|
||||
Array(cbp.quantity).fill(invertedBedTypeMap[cbp.bedType])
|
||||
)
|
||||
.join(",") ?? ""
|
||||
)
|
||||
.join("|"),
|
||||
country: hotel?.address.country,
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
hotelID: hotel.operaId,
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
noOfAdults,
|
||||
noOfChildren,
|
||||
noOfRooms,
|
||||
rateCode: rooms.map((r) => r.rateDefinition.rateCode).join(","),
|
||||
rateCodeCancellationRule: rooms
|
||||
.map((r) => r.rateDefinition.cancellationRule)
|
||||
.join(",")
|
||||
.toLowerCase(),
|
||||
rateCodeName: rooms.map(constructRateCodeName).join(","),
|
||||
rateCodeType: rooms.map((r) => r.rateCodeType?.toLowerCase()).join(","),
|
||||
region: hotel?.address.city,
|
||||
revenueCurrencyCode: [...new Set(rooms.map((r) => r.currencyCode))].join(
|
||||
","
|
||||
),
|
||||
rewardNight: booking.roomPoints > 0 ? "yes" : "no",
|
||||
rewardNightAvailability: booking.roomPoints > 0 ? "true" : "false",
|
||||
points: booking.roomPoints > 0 ? booking.roomPoints : undefined,
|
||||
roomPrice: rooms.reduce((total, room) => total + room.roomPrice, 0),
|
||||
roomTypeCode: rooms.map((r) => r.roomTypeCode ?? "").join(","),
|
||||
searchTerm,
|
||||
searchType: "hotel",
|
||||
specialRoomType: rooms
|
||||
.map((room) => getSpecialRoomType(room.packages))
|
||||
.join(","),
|
||||
totalPrice: rooms.reduce((total, room) => total + room.totalPrice, 0),
|
||||
lateArrivalGuarantee: booking.rateDefinition.mustBeGuaranteed
|
||||
? "mandatory"
|
||||
: isFlexBooking
|
||||
? booking.guaranteeInfo
|
||||
? "yes"
|
||||
: "no"
|
||||
: "na",
|
||||
guaranteedProduct: isGuaranteedFlexBooking ? "room" : "na",
|
||||
emailId: getSHAHash(booking.guest.email),
|
||||
mobileNumber: getSHAHash(booking.guest.phoneNumber),
|
||||
}
|
||||
|
||||
const paymentInfo: TrackingSDKPaymentInfo = {
|
||||
paymentStatus: isGuaranteedFlexBooking
|
||||
? "glacardsaveconfirmed"
|
||||
: "confirmed",
|
||||
type:
|
||||
booking.guaranteeInfo?.cardType ?? paymentInfoSessionData?.paymentMethod,
|
||||
}
|
||||
|
||||
return {
|
||||
hotelsTrackingData,
|
||||
pageTrackingData,
|
||||
paymentInfo,
|
||||
ancillaries,
|
||||
}
|
||||
}
|
||||
|
||||
function constructRateCodeName(room: Room) {
|
||||
if (room.cheques) {
|
||||
return "corporate cheque"
|
||||
} else if (room.vouchers) {
|
||||
return "voucher"
|
||||
} else if (room.roomPoints) {
|
||||
return "redemption"
|
||||
}
|
||||
|
||||
const rate = getRate(room.rateDefinition.cancellationRule)
|
||||
|
||||
const bookingCodeStr = room.bookingCode ? room.bookingCode.toUpperCase() : ""
|
||||
|
||||
const breakfastIncludedStr = room.breakfastIncluded
|
||||
? "incl. breakfast"
|
||||
: "excl. breakfast"
|
||||
|
||||
return [bookingCodeStr, rate, breakfastIncludedStr]
|
||||
.filter(Boolean)
|
||||
.join(" - ")
|
||||
}
|
||||
|
||||
function getSHAHash(key: string) {
|
||||
return createHash("sha256").update(key).digest("hex")
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { PaymentDetails } from "./PaymentDetails"
|
||||
import { Promos } from "./Promos"
|
||||
import { Receipt } from "./Receipt"
|
||||
import { Rooms } from "./Rooms"
|
||||
import BookingConfirmationTracking from "./Tracking"
|
||||
import { mapRoomState } from "./utils"
|
||||
|
||||
import styles from "./bookingConfirmation.module.css"
|
||||
@@ -26,17 +27,12 @@ type BookingConfirmationProps = {
|
||||
intl: IntlShape
|
||||
refId: string
|
||||
membershipFailedError: boolean
|
||||
renderTracking: (trackingProps: {
|
||||
bookingConfirmation: BookingConfirmation
|
||||
refId: string
|
||||
}) => React.ReactNode
|
||||
}
|
||||
|
||||
export async function BookingConfirmation({
|
||||
intl,
|
||||
refId,
|
||||
membershipFailedError,
|
||||
renderTracking,
|
||||
}: BookingConfirmationProps) {
|
||||
const bookingConfirmation = await getBookingConfirmation(refId)
|
||||
|
||||
@@ -112,7 +108,10 @@ export async function BookingConfirmation({
|
||||
</aside>
|
||||
</Confirmation>
|
||||
|
||||
{renderTracking({ bookingConfirmation, refId })}
|
||||
<BookingConfirmationTracking
|
||||
bookingConfirmation={bookingConfirmation}
|
||||
refId={refId}
|
||||
/>
|
||||
</BookingConfirmationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { getBookingConfirmation } from "../trpc/memoizedRequests/getBookingConfi
|
||||
import { MEMBERSHIP_FAILED_ERROR } from "../types/membershipFailedError"
|
||||
|
||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||
import type { BookingConfirmation as BookingConfirmationType } from "@scandic-hotels/trpc/types/bookingConfirmation"
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
import type { NextSearchParams } from "../types"
|
||||
@@ -17,15 +16,10 @@ export async function BookingConfirmationPage({
|
||||
intl,
|
||||
lang,
|
||||
searchParams,
|
||||
renderTracking,
|
||||
}: {
|
||||
intl: IntlShape
|
||||
lang: Lang
|
||||
searchParams: NextSearchParams
|
||||
renderTracking: (trackingProps: {
|
||||
bookingConfirmation: BookingConfirmationType
|
||||
refId: string
|
||||
}) => React.ReactNode
|
||||
}) {
|
||||
const refId = searchParams.RefId?.toString()
|
||||
|
||||
@@ -56,7 +50,6 @@ export async function BookingConfirmationPage({
|
||||
intl={intl}
|
||||
refId={refId}
|
||||
membershipFailedError={membershipFailedError}
|
||||
renderTracking={renderTracking}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,19 +23,20 @@ type PaymentCallbackPageProps = {
|
||||
lang: Lang
|
||||
searchParams: NextSearchParams
|
||||
userAccessToken: string | null
|
||||
status: PaymentCallbackStatusEnum
|
||||
}
|
||||
export async function PaymentCallbackPage({
|
||||
lang,
|
||||
userAccessToken,
|
||||
searchParams,
|
||||
status,
|
||||
}: PaymentCallbackPageProps) {
|
||||
const { status, confirmationNumber } = searchParams
|
||||
const { confirmationNumber } = searchParams
|
||||
|
||||
if (
|
||||
!status ||
|
||||
!confirmationNumber ||
|
||||
typeof confirmationNumber !== "string" ||
|
||||
typeof status !== "string"
|
||||
typeof confirmationNumber !== "string"
|
||||
) {
|
||||
logger.error(
|
||||
`[payment-callback] missing status or confirmationNumber in search params`
|
||||
@@ -143,8 +144,7 @@ export async function PaymentCallbackPage({
|
||||
<HandleErrorCallback
|
||||
returnUrl={returnUrl.toString()}
|
||||
searchObject={searchObject}
|
||||
// TODO we should parse instead of cast
|
||||
status={status as PaymentCallbackStatusEnum}
|
||||
status={status}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"./stores/enter-details": "./lib/stores/enter-details/index.ts",
|
||||
"./stores/enter-details/types": "./lib/stores/enter-details/types.ts",
|
||||
"./stores/hotels-map": "./lib/stores/hotels-map.ts",
|
||||
"./stores/booking-confirmation": "./lib/stores/booking-confirmation/index.ts",
|
||||
"./types/components/bookingConfirmation/bookingConfirmation": "./lib/types/components/bookingConfirmation/bookingConfirmation.ts",
|
||||
"./types/components/findMyBooking/additionalInfoCookieValue": "./lib/types/components/findMyBooking/additionalInfoCookieValue.ts",
|
||||
"./types/components/promo/promoProps": "./lib/types/components/promo/promoProps.ts",
|
||||
|
||||
Reference in New Issue
Block a user