mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
105 lines
3.1 KiB
TypeScript
105 lines
3.1 KiB
TypeScript
import type { OrganisationClaim, Subscription } from '@prisma/client';
|
|
import type Stripe from 'stripe';
|
|
|
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
|
import { appLog } from '@documenso/lib/utils/debugger';
|
|
import { prisma } from '@documenso/prisma';
|
|
|
|
import { isPriceSeatsBased } from './is-price-seats-based';
|
|
|
|
export type UpdateSubscriptionItemQuantityOptions = {
|
|
subscriptionId: string;
|
|
quantity: number;
|
|
priceId: string;
|
|
};
|
|
|
|
export const updateSubscriptionItemQuantity = async ({
|
|
subscriptionId,
|
|
quantity,
|
|
priceId,
|
|
}: UpdateSubscriptionItemQuantityOptions) => {
|
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
|
|
const items = subscription.items.data.filter((item) => item.price.id === priceId);
|
|
|
|
if (items.length !== 1) {
|
|
throw new Error('Subscription does not contain required item');
|
|
}
|
|
|
|
const hasYearlyItem = items.find((item) => item.price.recurring?.interval === 'year');
|
|
const oldQuantity = items[0].quantity;
|
|
|
|
if (oldQuantity === quantity) {
|
|
return;
|
|
}
|
|
|
|
const subscriptionUpdatePayload: Stripe.SubscriptionUpdateParams = {
|
|
items: items.map((item) => ({
|
|
id: item.id,
|
|
quantity,
|
|
})),
|
|
};
|
|
|
|
// Only invoice immediately when changing the quantity of yearly item.
|
|
if (hasYearlyItem) {
|
|
subscriptionUpdatePayload.proration_behavior = 'always_invoice';
|
|
}
|
|
|
|
await stripe.subscriptions.update(subscriptionId, subscriptionUpdatePayload);
|
|
};
|
|
|
|
/**
|
|
* Checks whether the member count should be synced with a given Stripe subscription.
|
|
*
|
|
* If the subscription is not "seat" based, it will be ignored.
|
|
*
|
|
* @param subscription - The subscription to sync the member count with.
|
|
* @param organisationClaim - The organisation claim
|
|
* @param quantity - The amount to sync the Stripe item with
|
|
* @returns
|
|
*/
|
|
export const syncMemberCountWithStripeSeatPlan = async (
|
|
subscription: Subscription,
|
|
organisationClaim: OrganisationClaim,
|
|
quantity: number,
|
|
) => {
|
|
const maximumMemberCount = organisationClaim.memberCount;
|
|
|
|
// Infinite seats means no sync needed.
|
|
if (maximumMemberCount === 0) {
|
|
return;
|
|
}
|
|
|
|
const syncMemberCountWithStripe = await isPriceSeatsBased(subscription.priceId);
|
|
|
|
// Throw error if quantity exceeds maximum member count and the subscription is not seats based.
|
|
if (quantity > maximumMemberCount && !syncMemberCountWithStripe) {
|
|
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
|
message: 'Maximum member count reached',
|
|
});
|
|
}
|
|
|
|
// Bill the user with the new quantity.
|
|
if (syncMemberCountWithStripe) {
|
|
appLog('BILLING', 'Updating seat based plan');
|
|
|
|
await updateSubscriptionItemQuantity({
|
|
priceId: subscription.priceId,
|
|
subscriptionId: subscription.planId,
|
|
quantity,
|
|
});
|
|
|
|
// This should be automatically updated after the Stripe webhook is fired
|
|
// but we just manually adjust it here as well to avoid any race conditions.
|
|
await prisma.organisationClaim.update({
|
|
where: {
|
|
id: organisationClaim.id,
|
|
},
|
|
data: {
|
|
memberCount: quantity,
|
|
},
|
|
});
|
|
}
|
|
};
|