mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
3887aa67c8
Replace per-event webhook handlers with a single sync function that fetches the current state from Stripe and converges the local subscription, claim, and organisation type. - Create organisations upfront before checkout, restricted as "pending payment" until the first payment syncs - Add rate-limited subscription sync route, triggered on checkout success so the UI doesn't wait on webhooks - Surface pending payment state in banner, billing table, and limits
129 lines
3.5 KiB
TypeScript
129 lines
3.5 KiB
TypeScript
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
|
import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription';
|
|
import { isOrganisationPendingPayment } from '@documenso/lib/utils/billing';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { DocumentSource, EnvelopeType, SubscriptionStatus } from '@prisma/client';
|
|
import { DateTime } from 'luxon';
|
|
|
|
import { FREE_PLAN_LIMITS, INACTIVE_PLAN_LIMITS, PAID_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS } from './constants';
|
|
import { ERROR_CODES } from './errors';
|
|
import type { TLimitsResponseSchema } from './schema';
|
|
|
|
export type GetServerLimitsOptions = {
|
|
userId: number;
|
|
teamId: number;
|
|
};
|
|
|
|
export const getServerLimits = async ({ userId, teamId }: GetServerLimitsOptions): Promise<TLimitsResponseSchema> => {
|
|
const organisation = await prisma.organisation.findFirst({
|
|
where: {
|
|
teams: {
|
|
some: {
|
|
id: teamId,
|
|
},
|
|
},
|
|
members: {
|
|
some: {
|
|
userId,
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
subscription: true,
|
|
organisationClaim: true,
|
|
},
|
|
});
|
|
|
|
if (!organisation) {
|
|
throw new Error(ERROR_CODES.USER_FETCH_FAILED);
|
|
}
|
|
|
|
const quota = structuredClone(FREE_PLAN_LIMITS);
|
|
const remaining = structuredClone(FREE_PLAN_LIMITS);
|
|
|
|
const subscription = organisation.subscription;
|
|
const maximumEnvelopeItemCount = organisation.organisationClaim.envelopeItemCount;
|
|
|
|
if (!IS_BILLING_ENABLED()) {
|
|
return {
|
|
quota: SELFHOSTED_PLAN_LIMITS,
|
|
remaining: SELFHOSTED_PLAN_LIMITS,
|
|
maximumEnvelopeItemCount,
|
|
};
|
|
}
|
|
|
|
// Bypass all limits even if plan expired for ENTERPRISE.
|
|
if (organisation.organisationClaimId === INTERNAL_CLAIM_ID.ENTERPRISE) {
|
|
return {
|
|
quota: PAID_PLAN_LIMITS,
|
|
remaining: PAID_PLAN_LIMITS,
|
|
maximumEnvelopeItemCount,
|
|
};
|
|
}
|
|
|
|
// Early return for users with an expired subscription.
|
|
if (subscription && subscription.status === SubscriptionStatus.INACTIVE) {
|
|
return {
|
|
quota: INACTIVE_PLAN_LIMITS,
|
|
remaining: INACTIVE_PLAN_LIMITS,
|
|
maximumEnvelopeItemCount,
|
|
};
|
|
}
|
|
|
|
// Early return for organisations created ahead of a paid checkout that are still awaiting payment.
|
|
if (isOrganisationPendingPayment(organisation)) {
|
|
return {
|
|
quota: INACTIVE_PLAN_LIMITS,
|
|
remaining: INACTIVE_PLAN_LIMITS,
|
|
maximumEnvelopeItemCount,
|
|
};
|
|
}
|
|
|
|
// Allow unlimited documents for users with an unlimited documents claim.
|
|
// This also allows "free" claim users without subscriptions if they have this flag.
|
|
if (organisation.organisationClaim.flags.unlimitedDocuments) {
|
|
return {
|
|
quota: PAID_PLAN_LIMITS,
|
|
remaining: PAID_PLAN_LIMITS,
|
|
maximumEnvelopeItemCount,
|
|
};
|
|
}
|
|
|
|
const [documents, directTemplates] = await Promise.all([
|
|
prisma.envelope.count({
|
|
where: {
|
|
type: EnvelopeType.DOCUMENT,
|
|
team: {
|
|
organisationId: organisation.id,
|
|
},
|
|
createdAt: {
|
|
gte: DateTime.utc().startOf('month').toJSDate(),
|
|
},
|
|
source: {
|
|
not: DocumentSource.TEMPLATE_DIRECT_LINK,
|
|
},
|
|
},
|
|
}),
|
|
prisma.envelope.count({
|
|
where: {
|
|
type: EnvelopeType.TEMPLATE,
|
|
team: {
|
|
organisationId: organisation.id,
|
|
},
|
|
directLink: {
|
|
isNot: null,
|
|
},
|
|
},
|
|
}),
|
|
]);
|
|
|
|
remaining.documents = Math.max(remaining.documents - documents, 0);
|
|
remaining.directTemplates = Math.max(remaining.directTemplates - directTemplates, 0);
|
|
|
|
return {
|
|
quota,
|
|
remaining,
|
|
maximumEnvelopeItemCount,
|
|
};
|
|
};
|