Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Aug 27, 2025

Summary by CodeRabbit

  • New Features

    • When a cancelled subscription coexists with an active one, the workspace now adopts the active subscription’s plan automatically.
  • Bug Fixes

    • Prevent unintended downgrades when cancellations occur alongside active subscriptions.
    • Payment-failure status is cleared once billing recovers.
  • Refactor

    • Centralized plan-update flow so plan changes consistently update usage limits, payouts, token limits, webhook availability, and folder access.

…s. Introduced updateWorkspacePlan utility to streamline plan updates and added logic to check for active subscriptions upon deletion. Cleaned up unused code in customerSubscriptionUpdated.
@vercel
Copy link
Contributor

vercel bot commented Aug 27, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Aug 28, 2025 9:50am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 27, 2025

Walkthrough

Centralizes workspace plan transition logic into a new updateWorkspacePlan helper. customer-subscription-updated delegates plan updates to that helper. customer-subscription-deleted now checks Stripe for any remaining active subscriptions; if found, it maps the priceId to a plan, calls updateWorkspacePlan, and short-circuits the cancellation flow. (48 words)

Changes

Cohort / File(s) Summary
Subscription webhook handlers
apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts, apps/web/app/(ee)/api/stripe/webhook/customer-subscription-updated.ts
customer-subscription-deleted.ts: adds Stripe active-subscription check, maps priceId via getPlanFromPriceId, and calls updateWorkspacePlan to short-circuit cancellation when an active subscription remains. customer-subscription-updated.ts: replaces inline multi-step plan-update logic with a call to updateWorkspacePlan, removing redundant imports and local side-effect handling.
Webhook utils
apps/web/app/(ee)/api/stripe/webhook/utils.ts
Adds and exports updateWorkspacePlan(...): validates plan, computes flags (disable webhooks, delete folders), updates project limits/plan (respecting legacy price IDs for payouts), updates restricted tokens and token cache, disables webhooks and refreshes webhook cache when needed, deletes folders for free moves, and clears payment-failure state. Uses parallel updates and Promise.allSettled.
Shared utils / imports
@dub/utils, apps/web/app/(ee)/api/stripe/webhook/*
New exports declared: getPlanFromPriceId(priceId: string) in @dub/utils; updateWorkspacePlan(...) in webhook utils. Endpoints import these to centralize plan mapping and workspace updates.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Stripe
  participant WebhookDeleted as "Webhook: customer.subscription.deleted"
  participant StripeAPI as "Stripe API"
  participant Utils as "updateWorkspacePlan"
  participant DB as "DB / Caches"

  Stripe->>WebhookDeleted: customer.subscription.deleted
  WebhookDeleted->>StripeAPI: subscriptions.list(customer, status: "active")
  StripeAPI-->>WebhookDeleted: activeSubscriptions[]
  alt Active subscriptions exist
    WebhookDeleted->>Utils: updateWorkspacePlan({ workspace, plan, priceId })
    Utils->>DB: update project plan & limits, update tokens, disable webhooks, delete folders (async)
    DB-->>Utils: results
    Utils-->>WebhookDeleted: done
    WebhookDeleted-->>Stripe: 200 OK
  else No active subscriptions
    WebhookDeleted->>WebhookDeleted: continue cancellation flow
    WebhookDeleted-->>Stripe: 200 OK
  end
Loading
sequenceDiagram
  autonumber
  actor Stripe
  participant WebhookUpdated as "Webhook: customer.subscription.updated"
  participant Utils as "updateWorkspacePlan"
  participant DB as "DB / Caches"

  Stripe->>WebhookUpdated: customer.subscription.updated
  WebhookUpdated->>WebhookUpdated: validate workspace & priceId -> get plan
  WebhookUpdated->>Utils: updateWorkspacePlan({ workspace, plan, priceId })
  Utils->>DB: apply project updates, token updates, webhook state, folder deletes (async)
  DB-->>Utils: results
  Utils-->>WebhookUpdated: done
  WebhookUpdated-->>Stripe: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • steven-tey

Poem

I hop through webhooks with a carrot of code,
Mapping price-IDs down the tidy road.
If subs still hum, I switch the plan,
Clear caches, tuck folders — neat as I can.
Binky, ship, and watch the workspace glow. 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch check-other-subscriptions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • 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/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/web/app/(ee)/api/stripe/webhook/customer-subscription-updated.ts (1)

65-69: LGTM, plus one defensive improvement suggestion.
The delegation is correct. Consider guarding priceId extraction to avoid undefined when subscription has multiple items/add-ons. Example (elsewhere in this file where priceId is read): prefer a recurring item and bail if none is found.

const priceId =
  subscriptionUpdated.items.data.find((i) => i.price?.type === "recurring")
    ?.price?.id;
if (!priceId) {
  console.log(`No recurring price on subscription ${subscriptionUpdated.id}`);
  return;
}
apps/web/app/(ee)/api/stripe/webhook/utils.ts (2)

80-116: Avoid allSettled for core DB mutations to prevent partial state.
project.update and restrictedToken.updateMany should succeed or fail together; cache expiry can remain best-effort.

-    await Promise.allSettled([
-      prisma.project.update({
+    await Promise.all([
+      prisma.project.update({
         where: {
           id: workspace.id,
         },
         data: {
           plan: newPlanName,
           usageLimit: plan.limits.clicks!,
           linksLimit: plan.limits.links!,
           payoutsLimit: plan.limits.payouts!,
           domainsLimit: plan.limits.domains!,
           aiLimit: plan.limits.ai!,
           tagsLimit: plan.limits.tags!,
           foldersLimit: plan.limits.folders!,
           groupsLimit: plan.limits.groups!,
           usersLimit: plan.limits.users!,
           paymentFailedAt: null,
           ...(shouldDeleteFolders && { foldersUsage: 0 }),
         },
       }),
-
-      prisma.restrictedToken.updateMany({
+      prisma.restrictedToken.updateMany({
         where: {
           projectId: workspace.id,
         },
         data: {
           rateLimit: plan.limits.api,
         },
       }),
-
-      // expire tokens cache
-      tokenCache.expireMany({
-        hashedKeys: workspace.restrictedTokens.map(
-          ({ hashedKey }) => hashedKey,
-        ),
-      }),
-    ]);
+    ]);
+    // expire tokens cache (best-effort)
+    await tokenCache.expireMany({
+      hashedKeys: workspace.restrictedTokens.map(({ hashedKey }) => hashedKey),
+    });

69-70: Derive webhook capability from plan metadata instead of name, if possible.
If available, prefer a plan capability flag (e.g., plan.features.webhooks) over string matching "free"/"pro" to future-proof.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e1dbc66 and 7d1d3b5.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts (3 hunks)
  • apps/web/app/(ee)/api/stripe/webhook/customer-subscription-updated.ts (2 hunks)
  • apps/web/app/(ee)/api/stripe/webhook/utils.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts (2)
packages/utils/src/constants/pricing.tsx (1)
  • getPlanFromPriceId (352-354)
apps/web/app/(ee)/api/stripe/webhook/utils.ts (1)
  • updateWorkspacePlan (47-174)
apps/web/app/(ee)/api/stripe/webhook/customer-subscription-updated.ts (1)
apps/web/app/(ee)/api/stripe/webhook/utils.ts (1)
  • updateWorkspacePlan (47-174)
apps/web/app/(ee)/api/stripe/webhook/utils.ts (3)
apps/web/lib/types.ts (1)
  • WorkspaceProps (161-177)
packages/utils/src/constants/pricing.tsx (2)
  • getPlanFromPriceId (352-354)
  • LEGACY_PRICE_IDS (32-35)
apps/web/lib/auth/token-cache.ts (1)
  • tokenCache (68-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
🔇 Additional comments (4)
apps/web/app/(ee)/api/stripe/webhook/customer-subscription-updated.ts (2)

2-2: Centralizing plan derivation is correct.
Importing getPlanFromPriceId here aligns with the new shared helper flow.


5-5: Good: delegate side-effects to a single helper.
Moving plan updates to updateWorkspacePlan reduces duplication and drift.

apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts (2)

28-32: Selecting plan, payoutsLimit, and paymentFailedAt is necessary for the new helper.
This matches updateWorkspacePlan’s input shape.


77-96: Confirm valid Stripe subscription statuses
According to Stripe’s official documentation, Subscription.status can be one of:

  • incomplete
  • incomplete_expired
  • trialing
  • active
  • past_due
  • canceled
  • unpaid
  • paused (docs.stripe.com)

Of these, only the following represent subscriptions you can continue to provision (keep the workspace active):

  • trialing
  • active
  • past_due

All other statuses either never become fully active ( incomplete, incomplete_expired ), or require you to revoke access ( canceled, unpaid ), or represent a paused state ( paused ) that should be handled separately (docs.stripe.com).

Please update the filter accordingly:

-  const validStatuses = new Set<Stripe.Subscription.Status>([
-    "active",
-    "trialing",
-    "past_due",
-  ]);
+  const validStatuses = new Set<Stripe.Subscription.Status>([
+    "trialing",
+    "active",
+    "past_due",
+  ]);

@devkiran
Copy link
Collaborator Author

@coderabbitai full review please

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 27, 2025

@devkiran I'll conduct a full review of all the changes in this PR, examining the webhook handling logic, the new updateWorkspacePlan function, and ensuring the implementation correctly handles cases where customers have multiple active subscriptions.

✅ Actions performed

Full review triggered.

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.

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/web/app/(ee)/api/stripe/webhook/utils.ts (1)

68-71: Folder clean-up condition fixed — matches original intent.

Using workspace.foldersUsage > 0 for Free downgrades is correct and avoids unnecessary deletes when there are no folders.

🧹 Nitpick comments (3)
apps/web/app/(ee)/api/stripe/webhook/utils.ts (3)

119-140: Reduce one DB write: include webhookEnabled in the primary project update.

You already know whether webhooks should be disabled. Fold this into the first update, then keep webhook.updateMany + cache refresh.

         data: {
           plan: newPlanName,
           usageLimit: plan.limits.clicks!,
           linksLimit: plan.limits.links!,
           payoutsLimit: plan.limits.payouts!,
           domainsLimit: plan.limits.domains!,
           aiLimit: plan.limits.ai!,
           tagsLimit: plan.limits.tags!,
           foldersLimit: plan.limits.folders!,
           groupsLimit: plan.limits.groups!,
           usersLimit: plan.limits.users!,
           paymentFailedAt: null,
+          ...(shouldDisableWebhooks && { webhookEnabled: false }),
         },
-    if (shouldDisableWebhooks) {
-      await Promise.all([
-        prisma.project.update({
-          where: {
-            id: workspace.id,
-          },
-          data: {
-            webhookEnabled: false,
-          },
-        }),
-
-        prisma.webhook.updateMany({
+    if (shouldDisableWebhooks) {
+      await prisma.webhook.updateMany({
         where: {
           projectId: workspace.id,
         },
         data: {
           disabledAt: new Date(),
         },
-        }),
-      ]);
+      });

Also applies to: 86-99


61-63: Use a type-only import for typing to avoid pulling code at runtime.

Minor build hygiene: import getPlanFromPriceId as a type.

-import { getPlanFromPriceId, LEGACY_PRICE_IDS } from "@dub/utils";
+import { LEGACY_PRICE_IDS } from "@dub/utils";
+import type { getPlanFromPriceId } from "@dub/utils";

Also applies to: 7-7


52-60: Clarify the workspace param type with a named alias.

Define a WorkspaceForPlanUpdate type for reuse and readability instead of an inline Pick & intersection.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7d1d3b5 and 77ed7f4.

📒 Files selected for processing (1)
  • apps/web/app/(ee)/api/stripe/webhook/utils.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/stripe/webhook/utils.ts (3)
apps/web/lib/types.ts (1)
  • WorkspaceProps (161-177)
packages/utils/src/constants/pricing.tsx (2)
  • getPlanFromPriceId (352-354)
  • LEGACY_PRICE_IDS (32-35)
apps/web/lib/auth/token-cache.ts (1)
  • tokenCache (68-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
🔇 Additional comments (3)
apps/web/app/(ee)/api/stripe/webhook/utils.ts (3)

141-156: Webhook cache refresh after disabling — looks good.

Fetching fresh webhooks and mset on the cache is the right sequence.


78-80: Confirm legacy payouts behavior.

This condition skips all updates when only payoutsLimit differs on a legacy price ID. Is that intentional (i.e., legacy plans keep existing payouts limit and no other side-effects should run)?


165-174: Graceful recovery path — LGTM.

Clearing paymentFailedAt when no other updates are needed is correct.

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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/web/app/(ee)/api/stripe/webhook/utils.ts (2)

81-118: Avoid masking failures; use a DB transaction and expire caches after.

Promise.allSettled can leave partial DB state. Make the two Prisma writes atomic and expire token cache only on success.

-    await Promise.allSettled([
-      prisma.project.update({
+    await prisma.$transaction([
+      prisma.project.update({
         where: {
           id: workspace.id,
         },
         data: {
           plan: newPlanName,
           usageLimit: plan.limits.clicks!,
           linksLimit: plan.limits.links!,
           payoutsLimit: plan.limits.payouts!,
           domainsLimit: plan.limits.domains!,
           aiLimit: plan.limits.ai!,
           tagsLimit: plan.limits.tags!,
           foldersLimit: plan.limits.folders!,
           groupsLimit: plan.limits.groups!,
           usersLimit: plan.limits.users!,
           paymentFailedAt: null,
-          ...(shouldDeleteFolders && { foldersUsage: 0 }),
         },
       }),
 
-      prisma.restrictedToken.updateMany({
+      prisma.restrictedToken.updateMany({
         where: {
           projectId: workspace.id,
         },
         data: {
           rateLimit: plan.limits.api,
         },
       }),
-
-      // expire tokens cache
-      tokenCache.expireMany({
-        hashedKeys: workspace.restrictedTokens.map(
-          ({ hashedKey }) => hashedKey,
-        ),
-      }),
     ]);
+
+    // expire tokens cache (non-DB)
+    await tokenCache.expireMany({
+      hashedKeys: workspace.restrictedTokens.map(({ hashedKey }) => hashedKey),
+    });

98-99: Don’t zero foldersUsage before deletion; update it after deletes succeed.

Zeroing early risks inconsistent stats if deletion fails.

@@
-          ...(shouldDeleteFolders && { foldersUsage: 0 }),
@@
-    if (shouldDeleteFolders) {
-      await deleteWorkspaceFolders({
-        workspaceId: workspace.id,
-      });
-    }
+    if (shouldDeleteFolders) {
+      await deleteWorkspaceFolders({ workspaceId: workspace.id });
+      await prisma.project.update({
+        where: { id: workspace.id },
+        data: { foldersUsage: 0 },
+      });
+    }

Also applies to: 160-164

🧹 Nitpick comments (1)
apps/web/app/(ee)/api/stripe/webhook/customer-subscription-updated.ts (1)

65-69: Plan update centralization: looks good; consider retry-safety.

Delegation to updateWorkspacePlan is the right move. Minor: wrap in try/catch to log context (event.id, workspace.id) so Stripe retries are diagnosable.

Example:

-  await updateWorkspacePlan({
+  try {
+    await updateWorkspacePlan({
       workspace,
       plan,
       priceId,
-  });
+    });
+  } catch (err) {
+    console.error("customer.subscription.updated failed", {
+      eventId: event.id,
+      stripeId,
+      priceId,
+      err,
+    });
+    throw err; // let Stripe retry
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e1dbc66 and 77ed7f4.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts (3 hunks)
  • apps/web/app/(ee)/api/stripe/webhook/customer-subscription-updated.ts (2 hunks)
  • apps/web/app/(ee)/api/stripe/webhook/utils.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: api-tests
🔇 Additional comments (1)
apps/web/app/(ee)/api/stripe/webhook/utils.ts (1)

69-70: No Changes Needed for Pro Webhook Behavior

The logic in apps/web/app/(ee)/api/stripe/webhook/utils.ts correctly disables webhooks for the “pro” plan. The central plan capabilities configuration explicitly disallows webhook creation on both “free” and “pro” tiers:

  • In apps/web/lib/plan-capabilities.ts, canCreateWebhooks is set to !!plan && !["free", "pro"].includes(plan), meaning only plans above “pro” (e.g., “business”, “advanced”, “enterprise”) may create webhooks.

Since the webhook util disables webhooks when newPlanName is "free" or "pro", it aligns perfectly with this configuration. No updates are required here.

@devkiran
Copy link
Collaborator Author

@coderabbitai final review. Any breaking changes in the Stripe webhook?

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 28, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Examine the webhook files to understand the changes and identify potential breaking changes

echo "=== Examining customer-subscription-deleted.ts ==="
cat apps/web/app/\(ee\)/api/stripe/webhook/customer-subscription-deleted.ts

echo -e "\n=== Examining customer-subscription-updated.ts ==="
cat apps/web/app/\(ee\)/api/stripe/webhook/customer-subscription-updated.ts

echo -e "\n=== Examining utils.ts for updateWorkspacePlan function ==="
cat apps/web/app/\(ee\)/api/stripe/webhook/utils.ts

echo -e "\n=== Check if there are any tests for these webhook endpoints ==="
fd -t f -e ts -e js "customer-subscription.*test" apps/web/

echo -e "\n=== Check for any schema changes or migrations ==="
fd -t f -e sql -e prisma "migration" | head -10

echo -e "\n=== Look for any TypeScript interface changes ==="
rg -n "interface.*Workspace" --type=ts -A 5 -B 2 | head -20

Length of output: 15568


@devkiran Based on my analysis of the Stripe webhook changes, there are no breaking changes to the external webhook contract. Here's my detailed assessment:

✅ No Breaking Changes Found

Webhook Contract Remains Intact:

  • Both customer-subscription-deleted and customer-subscription-updated endpoints maintain the same request/response format
  • Response format unchanged: NextResponse.json({ received: true })
  • Stripe event object processing remains compatible
  • Error handling patterns preserved

🔄 Internal Logic Changes (Non-Breaking)

customer-subscription-deleted.ts:

  • New Feature: Now checks for other active subscriptions before cancellation
  • Early Return: If active subscription found, updates workspace plan and returns early
  • Backward Compatible: Original cancellation flow preserved when no active subscriptions exist

customer-subscription-updated.ts:

  • Refactored: Simplified by delegating complex logic to updateWorkspacePlan utility
  • Functionally Equivalent: Same end result, cleaner implementation
  • Preserved: Cancellation feedback logic maintained

updateWorkspacePlan Function:

  • Centralized Logic: Consolidates workspace plan update operations
  • Feature Parity: Maintains all original functionality (database updates, cache invalidation, folder deletion, webhook management)
  • Conditional Operations: Preserves legacy price ID handling and conditional updates

🧪 Recommended Testing

Since there's significant refactoring, I recommend testing:

  1. Multiple Active Subscriptions: Verify workspace stays active when customer has other valid subscriptions
  2. Plan Transitions: Test upgrade/downgrade scenarios
  3. Edge Cases: Legacy price IDs, payment failures, folder deletions
  4. Cache Invalidation: Ensure tokens and webhooks are properly invalidated

The changes enhance functionality while maintaining full backward compatibility with Stripe's webhook contract.

@devkiran devkiran requested a review from steven-tey August 28, 2025 10:19
@steven-tey steven-tey merged commit 375acd4 into main Aug 28, 2025
8 checks passed
@steven-tey steven-tey deleted the check-other-subscriptions branch August 28, 2025 15:07
@coderabbitai coderabbitai bot mentioned this pull request Sep 17, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 28, 2025
@coderabbitai coderabbitai bot mentioned this pull request Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants