From 35c1724afb70ad39262b5452f0ff682ce4c63db2 Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Tue, 1 Apr 2025 09:38:36 +0000 Subject: [PATCH] Merged in feat/SW-1997-tracking-gla-my-stay-ancillaries (pull request #1657) Feat/SW-1997 tracking gla my stay ancillaries * feat(SW-1996): tracking gla my stay * feat(SW-1996): update gla tracking * feat(SW-1996): fix comment * feat(SW-1997): add tracking for gla my stay and ancillaries * feat(SW-1997): rebase master * feat(SW-1997): fix duplicate import * feat(SW-1997): add hotelId and category for ancillaries, and add more tracking * feat(SW-1997): remove commments and fix spelling mistake * feat(SW-1997): if addAncillary failed, but guarantee is successful, default to card in booking Approved-by: Niclas Edenvin --- .../gla-payment-callback/page.tsx | 15 +- .../ActionButtons/index.tsx | 15 +- .../AddAncillaryFlowModal/index.tsx | 251 ++++++++++-------- .../WrappedAncillaryCard/index.tsx | 10 +- .../Ancillaries/AddAncillaryFlow/schema.ts | 4 + .../Ancillaries/AddedAncillaries/index.tsx | 10 +- .../Ancillaries/GuaranteeCallback/index.tsx | 88 +++--- .../MyStay/Ancillaries/index.tsx | 3 + .../MyStay/Ancillaries/utils.ts | 31 ++- .../GuaranteeLateArrivalCallback/index.tsx | 110 ++++++++ .../MyStay/GuaranteeLateArrival/index.tsx | 17 +- .../hooks/booking/useGuaranteeBooking.ts | 50 ++-- .../server/routers/booking/mutation.ts | 1 - .../server/routers/hotels/output.ts | 3 + apps/scandic-web/utils/tracking/myStay.ts | 170 +++++++++++- 15 files changed, 596 insertions(+), 182 deletions(-) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx index 82b210e2d..5c4a83629 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx @@ -1,4 +1,4 @@ -import { notFound, redirect } from "next/navigation" +import { notFound } from "next/navigation" import { BookingErrorCodeEnum, @@ -8,6 +8,7 @@ import { myStay } from "@/constants/routes/myStay" import { serverClient } from "@/lib/trpc/server" import GuaranteeCallback from "@/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback" +import TrackGuarantee from "@/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback" import LoadingSpinner from "@/components/LoadingSpinner" import type { LangParams, PageArgs } from "@/types/params" @@ -48,7 +49,7 @@ export default async function GuaranteePaymentCallbackPage({ ) } console.log(`[gla-payment-callback] redirecting to: ${myStayUrl}`) - return redirect(myStayUrl) + return } let errorMessage = undefined @@ -86,7 +87,15 @@ export default async function GuaranteePaymentCallbackPage({ if (isAncillaryFlow) { searchObject.set("ancillary", "ancillary") } - redirect(`${myStayUrl}&${searchObject.toString()}`) + + return ( + + ) } return diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx index 6f842cf86..ba534548a 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx @@ -10,7 +10,9 @@ import { useAddAncillaryStore, } from "@/stores/my-stay/add-ancillary-flow" -import { type AncillaryFormData, quantitySchema } from "../../schema" +import { trackAddAncillary } from "@/utils/tracking/myStay" + +import { type AncillaryQuantityFormData,quantitySchema } from "../../schema" import styles from "./actionButtons.module.css" @@ -27,12 +29,14 @@ export default function ActionButtons({ selectQuantity, selectDeliveryTime, selectQuantityAndDeliveryTime, + selectedAncillary, } = useAddAncillaryStore((state) => ({ currentStep: state.currentStep, prevStep: state.prevStep, selectQuantity: state.selectQuantity, selectDeliveryTime: state.selectDeliveryTime, selectQuantityAndDeliveryTime: state.selectQuantityAndDeliveryTime, + selectedAncillary: state.selectedAncillary, })) const isMobile = useMediaQuery("(max-width: 767px)") const { setError } = useFormContext() @@ -41,10 +45,10 @@ export default function ActionButtons({ const isConfirmStep = currentStep === AncillaryStepEnum.confirmation const confirmLabel = intl.formatMessage({ id: "Confirm" }) const continueLabel = intl.formatMessage({ id: "Continue" }) - const quantityWithCard = useWatch({ + const quantityWithCard = useWatch({ name: "quantityWithCard", }) - const quantityWithPoints = useWatch({ + const quantityWithPoints = useWatch({ name: "quantityWithPoints", }) function handleNextStep() { @@ -54,6 +58,11 @@ export default function ActionButtons({ quantityWithPoints, }) if (validatedQuantity.success) { + trackAddAncillary( + selectedAncillary, + quantityWithCard, + quantityWithPoints + ) if (isMobile) { selectQuantityAndDeliveryTime() } else { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx index c80c836dc..ca5318ddc 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx @@ -26,8 +26,14 @@ import { toast } from "@/components/TempDesignSystem/Toasts" import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking" import useLang from "@/hooks/useLang" import { formatPrice } from "@/utils/numberFormatting" +import { + trackAncillaryFailed, + trackAncillarySuccess, + trackGlaAncillaryAttempt, +} from "@/utils/tracking/myStay" import { + buildAncillaryPackages, clearAncillarySessionData, generateDeliveryOptions, getAncillarySessionData, @@ -113,128 +119,155 @@ export default function AddAncillaryFlowModal({ } const utils = trpc.useUtils() - const addAncillary = trpc.booking.packages.useMutation({ - onSuccess: (data, variables) => { - if (data) { - clearAncillarySessionData() - closeModal() - utils.booking.confirmation.invalidate({ - confirmationNumber: variables.confirmationNumber, - }) + const addAncillary = trpc.booking.packages.useMutation() - toast.success( - intl.formatMessage( - { id: "{ancillary} added to your booking!" }, - { ancillary: selectedAncillary?.title } - ) - ) - router.refresh() - } else { - toast.error(ancillaryErrorMessage) - } - }, - onError: () => { - toast.error(ancillaryErrorMessage) - }, - }) + const { guaranteeBooking, isLoading, handleGuaranteeError } = + useGuaranteeBooking({ + confirmationNumber: booking.confirmationNumber, + isAncillaryFlow: true, + }) - const { guaranteeBooking, isLoading } = useGuaranteeBooking({ - confirmationNumber: booking.confirmationNumber, - }) - - const onSubmit = (data: AncillaryFormData) => { + function validateTermsAndConditions(data: AncillaryFormData): boolean { if (!data.termsAndConditions) { formMethods.setError("termsAndConditions", { message: "You must accept the terms", }) - return + return false } + return true + } - setAncillarySessionData({ - formData: data, - selectedAncillary, - }) - if (booking.guaranteeInfo) { - const packagesToAdd = [] - if (selectedAncillary?.id && data.quantityWithCard) { - if (!isBreakfast) { - packagesToAdd.push({ - code: selectedAncillary.id, - quantity: data.quantityWithCard, - comment: data.optionalText || undefined, - }) - } else { - if (!breakfastData) { - toast.error( - intl.formatMessage({ - id: "Something went wrong!", - }) - ) - return - } - - packagesToAdd.push({ - code: BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, - quantity: breakfastData.nrOfAdults, - comment: data.optionalText || undefined, - }) - packagesToAdd.push({ - code: BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, - quantity: breakfastData.nrOfPayingChildren, - comment: data.optionalText || undefined, - }) - packagesToAdd.push({ - code: BreakfastPackageEnum.FREE_CHILD_BREAKFAST, - quantity: breakfastData.nrOfFreeChildren, - comment: data.optionalText || undefined, - }) - } - } - - if (selectedAncillary?.loyaltyCode && data.quantityWithPoints) { - packagesToAdd.push({ - code: selectedAncillary.loyaltyCode, - quantity: data.quantityWithPoints, - comment: data.optionalText || undefined, - }) - } - - addAncillary.mutate({ + function handleAncillarySubmission( + data: AncillaryFormData, + packages: { + code: string + quantity: number + comment: string | undefined + }[] + ) { + addAncillary.mutate( + { confirmationNumber: booking.confirmationNumber, ancillaryComment: data.optionalText, ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime ? data.deliveryTime : undefined, - packages: packagesToAdd, + packages: packages, language: lang, - }) - } else { - const savedCreditCard = savedCreditCards?.find( - (card) => card.id === data.paymentMethod - ) - if (booking.confirmationNumber) { - const card = savedCreditCard - ? { + }, + { + onSuccess: (result) => { + if (result) { + trackAncillarySuccess( + booking.confirmationNumber, + packages, + data.deliveryTime, + "ancillary", + selectedAncillary, + booking.guaranteeInfo?.cardType, + booking.roomTypeCode + ) + toast.success( + intl.formatMessage( + { id: "{ancillary} added to your booking!" }, + { ancillary: selectedAncillary?.title } + ) + ) + clearAncillarySessionData() + closeModal() + utils.booking.confirmation.invalidate({ + confirmationNumber: booking.confirmationNumber, + }) + router.refresh() + } else { + trackAncillaryFailed(packages, data.deliveryTime, selectedAncillary) + toast.error(ancillaryErrorMessage) + } + }, + onError: () => { + trackAncillaryFailed(packages, data.deliveryTime, selectedAncillary) + toast.error(ancillaryErrorMessage) + }, + } + ) + } + + function handleGuaranteePayment(data: AncillaryFormData, packages: any) { + const savedCreditCard = savedCreditCards?.find( + (card) => card.id === data.paymentMethod + ) + trackGlaAncillaryAttempt( + savedCreditCard, + packages, + selectedAncillary, + data.deliveryTime + ) + if (booking.confirmationNumber) { + const card = savedCreditCard + ? { alias: savedCreditCard.alias, expiryDate: savedCreditCard.expirationDate, cardType: savedCreditCard.cardType, } - : undefined - guaranteeBooking.mutate({ - confirmationNumber: booking.confirmationNumber, - language: lang, - ...(card && { card }), - success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}&ancillary=1`, - error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}&ancillary=1`, - cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}&ancillary=1`, - }) - } else { - toast.error( - intl.formatMessage({ - id: "Something went wrong!", - }) - ) - } + : undefined + guaranteeBooking.mutate({ + confirmationNumber: booking.confirmationNumber, + language: lang, + ...(card && { card }), + success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}&ancillary=1`, + error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}&ancillary=1`, + cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}&ancillary=1`, + }) + } else { + handleGuaranteeError("No confirmation number") + } + } + + function buildBreakfastPackages( + data: AncillaryFormData, + breakfastData: BreakfastData + ) { + return [ + { + code: BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, + quantity: breakfastData.nrOfAdults, + comment: data.optionalText || undefined, + }, + { + code: BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, + quantity: breakfastData.nrOfPayingChildren, + comment: data.optionalText || undefined, + }, + { + code: BreakfastPackageEnum.FREE_CHILD_BREAKFAST, + quantity: breakfastData.nrOfFreeChildren, + comment: data.optionalText || undefined, + }, + ] + } + + const onSubmit = (data: AncillaryFormData) => { + if (!validateTermsAndConditions(data)) return + + setAncillarySessionData({ + formData: data, + selectedAncillary, + }) + const packagesToAdd = !isBreakfast + ? buildAncillaryPackages(data, selectedAncillary) + : breakfastData + ? buildBreakfastPackages(data, breakfastData) + : [] + + if (isBreakfast && !breakfastData) { + toast.error(intl.formatMessage({ id: "Something went wrong!" })) + return + } + + if (booking.guaranteeInfo) { + handleAncillarySubmission(data, packagesToAdd) + } else { + handleGuaranteePayment(data, packagesToAdd) } } @@ -249,11 +282,17 @@ export default function AddAncillaryFlowModal({ queryParams.delete("errorCode") const savedData = getAncillarySessionData() if (savedData?.formData) { - formMethods.reset(savedData.formData) + const updatedFormData = { + ...savedData.formData, + paymentMethod: booking?.guaranteeInfo + ? PaymentMethodEnum.card + : savedData.formData.paymentMethod, + } + formMethods.reset(updatedFormData) } router.replace(`${pathname}?${queryParams.toString()}`) } - }, [searchParams, pathname, formMethods, router]) + }, [searchParams, pathname, formMethods, router, booking.guaranteeInfo]) useEffect(() => { setBreakfastData( diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx index 85a2af1e0..b72434548 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx @@ -1,6 +1,7 @@ import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" import { AncillaryCard } from "@/components/TempDesignSystem/AncillaryCard" +import { trackViewAncillary } from "@/utils/tracking/myStay" import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries" @@ -13,8 +14,15 @@ export default function WrappedAncillaryCard({ }: WrappedAncillaryProps) { const { description, ...ancillaryWithoutDescription } = ancillary const selectAncillary = useAddAncillaryStore((state) => state.selectAncillary) + return ( -
selectAncillary(ancillary)}> +
{ + selectAncillary(ancillary) + trackViewAncillary(ancillary) + }} + >
) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts index c22cc48b5..0e5e0300b 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts @@ -6,6 +6,7 @@ const quantitySchemaWithoutRefine = z.object({ quantityWithPoints: z.number().nullable(), quantityWithCard: z.number().nullable(), }) + export const quantitySchema = z .object({}) .merge(quantitySchemaWithoutRefine) @@ -35,4 +36,7 @@ export const ancillaryFormSchema = z } ) +export type AncillaryQuantityFormData = z.output< + typeof quantitySchemaWithoutRefine +> export type AncillaryFormData = z.output diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddedAncillaries/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddedAncillaries/index.tsx index a0fe351e8..53dba4495 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddedAncillaries/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddedAncillaries/index.tsx @@ -9,6 +9,7 @@ import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { trackRemoveAncillary } from "@/utils/tracking/myStay" import { getBreakfastPackagesFromAncillaryFlow } from "../../utils/hasBreakfastPackage" import RemoveButton from "./RemoveButton" @@ -180,7 +181,14 @@ export function AddedAncillaries({ confirmationNumber={booking.confirmationNumber} code={ancillary.code} title={ancillaryTitle} - onSuccess={router.refresh} + onSuccess={() => { + trackRemoveAncillary( + ancillary, + booking.hotelId, + booking.ancillary?.deliveryTime + ) + router.refresh() + }} />
) : null} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx index 2385a6791..e82f7e339 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx @@ -6,8 +6,16 @@ import { useEffect } from "react" import { trpc } from "@/lib/trpc/client" import LoadingSpinner from "@/components/LoadingSpinner" +import { + trackAncillaryFailed, + trackAncillarySuccess, +} from "@/utils/tracking/myStay" -import { clearAncillarySessionData, getAncillarySessionData } from "../utils" +import { + buildAncillaryPackages, + clearAncillarySessionData, + getAncillarySessionData, +} from "../utils" import type { Lang } from "@/constants/languages" @@ -22,15 +30,7 @@ export default function GuaranteeAncillaryHandler({ }) { const router = useRouter() - const addAncillary = trpc.booking.packages.useMutation({ - onSuccess: () => { - clearAncillarySessionData() - router.replace(returnUrl) - }, - onError: () => { - router.replace(`${returnUrl}&errorCode=AncillaryFailed`) - }, - }) + const addAncillary = trpc.booking.packages.useMutation() useEffect(() => { if (addAncillary.isPending || addAncillary.submittedAt) { @@ -44,33 +44,49 @@ export default function GuaranteeAncillaryHandler({ } const { formData, selectedAncillary } = sessionData - const packages = [] + const packages = buildAncillaryPackages(formData, selectedAncillary) - if (selectedAncillary?.id && formData.quantityWithCard) { - packages.push({ - code: selectedAncillary.id, - quantity: formData.quantityWithCard, - comment: formData.optionalText || undefined, - }) - } - - if (selectedAncillary?.loyaltyCode && formData.quantityWithPoints) { - packages.push({ - code: selectedAncillary.loyaltyCode, - quantity: formData.quantityWithPoints, - comment: formData.optionalText || undefined, - }) - } - - addAncillary.mutate({ - confirmationNumber, - ancillaryComment: formData.optionalText, - ancillaryDeliveryTime: selectedAncillary.requiresDeliveryTime - ? formData.deliveryTime - : undefined, - packages, - language: lang, - }) + addAncillary.mutate( + { + confirmationNumber, + ancillaryComment: formData.optionalText, + ancillaryDeliveryTime: selectedAncillary.requiresDeliveryTime + ? formData.deliveryTime + : undefined, + packages, + language: lang, + }, + { + onSuccess: (data) => { + if (data) { + trackAncillarySuccess( + confirmationNumber, + packages, + formData.deliveryTime, + "room + ancillary", + selectedAncillary + ) + clearAncillarySessionData() + router.replace(returnUrl) + } else { + trackAncillaryFailed( + packages, + formData.deliveryTime, + selectedAncillary + ) + router.replace(`${returnUrl}&errorCode=AncillaryFailed`) + } + }, + onError: () => { + trackAncillaryFailed( + packages, + formData.deliveryTime, + selectedAncillary + ) + router.replace(`${returnUrl}&errorCode=AncillaryFailed`) + }, + } + ) }, [confirmationNumber, returnUrl, addAncillary, lang, router]) return diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx index 31b3546da..86b8e1a3d 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx @@ -126,6 +126,8 @@ export function Ancillaries({ requiresDeliveryTime: false, loyaltyCode: undefined, points: undefined, + hotelId: Number(booking.hotelId), + categoryName: "Food", } : undefined @@ -135,6 +137,7 @@ export function Ancillaries({ booking.rateDefinition.breakfastIncluded, intl, packages, + booking.hotelId, ]) const allAncillaries = useMemo(() => { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts index 611ecaab7..97cd3bc9f 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts @@ -1,4 +1,7 @@ -import type { Ancillary } from "@/types/components/myPages/myStay/ancillaries" +import type { + Ancillary, + SelectedAncillary, +} from "@/types/components/myPages/myStay/ancillaries" import type { AncillaryFormData } from "./AddAncillaryFlow/schema" export const generateDeliveryOptions = () => { @@ -9,6 +12,32 @@ export const generateDeliveryOptions = () => { value: slot, })) } + +export function buildAncillaryPackages( + data: AncillaryFormData, + ancillary: SelectedAncillary | null +) { + const packages = [] + + if (ancillary?.id && data.quantityWithCard) { + packages.push({ + code: ancillary.id, + quantity: data.quantityWithCard, + comment: data.optionalText || undefined, + }) + } + + if (ancillary?.loyaltyCode && data.quantityWithPoints) { + packages.push({ + code: ancillary.loyaltyCode, + quantity: data.quantityWithPoints, + comment: data.optionalText || undefined, + }) + } + + return packages +} + const ancillarySessionKey = "ancillarySessionData" export const getAncillarySessionData = (): | { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx new file mode 100644 index 000000000..f95f48a52 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx @@ -0,0 +1,110 @@ +"use client" + +import { useRouter } from "next/navigation" +import { useEffect } from "react" + +import { PaymentCallbackStatusEnum } from "@/constants/booking" + +import LoadingSpinner from "@/components/LoadingSpinner" +import { trackEvent } from "@/utils/tracking/base" + +import { + buildAncillaryPackages, + getAncillarySessionData, +} from "../../Ancillaries/utils" + +interface TrackGuaranteeProps { + status: string + isAncillaryFlow?: boolean + redirectUrl: string + errorMessage?: string +} + +export default function TrackGuarantee({ + status, + isAncillaryFlow = false, + redirectUrl, + errorMessage, +}: TrackGuaranteeProps) { + const router = useRouter() + + useEffect(() => { + const trackAncillaryPaymentEvent = (event: string, status: string) => { + const sessionData = getAncillarySessionData() + const { formData, selectedAncillary } = sessionData || {} + + const packages = + formData && selectedAncillary + ? buildAncillaryPackages(formData, selectedAncillary) + : [] + + trackEvent({ + event, + paymentInfo: { status }, + ancillaries: packages.map((pkg) => ({ + hotelId: selectedAncillary?.hotelId, + productId: pkg.code, + productUnits: pkg.quantity, + productPoints: selectedAncillary?.points, + productDeliveryTime: formData?.deliveryTime, + productPrice: selectedAncillary?.price, + productName: selectedAncillary?.title, + productCategory: selectedAncillary?.categoryName, + })), + lateArrivalGuarantee: "yes", + guaranteedProduct: "room + ancillary", + }) + } + + const trackGuaranteePaymentEvent = (event: string, status: string) => { + trackEvent({ + event, + hotelInfo: { + lateArrivalGuarantee: "yes", + guaranteedProduct: "room", + }, + paymentInfo: { + status, + ...(errorMessage && { errorMessage }), + }, + }) + } + + switch (status) { + case PaymentCallbackStatusEnum.Success: + trackEvent({ + event: "guaranteeBookingSuccess", + hotelInfo: { + lateArrivalGuarantee: "yes", + guaranteedProduct: "room", + }, + }) + break + + case PaymentCallbackStatusEnum.Cancel: + isAncillaryFlow + ? trackAncillaryPaymentEvent( + "GuaranteeCancelAncillary", + "glacardsavecancelled" + ) + : trackGuaranteePaymentEvent( + "glaCardSaveCancelled", + "glacardsavecancelled" + ) + break + + case PaymentCallbackStatusEnum.Error: + isAncillaryFlow + ? trackAncillaryPaymentEvent( + "GuaranteeFailAncillary", + "glacardsavefailed" + ) + : trackGuaranteePaymentEvent("glaCardSaveFailed", "glacardsavefailed") + break + } + + router.replace(redirectUrl) + }, [status, isAncillaryFlow, redirectUrl, errorMessage, router]) + + return +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx index 76d35e8ff..503c64f05 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx @@ -66,10 +66,11 @@ export default function GuaranteeLateArrival({ }) const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}` - const { guaranteeBooking, isLoading } = useGuaranteeBooking({ - confirmationNumber: bookedRoom.confirmationNumber, - handleBookingCompleted: router.refresh, - }) + const { guaranteeBooking, isLoading, handleGuaranteeError } = + useGuaranteeBooking({ + confirmationNumber: bookedRoom.confirmationNumber, + handleBookingCompleted: router.refresh, + }) if (isLoading) { return ( @@ -83,12 +84,7 @@ export default function GuaranteeLateArrival({ const savedCreditCard = savedCreditCards?.find( (card) => card.id === data.paymentMethod ) - trackGlaSaveCardAttempt( - bookedRoom.hotelId, - data.paymentMethod, - savedCreditCard, - "yes" - ) + trackGlaSaveCardAttempt(bookedRoom.hotelId, savedCreditCard, "yes") if (bookedRoom.confirmationNumber) { const card = savedCreditCard ? { @@ -106,6 +102,7 @@ export default function GuaranteeLateArrival({ cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`, }) } else { + handleGuaranteeError("No confirmation number") toast.error( intl.formatMessage({ id: "Something went wrong!", diff --git a/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts index a9e46c0f5..639bb5828 100644 --- a/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts +++ b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts @@ -7,6 +7,7 @@ import { trpc } from "@/lib/trpc/client" import { toast } from "@/components/TempDesignSystem/Toasts" import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" +import { trackEvent } from "@/utils/tracking/base" const maxRetries = 15 const retryInterval = 2000 @@ -14,22 +15,39 @@ const retryInterval = 2000 export function useGuaranteeBooking({ confirmationNumber, handleBookingCompleted = () => {}, + isAncillaryFlow, }: { confirmationNumber: string handleBookingCompleted?: () => void + isAncillaryFlow?: boolean }) { const intl = useIntl() const router = useRouter() const [isPollingForBookingStatus, setIsPollingForBookingStatus] = useState(false) - const handlePaymentError = useCallback(() => { - toast.error( - intl.formatMessage({ - id: "We had an issue guaranteeing your booking. Please try again.", + const handleGuaranteeError = useCallback( + (errorMessage?: string) => { + trackEvent({ + event: "glaCardSaveFailed", + hotelInfo: { + lateArrivalGuarantee: "yes", + guaranteedProduct: isAncillaryFlow ? "room + ancillary" : "room", + }, + paymentInfo: { + status: "glacardsavefailed", + errorMessage, + }, }) - ) - }, [intl]) + toast.error( + intl.formatMessage({ + id: "We had an issue guaranteeing your booking. Please try again.", + }) + ) + }, + [intl, isAncillaryFlow] + ) + const utils = trpc.useUtils() const guaranteeBooking = trpc.booking.guarantee.useMutation({ onSuccess: (result, variables) => { @@ -43,15 +61,11 @@ export function useGuaranteeBooking({ }) } } else { - handlePaymentError() + handleGuaranteeError() } }, - onError: () => { - toast.error( - intl.formatMessage({ - id: "Something went wrong!", - }) - ) + onError: (error) => { + handleGuaranteeError(error.message) }, }) @@ -68,12 +82,12 @@ export function useGuaranteeBooking({ router.push(bookingStatus.data.paymentUrl) setIsPollingForBookingStatus(false) } else if (bookingStatus.isTimeout) { - handlePaymentError() + handleGuaranteeError("Timeout") } }, [ bookingStatus, router, - handlePaymentError, + handleGuaranteeError, setIsPollingForBookingStatus, isPollingForBookingStatus, ]) @@ -84,5 +98,9 @@ export function useGuaranteeBooking({ !bookingStatus.data?.paymentUrl && !bookingStatus.isTimeout) - return { guaranteeBooking, isLoading } + return { + guaranteeBooking, + isLoading, + handleGuaranteeError, + } } diff --git a/apps/scandic-web/server/routers/booking/mutation.ts b/apps/scandic-web/server/routers/booking/mutation.ts index 5a092f3f7..b079a169f 100644 --- a/apps/scandic-web/server/routers/booking/mutation.ts +++ b/apps/scandic-web/server/routers/booking/mutation.ts @@ -525,7 +525,6 @@ export const bookingMutationRouter = router({ const apiJson = await apiResponse.json() - console.log("apiJson", apiJson) const verifiedData = bookingConfirmationSchema.safeParse(apiJson) if (!verifiedData.success) { updateBookingFailCounter.add(1, { diff --git a/apps/scandic-web/server/routers/hotels/output.ts b/apps/scandic-web/server/routers/hotels/output.ts index 782a98640..75bd3c625 100644 --- a/apps/scandic-web/server/routers/hotels/output.ts +++ b/apps/scandic-web/server/routers/hotels/output.ts @@ -543,6 +543,7 @@ export const ancillaryPackagesSchema = z .object({ data: z.object({ attributes: z.object({ + hotelId: z.number(), ancillaries: z.array(ancillaryPackageSchema), }), }), @@ -554,6 +555,7 @@ export const ancillaryPackagesSchema = z ancillaryContent: ancillary.ancillaryContent .filter((item) => item.status === "Available") .map((item) => ({ + hotelId: data.attributes.hotelId, id: item.id, title: item.title, description: item.descriptions.html, @@ -565,6 +567,7 @@ export const ancillaryPackagesSchema = z points: item.variants.ancillaryLoyalty?.points, loyaltyCode: item.variants.ancillaryLoyalty?.code, requiresDeliveryTime: item.requiresDeliveryTime, + categoryName: ancillary.categoryName, })), })) .filter((ancillary) => ancillary.ancillaryContent.length > 0) diff --git a/apps/scandic-web/utils/tracking/myStay.ts b/apps/scandic-web/utils/tracking/myStay.ts index 99fb660ce..70e0af110 100644 --- a/apps/scandic-web/utils/tracking/myStay.ts +++ b/apps/scandic-web/utils/tracking/myStay.ts @@ -1,7 +1,7 @@ -import { PaymentMethodEnum } from "@/constants/booking" - import { trackEvent } from "./base" +import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries" +import type { PackageSchema } from "@/types/trpc/routers/booking/confirmation" import type { CreditCard } from "@/types/user" export function trackCancelStay(hotelId: string, bnr: string) { @@ -27,7 +27,6 @@ type LateArrivalGuarantee = "mandatory" | "yes" | "no" | "na" export function trackGlaSaveCardAttempt( hotelId: string, - paymentMethod: string | null, savedCreditCard: CreditCard | undefined, lateArrivalGuarantee: LateArrivalGuarantee ) { @@ -39,10 +38,173 @@ export function trackGlaSaveCardAttempt( guaranteedProduct: "room", }, paymentInfo: { - isCreditCard: paymentMethod === PaymentMethodEnum.card, isSavedCreditCard: !!savedCreditCard, status: "glacardsaveattempt", type: savedCreditCard?.cardType, }, }) } + +export function trackGlaAncillaryAttempt( + savedCreditCard: CreditCard | undefined, + packages: { + code: string + quantity: number + comment: string | undefined + }[], + selectedAncillary: SelectedAncillary | null, + deliveryTime: string | undefined +) { + trackEvent({ + event: "GuaranteeAttemptAncillary", + paymentInfo: { + isSavedCreditCard: !!savedCreditCard, + status: "glacardsaveattempt", + type: savedCreditCard?.cardType, + }, + ancillaries: packages.map((pkg) => ({ + hotelId: selectedAncillary?.hotelId, + productId: pkg.code, + productUnits: pkg.quantity, + productPoints: selectedAncillary?.points, + productDeliveryTime: deliveryTime, + productPrice: selectedAncillary?.price, + productName: selectedAncillary?.title, + productCategory: selectedAncillary?.categoryName, + })), + lateArrivalGuarantee: "yes", + guaranteedProduct: "room + ancillary", + }) +} + +export function trackAncillarySuccess( + confirmationNumber: string, + packages: { + code: string + quantity: number + comment?: string + }[], + deliveryTime: string | null | undefined, + guaranteedProduct: string, + selectedAncillary: SelectedAncillary | null, + cardType?: string, + roomTypeCode?: string +) { + trackEvent({ + event: "AncillarySuccess", + hotelInfo: { + bnr: confirmationNumber, + roomTypeCode: roomTypeCode, + }, + paymentInfo: { + status: "glacardsaveconfirmed", + type: cardType, + }, + ancillaries: packages.map((pkg) => ({ + productId: pkg.code, + productUnits: pkg.quantity, + productPoints: selectedAncillary?.points, + productDeliveryTime: deliveryTime, + productPrice: selectedAncillary?.price, + productName: selectedAncillary?.title, + productCategory: selectedAncillary?.categoryName, + })), + lateArrivalGuarantee: "yes", + guaranteedProduct: guaranteedProduct, + }) +} + +export function trackAncillaryFailed( + packages: { + code: string + quantity: number + comment?: string + }[], + deliveryTime: string | null | undefined, + selectedAncillary: SelectedAncillary | null +) { + trackEvent({ + event: "GuaranteeFailAncillary", + ancillaries: packages.map((pkg) => ({ + productId: pkg.code, + productUnits: pkg.quantity, + productPoints: selectedAncillary?.points, + productDeliveryTime: deliveryTime, + productPrice: selectedAncillary?.price, + productName: selectedAncillary?.title, + productCategory: selectedAncillary?.categoryName, + })), + lateArrivalGuarantee: "yes", + guaranteedProduct: "ancillary", + }) +} + +export function trackViewAncillary(ancillary: SelectedAncillary) { + trackEvent({ + event: "viewAncillary", + ancillaries: [ + { + hotelId: ancillary.hotelId, + productId: ancillary.id, + productName: ancillary.title, + productCategory: ancillary.categoryName, + }, + ], + }) +} + +export function trackRemoveAncillary( + ancillary: PackageSchema, + hotelId: string, + deliveryTime?: string +) { + trackEvent({ + event: "removeAncillary", + ancillaries: [ + { + hotelId, + productId: ancillary.code, + productPrice: ancillary.totalPrice, + productPoints: ancillary.points, + productUnits: ancillary.totalUnit, + productType: ancillary.type, + productDeliveryTime: deliveryTime, + }, + ], + }) +} + +export function trackAddAncillary( + ancillary: SelectedAncillary | null, + quantityWithCard: number | null, + quantityWithPoints: number | null +) { + const ancillaries = [] + if ((quantityWithCard ?? 0) > 0) { + ancillaries.push({ + hotelId: ancillary?.hotelId, + productId: ancillary?.id, + productName: ancillary?.title, + productUnits: quantityWithCard, + productPrice: ancillary?.price, + productPoints: ancillary?.points, + productCategory: ancillary?.categoryName, + }) + } + + if ((quantityWithPoints ?? 0) > 0) { + ancillaries.push({ + hotelId: ancillary?.hotelId, + productId: ancillary?.loyaltyCode, + productName: ancillary?.title, + productUnits: quantityWithPoints, + productPrice: ancillary?.price, + productPoints: ancillary?.points, + productCategory: ancillary?.categoryName, + }) + } + trackEvent({ + event: "addAncillary", + ancillaries, + }) +}