Merged in chore/refactor-paymentclient (pull request #3219)
chore: Refactor PaymentClient * Extract function mustGuaranteeBooking * Break apart PaymentClient Approved-by: Joakim Jäderberg
This commit is contained in:
@@ -32,7 +32,6 @@ import { env } from "../../../../env/client"
|
|||||||
import { useBookingFlowContext } from "../../../hooks/useBookingFlowContext"
|
import { useBookingFlowContext } from "../../../hooks/useBookingFlowContext"
|
||||||
import { clearBookingWidgetState } from "../../../hooks/useBookingWidgetState"
|
import { clearBookingWidgetState } from "../../../hooks/useBookingWidgetState"
|
||||||
import { useHandleBookingStatus } from "../../../hooks/useHandleBookingStatus"
|
import { useHandleBookingStatus } from "../../../hooks/useHandleBookingStatus"
|
||||||
import { useIsLoggedIn } from "../../../hooks/useIsLoggedIn"
|
|
||||||
import useLang from "../../../hooks/useLang"
|
import useLang from "../../../hooks/useLang"
|
||||||
import { useEnterDetailsStore } from "../../../stores/enter-details"
|
import { useEnterDetailsStore } from "../../../stores/enter-details"
|
||||||
import ConfirmBooking from "../Confirm"
|
import ConfirmBooking from "../Confirm"
|
||||||
@@ -44,6 +43,7 @@ import {
|
|||||||
hasFlexibleRate,
|
hasFlexibleRate,
|
||||||
hasPrepaidRate,
|
hasPrepaidRate,
|
||||||
isPaymentMethodEnum,
|
isPaymentMethodEnum,
|
||||||
|
mustGuaranteeBooking,
|
||||||
writePaymentInfoToSessionStorage,
|
writePaymentInfoToSessionStorage,
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
import { type PaymentFormData, paymentSchema } from "./schema"
|
import { type PaymentFormData, paymentSchema } from "./schema"
|
||||||
@@ -51,6 +51,7 @@ import { getPaymentHeadingConfig } from "./utils"
|
|||||||
|
|
||||||
import styles from "./payment.module.css"
|
import styles from "./payment.module.css"
|
||||||
|
|
||||||
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
|
import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
|
||||||
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
|
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
|
||||||
|
|
||||||
@@ -74,10 +75,13 @@ export default function PaymentClient({
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const isUserLoggedIn = useIsLoggedIn()
|
|
||||||
const { getTopOffset } = useStickyPosition({})
|
const { getTopOffset } = useStickyPosition({})
|
||||||
const { user } = useBookingFlowContext()
|
const { user, isLoggedIn } = useBookingFlowContext()
|
||||||
|
const [refId, setRefId] = useState("")
|
||||||
|
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
||||||
|
useState(false)
|
||||||
|
const [priceChangeData, setPriceChangeData] =
|
||||||
|
useState<PriceChangeData | null>(null)
|
||||||
const [showBookingAlert, setShowBookingAlert] = useState(false)
|
const [showBookingAlert, setShowBookingAlert] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -96,35 +100,16 @@ export default function PaymentClient({
|
|||||||
runPreSubmitCallbacks: state.actions.runPreSubmitCallbacks,
|
runPreSubmitCallbacks: state.actions.runPreSubmitCallbacks,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => {
|
const bookingMustBeGuaranteed = mustGuaranteeBooking({
|
||||||
if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) {
|
isUserLoggedIn: isLoggedIn,
|
||||||
return true
|
booking,
|
||||||
}
|
rooms,
|
||||||
|
|
||||||
if (
|
|
||||||
(room.guest.join || room.guest.membershipNo) &&
|
|
||||||
booking.rooms[idx].counterRateCode
|
|
||||||
) {
|
|
||||||
return room.memberMustBeGuaranteed
|
|
||||||
}
|
|
||||||
|
|
||||||
return room.mustBeGuaranteed
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [refId, setRefId] = useState("")
|
|
||||||
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
|
||||||
useState(false)
|
|
||||||
|
|
||||||
const [priceChangeData, setPriceChangeData] =
|
|
||||||
useState<PriceChangeData | null>(null)
|
|
||||||
|
|
||||||
const { toDate, fromDate, hotelId } = booking
|
|
||||||
|
|
||||||
const hasPrepaidRates = rooms.some(hasPrepaidRate)
|
const hasPrepaidRates = rooms.some(hasPrepaidRate)
|
||||||
const hasFlexRates = rooms.some(hasFlexibleRate)
|
const hasFlexRates = rooms.some(hasFlexibleRate)
|
||||||
const hasOnlyFlexRates = rooms.every(hasFlexibleRate)
|
const hasOnlyFlexRates = rooms.every(hasFlexibleRate)
|
||||||
const hasMixedRates = hasPrepaidRates && hasFlexRates
|
const hasMixedRates = hasPrepaidRates && hasFlexRates
|
||||||
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
|
|
||||||
|
|
||||||
const methods = useForm<PaymentFormData>({
|
const methods = useForm<PaymentFormData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -171,9 +156,9 @@ export default function PaymentClient({
|
|||||||
|
|
||||||
const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata)
|
const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata)
|
||||||
if (hasPriceChange) {
|
if (hasPriceChange) {
|
||||||
const priceChangeData = booking.rooms.map(
|
const priceChangeData = booking.rooms
|
||||||
(room) => room.priceChangedMetadata || null
|
.map((room) => room.priceChangedMetadata || null)
|
||||||
)
|
.filter(isNotNull)
|
||||||
setPriceChangeData(priceChangeData)
|
setPriceChangeData(priceChangeData)
|
||||||
} else {
|
} else {
|
||||||
setIsPollingForBookingStatus(true)
|
setIsPollingForBookingStatus(true)
|
||||||
@@ -204,19 +189,9 @@ export default function PaymentClient({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const bookingStatus = useHandleBookingStatus({
|
const { toDate, fromDate, hotelId } = booking
|
||||||
refId,
|
const trackPaymentError = useCallback(
|
||||||
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
|
||||||
maxRetries,
|
|
||||||
retryInterval,
|
|
||||||
enabled: isPollingForBookingStatus,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handlePaymentError = useCallback(
|
|
||||||
(errorMessage: string) => {
|
(errorMessage: string) => {
|
||||||
setShowBookingAlert(true)
|
|
||||||
setIsSubmitting(false)
|
|
||||||
|
|
||||||
const currentPaymentMethod = methods.getValues("paymentMethod")
|
const currentPaymentMethod = methods.getValues("paymentMethod")
|
||||||
const smsEnable = methods.getValues("smsConfirmation")
|
const smsEnable = methods.getValues("smsConfirmation")
|
||||||
const guarantee = methods.getValues("guarantee")
|
const guarantee = methods.getValues("guarantee")
|
||||||
@@ -255,52 +230,30 @@ export default function PaymentClient({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
methods,
|
|
||||||
savedCreditCards,
|
|
||||||
hotelId,
|
|
||||||
bookingMustBeGuaranteed,
|
bookingMustBeGuaranteed,
|
||||||
hasOnlyFlexRates,
|
hasOnlyFlexRates,
|
||||||
setIsSubmitting,
|
hotelId,
|
||||||
|
methods,
|
||||||
|
savedCreditCards,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const handlePaymentError = useCallback(
|
||||||
if (bookingStatus?.data?.booking.paymentUrl) {
|
(errorMessage: string) => {
|
||||||
router.push(bookingStatus.data.booking.paymentUrl)
|
setShowBookingAlert(true)
|
||||||
} else if (
|
setIsSubmitting(false)
|
||||||
bookingStatus?.data?.booking.reservationStatus ===
|
|
||||||
BookingStatusEnum.BookingCompleted
|
|
||||||
) {
|
|
||||||
const mainRoom = bookingStatus.data.booking.rooms[0]
|
|
||||||
clearBookingWidgetState()
|
|
||||||
// Cookie is used by Booking Confirmation page to validate that the user came from payment callback
|
|
||||||
document.cookie = `bcsig=${bookingStatus.data.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
|
|
||||||
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
|
|
||||||
router.push(confirmationUrl)
|
|
||||||
} else if (bookingStatus.isTimeout) {
|
|
||||||
handlePaymentError("Timeout")
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
bookingStatus.data,
|
|
||||||
bookingStatus.isTimeout,
|
|
||||||
router,
|
|
||||||
intl,
|
|
||||||
lang,
|
|
||||||
handlePaymentError,
|
|
||||||
])
|
|
||||||
|
|
||||||
const getPaymentMethod = useCallback(
|
trackPaymentError(errorMessage)
|
||||||
(paymentMethod: string | null | undefined): PaymentMethodEnum => {
|
|
||||||
if (hasFlexRates) {
|
|
||||||
return PaymentMethodEnum.card
|
|
||||||
}
|
|
||||||
return paymentMethod && isPaymentMethodEnum(paymentMethod)
|
|
||||||
? paymentMethod
|
|
||||||
: PaymentMethodEnum.card
|
|
||||||
},
|
},
|
||||||
[hasFlexRates]
|
[setIsSubmitting, trackPaymentError]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useBookingStatusRedirect({
|
||||||
|
refId,
|
||||||
|
enabled: isPollingForBookingStatus,
|
||||||
|
onError: handlePaymentError,
|
||||||
|
})
|
||||||
|
|
||||||
const scrollToInvalidField = useCallback(async (): Promise<boolean> => {
|
const scrollToInvalidField = useCallback(async (): Promise<boolean> => {
|
||||||
// If any room is not complete/valid, scroll to the first invalid field, this is needed as rooms and other fields are in separate forms
|
// If any room is not complete/valid, scroll to the first invalid field, this is needed as rooms and other fields are in separate forms
|
||||||
|
|
||||||
@@ -308,20 +261,12 @@ export default function PaymentClient({
|
|||||||
const errorNames = Object.keys(methods.formState.errors)
|
const errorNames = Object.keys(methods.formState.errors)
|
||||||
const firstIncompleteRoomIndex = rooms.findIndex((room) => !room.isComplete)
|
const firstIncompleteRoomIndex = rooms.findIndex((room) => !room.isComplete)
|
||||||
|
|
||||||
const scrollToElement = (el: HTMLElement) => {
|
|
||||||
const offset = getTopOffset()
|
|
||||||
const top = el.getBoundingClientRect().top + window.scrollY - offset - 20
|
|
||||||
window.scrollTo({ top, behavior: "smooth" })
|
|
||||||
const input = el.querySelector<HTMLElement>("input")
|
|
||||||
input?.focus({ preventScroll: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invalidField) {
|
if (invalidField) {
|
||||||
scrollToElement(invalidField)
|
scrollToElement(invalidField, getTopOffset())
|
||||||
} else if (errorNames.length > 0) {
|
} else if (errorNames.length > 0) {
|
||||||
const firstErrorEl = document.querySelector(`[name="${errorNames[0]}"]`)
|
const firstErrorEl = document.querySelector(`[name="${errorNames[0]}"]`)
|
||||||
if (firstErrorEl) {
|
if (firstErrorEl) {
|
||||||
scrollToElement(firstErrorEl as HTMLElement)
|
scrollToElement(firstErrorEl as HTMLElement, getTopOffset())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,63 +283,32 @@ export default function PaymentClient({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentMethod = getPaymentMethod(data.paymentMethod)
|
const paymentMethod = getPaymentMethod(data.paymentMethod, hasFlexRates)
|
||||||
|
|
||||||
const savedCreditCard = savedCreditCards?.find(
|
const savedCreditCard = savedCreditCards?.find(
|
||||||
(card) => card.id === data.paymentMethod
|
(card) => card.id === data.paymentMethod
|
||||||
)
|
)
|
||||||
|
|
||||||
const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
|
|
||||||
const guarantee = data.guarantee
|
const guarantee = data.guarantee
|
||||||
const useSavedCard = savedCreditCard
|
|
||||||
? {
|
|
||||||
card: {
|
|
||||||
alias: savedCreditCard.alias,
|
|
||||||
expiryDate: savedCreditCard.expirationDate,
|
|
||||||
cardType: savedCreditCard.cardType,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
|
|
||||||
const shouldUsePayment =
|
const shouldUsePayment =
|
||||||
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
|
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
|
||||||
const payment = shouldUsePayment
|
const payment = shouldUsePayment
|
||||||
? {
|
? getPaymentData({ paymentMethod, savedCreditCard, lang })
|
||||||
paymentMethod: paymentMethod,
|
|
||||||
...useSavedCard,
|
|
||||||
success: `${paymentRedirectUrl}/success`,
|
|
||||||
error: `${paymentRedirectUrl}/error`,
|
|
||||||
cancel: `${paymentRedirectUrl}/cancel`,
|
|
||||||
}
|
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const paymentMethodType = savedCreditCard
|
const paymentMethodType = savedCreditCard
|
||||||
? savedCreditCard.type
|
? savedCreditCard.type
|
||||||
: paymentMethod
|
: paymentMethod
|
||||||
if (guarantee || (bookingMustBeGuaranteed && hasOnlyFlexRates)) {
|
trackPaymentEvents({
|
||||||
const lateArrivalGuarantee = guarantee ? "yes" : "mandatory"
|
isSavedCreditCard: !!savedCreditCard,
|
||||||
writeGlaToSessionStorage(
|
paymentMethodType,
|
||||||
lateArrivalGuarantee,
|
guarantee,
|
||||||
hotelId,
|
smsEnable: data.smsConfirmation,
|
||||||
paymentMethodType,
|
bookingMustBeGuaranteed,
|
||||||
!!savedCreditCard
|
hasOnlyFlexRates,
|
||||||
)
|
hotelId,
|
||||||
trackGlaSaveCardAttempt({
|
})
|
||||||
hotelId,
|
|
||||||
hasSavedCreditCard: !!savedCreditCard,
|
|
||||||
creditCardType: savedCreditCard?.cardType,
|
|
||||||
lateArrivalGuarantee,
|
|
||||||
})
|
|
||||||
} else if (!hasOnlyFlexRates) {
|
|
||||||
trackPaymentEvent({
|
|
||||||
event: "paymentAttemptStart",
|
|
||||||
hotelId,
|
|
||||||
method: paymentMethodType,
|
|
||||||
isSavedCreditCard: !!savedCreditCard,
|
|
||||||
smsEnable: data.smsConfirmation,
|
|
||||||
status: "attempt",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
writePaymentInfoToSessionStorage(paymentMethodType, !!savedCreditCard)
|
|
||||||
|
|
||||||
const payload: CreateBookingInput = {
|
const payload: CreateBookingInput = {
|
||||||
checkInDate: fromDate,
|
checkInDate: fromDate,
|
||||||
@@ -406,7 +320,7 @@ export default function PaymentClient({
|
|||||||
({ room }, idx): CreateBookingInput["rooms"][number] => {
|
({ room }, idx): CreateBookingInput["rooms"][number] => {
|
||||||
const isMainRoom = idx === 0
|
const isMainRoom = idx === 0
|
||||||
let rateCode = ""
|
let rateCode = ""
|
||||||
if (isMainRoom && isUserLoggedIn) {
|
if (isMainRoom && isLoggedIn) {
|
||||||
rateCode = booking.rooms[idx].rateCode
|
rateCode = booking.rooms[idx].rateCode
|
||||||
} else if (
|
} else if (
|
||||||
(room.guest.join || room.guest.membershipNo) &&
|
(room.guest.join || room.guest.membershipNo) &&
|
||||||
@@ -500,17 +414,17 @@ export default function PaymentClient({
|
|||||||
[
|
[
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
scrollToInvalidField,
|
scrollToInvalidField,
|
||||||
getPaymentMethod,
|
hasFlexRates,
|
||||||
savedCreditCards,
|
savedCreditCards,
|
||||||
lang,
|
|
||||||
bookingMustBeGuaranteed,
|
bookingMustBeGuaranteed,
|
||||||
hasOnlyFlexRates,
|
hasOnlyFlexRates,
|
||||||
|
lang,
|
||||||
fromDate,
|
fromDate,
|
||||||
toDate,
|
toDate,
|
||||||
hotelId,
|
hotelId,
|
||||||
rooms,
|
rooms,
|
||||||
initiateBooking,
|
initiateBooking,
|
||||||
isUserLoggedIn,
|
isLoggedIn,
|
||||||
booking.rooms,
|
booking.rooms,
|
||||||
user?.data?.partnerLoyaltyNumber,
|
user?.data?.partnerLoyaltyNumber,
|
||||||
]
|
]
|
||||||
@@ -526,6 +440,7 @@ export default function PaymentClient({
|
|||||||
const { preHeading, heading, subHeading, showLearnMore } =
|
const { preHeading, heading, subHeading, showLearnMore } =
|
||||||
getPaymentHeadingConfig(intl, bookingMustBeGuaranteed, hasOnlyFlexRates)
|
getPaymentHeadingConfig(intl, bookingMustBeGuaranteed, hasOnlyFlexRates)
|
||||||
|
|
||||||
|
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cx(styles.paymentSection, {
|
className={cx(styles.paymentSection, {
|
||||||
@@ -599,3 +514,148 @@ export default function PaymentClient({
|
|||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollToElement = (el: HTMLElement, offset: number) => {
|
||||||
|
const top = el.getBoundingClientRect().top + window.scrollY - offset - 20
|
||||||
|
window.scrollTo({ top, behavior: "smooth" })
|
||||||
|
const input = el.querySelector<HTMLElement>("input")
|
||||||
|
input?.focus({ preventScroll: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPaymentMethod = (
|
||||||
|
paymentMethod: string | null | undefined,
|
||||||
|
hasFlexRates: boolean
|
||||||
|
): PaymentMethodEnum => {
|
||||||
|
if (hasFlexRates) {
|
||||||
|
return PaymentMethodEnum.card
|
||||||
|
}
|
||||||
|
return paymentMethod && isPaymentMethodEnum(paymentMethod)
|
||||||
|
? paymentMethod
|
||||||
|
: PaymentMethodEnum.card
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPaymentCallbackUrl(lang: Lang) {
|
||||||
|
return `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBookingStatusRedirect({
|
||||||
|
refId,
|
||||||
|
enabled,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
refId: string
|
||||||
|
enabled: boolean
|
||||||
|
onError: (errorMessage: string) => void
|
||||||
|
}) {
|
||||||
|
const router = useRouter()
|
||||||
|
const lang = useLang()
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const bookingStatus = useHandleBookingStatus({
|
||||||
|
refId,
|
||||||
|
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
||||||
|
maxRetries,
|
||||||
|
retryInterval,
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bookingStatus?.data?.booking.paymentUrl) {
|
||||||
|
router.push(bookingStatus.data.booking.paymentUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
bookingStatus?.data?.booking.reservationStatus ===
|
||||||
|
BookingStatusEnum.BookingCompleted
|
||||||
|
) {
|
||||||
|
const mainRoom = bookingStatus.data.booking.rooms[0]
|
||||||
|
clearBookingWidgetState()
|
||||||
|
// Cookie is used by Booking Confirmation page to validate that the user came from payment callback
|
||||||
|
document.cookie = `bcsig=${bookingStatus.data.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
|
||||||
|
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
|
||||||
|
router.push(confirmationUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookingStatus.isTimeout) {
|
||||||
|
onError("Timeout")
|
||||||
|
}
|
||||||
|
}, [bookingStatus.data, bookingStatus.isTimeout, router, intl, lang, onError])
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaymentData({
|
||||||
|
paymentMethod,
|
||||||
|
savedCreditCard,
|
||||||
|
lang,
|
||||||
|
}: {
|
||||||
|
paymentMethod: PaymentMethodEnum
|
||||||
|
savedCreditCard?: CreditCard
|
||||||
|
lang: Lang
|
||||||
|
}) {
|
||||||
|
const paymentRedirectUrl = createPaymentCallbackUrl(lang)
|
||||||
|
|
||||||
|
return {
|
||||||
|
paymentMethod: paymentMethod,
|
||||||
|
success: `${paymentRedirectUrl}/success`,
|
||||||
|
error: `${paymentRedirectUrl}/error`,
|
||||||
|
cancel: `${paymentRedirectUrl}/cancel`,
|
||||||
|
card: savedCreditCard
|
||||||
|
? {
|
||||||
|
alias: savedCreditCard.alias,
|
||||||
|
expiryDate: savedCreditCard.expirationDate,
|
||||||
|
cardType: savedCreditCard.cardType,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNotNull<T>(value: T | null): value is T {
|
||||||
|
return value !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackPaymentEvents(data: {
|
||||||
|
isSavedCreditCard: boolean
|
||||||
|
paymentMethodType: string
|
||||||
|
guarantee: boolean
|
||||||
|
smsEnable: boolean
|
||||||
|
bookingMustBeGuaranteed: boolean
|
||||||
|
hasOnlyFlexRates: boolean
|
||||||
|
hotelId: string
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
isSavedCreditCard,
|
||||||
|
paymentMethodType,
|
||||||
|
guarantee,
|
||||||
|
smsEnable,
|
||||||
|
bookingMustBeGuaranteed,
|
||||||
|
hasOnlyFlexRates,
|
||||||
|
hotelId,
|
||||||
|
} = data
|
||||||
|
|
||||||
|
if (guarantee || (bookingMustBeGuaranteed && hasOnlyFlexRates)) {
|
||||||
|
const lateArrivalGuarantee = guarantee ? "yes" : "mandatory"
|
||||||
|
writeGlaToSessionStorage(
|
||||||
|
lateArrivalGuarantee,
|
||||||
|
hotelId,
|
||||||
|
paymentMethodType,
|
||||||
|
isSavedCreditCard
|
||||||
|
)
|
||||||
|
trackGlaSaveCardAttempt({
|
||||||
|
hotelId,
|
||||||
|
hasSavedCreditCard: isSavedCreditCard,
|
||||||
|
creditCardType: isSavedCreditCard ? paymentMethodType : undefined,
|
||||||
|
lateArrivalGuarantee,
|
||||||
|
})
|
||||||
|
} else if (!hasOnlyFlexRates) {
|
||||||
|
trackPaymentEvent({
|
||||||
|
event: "paymentAttemptStart",
|
||||||
|
hotelId,
|
||||||
|
method: paymentMethodType,
|
||||||
|
isSavedCreditCard,
|
||||||
|
smsEnable,
|
||||||
|
status: "attempt",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
writePaymentInfoToSessionStorage(paymentMethodType, isSavedCreditCard)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import { mustGuaranteeBooking } from "./helpers"
|
||||||
|
|
||||||
|
const buildRoom = (
|
||||||
|
overrides: Partial<{
|
||||||
|
memberMustBeGuaranteed: boolean
|
||||||
|
mustBeGuaranteed: boolean
|
||||||
|
guest: { join: boolean; membershipNo?: string }
|
||||||
|
}> = {}
|
||||||
|
) => ({
|
||||||
|
room: {
|
||||||
|
memberMustBeGuaranteed: false,
|
||||||
|
mustBeGuaranteed: false,
|
||||||
|
guest: { join: false, membershipNo: undefined },
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("mustGuaranteeBooking", () => {
|
||||||
|
it("returns true when the first room requires a member guarantee for a logged-in user", () => {
|
||||||
|
const rooms = [
|
||||||
|
buildRoom({
|
||||||
|
memberMustBeGuaranteed: true,
|
||||||
|
mustBeGuaranteed: false,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const booking = { rooms: [{}] }
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mustGuaranteeBooking({
|
||||||
|
isUserLoggedIn: true,
|
||||||
|
booking,
|
||||||
|
rooms,
|
||||||
|
})
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns memberMustBeGuaranteed when guest has membership details and counter rate code", () => {
|
||||||
|
const rooms = [
|
||||||
|
buildRoom(),
|
||||||
|
buildRoom({
|
||||||
|
memberMustBeGuaranteed: true,
|
||||||
|
guest: { join: true },
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const booking = { rooms: [{}, { counterRateCode: "COUNTER" }] }
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mustGuaranteeBooking({
|
||||||
|
isUserLoggedIn: false,
|
||||||
|
booking,
|
||||||
|
rooms,
|
||||||
|
})
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false when member condition is not met despite counter rate code", () => {
|
||||||
|
const rooms = [
|
||||||
|
buildRoom(),
|
||||||
|
buildRoom({
|
||||||
|
memberMustBeGuaranteed: false,
|
||||||
|
guest: { join: true },
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const booking = { rooms: [{}, { counterRateCode: "COUNTER" }] }
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mustGuaranteeBooking({
|
||||||
|
isUserLoggedIn: false,
|
||||||
|
booking,
|
||||||
|
rooms,
|
||||||
|
})
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("falls back to mustBeGuaranteed when no member-specific rules apply", () => {
|
||||||
|
const rooms = [
|
||||||
|
buildRoom({
|
||||||
|
memberMustBeGuaranteed: false,
|
||||||
|
mustBeGuaranteed: true,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const booking = { rooms: [{}] }
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mustGuaranteeBooking({
|
||||||
|
isUserLoggedIn: false,
|
||||||
|
booking,
|
||||||
|
rooms,
|
||||||
|
})
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -89,3 +89,37 @@ export function writePaymentInfoToSessionStorage(
|
|||||||
export function clearPaymentInfoSessionStorage() {
|
export function clearPaymentInfoSessionStorage() {
|
||||||
sessionStorage.removeItem(paymentInfoStorageName)
|
sessionStorage.removeItem(paymentInfoStorageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mustGuaranteeBooking({
|
||||||
|
isUserLoggedIn,
|
||||||
|
booking,
|
||||||
|
rooms,
|
||||||
|
}: {
|
||||||
|
isUserLoggedIn: boolean
|
||||||
|
booking: { rooms: { counterRateCode?: string }[] }
|
||||||
|
rooms: {
|
||||||
|
room: {
|
||||||
|
memberMustBeGuaranteed?: boolean
|
||||||
|
mustBeGuaranteed: boolean
|
||||||
|
guest: {
|
||||||
|
join: boolean
|
||||||
|
membershipNo?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
}) {
|
||||||
|
return rooms.some(({ room }, idx) => {
|
||||||
|
if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(room.guest.join || room.guest.membershipNo) &&
|
||||||
|
booking.rooms[idx].counterRateCode
|
||||||
|
) {
|
||||||
|
return room.memberMustBeGuaranteed
|
||||||
|
}
|
||||||
|
|
||||||
|
return room.mustBeGuaranteed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ export type PriceChangeData = Array<{
|
|||||||
roomPrice: number
|
roomPrice: number
|
||||||
totalPrice: number
|
totalPrice: number
|
||||||
packagePrice?: number
|
packagePrice?: number
|
||||||
} | null>
|
}>
|
||||||
|
|||||||
Reference in New Issue
Block a user