From da0bdfacae1cf86d9b36a101c4df98607317be71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Tue, 13 Dec 2022 05:43:37 -0700 Subject: [PATCH] Handle membership updates in Stripe (#5992) --- packages/features/ee/teams/lib/payments.ts | 50 +++++++++++++++---- packages/trpc/server/routers/viewer/teams.tsx | 18 ++----- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/packages/features/ee/teams/lib/payments.ts b/packages/features/ee/teams/lib/payments.ts index 2df29ff18e..ec09284257 100644 --- a/packages/features/ee/teams/lib/payments.ts +++ b/packages/features/ee/teams/lib/payments.ts @@ -1,9 +1,17 @@ +import { z } from "zod"; + import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer"; import stripe from "@calcom/app-store/stripepayment/lib/server"; import { WEBAPP_URL } from "@calcom/lib/constants"; import prisma from "@calcom/prisma"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; +const teamPaymentMetadataSchema = z.object({ + paymentId: z.string(), + subscriptionId: z.string(), + subscriptionItemId: z.string(), +}); + /** Used to prevent double charges for the same team */ export const checkIfTeamPaymentRequired = async ({ teamId = -1 }) => { const team = await prisma.team.findUniqueOrThrow({ @@ -56,21 +64,43 @@ export const purchaseTeamSubscription = async (input: { teamId: number; seats: n return { url: session.url }; }; +const getTeamWithPaymentMetadata = async (teamId: number) => { + const team = await prisma.team.findUniqueOrThrow({ + where: { id: teamId }, + select: { metadata: true, members: true }, + }); + const metadata = teamPaymentMetadataSchema.parse(team.metadata); + return { ...team, metadata }; +}; + export const cancelTeamSubscriptionFromStripe = async (teamId: number) => { try { - const team = await prisma.team.findUniqueOrThrow({ - where: { id: teamId }, - select: { metadata: true }, - }); - const metadata = teamMetadataSchema.parse(team.metadata); - if (!metadata?.subscriptionId) - throw Error( - `Couldn't cancelTeamSubscriptionFromStripe, Team id: ${teamId} didn't have a subscriptionId` - ); - return await stripe.subscriptions.cancel(metadata.subscriptionId); + const team = await getTeamWithPaymentMetadata(teamId); + const { subscriptionId } = team.metadata; + return await stripe.subscriptions.cancel(subscriptionId); } catch (error) { let message = "Unknown error on cancelTeamSubscriptionFromStripe"; if (error instanceof Error) message = error.message; console.error(message); } }; + +export const updateQuantitySubscriptionFromStripe = async (teamId: number) => { + try { + const { url } = await checkIfTeamPaymentRequired({ teamId }); + /** + * If there's no pending checkout URL it means that this team has not been paid. + * We cannot update the subscription yet, this will be handled on publish/checkout. + **/ + if (!url) return; + const team = await getTeamWithPaymentMetadata(teamId); + const { subscriptionId, subscriptionItemId } = team.metadata; + await stripe.subscriptions.update(subscriptionId, { + items: [{ quantity: team.members.length, id: subscriptionItemId }], + }); + } catch (error) { + let message = "Unknown error on updateQuantitySubscriptionFromStripe"; + if (error instanceof Error) message = error.message; + console.error(message); + } +}; diff --git a/packages/trpc/server/routers/viewer/teams.tsx b/packages/trpc/server/routers/viewer/teams.tsx index 2242798c68..81be573198 100644 --- a/packages/trpc/server/routers/viewer/teams.tsx +++ b/packages/trpc/server/routers/viewer/teams.tsx @@ -8,8 +8,9 @@ import { sendTeamInviteEmail } from "@calcom/emails"; import { cancelTeamSubscriptionFromStripe, purchaseTeamSubscription, + updateQuantitySubscriptionFromStripe, } from "@calcom/features/ee/teams/lib/payments"; -import { HOSTED_CAL_FEATURES, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; +import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { getTranslation } from "@calcom/lib/server/i18n"; import { getTeamWithMembers, isTeamAdmin, isTeamMember, isTeamOwner } from "@calcom/lib/server/queries/teams"; import slugify from "@calcom/lib/slugify"; @@ -242,8 +243,7 @@ export const viewerTeamsRouter = router({ // Sync Services closeComDeleteTeamMembership(membership.user); - // @TODO: Update with new logic - // if (HOSTED_CAL_FEATURES) await removeSeat(ctx.user.id, input.teamId, input.memberId); + if (IS_TEAM_BILLING_ENABLED) await updateQuantitySubscriptionFromStripe(input.teamId); }), inviteMember: authedProcedure .input( @@ -354,13 +354,7 @@ export const viewerTeamsRouter = router({ }); } } - - // @TODO: Update with new logic - // try { - // if (HOSTED_CAL_FEATURES) await addSeat(ctx.user.id, team.id, inviteeUserId); - // } catch (e) { - // console.log(e); - // } + if (IS_TEAM_BILLING_ENABLED) await updateQuantitySubscriptionFromStripe(input.teamId); }), acceptOrLeave: authedProcedure .input( @@ -392,10 +386,6 @@ export const viewerTeamsRouter = router({ include: { team: true }, }); - // TODO: disable if not hosted by Cal - // @TODO: Update with new logic - // if (teamOwner) await removeSeat(teamOwner.userId, input.teamId, ctx.user.id); - const membership = await ctx.prisma.membership.delete({ where: { userId_teamId: { userId: ctx.user.id, teamId: input.teamId },