Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/app/api/support/conversation/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { BillingPlanService } from "@calcom/features/ee/billing/billing-plan-service";
import type { Contact } from "@calcom/features/ee/support/lib/intercom/intercom";
import { intercom } from "@calcom/features/ee/support/lib/intercom/intercom";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
Expand Down Expand Up @@ -43,6 +44,7 @@ export async function POST(req: NextRequest) {

const { user } = session;

const plan = await BillingPlanService.getUserPlanByUserId(user.id);
if (!existingContact.data) {
const additionalUserInfo = await new UserRepository(prisma).getUserStats({ userId: session.user.id });
const sumOfTeamEventTypes = additionalUserInfo?.teams.reduce(
Expand Down Expand Up @@ -72,6 +74,7 @@ export async function POST(req: NextRequest) {
sum_of_teams: additionalUserInfo?._count?.teams,
sum_of_event_types: additionalUserInfo?._count?.eventTypes,
sum_of_team_event_types: sumOfTeamEventTypes,
Plan: plan,
},
});

Expand Down
46 changes: 46 additions & 0 deletions packages/features/ee/billing/billing-plan-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { BillingPlans, ENTERPRISE_SLUGS, PLATFORM_ENTERPRISE_SLUGS } from "@calcom/ee/billing/constants";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be tests added around this logic to verify its correctness

import { MembershipRepository } from "@calcom/lib/server/repository/membership";

export class BillingPlanService {
static async getUserPlanByUserId(userId: number) {
const memberships = await MembershipRepository.findAllMembershipsByUserIdForBilling({ userId });

if (memberships.length === 0) return BillingPlans.INDIVIDUALS;

for (const { team, user } of memberships) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this logic to a different class that takes in the data it uses. It should live in a folder inside of /billing called /domain. That way you can easily write unit tests for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service is ok to keep and have it read data from the repository, but then it should pass the data to a domain-level class/function for the logic-only piece

if (team.isPlatform || user.isPlatformManaged) {
if (PLATFORM_ENTERPRISE_SLUGS.includes(team.slug ?? "")) return BillingPlans.PLATFORM_ENTERPRISE;
if (!team.platformBilling) continue;

switch (team.platformBilling.plan) {
case "FREE":
case "STARTER":
return BillingPlans.PLATFORM_STARTER;
case "ESSENTIALS":
return BillingPlans.PLATFORM_ESSENTIALS;
case "SCALE":
return BillingPlans.PLATFORM_SCALE;
case "ENTERPRISE":
return BillingPlans.PLATFORM_ENTERPRISE;
default:
return team.platformBilling.plan;
}
} else {
if (team.parent && team.parent.isOrganization && team.parentId && !team.parent.isPlatform) {
return ENTERPRISE_SLUGS.includes(team.parent.slug ?? "")
? BillingPlans.ENTERPRISE
: BillingPlans.ORGANIZATIONS;
}

if (team.isOrganization) {
return ENTERPRISE_SLUGS.includes(team.slug ?? "")
? BillingPlans.ENTERPRISE
: BillingPlans.ORGANIZATIONS;
} else {
return BillingPlans.TEAMS;
}
}
}
return BillingPlans.UNKNOWN;
}
}
12 changes: 12 additions & 0 deletions packages/features/ee/billing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,15 @@ export const CHECKOUT_SESSION_TYPES = {
} as const;

export type CheckoutSessionType = (typeof CHECKOUT_SESSION_TYPES)[keyof typeof CHECKOUT_SESSION_TYPES];

export enum BillingPlans {
INDIVIDUALS = "INDIVIDUALS",
TEAMS = "TEAMS",
ORGANIZATIONS = "ORGANIZATIONS",
ENTERPRISE = "ENTERPRISE",
PLATFORM_STARTER = "PLATFORM_STARTER",
PLATFORM_ESSENTIALS = "PLATFORM_ESSENTIALS",
PLATFORM_SCALE = "PLATFORM_SCALE",
PLATFORM_ENTERPRISE = "PLATFORM_ENTERPRISE",
UNKNOWN = "Unknown",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are mapping needed for the workflows that is created inside Intercom. based on user plan they are adding tags and routing them

}
4 changes: 3 additions & 1 deletion packages/features/ee/support/lib/intercom/useIntercom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const useIntercom = () => {
},
},
});
const { hasPaidPlan } = useHasPaidPlan();
const { hasPaidPlan, plan } = useHasPaidPlan();
const { hasTeamPlan } = useHasTeamPlan();

