feat: add multi subscription support (#734)

## Description

Previously we assumed that there can only be 1 subscription per user.
However, that will soon no longer the case with the introduction of the
Teams subscription.

This PR will apply the required migrations to support multiple
subscriptions.

## Changes Made

- Updated the Prisma schema to allow for multiple `Subscriptions` per
`User`
- Added a Stripe `customerId` field to the `User` model
- Updated relevant billing sections to support multiple subscriptions

## Testing Performed

- Tested running the Prisma migration on a demo database created on the
main branch

Will require a lot of additional testing.

## Checklist

- [ ] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [X] I have followed the project's coding style guidelines.

## Additional Notes

Added the following custom SQL statement to the migration:

> DELETE FROM "Subscription" WHERE "planId" IS NULL OR "priceId" IS
NULL;

Prior to deployment this will require changes to Stripe products:
- Adding `type` meta attribute

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
This commit is contained in:
David Nguyen
2023-12-14 15:22:54 +11:00
committed by GitHub
parent 6d34ebd91b
commit 88534fa1c6
28 changed files with 288 additions and 366 deletions

View File

@@ -1,10 +1,10 @@
import { DateTime } from 'luxon';
import { stripe } from '@documenso/lib/server-only/stripe';
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
import { getPricesByType } from '../stripe/get-prices-by-type';
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS } from './constants';
import { ERROR_CODES } from './errors';
import { ZLimitsSchema } from './schema';
@@ -43,23 +43,29 @@ export const getServerLimits = async ({ email }: GetServerLimitsOptions) => {
let quota = structuredClone(FREE_PLAN_LIMITS);
let remaining = structuredClone(FREE_PLAN_LIMITS);
// Since we store details and allow for past due plans we need to check if the subscription is active.
if (user.Subscription?.status !== SubscriptionStatus.INACTIVE && user.Subscription?.priceId) {
const { product } = await stripe.prices
.retrieve(user.Subscription.priceId, {
expand: ['product'],
})
.catch((err) => {
console.error(err);
throw err;
});
const activeSubscriptions = user.Subscription.filter(
({ status }) => status === SubscriptionStatus.ACTIVE,
);
if (typeof product === 'string') {
throw new Error(ERROR_CODES.SUBSCRIPTION_FETCH_FAILED);
if (activeSubscriptions.length > 0) {
const individualPrices = await getPricesByType('individual');
for (const subscription of activeSubscriptions) {
const price = individualPrices.find((price) => price.id === subscription.priceId);
if (!price || typeof price.product === 'string' || price.product.deleted) {
continue;
}
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);
}
}
quota = ZLimitsSchema.parse('metadata' in product ? product.metadata : {});
remaining = structuredClone(quota);
}
const documents = await prisma.document.count({