Skip to content

feat: cancel subscriptions at the end of the period#2944

Merged
chronark merged 2 commits intomainfrom
cancel-at-end
Mar 11, 2025
Merged

feat: cancel subscriptions at the end of the period#2944
chronark merged 2 commits intomainfrom
cancel-at-end

Conversation

@chronark
Copy link
Collaborator

@chronark chronark commented Mar 10, 2025

Summary by CodeRabbit

  • New Features

    • Added a billing alert that shows when a cancellation is scheduled, including a countdown and an option to resume the subscription.
    • Introduced functionality for users to reverse their subscription cancellation before the current period ends.
  • Refactor

    • Updated cancellation processing so that subscriptions are set to cancel at period end rather than immediately, with confirmation messaging now clarifying the timing of the downgrade.
    • Enhanced subscription management logic to handle cancellation states more effectively.

@changeset-bot
Copy link

changeset-bot bot commented Mar 10, 2025

⚠️ No Changeset found

Latest commit: d24571c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Mar 10, 2025

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

Name Status Preview Comments Updated (UTC)
dashboard ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 10, 2025 5:08pm
engineering ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 10, 2025 5:08pm
play ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 10, 2025 5:08pm
www ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 10, 2025 5:08pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2025

📝 Walkthrough

Walkthrough

This pull request enhances subscription management by introducing a new optional cancelAt property to the subscription type. The update modifies cancellation logic so that subscriptions are marked to cancel at the end of the billing period rather than immediately. A new CancelAlert component displays relevant cancellation details, and the billing page now passes the additional property to its client component. Additionally, a new Stripe webhook endpoint has been implemented, along with changes in TRPC routers (including an added uncancelSubscription mutation), and minor updates to audit logs, environment variables, API schemas, and a migration script.

Changes

File(s) Change Summary
apps/dashboard/app/…/billing/client.tsx
apps/dashboard/app/…/billing/page.tsx
Added optional cancelAt property to subscription types and updated cancellation logic. Introduced CancelAlert component to show cancellation info; the page now passes the property derived from subscription.cancel_at.
apps/dashboard/app/…/webhooks/stripe/route.ts Introduced a new webhook handler for Stripe events that processes the customer.subscription.deleted event by updating the workspace and logging the cancellation.
apps/dashboard/lib/audit.ts
apps/dashboard/lib/env.ts
Extended the audit log actor type to include "system" and added STRIPE_WEBHOOK_SECRET to the Stripe environment schema.
apps/dashboard/lib/trpc/routers/… (index.ts, cancelSubscription.ts, uncancelSubscription.ts, updateSubscription.ts) Modified cancellation logic: switched from immediate cancellation to setting cancellation at period end, added new uncancelSubscription mutation, and adjusted update logic to reset the cancellation flag based on the cancelAt property.
go/api/openapi.json Reformatted the "required" field arrays in several OpenAPI schema definitions for improved readability without changing the underlying validation logic.
tools/migrate/migrate_subscription.ts Updated the migration script by changing the workspaceId to a new value, altering the target workspace for migration.

Possibly related PRs

Suggested labels

Bug, Core Team, 🕹️ oss.gg, :joystick: 150 points

Suggested reviewers

  • mcstepp
  • perkinsjr
  • MichaelUnkey
  • ogzhanolguncu

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbab45b and d24571c.

📒 Files selected for processing (1)
  • apps/dashboard/app/(app)/settings/billing/client.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/dashboard/app/(app)/settings/billing/client.tsx
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Test Packages / Test ./internal/clickhouse
  • GitHub Check: Test Go API Local / test_agent_local
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Agent Local / test_agent_local
  • GitHub Check: Build / Build

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.
    • Generate unit testing code for this file.
    • 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. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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

Documentation and Community

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

@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2025

Thank you for following the naming conventions for pull request titles! 🙏

const db = mysqlDrizzle(conn, { schema, mode: "default" });

const workspaceId = "ws_39g5eLLQTX8bVdbsGK9Dke";
const workspaceId = "ws_wB4SmWrYkhSbWE2rH61S6gMseWw";
Copy link
Member

Choose a reason for hiding this comment

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

Do you want to set this to WORKSPACE_ID versus an actual workspace?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ah this one is unrelated, I just recently migrated this user, ignore pls

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 (7)
apps/dashboard/app/api/webhooks/stripe/route.ts (4)

31-90: Consider adding more event types to handle

