Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add multi subscription support #734

Merged
merged 4 commits into from
Dec 14, 2023
Merged

Conversation

dguyen
Copy link
Collaborator

@dguyen dguyen commented Dec 6, 2023

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.
  • 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

Summary by CodeRabbit

  • New Features

    • Search functionality with a new search bar added to the app interface.
    • Pricing plans updated to reflect monthly options only.
    • Billing and subscription management logic updated to handle multiple subscriptions per user.
  • Bug Fixes

    • Fixed issues with subscription status updates in billing settings.
    • Corrected user statistics calculations to account for multiple subscriptions.
  • Documentation

    • Updated environment configuration examples to match current variables.
  • Refactor

    • Streamlined Stripe customer retrieval and subscription management processes.
    • Improved handling of Stripe webhook events for subscription updates and deletions.
  • Style

    • Enhanced styles for the new search bar component.
  • Chores

    • Database migrations to support multiple subscriptions per user.
    • Adjustments to TypeScript configurations following updated environment variables.
  • Revert

    • None reported.

Copy link
Contributor

coderabbitai bot commented Dec 6, 2023

Important

Auto Review Skipped

Auto reviews are limited to the following labels: coderabbit. Please add one of these labels to enable auto reviews.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository.

To trigger a single review, invoke the @coderabbitai review command.

Walkthrough

The changes reflect a shift in the application's billing and subscription management, particularly with Stripe integration. Yearly plans and free plans have been removed, and there's a focus on monthly pricing and handling multiple subscriptions per user. The codebase has been updated to accommodate these changes, including updates to environment variables, TypeScript declarations, and database schema adjustments.

Changes

File Path Change Summary
.env.example, .../process-env.d.ts, .../tsconfig/process-env.d.ts Removed yearly and free Stripe plan IDs.
.../marketing/src/.../pricing-table.tsx, .../web/src/app/.../admin/users/data-table-users.tsx Updated components to reflect billing changes and removed unused imports.
.../web/src/app/.../admin/users/page.tsx, .../web/src/app/.../settings/billing/page.tsx Added logic for handling individual price IDs and subscriptions.
.../web/src/app/.../settings/billing/... Modified import statements to type-only and updated Stripe customer handling.
packages/ee/server-only/..., packages/lib/server-only/... Introduced new server-side functions for Stripe and subscription management.
packages/prisma/... Database schema changes to support multiple subscriptions and unique customer IDs.

🐇✨
In the code, we hop and leap,
Stripe plans change, no yearly keep.
Monthly reigns, users cheer,
CodeRabbit's work, now crystal clear. 🌟🎩

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on X ?


Tips

Chat with CodeRabbit Bot (@coderabbitai)

  • If you reply to a review comment from CodeRabbit, the bot will automatically respond.
  • To engage with CodeRabbit bot directly around the specific lines of code in the PR, mention @coderabbitai in your review comment
  • Note: Review comments are made on code diffs or files, not on the PR overview.
  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Note: For conversation with the bot, please use the review comments on code diffs or files.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • The JSON schema for the configuration file is available here.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/coderabbit-overrides.v2.json

Copy link

vercel bot commented Dec 6, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
stg-app ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 11, 2023 1:04am
stg-marketing ❌ Failed (Inspect) Dec 11, 2023 1:04am
2 Ignored Deployments
Name Status Preview Comments Updated (UTC)
prd-app ⬜️ Ignored (Inspect) Visit Preview Dec 11, 2023 1:04am
prd-marketing ⬜️ Ignored (Inspect) Visit Preview Dec 11, 2023 1:04am

