diff --git a/README.md b/README.md
index aee9512e4..f4adfa9f1 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Inside the `/packages` directory, you'll find our shared libraries and utilities
> A note about dependencies between our apps and packages:
> In general all apps are allowed to depend on any package, but packages have a few caveats:
->
+>
> `design-system` should never import from `booking-flow` or `trpc`.
> `common` should never import from anything except `typescript-config`.
@@ -48,4 +48,5 @@ For more details see the respective apps and packages' README files.
## More documentation
- [Icons](./docs/icons.md)
-- [Translations (i18n)](./docs/translations.md)
\ No newline at end of file
+- [Payment](./docs/payment.md)
+- [Translations (i18n)](./docs/translations.md)
diff --git a/apps/partner-sas/app/[lang]/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/apps/partner-sas/app/[lang]/hotelreservation/(confirmation)/booking-confirmation/page.tsx
index cef2be4c8..54f0f1650 100644
--- a/apps/partner-sas/app/[lang]/hotelreservation/(confirmation)/booking-confirmation/page.tsx
+++ b/apps/partner-sas/app/[lang]/hotelreservation/(confirmation)/booking-confirmation/page.tsx
@@ -1,4 +1,22 @@
-export default async function BookingConfirmationPage() {
- // eslint-disable-next-line formatjs/no-literal-string-in-jsx
- return
booking-confirmation
+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
+) {
+ const searchParams = await props.searchParams
+ const lang = await getLang()
+ const intl = await getIntl()
+
+ return (
+
+ )
}
diff --git a/apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/layout.module.css b/apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/[status]/layout.module.css
similarity index 100%
rename from apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/layout.module.css
rename to apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/[status]/layout.module.css
diff --git a/apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/layout.tsx b/apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/[status]/layout.tsx
similarity index 100%
rename from apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/layout.tsx
rename to apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/[status]/layout.tsx
diff --git a/apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/page.tsx b/apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/[status]/page.tsx
similarity index 73%
rename from apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/page.tsx
rename to apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/[status]/page.tsx
index c8f0a8f00..1eb193a27 100644
--- a/apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/page.tsx
+++ b/apps/partner-sas/app/[lang]/hotelreservation/(payment-callback)/payment-callback/[status]/page.tsx
@@ -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) {
+export default async function PaymentCallbackPage(
+ props: PageArgs
+) {
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) {
lang={lang}
userAccessToken={userAccessToken}
searchParams={searchParams}
+ status={params.status as PaymentCallbackStatusEnum}
/>
)
}
diff --git a/apps/partner-sas/package.json b/apps/partner-sas/package.json
index 43e8cc90e..a3fc1afda 100644
--- a/apps/partner-sas/package.json
+++ b/apps/partner-sas/package.json
@@ -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",
diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx
index ec34c7f5d..b772d349d 100644
--- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx
@@ -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) => (
-
- )}
/>
)
}
diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx
index ab4d47d21..45daba87f 100644
--- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx
@@ -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}
/>
)
}
diff --git a/docs/payment.md b/docs/payment.md
new file mode 100644
index 000000000..104b7d7fe
--- /dev/null
+++ b/docs/payment.md
@@ -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
diff --git a/packages/booking-flow/lib/components/BookingConfirmation/Tracking/index.tsx b/packages/booking-flow/lib/components/BookingConfirmation/Tracking/index.tsx
new file mode 100644
index 000000000..526fc64e4
--- /dev/null
+++ b/packages/booking-flow/lib/components/BookingConfirmation/Tracking/index.tsx
@@ -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 (
+
+ )
+}
diff --git a/packages/booking-flow/lib/components/BookingConfirmation/Tracking/tracking.ts b/packages/booking-flow/lib/components/BookingConfirmation/Tracking/tracking.ts
new file mode 100644
index 000000000..5175a2dd2
--- /dev/null
+++ b/packages/booking-flow/lib/components/BookingConfirmation/Tracking/tracking.ts
@@ -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")
+}
diff --git a/packages/booking-flow/lib/components/BookingConfirmation/index.tsx b/packages/booking-flow/lib/components/BookingConfirmation/index.tsx
index 0ed523abd..b3834f0d3 100644
--- a/packages/booking-flow/lib/components/BookingConfirmation/index.tsx
+++ b/packages/booking-flow/lib/components/BookingConfirmation/index.tsx
@@ -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({
- {renderTracking({ bookingConfirmation, refId })}
+
)
}
diff --git a/packages/booking-flow/lib/pages/BookingConfirmationPage.tsx b/packages/booking-flow/lib/pages/BookingConfirmationPage.tsx
index 8bfb5d7a9..8c7ba2e2f 100644
--- a/packages/booking-flow/lib/pages/BookingConfirmationPage.tsx
+++ b/packages/booking-flow/lib/pages/BookingConfirmationPage.tsx
@@ -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}
/>
)
}
diff --git a/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx b/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx
index 709b83dc9..6f3f1eccd 100644
--- a/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx
+++ b/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx
@@ -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({
)
diff --git a/packages/booking-flow/package.json b/packages/booking-flow/package.json
index c1431bdc6..e4453ff4b 100644
--- a/packages/booking-flow/package.json
+++ b/packages/booking-flow/package.json
@@ -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",