diff --git a/apps/partner-sas/app/[lang]/hotelreservation/(standard)/select-hotel/page.tsx b/apps/partner-sas/app/[lang]/hotelreservation/(standard)/select-hotel/page.tsx index b896d98fa..8333b0f99 100644 --- a/apps/partner-sas/app/[lang]/hotelreservation/(standard)/select-hotel/page.tsx +++ b/apps/partner-sas/app/[lang]/hotelreservation/(standard)/select-hotel/page.tsx @@ -37,7 +37,7 @@ export default async function SelectHotelPage(props: PageArgs) { searchParams={searchParams} config={bookingFlowConfig} topSlot={ - bookingFlowConfig.redemptionEnabled ? ( + bookingFlowConfig.redemptionType !== "disabled" ? ( ) { config={bookingFlowConfig} /> - {bookingFlowConfig.redemptionEnabled && ( + {bookingFlowConfig.redemptionType !== "disabled" && (
closeOnBlur(e.nativeEvent)}> diff --git a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/index.tsx b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/index.tsx index ce2d5e4d6..5db3f9c11 100644 --- a/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/index.tsx +++ b/packages/booking-flow/lib/components/BookingWidget/BookingWidgetForm/FormContent/index.tsx @@ -41,7 +41,7 @@ export default function FormContent({ const { formState: { errors, isDirty }, } = useFormContext() - 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", }} > diff --git a/packages/booking-flow/lib/components/EnterDetails/Payment/PaymentClient.tsx b/packages/booking-flow/lib/components/EnterDetails/Payment/PaymentClient.tsx index 65e6ed593..4d35798d3 100644 --- a/packages/booking-flow/lib/components/EnterDetails/Payment/PaymentClient.tsx +++ b/packages/booking-flow/lib/components/EnterDetails/Payment/PaymentClient.tsx @@ -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 (
{ 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(value: T | null): value is T { return value !== null } diff --git a/packages/booking-flow/lib/components/EnterDetails/Payment/helpers.test.ts b/packages/booking-flow/lib/components/EnterDetails/Payment/helpers.test.ts index cdf2a7d93..cc1dfa144 100644 --- a/packages/booking-flow/lib/components/EnterDetails/Payment/helpers.test.ts +++ b/packages/booking-flow/lib/components/EnterDetails/Payment/helpers.test.ts @@ -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) + }) +}) diff --git a/packages/booking-flow/lib/components/EnterDetails/Payment/helpers.ts b/packages/booking-flow/lib/components/EnterDetails/Payment/helpers.ts index 808b7641f..55f214443 100644 --- a/packages/booking-flow/lib/components/EnterDetails/Payment/helpers.ts +++ b/packages/booking-flow/lib/components/EnterDetails/Payment/helpers.ts @@ -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 +} diff --git a/packages/booking-flow/lib/pages/EnterDetailsPage.tsx b/packages/booking-flow/lib/pages/EnterDetailsPage.tsx index b2c658bc1..c913462c9 100644 --- a/packages/booking-flow/lib/pages/EnterDetailsPage.tsx +++ b/packages/booking-flow/lib/pages/EnterDetailsPage.tsx @@ -48,7 +48,7 @@ export async function EnterDetailsPage({ // This should never happen unless a user tampers with the URL if ( - !config.redemptionEnabled && + config.redemptionType === "disabled" && booking.searchType === SEARCH_TYPE_REDEMPTION ) { throw new Error("Redemptions are disabled") diff --git a/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx b/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx index 965112808..221a0a31e 100644 --- a/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx +++ b/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx @@ -19,6 +19,8 @@ import { serverClient } from "../trpc" import type { Lang } from "@scandic-hotels/common/constants/language" 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 = { lang: Lang @@ -48,11 +50,28 @@ export async function PaymentCallbackPage({ } const returnUrl = details(lang) - const searchObject = new URLSearchParams() - let errorMessage = undefined if (status === PaymentCallbackStatusEnum.Cancel) { + const searchObject = new URLSearchParams() searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled) + return ( + + + + ) + } + + 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 ( + ) +} - const { booking } = bookingStatus - - // TODO: how to handle errors for multiple rooms? - const error = booking.errors.find((e) => e.errorCode) - - errorMessage = - error?.description ?? - `No error message found for booking ${confirmationNumber}, status: ${status}` - - searchObject.set( - "errorCode", - error - ? error.errorCode.toString() - : BookingErrorCodeEnum.TransactionFailed - ) - } 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) { +function HandleBookingStatusError({ + booking, + confirmationNumber, + returnUrl, + config, + status, +}: { + booking: CreateBookingSchema | null + confirmationNumber?: string + returnUrl: string + config: BookingFlowConfig + status: PaymentCallbackStatusEnum +}) { + if (!booking) { 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) - errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}` + const errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}` + + return ( + + + + ) } + // 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 ( , chinaUnionPay: (props) => , discover: (props) => , + PartnerPoints: () => null, } type PaymentMethodIconProps = { diff --git a/packages/trpc/lib/api/endpoints.ts b/packages/trpc/lib/api/endpoints.ts index 058843eb8..b451570aa 100644 --- a/packages/trpc/lib/api/endpoints.ts +++ b/packages/trpc/lib/api/endpoints.ts @@ -82,6 +82,9 @@ export namespace endpoints { export function confirmNotification(confirmationNumber: string) { return `${bookings}/${confirmationNumber}/confirmNotification` } + export function validatePartnerPayment(confirmationNumber: string) { + return `${bookings}/${confirmationNumber}/validate` + } export const enum Stays { future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`, diff --git a/packages/trpc/lib/routers/booking/mutation/create/index.ts b/packages/trpc/lib/routers/booking/mutation/create/index.ts index 5a6aff498..38ae73558 100644 --- a/packages/trpc/lib/routers/booking/mutation/create/index.ts +++ b/packages/trpc/lib/routers/booking/mutation/create/index.ts @@ -1,5 +1,6 @@ import "server-only" +import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod" import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../../api" @@ -38,6 +39,23 @@ export const create = safeProtectedServiceProcedure 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( api.endpoints.v1.Booking.bookings, { @@ -60,7 +78,6 @@ export const create = safeProtectedServiceProcedure } const apiJson = await apiResponse.json() - const verifiedData = createBookingSchema.safeParse(apiJson) if (!verifiedData.success) { metricsCreateBooking.validationError(verifiedData.error) diff --git a/packages/trpc/lib/routers/booking/mutation/create/schema.ts b/packages/trpc/lib/routers/booking/mutation/create/schema.ts index a8377e6a4..4165d0ee1 100644 --- a/packages/trpc/lib/routers/booking/mutation/create/schema.ts +++ b/packages/trpc/lib/routers/booking/mutation/create/schema.ts @@ -105,6 +105,11 @@ export const createBookingInput = z.object({ rooms: roomsSchema, payment: paymentSchema.optional(), language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]), + partnerSpecific: z + .object({ + eurobonusAccessToken: z.string(), + }) + .optional(), }) export const createBookingSchema = z @@ -114,6 +119,7 @@ export const createBookingSchema = z reservationStatus: z.string(), guest: guestSchema.optional(), paymentUrl: z.string().nullable().optional(), + paymentMethod: z.string().nullable().optional(), rooms: z .array( z.object({ @@ -161,6 +167,7 @@ export const createBookingSchema = z type: d.data.type, reservationStatus: d.data.attributes.reservationStatus, paymentUrl: d.data.attributes.paymentUrl, + paymentMethod: d.data.attributes.paymentMethod, rooms: d.data.attributes.rooms.map((room) => { const lastName = d.data.attributes.guest?.lastName ?? "" return { @@ -171,3 +178,4 @@ export const createBookingSchema = z errors: d.data.attributes.errors, guest: d.data.attributes.guest, })) +export type CreateBookingSchema = z.infer diff --git a/packages/trpc/lib/routers/booking/mutation/index.ts b/packages/trpc/lib/routers/booking/mutation/index.ts index 03fe20f45..2d7ba4ab7 100644 --- a/packages/trpc/lib/routers/booking/mutation/index.ts +++ b/packages/trpc/lib/routers/booking/mutation/index.ts @@ -17,12 +17,14 @@ import { bookingConfirmationSchema } from "../output" import { cancelBooking } from "../utils" import { createBookingSchema } from "./create/schema" import { create } from "./create" +import { validatePartnerPayment } from "./validatePartnerPayment" const refIdPlugin = createRefIdPlugin() const bookingLogger = createLogger("trpc.booking") export const bookingMutationRouter = router({ create, + validatePartnerPayment, priceChange: safeProtectedServiceProcedure .concat(refIdPlugin.toConfirmationNumber) .use(async ({ ctx, next }) => { diff --git a/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts b/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts new file mode 100644 index 000000000..35bd3b723 --- /dev/null +++ b/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts @@ -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 + })