Skip to content

fix: delete stripe_info when an org is removed#2580

Open
riderx wants to merge 4 commits into
mainfrom
fix/cleanup-stripe-info-on-org-delete
Open

fix: delete stripe_info when an org is removed#2580
riderx wants to merge 4 commits into
mainfrom
fix/cleanup-stripe-info-on-org-delete

Conversation

@riderx

@riderx riderx commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

  • Delete stripe_info (and pending placeholders) in on_organization_delete after canceling Stripe subscriptions, so org deletion no longer leaves orphaned billing rows
  • Add migration 20260623143000_cleanup_orphaned_stripe_info.sql to remove existing orphaned stripe_info rows with no linked org
  • Add stripe:cleanup-orphaned-stripe-info script for dry-run/apply cleanup reports outside migrations

Why

Org deletion only removed the orgs row and canceled Stripe subscriptions. Because orgs.customer_id references stripe_info (not the other way around), stripe_info rows survived org deletion. That inflated Capgo active subscriber counts and produced rows like the 13 Stripe-canceled customers with no org.

Test plan

  • Run migration on preprod and confirm orphaned stripe_info count drops (expect ~13+ rows)
  • Delete a test org and verify its stripe_info row is removed
  • bunx vitest run tests/on_organization_delete.unit.test.ts
  • Optional dry run: bun run stripe:cleanup-orphaned-stripe-info --env-file=./internal/cloudflare/.env.prod

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added a cleanup utility to identify and remove orphaned payment records from the system.
  • Improvements

    • Organization deletion now comprehensively removes associated payment data and active subscriptions.
    • Implemented automated cleanup of orphaned payment records in the database.
  • Chores

    • Added a new script entry for running cleanup operations.
  • Tests

    • Added comprehensive tests validating payment data cleanup during organization deletion.

Org deletion canceled Stripe subscriptions but left stripe_info rows behind, inflating active subscriber counts. Clean up orphaned billing rows on delete and backfill existing orphans via migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@riderx, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 3 minutes and 54 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2fec5db0-54fa-4cb4-9905-ea082ebc0b5b

📥 Commits

Reviewing files that changed from the base of the PR and between c8eb678 and 16c9590.

📒 Files selected for processing (7)
  • .github/workflows/visual-diff.yml
  • knip.json
  • package.json
  • scripts/cleanup_orphaned_stripe_info.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
  • supabase/functions/_backend/utils/stripe.ts
  • tests/on_organization_delete.unit.test.ts
📝 Walkthrough

Walkthrough

Adds three complementary mechanisms to clean up stripe_info rows orphaned after org deletion: the on_organization_delete trigger is updated to await subscription cancellation and delete stripe_info rows for both real and pending_-prefixed customer IDs; a SQL migration removes existing orphans; and a new standalone script generates a CSV report of orphans and optionally cancels subscriptions and deletes rows concurrently.

Changes

Orphaned stripe_info cleanup

Layer / File(s) Summary
on_organization_delete trigger cleanup + unit tests
supabase/functions/_backend/triggers/on_organization_delete.ts, tests/on_organization_delete.unit.test.ts
Trigger now awaits cancelSubscription and deletes stripe_info rows for both record.customer_id and pending_${record.id}, with success/failure logging. Unit tests mock Stripe and Supabase and assert deletion calls for both customer ID forms.
SQL migration: delete existing orphaned stripe_info rows
supabase/migrations/20260623143000_cleanup_orphaned_stripe_info.sql
Migration deletes all stripe_info rows where no matching orgs record exists for the same customer_id, including the pending_
Orphaned stripe_info report and apply script
scripts/cleanup_orphaned_stripe_info.ts, package.json
New Bun script paginates stripe_info, resolves linked org customer IDs in chunks, filters orphans via isActionableStripeCustomerId, writes a CSV report, and when --apply is passed concurrently cancels Stripe subscriptions and deletes rows. A new stripe:cleanup-orphaned-stripe-info npm script entry wires the command.

Sequence Diagram(s)

sequenceDiagram
  actor Operator
  participant Script as cleanup_orphaned_stripe_info.ts
  participant Supabase as Supabase (stripe_info / orgs)
  participant Stripe as Stripe API

  Operator->>Script: bun scripts/cleanup_orphaned_stripe_info.ts [--apply]
  Script->>Supabase: Paginate all stripe_info rows
  Supabase-->>Script: stripe_info pages
  Script->>Supabase: Fetch orgs.customer_id in chunks of 200
  Supabase-->>Script: linked org customer IDs
  Script->>Script: Filter orphans via isActionableStripeCustomerId
  Script->>Operator: Write CSV report
  alt --apply flag present
    Script->>Stripe: List subscriptions per orphan customer
    Stripe-->>Script: subscription list
    Script->>Stripe: Cancel each subscription
    Stripe-->>Script: cancelled
    Script->>Supabase: DELETE stripe_info WHERE customer_id = orphan
    Supabase-->>Script: deleted count / error
    Script->>Operator: Report success/failure counts
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Cap-go/capgo#2002: The cleanup script reuses the same isActionableStripeCustomerId and pending_ customer ID filtering logic introduced by this PR's admin Stripe backfill helpers.

Suggested labels

codex

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main objective: deleting stripe_info records when an organization is removed, which is the core change across all modified files.
Description check ✅ Passed The description includes a comprehensive summary of changes, explains the motivation, and provides a detailed test plan with multiple test scenarios, aligning well with the repository template structure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@codspeed-hq

