When deleting video app, change locations to Cal Video (#3022)
* tRPC endpoint to delete credentials * Replace deleted app with daily * Updated Jitisi logo * Update logo size * Update logo size * Fix type error * Remove type as prop * Add isPrismaArray to lib * Address feedback * Remove connect calednar * Expire payment sessions and cancel payment intent * Expire payment sessions and cancel payment intent * Refactor input Co-authored-by: Omar López <zomars@me.com> * Remove console.log Co-authored-by: Omar López <zomars@me.com> * Address feedback * Find connected account on Stripe * Fix type error * Add interactive transactions * Remove console.logs * Remove console.log Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
parent
67ca98018e
commit
89ef6c3f13
|
@ -6,6 +6,8 @@ import showToast from "@calcom/lib/notification";
|
|||
import { ButtonBaseProps } from "@calcom/ui/Button";
|
||||
import { Dialog } from "@calcom/ui/Dialog";
|
||||
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||
|
||||
export default function DisconnectIntegration(props: {
|
||||
|
@ -14,32 +16,23 @@ export default function DisconnectIntegration(props: {
|
|||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
||||
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
|
||||
}) {
|
||||
const { id } = props;
|
||||
const { t } = useLocale();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const mutation = useMutation(
|
||||
async () => {
|
||||
const res = await fetch("/api/integrations", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ id: props.id }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Something went wrong");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
{
|
||||
async onSettled() {
|
||||
|
||||
const mutation = trpc.useMutation("viewer.deleteCredential", {
|
||||
onSettled: async () => {
|
||||
await props.onOpenChange(modalOpen);
|
||||
},
|
||||
onSuccess(data) {
|
||||
showToast(data.message, "success");
|
||||
onSuccess: () => {
|
||||
showToast("Integration deleted successfully", "success");
|
||||
setModalOpen(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
onError: () => {
|
||||
throw new Error("Something went wrong");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
|
@ -49,7 +42,7 @@ export default function DisconnectIntegration(props: {
|
|||
confirmBtnText={t("yes_remove_app")}
|
||||
cancelBtnText="Cancel"
|
||||
onConfirm={() => {
|
||||
mutation.mutate();
|
||||
mutation.mutate({ id });
|
||||
}}>
|
||||
{t("are_you_sure_you_want_to_remove_this_app")}
|
||||
</ConfirmationDialogContent>
|
||||
|
|
|
@ -151,6 +151,27 @@ export async function refund(
|
|||
}
|
||||
}
|
||||
|
||||
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({
|
||||
|
|
|
@ -21,7 +21,6 @@ import Shell, { ShellSubHeading } from "@components/Shell";
|
|||
import SkeletonLoader from "@components/apps/SkeletonLoader";
|
||||
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||
import DisconnectStripeIntegration from "@components/integrations/DisconnectStripeIntegration";
|
||||
import IntegrationListItem from "@components/integrations/IntegrationListItem";
|
||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||
|
||||
|
@ -42,7 +41,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
if (credentialId) {
|
||||
if (type === "stripe_payment") {
|
||||
return (
|
||||
<DisconnectStripeIntegration
|
||||
<DisconnectIntegration
|
||||
id={credentialId}
|
||||
render={(btnProps) => (
|
||||
<Button {...btnProps} color="warn" data-testid="integration-connection-button">
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
import { BookingStatus, MembershipRole, Prisma } from "@prisma/client";
|
||||
import { BookingStatus, MembershipRole, AppCategories, Prisma } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import _ from "lodash";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
|
@ -8,8 +8,10 @@ import getApps, { getLocationOptions } from "@calcom/app-store/utils";
|
|||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { checkPremiumUsername } from "@calcom/ee/lib/core/checkPremiumUsername";
|
||||
import { sendFeedbackEmail } from "@calcom/emails";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { sendCancelledEmails } from "@calcom/emails";
|
||||
import { parseRecurringEvent, isPrismaObjOrUndefined } from "@calcom/lib";
|
||||
import { baseEventTypeSelect, bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { closePayments } from "@ee/lib/stripe/server";
|
||||
|
||||
import { checkRegularUsername } from "@lib/core/checkRegularUsername";
|
||||
import jackson from "@lib/jackson";
|
||||
|
@ -942,6 +944,239 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
|
||||
return locationOptions;
|
||||
},
|
||||
})
|
||||
.mutation("deleteCredential", {
|
||||
input: z.object({
|
||||
id: z.number(),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
const { id } = input;
|
||||
|
||||
const credential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
id: id,
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
include: {
|
||||
app: {
|
||||
select: {
|
||||
slug: true,
|
||||
categories: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!credential) {
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
const eventTypes = await prisma.eventType.findMany({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
locations: true,
|
||||
destinationCalendar: true,
|
||||
price: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const eventType of eventTypes) {
|
||||
if (eventType.locations) {
|
||||
// If it's a video, replace the location with Cal video
|
||||
if (credential.app?.categories.includes(AppCategories.video)) {
|
||||
// Find the user's event types
|
||||
|
||||
// Look for integration name from app slug
|
||||
const integrationQuery =
|
||||
credential.app?.slug === "msteams" ? "office365_video" : credential.app?.slug.split("-")[0];
|
||||
|
||||
// Check if the event type uses the deleted integration
|
||||
|
||||
// To avoid type errors, need to stringify and parse JSON to use array methods
|
||||
const locationsSchema = z.array(z.object({ type: z.string() }));
|
||||
const locations = locationsSchema.parse(eventType.locations);
|
||||
|
||||
const updatedLocations = locations.map((location: { type: string }) => {
|
||||
if (location.type.includes(integrationQuery)) {
|
||||
return { type: "integrations:daily" };
|
||||
}
|
||||
return location;
|
||||
});
|
||||
|
||||
await prisma.eventType.update({
|
||||
where: {
|
||||
id: eventType.id,
|
||||
},
|
||||
data: {
|
||||
locations: updatedLocations,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a calendar, remove the destination claendar from the event type
|
||||
if (credential.app?.categories.includes(AppCategories.calendar)) {
|
||||
if (eventType.destinationCalendar?.integration === credential.type) {
|
||||
await prisma.destinationCalendar.delete({
|
||||
where: {
|
||||
id: eventType.destinationCalendar.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
if (eventType.price) {
|
||||
await prisma.$transaction(async () => {
|
||||
await prisma.eventType.update({
|
||||
where: {
|
||||
id: eventType.id,
|
||||
},
|
||||
data: {
|
||||
hidden: true,
|
||||
price: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// Assuming that all bookings under this eventType need to be paid
|
||||
const unpaidBookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
eventTypeId: eventType.id,
|
||||
status: "PENDING",
|
||||
paid: false,
|
||||
payment: {
|
||||
every: {
|
||||
success: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
recurringEventId: true,
|
||||
userId: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
name: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
},
|
||||
},
|
||||
location: true,
|
||||
references: {
|
||||
select: {
|
||||
uid: true,
|
||||
type: true,
|
||||
externalCalendarId: true,
|
||||
},
|
||||
},
|
||||
payment: true,
|
||||
paid: true,
|
||||
eventType: {
|
||||
select: {
|
||||
recurringEvent: true,
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
uid: true,
|
||||
eventTypeId: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const booking of unpaidBookings) {
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: booking.id,
|
||||
},
|
||||
data: {
|
||||
status: BookingStatus.CANCELLED,
|
||||
cancellationReason: "Payment method removed",
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
await prisma.payment.delete({
|
||||
where: {
|
||||
id: payment.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.attendee.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
|
||||
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: {
|
||||
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
||||
locale: attendee.locale ?? "en",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const attendeesList = await Promise.all(attendeesListPromises);
|
||||
const tOrganizer = await getTranslation(booking?.user?.locale ?? "en", "common");
|
||||
|
||||
await sendCancelledEmails({
|
||||
type: booking?.eventType?.title as string,
|
||||
title: booking.title,
|
||||
description: booking.description,
|
||||
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: booking?.user?.email as string,
|
||||
name: booking?.user?.name ?? "Nameless",
|
||||
timeZone: booking?.user?.timeZone as string,
|
||||
language: { translate: tOrganizer, locale: booking?.user?.locale ?? "en" },
|
||||
},
|
||||
attendees: attendeesList,
|
||||
uid: booking.uid,
|
||||
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
||||
location: booking.location,
|
||||
destinationCalendar: booking.destinationCalendar || booking.user?.destinationCalendar,
|
||||
cancellationReason: "Payment method removed by organizer",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validated that credential is user's above
|
||||
await prisma.credential.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const viewerRouter = createRouter()
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 8.4 KiB |
|
@ -8,7 +8,7 @@ datasource db {
|
|||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["filterJson"]
|
||||
previewFeatures = ["filterJson", "interactiveTransactions"]
|
||||
}
|
||||
|
||||
generator zod {
|
||||
|
|
Loading…
Reference in New Issue
Block a user