import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { REDEMPTION } from "@/constants/booking" import { sumPackages } from "@/components/HotelReservation/utils" import { getSpecialRoomType } from "@/utils/specialRoomType" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { TrackingChannelEnum, type TrackingSDKAncillaries, type TrackingSDKHotelInfo, type TrackingSDKPageData, } from "@/types/components/tracking" import { CurrencyEnum } from "@/types/enums/currency" import type { Hotel } from "@/types/hotel" import type { Room } from "@/types/providers/details/room" import type { RoomState } from "@/types/stores/enter-details" import type { PriceProduct, Product, } from "@/types/trpc/routers/hotel/roomAvailability" import type { Lang } from "@/constants/languages" import type { SelectHotelParams } from "@/utils/url" export function getTracking( booking: SelectHotelParams, hotel: Hotel, rooms: Room[], city: string | undefined, isMember: boolean, lang: Lang, storedRooms: RoomState[], breakfastPackages: BreakfastPackages ) { 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: "bookingroomsandratespage", 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.description : "")) .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: rooms.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.cancellationText.toLowerCase()) .join(","), rateCodeName: rooms.map((room) => room.rateDefinitionTitle).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 === REDEMPTION ? "yes" : "no", rewardNightAvailability: booking.searchType === REDEMPTION ? "true" : "false", roomPrice: calcTotalRoomPrice(rooms, isMember), totalPrice: calcTotalPrice(rooms, 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: city, searchType: "hotel", specialRoomType: rooms .map((room) => getSpecialRoomType(room.packages)) .join(","), } const roomsWithPetRoom = rooms.filter(hasPetRoom) const ancillaries: 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] }) return { hotelsTrackingData, pageTrackingData, ancillaries, } } function hasPublicPrice( roomRate: Product ): roomRate is PriceProduct & { public: NonNullable } { if ("public" in roomRate && roomRate.public) { return true } return false } function hasMemberPrice( roomRate: Product ): roomRate is PriceProduct & { member: NonNullable } { if ("member" in roomRate && roomRate.member) { return true } return false } function hasPetRoom( room: Room ): room is Room & { packages: NonNullable } { if (!room.packages) { return false } return room.packages.some((p) => p.code === RoomPackageCodeEnum.PET_ROOM) } function calcTotalPrice(rooms: Room[], isMember: boolean) { const totalRoomPrice = calcTotalRoomPrice(rooms, isMember) const totalPackageSum = rooms.reduce((total, room) => { const packageSum = sumPackages(room.packages) return (total += packageSum.price ?? 0) }, 0) return totalRoomPrice + totalPackageSum } function calcTotalRoomPrice(rooms: Room[], 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 ( "corporateCheck" 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) }