Skip to content

feat(store): billing rows are soft deleted when an org is deleted - #1983

Open
rohilsurana wants to merge 10 commits into
mainfrom
feat/billing-rows-soft-delete
Open

rohilsurana wants to merge 10 commits into
mainfrom
feat/billing-rows-soft-delete

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

What changes

The org delete now keeps the billing rows. These five deletes set deleted_at instead of removing rows:

  • billing account (customer)
  • subscriptions
  • invoices
  • checkouts
  • token transactions

The order is the same as before: children first, the account last, the org row after everything else.

The account delete now refuses while a live subscription, invoice or checkout points at it. The DeleteBillingAccount API answers failed_precondition in that case. Before, it was an internal error from the foreign key.

A credit entry now also checks that the billing account is live. Before, only the debit side checked it. A credit to a deleted account now answers not found, like a debit does.

Stripe behavior is unchanged. Provider ids stay on the rows. Clearing them is a separate ticket.

Comments that said rows are removed are reworded.

Release note: deploy order

  1. Deploy feat(store): billing invoice and subscription provider ids are unique over live rows only #1981 (provider id unique indexes over live rows).
  2. Deploy feat(billing): invoice jobs see deleted invoices #1982 (invoice jobs read deleted invoices).
  3. Deploy the org soft delete stack (feat(store): organization delete keeps the row and frees its name #1973 and feat(deleter): delete disabled projects and groups with the org #1976).
  4. Deploy this PR last.

Do not ship this before #1973. A hard org row would make every org delete fail on the billing_customers.org_id foreign key.

Do not ship it before #1982. If a delete stops half way, an overdraft customer could be billed twice.

This PR stays a draft until #1981, #1982 and #1973 are merged.

How it was tested

  • Repository tests for each of the five deletes, including the account refusal.
  • A cascade test that runs the five deletes in the deleter's order and checks every row is kept. It also checks a second run works. It is billing only, because the org soft delete is not on main yet.
  • Repository test that a credit to a deleted account is refused.
  • API error mapping test.
  • Sandbox run with an offline billing account on this branch merged over the org stack. The delete succeeded, every billing row the account owns was soft deleted, the org row stayed, the forfeit and account audit records were written, a second delete was refused, and a retry after a half finished attempt worked.

One thing to know: the onboarding credit is a double entry. Its debit side sits on the system account, so the delete does not hide that row. It only hides rows owned by the deleted account.

@vercel

vercel Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
frontier Ready Ready Preview Oct 16, 2026 10:21am UTC

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

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

Next included review available in 43 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: raystack/frontier/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 93c118d6-d2b0-4799-b8ce-5716cb705e35
📥 Commits

Reviewing files that changed from the base of the PR and between c2b82b8 and e625872.

📒 Files selected for processing: 17
  • billing/checkout/service.go
  • billing/credit/service.go
  • billing/customer/errors.go
  • billing/subscription/service.go
  • core/deleter/service.go
  • internal/api/v1beta1connect/billing_errors.go
  • internal/api/v1beta1connect/billing_errors_test.go
  • internal/store/postgres/billing_checkout_repository.go
  • internal/store/postgres/billing_checkout_repository_pg_test.go
  • internal/store/postgres/billing_customer_repository.go
  • internal/store/postgres/billing_customer_repository_pg_test.go
  • internal/store/postgres/billing_invoice_repository.go
  • internal/store/postgres/billing_invoice_repository_pg_test.go
  • internal/store/postgres/billing_subscription_repository.go
  • internal/store/postgres/billing_subscription_repository_pg_test.go
  • internal/store/postgres/billing_transactions_repository.go
  • internal/store/postgres/billing_transactions_repository_pg_test.go
📝 Summary

Summary by CodeRabbit

  • New Features
    • Billing account deletion is blocked while active subscriptions, invoices, or checkouts remain. The API reports this as a failed precondition.
    • Billing records are now retained after deletion but hidden from normal listings and lookups.
    • Credits cannot be added to deleted billing accounts.
  • Bug Fixes
    • Repeated deletion preserves the original deletion record and does not restore hidden billing records.

Billing repositories now retain deleted billing records as soft-deleted rows. Credit entry creation checks for an existing non-platform customer. Customer deletion is blocked while live subscriptions, invoices, or checkouts exist, and the new error maps to a Connect failed-precondition response.

Changes: Billing record lifecycle

Layer / File(s) Summary
Persist billing record deletions
billing/checkout/service.go, billing/credit/service.go, billing/subscription/service.go, core/deleter/service.go, internal/store/postgres/billing_checkout_repository.go, internal/store/postgres/billing_checkout_repository_pg_test.go, internal/store/postgres/billing_invoice_repository.go, internal/store/postgres/billing_invoice_repository_pg_test.go, internal/store/postgres/billing_subscription_repository.go, internal/store/postgres/billing_subscription_repository_pg_test.go, internal/store/postgres/billing_transactions_repository.go, internal/store/postgres/billing_transactions_repository_pg_test.go
Checkout, invoice, subscription, and transaction deletion now marks rows deleted instead of removing them. Tests check retained rows, hidden reads, and preservation of existing deletion timestamps. Comments describe the updated deletion behavior.
Validate credit entry customers
internal/store/postgres/billing_transactions_repository.go, internal/store/postgres/billing_transactions_repository_pg_test.go
Credit entry creation checks that a non-platform customer exists. A test verifies that creating an entry for a deleted customer returns customer.ErrNotFound without inserting a transaction.
Guard customer deletion
billing/customer/errors.go, internal/store/postgres/billing_customer_repository.go, internal/store/postgres/billing_customer_repository_pg_test.go, internal/api/v1beta1connect/billing_errors.go, internal/api/v1beta1connect/billing_errors_test.go
Customer deletion locks the live customer, checks for live subscriptions, invoices, and checkouts, and returns customer.ErrHasLiveRecords if any remain. Otherwise, it soft-deletes the customer and records the deletion audit event. The error maps to connect.CodeFailedPrecondition. Integration tests cover child-record checks, teardown, repeated deletion, and an in-flight checkout insert.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Suggested reviewers: amangit07

Merge Risk: 🔵 Low · up to c2b82

The concurrency test can fail spuriously when other database sessions wait on locks. The risk is bounded to test reliability and can be addressed as a follow-up.

Pre-merge checks: Passed 2
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.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@coveralls

coveralls commented Oct 12, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38886573055

Coverage increased (+0.2%) to 55.463%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 6 uncovered changes across 1 file (39 of 45 lines covered, 86.67%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
internal/store/postgres/billing_customer_repository.go 34 28 82.35%
Total (7 files) 45 39 86.67%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

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

Coverage Stats

Coverage Status
Relevant Lines: 41756
Covered Lines: 23159
Line Coverage: 55.46%
Coverage Strength: 19.25 hits per line

💛 - Coveralls

@rohilsurana
rohilsurana force-pushed the feat/invoice-jobs-count-deleted-invoices branch from 569b820 to a7af83d Compare October 13, 2026 09:16
@rohilsurana
rohilsurana force-pushed the feat/billing-rows-soft-delete branch from 7d949a4 to 68311b4 Compare October 13, 2026 09:16
@rohilsurana
rohilsurana force-pushed the feat/invoice-jobs-count-deleted-invoices branch from a7af83d to ebe1dc5 Compare October 13, 2026 11:42
@rohilsurana
rohilsurana force-pushed the feat/billing-rows-soft-delete branch from 68311b4 to be61aad Compare October 13, 2026 11:42
@rohilsurana
rohilsurana marked this pull request as ready for review October 13, 2026 11:53
@rohilsurana
rohilsurana force-pushed the feat/invoice-jobs-count-deleted-invoices branch from ebe1dc5 to 0031cdd Compare October 14, 2026 09:03
@rohilsurana
rohilsurana force-pushed the feat/billing-rows-soft-delete branch from be61aad to 49f5fe5 Compare October 14, 2026 09:03
@rohilsurana
rohilsurana force-pushed the feat/invoice-jobs-count-deleted-invoices branch from 0031cdd to 8d97dcd Compare October 16, 2026 08:39
Base automatically changed from feat/invoice-jobs-count-deleted-invoices to main October 16, 2026 08:44
@rohilsurana
rohilsurana force-pushed the feat/billing-rows-soft-delete branch from 49f5fe5 to 170adbe Compare October 16, 2026 08:44

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

🧹 Nitpick comments: 2
internal/store/postgres/billing_transactions_repository.go: 1

146-151: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Credit-customer check runs outside the transaction.

GetByID runs before WithTxn. A customer deleted between the check and the insert still receives the credit entry. The window is small. The deleter removes transactions before the customer, and the customer delete now takes a row lock. An in-transaction SELECT ... FOR SHARE on the credit customer would close the gap.

internal/store/postgres/billing_customer_repository_pg_test.go: 1

196-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The lock-wait probe can match other sessions.

The query counts any session with wait_event_type = 'Lock' and FOR UPDATE in its text. It spans the whole server. Each test uses its own database, but parallel packages share the instance, so the count can differ from "1". Filter on datname = current_database() to avoid flaky runs.

Proposed fix
-			`SELECT count(*)::text FROM pg_stat_activity WHERE wait_event_type = 'Lock' AND query ILIKE '%FOR UPDATE%'`) == "1"
+			`SELECT count(*)::text FROM pg_stat_activity WHERE datname = current_database() AND wait_event_type = 'Lock' AND query ILIKE '%FOR UPDATE%'`) == "1"

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: raystack/frontier/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5919f5a2-0a4b-4974-987c-9088c541f7e5
📥 Commits

