feat/payment-service-6438-cal-767 (#6677)

* WIP paymentService

* Changes for payment Service

* Fix for stripe payment flow

* Remove logs/comments

* Refactored refund for stripe app

* Move stripe handlePayment to own lib

* Move stripe delete payments to paymentService

* lint fix

* Change handleRefundError as generic function

* remove log

* remove logs

* remove logs

* Return stripe default export to lib/server

* Fixing types

* Fix types

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Revert "lint fix"

This reverts commit e1a2e4a357e58e6673c47399888ae2e00d1351a6.

* Add validation

* Revert changes removed in force push

* Removing abstract class and just leaving interface implementation

* Fix types for handlePayments

* Fix payment test appStore import

* Fix stripe metadata in event type

* Move migration to separate PR

* Revert "Move migration to separate PR"

This reverts commit 48aa64e0724a522d3cc2fefaaaee5792ee9cd9e6.

* Update packages/prisma/migrations/20230125175109_remove_type_from_payment_and_add_app_relationship/migration.sql

Co-authored-by: Omar López <zomars@me.com>

---------

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
alannnc
2023-02-08 13:36:22 -07:00
committed by GitHub
co-authored by zomars Hariom Balhara
parent 1ee3783db3
commit 2f628a17df
30 changed files with 761 additions and 273 deletions
@@ -53,7 +53,7 @@ function BookingListItem(booking: BookingItemProps) {
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data.status === BookingStatus.REJECTED) {
if (data?.status === BookingStatus.REJECTED) {
setRejectionDialogIsOpen(false);
showToast(t("booking_rejection_success"), "success");
} else {
@@ -19,7 +19,7 @@ import {
import DatePicker from "@calcom/features/calendars/DatePicker";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import notEmpty from "@calcom/lib/notEmpty";
@@ -291,7 +291,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
[timeZone]
);
const stripeAppData = getStripeAppData(eventType);
const paymentAppData = getPaymentAppData(eventType);
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
@@ -379,14 +379,14 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
</div>
</div>
)}
{stripeAppData.price > 0 && (
{paymentAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
value={paymentAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
</p>
@@ -32,7 +32,7 @@ import {
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { HttpError } from "@calcom/lib/http-error";
@@ -122,7 +122,7 @@ const BookingPage = ({
}),
{}
);
const stripeAppData = getStripeAppData(eventType);
const paymentAppData = getPaymentAppData(eventType);
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (
@@ -148,6 +148,7 @@ const BookingPage = ({
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { uid, paymentUid } = responseData;
if (paymentUid) {
return await router.push(
createPaymentLink({
@@ -557,14 +558,14 @@ const BookingPage = ({
{showEventTypeDetails && (
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
{stripeAppData.price > 0 && (
{paymentAppData.price > 0 && (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
value={paymentAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
currency={paymentAppData?.currency?.toUpperCase()}
/>
</IntlProvider>
</p>
@@ -1,13 +1,13 @@
import { EventTypeSetupProps } from "pages/event-types/[type]";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import RecurringEventController from "./RecurringEventController";
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const stripeAppData = getStripeAppData(eventType);
const paymentAppData = getPaymentAppData(eventType);
const requirePayment = stripeAppData.price > 0;
const requirePayment = paymentAppData.price > 0;
return (
<div className="">
+1 -1
View File
@@ -136,7 +136,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
)}
</div>
<EventTypeDescription
// @ts-expect-error FIXME We have a type mismtach here @hariombalhara @sean-brydon
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
eventType={type}
/>
</Link>
+5 -1
View File
@@ -22,7 +22,11 @@ export const createPaymentsFixture = (page: Page) => {
currency: "usd",
success,
refunded,
type: "STRIPE",
app: {
connect: {
slug: "stripe",
},
},
data: {},
externalId: "DEMO_PAYMENT_FROM_DB_" + Date.now(),
booking: {
+16 -1
View File
@@ -113,7 +113,22 @@ test.describe("Reschedule Tests", async () => {
status: BookingStatus.CANCELLED,
paid: false,
});
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
metadata: {
apps: {
stripe: {
price: 20000,
enabled: true,
currency: "usd",
},
},
},
},
});
const payment = await payments.create(booking.id);
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);
@@ -3,7 +3,7 @@ import { Dispatch, SetStateAction } from "react";
import { getQueryBuilderConfig } from "../lib/getQueryBuilderConfig";
import isRouterLinkedField from "../lib/isRouterLinkedField";
import { Response, SerializableForm } from "../types/types";
import { SerializableForm, Response } from "../types/types";
type Props = {
form: SerializableForm<App_RoutingForms_Form>;
@@ -1,2 +1,3 @@
export * as api from "./api";
export * as lib from "./lib";
export { metadata } from "./_metadata";
@@ -0,0 +1,215 @@
import { Booking, Payment, Prisma } from "@prisma/client";
import Stripe from "stripe";
import { v4 as uuidv4 } from "uuid";
import z from "zod";
import { sendAwaitingPaymentEmail } from "@calcom/emails";
import { IAbstractPaymentService } from "@calcom/lib/PaymentService";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
import { CalendarEvent } from "@calcom/types/Calendar";
import { createPaymentLink } from "./client";
import { StripePaymentData } from "./server";
const stripeCredentialKeysSchema = z.object({
stripe_user_id: z.string(),
default_currency: z.string(),
stripe_publishable_key: z.string(),
});
const stripeAppKeysSchema = z.object({
client_id: z.string(),
payment_fee_fixed: z.number(),
payment_fee_percentage: z.number(),
});
export class PaymentService implements IAbstractPaymentService {
private stripe: Stripe;
private credentials: z.infer<typeof stripeCredentialKeysSchema>;
constructor(credentials: { key: Prisma.JsonValue }) {
// parse credentials key
this.credentials = stripeCredentialKeysSchema.parse(credentials.key);
this.stripe = new Stripe(process.env.STRIPE_PRIVATE_KEY || "", {
apiVersion: "2020-08-27",
});
}
async create(
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
bookingId: Booking["id"]
) {
try {
// Load stripe keys
const stripeAppKeys = await prisma?.app.findFirst({
select: {
keys: true,
},
where: {
slug: "stripe",
},
});
// Parse keys with zod
const { client_id, payment_fee_fixed, payment_fee_percentage } = stripeAppKeysSchema.parse(
stripeAppKeys?.keys
);
const paymentFee = Math.round(payment.amount * payment_fee_percentage + payment_fee_fixed);
const params: Stripe.PaymentIntentCreateParams = {
amount: payment.amount,
currency: this.credentials.default_currency,
payment_method_types: ["card"],
application_fee_amount: paymentFee,
};
const paymentIntent = await this.stripe.paymentIntents.create(params, {
stripeAccount: this.credentials.stripe_user_id,
});
const paymentData = await prisma?.payment.create({
data: {
uid: uuidv4(),
app: {
connect: {
slug: "stripe",
},
},
booking: {
connect: {
id: bookingId,
},
},
amount: payment.amount,
currency: payment.currency,
externalId: paymentIntent.id,
data: Object.assign({}, paymentIntent, {
stripe_publishable_key: this.credentials.stripe_publishable_key,
stripeAccount: this.credentials.stripe_user_id,
}) as unknown as Prisma.InputJsonValue,
fee: paymentFee,
refunded: false,
success: false,
},
});
if (!paymentData) {
throw new Error();
}
return paymentData;
} catch (error) {
console.error(error);
throw new Error("Payment could not be created");
}
}
async update(): Promise<Payment> {
throw new Error("Method not implemented.");
}
async refund(paymentId: Payment["id"]): Promise<Payment> {
try {
const payment = await prisma.payment.findFirst({
where: {
id: paymentId,
success: true,
refunded: false,
},
});
if (!payment) {
throw new Error("Payment not found");
}
const refund = await this.stripe.refunds.create(
{
payment_intent: payment.externalId,
},
{ stripeAccount: (payment.data as unknown as StripePaymentData)["stripeAccount"] }
);
if (!refund || refund.status === "failed") {
throw new Error("Refund failed");
}
const updatedPayment = await prisma.payment.update({
where: {
id: payment.id,
},
data: {
refunded: true,
},
});
return updatedPayment;
} catch (e) {
const err = getErrorFromUnknown(e);
throw err;
}
}
async afterPayment(
event: CalendarEvent,
booking: {
user: { email: string | null; name: string | null; timeZone: string } | null;
id: number;
startTime: { toISOString: () => string };
uid: string;
},
paymentData: Payment
): Promise<void> {
await sendAwaitingPaymentEmail({
...event,
paymentInfo: {
link: createPaymentLink({
paymentUid: paymentData.uid,
name: booking.user?.name,
email: booking.user?.email,
date: booking.startTime.toISOString(),
}),
},
});
}
async deletePayment(paymentId: Payment["id"]): Promise<boolean> {
try {
const payment = await prisma.payment.findFirst({
where: {
id: paymentId,
},
});
if (!payment) {
throw new Error("Payment not found");
}
const stripeAccount = (payment.data as unknown as StripePaymentData).stripeAccount;
if (!stripeAccount) {
throw new Error("Stripe account not found");
}
// Expire all current sessions
const sessions = await this.stripe.checkout.sessions.list(
{
payment_intent: payment.externalId,
},
{ stripeAccount }
);
for (const session of sessions.data) {
await this.stripe.checkout.sessions.expire(session.id, { stripeAccount });
}
// Then cancel the payment intent
await this.stripe.paymentIntents.cancel(payment.externalId, { stripeAccount });
return true;
} catch (e) {
console.error(e);
return false;
}
}
getPaymentPaidStatus(): Promise<string> {
throw new Error("Method not implemented.");
}
getPaymentDetails(): Promise<Payment> {
throw new Error("Method not implemented.");
}
}
@@ -1 +1,2 @@
export * from "./constants";
export * from "./PaymentService";
+1 -179
View File
@@ -1,19 +1,7 @@
import { PaymentType, Prisma } from "@prisma/client";
import Stripe from "stripe";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@calcom/emails";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import prisma from "@calcom/prisma";
import { EventTypeModel } from "@calcom/prisma/zod";
import type { CalendarEvent } from "@calcom/types/Calendar";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { createPaymentLink } from "./client";
export type PaymentData = Stripe.Response<Stripe.PaymentIntent> & {
export type StripePaymentData = Stripe.Response<Stripe.PaymentIntent> & {
stripe_publishable_key: string;
stripeAccount: string;
};
@@ -40,170 +28,4 @@ const stripe = new Stripe(stripePrivateKey, {
apiVersion: "2020-08-27",
});
const stripeKeysSchema = z.object({
payment_fee_fixed: z.number(),
payment_fee_percentage: z.number(),
});
const stripeCredentialSchema = z.object({
stripe_user_id: z.string(),
stripe_publishable_key: z.string(),
});
export async function handlePayment(
evt: CalendarEvent,
selectedEventType: Pick<z.infer<typeof EventTypeModel>, "price" | "currency" | "metadata">,
stripeCredential: { key: Prisma.JsonValue },
booking: {
user: { email: string | null; name: string | null; timeZone: string } | null;
id: number;
startTime: { toISOString: () => string };
uid: string;
}
) {
const appKeys = await getAppKeysFromSlug("stripe");
const { payment_fee_fixed, payment_fee_percentage } = stripeKeysSchema.parse(appKeys);
const stripeAppData = getStripeAppData(selectedEventType);
const paymentFee = Math.round(stripeAppData.price * payment_fee_percentage + payment_fee_fixed);
const { stripe_user_id, stripe_publishable_key } = stripeCredentialSchema.parse(stripeCredential.key);
const params: Stripe.PaymentIntentCreateParams = {
amount: stripeAppData.price,
currency: stripeAppData.currency,
payment_method_types: ["card"],
application_fee_amount: paymentFee,
};
const paymentIntent = await stripe.paymentIntents.create(params, { stripeAccount: stripe_user_id });
const payment = await prisma.payment.create({
data: {
type: PaymentType.STRIPE,
uid: uuidv4(),
booking: {
connect: {
id: booking.id,
},
},
amount: stripeAppData.price,
fee: paymentFee,
currency: stripeAppData.currency,
success: false,
refunded: false,
data: Object.assign({}, paymentIntent, {
stripe_publishable_key,
stripeAccount: stripe_user_id,
}) /* We should treat this */ as PaymentData /* but Prisma doesn't know how to handle it, so it we treat it */ as unknown /* and then */ as Prisma.InputJsonValue,
externalId: paymentIntent.id,
},
});
await sendAwaitingPaymentEmail({
...evt,
paymentInfo: {
link: createPaymentLink({
paymentUid: payment.uid,
name: booking.user?.name,
email: booking.user?.email,
date: booking.startTime.toISOString(),
}),
},
});
return payment;
}
export async function refund(
booking: {
id: number;
uid: string;
startTime: Date;
payment: {
id: number;
success: boolean;
refunded: boolean;
externalId: string;
data: Prisma.JsonValue;
type: PaymentType;
}[];
},
calEvent: CalendarEvent
) {
try {
const payment = booking.payment.find((e) => e.success && !e.refunded);
if (!payment) return;
if (payment.type !== PaymentType.STRIPE) {
await handleRefundError({
event: calEvent,
reason: "cannot refund non Stripe payment",
paymentId: "unknown",
});
return;
}
const refund = await stripe.refunds.create(
{
payment_intent: payment.externalId,
},
{ stripeAccount: (payment.data as unknown as PaymentData)["stripeAccount"] }
);
if (!refund || refund.status === "failed") {
await handleRefundError({
event: calEvent,
reason: refund?.failure_reason || "unknown",
paymentId: payment.externalId,
});
return;
}
await prisma.payment.update({
where: {
id: payment.id,
},
data: {
refunded: true,
},
});
} catch (e) {
const err = getErrorFromUnknown(e);
console.error(err, "Refund failed");
await handleRefundError({
event: calEvent,
reason: err.message || "unknown",
paymentId: "unknown",
});
}
}
export const closePayments = async (paymentIntentId: string, stripeAccount: string) => {
try {
// Expire all current sessions
const sessions = await stripe.checkout.sessions.list(
{
payment_intent: paymentIntentId,
},
{ stripeAccount }
);
for (const session of sessions.data) {
await stripe.checkout.sessions.expire(session.id, { stripeAccount });
}
// Then cancel the payment intent
await stripe.paymentIntents.cancel(paymentIntentId, { stripeAccount });
return;
} catch (e) {
console.error(e);
return;
}
};
async function handleRefundError(opts: { event: CalendarEvent; reason: string; paymentId: string }) {
console.error(`refund failed: ${opts.reason} for booking '${opts.event.uid}'`);
await sendOrganizerPaymentRefundFailedEmail({
...opts.event,
paymentInfo: { reason: opts.reason, id: opts.paymentId },
});
}
export default stripe;
@@ -1,5 +1,6 @@
import {
BookingStatus,
MembershipRole,
Prisma,
PrismaPromise,
WebhookTriggerEvents,
@@ -8,10 +9,10 @@ import {
} from "@prisma/client";
import { NextApiRequest } from "next";
import appStore from "@calcom/app-store";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { DailyLocationType } from "@calcom/app-store/locations";
import { refund } from "@calcom/app-store/stripepayment/lib/server";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { deleteMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
@@ -23,6 +24,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import sendPayload, { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { HttpError } from "@calcom/lib/http-error";
import { handleRefundError } from "@calcom/lib/payment/handleRefundError";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils";
@@ -65,6 +67,8 @@ async function handler(req: NextApiRequest & { userId?: number }) {
paid: true,
eventType: {
select: {
owner: true,
teamId: true,
recurringEvent: true,
title: true,
description: true,
@@ -377,7 +381,76 @@ async function handler(req: NextApiRequest & { userId?: number }) {
uid: bookingToDelete.uid ?? "",
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
};
await refund(bookingToDelete, evt);
const successPayment = bookingToDelete.payment.find((payment) => payment.success);
if (!successPayment) {
throw new Error("Cannot reject a booking without a successful payment");
}
let eventTypeOwnerId;
if (bookingToDelete.eventType?.owner) {
eventTypeOwnerId = bookingToDelete.eventType.owner.id;
} else if (bookingToDelete.eventType?.teamId) {
const teamOwner = await prisma.membership.findFirst({
where: {
teamId: bookingToDelete.eventType.teamId,
role: MembershipRole.OWNER,
},
select: {
userId: true,
},
});
eventTypeOwnerId = teamOwner?.userId;
}
if (!eventTypeOwnerId) {
throw new Error("Event Type owner not found for obtaining payment app credentials");
}
const paymentAppCredentials = await prisma.credential.findMany({
where: {
userId: eventTypeOwnerId,
appId: successPayment.appId,
},
select: {
key: true,
appId: true,
app: {
select: {
categories: true,
dirName: true,
},
},
},
});
const paymentAppCredential = paymentAppCredentials.find((credential) => {
return credential.appId === successPayment.appId;
});
if (!paymentAppCredential) {
throw new Error("Payment app credentials not found");
}
// Posible to refactor TODO:
const paymentApp = appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore];
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return null;
}
const PaymentService = paymentApp.lib.PaymentService;
const paymentInstance = new PaymentService(paymentAppCredential);
try {
await paymentInstance.refund(successPayment.id);
} catch (error) {
await handleRefundError({
event: evt,
reason: error?.toString() || "unknown",
paymentId: successPayment.externalId,
});
}
await prisma.booking.update({
where: {
id: bookingToDelete.id,
@@ -1,4 +1,5 @@
import {
App,
BookingStatus,
Credential,
EventTypeCustomInput,
@@ -18,8 +19,7 @@ import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_m
import { getLocationValueForDB, LocationObject } from "@calcom/app-store/locations";
import { MeetLocationType } from "@calcom/app-store/locations";
import { handleEthSignature } from "@calcom/app-store/rainbow/utils/ethereum";
import { handlePayment } from "@calcom/app-store/stripepayment/lib/server";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import { EventTypeAppsList, getEventTypeAppData } from "@calcom/app-store/utils";
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import EventManager from "@calcom/core/EventManager";
import { getEventName } from "@calcom/core/event";
@@ -37,10 +37,11 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { HttpError } from "@calcom/lib/http-error";
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger";
import { handlePayment } from "@calcom/lib/payment/handlePayment";
import { checkBookingLimits, getLuckyUser } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
@@ -63,6 +64,15 @@ const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
type User = Prisma.UserGetPayload<typeof userSelect>;
type BufferedBusyTimes = BufferedBusyTime[];
interface IEventTypePaymentCredentialType {
appId: EventTypeAppsList;
app: {
categories: App["categories"];
dirName: string;
};
key: Prisma.JsonValue;
}
/**
* Refreshes a Credential with fresh data from the database.
*
@@ -324,7 +334,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
const stripeAppData = getStripeAppData(eventType);
const paymentAppData = getPaymentAppData(eventType);
// Check if required custom inputs exist
handleCustomInputs(eventType.customInputs as EventTypeCustomInput[], reqBody.customInputs);
@@ -634,18 +644,48 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
const eventManager = new EventManager({ ...organizerUser, credentials });
await eventManager.updateCalendarAttendees(evt, booking);
if (!Number.isNaN(stripeAppData.price) && stripeAppData.price > 0 && !!booking) {
const [firstStripeCredential] = organizerUser.credentials.filter(
(cred) => cred.type == "stripe_payment"
if (!Number.isNaN(paymentAppData.price) && paymentAppData.price > 0 && !!booking) {
const credentialPaymentAppCategories = await prisma.credential.findMany({
where: {
userId: organizerUser.id,
app: {
categories: {
hasSome: ["payment"],
},
},
},
select: {
key: true,
appId: true,
app: {
select: {
categories: true,
dirName: true,
},
},
},
});
const eventTypePaymentAppCredential = credentialPaymentAppCategories.find((credential) => {
return credential.appId === paymentAppData.appId;
});
if (!eventTypePaymentAppCredential) {
throw new HttpError({ statusCode: 400, message: "Missing payment credentials" });
}
if (!eventTypePaymentAppCredential?.appId) {
throw new HttpError({ statusCode: 400, message: "Missing payment app id" });
}
const payment = await handlePayment(
evt,
eventType,
eventTypePaymentAppCredential as IEventTypePaymentCredentialType,
booking
);
if (!firstStripeCredential)
throw new HttpError({ statusCode: 400, message: "Missing payment credentials" });
const payment = await handlePayment(evt, eventType, firstStripeCredential, booking);
req.statusCode = 201;
return { ...booking, message: "Payment required", paymentUid: payment.uid };
return { ...booking, message: "Payment required", paymentUid: payment?.uid };
}
req.statusCode = 201;
@@ -713,7 +753,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
// Otherwise, an owner rescheduling should be always accepted.
// Before comparing make sure that userId is set, otherwise undefined === undefined
const userReschedulingIsOwner = userId && originalRescheduledBooking?.user?.id === userId;
const isConfirmedByDefault = (!requiresConfirmation && !stripeAppData.price) || userReschedulingIsOwner;
const isConfirmedByDefault = (!requiresConfirmation && !paymentAppData.price) || userReschedulingIsOwner;
async function createBooking() {
if (originalRescheduledBooking) {
@@ -807,7 +847,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
}
}
if (typeof stripeAppData.price === "number" && stripeAppData.price > 0) {
if (typeof paymentAppData.price === "number" && paymentAppData.price > 0) {
/* Validate if there is any stripe_payment credential for this user */
/* note: removes custom error message about stripe */
await prisma.credential.findFirstOrThrow({
@@ -939,7 +979,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
}
// If it's not a reschedule, doesn't require confirmation and there's no price,
// Create a booking
} else if (!requiresConfirmation && !stripeAppData.price) {
} else if (!requiresConfirmation && !paymentAppData.price) {
// Use EventManager to conditionally use all needed integrations.
const createManager = await eventManager.create(evt);
@@ -1019,21 +1059,51 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
}
if (
!Number.isNaN(stripeAppData.price) &&
stripeAppData.price > 0 &&
!Number.isNaN(paymentAppData.price) &&
paymentAppData.price > 0 &&
!originalRescheduledBooking?.paid &&
!!booking
) {
const [firstStripeCredential] = organizerUser.credentials.filter((cred) => cred.type == "stripe_payment");
// Load credentials.app.categories
const credentialPaymentAppCategories = await prisma.credential.findMany({
where: {
userId: organizerUser.id,
app: {
categories: {
hasSome: ["payment"],
},
},
},
select: {
key: true,
appId: true,
app: {
select: {
categories: true,
dirName: true,
},
},
},
});
const eventTypePaymentAppCredential = credentialPaymentAppCategories.find((credential) => {
return credential.appId === paymentAppData.appId;
});
if (!firstStripeCredential)
if (!eventTypePaymentAppCredential) {
throw new HttpError({ statusCode: 400, message: "Missing payment credentials" });
}
// Convert type of eventTypePaymentAppCredential to appId: EventTypeAppList
if (!booking.user) booking.user = organizerUser;
const payment = await handlePayment(evt, eventType, firstStripeCredential, booking);
const payment = await handlePayment(
evt,
eventType,
eventTypePaymentAppCredential as IEventTypePaymentCredentialType,
booking
);
req.statusCode = 201;
return { ...booking, message: "Payment required", paymentUid: payment.uid };
return { ...booking, message: "Payment required", paymentUid: payment?.uid };
}
log.debug(`Booking ${organizerUser.username} completed`);
@@ -1082,7 +1152,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
eventTitle: eventType.title,
eventDescription: eventType.description,
requiresConfirmation: requiresConfirmation || null,
price: stripeAppData.price,
price: paymentAppData.price,
currency: eventType.currency,
length: eventType.length,
};
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { stringify } from "querystring";
import { SyntheticEvent, useEffect, useState } from "react";
import { PaymentData } from "@calcom/app-store/stripepayment/lib/server";
import { StripePaymentData } from "@calcom/app-store/stripepayment/lib/server";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
@@ -29,7 +29,7 @@ const CARD_OPTIONS: stripejs.StripeCardElementOptions = {
type Props = {
payment: {
data: PaymentData;
data: StripePaymentData;
};
eventType: { id: number };
user: { username: string | null };
@@ -6,10 +6,11 @@ import { FormattedNumber, IntlProvider } from "react-intl";
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
import getStripe from "@calcom/app-store/stripepayment/lib/client";
import { StripePaymentData } from "@calcom/app-store/stripepayment/lib/server";
import dayjs from "@calcom/dayjs";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
@@ -26,7 +27,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
const [timezone, setTimezone] = useState<string | null>(null);
useTheme(props.profile.theme);
const isEmbed = useIsEmbed();
const stripeAppData = getStripeAppData(props.eventType);
const paymentAppData = getPaymentAppData(props.eventType);
useEffect(() => {
let embedIframeWidth = 0;
const _timezone = localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess();
@@ -110,9 +111,9 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
<div className="col-span-2 mb-6">
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
value={paymentAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
currency={paymentAppData?.currency?.toUpperCase()}
/>
</IntlProvider>
</div>
@@ -123,8 +124,9 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
{props.payment.success && !props.payment.refunded && (
<div className="mt-4 text-center text-gray-700 dark:text-gray-300">{t("paid")}</div>
)}
{!props.payment.success && (
<Elements stripe={getStripe(props.payment.data.stripe_publishable_key)}>
{props.payment.appId === "stripe" && !props.payment.success && (
<Elements
stripe={getStripe((props.payment.data as StripePaymentData).stripe_publishable_key)}>
<PaymentComponent
payment={props.payment}
eventType={props.eventType}
@@ -1,7 +1,7 @@
import { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { PaymentData } from "@calcom/app-store/stripepayment/lib/server";
import { StripePaymentData } from "@calcom/app-store/stripepayment/lib/server";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -28,6 +28,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
uid: true,
refunded: true,
bookingId: true,
appId: true,
booking: {
select: {
id: true,
@@ -81,7 +82,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const { data, booking: _booking, ...restPayment } = rawPayment;
const payment = {
...restPayment,
data: data as unknown as PaymentData,
data: data as unknown as StripePaymentData,
};
if (!_booking) return { notFound: true };
@@ -4,7 +4,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
import { z } from "zod";
import { classNames, parseRecurringEvent } from "@calcom/lib";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { baseEventTypeSelect } from "@calcom/prisma";
import { EventTypeModel } from "@calcom/prisma/zod";
@@ -29,7 +29,7 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
[eventType.recurringEvent]
);
const stripeAppData = getStripeAppData(eventType);
const stripeAppData = getPaymentAppData(eventType);
return (
<>
@@ -80,7 +80,7 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
<FormattedNumber
value={stripeAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
currency={stripeAppData?.currency?.toUpperCase()}
/>
</IntlProvider>
</Badge>
+25
View File
@@ -0,0 +1,25 @@
import { Payment, Prisma, Booking } from "@prisma/client";
import { CalendarEvent } from "@calcom/types/Calendar";
export interface IAbstractPaymentService {
create(
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
bookingId: Booking["id"]
): Promise<Payment>;
update(paymentId: Payment["id"], data: Partial<Prisma.PaymentUncheckedCreateInput>): Promise<Payment>;
refund(paymentId: Payment["id"]): Promise<Payment>;
getPaymentPaidStatus(): Promise<string>;
getPaymentDetails(): Promise<Payment>;
afterPayment(
event: CalendarEvent,
booking: {
user: { email: string | null; name: string | null; timeZone: string } | null;
id: number;
startTime: { toISOString: () => string };
uid: string;
},
paymentData: Payment
): Promise<void>;
deletePayment(paymentId: Payment["id"]): Promise<boolean>;
}
+2 -2
View File
@@ -6,7 +6,7 @@ import { LocationObject } from "@calcom/core/location";
import { parseBookingLimit, parseRecurringEvent } from "@calcom/lib";
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
import { CAL_URL } from "@calcom/lib/constants";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { getTranslation } from "@calcom/lib/server/i18n";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -201,7 +201,7 @@ export default async function getEventTypeById({
newMetadata.apps = {
...apps,
stripe: {
...getStripeAppData(eventTypeWithParsedMetadata, true),
...getPaymentAppData(eventTypeWithParsedMetadata, true),
currency:
(
credentials.find((integration) => integration.type === "stripe_payment")
+42
View File
@@ -0,0 +1,42 @@
import { z } from "zod";
import { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
import { appDataSchema } from "@calcom/app-store/stripepayment/zod";
import { EventTypeAppsList, getEventTypeAppData } from "@calcom/app-store/utils";
export default function getPaymentAppData(
eventType: Parameters<typeof getEventTypeAppData>[0],
forcedGet?: boolean
) {
const metadataApps = eventType?.metadata?.apps as unknown as EventTypeAppsList;
if (!metadataApps) {
return { enabled: false, price: 0, currency: "usd", appId: null };
}
type appId = keyof typeof metadataApps;
// @TODO: a lot of unknowns types here can be improved later
const paymentAppIds = (Object.keys(metadataApps) as Array<keyof typeof appDataSchemas>).filter(
(app) =>
(metadataApps[app as appId] as unknown as z.infer<typeof appDataSchema>)?.price &&
(metadataApps[app as appId] as unknown as z.infer<typeof appDataSchema>)?.enabled
);
// Event type should only have one payment app data
let paymentAppData: {
enabled: boolean;
price: number;
currency: string;
appId: EventTypeAppsList | null;
} | null = null;
for (const appId of paymentAppIds) {
const appData = getEventTypeAppData(eventType, appId, forcedGet);
if (appData && paymentAppData === null) {
paymentAppData = {
...appData,
appId,
};
}
}
// This is the current expectation of system to have price and currency set always(using DB Level defaults).
// Newly added apps code should assume that their app data might not be set.
return paymentAppData || { enabled: false, price: 0, currency: "usd", appId: null };
}
-11
View File
@@ -1,11 +0,0 @@
import { getEventTypeAppData } from "@calcom/app-store/utils";
export default function getStripeAppData(
eventType: Parameters<typeof getEventTypeAppData>[0],
forcedGet?: boolean
) {
const stripeAppData = getEventTypeAppData(eventType, "stripe", forcedGet);
// This is the current expectation of system to have price and currency set always(using DB Level defaults).
// Newly added apps code should assume that their app data might not be set.
return stripeAppData || { enabled: false, price: 0, currency: "usd" };
}
+28
View File
@@ -0,0 +1,28 @@
import appStore from "@calcom/app-store";
import { EventTypeAppsList } from "@calcom/app-store/utils";
import { AppCategories, Payment, Prisma } from ".prisma/client";
const deletePayment = async (
paymentId: Payment["id"],
paymentAppCredentials: {
key: Prisma.JsonValue;
appId: string | null;
app: {
dirName: string;
categories: AppCategories[];
} | null;
}
): Promise<boolean> => {
const paymentApp = appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return false;
}
const PaymentService = paymentApp.lib.PaymentService;
const paymentInstance = new PaymentService(paymentAppCredentials);
const deleted = await paymentInstance.deletePayment(paymentId);
return deleted;
};
export { deletePayment };
+53
View File
@@ -0,0 +1,53 @@
import appStore from "@calcom/app-store";
import { EventTypeAppsList } from "@calcom/app-store/utils";
import { EventTypeModel } from "@calcom/prisma/zod";
import { CalendarEvent } from "@calcom/types/Calendar";
import { AppCategories, Prisma } from ".prisma/client";
const handlePayment = async (
evt: CalendarEvent,
selectedEventType: Pick<Zod.infer<typeof EventTypeModel>, "metadata">,
paymentAppCredentials: {
key: Prisma.JsonValue;
appId: EventTypeAppsList;
app: {
dirName: string;
categories: AppCategories[];
} | null;
},
booking: {
user: { email: string | null; name: string | null; timeZone: string } | null;
id: number;
startTime: { toISOString: () => string };
uid: string;
}
) => {
const paymentApp = appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return null;
}
const PaymentService = paymentApp.lib.PaymentService;
const paymentInstance = new PaymentService(paymentAppCredentials);
const paymentData = await paymentInstance.create(
{
amount: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].price,
currency: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].currency,
},
booking.id
);
if (!paymentData) {
console.error("Payment data is null");
throw new Error("Payment data is null");
}
try {
await paymentInstance.afterPayment(evt, booking, paymentData);
} catch (e) {
console.error(e);
}
return paymentData;
};
export { handlePayment };
+16
View File
@@ -0,0 +1,16 @@
import { sendOrganizerPaymentRefundFailedEmail } from "@calcom/emails";
import { CalendarEvent } from "@calcom/types/Calendar";
const handleRefundError = async (opts: { event: CalendarEvent; reason: string; paymentId: string }) => {
console.error(`refund failed: ${opts.reason} for booking '${opts.event.uid}'`);
try {
await sendOrganizerPaymentRefundFailedEmail({
...opts.event,
paymentInfo: { reason: opts.reason, id: opts.paymentId },
});
} catch (e) {
console.error(e);
}
};
export { handleRefundError };
@@ -0,0 +1,19 @@
/*
Warnings:
- You are about to drop the column `type` on the `Payment` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Payment" DROP COLUMN "type",
ADD COLUMN "appId" TEXT;
-- DropEnum
DROP TYPE "PaymentType";
-- AddForeignKey
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App"("slug") ON DELETE CASCADE ON UPDATE CASCADE;
-- At this point all payments are from stripe. We update existing values to reflect this.
UPDATE "Payment" SET "appId" = 'stripe';
+7 -10
View File
@@ -399,24 +399,20 @@ model ReminderMail {
createdAt DateTime @default(now())
}
enum PaymentType {
STRIPE
}
model Payment {
id Int @id @default(autoincrement())
uid String @unique
// TODO: Use an App relationship instead of PaymentType enum?
type PaymentType
id Int @id @default(autoincrement())
uid String @unique
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
appId String?
bookingId Int
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
amount Int
fee Int
currency String
success Boolean
refunded Boolean
data Json
externalId String @unique
externalId String @unique
}
enum WebhookTriggerEvents {
@@ -525,6 +521,7 @@ model App {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
credentials Credential[]
payments Payment[]
Webhook Webhook[]
ApiKey ApiKey[]
enabled Boolean @default(false)
+19 -12
View File
@@ -6,7 +6,7 @@ import z from "zod";
import app_RoutingForms from "@calcom/app-store/ee/routing-forms/trpc-router";
import ethRouter from "@calcom/app-store/rainbow/trpc/router";
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
import stripe, { closePayments } from "@calcom/app-store/stripepayment/lib/server";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { getPremiumPlanProductId } from "@calcom/app-store/stripepayment/lib/utils";
import getApps, { getLocationGroupedOptions } from "@calcom/app-store/utils";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
@@ -20,8 +20,9 @@ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
import { ErrorCode, verifyPassword } from "@calcom/lib/auth";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { deletePayment } from "@calcom/lib/payment/deletePayment";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { getTranslation } from "@calcom/lib/server/i18n";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
@@ -817,11 +818,14 @@ const loggedInViewerRouter = router({
id: id,
userId: ctx.user.id,
},
include: {
select: {
key: true,
appId: true,
app: {
select: {
slug: true,
categories: true,
dirName: true,
},
},
},
@@ -838,7 +842,11 @@ const loggedInViewerRouter = router({
select: {
id: true,
locations: true,
destinationCalendar: true,
destinationCalendar: {
include: {
credential: true,
},
},
price: true,
currency: true,
metadata: true,
@@ -882,7 +890,7 @@ const loggedInViewerRouter = router({
// If it's a calendar, remove the destination calendar from the event type
if (credential.app?.categories.includes(AppCategories.calendar)) {
if (eventType.destinationCalendar?.integration === credential.type) {
if (eventType.destinationCalendar?.credential?.appId === credential.appId) {
const destinationCalendar = await prisma.destinationCalendar.findFirst({
where: {
id: eventType.destinationCalendar?.id,
@@ -920,7 +928,7 @@ const loggedInViewerRouter = router({
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
const stripeAppData = getStripeAppData({ ...eventType, metadata });
const stripeAppData = getPaymentAppData({ ...eventType, metadata });
// If it's a payment, hide the event type and set the price to 0. Also cancel all pending bookings
if (credential.app?.categories.includes(AppCategories.payment)) {
@@ -1007,12 +1015,11 @@ const loggedInViewerRouter = router({
});
for (const payment of booking.payment) {
// Right now we only close payments on Stripe
const stripeKeysSchema = z.object({
stripe_user_id: z.string(),
});
const { stripe_user_id } = stripeKeysSchema.parse(credential.key);
await closePayments(payment.externalId, stripe_user_id);
try {
await deletePayment(payment.id, credential);
} catch (e) {
console.error(e);
}
await prisma.payment.delete({
where: {
id: payment.id,
@@ -279,4 +279,45 @@ export const appsRouter = router({
});
return !!gCalPresent;
}),
updateAppCredentials: authedProcedure
.input(
z.object({
credentialId: z.number(),
key: z.object({}).passthrough(),
})
)
.mutation(async ({ ctx, input }) => {
const { user } = ctx;
const { key } = input;
// Find user credential
const credential = await ctx.prisma.credential.findFirst({
where: {
id: input.credentialId,
userId: user.id,
},
});
// Check if credential exists
if (!credential) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Could not find credential ${input.credentialId}`,
});
}
const updated = await ctx.prisma.credential.update({
where: {
id: credential.id,
},
data: {
key: {
...(credential.key as Prisma.JsonObject),
...(key as Prisma.JsonObject),
},
},
});
return !!updated;
}),
});
@@ -2,6 +2,7 @@ import {
BookingReference,
BookingStatus,
EventType,
MembershipRole,
Prisma,
SchedulingType,
User,
@@ -13,9 +14,9 @@ import {
import type { TFunction } from "next-i18next";
import { z } from "zod";
import appStore from "@calcom/app-store";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { DailyLocationType } from "@calcom/app-store/locations";
import { refund } from "@calcom/app-store/stripepayment/lib/server";
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import EventManager from "@calcom/core/EventManager";
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
@@ -319,7 +320,7 @@ export const bookingsRouter = router({
const recurringInfo = recurringInfoBasic.map(
(
info: typeof recurringInfoBasic[number]
info: (typeof recurringInfoBasic)[number]
): {
recurringEventId: string | null;
count: number;
@@ -709,6 +710,8 @@ export const bookingsRouter = router({
eventType: {
select: {
id: true,
owner: true,
teamId: true,
recurringEvent: true,
title: true,
requiresConfirmation: true,
@@ -1074,7 +1077,70 @@ export const bookingsRouter = router({
},
});
} else {
await refund(booking, evt); // No payment integration for recurring events for v1
const successPayment = booking.payment.find((payment) => payment.success);
if (!successPayment) {
throw new Error("Cannot reject a booking without a successful payment");
}
let eventTypeOwnerId;
if (booking.eventType?.owner) {
eventTypeOwnerId = booking.eventType.owner.id;
} else if (booking.eventType?.teamId) {
const teamOwner = await prisma.membership.findFirst({
where: {
teamId: booking.eventType.teamId,
role: MembershipRole.OWNER,
},
select: {
userId: true,
},
});
eventTypeOwnerId = teamOwner?.userId;
}
if (!eventTypeOwnerId) {
throw new Error("Event Type owner not found for obtaining payment app credentials");
}
const paymentAppCredentials = await prisma.credential.findMany({
where: {
userId: eventTypeOwnerId,
appId: successPayment.appId,
},
select: {
key: true,
appId: true,
app: {
select: {
categories: true,
dirName: true,
},
},
},
});
const paymentAppCredential = paymentAppCredentials.find((credential) => {
return credential.appId === successPayment.appId;
});
if (!paymentAppCredential) {
throw new Error("Payment app credentials not found");
}
// Posible to refactor TODO:
const paymentApp = appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore];
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return null;
}
const PaymentService = paymentApp.lib.PaymentService;
const paymentInstance = new PaymentService(paymentAppCredential);
const paymentData = await paymentInstance.refund(successPayment.id);
if (!paymentData.refunded) {
throw new Error("Payment could not be refunded");
}
await prisma.booking.update({
where: {
id: bookingId,