diff --git a/apps/web/modules/settings/billing/billing-view.tsx b/apps/web/modules/settings/billing/billing-view.tsx index d1098b9027a768..82459fab4918c7 100644 --- a/apps/web/modules/settings/billing/billing-view.tsx +++ b/apps/web/modules/settings/billing/billing-view.tsx @@ -1,5 +1,6 @@ "use client"; +import { useSession } from "next-auth/react"; import { usePathname } from "next/navigation"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -42,9 +43,32 @@ export const CtaRow = ({ title, description, className, children }: CtaRowProps) const BillingView = () => { const pathname = usePathname(); + const session = useSession(); const { t } = useLocale(); const returnTo = pathname; - const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`; + + // Determine the billing context and extract appropriate team/org ID + const getTeamIdFromContext = () => { + if (!pathname) return null; + + // Team billing: /settings/teams/{id}/billing + if (pathname.includes("/teams/") && pathname.includes("/billing")) { + const teamIdMatch = pathname.match(/\/teams\/(\d+)\/billing/); + return teamIdMatch ? teamIdMatch[1] : null; + } + + // Organization billing: /settings/organizations/billing + if (pathname.includes("/organizations/billing")) { + const orgId = session.data?.user?.org?.id; + return typeof orgId === "number" ? orgId.toString() : null; + } + }; + + const teamId = getTeamIdFromContext(); + + const billingHref = teamId + ? `/api/integrations/stripepayment/portal?teamId=${teamId}&returnTo=${WEBAPP_URL}${returnTo}` + : `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`; const onContactSupportClick = async () => { if (window.Plain) { diff --git a/packages/app-store/stripepayment/api/portal.ts b/packages/app-store/stripepayment/api/portal.ts index 8c508bae51c4d4..fee06da72cd45c 100644 --- a/packages/app-store/stripepayment/api/portal.ts +++ b/packages/app-store/stripepayment/api/portal.ts @@ -2,22 +2,32 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import { TeamRepository } from "@calcom/lib/server/repository/team"; +import prisma from "@calcom/prisma"; +import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { getStripeCustomerIdFromUserId } from "../lib/customer"; import stripe from "../lib/server"; +import { getSubscriptionFromId } from "../lib/subscriptions"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== "POST" && req.method !== "GET") return res.status(405).json({ message: "Method not allowed" }); - // if (!referer) return res.status(400).json({ message: "Missing referrer" }); - if (!req.session?.user?.id) return res.status(401).json({ message: "Not authenticated" }); - // If accessing a user's portal - const customerId = await getStripeCustomerIdFromUserId(req.session.user.id); - if (!customerId) return res.status(400).json({ message: "CustomerId not found in stripe" }); + const userId = req.session.user.id; + const teamId = req.query.teamId ? parseInt(req.query.teamId as string) : null; + + if (!teamId) { + return res.status(400).json({ message: "Team ID is required" }); + } + const teamRepository = new TeamRepository(prisma); + const team = await teamRepository.getTeamByIdIfUserIsAdmin({ + teamId, + userId, + }); let return_url = `${WEBAPP_URL}/settings/billing`; if (typeof req.query.returnTo === "string") { @@ -25,6 +35,42 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (safeRedirectUrl) return_url = safeRedirectUrl; } + if (!team) { + const customerId = await getStripeCustomerIdFromUserId(userId); + if (!customerId) return res.status(404).json({ message: "CustomerId not found" }); + + const portalSession = await stripe.billingPortal.sessions.create({ + customer: customerId, + return_url, + }); + + return res.status(200).json({ url: portalSession.url }); + } + + const teamMetadataParsed = teamMetadataSchema.safeParse(team.metadata); + + if (!teamMetadataParsed.success) { + return res.status(400).json({ message: "Invalid team metadata" }); + } + + if (!teamMetadataParsed.data?.subscriptionId) { + return res.status(400).json({ message: "subscriptionId not found for team" }); + } + + const subscription = await getSubscriptionFromId(teamMetadataParsed.data.subscriptionId); + + if (!subscription) { + return res.status(400).json({ message: "Subscription not found" }); + } + + if (!subscription.customer) { + return res.status(400).json({ message: "Subscription customer not found" }); + } + + const customerId = subscription.customer as string; + + if (!customerId) return res.status(400).json({ message: "CustomerId not found in stripe" }); + const stripeSession = await stripe.billingPortal.sessions.create({ customer: customerId, return_url, diff --git a/packages/app-store/stripepayment/lib/subscriptions.ts b/packages/app-store/stripepayment/lib/subscriptions.ts index ed1bc184cfc836..669d257bf6f4ec 100644 --- a/packages/app-store/stripepayment/lib/subscriptions.ts +++ b/packages/app-store/stripepayment/lib/subscriptions.ts @@ -27,3 +27,7 @@ export async function retrieveSubscriptionIdFromStripeCustomerId( subscriptionId: subscription.id, }; } + +export async function getSubscriptionFromId(subscriptionId: string) { + return await stripe.subscriptions.retrieve(subscriptionId); +} diff --git a/packages/lib/server/repository/team.ts b/packages/lib/server/repository/team.ts index 984c5fed937fd5..72769f88311f34 100644 --- a/packages/lib/server/repository/team.ts +++ b/packages/lib/server/repository/team.ts @@ -5,6 +5,7 @@ import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizatio import logger from "@calcom/lib/logger"; import type { PrismaClient } from "@calcom/prisma"; import prisma from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { getParsedTeam } from "./teamUtils"; @@ -377,4 +378,24 @@ export class TeamRepository { }, }); } + + async getTeamByIdIfUserIsAdmin({ userId, teamId }: { userId: number; teamId: number }) { + return await this.prismaClient.team.findUnique({ + where: { + id: teamId, + }, + select: { + id: true, + metadata: true, + members: { + where: { + userId, + role: { + in: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }, + }, + }); + } }