From ae1239586a9d4019da8ed0496a565235c4f7286c Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Jan 2025 18:49:32 -0500 Subject: [PATCH 1/9] Create `checkIfTeamHasActivePlan` method on `stripe-billing-service` --- packages/features/ee/billing/billing-service.ts | 1 + packages/features/ee/billing/stripe-billling-service.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/packages/features/ee/billing/billing-service.ts b/packages/features/ee/billing/billing-service.ts index dc6b29708e51a8..63c785f843218a 100644 --- a/packages/features/ee/billing/billing-service.ts +++ b/packages/features/ee/billing/billing-service.ts @@ -7,4 +7,5 @@ export interface BillingService { subscriptionItemId: string; membershipCount: number; }): Promise; + checkIfTeamHasActivePlan(subscriptionId: string): Promise; } diff --git a/packages/features/ee/billing/stripe-billling-service.ts b/packages/features/ee/billing/stripe-billling-service.ts index 495c490c138b10..ec31eeb14e8a7f 100644 --- a/packages/features/ee/billing/stripe-billling-service.ts +++ b/packages/features/ee/billing/stripe-billling-service.ts @@ -30,4 +30,10 @@ export class StripeBillingService implements BillingService { const checkoutSession = await this.stripe.checkout.sessions.retrieve(paymentId); return checkoutSession.payment_status === "paid"; } + async checkIfTeamHasActivePlan(subscriptionId: string) { + const subscription = await this.stripe.subscriptions.retrieve(subscriptionId); + if (!subscription || !subscription.status) return false; + + return subscription.status === "active"; + } } From 02dfe969973874cd933448b97b9906cff38ce14d Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Jan 2025 19:01:26 -0500 Subject: [PATCH 2/9] Add `checkIfTeamPlanIsActive` to `InternalTeamBilling` --- packages/features/ee/billing/teams/internal-team-billing.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/features/ee/billing/teams/internal-team-billing.ts b/packages/features/ee/billing/teams/internal-team-billing.ts index 790d847461c114..89626432f4a040 100644 --- a/packages/features/ee/billing/teams/internal-team-billing.ts +++ b/packages/features/ee/billing/teams/internal-team-billing.ts @@ -161,4 +161,10 @@ export class InternalTeamBilling implements TeamBilling { paymentRequired: false, }; } + /** Used to check if the current team plan is active */ + async checkIfTeamHasActivePlan() { + const { subscriptionId } = this.team.metadata; + if (!subscriptionId) return false; + return billing.checkIfTeamHasActivePlan(subscriptionId); + } } From 16e32ee5253e8c423973f78a39b89d3044bf91e6 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Jan 2025 19:04:15 -0500 Subject: [PATCH 3/9] Create `hasActiveTeamPlan` trpc endpoint --- .../server/routers/viewer/teams/_router.tsx | 8 ++++ .../viewer/teams/hasActiveTeamPlan.handler.ts | 40 +++++++++++++++++++ .../viewer/teams/hasActiveTeamPlan.schema.ts | 7 ++++ 3 files changed, 55 insertions(+) create mode 100644 packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts create mode 100644 packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.schema.ts diff --git a/packages/trpc/server/routers/viewer/teams/_router.tsx b/packages/trpc/server/routers/viewer/teams/_router.tsx index ad12d435da9dfd..a9cffda85ad033 100644 --- a/packages/trpc/server/routers/viewer/teams/_router.tsx +++ b/packages/trpc/server/routers/viewer/teams/_router.tsx @@ -12,6 +12,7 @@ import { ZGetSchema } from "./get.schema"; import { ZGetMemberAvailabilityInputSchema } from "./getMemberAvailability.schema"; import { ZGetMembershipbyUserInputSchema } from "./getMembershipbyUser.schema"; import { ZGetUserConnectedAppsInputSchema } from "./getUserConnectedApps.schema"; +import { ZHasActiveTeamPlanSchema } from "./hasActiveTeamPlan.schema"; import { ZHasEditPermissionForUserSchema } from "./hasEditPermissionForUser.schema"; import { ZInviteMemberInputSchema } from "./inviteMember/inviteMember.schema"; import { ZInviteMemberByTokenSchemaInputSchema } from "./inviteMemberByToken.schema"; @@ -222,4 +223,11 @@ export const viewerTeamsRouter = router({ ); return handler(opts); }), + hasActiveTeamPlan: authedProcedure.input(ZHasActiveTeamPlanSchema).query(async (opts) => { + const handler = await importHandler( + namespaced("hasActiveTeamPlan"), + () => import("./hasActiveTeamPlan.handler") + ); + return handler(opts); + }), }); diff --git a/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts b/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts new file mode 100644 index 00000000000000..7020628a8f3158 --- /dev/null +++ b/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts @@ -0,0 +1,40 @@ +import { InternalTeamBilling } from "@calcom/ee/billing/teams/internal-team-billing"; +import { IS_SELF_HOSTED } from "@calcom/lib/constants"; +import { prisma } from "@calcom/prisma"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { THasActiveTeamPlanSchema } from "./hasActiveTeamPlan.schema"; + +type HasActiveTeamPlanOptions = { + ctx: { + user: NonNullable; + }; + input: THasActiveTeamPlanSchema; +}; + +export const hasActiveTeamPlanHandler = async ({ input, ctx }: HasActiveTeamPlanOptions) => { + if (IS_SELF_HOSTED) return true; + + if (!input.teamId) return false; + + const userId = ctx.user.id; + + // Check if the user is a member of the requested team + const team = await prisma.team.findFirst({ + where: { + id: input.teamId, + members: { + some: { + userId: userId, + }, + }, + }, + }); + if (!team) return false; + + // Get the current team's subscription + const teamBillingService = new InternalTeamBilling(team); + return await teamBillingService.checkIfTeamHasActivePlan(); +}; + +export default hasActiveTeamPlanHandler; diff --git a/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.schema.ts b/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.schema.ts new file mode 100644 index 00000000000000..90a3e1bee419ee --- /dev/null +++ b/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ZHasActiveTeamPlanSchema = z.object({ + teamId: z.number().optional(), +}); + +export type THasActiveTeamPlanSchema = z.infer; From 2107ad8dcf87e72450ca15f9d2bc0c55c27d818a Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Jan 2025 19:30:18 -0500 Subject: [PATCH 4/9] Check for active team plan when updating workflows --- .../trpc/server/routers/viewer/workflows/update.handler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts index f65c6af17c4101..94bb755e7215fa 100755 --- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts @@ -11,7 +11,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { TRPCError } from "@trpc/server"; -import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler"; +import { hasActiveTeamPlanHandler } from "../teams/hasActiveTeamPlan.handler"; import type { TUpdateInputSchema } from "./update.schema"; import { getSender, @@ -82,7 +82,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { let isTeamsPlan = false; if (!isCurrentUsernamePremium) { - const { hasTeamPlan } = await hasTeamPlanHandler({ ctx }); + const { hasTeamPlan } = await hasActiveTeamPlanHandler({ teamId: userWorkflow?.teamId }); isTeamsPlan = !!hasTeamPlan; } const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan; From f7b76eb0def701d2fb3787ce52bca34db95adb65 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Jan 2025 19:31:49 -0500 Subject: [PATCH 5/9] Create `useHasActiveTeamPlan` hook --- packages/lib/hooks/useHasPaidPlan.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/lib/hooks/useHasPaidPlan.ts b/packages/lib/hooks/useHasPaidPlan.ts index c940939b52ebc4..38eadea9d7cccc 100644 --- a/packages/lib/hooks/useHasPaidPlan.ts +++ b/packages/lib/hooks/useHasPaidPlan.ts @@ -39,4 +39,12 @@ export function useHasEnterprisePlan() { return { isPending, hasTeamPlan: hasTeamPlan?.hasTeamPlan }; } +export function useHasActiveTeamPlan(teamId?: number) { + if (IS_SELF_HOSTED) return { isPending: false, hasActiveTeamPlan: true }; + + const { data, isPending } = trpc.viewer.teams.hasActiveTeamPlan.useQuery({ teamId }); + + return { isPending, hasActiveTeamPlan: data }; +} + export default useHasPaidPlan; From 04639bc22856bd48d722ebc992aabc25cd12417e Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Jan 2025 19:32:23 -0500 Subject: [PATCH 6/9] Use `useHasActiveTeamPlan` on workflow edit page --- .../workflows/components/WorkflowStepContainer.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index f56ba81c5c942d..bac89220087c47 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -8,7 +8,7 @@ import "react-phone-number-input/style.css"; import { classNames } from "@calcom/lib"; import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants"; -import useHasPaidPlan from "@calcom/lib/hooks/useHasPaidPlan"; +import { useHasActiveTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; @@ -85,14 +85,14 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { const { t, i18n } = useLocale(); const utils = trpc.useUtils(); - const { hasPaidPlan } = useHasPaidPlan(); - const { step, form, reload, setReload, teamId } = props; const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery( { teamId }, { enabled: !!teamId } ); + const { hasActiveTeamPlan } = useHasActiveTeamPlan(teamId); + const { data: _verifiedEmails } = trpc.viewer.workflows.getVerifiedEmails.useQuery({ teamId }); const timeFormat = getTimeFormatStringFromUserTimeFormat(props.user.timeFormat); @@ -132,7 +132,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery(); const triggerOptions = getWorkflowTriggerOptions(t); - const templateOptions = getWorkflowTemplateOptions(t, step?.action, hasPaidPlan); + const templateOptions = getWorkflowTemplateOptions(t, step?.action, hasActiveTeamPlan); if (step && form.getValues(`steps.${step.stepNumber - 1}.template`) === WorkflowTemplates.REMINDER) { if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) { @@ -918,7 +918,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { refEmailSubject.current = e; }} rows={1} - disabled={props.readOnly || !hasPaidPlan} + disabled={props.readOnly || !hasActiveTeamPlan} className="my-0 focus:ring-transparent" required {...restEmailSubjectForm} @@ -951,7 +951,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { updateTemplate={updateTemplate} firstRender={firstRender} setFirstRender={setFirstRender} - editable={!props.readOnly && !isWhatsappAction(step.action) && hasPaidPlan} + editable={!props.readOnly && !isWhatsappAction(step.action) && hasActiveTeamPlan} excludedToolbarItems={ !isSMSAction(step.action) ? [] : ["blockType", "bold", "italic", "link"] } From 4d86e2cd280dee6c20f38b228643b0c38e2b6eb5 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Jan 2025 19:46:26 -0500 Subject: [PATCH 7/9] Type fixes --- .../server/routers/viewer/workflows/update.handler.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts index 94bb755e7215fa..d47da0c950e4ef 100755 --- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts @@ -11,7 +11,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { TRPCError } from "@trpc/server"; -import { hasActiveTeamPlanHandler } from "../teams/hasActiveTeamPlan.handler"; +import hasActiveTeamPlanHandler from "../teams/hasActiveTeamPlan.handler"; import type { TUpdateInputSchema } from "./update.schema"; import { getSender, @@ -82,8 +82,10 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { let isTeamsPlan = false; if (!isCurrentUsernamePremium) { - const { hasTeamPlan } = await hasActiveTeamPlanHandler({ teamId: userWorkflow?.teamId }); - isTeamsPlan = !!hasTeamPlan; + isTeamsPlan = await hasActiveTeamPlanHandler({ + ctx, + input: { teamId: userWorkflow?.teamId ?? undefined }, + }); } const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan; From 556958e6eb45632b39e23fcdf59fc4b0f97049df Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Jan 2025 19:58:55 -0500 Subject: [PATCH 8/9] Type fix --- packages/lib/hooks/useHasPaidPlan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/hooks/useHasPaidPlan.ts b/packages/lib/hooks/useHasPaidPlan.ts index 38eadea9d7cccc..eec39051f10b16 100644 --- a/packages/lib/hooks/useHasPaidPlan.ts +++ b/packages/lib/hooks/useHasPaidPlan.ts @@ -44,7 +44,7 @@ export function useHasActiveTeamPlan(teamId?: number) { const { data, isPending } = trpc.viewer.teams.hasActiveTeamPlan.useQuery({ teamId }); - return { isPending, hasActiveTeamPlan: data }; + return { isPending, hasActiveTeamPlan: !!data }; } export default useHasPaidPlan; From 3d80532e0cbf6e183d12a0a8faf31b9c07705fe9 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung Date: Mon, 20 Jan 2025 09:27:51 -0500 Subject: [PATCH 9/9] Add `accepted` to team query --- .../server/routers/viewer/teams/hasActiveTeamPlan.handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts b/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts index 7020628a8f3158..f7b2b0a9c8f8b3 100644 --- a/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts @@ -26,6 +26,7 @@ export const hasActiveTeamPlanHandler = async ({ input, ctx }: HasActiveTeamPlan members: { some: { userId: userId, + accepted: true, }, }, },