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:
@@ -37,7 +37,7 @@ export default async function SelectHotelPage(props: PageArgs<LangParams>) {
|
|||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
config={bookingFlowConfig}
|
config={bookingFlowConfig}
|
||||||
topSlot={
|
topSlot={
|
||||||
bookingFlowConfig.redemptionEnabled ? (
|
bookingFlowConfig.redemptionType !== "disabled" ? (
|
||||||
<Alert
|
<Alert
|
||||||
heading={intl.formatMessage({
|
heading={intl.formatMessage({
|
||||||
id: "selectHotel.earnEuroBonusPointsAlert.heading",
|
id: "selectHotel.earnEuroBonusPointsAlert.heading",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default async function Home(props: PageArgs<LangParams>) {
|
|||||||
config={bookingFlowConfig}
|
config={bookingFlowConfig}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
{bookingFlowConfig.redemptionEnabled && (
|
{bookingFlowConfig.redemptionType !== "disabled" && (
|
||||||
<section className={styles.infoBoxes}>
|
<section className={styles.infoBoxes}>
|
||||||
<InfoBox
|
<InfoBox
|
||||||
heading={intl.formatMessage({
|
heading={intl.formatMessage({
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRout
|
|||||||
export const bookingFlowConfig: BookingFlowConfig = {
|
export const bookingFlowConfig: BookingFlowConfig = {
|
||||||
bookingCodeEnabled: false,
|
bookingCodeEnabled: false,
|
||||||
savedCreditCardsEnabled: false,
|
savedCreditCardsEnabled: false,
|
||||||
redemptionEnabled: env.REDEMPTION_ENABLED === true,
|
redemptionType: env.REDEMPTION_ENABLED === true ? "partner" : "disabled",
|
||||||
enterDetailsMembershipIdInputLocation: "join-card",
|
enterDetailsMembershipIdInputLocation: "join-card",
|
||||||
variant: "partner-sas",
|
variant: "partner-sas",
|
||||||
routes: {
|
routes: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { BookingFlowConfig } from "@scandic-hotels/booking-flow/BookingFlow
|
|||||||
|
|
||||||
export const bookingFlowConfig: BookingFlowConfig = {
|
export const bookingFlowConfig: BookingFlowConfig = {
|
||||||
bookingCodeEnabled: true,
|
bookingCodeEnabled: true,
|
||||||
redemptionEnabled: true,
|
redemptionType: "scandic",
|
||||||
savedCreditCardsEnabled: true,
|
savedCreditCardsEnabled: true,
|
||||||
enterDetailsMembershipIdInputLocation: "form",
|
enterDetailsMembershipIdInputLocation: "form",
|
||||||
variant: "scandic",
|
variant: "scandic",
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRout
|
|||||||
|
|
||||||
import type { BookingFlowVariant } from "./bookingFlowVariants"
|
import type { BookingFlowVariant } from "./bookingFlowVariants"
|
||||||
|
|
||||||
|
export type RedemptionType = "scandic" | "partner" | "disabled"
|
||||||
export type BookingFlowConfig = {
|
export type BookingFlowConfig = {
|
||||||
bookingCodeEnabled: boolean
|
bookingCodeEnabled: boolean
|
||||||
redemptionEnabled: boolean
|
redemptionType: RedemptionType
|
||||||
savedCreditCardsEnabled: boolean
|
savedCreditCardsEnabled: boolean
|
||||||
enterDetailsMembershipIdInputLocation: "form" | "join-card"
|
enterDetailsMembershipIdInputLocation: "form" | "join-card"
|
||||||
variant: BookingFlowVariant
|
variant: BookingFlowVariant
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function RewardNight() {
|
|||||||
}
|
}
|
||||||
}, [resetOnMultiroomError, ref])
|
}, [resetOnMultiroomError, ref])
|
||||||
|
|
||||||
if (!config.redemptionEnabled) return null
|
if (config.redemptionType === "disabled") return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} onBlur={(e) => closeOnBlur(e.nativeEvent)}>
|
<div ref={ref} onBlur={(e) => closeOnBlur(e.nativeEvent)}>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function FormContent({
|
|||||||
const {
|
const {
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
} = useFormContext<BookingWidgetSchema>()
|
} = useFormContext<BookingWidgetSchema>()
|
||||||
const { bookingCodeEnabled, redemptionEnabled } = useBookingFlowConfig()
|
const { bookingCodeEnabled, redemptionType } = useBookingFlowConfig()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const focusWidget = searchParams.get(FOCUS_WIDGET) === "true"
|
const focusWidget = searchParams.get(FOCUS_WIDGET) === "true"
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -126,7 +126,7 @@ export default function FormContent({
|
|||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
// TODO: Remove this when redemption is enabled for partner-sas
|
// TODO: Remove this when redemption is enabled for partner-sas
|
||||||
display: redemptionEnabled ? undefined : "none",
|
display: redemptionType !== "disabled" ? undefined : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Voucher />
|
<Voucher />
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
|
|||||||
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
|
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
|
||||||
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
||||||
|
|
||||||
import { env } from "../../../../env/client"
|
import { useBookingFlowConfig } from "../../../bookingFlowConfig/bookingFlowConfigContext"
|
||||||
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"
|
||||||
@@ -40,9 +40,10 @@ import { writeGlaToSessionStorage } from "./PaymentCallback/helpers"
|
|||||||
import BookingAlert from "./BookingAlert"
|
import BookingAlert from "./BookingAlert"
|
||||||
import { GuaranteeInfo } from "./GuaranteeInfo"
|
import { GuaranteeInfo } from "./GuaranteeInfo"
|
||||||
import {
|
import {
|
||||||
|
getPaymentData,
|
||||||
|
getPaymentMethod,
|
||||||
hasFlexibleRate,
|
hasFlexibleRate,
|
||||||
hasPrepaidRate,
|
hasPrepaidRate,
|
||||||
isPaymentMethodEnum,
|
|
||||||
mustGuaranteeBooking,
|
mustGuaranteeBooking,
|
||||||
writePaymentInfoToSessionStorage,
|
writePaymentInfoToSessionStorage,
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
@@ -51,7 +52,6 @@ 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"
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ export default function PaymentClient({
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const { getTopOffset } = useStickyPosition({})
|
const { getTopOffset } = useStickyPosition({})
|
||||||
const { user, isLoggedIn } = useBookingFlowContext()
|
const { user, isLoggedIn } = useBookingFlowContext()
|
||||||
|
const { redemptionType } = useBookingFlowConfig()
|
||||||
const [refId, setRefId] = useState("")
|
const [refId, setRefId] = useState("")
|
||||||
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
||||||
useState(false)
|
useState(false)
|
||||||
@@ -273,6 +274,7 @@ export default function PaymentClient({
|
|||||||
return firstIncompleteRoomIndex !== -1
|
return firstIncompleteRoomIndex !== -1
|
||||||
}, [runPreSubmitCallbacks, rooms, methods.formState.errors, getTopOffset])
|
}, [runPreSubmitCallbacks, rooms, methods.formState.errors, getTopOffset])
|
||||||
|
|
||||||
|
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (data: PaymentFormData) => {
|
async (data: PaymentFormData) => {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
@@ -283,19 +285,26 @@ export default function PaymentClient({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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 guarantee = data.guarantee
|
const guarantee = data.guarantee
|
||||||
|
const paymentMethod = getPaymentMethod({
|
||||||
const shouldUsePayment =
|
paymentMethod: data.paymentMethod,
|
||||||
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
|
hasFlexRates,
|
||||||
const payment = shouldUsePayment
|
isRedemptionBooking,
|
||||||
? getPaymentData({ paymentMethod, savedCreditCard, lang })
|
redemptionType,
|
||||||
: undefined
|
})
|
||||||
|
const payment = getPaymentData({
|
||||||
|
guarantee,
|
||||||
|
bookingMustBeGuaranteed,
|
||||||
|
hasOnlyFlexRates,
|
||||||
|
paymentMethod,
|
||||||
|
savedCreditCard,
|
||||||
|
isRedemptionBooking,
|
||||||
|
lang,
|
||||||
|
})
|
||||||
|
|
||||||
const paymentMethodType = savedCreditCard
|
const paymentMethodType = savedCreditCard
|
||||||
? savedCreditCard.type
|
? savedCreditCard.type
|
||||||
@@ -315,7 +324,7 @@ export default function PaymentClient({
|
|||||||
checkOutDate: toDate,
|
checkOutDate: toDate,
|
||||||
hotelId,
|
hotelId,
|
||||||
language: lang,
|
language: lang,
|
||||||
payment,
|
payment: payment ?? undefined,
|
||||||
rooms: rooms.map(
|
rooms: rooms.map(
|
||||||
({ room }, idx): CreateBookingInput["rooms"][number] => {
|
({ room }, idx): CreateBookingInput["rooms"][number] => {
|
||||||
const isMainRoom = idx === 0
|
const isMainRoom = idx === 0
|
||||||
@@ -414,14 +423,16 @@ export default function PaymentClient({
|
|||||||
[
|
[
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
scrollToInvalidField,
|
scrollToInvalidField,
|
||||||
hasFlexRates,
|
|
||||||
savedCreditCards,
|
savedCreditCards,
|
||||||
|
hasFlexRates,
|
||||||
|
redemptionType,
|
||||||
bookingMustBeGuaranteed,
|
bookingMustBeGuaranteed,
|
||||||
hasOnlyFlexRates,
|
hasOnlyFlexRates,
|
||||||
lang,
|
lang,
|
||||||
|
isRedemptionBooking,
|
||||||
|
hotelId,
|
||||||
fromDate,
|
fromDate,
|
||||||
toDate,
|
toDate,
|
||||||
hotelId,
|
|
||||||
rooms,
|
rooms,
|
||||||
initiateBooking,
|
initiateBooking,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
@@ -440,7 +451,6 @@ 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, {
|
||||||
@@ -522,22 +532,6 @@ const scrollToElement = (el: HTMLElement, offset: number) => {
|
|||||||
input?.focus({ preventScroll: true })
|
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({
|
function useBookingStatusRedirect({
|
||||||
refId,
|
refId,
|
||||||
enabled,
|
enabled,
|
||||||
@@ -584,32 +578,6 @@ function useBookingStatusRedirect({
|
|||||||
}, [bookingStatus.data, bookingStatus.isTimeout, router, intl, lang, onError])
|
}, [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 {
|
function isNotNull<T>(value: T | null): value is T {
|
||||||
return value !== null
|
return value !== null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { describe, expect, it } from "vitest"
|
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 = (
|
const buildRoom = (
|
||||||
overrides: Partial<{
|
overrides: Partial<{
|
||||||
@@ -96,3 +103,179 @@ describe("mustGuaranteeBooking", () => {
|
|||||||
).toBe(true)
|
).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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
||||||
import { logger } from "@scandic-hotels/common/logger"
|
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"
|
import type { RoomState } from "../../../stores/enter-details/types"
|
||||||
|
|
||||||
export function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
export function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||||
@@ -123,3 +129,84 @@ export function mustGuaranteeBooking({
|
|||||||
return room.mustBeGuaranteed
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export async function EnterDetailsPage({
|
|||||||
|
|
||||||
// This should never happen unless a user tampers with the URL
|
// This should never happen unless a user tampers with the URL
|
||||||
if (
|
if (
|
||||||
!config.redemptionEnabled &&
|
config.redemptionType === "disabled" &&
|
||||||
booking.searchType === SEARCH_TYPE_REDEMPTION
|
booking.searchType === SEARCH_TYPE_REDEMPTION
|
||||||
) {
|
) {
|
||||||
throw new Error("Redemptions are disabled")
|
throw new Error("Redemptions are disabled")
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { serverClient } from "../trpc"
|
|||||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
import type { NextSearchParams } from "../types"
|
import type { NextSearchParams } from "../types"
|
||||||
|
import { CreateBookingSchema } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
|
||||||
|
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
||||||
|
|
||||||
type PaymentCallbackPageProps = {
|
type PaymentCallbackPageProps = {
|
||||||
lang: Lang
|
lang: Lang
|
||||||
@@ -48,11 +50,28 @@ export async function PaymentCallbackPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const returnUrl = details(lang)
|
const returnUrl = details(lang)
|
||||||
const searchObject = new URLSearchParams()
|
|
||||||
let errorMessage = undefined
|
|
||||||
|
|
||||||
if (status === PaymentCallbackStatusEnum.Cancel) {
|
if (status === PaymentCallbackStatusEnum.Cancel) {
|
||||||
|
const searchObject = new URLSearchParams()
|
||||||
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
|
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
|
||||||
|
return (
|
||||||
|
<BookingFlowConfig config={config}>
|
||||||
|
<HandleErrorCallback
|
||||||
|
returnUrl={returnUrl.toString()}
|
||||||
|
searchObject={searchObject}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
|
</BookingFlowConfig>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === PaymentCallbackStatusEnum.Error) {
|
||||||
|
logger.error(
|
||||||
|
`[payment-callback] error status received for ${confirmationNumber}, status: ${status}`
|
||||||
|
)
|
||||||
|
const searchObject = new URLSearchParams()
|
||||||
|
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
|
||||||
|
const errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||||
return (
|
return (
|
||||||
<BookingFlowConfig config={config}>
|
<BookingFlowConfig config={config}>
|
||||||
<HandleErrorCallback
|
<HandleErrorCallback
|
||||||
@@ -81,14 +100,24 @@ export async function PaymentCallbackPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const booking = await getBooking(confirmationNumber, lang, token)
|
const booking = await getBooking(confirmationNumber, lang, token)
|
||||||
|
|
||||||
const refId = booking?.refId
|
const refId = booking?.refId
|
||||||
|
|
||||||
if (
|
const caller = await serverClient()
|
||||||
status === PaymentCallbackStatusEnum.Success &&
|
const bookingStatus = refId
|
||||||
confirmationNumber &&
|
? await caller.booking.status({
|
||||||
refId
|
refId,
|
||||||
) {
|
})
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (status === PaymentCallbackStatusEnum.Success && refId) {
|
||||||
|
const shouldValidatePayment =
|
||||||
|
bookingStatus?.booking.paymentMethod === PaymentMethodEnum.PartnerPoints
|
||||||
|
if (shouldValidatePayment) {
|
||||||
|
// TODO We probably need better error handling for this mutation,
|
||||||
|
// but for now we've just implemented the happy path
|
||||||
|
await caller.booking.validatePartnerPayment({ confirmationNumber })
|
||||||
|
}
|
||||||
|
|
||||||
const expire = Math.floor(Date.now() / 1000) + 60
|
const expire = Math.floor(Date.now() / 1000) + 60
|
||||||
const sig = encrypt(expire.toString())
|
const sig = encrypt(expire.toString())
|
||||||
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(refId)}`
|
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(refId)}`
|
||||||
@@ -108,49 +137,67 @@ export async function PaymentCallbackPage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refId) {
|
return (
|
||||||
try {
|
<HandleBookingStatusError
|
||||||
const caller = await serverClient()
|
status={status}
|
||||||
const bookingStatus = await caller.booking.status({
|
booking={bookingStatus?.booking ?? null}
|
||||||
refId,
|
returnUrl={returnUrl.toString()}
|
||||||
})
|
confirmationNumber={confirmationNumber}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const { booking } = bookingStatus
|
function HandleBookingStatusError({
|
||||||
|
booking,
|
||||||
// TODO: how to handle errors for multiple rooms?
|
confirmationNumber,
|
||||||
const error = booking.errors.find((e) => e.errorCode)
|
returnUrl,
|
||||||
|
config,
|
||||||
errorMessage =
|
status,
|
||||||
error?.description ??
|
}: {
|
||||||
`No error message found for booking ${confirmationNumber}, status: ${status}`
|
booking: CreateBookingSchema | null
|
||||||
|
confirmationNumber?: string
|
||||||
searchObject.set(
|
returnUrl: string
|
||||||
"errorCode",
|
config: BookingFlowConfig
|
||||||
error
|
status: PaymentCallbackStatusEnum
|
||||||
? error.errorCode.toString()
|
}) {
|
||||||
: BookingErrorCodeEnum.TransactionFailed
|
if (!booking) {
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
logger.error(
|
|
||||||
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
|
||||||
)
|
|
||||||
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
|
|
||||||
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === PaymentCallbackStatusEnum.Error) {
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`[payment-callback] error status received for ${confirmationNumber}, status: ${status}`
|
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||||
)
|
)
|
||||||
|
const searchObject = new URLSearchParams()
|
||||||
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
|
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
|
||||||
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
|
const errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BookingFlowConfig config={config}>
|
||||||
|
<HandleErrorCallback
|
||||||
|
returnUrl={returnUrl}
|
||||||
|
searchObject={searchObject}
|
||||||
|
status={status}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
</BookingFlowConfig>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: how to handle errors for multiple rooms?
|
||||||
|
const error = booking.errors.find((e) => e.errorCode)
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
error?.description ??
|
||||||
|
`No error message found for booking ${confirmationNumber}, status: ${status}`
|
||||||
|
|
||||||
|
const searchObject = new URLSearchParams()
|
||||||
|
searchObject.set(
|
||||||
|
"errorCode",
|
||||||
|
error ? error.errorCode.toString() : BookingErrorCodeEnum.TransactionFailed
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookingFlowConfig config={config}>
|
<BookingFlowConfig config={config}>
|
||||||
<HandleErrorCallback
|
<HandleErrorCallback
|
||||||
returnUrl={returnUrl.toString()}
|
returnUrl={returnUrl}
|
||||||
searchObject={searchObject}
|
searchObject={searchObject}
|
||||||
status={status}
|
status={status}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export enum PaymentMethodEnum {
|
|||||||
maestro = "maestro",
|
maestro = "maestro",
|
||||||
chinaUnionPay = "chinaUnionPay",
|
chinaUnionPay = "chinaUnionPay",
|
||||||
discover = "discover",
|
discover = "discover",
|
||||||
|
PartnerPoints = "PartnerPoints",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PAYMENT_METHOD_TITLES: Record<
|
export const PAYMENT_METHOD_TITLES: Record<
|
||||||
@@ -45,4 +46,5 @@ export const PAYMENT_METHOD_TITLES: Record<
|
|||||||
maestro: "Maestro",
|
maestro: "Maestro",
|
||||||
chinaUnionPay: "China UnionPay",
|
chinaUnionPay: "China UnionPay",
|
||||||
discover: "Discover",
|
discover: "Discover",
|
||||||
|
PartnerPoints: "N/A",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const paymentMethods: Partial<
|
|||||||
maestro: (props) => <MaestroIcon {...props} />,
|
maestro: (props) => <MaestroIcon {...props} />,
|
||||||
chinaUnionPay: (props) => <ChinaUnionPayIcon {...props} />,
|
chinaUnionPay: (props) => <ChinaUnionPayIcon {...props} />,
|
||||||
discover: (props) => <DiscoverIcon {...props} />,
|
discover: (props) => <DiscoverIcon {...props} />,
|
||||||
|
PartnerPoints: () => null,
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentMethodIconProps = {
|
type PaymentMethodIconProps = {
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export namespace endpoints {
|
|||||||
export function confirmNotification(confirmationNumber: string) {
|
export function confirmNotification(confirmationNumber: string) {
|
||||||
return `${bookings}/${confirmationNumber}/confirmNotification`
|
return `${bookings}/${confirmationNumber}/confirmNotification`
|
||||||
}
|
}
|
||||||
|
export function validatePartnerPayment(confirmationNumber: string) {
|
||||||
|
return `${bookings}/${confirmationNumber}/validate`
|
||||||
|
}
|
||||||
|
|
||||||
export const enum Stays {
|
export const enum Stays {
|
||||||
future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`,
|
future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
|
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
||||||
import { createCounter } from "@scandic-hotels/common/telemetry"
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
import * as api from "../../../../api"
|
import * as api from "../../../../api"
|
||||||
@@ -38,6 +39,23 @@ export const create = safeProtectedServiceProcedure
|
|||||||
Authorization: `Bearer ${ctx.token ?? ctx.serviceToken}`,
|
Authorization: `Bearer ${ctx.token ?? ctx.serviceToken}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const includePartnerSpecific =
|
||||||
|
inputWithoutLang.payment?.paymentMethod ===
|
||||||
|
PaymentMethodEnum.PartnerPoints
|
||||||
|
if (includePartnerSpecific) {
|
||||||
|
const session = await ctx.auth()
|
||||||
|
const token = session?.token.access_token
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
"Cannot create booking with partner points without partner token"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputWithoutLang.partnerSpecific = {
|
||||||
|
eurobonusAccessToken: session?.token.access_token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const apiResponse = await api.post(
|
const apiResponse = await api.post(
|
||||||
api.endpoints.v1.Booking.bookings,
|
api.endpoints.v1.Booking.bookings,
|
||||||
{
|
{
|
||||||
@@ -60,7 +78,6 @@ export const create = safeProtectedServiceProcedure
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
|
|
||||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||||
if (!verifiedData.success) {
|
if (!verifiedData.success) {
|
||||||
metricsCreateBooking.validationError(verifiedData.error)
|
metricsCreateBooking.validationError(verifiedData.error)
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ export const createBookingInput = z.object({
|
|||||||
rooms: roomsSchema,
|
rooms: roomsSchema,
|
||||||
payment: paymentSchema.optional(),
|
payment: paymentSchema.optional(),
|
||||||
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
|
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
|
||||||
|
partnerSpecific: z
|
||||||
|
.object({
|
||||||
|
eurobonusAccessToken: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createBookingSchema = z
|
export const createBookingSchema = z
|
||||||
@@ -114,6 +119,7 @@ export const createBookingSchema = z
|
|||||||
reservationStatus: z.string(),
|
reservationStatus: z.string(),
|
||||||
guest: guestSchema.optional(),
|
guest: guestSchema.optional(),
|
||||||
paymentUrl: z.string().nullable().optional(),
|
paymentUrl: z.string().nullable().optional(),
|
||||||
|
paymentMethod: z.string().nullable().optional(),
|
||||||
rooms: z
|
rooms: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -161,6 +167,7 @@ export const createBookingSchema = z
|
|||||||
type: d.data.type,
|
type: d.data.type,
|
||||||
reservationStatus: d.data.attributes.reservationStatus,
|
reservationStatus: d.data.attributes.reservationStatus,
|
||||||
paymentUrl: d.data.attributes.paymentUrl,
|
paymentUrl: d.data.attributes.paymentUrl,
|
||||||
|
paymentMethod: d.data.attributes.paymentMethod,
|
||||||
rooms: d.data.attributes.rooms.map((room) => {
|
rooms: d.data.attributes.rooms.map((room) => {
|
||||||
const lastName = d.data.attributes.guest?.lastName ?? ""
|
const lastName = d.data.attributes.guest?.lastName ?? ""
|
||||||
return {
|
return {
|
||||||
@@ -171,3 +178,4 @@ export const createBookingSchema = z
|
|||||||
errors: d.data.attributes.errors,
|
errors: d.data.attributes.errors,
|
||||||
guest: d.data.attributes.guest,
|
guest: d.data.attributes.guest,
|
||||||
}))
|
}))
|
||||||
|
export type CreateBookingSchema = z.infer<typeof createBookingSchema>
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ import { bookingConfirmationSchema } from "../output"
|
|||||||
import { cancelBooking } from "../utils"
|
import { cancelBooking } from "../utils"
|
||||||
import { createBookingSchema } from "./create/schema"
|
import { createBookingSchema } from "./create/schema"
|
||||||
import { create } from "./create"
|
import { create } from "./create"
|
||||||
|
import { validatePartnerPayment } from "./validatePartnerPayment"
|
||||||
|
|
||||||
const refIdPlugin = createRefIdPlugin()
|
const refIdPlugin = createRefIdPlugin()
|
||||||
const bookingLogger = createLogger("trpc.booking")
|
const bookingLogger = createLogger("trpc.booking")
|
||||||
|
|
||||||
export const bookingMutationRouter = router({
|
export const bookingMutationRouter = router({
|
||||||
create,
|
create,
|
||||||
|
validatePartnerPayment,
|
||||||
priceChange: safeProtectedServiceProcedure
|
priceChange: safeProtectedServiceProcedure
|
||||||
.concat(refIdPlugin.toConfirmationNumber)
|
.concat(refIdPlugin.toConfirmationNumber)
|
||||||
.use(async ({ ctx, next }) => {
|
.use(async ({ ctx, next }) => {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import "server-only"
|
||||||
|
|
||||||
|
import z from "zod"
|
||||||
|
|
||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { serverErrorByStatus } from "../../../errors"
|
||||||
|
import { safeProtectedServiceProcedure } from "../../../procedures"
|
||||||
|
import { toApiLang } from "../../../utils"
|
||||||
|
|
||||||
|
const validatePartnerPaymentInput = z.object({
|
||||||
|
confirmationNumber: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const validatePartnerPayment = safeProtectedServiceProcedure
|
||||||
|
.input(validatePartnerPaymentInput)
|
||||||
|
.use(async ({ ctx, next }) => {
|
||||||
|
const token = await ctx.getScandicUserToken()
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.mutation(async function ({ ctx, input }) {
|
||||||
|
const { confirmationNumber } = input
|
||||||
|
const getValidateBooking = createCounter("booking.validate")
|
||||||
|
const metricsValidateBooking = getValidateBooking.init({
|
||||||
|
confirmationNumber,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsValidateBooking.start()
|
||||||
|
|
||||||
|
const apiResponse = await api.put(
|
||||||
|
api.endpoints.v1.Booking.validatePartnerPayment(confirmationNumber),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${ctx.token ?? ctx.serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ language: toApiLang(ctx.lang) }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsValidateBooking.httpError(apiResponse)
|
||||||
|
|
||||||
|
// If the booking is not found, return null.
|
||||||
|
if (apiResponse.status === 404) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsValidateBooking.success()
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user