Comment on lines +24 to 32
if (foundSubscription) {
return getPortalSession({
customerId: stripeCustomer.id,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
});
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting we can actually give the user a Stripe session for that exact subscription if that's what we want.

Meaning if they go through the portal for a given price ID (somehow), then they can see the Stripe UI specifically set to that subscription.

Comment on lines +10 to +11
-- Custom migration statement
DELETE FROM "Subscription" WHERE "planId" IS NULL OR "priceId" IS NULL;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, this is custom.

Since planId and priceID should not actually be null anymore we need to drop all previous instances where it is null.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm

@@ -10,8 +10,6 @@ declare namespace NodeJS {
NEXT_PRIVATE_ENCRYPTION_KEY: string;

NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, this remains as it is still required in the marketing website.

@dguyen
Copy link
Collaborator Author

dguyen commented Dec 6, 2023

@coderabbitai review

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 5

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between b39a42e and 287dcce.
Files ignored due to filter (2)
  • render.yaml
  • turbo.json
Files selected for processing (22)
  • .env.example (1 hunks)
  • apps/marketing/process-env.d.ts (1 hunks)
  • apps/marketing/src/components/(marketing)/pricing-table.tsx (2 hunks)
  • apps/web/process-env.d.ts (1 hunks)
  • apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx (4 hunks)
  • apps/web/src/app/(dashboard)/admin/users/page.tsx (2 hunks)
  • apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts (1 hunks)
  • apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts (1 hunks)
  • apps/web/src/app/(dashboard)/settings/billing/page.tsx (2 hunks)
  • packages/ee/server-only/limits/server.ts (2 hunks)
  • packages/ee/server-only/stripe/get-customer.ts (2 hunks)
  • packages/ee/server-only/stripe/get-prices-by-interval.ts (3 hunks)
  • packages/ee/server-only/stripe/get-prices-by-type.ts (1 hunks)
  • packages/ee/server-only/stripe/webhook/on-subscription-deleted.ts (2 hunks)
  • packages/ee/server-only/stripe/webhook/on-subscription-updated.ts (2 hunks)
  • packages/lib/server-only/admin/get-users-stats.ts (1 hunks)
  • packages/lib/server-only/subscription/get-subscriptions-by-user-id.ts (1 hunks)
  • packages/lib/server-only/user/create-user.ts (4 hunks)
  • packages/prisma/migrations/20231206073509_add_multple_subscriptions/migration.sql (1 hunks)
  • packages/prisma/schema.prisma (3 hunks)
  • packages/tsconfig/process-env.d.ts (1 hunks)
Additional comments: 56
.env.example (1)
  • 77-86: The .env.example file now includes the NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID variable, which is not mentioned in the summary. Please ensure that the summary reflects all changes made to the file for accurate documentation.
apps/marketing/process-env.d.ts (1)
  • 6-11: The changes in the environment variables reflect the PR's objective to support multiple subscriptions per user and the shift away from single plan IDs. Ensure that the removal of NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID and NEXT_PUBLIC_STRIPE_FREE_PLAN_ID is reflected in the application logic where these variables were previously used.

Verification result:

The removal of NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID and NEXT_PUBLIC_STRIPE_FREE_PLAN_ID from the environment variables is consistent with the PR's objectives to support multiple subscriptions per user. The search for these variables in the codebase did not yield any results, indicating that they are not being used elsewhere and the removal should not cause any issues.

apps/marketing/src/components/(marketing)/pricing-table.tsx (3)
  • 3-4: The reorganization of import statements and the use of the type keyword for useState align with TypeScript best practices.

  • 21-21: The direct initialization of the useState hook for the period variable to 'MONTHLY' is consistent with the changes described in the summary.

  • 18-24: The modifications to the PricingTable function, including the removal of useSearchParams and the direct state initialization, are in line with the PR's objectives to support the new subscription model.

apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx (4)
  • 11-11: The import statement correctly uses the type keyword for type-only imports.

  • 19-24: The UserData type has been updated to support an array of SubscriptionLite, which aligns with the PR's objective.

  • 35-39: The UsersDataTableProps type has been extended to include individualPriceIds, which is necessary for the new subscription filtering logic.

  • 107-117: The logic within the 'Subscription' column's cell function has been updated to handle multiple subscriptions, which is consistent with the new feature requirements.

apps/web/src/app/(dashboard)/admin/users/page.tsx (3)
  • 1-1: The addition of the getPricesByType import aligns with the PR's objective to handle multiple subscriptions.

  • 19-24: The integration of getPricesByType within AdminManageUsers and passing its result to UsersDataTable is consistent with the PR's objectives.

  • 29-35: The update to UsersDataTable to accept individualPriceIds is a necessary adjustment for the new subscription model.

apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx (2)
  • 7-7: The change to import PriceIntervals as a type is consistent with TypeScript best practices for type-only imports and aligns with the PR's objectives to enhance type safety.

  • 4-10: No further issues found in the reviewed hunk.

apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts (2)
  • 3-13: The refactoring of the createBillingPortal function to use getStripeCustomerByUser simplifies the logic for Stripe customer management and aligns with the PR's objective to support multiple subscriptions per user.

  • 13-13: Ensure that the NEXT_PUBLIC_WEBAPP_URL environment variable is correctly set in the deployment environment to avoid any issues with the return URL after the billing portal session is created.


Verification result:

The review comment regarding the NEXT_PUBLIC_WEBAPP_URL environment variable cannot be verified because the .env.local file does not exist in the directory where the command was executed. This could mean that the environment variable is not set, or it might be set in a different environment file or at a different level (like global environment variables on the server or in a container orchestration system).

Please ensure that the NEXT_PUBLIC_WEBAPP_URL environment variable is correctly set in the appropriate configuration file or environment variable management system used in the deployment environment to avoid any issues with the return URL after the billing portal session is created.

apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts (2)
  • 3-33: The changes in the createCheckout function reflect the PR's objective to support multiple subscriptions per user. The use of getStripeCustomerByUser and getSubscriptionsByUserId aligns with the updated logic for handling Stripe customers and subscriptions. Ensure that the new functions are thoroughly tested, especially since they replace previous functions like getStripeCustomerById and getStripeCustomerByEmail.

  • 20-33: The logic to handle existing subscriptions and redirect to the billing portal or create a new checkout session appears to be correctly implemented. It's important to verify that the returnUrl is correctly configured and that the environment variable NEXT_PUBLIC_WEBAPP_URL is properly set in all deployment environments.

apps/web/src/app/(dashboard)/settings/billing/page.tsx (9)
  • 8-8: The summary does not mention the addition of the getProductByPriceId import from @documenso/ee/server-only/stripe/get-product-by-price-id, which is present in the hunk.

  • 6-6: The summary does not mention the addition of the getPricesByInterval import from @documenso/ee/server-only/stripe/get-prices-by-interval, which is present in the hunk.

  • 10-10: The summary does not mention the addition of the getServerComponentFlag import from @documenso/lib/server-only/feature-flags/get-server-component-feature-flag, which is present in the hunk.

  • 9-9: The summary does not mention the addition of the getRequiredServerComponentSession import from @documenso/lib/next-auth/get-server-component-session, which is present in the hunk.

  • 2-24: > Note: This review was outside the patches, and no patch overlapping with it was found. Original lines [1-1]

The summary does not mention the addition of the redirect import from next/navigation, which is present in the hunk.

  • 3-3: The summary does not mention the addition of the match import from ts-pattern, which is present in the hunk.

  • 17-18: The summary does not mention the addition of BillingPlans and BillingPortalButton components, which are present in the hunk.

  • 15-15: The summary does not mention the addition of the LocaleDate import from ~/components/formatter/locale-date, which is present in the hunk.

  • 27-32: The summary does not mention the logic for redirecting if billing is not enabled or if the user does not have a customerId, which is present in the hunk.

packages/ee/server-only/limits/server.ts (1)
  • 64-64: The logic for updating the quota and remaining values checks if both documents and recipients are higher in the current quota. Consider if this is the intended behavior, as it may not update the quota if only one of the values is higher.
packages/ee/server-only/stripe/get-customer.ts (3)
  • 1-12: The changes to import statements and function signatures are consistent with the PR objectives and the summary provided.

  • 32-81: The implementation of getStripeCustomerByUser appears to correctly handle the retrieval or creation of a Stripe customer based on the user's customerId. It also updates the user record with the new customerId if a new Stripe customer is created. Ensure that the error handling and subscription syncing logic are thoroughly tested, as these are critical operations.

  • 83-96: The syncStripeCustomerSubscriptions function correctly retrieves Stripe subscriptions and processes them concurrently using Promise.all. This approach is efficient, but ensure that the system can handle the potential load and that there is proper error handling for each subscription update.

packages/ee/server-only/stripe/get-prices-by-interval.ts (5)
  • 1-1: The import statement has been correctly updated to import Stripe as a type, aligning with TypeScript best practices for type-only imports.

  • 10-15: The introduction of GetPricesByIntervalOptions with an optional filterType property is a good practice for encapsulating function parameters, especially when dealing with options that may expand in the future.

  • 17-20: The function getPricesByInterval has been correctly updated to accept an options object, which is a good practice for functions that may need more parameters in the future or have optional parameters.

  • 29-32: The filtering logic based on the filterType parameter is implemented correctly, allowing the function to be more flexible and support the new feature of filtering by product type.

  • 26-27: The use of a type assertion for price.product is necessary due to the use of the expand option in Stripe's API call, which is not reflected in the base Price type. However, it's important to ensure that the expanded product is always present to avoid runtime errors.

packages/ee/server-only/stripe/webhook/on-subscription-deleted.ts (3)
  • 1-1: The import statement for the Stripe type has been correctly updated to use the type keyword, aligning with TypeScript best practices for type-only imports.

  • 9-15: The onSubscriptionDeleted function has been updated to use planId from the subscription object, which aligns with the PR objectives to handle multiple subscriptions per user and the shift to using planId for subscription identification.

  • 7-15: > Note: This review was outside the patches, so it was mapped to the patch with the greatest overlap. Original lines [1-15]

The changes in the provided hunks are consistent with the PR objectives and summaries, and there are no apparent issues with logic, security, performance, or maintainability.

packages/ee/server-only/stripe/webhook/on-subscription-updated.ts (4)
  • 3-3: The change to the import statement using the type keyword is correct and aligns with TypeScript best practices for importing types.

  • 26-26: Verify that planId is the correct and unique identifier for a subscription in the context of the application's logic and database schema.

  • 23-29: > Note: This review was outside the patches, so it was mapped to the patch with the greatest overlap. Original lines [28-37]

The use of subscription.id for planId in both create and update blocks of the upsert operation is consistent with the PR's objectives to handle multiple subscriptions.

  • 1-6: > Note: This review was outside the patches, and no patch overlapping with it was found. Original lines [10-11]

Ensure that the subscription.customer field is always present and correctly formatted to avoid potential issues when determining customerId.

packages/lib/server-only/admin/get-users-stats.ts (1)
  • 9-17: The logic change in getUsersWithSubscriptionsCount to count users with any active subscriptions aligns with the PR's objective to support multiple subscriptions per user.
packages/lib/server-only/subscription/get-subscriptions-by-user-id.ts (1)
  • 9-15: The implementation of getSubscriptionsByUserId aligns with the PR's objective to support multiple subscriptions per user and appears to be correctly fetching subscriptions based on the userId.
packages/prisma/migrations/20231206073509_add_multple_subscriptions/migration.sql (5)
  • 11-11: The custom SQL statement to delete Subscription records with null planId or priceId is in line with the PR objectives to clean up the data before applying new constraints.

  • 20-21: The alteration of the Subscription table to set planId and priceId as NOT NULL is consistent with the PR objectives to enforce data integrity for subscriptions.

  • 24-24: The addition of the customerId column to the User table aligns with the PR objectives to manage Stripe customer IDs.

  • 27-27: The creation of a unique index on the planId column of the Subscription table is consistent with the PR objectives to ensure unique subscriptions per plan.

  • 30-30: The creation of a unique index on the customerId column of the User table is consistent with the PR objectives to ensure unique Stripe customer IDs per user.

packages/prisma/schema.prisma (5)
  • 21-27: The addition of the customerId field to the User model with the @unique directive is consistent with the PR objectives to manage Stripe customer IDs.

  • 38-38: The change from a singular to a plural Subscription relation in the User model supports the objective of allowing multiple subscriptions per user.

  • 76-76: Verify that the planId field in the Subscription model being marked as @unique aligns with the intended business logic, as this enforces that each plan ID can only be associated with one subscription at a time.

  • 80-80: The removal of the @unique directive from the userId field in the Subscription model is consistent with the objective of supporting multiple subscriptions per user.

  • 79-79: The addition of the periodEnd field in the Subscription model is not mentioned in the summary. Ensure that this new field is integrated into the application logic where necessary.

packages/tsconfig/process-env.d.ts (1)
  • 10-15: The hunk provided does not show the changes mentioned in the summary regarding NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID and NEXT_PUBLIC_STRIPE_FREE_PLAN_ID. Ensure that the changes are correctly reflected in the file and that any code relying on these environment variables is updated accordingly.

packages/lib/server-only/user/create-user.ts Show resolved Hide resolved
packages/lib/server-only/user/create-user.ts Show resolved Hide resolved
Comment on lines +14 to +17
DROP INDEX "Subscription_customerId_key";

-- DropIndex
DROP INDEX "Subscription_userId_key";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 NOTE
Codebase verification is a beta feature.

The migration script drops indexes on customerId and userId from the Subscription table, and verification shows that these indexes are not recreated or modified later in the migration process. This change should be carefully reviewed to ensure it is intentional and does not negatively impact the database's performance or integrity.

Original review comment

The migration script drops indexes related to customerId and userId on the Subscription table. This change was not mentioned in the summary and should be verified to ensure it aligns with the intended database schema changes.

.env.example Show resolved Hide resolved

export type CreateCheckoutOptions = {
priceId: string;
};

export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
const { user } = await getRequiredServerComponentSession();
const session = await getRequiredServerComponentSession();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can just have const { user } = await ... ?

apps/web/src/app/(dashboard)/settings/billing/page.tsx Outdated Show resolved Hide resolved
apps/web/src/app/(dashboard)/settings/billing/page.tsx Outdated Show resolved Hide resolved
@Mythie
Copy link
Collaborator

Mythie commented Dec 8, 2023

I'll trust ya on the rest, reading this is making me tired

@dguyen
Copy link
Collaborator Author

dguyen commented Dec 8, 2023

It's pretty critical since it changes billing a lot so could you please do a mini migration test as well to confirm?

dguyen and others added 2 commits December 8, 2023 16:37
Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
planId String?
priceId String?
planId String @unique
priceId String
customerId String
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be tossing the customerId here since it's on the user?

Copy link
Collaborator Author

@dguyen dguyen Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, I've removed it but it required some changes to the webhook handlers

Also noticed that we aren't actually updating cancelAtPeriodEnd when a subscription changes so I've included those changes

7053c95

Copy link

github-actions bot commented Dec 11, 2023

Thank you for following the naming conventions for pull request titles! 💚🚀

Copy link
Collaborator

@Mythie Mythie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

cat-button-explosion

@dguyen dguyen merged commit 88534fa into main Dec 14, 2023
12 of 13 checks passed
@dguyen dguyen deleted the feat/multi-subscription-support branch December 14, 2023 04:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
apps: marketing Issues related to website or marketing app apps: web Issues related to the webapp 🚨 .env changes 🚨 🚨 migrations 🚨 pkg: ee changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants