From c1dbb7cffd8d7a9b87511264a68a9ca5420e4eb1 Mon Sep 17 00:00:00 2001 From: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:06:37 +0530 Subject: [PATCH] feat: refund policies for payment apps (#18428) * feat: refund policies for payment apps * tests for payment refund --------- Co-authored-by: Peer Richelsen --- .../components/booking/BookingListItem.tsx | 2 +- apps/web/public/static/locales/en/common.json | 6 + .../utils/bookingScenario/bookingScenario.ts | 28 +++- .../EventTypeAppSettingsInterface.tsx | 81 +++++++++- packages/app-store/stripepayment/zod.ts | 5 + .../BookEventForm/BookEventForm.tsx | 2 +- .../components/event-meta/Details.tsx | 2 +- .../bookings/lib/handleCancelBooking.ts | 8 + ...ok.test.ts => handleCancelBooking.test.ts} | 139 +++++++++++++++- .../features/bookings/lib/handleNewBooking.ts | 2 +- .../ee/payments/components/PaymentPage.tsx | 2 +- .../components/EventTypeDescription.tsx | 2 +- .../tabs/instant/EventInstantTab.tsx | 2 +- .../tabs/recurring/EventRecurringTab.tsx | 2 +- packages/lib/getPaymentAppData.ts | 8 +- packages/lib/payment/handlePaymentRefund.ts | 32 ++++ .../lib/payment/processPaymentRefund.test.ts | 151 ++++++++++++++++++ packages/lib/payment/processPaymentRefund.ts | 86 ++++++++++ packages/lib/payment/types.ts | 5 + .../hooks/usePlatformTabsNavigations.tsx | 2 +- .../event-types/hooks/useTabsNavigations.tsx | 2 +- .../event-types/payments/PaymentForm.tsx | 2 +- .../viewer/bookings/confirm.handler.ts | 73 +-------- 23 files changed, 559 insertions(+), 85 deletions(-) rename packages/features/bookings/lib/handleCancelBooking/test/{webhook.test.ts => handleCancelBooking.test.ts} (50%) create mode 100644 packages/lib/payment/handlePaymentRefund.ts create mode 100644 packages/lib/payment/processPaymentRefund.test.ts create mode 100644 packages/lib/payment/processPaymentRefund.ts create mode 100644 packages/lib/payment/types.ts diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 3cfa857580..845fd65e2a 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -11,7 +11,7 @@ import "@calcom/dayjs/locales"; import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog"; import classNames from "@calcom/lib/classNames"; import { formatTime } from "@calcom/lib/date-fns"; -import getPaymentAppData from "@calcom/lib/getPaymentAppData"; +import { getPaymentAppData } from "@calcom/lib/getPaymentAppData"; import { useCopy } from "@calcom/lib/hooks/useCopy"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useGetTheme } from "@calcom/lib/hooks/useTheme"; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 21eb02b616..3b64f74d62 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2737,6 +2737,12 @@ "month_to_date": "month to date", "year_to_date": "year to date", "custom_range": "custom range", + "refund_policy": "Refund Policy", + "always": "Always", + "never": "Never", + "payment_option": "Payment Option", + "if_cancelled": "If cancelled", + "before": "before", "show_all_columns": "Show all columns", "toggle_columns": "Toggle columns", "no_columns_found": "No columns found", diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 2d6de0a36d..4aec51d3e5 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -24,7 +24,7 @@ import type { WorkflowTriggerEvents, WorkflowMethods, } from "@calcom/prisma/client"; -import type { SchedulingType, SMSLockState, TimeUnit } from "@calcom/prisma/enums"; +import type { PaymentOption, SchedulingType, SMSLockState, TimeUnit } from "@calcom/prisma/enums"; import type { BookingStatus } from "@calcom/prisma/enums"; import type { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import type { userMetadataType } from "@calcom/prisma/zod-utils"; @@ -74,6 +74,21 @@ type InputWorkflow = { sendTo?: string; }; +type InputPayment = { + id?: number; + uid: string; + appId?: string | null; + bookingId: number; + amount: number; + fee: number; + currency: string; + success: boolean; + refunded: boolean; + data: Record; + externalId: string; + paymentOption?: PaymentOption; +}; + type InputWorkflowReminder = { id?: number; bookingUid: string; @@ -108,6 +123,7 @@ export type ScenarioData = { bookings?: InputBooking[]; webhooks?: InputWebhook[]; workflows?: InputWorkflow[]; + payment?: InputPayment[]; }; type InputCredential = typeof TestData.credentials.google & { @@ -535,6 +551,12 @@ async function addWebhooksToDb(webhooks: any[]) { }); } +async function addPaymentToDb(payment: InputPayment[]) { + await prismock.payment.createMany({ + data: payment, + }); +} + async function addWebhooks(webhooks: InputWebhook[]) { log.silly("TestData: Creating Webhooks", safeStringify(webhooks)); @@ -810,6 +832,7 @@ export async function createBookingScenario(data: ScenarioData) { await addWebhooks(data.webhooks || []); // addPaymentMock(); const workflows = await addWorkflows(data.workflows || []); + await addPaymentToDb(data.payment || []); return { eventTypes, @@ -1304,6 +1327,7 @@ export function getScenarioData( webhooks, workflows, bookings, + payment, }: { organizer?: ReturnType; eventTypes: ScenarioData["eventTypes"]; @@ -1313,6 +1337,7 @@ export function getScenarioData( webhooks?: ScenarioData["webhooks"]; workflows?: ScenarioData["workflows"]; bookings?: ScenarioData["bookings"]; + payment?: ScenarioData["payment"]; }, org?: { id: number | null } | undefined | null ) { @@ -1373,6 +1398,7 @@ export function getScenarioData( webhooks, bookings: bookings || [], workflows, + payment, } satisfies ScenarioData; } diff --git a/packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx index d0b4eac70c..ceae9d195d 100644 --- a/packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx +++ b/packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx @@ -1,8 +1,11 @@ +import * as RadioGroup from "@radix-ui/react-radio-group"; import { useState, useEffect } from "react"; import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; +import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Alert, Select, TextField } from "@calcom/ui"; +import { RefundPolicy } from "@calcom/lib/payment/types"; +import { Alert, RadioField, Select, TextField } from "@calcom/ui"; import { convertToSmallestCurrencyUnit, @@ -30,6 +33,8 @@ const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ const paymentOption = getAppData("paymentOption"); const paymentOptionSelectValue = paymentOptions.find((option) => paymentOption === option.value); const requirePayment = getAppData("enabled"); + const getSelectedOption = () => + options.find((opt) => opt.value === (getAppData("refundCountCalendarDays") === true ? 1 : 0)); const { t } = useLocale(); const recurringEventDefined = eventType.recurringEvent?.count !== undefined; @@ -54,8 +59,16 @@ const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ setAppData("paymentOption", paymentOptions[0].value); } } + + if (!getAppData("refundPolicy")) { + setAppData("refundPolicy", RefundPolicy.NEVER); + } }, [requirePayment, getAppData, setAppData]); + const options = [ + { value: 0, label: t("business_days") }, + { value: 1, label: t("calendar_days") }, + ]; return ( <> {recurringEventDefined && ( @@ -109,7 +122,7 @@ const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({
data-testid="stripe-payment-option-select" @@ -122,7 +135,14 @@ const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ return { ...option, label: t(option.label) || option.label }; })} onChange={(input) => { - if (input) setAppData("paymentOption", input.value); + if (input) { + setAppData("paymentOption", input.value); + if (input.value === "HOLD") { + setAppData("refundPolicy", RefundPolicy.NEVER); + setAppData("refundDaysCount", undefined); + setAppData("refundCountCalendarDays", undefined); + } + } }} className="mb-1 h-[38px] w-full" isDisabled={seatsEnabled || disabled} @@ -132,6 +152,61 @@ const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ {seatsEnabled && paymentOption === "HOLD" && ( )} + + {paymentOption !== "HOLD" && ( +
+ + { + setAppData("refundPolicy", val); + if (val !== RefundPolicy.DAYS) { + setAppData("refundDaysCount", undefined); + setAppData("refundCountCalendarDays", undefined); + } + }}> + + +
+ + + +
+  {t("if_cancelled")} + setAppData("refundDaysCount", parseInt(e.currentTarget.value))} + /> +