Files
sign/packages/ee/server-only/limits/server.ts

167 lines
4.0 KiB
TypeScript
Raw Normal View History

2023-10-15 20:26:32 +11:00
import { DateTime } from 'luxon';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
2023-10-15 20:26:32 +11:00
import { prisma } from '@documenso/prisma';
import { DocumentSource, SubscriptionStatus } from '@documenso/prisma/client';
2023-10-15 20:26:32 +11:00
import { getDocumentRelatedPrices } from '../stripe/get-document-related-prices.ts';
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS, TEAM_PLAN_LIMITS } from './constants';
2023-10-15 20:26:32 +11:00
import { ERROR_CODES } from './errors';
import type { TLimitsResponseSchema } from './schema';
2023-10-15 20:26:32 +11:00
import { ZLimitsSchema } from './schema';
export type GetServerLimitsOptions = {
2025-02-16 00:44:01 +11:00
email: string;
teamId?: number | null;
2023-10-15 20:26:32 +11:00
};
export const getServerLimits = async ({
email,
teamId,
}: GetServerLimitsOptions): Promise<TLimitsResponseSchema> => {
2024-02-12 01:29:22 +00:00
if (!IS_BILLING_ENABLED()) {
2023-10-15 20:26:32 +11:00
return {
quota: SELFHOSTED_PLAN_LIMITS,
remaining: SELFHOSTED_PLAN_LIMITS,
};
}
if (!email) {
throw new Error(ERROR_CODES.UNAUTHORIZED);
}
return teamId ? handleTeamLimits({ email, teamId }) : handleUserLimits({ email });
};
type HandleUserLimitsOptions = {
email: string;
};
const handleUserLimits = async ({ email }: HandleUserLimitsOptions) => {
2023-10-15 20:26:32 +11:00
const user = await prisma.user.findFirst({
where: {
email,
},
include: {
2025-01-13 13:41:53 +11:00
subscriptions: true,
2023-10-15 20:26:32 +11:00
},
});
if (!user) {
throw new Error(ERROR_CODES.USER_FETCH_FAILED);
}
let quota = structuredClone(FREE_PLAN_LIMITS);
let remaining = structuredClone(FREE_PLAN_LIMITS);
2025-01-13 13:41:53 +11:00
const activeSubscriptions = user.subscriptions.filter(
({ status }) => status === SubscriptionStatus.ACTIVE,
);
if (activeSubscriptions.length > 0) {
const documentPlanPrices = await getDocumentRelatedPrices();
for (const subscription of activeSubscriptions) {
const price = documentPlanPrices.find((price) => price.id === subscription.priceId);
if (!price || typeof price.product === 'string' || price.product.deleted) {
continue;
}
2023-10-15 20:26:32 +11:00
const currentQuota = ZLimitsSchema.parse(
'metadata' in price.product ? price.product.metadata : {},
);
// Use the subscription with the highest quota.
if (currentQuota.documents > quota.documents && currentQuota.recipients > quota.recipients) {
quota = currentQuota;
remaining = structuredClone(quota);
}
}
// Assume all active subscriptions provide unlimited direct templates.
remaining.directTemplates = Infinity;
2023-10-15 20:26:32 +11:00
}
const [documents, directTemplates] = await Promise.all([
prisma.document.count({
where: {
userId: user.id,
teamId: null,
createdAt: {
gte: DateTime.utc().startOf('month').toJSDate(),
},
source: {
not: DocumentSource.TEMPLATE_DIRECT_LINK,
},
2023-10-15 20:26:32 +11:00
},
}),
prisma.template.count({
where: {
userId: user.id,
teamId: null,
directLink: {
isNot: null,
},
},
}),
]);
2023-10-15 20:26:32 +11:00
remaining.documents = Math.max(remaining.documents - documents, 0);
remaining.directTemplates = Math.max(remaining.directTemplates - directTemplates, 0);
2023-10-15 20:26:32 +11:00
return {
quota,
remaining,
};
};
type HandleTeamLimitsOptions = {
email: string;
teamId: number;
};
const handleTeamLimits = async ({ email, teamId }: HandleTeamLimitsOptions) => {
const team = await prisma.team.findFirst({
where: {
id: teamId,
members: {
some: {
user: {
email,
},
},
},
},
include: {
subscription: true,
},
});
if (!team) {
throw new Error('Team not found');
}
const { subscription } = team;
if (subscription && subscription.status === SubscriptionStatus.INACTIVE) {
return {
quota: {
documents: 0,
recipients: 0,
directTemplates: 0,
},
remaining: {
documents: 0,
recipients: 0,
directTemplates: 0,
},
};
}
return {
quota: structuredClone(TEAM_PLAN_LIMITS),
remaining: structuredClone(TEAM_PLAN_LIMITS),
};
};