Merged in fix/SW-2853-tracking-breakfast-ancillaries (pull request #2188)

fix(SW-2853): tracking for breakfast ancillaries

* fix(SW-2853): tracking for breakfast ancillaries

* fix(SW-2853): viewAncillary event fix for breakfast

* fix(SW-2853): pr comment spread room facilities


Approved-by: Tobias Johansson
This commit is contained in:
Bianca Widstam
2025-05-22 10:05:54 +00:00
parent 818c1a3bcf
commit c4229c2dd2
10 changed files with 183 additions and 48 deletions

View File

@@ -32,6 +32,7 @@ export default function ActionButtons({
selectDeliveryTime, selectDeliveryTime,
selectQuantityAndDeliveryTime, selectQuantityAndDeliveryTime,
selectedAncillary, selectedAncillary,
breakfastData,
} = useAddAncillaryStore((state) => ({ } = useAddAncillaryStore((state) => ({
currentStep: state.currentStep, currentStep: state.currentStep,
isBreakfast: state.isBreakfast, isBreakfast: state.isBreakfast,
@@ -41,6 +42,7 @@ export default function ActionButtons({
selectDeliveryTime: state.selectDeliveryTime, selectDeliveryTime: state.selectDeliveryTime,
selectQuantityAndDeliveryTime: state.selectQuantityAndDeliveryTime, selectQuantityAndDeliveryTime: state.selectQuantityAndDeliveryTime,
selectedAncillary: state.selectedAncillary, selectedAncillary: state.selectedAncillary,
breakfastData: state.breakfastData,
})) }))
const isMobile = useMediaQuery("(max-width: 767px)") const isMobile = useMediaQuery("(max-width: 767px)")
const { setError } = useFormContext() const { setError } = useFormContext()
@@ -69,7 +71,8 @@ export default function ActionButtons({
trackAddAncillary( trackAddAncillary(
selectedAncillary, selectedAncillary,
quantityWithCard, quantityWithCard,
quantityWithPoints quantityWithPoints,
breakfastData
) )
if (isMobile) { if (isMobile) {
selectQuantityAndDeliveryTime() selectQuantityAndDeliveryTime()

View File

@@ -162,6 +162,7 @@ export default function AddAncillaryFlowModal({
data.deliveryTime, data.deliveryTime,
"ancillary", "ancillary",
selectedAncillary, selectedAncillary,
breakfastData,
booking.guaranteeInfo?.cardType, booking.guaranteeInfo?.cardType,
booking.roomTypeCode booking.roomTypeCode
) )
@@ -180,12 +181,22 @@ export default function AddAncillaryFlowModal({
}) })
router.refresh() router.refresh()
} else { } else {
trackAncillaryFailed(packages, data.deliveryTime, selectedAncillary) trackAncillaryFailed(
packages,
data.deliveryTime,
selectedAncillary,
breakfastData
)
toast.error(ancillaryErrorMessage) toast.error(ancillaryErrorMessage)
} }
}, },
onError: () => { onError: () => {
trackAncillaryFailed(packages, data.deliveryTime, selectedAncillary) trackAncillaryFailed(
packages,
data.deliveryTime,
selectedAncillary,
breakfastData
)
toast.error(ancillaryErrorMessage) toast.error(ancillaryErrorMessage)
}, },
} }
@@ -200,7 +211,8 @@ export default function AddAncillaryFlowModal({
savedCreditCard, savedCreditCard,
packages, packages,
selectedAncillary, selectedAncillary,
data.deliveryTime data.deliveryTime,
breakfastData
) )
if (booking.refId) { if (booking.refId) {
const card = savedCreditCard const card = savedCreditCard
@@ -270,6 +282,7 @@ export default function AddAncillaryFlowModal({
selectedAncillary, selectedAncillary,
packages: packagesToAdd, packages: packagesToAdd,
isBreakfast, isBreakfast,
breakfastData,
}) })
const shouldSkipGuarantee = const shouldSkipGuarantee =
booking.guaranteeInfo || (data.quantityWithCard ?? 0) <= 0 booking.guaranteeInfo || (data.quantityWithCard ?? 0) <= 0

View File

