Files
f4abbb2de1 feat: refactor billing to strategy implemention (#27828)
* factory and statergie

* chore: use correct method of DI

* feat: add onchagne

* add logic to HWM stat

* add webhook resolver methods to each statergy

* move seat tracking + webhooks over to own statergy

* Move to factory base approach

* move logic to correct class

* rename create -> createByTeamId

* fix: remove debug `true ||` overrides from IS_STRIPE_ENABLED and IS_TEAM_BILLING_ENABLED

Remove accidentally committed debug overrides that short-circuited
IS_STRIPE_ENABLED and IS_TEAM_BILLING_ENABLED to always be true,
bypassing Stripe credential checks. This would break self-hosted
instances without Stripe configured.

Identified by cubic (https://cubic.dev)

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 13:11:36 -03:00

166 lines
4.6 KiB
TypeScript

import { getTeamBillingServiceFactory } from "@calcom/ee/billing/di/containers/Billing";
import { SeatChangeTrackingService } from "@calcom/features/ee/billing/service/seatTracking/SeatChangeTrackingService";
import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry";
import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions";
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../types";
import type { TBulkUsersDelete } from "./bulkDeleteUsers.schema.";
type BulkDeleteUsersHandler = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TBulkUsersDelete;
};
export async function bulkDeleteUsersHandler({ ctx, input }: BulkDeleteUsersHandler) {
const currentUser = ctx.user;
const currentUserOrgId = currentUser.organizationId ?? currentUser.profiles[0].organizationId;
if (!currentUserOrgId) throw new TRPCError({ code: "UNAUTHORIZED" });
// Get user's membership role in the organization
const membership = await prisma.membership.findFirst({
where: {
userId: currentUser.id,
teamId: currentUserOrgId,
},
select: {
role: true,
},
});
if (!membership) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "User is not a member of this organization." });
}
// Check PBAC permissions for removing organization members
const permissions = await getSpecificPermissions({
userId: currentUser.id,
teamId: currentUserOrgId,
resource: Resource.Organization,
userRole: membership.role,
actions: [CustomAction.Remove],
fallbackRoles: {
[CustomAction.Remove]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
});
if (!permissions[CustomAction.Remove]) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const deleteOrganizationMemberships = prisma.membership.deleteMany({
where: {
teamId: currentUserOrgId,
userId: {
in: input.userIds,
},
},
});
const deleteSubteamMemberships = prisma.membership.deleteMany({
where: {
userId: {
in: input.userIds,
},
team: {
parentId: currentUserOrgId,
},
},
});
const removeOrgrelation = prisma.user.updateMany({
where: {
id: {
in: input.userIds,
},
},
data: {
organizationId: null,
// Set username to null - to make sure there is no conflicts
username: null,
// Set completedOnboarding to false - to make sure the user has to complete onboarding again -> Setup a new username
completedOnboarding: false,
},
});
const removeManagedEventTypes = prisma.eventType.deleteMany({
where: {
userId: {
in: input.userIds,
},
parent: {
team: {
OR: [
{
parentId: currentUserOrgId,
},
{ id: currentUserOrgId },
],
},
},
},
});
const removeHostAssignment = prisma.host.deleteMany({
where: {
userId: {
in: input.userIds,
},
eventType: {
team: {
OR: [
{
parentId: currentUserOrgId,
},
{ id: currentUserOrgId },
],
},
},
},
});
const removeProfiles = ProfileRepository.deleteMany({
userIds: input.userIds,
});
// We do this in a transaction to make sure that all memberships are removed before we remove the organization relation from the user
// We also do this to make sure that if one of the queries fail, the whole transaction fails
const [, { count: orgMembershipRemovalCount }] = await prisma.$transaction([
removeProfiles,
deleteOrganizationMemberships,
deleteSubteamMemberships,
removeOrgrelation,
removeManagedEventTypes,
removeHostAssignment,
]);
if (orgMembershipRemovalCount > 0) {
const seatTracker = new SeatChangeTrackingService();
await seatTracker.logSeatRemoval({
teamId: currentUserOrgId,
seatCount: orgMembershipRemovalCount,
triggeredBy: currentUser.id,
});
}
const teamBillingServiceFactory = getTeamBillingServiceFactory();
const teamBillingService = await teamBillingServiceFactory.findAndInit(currentUserOrgId);
await teamBillingService.updateQuantity("removal");
return {
success: true,
usersDeleted: input.userIds.length,
};
}
export default bulkDeleteUsersHandler;