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:
@@ -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 />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 = ():
|
||||
| {
|
||||
|
||||
@@ -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 />
|
||||
}
|
||||
@@ -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!",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user