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
This commit is contained in:
Bianca Widstam
2025-04-01 09:38:36 +00:00
parent e6c9e25222
commit 35c1724afb
15 changed files with 596 additions and 182 deletions

View File

@@ -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 <TrackGuarantee status={status} redirectUrl={myStayUrl} />
}
let errorMessage = undefined
@@ -86,7 +87,15 @@ export default async function GuaranteePaymentCallbackPage({
if (isAncillaryFlow) {
searchObject.set("ancillary", "ancillary")
}
redirect(`${myStayUrl}&${searchObject.toString()}`)
return (
<TrackGuarantee
status={status}
isAncillaryFlow={!!isAncillaryFlow}
redirectUrl={`${myStayUrl}&${searchObject.toString()}`}
errorMessage={errorMessage}
/>
)
}
return <LoadingSpinner />

View File

@@ -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<AncillaryFormData>({
const quantityWithCard = useWatch<AncillaryQuantityFormData>({
name: "quantityWithCard",
})
const quantityWithPoints = useWatch<AncillaryFormData>({
const quantityWithPoints = useWatch<AncillaryQuantityFormData>({
name: "quantityWithPoints",
})
function handleNextStep() {
@@ -54,6 +58,11 @@ export default function ActionButtons({
quantityWithPoints,
})
if (validatedQuantity.success) {
trackAddAncillary(
selectedAncillary,
quantityWithCard,
quantityWithPoints
)
if (isMobile) {
selectQuantityAndDeliveryTime()
} else {

View File

@@ -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(

View File

@@ -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 (
<div role="button" onClick={() => selectAncillary(ancillary)}>
<div
role="button"
onClick={() => {
selectAncillary(ancillary)
trackViewAncillary(ancillary)
}}
>
<AncillaryCard ancillary={ancillaryWithoutDescription} />
</div>
)

View File

@@ -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<typeof ancillaryFormSchema>

View File

@@ -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()
}}
/>
</div>
) : null}

View File

@@ -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 <LoadingSpinner />

View File

@@ -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(() => {

View File

@@ -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 = ():
| {

View File

@@ -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 <LoadingSpinner />
}

View File

@@ -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!",

View File

@@ -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,
}
}

View File

@@ -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, {

View File

@@ -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)

View File

@@ -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,
})
}