Files
web/apps/scandic-web/components/HotelReservation/EnterDetails/Tracking/tracking.ts
Hrishikesh Vaipurkar 0a4bf40a15 Merged in chore/SW-3321-move-selectratecontext-to- (pull request #2729)
chore(SW-3321): Moved Select rate context to booking-flow package

* chore(SW-3321): Moved Select rate context to booking-flow package

* chore(SW-3321): Optimised code


Approved-by: Joakim Jäderberg
2025-09-02 07:40:01 +00:00

362 lines
12 KiB
TypeScript

import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import {
TrackingChannelEnum,
type TrackingSDKAncillaries,
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
} from "@scandic-hotels/common/tracking/types"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import { PackageTypeEnum } from "@scandic-hotels/trpc/enums/packages"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { getSpecialRoomType } from "@/utils/specialRoomType"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { Hotel } from "@scandic-hotels/trpc/types/hotel"
import type { Room } from "@scandic-hotels/trpc/types/room"
import type {
PriceProduct,
Product,
} from "@scandic-hotels/trpc/types/roomAvailability"
import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast"
import type {
DetailsBooking,
RoomRate,
} from "@/types/components/hotelReservation/enterDetails/details"
import type { RoomState } from "@/types/stores/enter-details"
export function getTracking(
booking: DetailsBooking,
hotel: Hotel,
rooms: Room[],
isMember: boolean,
lang: Lang,
storedRooms: RoomState[],
breakfastPackages: BreakfastPackages,
searchTerm?: string
) {
const arrivalDate = new Date(booking.fromDate)
const departureDate = new Date(booking.toDate)
const shouldSkipBreakfastOption =
storedRooms.every((r) => r.room.breakfast === undefined) ||
!breakfastPackages.length
const breakfastOption = shouldSkipBreakfastOption
? undefined
: storedRooms
.map((r) => {
if (r.room.breakfast === undefined) return ""
if (!r.room.breakfast) return "no breakfast"
return "breakfast buffet"
})
.join("|")
const pageTrackingData: TrackingSDKPageData = {
channel: TrackingChannelEnum.hotelreservation,
domainLanguage: lang,
pageId: "details",
pageName: "hotelreservation|details",
pageType: "bookingenterdetailspage",
siteSections: "hotelreservation|details",
siteVersion: "new-web",
}
const hotelsTrackingData: TrackingSDKHotelInfo = {
ageOfChildren: booking.rooms
.map((room) => room.childrenInRoom?.map((kid) => kid.age).join(",") ?? "")
.join("|"),
analyticsRateCode: rooms.map((room) => room.rate).join("|"),
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
bedType: storedRooms
.map((r) => (r.room.bedType ? r.room.bedType.type : ""))
.join("|"),
// Comma separated booking code values in "code,code,n/a" format for multiroom and "code" or "n/a" for singleroom
// n/a is used whenever code is Not applicable as defined by Tracking team
bookingCode: rooms
.map((room) => room.roomRate.bookingCode ?? "n/a")
.join(", "),
// Similar to booking code, comma separated room based values.
bookingCodeAvailability: booking.bookingCode
? rooms
.map((room) => (room.roomRate.bookingCode ? "true" : "false"))
.join(", ")
: undefined,
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
breakfastOption,
childBedPreference: booking.rooms
.map(
(room) =>
room.childrenInRoom
?.map((kid) => ChildBedMapEnum[kid.bed])
.join(",") ?? ""
)
.join("|"),
country: hotel?.address.country,
departureDate: format(departureDate, "yyyy-MM-dd"),
discount: storedRooms.reduce((total, { room }, idx) => {
const isMainRoom = idx === 0
if (
hasMemberPrice(room.roomRate) &&
isMainRoom &&
isMember &&
hasPublicPrice(room.roomRate)
) {
const memberPrice = room.roomRate.member.localPrice.pricePerStay
const publicPrice = room.roomRate.public.localPrice.pricePerStay
total += publicPrice - memberPrice
} else if (
hasPublicPrice(room.roomRate) &&
room.roomRate.public.localPrice.regularPricePerStay
) {
const publicPrice = room.roomRate.public.localPrice.pricePerStay
const regularPrice = room.roomRate.public.localPrice.regularPricePerStay
total += regularPrice - publicPrice
}
return total
}, 0),
duration: differenceInCalendarDays(departureDate, arrivalDate),
hotelID: hotel?.operaId,
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
noOfAdults: booking.rooms.map((room) => room.adults).join(","),
noOfChildren: booking.rooms
.map((room) => room.childrenInRoom?.length ?? 0)
.join(","),
noOfRooms: booking.rooms.length,
...(rooms.length > 1 && {
multiroomRateIdentity: rooms.every((room) => {
const firstRoom = rooms[0]
if (
hasPublicPrice(firstRoom.roomRate) &&
hasPublicPrice(room.roomRate)
) {
return (
firstRoom.roomRate.public?.localPrice.pricePerNight ===
room.roomRate.public?.localPrice.pricePerNight
)
}
})
? "same rate"
: "mixed rate",
}),
rateCode: rooms
.map((room, idx) => {
const isMainRoom = idx === 0
if (hasMemberPrice(room.roomRate) && isMember && isMainRoom) {
return room.roomRate.member.rateCode
} else if (hasPublicPrice(room.roomRate)) {
return room.roomRate.public.rateCode
} else if ("corporateCheque" in room.roomRate) {
return room.roomRate.corporateCheque.rateCode
} else if ("redemption" in room.roomRate) {
return room.roomRate.redemption.rateCode
} else if ("voucher" in room.roomRate) {
return room.roomRate.voucher.rateCode
}
return ""
})
.join("|"),
rateCodeCancellationRule: rooms
.map((room) => room.cancellationRule)
.join(","),
rateCodeName: rooms
.map((room) =>
constructRateCodeName(
room.roomRate,
room.breakfastIncluded,
booking.bookingCode
)
)
.join(","),
rateCodeType: rooms.map((room) => room.rateType.toLowerCase()).join(","),
region: hotel?.address.city,
revenueCurrencyCode: [
...new Set(
rooms.map((room) => {
if ("corporateCheque" in room.roomRate) {
return CurrencyEnum.CC
} else if ("redemption" in room.roomRate) {
return CurrencyEnum.POINTS
} else if ("voucher" in room.roomRate) {
return CurrencyEnum.Voucher
} else if (hasPublicPrice(room.roomRate)) {
return room.roomRate.public.localPrice.currency
} else if (hasMemberPrice(room.roomRate)) {
return room.roomRate.member.localPrice.currency
}
return CurrencyEnum.Unknown
})
),
].join(","),
rewardNight: booking.searchType === SEARCH_TYPE_REDEMPTION ? "yes" : "no",
rewardNightAvailability:
booking.searchType === SEARCH_TYPE_REDEMPTION ? "true" : "false",
roomPrice: calcTotalRoomPrice(storedRooms, isMember),
roomTypeCode: rooms.map((room) => room.roomTypeCode).join("|"),
roomTypeName: rooms.map((room) => room.roomType).join("|"),
totalPrice: calcTotalPrice(storedRooms, isMember),
points:
// @ts-expect-error - redemption object doesn't exist error
rooms.find((room) => "redemption" in room.roomRate)?.roomRate.redemption
.localPrice.pointsPerStay ?? "n/a",
searchTerm,
searchType: "hotel",
specialRoomType: rooms
.map((room) => getSpecialRoomType(room.packages))
.join(","),
}
const roomsWithPetRoom = rooms.filter(hasPetRoom)
const petRoomAncillaries: TrackingSDKAncillaries = roomsWithPetRoom
.slice(0, 1) // should only be one item
.map((room) => {
return room.packages
.filter((p) => p.code === RoomPackageCodeEnum.PET_ROOM)
.map((pkg) => ({
hotelId: hotel.operaId,
productId: pkg.code,
productUnits: roomsWithPetRoom.length,
productPoints: 0,
productPrice: pkg.localPrice.totalPrice * roomsWithPetRoom.length,
productType: "room preference",
productName: "pet room",
productCategory: "",
}))[0]
})
const breakfastAncillaries: TrackingSDKAncillaries = storedRooms
.map((room) => {
return { breakfast: room.room.breakfast, adults: room.room.adults }
})
.filter(hasBreakfast)
.map(({ breakfast, adults }) => {
return {
hotelId: hotel.operaId,
productId: breakfast.code,
productUnits:
adults * differenceInCalendarDays(departureDate, arrivalDate),
productPoints: 0,
productPrice:
breakfast.localPrice.price *
adults *
differenceInCalendarDays(departureDate, arrivalDate),
productType: "food",
productName: breakfast.description,
productCategory: breakfast.packageType,
}
})
const ancillaries = petRoomAncillaries.concat(breakfastAncillaries)
return {
hotelsTrackingData,
pageTrackingData,
ancillaries,
}
}
function hasPublicPrice(
roomRate: Product
): roomRate is PriceProduct & { public: NonNullable<PriceProduct["public"]> } {
if ("public" in roomRate && roomRate.public) {
return true
}
return false
}
function hasMemberPrice(
roomRate: Product
): roomRate is PriceProduct & { member: NonNullable<PriceProduct["member"]> } {
if ("member" in roomRate && roomRate.member) {
return true
}
return false
}
function hasPetRoom(
room: Room
): room is Room & { packages: NonNullable<Room["packages"]> } {
if (!room.packages) {
return false
}
return room.packages.some((p) => p.code === RoomPackageCodeEnum.PET_ROOM)
}
type RoomEntry = {
breakfast?: BreakfastPackages[number] | false
adults: number
}
function hasBreakfast(entry: RoomEntry): entry is RoomEntry & {
breakfast: BreakfastPackages[number]
adults: number
} {
return (
entry.breakfast !== false &&
entry.breakfast?.packageType === PackageTypeEnum.BreakfastAdult
)
}
function calcTotalPrice(rooms: RoomState[], isMember: boolean) {
const totalRoomPrice = calcTotalRoomPrice(rooms, isMember)
const totalPackageSum = rooms.reduce((total, { room }) => {
if (room.breakfast) {
total += Number(room.breakfast.localPrice.totalPrice) * room.adults
}
if (room.roomFeatures?.length) {
const packageSum = sumPackages(room.roomFeatures)
total += packageSum.price
}
return total
}, 0)
return totalRoomPrice + totalPackageSum
}
function calcTotalRoomPrice(rooms: RoomState[], isMember: boolean) {
return rooms.reduce((total, { room }, idx) => {
// When it comes special rates, only redemption has additional price and that should be added
// other special rates like voucher, corporateCheck should be added as 0 according to Priyanka
if ("redemption" in room.roomRate) {
return room.roomRate.redemption.localPrice?.additionalPricePerStay ?? 0
} else if (
"corporateCheque" in room.roomRate ||
"voucher" in room.roomRate
) {
return 0
}
const isMainRoom = idx === 0
if (hasMemberPrice(room.roomRate) && isMember && isMainRoom) {
total += room.roomRate.member.localPrice.pricePerStay
} else if (hasPublicPrice(room.roomRate)) {
total += room.roomRate.public.localPrice.pricePerStay
}
return total
}, 0)
}
function constructRateCodeName(
roomRate: RoomRate,
breakfastIncluded: boolean,
bookingCode?: string
) {
if ("corporateCheque" in roomRate) {
return "corporate cheque"
} else if ("voucher" in roomRate) {
return "voucher"
} else if ("redemption" in roomRate) {
return "redemption"
}
const bookingCodeStr = bookingCode ? bookingCode.toUpperCase() : ""
const breakfastIncludedStr = breakfastIncluded
? "incl. breakfast"
: "excl. breakfast"
return [bookingCodeStr, roomRate.rate, breakfastIncludedStr]
.filter(Boolean)
.join(" - ")
}