@@ -13,14 +13,17 @@ export default function WrappedAncillaryCard({
ancillary, ancillary,
}: WrappedAncillaryProps) { }: WrappedAncillaryProps) {
const { description, ...ancillaryWithoutDescription } = ancillary const { description, ...ancillaryWithoutDescription } = ancillary
const selectAncillary = useAddAncillaryStore((state) => state.selectAncillary) const { selectAncillary, booking } = useAddAncillaryStore((state) => ({
selectAncillary: state.selectAncillary,
booking: state.booking,
}))
return ( return (
<div <div
role="button" role="button"
onClick={() => { onClick={() => {
selectAncillary(ancillary) selectAncillary(ancillary)
trackViewAncillary(ancillary) trackViewAncillary(ancillary, booking)
}} }}
> >
<AncillaryCard ancillary={ancillaryWithoutDescription} /> <AncillaryCard ancillary={ancillaryWithoutDescription} />

View File

@@ -20,12 +20,14 @@ export default function RemoveButton({
title, title,
booking, booking,
ancillary, ancillary,
addedBreakfastPackages,
}: { }: {
refId: string refId: string
codes: string[] codes: string[]
title?: string title?: string
booking: Room booking: Room
ancillary: PackageSchema ancillary: PackageSchema
addedBreakfastPackages: PackageSchema[] | undefined
}) { }) {
const lang = useLang() const lang = useLang()
const intl = useIntl() const intl = useIntl()
@@ -75,7 +77,8 @@ export default function RemoveButton({
trackRemoveAncillary( trackRemoveAncillary(
ancillary, ancillary,
booking.hotelId, booking.hotelId,
booking.ancillary?.deliveryTime booking.ancillary?.deliveryTime,
addedBreakfastPackages
) )
router.refresh() router.refresh()

View File

@@ -137,6 +137,7 @@ export function AddedAncillaries({
title={ancillaryTitle} title={ancillaryTitle}
booking={booking} booking={booking}
ancillary={ancillary} ancillary={ancillary}
addedBreakfastPackages={addedBreakfastPackages}
/> />
</div> </div>
) : null} ) : null}
@@ -205,6 +206,7 @@ export function AddedAncillaries({
title={ancillaryTitle} title={ancillaryTitle}
booking={booking} booking={booking}
ancillary={ancillary} ancillary={ancillary}
addedBreakfastPackages={addedBreakfastPackages}
/> />
</div> </div>
) : null} ) : null}

View File

@@ -47,7 +47,7 @@ export default function GuaranteeAncillaryHandler({
return return
} }
const { formData, selectedAncillary, packages } = sessionData const { formData, selectedAncillary, packages, breakfastData } = sessionData
addAncillary.mutate( addAncillary.mutate(
{ {
@@ -67,7 +67,8 @@ export default function GuaranteeAncillaryHandler({
packages, packages,
formData.deliveryTime, formData.deliveryTime,
"room + ancillary", "room + ancillary",
selectedAncillary selectedAncillary,
breakfastData
) )
clearAncillarySessionData() clearAncillarySessionData()
router.replace(returnUrl) router.replace(returnUrl)
@@ -75,7 +76,8 @@ export default function GuaranteeAncillaryHandler({
trackAncillaryFailed( trackAncillaryFailed(
packages, packages,
formData.deliveryTime, formData.deliveryTime,
selectedAncillary selectedAncillary,
breakfastData
) )
router.replace(`${returnUrl}&errorCode=AncillaryFailed`) router.replace(`${returnUrl}&errorCode=AncillaryFailed`)
} }
@@ -84,7 +86,8 @@ export default function GuaranteeAncillaryHandler({
trackAncillaryFailed( trackAncillaryFailed(
packages, packages,
formData.deliveryTime, formData.deliveryTime,
selectedAncillary selectedAncillary,
breakfastData
) )
router.replace(`${returnUrl}&errorCode=AncillaryFailed`) router.replace(`${returnUrl}&errorCode=AncillaryFailed`)
}, },

View File

@@ -12,7 +12,10 @@ import {
import { getAncillarySessionData } from "@/components/HotelReservation/MyStay/utils/ancillaries" import { getAncillarySessionData } from "@/components/HotelReservation/MyStay/utils/ancillaries"
import LoadingSpinner from "@/components/LoadingSpinner" import LoadingSpinner from "@/components/LoadingSpinner"
import { trackEvent } from "@/utils/tracking/base" import { trackEvent } from "@/utils/tracking/base"
import { buildAncillaries } from "@/utils/tracking/myStay" import {
buildAncillariesTracking,
buildBreakfastTracking,
} from "@/utils/tracking/myStay"
interface TrackGuaranteeProps { interface TrackGuaranteeProps {
status: string status: string
@@ -32,7 +35,8 @@ export default function TrackGuarantee({
useEffect(() => { useEffect(() => {
const trackAncillaryPaymentEvent = (event: string, status: string) => { const trackAncillaryPaymentEvent = (event: string, status: string) => {
const sessionData = getAncillarySessionData() const sessionData = getAncillarySessionData()
const { formData, selectedAncillary, packages } = sessionData || {} const { formData, selectedAncillary, packages, breakfastData } =
sessionData || {}
trackEvent({ trackEvent({
event, event,
@@ -42,11 +46,13 @@ export default function TrackGuarantee({
lateArrivalGuarantee: "yes", lateArrivalGuarantee: "yes",
guaranteedProduct: "room + ancillary", guaranteedProduct: "room + ancillary",
}, },
ancillaries: buildAncillaries( ancillaries: breakfastData
packages ?? [], ? buildBreakfastTracking(breakfastData)
selectedAncillary, : buildAncillariesTracking(
formData?.deliveryTime packages ?? [],
), selectedAncillary,
formData?.deliveryTime
),
}) })
} }

View File

@@ -3,6 +3,7 @@ import type {
SelectedAncillary, SelectedAncillary,
} from "@/types/components/myPages/myStay/ancillaries" } from "@/types/components/myPages/myStay/ancillaries"
import type { AncillaryFormData } from "@/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema" import type { AncillaryFormData } from "@/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema"
import type { BreakfastData } from "@/stores/my-stay/add-ancillary-flow"
export const generateDeliveryOptions = () => { export const generateDeliveryOptions = () => {
const timeSlots = ["16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00"] const timeSlots = ["16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00"]
@@ -49,6 +50,7 @@ export const getAncillarySessionData = ():
comment: string | undefined comment: string | undefined
}[] }[]
isBreakfast: boolean isBreakfast: boolean
breakfastData: BreakfastData | null
} }
| undefined => { | undefined => {
if (typeof window === "undefined") return undefined if (typeof window === "undefined") return undefined
@@ -67,6 +69,7 @@ export function setAncillarySessionData({
selectedAncillary, selectedAncillary,
packages, packages,
isBreakfast, isBreakfast,
breakfastData,
}: { }: {
formData?: AncillaryFormData formData?: AncillaryFormData
selectedAncillary?: Ancillary["ancillaryContent"][number] | null selectedAncillary?: Ancillary["ancillaryContent"][number] | null
@@ -76,6 +79,7 @@ export function setAncillarySessionData({
comment: string | undefined comment: string | undefined
}[] }[]
isBreakfast: boolean isBreakfast: boolean
breakfastData: BreakfastData | null
}) { }) {
if (typeof window === "undefined") return if (typeof window === "undefined") return
try { try {
@@ -88,6 +92,7 @@ export function setAncillarySessionData({
selectedAncillary, selectedAncillary,
packages, packages,
isBreakfast, isBreakfast,
breakfastData,
}) })
) )
} catch (error) { } catch (error) {

View File

@@ -19,7 +19,7 @@ export default function RoomDetails({
}: RoomDetailsProps) { }: RoomDetailsProps) {
const intl = useIntl() const intl = useIntl()
const filteredSortedFacilities = roomFacilities const filteredSortedFacilities = [...roomFacilities]
.sort((a, b) => a.sortOrder - b.sortOrder) .sort((a, b) => a.sortOrder - b.sortOrder)
.map((facility) => { .map((facility) => {
const Icon = <FacilityIcon name={facility.icon} color="Icon/Default" /> const Icon = <FacilityIcon name={facility.icon} color="Icon/Default" />

View File

@@ -1,9 +1,12 @@
import { trackEvent } from "./base" import { trackEvent } from "./base"
import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries" import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { CurrencyEnum } from "@/types/enums/currency" import { CurrencyEnum } from "@/types/enums/currency"
import type { Room } from "@/types/stores/my-stay"
import type { PackageSchema } from "@/types/trpc/routers/booking/confirmation" import type { PackageSchema } from "@/types/trpc/routers/booking/confirmation"
import type { CreditCard } from "@/types/user" import type { CreditCard } from "@/types/user"
import type { BreakfastData } from "@/stores/my-stay/add-ancillary-flow"
export function trackCancelStay(hotelId: string, bnr: string) { export function trackCancelStay(hotelId: string, bnr: string) {
trackEvent({ trackEvent({
@@ -45,7 +48,7 @@ export function trackGlaSaveCardAttempt(
}) })
} }
export function buildAncillaries( export function buildAncillariesTracking(
packages: { packages: {
code: string code: string
quantity: number quantity: number
@@ -57,11 +60,14 @@ export function buildAncillaries(
return packages.map((pkg) => { return packages.map((pkg) => {
const payedWithCard = pkg.code === selectedAncillary?.id const payedWithCard = pkg.code === selectedAncillary?.id
const payedWithPoints = pkg.code === selectedAncillary?.loyaltyCode const payedWithPoints = pkg.code === selectedAncillary?.loyaltyCode
const ancillaryDeliveryTime = selectedAncillary?.requiresDeliveryTime
? deliveryTime
: undefined
return { return {
productId: pkg.code, productId: pkg.code,
productUnits: pkg.quantity, productUnits: pkg.quantity,
productDeliveryTime: deliveryTime, productDeliveryTime: ancillaryDeliveryTime,
productName: selectedAncillary?.title, productName: selectedAncillary?.title,
productCategory: selectedAncillary?.categoryName, productCategory: selectedAncillary?.categoryName,
...(payedWithCard && { ...(payedWithCard && {
@@ -83,7 +89,8 @@ export function trackGlaAncillaryAttempt(
comment: string | undefined comment: string | undefined
}[], }[],
selectedAncillary: SelectedAncillary | null, selectedAncillary: SelectedAncillary | null,
deliveryTime: string | undefined deliveryTime: string | undefined,
breakfastData: BreakfastData | null
) { ) {
trackEvent({ trackEvent({
event: "GuaranteeAttemptAncillary", event: "GuaranteeAttemptAncillary",
@@ -91,7 +98,9 @@ export function trackGlaAncillaryAttempt(
status: "glacardsaveattempt", status: "glacardsaveattempt",
type: savedCreditCard?.cardType, type: savedCreditCard?.cardType,
}, },
ancillaries: buildAncillaries(packages, selectedAncillary, deliveryTime), ancillaries: breakfastData
? buildBreakfastTracking(breakfastData, selectedAncillary?.hotelId)
: buildAncillariesTracking(packages, selectedAncillary, deliveryTime),
hotelInfo: { hotelInfo: {
hotelId: selectedAncillary?.hotelId, hotelId: selectedAncillary?.hotelId,
lateArrivalGuarantee: "yes", lateArrivalGuarantee: "yes",
@@ -110,6 +119,7 @@ export function trackAncillarySuccess(
deliveryTime: string | null | undefined, deliveryTime: string | null | undefined,
guaranteedProduct: string, guaranteedProduct: string,
selectedAncillary: SelectedAncillary | null, selectedAncillary: SelectedAncillary | null,
breakfastData: BreakfastData | null,
cardType?: string, cardType?: string,
roomTypeCode?: string roomTypeCode?: string
) { ) {
@@ -125,7 +135,9 @@ export function trackAncillarySuccess(
status: "glacardsaveconfirmed", status: "glacardsaveconfirmed",
type: cardType, type: cardType,
}, },
ancillaries: buildAncillaries(packages, selectedAncillary, deliveryTime), ancillaries: breakfastData
? buildBreakfastTracking(breakfastData, selectedAncillary?.hotelId)
: buildAncillariesTracking(packages, selectedAncillary, deliveryTime),
}) })
} }
@@ -136,11 +148,14 @@ export function trackAncillaryFailed(
comment?: string comment?: string
}[], }[],
deliveryTime: string | null | undefined, deliveryTime: string | null | undefined,
selectedAncillary: SelectedAncillary | null selectedAncillary: SelectedAncillary | null,
breakfastData: BreakfastData | null
) { ) {
trackEvent({ trackEvent({
event: "GuaranteeFailAncillary", event: "GuaranteeFailAncillary",
ancillaries: buildAncillaries(packages, selectedAncillary, deliveryTime), ancillaries: breakfastData
? buildBreakfastTracking(breakfastData, selectedAncillary?.hotelId)
: buildAncillariesTracking(packages, selectedAncillary, deliveryTime),
hotelInfo: { hotelInfo: {
hotelId: selectedAncillary?.hotelId, hotelId: selectedAncillary?.hotelId,
lateArrivalGuarantee: "yes", lateArrivalGuarantee: "yes",
@@ -149,47 +164,129 @@ export function trackAncillaryFailed(
}) })
} }
export function trackViewAncillary(ancillary: SelectedAncillary) { export function buildBreakfastTracking(
breakfastData: BreakfastData,
hotelId?: number
) {
const items = []
if (breakfastData.nrOfAdults) {
items.push({
hotelId,
productId: BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST,
productName: "Breakfast",
productUnits: breakfastData.nrOfAdults * breakfastData.nrOfNights,
productPrice:
breakfastData.priceAdult *
breakfastData.nrOfAdults *
breakfastData.nrOfNights,
currency: breakfastData.currency,
productCategory: "Food",
})
}
if (breakfastData.nrOfPayingChildren) {
items.push({
hotelId,
productId: BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST,
productName: "Breakfast",
productUnits: breakfastData.nrOfPayingChildren * breakfastData.nrOfNights,
productPrice:
breakfastData.priceChild *
breakfastData.nrOfPayingChildren *
breakfastData.nrOfNights,
currency: breakfastData.currency,
productCategory: "Food",
})
}
return items
}
export function trackViewAncillary(
ancillary: SelectedAncillary,
booking: Room
) {
const { hotelId, id, title, categoryName } = ancillary
const isBreakfast = id === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
const hasPayingChildren = booking.childrenAges.some((age) => age >= 4)
const ancillaries = [
{
hotelId,
productId: id,
productName: title,
productCategory: categoryName,
},
]
if (isBreakfast && hasPayingChildren) {
ancillaries.push({
hotelId,
productId: BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST,
productName: title,
productCategory: categoryName,
})
}
trackEvent({ trackEvent({
event: "viewAncillary", event: "viewAncillary",
ancillaries: [ ancillaries,
{
hotelId: ancillary.hotelId,
productId: ancillary.id,
productName: ancillary.title,
productCategory: ancillary.categoryName,
},
],
}) })
} }
export function trackRemoveAncillary( export function trackRemoveAncillary(
ancillary: PackageSchema, ancillary: PackageSchema,
hotelId: string, hotelId: string,
deliveryTime?: string deliveryTime?: string,
addedBreakfastPackages?: PackageSchema[]
) { ) {
const isBreakfastPackage =
ancillary.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
const isPoints = ancillary.currency === CurrencyEnum.POINTS const isPoints = ancillary.currency === CurrencyEnum.POINTS
const packagesWithoutFreeChildBreakfast = addedBreakfastPackages?.filter(
(p) => p.code !== BreakfastPackageEnum.FREE_CHILD_BREAKFAST
)
const ancillaries = isBreakfastPackage
? (packagesWithoutFreeChildBreakfast?.map((pkg) => ({
hotelId,
productId: pkg.code,
productPrice: pkg.totalPrice,
productUnits: pkg.totalUnit,
productType: pkg.type,
})) ?? [])
: [
{
hotelId,
productId: ancillary.code,
productPrice: isPoints ? 0 : ancillary.totalPrice,
productPoints: isPoints ? ancillary.totalPrice : 0,
productUnits: ancillary.totalUnit,
productType: ancillary.type,
productDeliveryTime: deliveryTime,
},
]
trackEvent({ trackEvent({
event: "removeAncillary", event: "removeAncillary",
ancillaries: [ ancillaries,
{
hotelId,
productId: ancillary.code,
productPrice: isPoints ? 0 : ancillary.totalPrice,
productPoints: isPoints ? ancillary.totalPrice : 0,
productUnits: ancillary.totalUnit,
productType: ancillary.type,
productDeliveryTime: deliveryTime,
},
],
}) })
} }
export function trackAddAncillary( export function trackAddAncillary(
ancillary: SelectedAncillary | null, ancillary: SelectedAncillary | null,
quantityWithCard: number | null, quantityWithCard: number | null,
quantityWithPoints: number | null quantityWithPoints: number | null,
breakfastData: BreakfastData | null
) { ) {
if (breakfastData) {
return trackEvent({
event: "addAncillary",
ancillaries: buildBreakfastTracking(breakfastData, ancillary?.hotelId),
})
}
const ancillaries = [] const ancillaries = []
if ((quantityWithCard ?? 0) > 0) { if ((quantityWithCard ?? 0) > 0) {
ancillaries.push({ ancillaries.push({