Currently, only customer.subscription.deleted is handled specifically. Consider expanding this to handle other subscription-related events like updates or creations.

  switch (event.type) {
    case "customer.subscription.deleted": {
      // Current implementation...
    }
+   case "customer.subscription.updated": {
+     const sub = event.data.object as Stripe.Subscription;
+     // Handle subscription updates
+     break;
+   }
+   case "customer.subscription.created": {
+     const sub = event.data.object as Stripe.Subscription;
+     // Handle new subscriptions
+     break;
+   }

    default:
      console.error("Incoming stripe event, that should not be received", event.type);
      break;
  }

8-18: Improve error handling with HTTP status codes

The current implementation throws errors directly. Consider returning appropriate HTTP status codes for better error handling.

export const POST = async (req: Request): Promise<Response> => {
  const signature = req.headers.get("stripe-signature");
  if (!signature) {
-   throw new Error("Signature missing");
+   return new Response("Webhook signature missing", { status: 400 });
  }

  const e = stripeEnv();

  if (!e) {
-   throw new Error("stripe env variables are not set up");
+   return new Response("Internal server error: missing configuration", { status: 500 });
  }

44-46: Improve workspace not found error handling

Consider returning a proper error response instead of throwing when the workspace isn't found.

if (!ws) {
-  throw new Error("workspace does not exist");
+  console.error(`Subscription ${sub.id} doesn't match any workspace`);
+  return new Response(`No workspace found for subscription ID: ${sub.id}`, { status: 404 });
}

54-68: Consider extracting free tier quotas to a constant

The free tier quotas are defined inline. Consider extracting to a reusable constant that can be imported across files.

+ // In a separate file, e.g., apps/dashboard/lib/constants.ts
+ export const FREE_TIER_QUOTAS = {
+   requestsPerMonth: 150_000,
+   logsRetentionDays: 7,
+   auditLogsRetentionDays: 30,
+   team: false,
+ } as const;

// Then in this file:
+ import { FREE_TIER_QUOTAS } from "@/lib/constants";

- const freeTierQuotas: Omit<Quotas, "workspaceId"> = {
-   requestsPerMonth: 150_000,
-   logsRetentionDays: 7,
-   auditLogsRetentionDays: 30,
-   team: false,
- };
+ const freeTierQuotas: Omit<Quotas, "workspaceId"> = FREE_TIER_QUOTAS;
apps/dashboard/app/(app)/settings/billing/client.tsx (3)

131-134: Refine multiline string interpolation for readability.

Spreading the template string across multiple lines can reduce clarity. Consider consolidating it:

- description={`Changing to ${p.name
- } updates your request quota to ${formatNumber(
-   p.quota.requestsPerMonth,
- )} per month immediately.`}
+ description={`Changing to ${p.name} updates your request quota to ${formatNumber(p.quota.requestsPerMonth)} per month immediately.`}

210-210: Unify the spelling of “cancelling.”

Line 203 uses “Cancelling” with double L, and this line uses “Canceling” with a single L. Keeping the spelling consistent helps maintain a polished user experience.

- description="Canceling your plan will downgrade your workspace to the free tier at the end of the current period. You can resume your subscription until then."
+ description="Cancelling your plan will downgrade your workspace to the free tier at the end of the current period. You can resume your subscription until then."

242-283: Handle potential time-zone disparities or edge cases in CancelAlert.

Displaying cancellation time with toLocaleDateString() might produce different formats across regions. Consider a consistent formatting approach or highlight the user’s time zone. Also consider an early-return or notice if Date.now() surpasses cancelAt.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a31e5da and cbab45b.

📒 Files selected for processing (11)
  • apps/dashboard/app/(app)/settings/billing/client.tsx (7 hunks)
  • apps/dashboard/app/(app)/settings/billing/page.tsx (1 hunks)
  • apps/dashboard/app/api/webhooks/stripe/route.ts (1 hunks)
  • apps/dashboard/lib/audit.ts (1 hunks)
  • apps/dashboard/lib/env.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/index.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/stripe/cancelSubscription.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/stripe/uncancelSubscription.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/stripe/updateSubscription.ts (1 hunks)
  • go/api/openapi.json (15 hunks)
  • tools/migrate/migrate_subscription.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (18)
  • GitHub Check: Test Packages / Test ./packages/rbac
  • GitHub Check: Test Packages / Test ./packages/nextjs
  • GitHub Check: Test Packages / Test ./packages/hono
  • GitHub Check: Test Packages / Test ./packages/cache
  • GitHub Check: Test Packages / Test ./packages/api
  • GitHub Check: Test Packages / Test ./internal/clickhouse
  • GitHub Check: Test Packages / Test ./internal/resend
  • GitHub Check: Test Packages / Test ./internal/keys
  • GitHub Check: Test Packages / Test ./internal/id
  • GitHub Check: Test Packages / Test ./internal/hash
  • GitHub Check: Test Go API Local / test_agent_local
  • GitHub Check: Test Agent Local / test_agent_local
  • GitHub Check: Test Packages / Test ./internal/encryption
  • GitHub Check: Build / Build
  • GitHub Check: Test Packages / Test ./internal/billing
  • GitHub Check: Test API / API Test Local
  • GitHub Check: autofix
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (15)
tools/migrate/migrate_subscription.ts (1)

17-17: Update to workspace ID in migration script.

The workspace ID has been updated to "ws_wB4SmWrYkhSbWE2rH61S6gMseWw". Make sure this ID is correct for its intended purpose (testing, development, etc.).

apps/dashboard/app/(app)/settings/billing/page.tsx (1)

177-177: LGTM: Added cancelAt property to subscription object.

Good addition of the cancelAt property derived from subscription.cancel_at. The conversion from seconds to milliseconds is consistent with the existing trialUntil property handling.

apps/dashboard/lib/trpc/routers/stripe/updateSubscription.ts (1)

81-85: LGTM: Added logic to uncancel a subscription when updating.

The added code correctly uncancels a subscription if it was previously scheduled for cancellation. This makes sense as changing the subscription plan indicates the user wants to continue their subscription.

apps/dashboard/lib/trpc/routers/index.ts (2)

56-56: LGTM: Added uncancelSubscription import.

Correctly imported the new uncancelSubscription function to support the enhanced subscription management features.


109-109: LGTM: Added uncancelSubscription to the stripe router.

Properly added the uncancelSubscription function to the stripe router, providing a way for users to reverse a pending cancellation.

apps/dashboard/lib/env.ts (1)

66-66: LGTM: Added Stripe webhook secret for new webhook functionality

The addition of the STRIPE_WEBHOOK_SECRET to the stripeSchema is necessary for the new Stripe webhook handler implementation.

go/api/openapi.json (1)

53-53: LGTM: Formatting improvements to OpenAPI schema

These changes improve readability by consolidating "required" arrays into a single line format and standardizing the positioning of "tags" properties.

Also applies to: 83-83, 107-107, 118-118, 154-154, 165-165, 191-191, 228-228, 266-266, 296-296, 322-322, 407-407, 485-485, 490-490, 568-568, 681-681

apps/dashboard/lib/audit.ts (1)

60-60: LGTM: Added "system" to actor type options

Adding "system" as a valid actor type is appropriate for supporting system-initiated actions like webhook-triggered events.

apps/dashboard/app/api/webhooks/stripe/route.ts (1)

8-92: New Stripe webhook handler implementation looks good

The webhook handler correctly processes subscription cancellation events and updates database records accordingly.

apps/dashboard/lib/trpc/routers/stripe/cancelSubscription.ts (1)

29-30: Ensure subscription state is valid before scheduling cancellation.

This sets cancel_at_period_end: true, which relies on the subscription being in a valid state (e.g., active or trialing). Consider verifying or gracefully handling scenarios in which the subscription may already be canceled or in a past-due state.

Would you like a script to search for all other references to stripeSubscriptionId and confirm they handle these edge cases?

apps/dashboard/lib/trpc/routers/stripe/uncancelSubscription.ts (1)

1-32: LGTM: The uncancel logic is correct.

Reverting cancel_at_period_end to false is straightforward and well structured. This approach correctly checks for the presence of stripeSubscriptionId and stripeCustomerId before calling Stripe.

apps/dashboard/app/(app)/settings/billing/client.tsx (4)

28-28: Clarify the time unit for cancelAt.

cancelAt?: number; suggests a timestamp. Ensure there's a clear distinction between seconds vs. milliseconds and keep it consistent throughout the codebase.

If you need to confirm usage, I can provide a script to scan for all references to cancelAt for any type inconsistencies.


76-78: Good check for scheduled cancellations.

Skipping cancellation when cancelAt is already set prevents duplicate end-of-period cancellations. This logic looks correct.


94-94: Clean integration of the new CancelAlert component.

Passing props.subscription?.cancelAt into CancelAlert ensures a clear separation of concerns and helps render a proper cancellation notice.


150-153: Same suggestion for multiline string formatting.

@chronark
Copy link
Collaborator Author

fixes ENG-1655

@chronark chronark enabled auto-merge March 10, 2025 17:44
@chronark chronark disabled auto-merge March 11, 2025 09:43
@chronark chronark merged commit 6434efd into main Mar 11, 2025
30 checks passed
@chronark chronark deleted the cancel-at-end branch March 11, 2025 09:43
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.

2 participants