Merged in feat/sw-3642-inject-sas-eb-payment (pull request #3243)

feat(SW-3642): Enable SAS EB payments

* Wip add SAS eb payment

* Add validate payment call

* Check booking status payment method to determine validation

* Clean up getPaymentData

* Fix PartnerPoints casing

* Add comment for validatePartnerPayment error handling

* Remove comment


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-12-11 13:23:12 +00:00
parent eb90c382b8
commit 7faa9933a2
19 changed files with 489 additions and 110 deletions

View File

@@ -79,7 +79,7 @@ export default function RewardNight() {
}
}, [resetOnMultiroomError, ref])
if (!config.redemptionEnabled) return null
if (config.redemptionType === "disabled") return null
return (
<div ref={ref} onBlur={(e) => closeOnBlur(e.nativeEvent)}>

View File

@@ -41,7 +41,7 @@ export default function FormContent({
const {
formState: { errors, isDirty },
} = useFormContext<BookingWidgetSchema>()
const { bookingCodeEnabled, redemptionEnabled } = useBookingFlowConfig()
const { bookingCodeEnabled, redemptionType } = useBookingFlowConfig()
const searchParams = useSearchParams()
const focusWidget = searchParams.get(FOCUS_WIDGET) === "true"
useEffect(() => {
@@ -126,7 +126,7 @@ export default function FormContent({
)}
style={{
// TODO: Remove this when redemption is enabled for partner-sas
display: redemptionEnabled ? undefined : "none",
display: redemptionType !== "disabled" ? undefined : "none",
}}
>
<Voucher />

View File

@@ -28,7 +28,7 @@ import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { env } from "../../../../env/client"
import { useBookingFlowConfig } from "../../../bookingFlowConfig/bookingFlowConfigContext"
import { useBookingFlowContext } from "../../../hooks/useBookingFlowContext"
import { clearBookingWidgetState } from "../../../hooks/useBookingWidgetState"
import { useHandleBookingStatus } from "../../../hooks/useHandleBookingStatus"
@@ -40,9 +40,10 @@ import { writeGlaToSessionStorage } from "./PaymentCallback/helpers"
import BookingAlert from "./BookingAlert"
import { GuaranteeInfo } from "./GuaranteeInfo"
import {
getPaymentData,
getPaymentMethod,
hasFlexibleRate,
hasPrepaidRate,
isPaymentMethodEnum,
mustGuaranteeBooking,
writePaymentInfoToSessionStorage,
} from "./helpers"
@@ -51,7 +52,6 @@ import { getPaymentHeadingConfig } from "./utils"
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 { CreditCard } from "@scandic-hotels/trpc/types/user"
@@ -77,6 +77,7 @@ export default function PaymentClient({
const searchParams = useSearchParams()
const { getTopOffset } = useStickyPosition({})
const { user, isLoggedIn } = useBookingFlowContext()
const { redemptionType } = useBookingFlowConfig()
const [refId, setRefId] = useState("")
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
useState(false)
@@ -273,6 +274,7 @@ export default function PaymentClient({
return firstIncompleteRoomIndex !== -1
}, [runPreSubmitCallbacks, rooms, methods.formState.errors, getTopOffset])
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
const handleSubmit = useCallback(
async (data: PaymentFormData) => {
setIsSubmitting(true)
@@ -283,19 +285,26 @@ export default function PaymentClient({
return
}
const paymentMethod = getPaymentMethod(data.paymentMethod, hasFlexRates)
const savedCreditCard = savedCreditCards?.find(
(card) => card.id === data.paymentMethod
)
const guarantee = data.guarantee
const shouldUsePayment =
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
const payment = shouldUsePayment
? getPaymentData({ paymentMethod, savedCreditCard, lang })
: undefined
const paymentMethod = getPaymentMethod({
paymentMethod: data.paymentMethod,
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
const payment = getPaymentData({
guarantee,
bookingMustBeGuaranteed,
hasOnlyFlexRates,
paymentMethod,
savedCreditCard,
isRedemptionBooking,
lang,
})
const paymentMethodType = savedCreditCard
? savedCreditCard.type
@@ -315,7 +324,7 @@ export default function PaymentClient({
checkOutDate: toDate,
hotelId,
language: lang,
payment,
payment: payment ?? undefined,
rooms: rooms.map(
({ room }, idx): CreateBookingInput["rooms"][number] => {
const isMainRoom = idx === 0
@@ -414,14 +423,16 @@ export default function PaymentClient({
[
setIsSubmitting,
scrollToInvalidField,
hasFlexRates,
savedCreditCards,
hasFlexRates,
redemptionType,
bookingMustBeGuaranteed,
hasOnlyFlexRates,
lang,
isRedemptionBooking,
hotelId,
fromDate,
toDate,
hotelId,
rooms,
initiateBooking,
isLoggedIn,
@@ -440,7 +451,6 @@ export default function PaymentClient({
const { preHeading, heading, subHeading, showLearnMore } =
getPaymentHeadingConfig(intl, bookingMustBeGuaranteed, hasOnlyFlexRates)
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
return (
<section
className={cx(styles.paymentSection, {
@@ -522,22 +532,6 @@ const scrollToElement = (el: HTMLElement, offset: number) => {
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,
@@ -584,32 +578,6 @@ function useBookingStatusRedirect({
}, [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
}

View File

@@ -1,6 +1,13 @@
import { describe, expect, it } from "vitest"
import { mustGuaranteeBooking } from "./helpers"
import { Lang } from "@scandic-hotels/common/constants/language"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import {
getPaymentData,
getPaymentMethod,
mustGuaranteeBooking,
} from "./helpers"
const buildRoom = (
overrides: Partial<{
@@ -96,3 +103,179 @@ describe("mustGuaranteeBooking", () => {
).toBe(true)
})
})
describe("getPaymentData", () => {
it("returns correct URLs and method when guarantee is true", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.swish,
lang: Lang.en,
guarantee: true,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: true,
isRedemptionBooking: false,
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.swish,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
})
})
it("returns correct URLs and method when bookingMustBeGuaranteed", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.swish,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: true,
hasOnlyFlexRates: true,
isRedemptionBooking: false,
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.swish,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
})
})
it("returns correct URLs and method when has only flex rates is false", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.swish,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: false,
isRedemptionBooking: false,
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.swish,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
})
})
it("returns null when payment isn't required", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.swish,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: true,
isRedemptionBooking: false,
})
expect(result).toBeNull()
})
it("returns saved credit card when provided", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.card,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: false,
isRedemptionBooking: false,
savedCreditCard: {
alias: "My Visa",
expirationDate: "12/25",
cardType: "visa",
id: "",
type: "",
truncatedNumber: "",
},
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.card,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
card: {
alias: "My Visa",
expiryDate: "12/25",
cardType: "visa",
},
})
})
it("returns correct URLs and method when isRedemptionBooking is true and type is PartnerPoints", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.PartnerPoints,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: true,
isRedemptionBooking: true,
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.PartnerPoints,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
})
})
})
describe("getPaymentMethod", () => {
it("returns card when hasFlexRates is true", () => {
const hasFlexRates = true
const isRedemptionBooking = false
const redemptionType = "scandic"
const method = getPaymentMethod({
paymentMethod: PaymentMethodEnum.swish,
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
expect(method).toBe(PaymentMethodEnum.card)
})
it("returns PartnerPoints when is redemption and redemptionType is partner", () => {
const hasFlexRates = false
const isRedemptionBooking = true
const redemptionType = "partner"
const method = getPaymentMethod({
paymentMethod: PaymentMethodEnum.swish,
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
expect(method).toBe(PaymentMethodEnum.PartnerPoints)
})
it("returns paymentMethod when not redemption but redemptionType is partner", () => {
const hasFlexRates = false
const isRedemptionBooking = false
const redemptionType = "partner"
const method = getPaymentMethod({
paymentMethod: PaymentMethodEnum.swish,
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
expect(method).toBe(PaymentMethodEnum.swish)
})
it("returns card when payment method is string", () => {
const hasFlexRates = false
const isRedemptionBooking = false
const redemptionType = "scandic"
const method = getPaymentMethod({
paymentMethod: "something-else",
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
expect(method).toBe(PaymentMethodEnum.card)
})
})

View File

@@ -1,6 +1,12 @@
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import { logger } from "@scandic-hotels/common/logger"
import { env } from "../../../../env/client"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
import type { RedemptionType } from "../../../bookingFlowConfig/bookingFlowConfig"
import type { RoomState } from "../../../stores/enter-details/types"
export function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
@@ -123,3 +129,84 @@ export function mustGuaranteeBooking({
return room.mustBeGuaranteed
})
}
function createPaymentCallbackUrl(lang: Lang) {
return `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
}
export function getPaymentData({
guarantee,
bookingMustBeGuaranteed,
hasOnlyFlexRates,
paymentMethod,
isRedemptionBooking,
savedCreditCard,
lang,
}: {
guarantee: boolean
bookingMustBeGuaranteed: boolean
hasOnlyFlexRates: boolean
paymentMethod: PaymentMethodEnum
isRedemptionBooking: boolean
savedCreditCard?: CreditCard
lang: Lang
}) {
const paymentRedirectUrl = createPaymentCallbackUrl(lang)
const redirectUrls = {
success: `${paymentRedirectUrl}/success`,
error: `${paymentRedirectUrl}/error`,
cancel: `${paymentRedirectUrl}/cancel`,
}
if (
isRedemptionBooking &&
paymentMethod === PaymentMethodEnum.PartnerPoints
) {
return {
paymentMethod: paymentMethod,
...redirectUrls,
}
}
const shouldUsePayment =
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
if (!shouldUsePayment) {
return null
}
return {
paymentMethod: paymentMethod,
...redirectUrls,
card: savedCreditCard
? {
alias: savedCreditCard.alias,
expiryDate: savedCreditCard.expirationDate,
cardType: savedCreditCard.cardType,
}
: undefined,
}
}
export const getPaymentMethod = ({
paymentMethod,
hasFlexRates,
isRedemptionBooking,
redemptionType,
}: {
paymentMethod: string | null | undefined
hasFlexRates: boolean
isRedemptionBooking: boolean
redemptionType: RedemptionType
}): PaymentMethodEnum => {
if (isRedemptionBooking && redemptionType === "partner") {
return PaymentMethodEnum.PartnerPoints
}
if (hasFlexRates) {
return PaymentMethodEnum.card
}
return paymentMethod && isPaymentMethodEnum(paymentMethod)
? paymentMethod
: PaymentMethodEnum.card
}