Files
calendar/packages/trpc/server/routers/viewer/teams/getActiveUserBookings.handler.ts
T
sean-brydonGitHubunknown <>unknown <>sean@cal.com <Sean@brydon.io>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
d52e2d9fad feat: active user billing (#27867)
* 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 <>

* feat: active user billing

* add tests

* UI side for users on active billing

* use correct period of stripe sub

* feat: claude feedback

* fix: skip Stripe sync for canceled/expired subscriptions to prevent repeated API calls

Co-Authored-By: unknown <>

* feat: feedback

* feat: only render when active users mode is set

* fix type error

* fix: constants + feature flags

* fix: default to null in tests

* chore: use seats in test

* fix: use node:crypto protocol for Node.js builtin imports

Co-Authored-By: sean@cal.com <Sean@brydon.io>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 13:57:32 +00:00

128 lines
3.9 KiB
TypeScript

import { getActiveUserBillingService } from "@calcom/features/ee/billing/active-user/di/ActiveUserBillingService.container";
import { BillingPeriodService } from "@calcom/features/ee/billing/service/billingPeriod/BillingPeriodService";
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../types";
import type { TGetActiveUserBookingsInputSchema } from "./getActiveUserBookings.schema";
const log = logger.getSubLogger({ prefix: ["getActiveUserBookings"] });
type GetActiveUserBookingsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TGetActiveUserBookingsInputSchema;
};
export const getActiveUserBookingsHandler = async ({ ctx, input }: GetActiveUserBookingsOptions) => {
if (!IS_TEAM_BILLING_ENABLED) {
return null;
}
const { teamId, userId, activeAs } = input;
const membershipRepository = new MembershipRepository();
const membership = await membershipRepository.findUniqueByUserIdAndTeamId({
userId: ctx.user.id,
teamId,
});
if (!membership || !membership.accepted) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not a member of this team",
});
}
const team = await TeamService.fetchTeamOrThrow(teamId);
const permissionService = new PermissionCheckService();
const hasManageBillingPermission = await permissionService.checkPermission({
userId: ctx.user.id,
teamId,
permission: team.isOrganization ? "organization.manageBilling" : "team.manageBilling",
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
});
if (!hasManageBillingPermission) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Only team owners and admins can view active user bookings",
});
}
try {
const targetMembership = await membershipRepository.findUniqueByUserIdAndTeamId({
userId,
teamId,
});
if (!targetMembership || !targetMembership.accepted) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Target user is not a member of this team",
});
}
const targetUser = await prisma.user.findUnique({
where: { id: userId },
select: { email: true },
});
if (!targetUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Target user not found",
});
}
const billingPeriodService = new BillingPeriodService();
const billingInfo = await billingPeriodService.getOrCreateBillingPeriodInfo(teamId);
if (
billingInfo.billingMode !== "ACTIVE_USERS" ||
!billingInfo.subscriptionStart ||
!billingInfo.subscriptionEnd
) {
return null;
}
const activeUserBillingService = getActiveUserBillingService();
const bookings = await activeUserBillingService.getBookingsForUser(
userId,
targetUser.email,
activeAs,
billingInfo.subscriptionStart,
billingInfo.subscriptionEnd
);
return {
bookings: bookings.map((b) => ({
id: b.id,
uid: b.uid,
title: b.title,
startTime: b.startTime.toISOString(),
endTime: b.endTime.toISOString(),
otherParty: b.otherParty,
})),
};
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
log.error("Error getting active user bookings", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get active user bookings",
});
}
};
export default getActiveUserBookingsHandler;