Files
documenso/packages/ee/server-only/stripe/transfer-team-subscription.ts
David Nguyen 0c339b78b6 feat: add teams (#848)
## Description

Add support for teams which will allow users to collaborate on
documents.

Teams features allows users to:

- Create, manage and transfer teams
- Manage team members
- Manage team emails
- Manage a shared team inbox and documents

These changes do NOT include the following, which are planned for a
future release:

- Team templates
- Team API
- Search menu integration

## Testing Performed

- Added E2E tests for general team management
- Added E2E tests to validate document counts

## Checklist

- [X] I have tested these changes locally and they work as expected.
- [X] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [X] I have followed the project's coding style guidelines.
2024-02-06 16:16:10 +11:00

127 lines
3.6 KiB
TypeScript

import type Stripe from 'stripe';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { stripe } from '@documenso/lib/server-only/stripe';
import { subscriptionsContainsActiveCommunityPlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import { type Subscription, type Team, type User } from '@documenso/prisma/client';
import { deleteCustomerPaymentMethods } from './delete-customer-payment-methods';
import { getCommunityPlanPriceIds } from './get-community-plan-prices';
import { getTeamPrices } from './get-team-prices';
type TransferStripeSubscriptionOptions = {
/**
* The user to transfer the subscription to.
*/
user: User & { Subscription: Subscription[] };
/**
* The team the subscription is associated with.
*/
team: Team & { subscription?: Subscription | null };
/**
* Whether to clear any current payment methods attached to the team.
*/
clearPaymentMethods: boolean;
};
/**
* Transfer the Stripe Team seats subscription from one user to another.
*
* Will create a new subscription for the new owner and cancel the old one.
*
* Returns the subscription that should be associated with the team, null if
* no subscription is needed (for community plan).
*/
export const transferTeamSubscription = async ({
user,
team,
clearPaymentMethods,
}: TransferStripeSubscriptionOptions) => {
const teamCustomerId = team.customerId;
if (!teamCustomerId) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Missing customer ID.');
}
const [communityPlanIds, teamSeatPrices] = await Promise.all([
getCommunityPlanPriceIds(),
getTeamPrices(),
]);
const teamSubscriptionRequired = !subscriptionsContainsActiveCommunityPlan(
user.Subscription,
communityPlanIds,
);
let teamSubscription: Stripe.Subscription | null = null;
if (team.subscription) {
teamSubscription = await stripe.subscriptions.retrieve(team.subscription.planId);
if (!teamSubscription) {
throw new Error('Could not find the current subscription.');
}
if (clearPaymentMethods) {
await deleteCustomerPaymentMethods({ customerId: teamCustomerId });
}
}
await stripe.customers.update(teamCustomerId, {
name: user.name ?? team.name,
email: user.email,
});
// If team subscription is required and the team does not have a subscription, create one.
if (teamSubscriptionRequired && !teamSubscription) {
const numberOfSeats = await prisma.teamMember.count({
where: {
teamId: team.id,
},
});
const teamSeatPriceId = teamSeatPrices.monthly.priceId;
teamSubscription = await stripe.subscriptions.create({
customer: teamCustomerId,
items: [
{
price: teamSeatPriceId,
quantity: numberOfSeats,
},
],
metadata: {
teamId: team.id.toString(),
},
});
}
// If no team subscription is required, cancel the current team subscription if it exists.
if (!teamSubscriptionRequired && teamSubscription) {
try {
// Set the quantity to 0 so we can refund/charge the old Stripe customer the prorated amount.
await stripe.subscriptions.update(teamSubscription.id, {
items: teamSubscription.items.data.map((item) => ({
id: item.id,
quantity: 0,
})),
});
await stripe.subscriptions.cancel(teamSubscription.id, {
invoice_now: true,
prorate: false,
});
} catch (e) {
// Do not error out since we can't easily undo the transfer.
// Todo: Teams - Alert us.
}
return null;
}
return teamSubscription;
};