Skip to content

feat(deleter): clear or report org delete blockers up front - #1857

Open
whoAbhishekSah wants to merge 1 commit into
org-delete-preflight-mocksfrom
org-delete-preflight-blockers
Open

feat(deleter): clear or report org delete blockers up front#1857
whoAbhishekSah wants to merge 1 commit into
org-delete-preflight-mocksfrom
org-delete-preflight-blockers

Conversation

@whoAbhishekSah

@whoAbhishekSah whoAbhishekSah commented Aug 5, 2026

Copy link
Copy Markdown
Member

Closes #1837. Top of the stack, based on #1865.

DeleteOrganization now checks everything that blocks the delete before touching any data, and returns every reason together as one failed_precondition response with a PreconditionFailure detail (one violation per blocker). The design changed after review discussions, so this differs from the issue text in two ways: paid subscriptions ask for a downgrade instead of a cancel, and unused tokens no longer need an acknowledgement field (the proto change was dropped, raystack/proton#497 is closed).

What blocks the delete:

  • Active subscription on a paid plan (ACTIVE_SUBSCRIPTION): the caller downgrades it to the standard plan (the configured billing.customer.default_plan) through the normal plan-change flow, then retries.
  • Unpaid invoice (UNPAID_INVOICE): an open or uncollectible invoice with a non-zero total. The caller pays it via its hosted payment page and retries.
  • Negative token balance (NEGATIVE_TOKEN_BALANCE): the account owes tokens (overdraft). Support has to settle it; the message says so.

What does not block:

  • A subscription on the standard plan: once nothing else blocks, the delete cancels it itself (immediate cancel, unbilled usage invoiced on the spot). The invoice check runs again after the cancel, so a final invoice still blocks the delete until it is paid. Because cancellation only happens when everything else is clear, a blocked delete never costs the caller their subscription.
  • Unused tokens: the delete forfeits them. The client confirms this with the user before calling, and the forfeited amount is written to an app.billing.tokens.forfeited audit record during teardown. A follow-up PR notifies the account owners by email.

Other changes:

  • Invoice checks sync from the billing provider first (local rows sync on a 5 minute timer otherwise), so a just-paid invoice does not block and a just-created one does.
  • Deleting an org that is already gone returns not_found before any checks run. Disabled orgs stay deletable.
  • New invoice state constant UncollectibleState.
  • First use of errdetails in the repo; google.golang.org/genproto/googleapis/rpc becomes a direct dependency.

🤖 Generated with Claude Code

@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 13, 2026 11:16am

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4fb06d56-a854-4ed5-a7e7-40838dedb619

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Organization deletion now checks for active subscriptions, unpaid invoices, and token balances before making changes.
    • Deletion responses clearly identify all conditions preventing completion.
    • Positive token balances can be forfeited when explicitly acknowledged.
    • Billing-account deletion records forfeited tokens for audit purposes.
  • Bug Fixes
    • Improved handling and reporting of billing-related deletion failures.
  • API Updates
    • Added support for the “uncollectible” invoice status.

Walkthrough

Organization deletion now performs billing preflight checks, aggregates structured blockers, and accepts token-forfeit acknowledgement. Billing-account deletion audits forfeited tokens. The Connect API returns structured failed-precondition details for blocked deletions.

Changes

Organization deletion workflow

Layer / File(s) Summary
Deletion contracts and billing events
billing/invoice/invoice.go, core/audit/audit.go, core/deleter/deleter.go, core/deleter/service.go, core/deleter/mocks/*
Added the uncollectible invoice state, token-forfeiture system event, structured deletion blockers, and subscription and balance service contracts with generated mocks.
Deletion preflight and token forfeiture
core/deleter/service.go, core/deleter/service_test.go
Organization deletion aggregates subscription, invoice, and balance blockers before modifying data. Billing-account deletion audits positive token balances before deleting credit transactions.
Connect API propagation and validation
internal/api/v1beta1connect/deleter.go, internal/api/v1beta1connect/interfaces.go, internal/api/v1beta1connect/mocks/*, internal/api/v1beta1connect/deleter_test.go, go.mod, Makefile
The API forwards token-forfeit acknowledgement and maps blocked deletions to structured FailedPrecondition responses. RPC dependencies and the protobuf source commit were updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • raystack/frontier#1838 — Also changes organization-deletion behavior and core/deleter/service.go.
  • raystack/frontier#1839 — Also changes billing-related deletion audit handling in core/audit/audit.go and core/deleter/service.go.

Suggested reviewers: rohilsurana

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
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.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch org-delete-preflight-blockers

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.

@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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 51d78c26-5a73-43e9-8050-b9aec5ac46cb

📥 Commits

Reviewing files that changed from the base of the PR and between c294c5f and 783da24.

⛔ 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

Comment thread core/deleter/service_test.go Outdated
Comment on lines +250 to +252
// the ack flag cannot bypass a debt
err = m.build().DeleteOrganization(context.Background(), "org-1", true)
assert.ErrorAs(t, err, &blocked)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the blocker type on the acknowledged retry.

The second call only checks that the error is a BlockedError. A regression that returns a different blocker type for ackTokenForfeit=true would still pass. Assert the type to close the gap.

💚 Proposed test assertion
 		// the ack flag cannot bypass a debt
 		err = m.build().DeleteOrganization(context.Background(), "org-1", true)
 		assert.ErrorAs(t, err, &blocked)
+		assert.Len(t, blocked.Blockers, 1)
+		assert.Equal(t, deleter.BlockerNegativeTokenBalance, blocked.Blockers[0].Type)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// the ack flag cannot bypass a debt
err = m.build().DeleteOrganization(context.Background(), "org-1", true)
assert.ErrorAs(t, err, &blocked)
// the ack flag cannot bypass a debt
err = m.build().DeleteOrganization(context.Background(), "org-1", true)
assert.ErrorAs(t, err, &blocked)
assert.Len(t, blocked.Blockers, 1)
assert.Equal(t, deleter.BlockerNegativeTokenBalance, blocked.Blockers[0].Type)

@whoAbhishekSah
whoAbhishekSah force-pushed the org-delete-preflight-blockers branch from 783da24 to 3c17128 Compare August 10, 2026 06:28
@whoAbhishekSah
whoAbhishekSah force-pushed the org-delete-preflight-blockers branch from 3c17128 to 969ef16 Compare August 13, 2026 10:57
@whoAbhishekSah whoAbhishekSah changed the title feat(deleter): check all org delete blockers up front and return them together feat(deleter): clear or report org delete blockers up front Aug 13, 2026
@coveralls

coveralls commented Aug 13, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 31694738096

Coverage increased (+0.1%) to 48.363%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: 32 uncovered changes across 4 files (115 of 147 lines covered, 78.23%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
core/deleter/service.go 116 92 79.31%
core/deleter/deleter.go 6 0 0.0%
cmd/serve.go 1 0 0.0%
internal/api/v1beta1connect/deleter.go 24 23 95.83%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
core/deleter/service.go 1 77.14%

Coverage Stats

Coverage Status
Relevant Lines: 39896
Covered Lines: 19295
Line Coverage: 48.36%
Coverage Strength: 15.43 hits per line

💛 - Coveralls

The delete first checks everything that blocks it and returns all the
reasons together as one failed_precondition response: a running
subscription on a paid plan (the caller downgrades it to the standard
plan), unpaid (open or uncollectible) invoices, and a negative token
balance which support has to settle. When nothing blocks, subscriptions
still running on the standard plan are canceled immediately with
unbilled usage invoiced on the spot, and the invoice check runs again
so a final invoice still blocks the delete. Unused tokens do not block:
the delete forfeits them and writes the amount to an audit record. An
already-deleted org returns not found before any checks run. Invoice
checks always sync from the billing provider first so the decision is
made on fresh data.
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

2 participants