Reviewing files that changed from the base of the PR and between 2c6d9a2 and 170adbe.

📒 Files selected for processing: 18
  • billing/checkout/service.go
  • billing/credit/service.go
  • billing/customer/errors.go
  • billing/subscription/service.go
  • core/deleter/service.go
  • internal/api/v1beta1connect/billing_errors.go
  • internal/api/v1beta1connect/billing_errors_test.go
  • internal/store/postgres/billing_checkout_repository.go
  • internal/store/postgres/billing_checkout_repository_pg_test.go
  • internal/store/postgres/billing_customer_repository.go
  • internal/store/postgres/billing_customer_repository_pg_test.go
  • internal/store/postgres/billing_invoice_repository.go
  • internal/store/postgres/billing_invoice_repository_pg_test.go
  • internal/store/postgres/billing_subscription_repository.go
  • internal/store/postgres/billing_subscription_repository_pg_test.go
  • internal/store/postgres/billing_teardown_pg_test.go
  • internal/store/postgres/billing_transactions_repository.go
  • internal/store/postgres/billing_transactions_repository_pg_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

@rohilsurana
rohilsurana requested a review from AmanGIT07 October 16, 2026 09:10
Comment thread internal/store/postgres/billing_customer_repository.go
Comment thread internal/store/postgres/billing_teardown_pg_test.go Outdated

@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: Repository: raystack/frontier/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 73087e52-9403-4f6b-87c9-078206ff9536
📥 Commits

Reviewing files that changed from the base of the PR and between 170adbe and c2b82b8.

📒 Files selected for processing: 1
  • internal/store/postgres/billing_customer_repository_pg_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Comment thread internal/store/postgres/billing_customer_repository_pg_test.go Outdated

This branch was successfully deployed

1 active deployment
Preview — e6258722 Deployed Oct 16, 2026 by vercel[bot]
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.

3 participants