codspeed-hq Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing fix/cleanup-stripe-info-on-org-delete (16c9590) with main (a52498a)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@coderabbitai coderabbitai Bot added the codex label Jun 23, 2026
Comment thread scripts/cleanup_orphaned_stripe_info.ts Outdated
Comment thread supabase/functions/_backend/triggers/on_organization_delete.ts
Comment thread supabase/functions/_backend/triggers/on_organization_delete.ts
@cursor cursor Bot requested review from Dalanir and WcaleNieWolny June 23, 2026 19:58

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: high. Not approving: Cursor Bugbot finished with 3 unresolved findings (including a high-severity billing cancel/delete ordering concern), and the Bugbot check did not pass cleanly. Assigned @Dalanir and @WcaleNieWolny for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: medium. Cursor Bugbot completed as skipped and reported 3 potential issues, including a high-severity billing concern around deleting stripe_info after cancelSubscription. Human review is needed before merge; assigned WcaleNieWolny and Dalanir.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/cleanup_orphaned_stripe_info.ts`:
- Around line 140-162: The cleanup script logs failures but always exits with
code 0, preventing CI/ops systems from detecting incomplete cleanup. After the
asyncPool operation completes and the failure summary is logged, add a check
that sets process.exitCode to 1 when failures.length is greater than 0. This
ensures that when partial failures occur in the failures array, the process
exits with a non-zero status code to signal to callers that the cleanup was not
fully successful.
- Around line 127-129: The orphan filtering logic in the filter() call uses
isActionableStripeCustomerId which explicitly excludes customer IDs starting
with pending_, meaning these potential orphans are missing from both the CSV
report and the cleanup process. Either update the filtering condition to include
actionable pending_ prefixed customer IDs in the orphan set so they are properly
accounted for in the report and cleanup, or add clear documentation explaining
why pending_ customer IDs are intentionally excluded from the scope of this
orphan detection and remediation script.
- Around line 93-96: The `cancelStripeSubscriptions` function only fetches the
first 100 subscriptions with a hardcoded limit, leaving uncanceled subscriptions
for customers exceeding this threshold. Replace the single
`stripe.subscriptions.list` call with async iteration using a for-await loop to
automatically paginate through all subscription pages. Additionally, filter the
subscriptions to skip those that are already canceled before attempting to
cancel them, reducing unnecessary API calls while ensuring all active
subscriptions are properly canceled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c2dec5e7-7f95-4560-92bb-254d3b3b794e

📥 Commits

Reviewing files that changed from the base of the PR and between 9a48e19 and c8eb678.

📒 Files selected for processing (5)
  • package.json
  • scripts/cleanup_orphaned_stripe_info.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
  • supabase/migrations/20260623143000_cleanup_orphaned_stripe_info.sql
  • tests/on_organization_delete.unit.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)

Comment thread scripts/cleanup_orphaned_stripe_info.ts
Comment thread scripts/cleanup_orphaned_stripe_info.ts Outdated
Comment thread scripts/cleanup_orphaned_stripe_info.ts
riderx and others added 2 commits June 23, 2026 22:18
Fix invalid visual-diff workflow YAML and register the visual diff config as a knip entry so dynamically imported exports are not flagged as unused.

Co-authored-by: Cursor <cursoragent@cursor.com>
.eq('customer_id', pendingCustomerId)

if (pendingStripeInfoDeleteError) {
cloudlog({ requestId: c.get('requestId'), message: 'failed to delete pending stripe_info during org delete', error: pendingStripeInfoDeleteError, customer_id: pendingCustomerId })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Org delete ignores billing failures

Medium Severity

After org deletion, the handler always removes stripe_info and returns success even when Stripe cancellation or the database delete fails. cancelSubscription logs errors without rethrowing, so stripe_info can be deleted while subscriptions stay active, or delete errors can leave orphaned stripe_info rows with no retry.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1291bcc. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: high. Not approving: Cursor Bugbot completed as skipped and reports 4 unresolved findings, including a high-severity billing cancel/delete concern on the latest commit. Dalanir and WcaleNieWolny are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: high. Not approving: Cursor Bugbot completed as skipped with 4 unresolved findings (including billing cancel/delete ordering), and this Stripe org-cleanup change exceeds the low-risk approval threshold. @Dalanir and @WcaleNieWolny are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

Paginate Stripe subscription cancellation, include pending_ orphans in the cleanup script, exit non-zero on partial failures, and ensure org delete still removes stripe_info when Stripe cancel fails.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 16c9590. Configure here.

customerId: row.customerId,
error: error instanceof Error ? error.message : String(error),
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Apply skips DB on cancel

Medium Severity

In --apply mode, if cancelStripeSubscriptions fails for an orphaned stripe_info row, the script catches the error and skips deleting that stripe_info row from the database, leaving confirmed orphans behind.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 16c9590. Configure here.

return !!orgId && !linkedOrgIds.has(orgId)
}

return isActionableStripeCustomerId(row.customerId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Orphan rules differ from migration

Low Severity

isOrphanedStripeInfoRow treats non-pending_ rows with blank or whitespace-only customer_id as not orphaned, while the migration deletes any stripe_info row with no matching org. Dry-run counts and CSV output can understate what the migration removes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 16c9590. Configure here.

@sonarqubecloud

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Risk: high. Not approving: Cursor Bugbot completed as skipped with 3 unresolved findings on the latest commit, and this Stripe billing cleanup exceeds the low-risk approval threshold. @Dalanir and @WcaleNieWolny are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Risk: high. Not approving: Cursor Bugbot completed as skipped and reports 3 unresolved findings on org-delete Stripe cleanup. Dalanir and WcaleNieWolny are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant