diff --git a/packages/features/ee/billing/billing-service.ts b/packages/features/ee/billing/billing-service.ts index dc6b29708e..63c785f843 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 495c490c13..ec31eeb14e 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"; + } } diff --git a/packages/features/ee/billing/teams/internal-team-billing.ts b/packages/features/ee/billing/teams/internal-team-billing.ts index 790d847461..89626432f4 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); + } } diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index f56ba81c5c..bac8922008 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"] } diff --git a/packages/lib/hooks/useHasPaidPlan.ts b/packages/lib/hooks/useHasPaidPlan.ts index c940939b52..eec39051f1 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; diff --git a/packages/trpc/server/routers/viewer/teams/_router.tsx b/packages/trpc/server/routers/viewer/teams/_router.tsx index ad12d435da..a9cffda85a 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 0000000000..f7b2b0a9c8 --- /dev/null +++ b/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts @@ -0,0 +1,41 @@ +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, + accepted: true, + }, + }, + }, + }); + 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 0000000000..90a3e1bee4 --- /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; diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts index f65c6af17c..d47da0c950 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,8 +82,10 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { let isTeamsPlan = false; if (!isCurrentUsernamePremium) { - const { hasTeamPlan } = await hasTeamPlanHandler({ ctx }); - isTeamsPlan = !!hasTeamPlan; + isTeamsPlan = await hasActiveTeamPlanHandler({ + ctx, + input: { teamId: userWorkflow?.teamId ?? undefined }, + }); } const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan;