You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 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.
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
internalerror with no explanation.This check is wrong in both directions:
The fix
Run one pre-flight check before deleting anything, collect all blockers, and return them together as a
failed_preconditionerror. Each blocker names the fix, and each fix is something the caller can do themselves through the API:CancelSubscription), then retryDetails of the behavior:
PreconditionFailureerror details (one violation per blocker, with a machine-readable type likeACTIVE_SUBSCRIPTIONand a human message).failed_precondition; only unexpected failures stayinternal.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:
DeleteOrganizationRequestin the proto definitions (raystack/proton), for examplebool acknowledge_token_forfeit.UNUSED_TOKENSviolation saying how many tokens would be lost and that the caller must re-send the delete with the flag set to proceed.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.