const boot = async () => {
Expand Down Expand Up @@ -83,6 +83,7 @@ export const useIntercom = () => {
sum_of_event_types: statsData?.sumOfEventTypes,
sum_of_team_event_types: statsData?.sumOfTeamEventTypes,
is_premium: data?.isPremium,
Plan: plan,
},
});
};
Expand Down Expand Up @@ -127,6 +128,7 @@ export const useIntercom = () => {
sum_of_event_types: statsData?.sumOfEventTypes,
sum_of_team_event_types: statsData?.sumOfTeamEventTypes,
is_premium: data?.isPremium,
Plan: plan,
},
});
hookData.show();
Expand Down
6 changes: 3 additions & 3 deletions packages/lib/hooks/useHasPaidPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import hasKeyInMetadata from "../hasKeyInMetadata";
export function useHasPaidPlan() {
if (IS_SELF_HOSTED) return { isPending: false, hasPaidPlan: true };

const { data: hasTeamPlan, isPending: isPendingTeamQuery } = trpc.viewer.teams.hasTeamPlan.useQuery();
const { data, isPending: isPendingTeamQuery } = trpc.viewer.teams.hasTeamPlan.useQuery();

const { data: user, isPending: isPendingUserQuery } = trpc.viewer.me.get.useQuery();

Expand All @@ -15,9 +15,9 @@ export function useHasPaidPlan() {
const isCurrentUsernamePremium =
user && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;

const hasPaidPlan = hasTeamPlan?.hasTeamPlan || isCurrentUsernamePremium;
const hasPaidPlan = data?.hasTeamPlan || isCurrentUsernamePremium;

return { isPending, hasPaidPlan };
return { isPending, hasPaidPlan, plan: data?.plan };
}

export function useTeamInvites() {
Expand Down
39 changes: 37 additions & 2 deletions packages/lib/server/repository/membership.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

import { availabilityUserSelect, prisma, type PrismaTransaction } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { Prisma, Membership, PrismaClient } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";

import logger from "../../logger";
Expand Down Expand Up @@ -315,6 +314,42 @@ export class MembershipRepository {
});
}

static async findAllMembershipsByUserIdForBilling({ userId }: { userId: number }) {
return await prisma.membership.findMany({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to be static and no need the await here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

where: { userId },
select: {
user: {
select: {
isPlatformManaged: true,
},
},
team: {
select: {
id: true,
slug: true,
isOrganization: true,
isPlatform: true,
parentId: true,
metadata: true,
platformBilling: {
select: {
plan: true,
},
},
parent: {
select: {
slug: true,
isOrganization: true,
isPlatform: true,
metadata: true,
},
},
},
},
},
});
}

static async findByTeamIdForAvailability({ teamId }: { teamId: number }) {
const memberships = await prisma.membership.findMany({
where: { teamId },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BillingPlanService } from "@calcom/features/ee/billing/billing-plan-service";
import { MembershipRepository } from "@calcom/lib/server/repository/membership";

type HasTeamPlanOptions = {
Expand All @@ -10,8 +11,9 @@ export const hasTeamPlanHandler = async ({ ctx }: HasTeamPlanOptions) => {
const userId = ctx.user.id;

const hasTeamPlan = await MembershipRepository.findFirstAcceptedMembershipByUserId(userId);
const plan = await BillingPlanService.getUserPlanByUserId(userId);

return { hasTeamPlan: !!hasTeamPlan };
return { hasTeamPlan: !!hasTeamPlan, plan };
};

export default hasTeamPlanHandler;
Loading