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 { BreakfastPackages } from "@scandic-hotels/trpc/routers/hotels/output" 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 { 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 } { 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) } 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(" - ") }