Skip to content

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

Description

@whoAbhishekSah

The problem

Before deleting an org, the only check we run today is: does the billing account have any invoice, ever? If yes, we refuse — and because of the error mapping issue (#1836), the caller sees a plain internal error with no explanation.

This check is wrong in both directions:

  • Too strict: even a fully paid, closed invoice blocks deletion forever. The user has no way to remove invoice history, so the org becomes undeletable and the error cannot guide them anywhere.
  • Too loose: an active subscription does not block anything. The delete flow silently cancels it on Stripe mid-delete. Token balances are not looked at either — unused paid tokens vanish, and unpaid token debt is written off.

The fix

Run one pre-flight check before deleting anything, collect all blockers, and return them together as a failed_precondition error. Each blocker names the fix, and each fix is something the caller can do themselves through the API:

Blocker What the user does
Active or trialing subscription cancel it (CancelSubscription), then retry
Open or unpaid invoice pay it via the invoice's hosted URL, then retry
Postpaid account with negative token balance buy tokens to clear the debt, then retry
Prepaid account with unused tokens re-send the delete with the acknowledgment flag (see below)

Details of the behavior:

  • Paid invoices stop blocking. Stripe keeps its own permanent copy of every invoice, so removing our rows loses nothing. Only open/unpaid invoices block.
  • All blockers come back in one response, using standard PreconditionFailure error details (one violation per blocker, with a machine-readable type like ACTIVE_SUBSCRIPTION and a human message).
  • The check runs before any deletion starts, so a blocked delete changes nothing (this depends on the ordering work in Organization delete can fail halfway and leave the org in a broken state #1835).
  • The handler maps this error to failed_precondition; only unexpected failures stay internal.

Proto change: acknowledging unused tokens

Unused prepaid tokens are the one blocker with no clean-up action — the user already paid for them, and there is nothing to cancel or pay. What we need from the user is informed consent: "I understand deleting this org forfeits my remaining tokens."

A confirmation popup in a UI is not enough on its own. The API is public — SDKs, scripts, curl — and the server cannot see a popup. For the server to enforce the consent, it has to arrive inside the request. So:

  • Add a field to DeleteOrganizationRequest in the proto definitions (raystack/proton), for example bool acknowledge_token_forfeit.
  • If the org has unused prepaid tokens and the flag is not set, the pre-flight returns an UNUSED_TOKENS violation saying how many tokens would be lost and that the caller must re-send the delete with the flag set to proceed.
  • If the flag is set, the delete goes ahead and the forfeited amount is written to the audit record.
  • A UI popup then becomes the natural front end for this: show the warning, and set the flag when the user confirms.

Why this way

A block the user cannot act on is a dead end — that is what the current invoice rule is. Every blocker we return names a concrete next step the caller can take themselves, including the token case. And returning all blockers at once matters: otherwise the user cancels a plan, retries, hits the invoice error, pays, retries, and hits the balance error — three round trips for what should be one checklist.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions