Chore/cleanup after trpc migration * Clean up TODOs * Rename REDEMPTION constant to SEARCH_TYPE_REDEMPTION * Update dependencies Remove unused deps from scandic-web Add missing deps to trpc package * Update self-referencing imports * Remove unused variables from scandic-web env * Fix missing graphql-tag package * Actually fix * Remove unused env var Approved-by: Christian Andolf Approved-by: Linus Flood
362 lines
12 KiB
TypeScript
362 lines
12 KiB
TypeScript
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
|
|
|
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
|
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 { sumPackages } from "@/components/HotelReservation/utils"
|
|
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 {
|
|
TrackingChannelEnum,
|
|
type TrackingSDKAncillaries,
|
|
type TrackingSDKHotelInfo,
|
|
type TrackingSDKPageData,
|
|
} from "@/types/components/tracking"
|
|
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(" - ")
|
|
}
|