Merged in feat/BOOK-131-tracking-no-availability (pull request #2886)

feat(BOOK-131): add no availability tracking

* feat(BOOK-131): add no availability tracking

* feat(BOOK-131): add no availability tracking

* feat(BOOK-131): extract noAvailability function

* feat(BOOK-131): fix every render problem

* feat(BOOK-131): noavailability handle return in function


Approved-by: Erik Tiekstra
Approved-by: Joakim Jäderberg
This commit is contained in:
Bianca Widstam
2025-10-07 06:59:49 +00:00
parent 973a665aba
commit 30b214c6ff
12 changed files with 393 additions and 57 deletions

View File

@@ -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

View File

@@ -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<HTMLDivElement>(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 (
<div className={styles.wrapper} ref={ref}>
@@ -149,3 +160,48 @@ export default function BookingAlert({ isVisible = false }: BookingAlertProps) {
</div>
)
}
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
}

View File

@@ -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
}

View File

@@ -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<RouterOutput["hotel"]["get"]>
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 (
<>
<NoAvailabilityTracking
lang={lang}
shouldTrackNoAvailability={shouldTrackNoAvailability}
hotelsTrackingData={hotelsTrackingData}
pageTrackingData={pageTrackingData}
/>
<TrackingSDK hotelInfo={hotelsTrackingData} pageData={pageTrackingData} />
</>
)
}

View File

@@ -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 {

View File

@@ -87,7 +87,7 @@ export async function SelectRate({
/>
)}
<AvailabilityError />
<AvailabilityError booking={booking} />
</>
)
}

View File

@@ -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 (
<>
<SelectHotel
@@ -128,6 +133,12 @@ export async function AlternativeHotelsPage({
hotelInfo={hotelsTrackingData}
pageData={pageTrackingData}
/>
<NoAvailabilityTracking
lang={lang}
hotelsTrackingData={hotelsTrackingData}
pageTrackingData={pageTrackingData}
shouldTrackNoAvailability={shouldTrackNoAvailability}
/>
</Suspense>
</>
)

View File

@@ -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 (
<>
<SelectHotel
@@ -111,6 +117,12 @@ export async function SelectHotelPage({
hotelInfo={hotelsTrackingData}
pageData={pageTrackingData}
/>
<NoAvailabilityTracking
lang={lang}
hotelsTrackingData={hotelsTrackingData}
pageTrackingData={pageTrackingData}
shouldTrackNoAvailability={shouldTrackNoAvailability}
/>
</Suspense>
</>
)

View File

@@ -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 (
<>
<SelectRateProvider hotelData={hotelData}>
<SelectRate hotelData={hotelData} booking={booking} />
<SelectRateTracking hotelData={hotelData} booking={booking} />
</SelectRateProvider>
<TrackingSDK hotelInfo={hotelsTrackingData} pageData={pageTrackingData} />
</>
)
}

View File

@@ -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 ""
}
}

View File

@@ -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,
},
})
}

View File

@@ -12,7 +12,8 @@
},
"exports": {
"./*": "./lib/*.ts",
"./TrackingSDK": "./lib/TrackingSDK.tsx"
"./TrackingSDK": "./lib/TrackingSDK.tsx",
"./NoAvailabilityTracking": "./lib/NoAvailabilityTracking.tsx"
},
"dependencies": {
"@scandic-hotels/common": "workspace:*",