Skip to content

feat(deleter): check all org delete blockers up front and return them together - #1857

Open
whoAbhishekSah wants to merge 2 commits into
mainfrom
org-delete-preflight-blockers
Open

feat(deleter): check all org delete blockers up front and return them together#1857
whoAbhishekSah wants to merge 2 commits into
mainfrom
org-delete-preflight-blockers

Conversation

@whoAbhishekSah

Copy link
Copy Markdown
Member

Closes #1837. Depends on raystack/proton#497 for the new request field.

What changed

Deleting an organization used to be gated by one rule: if the billing account ever had an invoice, the delete was refused — and the caller saw a bare internal server error. That rule was too strict (a fully paid invoice blocked the org forever) and too loose at the same time (an active subscription didn't block anything; the delete silently cancelled it mid-flight, and token balances were never looked at).

This PR replaces that gate with a pre-flight check that runs before any deletion starts, collects all blockers, and returns them together as a failed_precondition error with one standard PreconditionFailure violation per blocker:

Violation type What blocks What the caller does
ACTIVE_SUBSCRIPTION an active or trialing subscription cancel it, then retry
UNPAID_INVOICE an open or uncollectible invoice pay it, then retry
NEGATIVE_TOKEN_BALANCE token debt on the account buy tokens to clear the debt, then retry
UNUSED_TOKENS a positive token balance retry with acknowledge_token_forfeit set

Paid, void, draft, and zero-amount invoices no longer block: the billing provider keeps its own permanent copy of every invoice, so deleting our rows loses nothing.

The token forfeit acknowledgment

Unused prepaid tokens have no clean-up action — the user already paid for them. What the server needs is informed consent, and a UI popup alone can't provide that because the API is public (SDKs, scripts, curl). So DeleteOrganizationRequest gains a bool acknowledge_token_forfeit field (added in raystack/proton#497; this PR bumps PROTON_COMMIT). Without the flag, a positive balance returns an UNUSED_TOKENS violation naming the amount at stake. With it, the delete proceeds and the forfeited amount is written to a new app.billing.tokens.forfeited audit record during the credit teardown. The flag cannot bypass a debt.

Accounts without a billing provider (offline accounts) only get the token checks: their subscription and invoice rows have nothing behind them the caller could cancel or pay, and the teardown already skips the provider for them.

Error mapping

The handler maps deleter.BlockedError to failed_precondition and attaches the violations as errdetails.PreconditionFailure. The error sanitizer interceptor passes non-internal codes through untouched, so both the message and the structured details reach the caller. Everything unexpected stays internal, same as before.

Example response:

{
  "code": "failed_precondition",
  "message": "organization cannot be deleted yet: subscription[...] is active: cancel it, then retry the delete; billing account[...] has 500 unused tokens that deleting the organization forfeits: retry the delete with acknowledge_token_forfeit set to proceed",
  "details": [{
    "type": "google.rpc.PreconditionFailure",
    "value": { "violations": [
      { "type": "ACTIVE_SUBSCRIPTION", "subject": "<subscription-id>", "description": "..." },
      { "type": "UNUSED_TOKENS", "subject": "<billing-account-id>", "description": "..." }
    ]}
  }]
}

Notes for review

  • The PROTON_COMMIT pin points at proton 91eaffc + only the new field (branch frontier-pin/org-delete-token-forfeit), because proton main has since picked up breaking changes (plan RPC moves) that frontier hasn't absorbed yet. Regenerated frontier.pb.go is the first commit.
  • google.golang.org/genproto/googleapis/rpc moves from indirect to direct in go.mod for the errdetails import; no version change.
  • This builds on the cascade-ordering work from Organization delete can fail halfway and leave the org in a broken state #1835, so a blocked or failed delete changes nothing and can simply be retried.

🤖 Generated with Claude Code

whoAbhishekSah and others added 2 commits August 5, 2026 17:42
…tionRequest

Generated from raystack/proton@0b8bdb2 (91eaffc + the new field only,
so the recent breaking proto changes on proton main stay out of this
change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… together

Replace the blanket "any invoice ever" gate with a pre-flight check
that runs before any deletion starts and collects every blocker:

- active or trialing subscription: cancel it, then retry
- open or uncollectible invoice: pay it, then retry; paid invoices
  stop blocking since the billing provider keeps its own copy
- negative token balance: buy tokens to clear the debt, then retry
- unused tokens: retry with acknowledge_token_forfeit set; the
  forfeited amount is written to an audit record during the delete

Accounts without a billing provider only get the token checks, since
their subscription and invoice rows have nothing the caller could
cancel or pay.

The handler returns the blockers as a failed_precondition error with
one PreconditionFailure violation per blocker, so a caller sees the
whole checklist in one response instead of one blocker per retry.
Unexpected failures stay internal.

Closes #1837

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview Aug 5, 2026 12:25pm

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@whoAbhishekSah, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

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

How do review limits work?

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

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, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e93ed47a-99da-434b-ac51-6de28ee130b9

📥 Commits

Reviewing files that changed from the base of the PR and between 5c7721f and 29a0e18.

⛔ Files ignored due to path filters (1)
  • proto/v1beta1/frontier.pb.go is excluded by !**/*.pb.go, !proto/**
📒 Files selected for processing (13)
  • Makefile
  • billing/invoice/invoice.go
  • core/audit/audit.go
  • core/deleter/deleter.go
  • core/deleter/mocks/credit_service.go
  • core/deleter/mocks/subscription_service.go
  • core/deleter/service.go
  • core/deleter/service_test.go
  • go.mod
  • internal/api/v1beta1connect/deleter.go
  • internal/api/v1beta1connect/deleter_test.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/cascade_deleter.go

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

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

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.

Organization delete: check all blockers up front and tell the user what to fix

1 participant