From 9b90e99adf11b27b00de5b73cf84f636cda8e570 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 28 Nov 2024 08:08:39 +0000 Subject: [PATCH] Merged in feat/SW-1007-saved-cards-filtering (pull request #980) Feat/SW-1007 saved payment cards now shown based on supported cards by hotel * fix(SW-1007): refactored savedCards to only show supported payment cards * fix(SW-1007): show error message even if metadata is null * fix: merge changes that were missed * fix: remove use server Approved-by: Christel Westerberg --- .../payment-callback/page.tsx | 12 +- .../hotelreservation/(standard)/step/page.tsx | 7 +- .../EnterDetails/Payment/PaymentClient.tsx | 425 ++++++++++++++++++ .../EnterDetails/Payment/index.tsx | 425 +----------------- .../Summary/BottomSheet/index.tsx | 2 +- lib/trpc/memoizedRequests/index.ts | 9 +- server/routers/hotels/output.ts | 1 + server/routers/user/input.ts | 8 + server/routers/user/query.ts | 31 +- .../hotelReservation/selectRate/section.ts | 7 +- 10 files changed, 494 insertions(+), 433 deletions(-) create mode 100644 components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx index 0e4e716f2..d934e1155 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx @@ -42,12 +42,12 @@ export default async function PaymentCallbackPage({ const bookingStatus = await serverClient().booking.status({ confirmationNumber, }) - if (bookingStatus.metadata) { - searchObject.set( - "errorCode", - bookingStatus.metadata.errorCode?.toString() ?? "" - ) - } + searchObject.set( + "errorCode", + bookingStatus?.metadata?.errorCode + ? bookingStatus.metadata.errorCode.toString() + : PaymentErrorCodeEnum.Failed.toString() + ) } catch (error) { console.error( `[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}` diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index bcfcca8c6..eebfbd289 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -3,7 +3,6 @@ import { Suspense } from "react" import { getBreakfastPackages, - getCreditCardsSafely, getHotelData, getPackages, getProfileSafely, @@ -75,7 +74,6 @@ export default async function StepPage({ } void getProfileSafely() - void getCreditCardsSafely() void getBreakfastPackages(breakfastInput) void getSelectedRoomAvailability(selectedRoomAvailabilityInput) if (packageCodes?.length) { @@ -110,7 +108,6 @@ export default async function StepPage({ }) const breakfastPackages = await getBreakfastPackages(breakfastInput) const user = await getProfileSafely() - const savedCreditCards = await getCreditCardsSafely() if (!hotelData || !roomAvailability) { return notFound() @@ -213,7 +210,9 @@ export default async function StepPage({ hotelData.data.attributes.merchantInformationData .alternatePaymentOptions } - savedCreditCards={savedCreditCards} + supportedCards={ + hotelData.data.attributes.merchantInformationData.cards + } mustBeGuaranteed={mustBeGuaranteed} /> diff --git a/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx new file mode 100644 index 000000000..ae1e523d8 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -0,0 +1,425 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter, useSearchParams } from "next/navigation" +import { useCallback, useEffect, useState } from "react" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { + BookingStatusEnum, + PAYMENT_METHOD_TITLES, + PaymentMethodEnum, +} from "@/constants/booking" +import { + bookingTermsAndConditions, + privacyPolicy, +} from "@/constants/currentWebHrefs" +import { selectRate } from "@/constants/routes/hotelReservation" +import { env } from "@/env/client" +import { trpc } from "@/lib/trpc/client" +import { useEnterDetailsStore } from "@/stores/enter-details" + +import LoadingSpinner from "@/components/LoadingSpinner" +import Button from "@/components/TempDesignSystem/Button" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import { toast } from "@/components/TempDesignSystem/Toasts" +import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions" +import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" +import { usePaymentFailedToast } from "@/hooks/booking/usePaymentFailedToast" +import useLang from "@/hooks/useLang" + +import { bedTypeMap } from "../../SelectRate/RoomSelection/utils" +import PriceChangeDialog from "../PriceChangeDialog" +import GuaranteeDetails from "./GuaranteeDetails" +import PaymentOption from "./PaymentOption" +import { PaymentFormData, paymentSchema } from "./schema" + +import styles from "./payment.module.css" + +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import type { PaymentClientProps } from "@/types/components/hotelReservation/selectRate/section" + +const maxRetries = 4 +const retryInterval = 2000 + +export const formId = "submit-booking" + +function isPaymentMethodEnum(value: string): value is PaymentMethodEnum { + return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum) +} + +export default function PaymentClient({ + user, + roomPrice, + otherPaymentOptions, + savedCreditCards, + mustBeGuaranteed, +}: PaymentClientProps) { + const router = useRouter() + const lang = useLang() + const intl = useIntl() + const searchParams = useSearchParams() + + const totalPrice = useEnterDetailsStore((state) => state.totalPrice) + const { bedType, booking, breakfast } = useEnterDetailsStore((state) => ({ + bedType: state.bedType, + booking: state.booking, + breakfast: state.breakfast, + })) + const userData = useEnterDetailsStore((state) => state.guest) + const setIsSubmittingDisabled = useEnterDetailsStore( + (state) => state.actions.setIsSubmittingDisabled + ) + + const [confirmationNumber, setConfirmationNumber] = useState("") + const [isPollingForBookingStatus, setIsPollingForBookingStatus] = + useState(false) + + const availablePaymentOptions = + useAvailablePaymentOptions(otherPaymentOptions) + const [priceChangeData, setPriceChangeData] = useState<{ + oldPrice: number + newPrice: number + } | null>() + + usePaymentFailedToast() + + const methods = useForm({ + defaultValues: { + paymentMethod: savedCreditCards?.length + ? savedCreditCards[0].id + : PaymentMethodEnum.card, + smsConfirmation: false, + termsAndConditions: false, + }, + mode: "all", + reValidateMode: "onChange", + resolver: zodResolver(paymentSchema), + }) + + const initiateBooking = trpc.booking.create.useMutation({ + onSuccess: (result) => { + if (result?.confirmationNumber) { + setConfirmationNumber(result.confirmationNumber) + + if (result.metadata?.priceChangedMetadata) { + setPriceChangeData({ + oldPrice: roomPrice.publicPrice, + newPrice: result.metadata.priceChangedMetadata.totalPrice, + }) + } else { + setIsPollingForBookingStatus(true) + } + } else { + toast.error( + intl.formatMessage({ + id: "payment.error.failed", + }) + ) + } + }, + onError: (error) => { + console.error("Error", error) + toast.error( + intl.formatMessage({ + id: "payment.error.failed", + }) + ) + }, + }) + + const priceChange = trpc.booking.priceChange.useMutation({ + onSuccess: (result) => { + if (result?.confirmationNumber) { + setIsPollingForBookingStatus(true) + } else { + toast.error(intl.formatMessage({ id: "payment.error.failed" })) + } + + setPriceChangeData(null) + }, + onError: (error) => { + console.error("Error", error) + setPriceChangeData(null) + toast.error(intl.formatMessage({ id: "payment.error.failed" })) + }, + }) + + const bookingStatus = useHandleBookingStatus({ + confirmationNumber, + expectedStatus: BookingStatusEnum.BookingCompleted, + maxRetries, + retryInterval, + enabled: isPollingForBookingStatus, + }) + + useEffect(() => { + if (bookingStatus?.data?.paymentUrl) { + router.push(bookingStatus.data.paymentUrl) + } else if (bookingStatus.isTimeout) { + toast.error( + intl.formatMessage({ + id: "payment.error.failed", + }) + ) + } + }, [bookingStatus, router, intl]) + + useEffect(() => { + setIsSubmittingDisabled( + !methods.formState.isValid || methods.formState.isSubmitting + ) + }, [ + methods.formState.isValid, + methods.formState.isSubmitting, + setIsSubmittingDisabled, + ]) + + const handleSubmit = useCallback( + (data: PaymentFormData) => { + const { + firstName, + lastName, + email, + phoneNumber, + countryCode, + membershipNo, + join, + dateOfBirth, + zipCode, + } = userData + const { toDate, fromDate, rooms, hotel } = booking + + // set payment method to card if saved card is submitted + const paymentMethod = isPaymentMethodEnum(data.paymentMethod) + ? data.paymentMethod + : PaymentMethodEnum.card + + const savedCreditCard = savedCreditCards?.find( + (card) => card.id === data.paymentMethod + ) + + const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback` + + initiateBooking.mutate({ + hotelId: hotel, + checkInDate: fromDate, + checkOutDate: toDate, + rooms: rooms.map((room) => ({ + adults: room.adults, + childrenAges: room.children?.map((child) => ({ + age: child.age, + bedType: bedTypeMap[parseInt(child.bed.toString())], + })), + rateCode: + user || join || membershipNo ? room.counterRateCode : room.rateCode, + roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step. + guest: { + firstName, + lastName, + email, + phoneNumber, + countryCode, + membershipNumber: membershipNo, + becomeMember: join, + dateOfBirth, + postalCode: zipCode, + }, + packages: { + breakfast: !!(breakfast && breakfast.code), + allergyFriendly: + room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? + false, + petFriendly: + room.packages?.includes(RoomPackageCodeEnum.PET_ROOM) ?? false, + accessibility: + room.packages?.includes(RoomPackageCodeEnum.ACCESSIBILITY_ROOM) ?? + false, + }, + smsConfirmationRequested: data.smsConfirmation, + roomPrice, + })), + payment: { + paymentMethod, + card: savedCreditCard + ? { + alias: savedCreditCard.alias, + expiryDate: savedCreditCard.expirationDate, + cardType: savedCreditCard.cardType, + } + : undefined, + + success: `${paymentRedirectUrl}/success`, + error: `${paymentRedirectUrl}/error`, + cancel: `${paymentRedirectUrl}/cancel`, + }, + }) + }, + [ + breakfast, + bedType, + userData, + booking, + roomPrice, + savedCreditCards, + lang, + user, + initiateBooking, + ] + ) + + if ( + initiateBooking.isPending || + (isPollingForBookingStatus && !bookingStatus.data?.paymentUrl) + ) { + return + } + + const guaranteeing = intl.formatMessage({ id: "guaranteeing" }) + const paying = intl.formatMessage({ id: "paying" }) + const paymentVerb = mustBeGuaranteed ? guaranteeing : paying + + return ( + <> + +
+ {mustBeGuaranteed ? ( +
+ + {intl.formatMessage({ + id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", + })} + + +
+ ) : null} + {savedCreditCards?.length ? ( +
+ + {intl.formatMessage({ id: "MY SAVED CARDS" })} + +
+ {savedCreditCards?.map((savedCreditCard) => ( + + ))} +
+
+ ) : null} +
+ {savedCreditCards?.length ? ( + + {intl.formatMessage({ id: "OTHER PAYMENT METHODS" })} + + ) : null} +
+ + {availablePaymentOptions.map((paymentMethod) => ( + + ))} +
+
+
+ + {intl.formatMessage( + { + id: "booking.terms", + }, + { + paymentVerb, + termsLink: (str) => ( + + {str} + + ), + privacyLink: (str) => ( + + {str} + + ), + } + )} + + + + {intl.formatMessage({ + id: "I accept the terms and conditions", + })} + + + + + {intl.formatMessage({ + id: "I would like to get my booking confirmation via sms", + })} + + +
+
+ +
+
+
+ {priceChangeData ? ( + { + const allSearchParams = searchParams.size + ? `?${searchParams.toString()}` + : "" + router.push(`${selectRate(lang)}${allSearchParams}`) + }} + onAccept={() => priceChange.mutate({ confirmationNumber })} + /> + ) : null} + + ) +} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 27b15aad5..74cce7e79 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -1,424 +1,27 @@ -"use client" +import { getSavedPaymentCardsSafely } from "@/lib/trpc/memoizedRequests" -import { zodResolver } from "@hookform/resolvers/zod" -import { useRouter, useSearchParams } from "next/navigation" -import { useCallback, useEffect, useState } from "react" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" +import PaymentClient from "./PaymentClient" -import { - BookingStatusEnum, - PAYMENT_METHOD_TITLES, - PaymentMethodEnum, -} from "@/constants/booking" -import { - bookingTermsAndConditions, - privacyPolicy, -} from "@/constants/currentWebHrefs" -import { selectRate } from "@/constants/routes/hotelReservation" -import { env } from "@/env/client" -import { trpc } from "@/lib/trpc/client" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" -import LoadingSpinner from "@/components/LoadingSpinner" -import Button from "@/components/TempDesignSystem/Button" -import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import { toast } from "@/components/TempDesignSystem/Toasts" -import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions" -import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" -import { usePaymentFailedToast } from "@/hooks/booking/usePaymentFailedToast" -import useLang from "@/hooks/useLang" - -import { bedTypeMap } from "../../SelectRate/RoomSelection/utils" -import PriceChangeDialog from "../PriceChangeDialog" -import GuaranteeDetails from "./GuaranteeDetails" -import PaymentOption from "./PaymentOption" -import { PaymentFormData, paymentSchema } from "./schema" - -import styles from "./payment.module.css" - -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" - -const maxRetries = 4 -const retryInterval = 2000 - -export const formId = "submit-booking" - -function isPaymentMethodEnum(value: string): value is PaymentMethodEnum { - return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum) -} - -export default function Payment({ +export default async function Payment({ user, roomPrice, otherPaymentOptions, - savedCreditCards, mustBeGuaranteed, + supportedCards, }: PaymentProps) { - const router = useRouter() - const lang = useLang() - const intl = useIntl() - const searchParams = useSearchParams() - const totalPrice = useEnterDetailsStore((state) => state.totalPrice) - const { bedType, booking, breakfast } = useEnterDetailsStore((state) => ({ - bedType: state.bedType, - booking: state.booking, - breakfast: state.breakfast, - })) - const userData = useEnterDetailsStore((state) => state.guest) - const setIsSubmittingDisabled = useEnterDetailsStore( - (state) => state.actions.setIsSubmittingDisabled - ) - - const [confirmationNumber, setConfirmationNumber] = useState("") - const [isPollingForBookingStatus, setIsPollingForBookingStatus] = - useState(false) - - const availablePaymentOptions = - useAvailablePaymentOptions(otherPaymentOptions) - const [priceChangeData, setPriceChangeData] = useState<{ - oldPrice: number - newPrice: number - } | null>() - - usePaymentFailedToast() - - const methods = useForm({ - defaultValues: { - paymentMethod: savedCreditCards?.length - ? savedCreditCards[0].id - : PaymentMethodEnum.card, - smsConfirmation: false, - termsAndConditions: false, - }, - mode: "all", - reValidateMode: "onChange", - resolver: zodResolver(paymentSchema), + const savedCreditCards = await getSavedPaymentCardsSafely({ + supportedCards, }) - const initiateBooking = trpc.booking.create.useMutation({ - onSuccess: (result) => { - if (result?.confirmationNumber) { - setConfirmationNumber(result.confirmationNumber) - - if (result.metadata?.priceChangedMetadata) { - setPriceChangeData({ - oldPrice: roomPrice.publicPrice, - newPrice: result.metadata.priceChangedMetadata.totalPrice, - }) - } else { - setIsPollingForBookingStatus(true) - } - } else { - toast.error( - intl.formatMessage({ - id: "payment.error.failed", - }) - ) - } - }, - onError: (error) => { - console.error("Error", error) - toast.error( - intl.formatMessage({ - id: "payment.error.failed", - }) - ) - }, - }) - - const priceChange = trpc.booking.priceChange.useMutation({ - onSuccess: (result) => { - if (result?.confirmationNumber) { - setIsPollingForBookingStatus(true) - } else { - toast.error(intl.formatMessage({ id: "payment.error.failed" })) - } - - setPriceChangeData(null) - }, - onError: (error) => { - console.error("Error", error) - setPriceChangeData(null) - toast.error(intl.formatMessage({ id: "payment.error.failed" })) - }, - }) - - const bookingStatus = useHandleBookingStatus({ - confirmationNumber, - expectedStatus: BookingStatusEnum.BookingCompleted, - maxRetries, - retryInterval, - enabled: isPollingForBookingStatus, - }) - - useEffect(() => { - if (bookingStatus?.data?.paymentUrl) { - router.push(bookingStatus.data.paymentUrl) - } else if (bookingStatus.isTimeout) { - toast.error( - intl.formatMessage({ - id: "payment.error.failed", - }) - ) - } - }, [bookingStatus, router, intl]) - - useEffect(() => { - setIsSubmittingDisabled( - !methods.formState.isValid || methods.formState.isSubmitting - ) - }, [ - methods.formState.isValid, - methods.formState.isSubmitting, - setIsSubmittingDisabled, - ]) - - const handleSubmit = useCallback( - (data: PaymentFormData) => { - const { - firstName, - lastName, - email, - phoneNumber, - countryCode, - membershipNo, - join, - dateOfBirth, - zipCode, - } = userData - const { toDate, fromDate, rooms, hotel } = booking - - // set payment method to card if saved card is submitted - const paymentMethod = isPaymentMethodEnum(data.paymentMethod) - ? data.paymentMethod - : PaymentMethodEnum.card - - const savedCreditCard = savedCreditCards?.find( - (card) => card.id === data.paymentMethod - ) - - const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback` - - initiateBooking.mutate({ - hotelId: hotel, - checkInDate: fromDate, - checkOutDate: toDate, - rooms: rooms.map((room) => ({ - adults: room.adults, - childrenAges: room.children?.map((child) => ({ - age: child.age, - bedType: bedTypeMap[parseInt(child.bed.toString())], - })), - rateCode: - user || join || membershipNo ? room.counterRateCode : room.rateCode, - roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step. - guest: { - firstName, - lastName, - email, - phoneNumber, - countryCode, - membershipNumber: membershipNo, - becomeMember: join, - dateOfBirth, - postalCode: zipCode, - }, - packages: { - breakfast: !!(breakfast && breakfast.code), - allergyFriendly: - room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? - false, - petFriendly: - room.packages?.includes(RoomPackageCodeEnum.PET_ROOM) ?? false, - accessibility: - room.packages?.includes(RoomPackageCodeEnum.ACCESSIBILITY_ROOM) ?? - false, - }, - smsConfirmationRequested: data.smsConfirmation, - roomPrice, - })), - payment: { - paymentMethod, - card: savedCreditCard - ? { - alias: savedCreditCard.alias, - expiryDate: savedCreditCard.expirationDate, - cardType: savedCreditCard.cardType, - } - : undefined, - - success: `${paymentRedirectUrl}/success`, - error: `${paymentRedirectUrl}/error`, - cancel: `${paymentRedirectUrl}/cancel`, - }, - }) - }, - [ - breakfast, - bedType, - userData, - booking, - roomPrice, - savedCreditCards, - lang, - user, - initiateBooking, - ] - ) - - if ( - initiateBooking.isPending || - (isPollingForBookingStatus && !bookingStatus.data?.paymentUrl) - ) { - return - } - - const guaranteeing = intl.formatMessage({ id: "guaranteeing" }) - const paying = intl.formatMessage({ id: "paying" }) - const paymentVerb = mustBeGuaranteed ? guaranteeing : paying - return ( - <> - -
- {mustBeGuaranteed ? ( -
- - {intl.formatMessage({ - id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", - })} - - -
- ) : null} - {savedCreditCards?.length ? ( -
- - {intl.formatMessage({ id: "MY SAVED CARDS" })} - -
- {savedCreditCards?.map((savedCreditCard) => ( - - ))} -
-
- ) : null} -
- {savedCreditCards?.length ? ( - - {intl.formatMessage({ id: "OTHER PAYMENT METHODS" })} - - ) : null} -
- - {availablePaymentOptions.map((paymentMethod) => ( - - ))} -
-
-
- - {intl.formatMessage( - { - id: "booking.terms", - }, - { - paymentVerb, - termsLink: (str) => ( - - {str} - - ), - privacyLink: (str) => ( - - {str} - - ), - } - )} - - - - {intl.formatMessage({ - id: "I accept the terms and conditions", - })} - - - - - {intl.formatMessage({ - id: "I would like to get my booking confirmation via sms", - })} - - -
-
- -
-
-
- {priceChangeData ? ( - { - const allSearchParams = searchParams.size - ? `?${searchParams.toString()}` - : "" - router.push(`${selectRate(lang)}${allSearchParams}`) - }} - onAccept={() => priceChange.mutate({ confirmationNumber })} - /> - ) : null} - + ) } diff --git a/components/HotelReservation/Summary/BottomSheet/index.tsx b/components/HotelReservation/Summary/BottomSheet/index.tsx index c71484e6b..085325a13 100644 --- a/components/HotelReservation/Summary/BottomSheet/index.tsx +++ b/components/HotelReservation/Summary/BottomSheet/index.tsx @@ -5,7 +5,7 @@ import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" -import { formId } from "@/components/HotelReservation/EnterDetails/Payment" +import { formId } from "@/components/HotelReservation/EnterDetails/Payment/PaymentClient" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index cb7453f0b..7db2e66da 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -13,6 +13,7 @@ import type { GetSelectedRoomAvailabilityInput, HotelDataInput, } from "@/server/routers/hotels/input" +import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input" export const getLocations = cache(async function getMemoizedLocations() { return serverClient().hotel.locations.get() @@ -32,9 +33,11 @@ export const getProfileSafely = cache( } ) -export const getCreditCardsSafely = cache( - async function getMemoizedCreditCardsSafely() { - return serverClient().user.safeCreditCards() +export const getSavedPaymentCardsSafely = cache( + async function getMemoizedSavedPaymentCardsSafely( + args: GetSavedPaymentCardsInput + ) { + return serverClient().user.safePaymentCards(args) } ) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 9a575df46..14bea40c1 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -369,6 +369,7 @@ const merchantInformationSchema = z.object({ return Object.entries(val) .filter(([_, enabled]) => enabled) .map(([key]) => key) + .filter((key): key is PaymentMethodEnum => !!key) }), alternatePaymentOptions: z .record(z.string(), z.boolean()) diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts index 1e9fee267..9172285c5 100644 --- a/server/routers/user/input.ts +++ b/server/routers/user/input.ts @@ -55,3 +55,11 @@ export const signupInput = signUpSchema streetAddress: "", }, })) + +export const getSavedPaymentCardsInput = z.object({ + supportedCards: z.array(z.string()), +}) + +export type GetSavedPaymentCardsInput = z.input< + typeof getSavedPaymentCardsInput +> diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 94376b153..730a7972f 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -13,7 +13,11 @@ import { countries } from "@/components/TempDesignSystem/Form/Country/countries" import * as maskValue from "@/utils/maskValue" import { getMembership, getMembershipCards } from "@/utils/user" -import { friendTransactionsInput, staysInput } from "./input" +import { + friendTransactionsInput, + getSavedPaymentCardsInput, + staysInput, +} from "./input" import { creditCardsSchema, getFriendTransactionsSchema, @@ -752,13 +756,26 @@ export const userQueryRouter = router({ creditCards: protectedProcedure.query(async function ({ ctx }) { return await getCreditCards({ session: ctx.session }) }), - safeCreditCards: safeProtectedProcedure.query(async function ({ ctx }) { - if (!ctx.session) { - return null - } + safePaymentCards: safeProtectedProcedure + .input(getSavedPaymentCardsInput) + .query(async function ({ ctx, input }) { + if (!ctx.session) { + return null + } - return await getCreditCards({ session: ctx.session, onlyNonExpired: true }) - }), + const savedCards = await getCreditCards({ + session: ctx.session, + onlyNonExpired: true, + }) + + if (!savedCards) { + return null + } + + return savedCards.filter((card) => + input.supportedCards.includes(card.type) + ) + }), membershipCards: protectedProcedure.query(async function ({ ctx }) { getProfileCounter.add(1) diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index eb8b6c299..15f50da7c 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -33,8 +33,13 @@ export interface PaymentProps { user: SafeUser roomPrice: { publicPrice: number; memberPrice: number | undefined } otherPaymentOptions: PaymentMethodEnum[] - savedCreditCards: CreditCard[] | null mustBeGuaranteed: boolean + supportedCards: PaymentMethodEnum[] +} + +export interface PaymentClientProps + extends Omit { + savedCreditCards: CreditCard[] | null } export interface SectionPageProps {