diff --git a/packages/booking-flow/lib/components/BookingFlowInput/errors.ts b/packages/booking-flow/lib/components/BookingFlowInput/errors.ts index 150f432cd..1fc613c74 100644 --- a/packages/booking-flow/lib/components/BookingFlowInput/errors.ts +++ b/packages/booking-flow/lib/components/BookingFlowInput/errors.ts @@ -142,6 +142,11 @@ export function getErrorMessage( defaultMessage: "Membership number can't be the same for two different rooms", }) + + case undefined: + case null: + case "": + return errorCode default: logger.warn("Error code not supported:", errorCode) return errorCode diff --git a/packages/booking-flow/lib/components/EnterDetails/Payment/BookingAlert/index.tsx b/packages/booking-flow/lib/components/EnterDetails/Payment/BookingAlert/index.tsx index 55fc430af..d256179b4 100644 --- a/packages/booking-flow/lib/components/EnterDetails/Payment/BookingAlert/index.tsx +++ b/packages/booking-flow/lib/components/EnterDetails/Payment/BookingAlert/index.tsx @@ -1,17 +1,19 @@ "use client" import { usePathname, useSearchParams } from "next/navigation" -import { useEffect, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { useIntl } from "react-intl" import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert" import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation" import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition" import { Alert } from "@scandic-hotels/design-system/Alert" +import { trackNoAvailability } from "@scandic-hotels/tracking/NoAvailabilityTracking" import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode" import useLang from "../../../../hooks/useLang" import { useEnterDetailsStore } from "../../../../stores/enter-details" +import { mapPackageToLabel } from "../../../../utils/getRoomFeatureDescription" import styles from "./bookingAlert.module.css" @@ -19,6 +21,7 @@ function useBookingErrorAlert() { const updateSearchParams = useEnterDetailsStore( (state) => state.actions.updateSeachParamString ) + const intl = useIntl() const lang = useLang() const searchParams = useSearchParams() @@ -102,6 +105,12 @@ export default function BookingAlert({ isVisible = false }: BookingAlertProps) { selectRateReturnUrl, } = useBookingErrorAlert() + const trackNoAvailabilityError = useNoAvailabilityTracking() + + const isAvailabilityError = + errorCode === BookingErrorCodeEnum.AvailabilityError || + errorCode === BookingErrorCodeEnum.NoAvailabilityForRateAndRoomType + const ref = useRef(null) const { getTopOffset } = useStickyPosition() @@ -122,11 +131,13 @@ export default function BookingAlert({ isVisible = false }: BookingAlertProps) { } }, [showAlert, getTopOffset]) - if (!showAlert) return null + useEffect(() => { + if (showAlert && isAvailabilityError) { + trackNoAvailabilityError() + } + }, [showAlert, isAvailabilityError, trackNoAvailabilityError]) - const isAvailabilityError = - errorCode === BookingErrorCodeEnum.AvailabilityError || - errorCode === BookingErrorCodeEnum.NoAvailabilityForRateAndRoomType + if (!showAlert) return null return (
@@ -149,3 +160,48 @@ export default function BookingAlert({ isVisible = false }: BookingAlertProps) {
) } + +function useNoAvailabilityTracking() { + const { fromDate, toDate, hotelId, bookingCode, searchType, rooms } = + useEnterDetailsStore((state) => state.booking) + const lang = useLang() + + const specialRoomType = rooms + ?.map((room) => { + const packages = room.packages + ?.map((pkg) => mapPackageToLabel(pkg)) + .join(",") + return packages ?? "" + }) + .join("|") + + const track = useCallback( + () => + trackNoAvailability({ + specialRoomType, + lang, + searchTerm: hotelId, + fromDate, + toDate, + hotelId, + noOfRooms: rooms.length, + searchType, + bookingCode: bookingCode ?? "n/a", + pageId: "details", + pageName: "hotelreservation|details", + pageType: "bookingenterdetailspage", + siteSections: "hotelreservation|details", + }), + [ + specialRoomType, + lang, + hotelId, + fromDate, + toDate, + rooms.length, + searchType, + bookingCode, + ] + ) + return track +} diff --git a/packages/booking-flow/lib/components/SelectRate/AvailabilityError.tsx b/packages/booking-flow/lib/components/SelectRate/AvailabilityError.tsx index 8c27f4120..9328ef21b 100644 --- a/packages/booking-flow/lib/components/SelectRate/AvailabilityError.tsx +++ b/packages/booking-flow/lib/components/SelectRate/AvailabilityError.tsx @@ -1,16 +1,29 @@ "use client" import { usePathname, useSearchParams } from "next/navigation" -import { useEffect } from "react" +import { useCallback, useEffect } from "react" import { useIntl } from "react-intl" import { toast } from "@scandic-hotels/design-system/Toast" +import { trackNoAvailability } from "@scandic-hotels/tracking/NoAvailabilityTracking" import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode" -export default function AvailabilityError() { +import useLang from "../../hooks/useLang" +import { mapPackageToLabel } from "../../utils/getRoomFeatureDescription" + +import type { SelectRateBooking } from "../../types/components/selectRate/selectRate" + +type AvailabilityErrorProps = { + booking: SelectRateBooking +} + +export default function AvailabilityError({ booking }: AvailabilityErrorProps) { const intl = useIntl() const pathname = usePathname() const searchParams = useSearchParams() + const lang = useLang() + + const { rooms, fromDate, toDate, hotelId, bookingCode, searchType } = booking const errorCode = searchParams.get("errorCode") const hasAvailabilityError = @@ -21,15 +34,51 @@ export default function AvailabilityError() { "Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.", }) + const noAvailabilityTracking = useCallback(() => { + const specialRoomType = rooms + ?.map((room) => { + const packages = room.packages + ?.map((pkg) => { + mapPackageToLabel(pkg) + }) + .join(",") + + return packages ?? "" + }) + .join("|") + + trackNoAvailability({ + specialRoomType, + searchTerm: hotelId, + lang, + fromDate, + toDate, + noOfRooms: rooms.length, + searchType, + hotelId, + bookingCode, + pageId: "select-rate", + pageName: "hotelreservation|select-rate", + pageType: "bookingroomsandratespage", + siteSections: "hotelreservation|select-rate", + }) + }, [rooms, hotelId, lang, fromDate, toDate, searchType, bookingCode]) + useEffect(() => { if (hasAvailabilityError) { toast.error(errorMessage) - + noAvailabilityTracking() const newParams = new URLSearchParams(searchParams.toString()) newParams.delete("errorCode") window.history.replaceState({}, "", `${pathname}?${newParams.toString()}`) } - }, [errorMessage, hasAvailabilityError, pathname, searchParams]) + }, [ + errorMessage, + hasAvailabilityError, + noAvailabilityTracking, + pathname, + searchParams, + ]) return null } diff --git a/packages/booking-flow/lib/components/SelectRate/Tracking/SelectRateTracking.tsx b/packages/booking-flow/lib/components/SelectRate/Tracking/SelectRateTracking.tsx new file mode 100644 index 000000000..55652fe6b --- /dev/null +++ b/packages/booking-flow/lib/components/SelectRate/Tracking/SelectRateTracking.tsx @@ -0,0 +1,100 @@ +"use client" + +import { NoAvailabilityTracking } from "@scandic-hotels/tracking/NoAvailabilityTracking" +import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" +import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking" +import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel" + +import { useSelectRateContext } from "../../../contexts/SelectRate/SelectRateContext" +import useLang from "../../../hooks/useLang" +import { mapPackageToLabel } from "../../../utils/getRoomFeatureDescription" +import { getValidDates } from "../getValidDates" +import { getSelectRateTracking } from "./tracking" + +import type { RouterOutput } from "@scandic-hotels/trpc/client" + +import type { SelectRateBooking } from "../../../types/components/selectRate/selectRate" + +interface TrackingProps { + hotelData: NonNullable + booking: SelectRateBooking +} + +export function SelectRateTracking({ hotelData, booking }: TrackingProps) { + const lang = useLang() + const { availability, input } = useSelectRateContext() + + const { fromDate, toDate } = getValidDates(booking.fromDate, booking.toDate) + + const { rooms, searchType, bookingCode, city: paramCity } = booking + + const arrivalDate = fromDate.toDate() + const departureDate = toDate.toDate() + + const specialRoomType = input.data?.booking.rooms + ?.map((room) => { + const packages = room.packages + ?.map((pkg) => mapPackageToLabel(pkg)) + .join(",") + return packages ?? "" + }) + .join("|") + const { hotelsTrackingData, pageTrackingData } = getSelectRateTracking({ + lang, + arrivalDate, + departureDate, + hotelId: hotelData.hotel.id, + hotelName: hotelData.hotel.name, + country: hotelData.hotel.address.country, + hotelCity: hotelData.hotel.address.city, + paramCity, + bookingCode, + isRedemption: searchType === SEARCH_TYPE_REDEMPTION, + specialRoomType, + rooms, + }) + + let shouldTrackNoAvailability = false + if (availability && !availability.isFetching && availability.data) { + shouldTrackNoAvailability = availability.data.some((room) => { + if (!room || "error" in room) return false + + const allRoomsNotAvailable = room.roomConfigurations.every( + (roomConfig) => roomConfig.status === AvailabilityEnum.NotAvailable + ) + + const isPublicPromotionWithCode = room.roomConfigurations.some((r) => { + const filteredCampaigns = r.campaign.filter(Boolean) + return filteredCampaigns.length + ? filteredCampaigns.every( + (product) => !!product.rateDefinition?.isCampaignRate + ) + : false + }) + + const noAvailableBookingCodeRooms = + !isPublicPromotionWithCode && + room.roomConfigurations.every( + (r) => r.status === AvailabilityEnum.NotAvailable || !r.code.length + ) + + return ( + allRoomsNotAvailable || + (input.bookingCode && noAvailableBookingCodeRooms) + ) + }) + } + + return ( + <> + + + + + ) +} diff --git a/packages/booking-flow/lib/components/SelectRate/Tracking/tracking.ts b/packages/booking-flow/lib/components/SelectRate/Tracking/tracking.ts index 3e6e90845..1b3eb72df 100644 --- a/packages/booking-flow/lib/components/SelectRate/Tracking/tracking.ts +++ b/packages/booking-flow/lib/components/SelectRate/Tracking/tracking.ts @@ -8,7 +8,6 @@ import { type TrackingSDKPageData, } from "@scandic-hotels/tracking/types" import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum" -import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" import type { Lang } from "@scandic-hotels/common/constants/language" import type { Child } from "@scandic-hotels/trpc/types/child" @@ -28,6 +27,7 @@ type SelectRateTrackingInput = { paramCity: string | undefined bookingCode?: string isRedemption?: boolean + specialRoomType?: string rooms?: Room[] } @@ -42,6 +42,7 @@ export function getSelectRateTracking({ paramCity, bookingCode, isRedemption = false, + specialRoomType, rooms = [], }: SelectRateTrackingInput) { const pageTrackingData: TrackingSDKPageData = { @@ -83,25 +84,7 @@ export function getSelectRateTracking({ searchType: "hotel", bookingCode: bookingCode ?? "n/a", rewardNight: isRedemption ? "yes" : "no", - specialRoomType: rooms - ?.map((room) => { - const packages = room.packages - ?.map((pkg) => { - if (pkg === RoomPackageCodeEnum.ACCESSIBILITY_ROOM) { - return "accessibility" - } else if (pkg === RoomPackageCodeEnum.ALLERGY_ROOM) { - return "allergy friendly" - } else if (pkg === RoomPackageCodeEnum.PET_ROOM) { - return "pet room" - } else { - return "" - } - }) - .join(",") - - return packages ?? "" - }) - .join("|"), + specialRoomType, } return { diff --git a/packages/booking-flow/lib/components/SelectRate/index.tsx b/packages/booking-flow/lib/components/SelectRate/index.tsx index 0bc4ffb70..1e236715b 100644 --- a/packages/booking-flow/lib/components/SelectRate/index.tsx +++ b/packages/booking-flow/lib/components/SelectRate/index.tsx @@ -87,7 +87,7 @@ export async function SelectRate({ /> )} - + ) } diff --git a/packages/booking-flow/lib/pages/AlternativeHotelsPage.tsx b/packages/booking-flow/lib/pages/AlternativeHotelsPage.tsx index aaf0acd0a..6304a9fa3 100644 --- a/packages/booking-flow/lib/pages/AlternativeHotelsPage.tsx +++ b/packages/booking-flow/lib/pages/AlternativeHotelsPage.tsx @@ -4,6 +4,7 @@ import { notFound } from "next/navigation" import { Suspense } from "react" import { FamilyAndFriendsCodes } from "@scandic-hotels/common/constants/familyAndFriends" +import { NoAvailabilityTracking } from "@scandic-hotels/tracking/NoAvailabilityTracking" import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" import { AlternativeHotelsPageTitle } from "../components/AlternativeHotelsPageTitle" @@ -110,6 +111,10 @@ export async function AlternativeHotelsPage({ const suspenseKey = stringify(searchParams) + const shouldTrackNoAvailability = !!( + hotels.every((hotel) => hotel.availability.status !== "Available") || + (booking.bookingCode && hotels.length > 0 && !isBookingCodeRateAvailable) + ) return ( <> + ) diff --git a/packages/booking-flow/lib/pages/SelectHotelPage.tsx b/packages/booking-flow/lib/pages/SelectHotelPage.tsx index d203dff8f..f006d4bda 100644 --- a/packages/booking-flow/lib/pages/SelectHotelPage.tsx +++ b/packages/booking-flow/lib/pages/SelectHotelPage.tsx @@ -4,6 +4,7 @@ import { notFound } from "next/navigation" import { Suspense } from "react" import { FamilyAndFriendsCodes } from "@scandic-hotels/common/constants/familyAndFriends" +import { NoAvailabilityTracking } from "@scandic-hotels/tracking/NoAvailabilityTracking" import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" import FnFNotAllowedAlert from "../components/FnFNotAllowedAlert" @@ -85,7 +86,7 @@ export async function SelectHotelPage({ arrivalDate, departureDate, hotelsResult: hotels?.length ?? 0, - searchTerm: booking.hotelId, + searchTerm: city.name, country: hotels?.[0]?.hotel.address.country, hotelCity: hotels?.[0]?.hotel.address.city, bookingCode: booking.bookingCode, @@ -96,6 +97,11 @@ export async function SelectHotelPage({ const suspenseKey = stringify(searchParams) + const shouldTrackNoAvailability = !!( + hotels.every((hotel) => hotel.availability.status !== "Available") || + (booking.bookingCode && hotels.length > 0 && !isBookingCodeRateAvailable) + ) + return ( <> + ) diff --git a/packages/booking-flow/lib/pages/SelectRatePage.tsx b/packages/booking-flow/lib/pages/SelectRatePage.tsx index 6cb9a7a54..71b09adf9 100644 --- a/packages/booking-flow/lib/pages/SelectRatePage.tsx +++ b/packages/booking-flow/lib/pages/SelectRatePage.tsx @@ -1,12 +1,10 @@ import { notFound } from "next/navigation" import { logger } from "@scandic-hotels/common/logger" -import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking" import { SelectRate } from "../components/SelectRate" -import { getValidDates } from "../components/SelectRate/getValidDates" -import { getSelectRateTracking } from "../components/SelectRate/Tracking/tracking" +import { SelectRateTracking } from "../components/SelectRate/Tracking/SelectRateTracking" import { SelectRateProvider } from "../contexts/SelectRate/SelectRateContext" import { getHotel } from "../trpc/memoizedRequests" import { parseSelectRateSearchParams } from "../utils/url" @@ -72,33 +70,12 @@ export async function SelectRatePage({ notFound() } - const { fromDate, toDate } = getValidDates(booking.fromDate, booking.toDate) - - const { rooms, searchType, bookingCode, city: paramCity } = booking - - const arrivalDate = fromDate.toDate() - const departureDate = toDate.toDate() - - const { hotelsTrackingData, pageTrackingData } = getSelectRateTracking({ - lang, - arrivalDate, - departureDate, - hotelId: hotelData.hotel.id, - hotelName: hotelData.hotel.name, - country: hotelData.hotel.address.country, - hotelCity: hotelData.hotel.address.city, - paramCity, - bookingCode, - isRedemption: searchType === SEARCH_TYPE_REDEMPTION, - rooms, - }) - return ( <> + - ) } diff --git a/packages/booking-flow/lib/utils/getRoomFeatureDescription.ts b/packages/booking-flow/lib/utils/getRoomFeatureDescription.ts index f1dfcf0b4..197681b17 100644 --- a/packages/booking-flow/lib/utils/getRoomFeatureDescription.ts +++ b/packages/booking-flow/lib/utils/getRoomFeatureDescription.ts @@ -1,5 +1,6 @@ import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" +import type { PackageEnum } from "@scandic-hotels/trpc/types/packages" import type { IntlShape } from "react-intl" export function getRoomFeatureDescription( @@ -21,3 +22,16 @@ export function getRoomFeatureDescription( return roomFeatureDescriptions[code] ?? description } + +export function mapPackageToLabel(pkgCode: PackageEnum): string { + switch (pkgCode) { + case RoomPackageCodeEnum.ACCESSIBILITY_ROOM: + return "accessibility" + case RoomPackageCodeEnum.ALLERGY_ROOM: + return "allergy friendly" + case RoomPackageCodeEnum.PET_ROOM: + return "pet room" + default: + return "" + } +} diff --git a/packages/tracking/lib/NoAvailabilityTracking.tsx b/packages/tracking/lib/NoAvailabilityTracking.tsx new file mode 100644 index 000000000..165ec4736 --- /dev/null +++ b/packages/tracking/lib/NoAvailabilityTracking.tsx @@ -0,0 +1,128 @@ +"use client" + +import { differenceInCalendarDays, isWeekend } from "date-fns" +import { useEffect } from "react" + +import { + TrackingChannelEnum, + type TrackingSDKHotelInfo, + type TrackingSDKPageData, +} from "@scandic-hotels/tracking/types" +import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking" + +import { trackEvent } from "./base" + +import type { Lang } from "@scandic-hotels/common/constants/language" + +type NoAvailabilityTrackingProps = { + lang: Lang + shouldTrackNoAvailability: boolean + hotelsTrackingData: TrackingSDKHotelInfo + pageTrackingData: TrackingSDKPageData +} + +export function NoAvailabilityTracking({ + lang, + shouldTrackNoAvailability, + hotelsTrackingData, + pageTrackingData, +}: NoAvailabilityTrackingProps) { + useEffect(() => { + if (!shouldTrackNoAvailability) { + return + } + const { + searchTerm, + searchType, + hotelID, + leadTime, + noOfRooms, + bookingCode, + rewardNight, + bookingTypeofDay, + duration, + specialRoomType, + } = hotelsTrackingData + + trackEvent({ + event: "noRoomsAvailable", + hotelInfo: { + searchTerm, + searchType, + noRoomsAvailable: "yes", + hotelID, + leadTime, + noOfRooms, + bookingCode, + rewardNight, + bookingTypeofDay, + duration, + specialRoomType, + }, + pageInfo: pageTrackingData, + }) + }, [lang, hotelsTrackingData, pageTrackingData]) + + return null +} + +type TrackNoAvailabilityParams = { + specialRoomType: string + fromDate: string + toDate: string + hotelId: string + noOfRooms: number + searchType?: string + bookingCode?: string + searchTerm: string + pageId: string + pageName: string + pageType: string + siteSections: string + lang: Lang +} + +export function trackNoAvailability({ + specialRoomType, + lang, + fromDate, + toDate, + hotelId, + noOfRooms, + searchType, + bookingCode, + searchTerm, + pageId, + pageName, + pageType, + siteSections, +}: TrackNoAvailabilityParams) { + const arrivalDate = new Date(fromDate) + const departureDate = new Date(toDate) + + trackEvent({ + event: "noRoomsAvailable", + hotelInfo: { + searchTerm, + searchType, + noRoomsAvailable: "yes", + hotelId, + leadTime: differenceInCalendarDays(arrivalDate, new Date()), + noOfRooms, + bookingCode: bookingCode ?? "n/a", + rewardNight: searchType === SEARCH_TYPE_REDEMPTION ? "yes" : "no", + bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", + duration: differenceInCalendarDays(departureDate, arrivalDate), + specialRoomType, + }, + pageInfo: { + channel: TrackingChannelEnum.hotelreservation, + domain: "www.scandichotels.com", + domainLanguage: lang, + pageId, + pageName, + pageType, + siteSections, + }, + }) +} diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 62498ff3c..c7cd002ba 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -12,7 +12,8 @@ }, "exports": { "./*": "./lib/*.ts", - "./TrackingSDK": "./lib/TrackingSDK.tsx" + "./TrackingSDK": "./lib/TrackingSDK.tsx", + "./NoAvailabilityTracking": "./lib/NoAvailabilityTracking.tsx" }, "dependencies": { "@scandic-hotels/common": "workspace:*",