feat(deleter): send the delete notice on every organization delete - #1903
Conversation
The owner email used to go out only when the delete forfeited tokens. Every deleted organization now notifies all of its owners, whether it held tokens or not. The owners are resolved after the blocker check passes and before teardown removes their policies, still best-effort so the delete never depends on the policy machinery. Because the mail is no longer about forfeits, the config key moves from billing.token_forfeit_notice to billing.org_delete_notice (nothing has deployed the old key), and the built-in default becomes a plain deletion notice: it names no token amounts and only mentions that purchased tokens will be settled when some existed. The template still receives .Amount and .Purchased for deployments that want them, and the forfeit audit records keep the exact per-account numbers, which is where support settles from.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. 📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR renames billing notice configuration, updates cascade deletion wiring, changes owner notification behavior, preserves audit amount validation, and registers two billing webhook events. ChangesOrganization deletion notice flow
Estimated code review effort: 4 (Complex) | ~40 minutes Merge Risk: ⚪ Minimal · up to The change makes organization deletion notices consistent and updates the related configuration and documentation without any identified current-head merge-blocking risk. 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9df21cb3-a6bd-4ce1-b9b1-0869551ef988
📒 Files selected for processing (5)
billing/config.gocmd/serve.gocore/deleter/delete_notice.gocore/deleter/service.gocore/deleter/service_test.go
Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.
Coverage Report for CI Build 32833795403Coverage increased (+0.03%) to 49.157%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
|
Tested this live on a local build of the branch, with a real SMTP sink catching the mails and a real Stripe test checkout for the purchased-token case.
The default mails never contain token counts, which matches the settlement concern in the description. |
Teardown deletes the owner policies before the roles and the org row. A delete failing in that window left the retry unable to resolve the owners, so the completing retry emailed no one. The owner ids now go into an audit record before teardown starts, and a retry that finds no resolvable owners loads them from there — the users themselves outlive the org, so ids are all it needs. Best-effort on both sides, like the notice itself: without a readable audit store the send path logs that no owner could be notified.
rohilsurana
left a comment
There was a problem hiding this comment.
A few notes on the audit record, the tests, and some leftovers from the rename.
The recipients record turned internal retry state into an audit event, and every audit log fans out to the registered webhooks — pushing owner user ids to third-party endpoints for a window that spans two local statements at the very tail of teardown. The audit log stays what it was meant to be here: the record of the forfeited amounts. A delete that fails in that narrow window and completes on retry logs that no owner could be notified, which is the visible, honest outcome.
The forfeit record and its retry recovery now have real coverage: the tests put an audit service with an in-memory repository into the context, assert the record carries the amount and the purchased share, and assert a retry that finds no billing accounts rebuilds the notice from the record. The two successful-delete subscription tests assert the owner mail instead of silencing it through a failed owner lookup. The dead zero-total return in collectDeleteNotice is gone, the last "transferable" comments say "settled" like the mail copy, and the webhook event list plus its docs page now name app.billing.tokens.forfeited and app.billing.checkout.deleted, which audit publishing already sends to subscribers.
Follow-up to #1880. The owner email used to go out only when the delete forfeited unused tokens. Every deleted organization now notifies all of its owners, tokens or not.
billing.token_forfeit_noticetobilling.org_delete_notice. Nothing has deployed the old key yet, so this is a rename, not a migration..Amountand.Purchasedstay available to config templates for deployments that want them.app.billing.tokens.forfeitedstill records the exact per-accountamountandpurchased, and the per-account recovery on retried deletes still fills the template data. The webhook event list and its docs page now nameapp.billing.tokens.forfeitedandapp.billing.checkout.deleted, which audit publishing already sends to subscribers.How support reconstructs a refund after the org is gone
The delete hard-deletes the org's ledger rows, so the forfeit audit record is the number support settles from:
amount(balance at delete) andpurchased(the refundable share) per billing account. If the record is ever in doubt, this query rebuilds what the surviving remnants still know — the platform-side halves of purchase entries carry acheckout_id, and the checkout's deletion audit record remembers the org:tokens_boughtis only a ceiling: usage rows carry no org attribution on their surviving halves, so the unspent share is not derivable from the ledger remnants — that is exactly what the forfeit record exists for. Thestripe_sessionids bridge to Stripe for the payment and refund history, which is the double-refund guard.🤖 Generated with Claude Code