Skip to content

feat(store): billing account delete hides the account's token transactions with the row - #1999

Open
rohilsurana wants to merge 10 commits into
mainfrom
feat/billing-account-delete-hides-transactions
Open

rohilsurana wants to merge 10 commits into
mainfrom
feat/billing-account-delete-hides-transactions

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Deleting a billing account now hides the account's token transactions and records what was forfeited on the account's billing_customer.deleted audit record.

An account that owes tokens cannot be deleted. The API answers with failed precondition.

Changes

  • The delete in the repository hides the account's live token transactions. It writes the forfeited amount and the purchased share into the audit record metadata, in the same transaction.
  • The delete is rolled back if the hidden transactions add up to a debt.
  • The service refuses the delete when the balance is below zero, and when the account has live subscriptions, invoices or checkouts. Both checks run before the Stripe call, so a refused delete never touches Stripe. The live records check uses a new HasLiveRecords repository method.
  • The API maps the new ErrHasTokenDebt error to failed precondition.
  • Org delete no longer writes its own forfeit event and no longer deletes the credit transactions. The account delete does both.
  • When an org delete is retried, the owner notice recovers the forfeit of accounts that an earlier attempt already deleted. It reads the billing_customer.deleted records. Only records written by an org delete count, so an account deleted on its own is not added to the notice. The record carries org_teardown: true for this.
  • The app.billing.tokens.forfeited audit log event is removed, together with its entries in the webhook docs and the admin webhook event list. The event has shipped since v0.116.0. It was only an audit log entry and the forfeit now lives on the audit record, so it will no longer be sent as a webhook event.
  • The unused DeleteByAccountID on the credit service and repository is removed.

Technical Details

  • One SQL statement hides the transactions and returns the totals. It only touches rows where account_id is the customer id, never the system account.
  • The purchased share is the net of rows with source system.buy, kept between zero and the forfeited amount.
  • The debt check and the live records check each run twice. The service checks before the Stripe call. The repository checks again inside the transaction, where the customer row is locked, so a race cannot hide a debt or a live record.
  • Audit records written before this change have no org_teardown flag, so a retry that started before the deploy recovers nothing.
  • A gap remains. A credit that passed the account check just before the delete can still land on the deleted account. It is tracked in A credit can be added to a billing account that is being deleted #2010.

Test Plan

  • go build ./...
  • go vet on the touched packages
  • go test ./internal/store/postgres/ ./billing/... ./core/deleter/... ./internal/api/v1beta1connect/
  • golangci-lint run on the touched packages, 0 issues
  • gofmt on the changed files
  • Each commit was written test first and the new tests were seen failing before the code

SQL Safety

  • Values go in through goqu.Record and goqu.Ex or bind parameters
  • Callers of ToSQL forward the params
  • No ? placeholders inside quoted literals in goqu.L
  • No new nolint annotations

@vercel

vercel Bot commented Oct 14, 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 20, 2026 12:16pm UTC

@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Summary

Summary by CodeRabbit

  • Bug Fixes

    • Customer account deletion is blocked when the account has token debt or live billing records, helping prevent deletion when outstanding balances or billing activity remain.
    • When deletion succeeds, token totals are recorded with the deletion and can be recovered for organization deletion notices, including after a retry.
    • Billing activity is handled safely during deletion, preserving transaction history while excluding deleted records from active billing checks.
  • Changes

    • The forfeited-tokens webhook event is no longer listed among supported webhook events.

Billing-customer deletion now checks token debt and live records, hides transactions, and records token totals. Organization delete notices recover those totals from billing-customer deletion audit records. Organization teardown no longer separately deletes credit transactions or writes token-forfeiture audit records.

Changes: Billing deletion and token recovery

Layer / File(s) Summary
Billing deletion rules and persistence
billing/customer/..., internal/store/postgres/billing_customer_repository.go, internal/store/postgres/billing_customer_repository_pg_test.go
Deletion accepts options, hides transactions, rejects net debt, checks live records, and records token metadata. PostgreSQL tests cover deletion, rollback, and live-record checks.
Customer deletion guards and API contracts
billing/customer/service.go, internal/api/v1beta1connect/..., billing/customer/service_test.go
The customer service checks balance and live records before deletion. API contracts and error mapping use the updated deletion signature.
Organization teardown and audit-service wiring
cmd/serve.go, core/deleter/service.go, billing/credit/..., internal/store/postgres/billing_transactions_repository.go, core/deleter/service_test.go
The cascade deleter receives an audit-record service. Teardown passes OrgTeardown: true and removes separate credit-transaction deletion.
Delete-notice recovery from audit records
core/deleter/delete_notice.go, core/deleter/mocks/audit_record_service.go, core/audit/audit.go, docs/content/docs/reference/webhook.mdx, web/sdk/admin/utils/webhook-events.ts
Delete notices recover token totals from billing-customer deletion records. The legacy token-forfeiture event is removed from event declarations and webhook lists.

Priority: ➖ Normal

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

Change: Feature

Suggested reviewers: amangit07, whoabhisheksah

Merge Risk: 🟡 Moderate · up to fc474

Large deletion histories can understate forfeited tokens on retry, and a concurrent credit can remain live after its account is deleted. Resolve or explicitly accept these risks before merging.

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 14, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 39420937993

Coverage increased (+0.05%) to 55.627%

Details

  • Coverage increased (+0.05%) from the base build.
  • Patch coverage: 12 uncovered changes across 4 files (66 of 78 lines covered, 84.62%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
internal/store/postgres/billing_customer_repository.go 39 33 84.62%
billing/customer/service.go 16 12 75.0%
cmd/serve.go 1 0 0.0%
internal/api/v1beta1connect/billing_customer.go 1 0 0.0%
Total (7 files) 78 66 84.62%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
billing/credit/service.go 1 64.96%

Coverage Stats

Coverage Status
Relevant Lines: 41863
Covered Lines: 23287
Line Coverage: 55.63%
Coverage Strength: 19.3 hits per line

💛 - Coveralls

@rohilsurana
rohilsurana force-pushed the feat/billing-account-delete-hides-transactions branch from 4f3a2c0 to 88f48ee Compare October 14, 2026 06:27
@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/billing-account-delete-hides-transactions branch from 88f48ee to 55510df Compare October 14, 2026 09:04
@rohilsurana
rohilsurana marked this pull request as ready for review October 14, 2026 10:45
@rohilsurana
rohilsurana force-pushed the feat/billing-rows-soft-delete branch from 49f5fe5 to 170adbe Compare October 16, 2026 08:44
@rohilsurana
rohilsurana force-pushed the feat/billing-account-delete-hides-transactions branch from 55510df to 5a6a5bc Compare October 16, 2026 10:04
@rohilsurana
rohilsurana force-pushed the feat/billing-rows-soft-delete branch from 170adbe to c2b82b8 Compare October 16, 2026 10:04
@rohilsurana
rohilsurana force-pushed the feat/billing-rows-soft-delete branch from c2b82b8 to e625872 Compare October 16, 2026 10:20
@rohilsurana
rohilsurana force-pushed the feat/billing-account-delete-hides-transactions branch from 5a6a5bc to 0286c5f Compare October 16, 2026 10:21
Comment thread billing/customer/service.go Outdated
Base automatically changed from feat/billing-rows-soft-delete to main October 19, 2026 09:12
@rohilsurana
rohilsurana force-pushed the feat/billing-account-delete-hides-transactions branch from 5d7f7fc to 7f9267e Compare October 19, 2026 09:13
@rohilsurana
rohilsurana requested a review from AmanGIT07 October 19, 2026 09:13

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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: raystack/frontier/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e8cc6bc4-0fe1-4ef3-ac62-1754a045a56d
📥 Commits

Reviewing files that changed from the base of the PR and between 4e266f1 and 7f9267e.

📒 Files selected for processing: 18
  • billing/customer/errors.go
  • billing/customer/mocks/repository.go
  • billing/customer/service.go
  • billing/customer/service_test.go
  • cmd/serve.go
  • core/audit/audit.go
  • core/deleter/delete_notice.go
  • core/deleter/delete_notice_test.go
  • core/deleter/mocks/audit_record_service.go
  • core/deleter/mocks/credit_service.go
  • core/deleter/service.go
  • core/deleter/service_test.go
  • docs/content/docs/reference/webhook.mdx
  • internal/api/v1beta1connect/billing_errors.go
  • internal/api/v1beta1connect/billing_errors_test.go
  • internal/store/postgres/billing_customer_repository.go
  • internal/store/postgres/billing_customer_repository_pg_test.go
  • web/sdk/admin/utils/webhook-events.ts
💤 Files with no reviewable changes: 4
  • docs/content/docs/reference/webhook.mdx
  • core/deleter/mocks/credit_service.go
  • web/sdk/admin/utils/webhook-events.ts
  • core/audit/audit.go

Comment thread core/deleter/delete_notice.go
Comment on lines +521 to +532
hideTransactionsQuery := `WITH hidden AS (
UPDATE billing_transactions SET deleted_at = now()
WHERE account_id = $1 AND deleted_at IS NULL
RETURNING type, source, amount
)
SELECT coalesce(sum(CASE WHEN type = $2 THEN amount ELSE -amount END), 0),
coalesce(sum(CASE WHEN source = $3 THEN (CASE WHEN type = $2 THEN amount ELSE -amount END) ELSE 0 END), 0)
FROM hidden`
hideTransactionsParams := []any{id, credit.CreditType, credit.SourceSystemBuyEvent}

var customerModel Customer
var forfeitedTokens, purchasedTokens int64

@coderabbitai coderabbitai Bot Oct 19, 2026 •

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

rg -n 'func .*CreateEntry|func .*CreateTransaction|Insert.*Transaction|insert into billing_transactions|Insert\("billing_transactions"|billing_transactions.*INSERT' billing internal | head -120
sed -n '105,130p' internal/store/postgres/migrations/20231012175641_create_billing_tables.up.sql
sed -n '500,595p' internal/store/postgres/billing_customer_repository.go

Repository: raystack/frontier

Length of output: 6128


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- transaction repository ---'
sed -n '1,280p' internal/store/postgres/billing_transactions_repository.go
printf '%s\n' '--- concrete CreateEntry callers ---'
rg -n -F --glob '*.go' -e '.CreateEntry(' -e 'CreateEntry(' billing internal | head -240
printf '%s\n' '--- credit service and repository declarations ---'
rg -n -F --glob '*.go' -e 'type TransactionRepository' -e 'func (.*Credit' -e 'Credit(' billing internal | head -240
printf '%s\n' '--- focused tests ---'
sed -n '1,240p' internal/store/postgres/billing_transactions_repository_pg_test.go
printf '%s\n' '--- billing transaction migrations ---'
rg -n -F --glob '*.sql' -e 'billing_transactions' -e 'ALTER TABLE billing_transactions' internal/store/postgres/migrations | head -240

Repository: raystack/frontier

Length of output: 23712


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- CreateEntry dependencies ---'
rg -n -F --glob 'internal/store/postgres/billing_transactions_repository.go' -e 'func (r BillingTransactionRepository) getBalanceInTx' -e 'func (r BillingTransactionRepository) GetBalance' -e 'customerRepo.GetByID' -e 'fromLive(TABLE_BILLING_TRANSACTIONS)' internal/store/postgres/billing_transactions_repository.go
rg -n -F --glob 'internal/store/postgres/billing_customer_repository.go' -e 'func (r BillingCustomerRepository) GetByID' -e 'func (r BillingCustomerRepository) Delete' internal/store/postgres/billing_customer_repository.go
printf '%s\n' '--- relevant transaction and customer blocks ---'
sed -n '100,245p' internal/store/postgres/billing_transactions_repository.go
sed -n '70,135p' internal/store/postgres/billing_customer_repository.go
sed -n '485,585p' internal/store/postgres/billing_customer_repository.go
printf '%s\n' '--- credit service interface and callers ---'
sed -n '1,175p' billing/credit/service.go
rg -n -F --glob '*.go' -e 'NewCreditService' -e 'credit.Service' -e 'Credit(ctx' -e 'Debit(ctx' billing internal cmd | head -240
printf '%s\n' '--- all billing transaction constraints in migrations ---'
rg -n -i --glob '*.sql' -e 'foreign key' -e 'references billing_customers' -e 'billing_transactions' internal/store/postgres/migrations

Repository: raystack/frontier

Length of output: 21189


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- production Add callers and wiring ---'
rg -n -F --glob '*.go' -g '!**/*_test.go' -e '.CreditService.Add(' -e '.creditService.Add(' -e 'CreditService.Add(' -e 'creditService.Add(' -e 'credit.Service' -e 'NewService(' billing internal cmd | head -260
printf '%s\n' '--- exact customer lookup and SQL helper blocks ---'
sed -n '175,235p' internal/store/postgres/billing_customer_repository.go
rg -n -F --glob 'internal/store/postgres/*.go' -e 'func fromLive' -e 'func softDelete' -e 'func (.*WithTxn' -e 'WithTxn(ctx' internal/store/postgres pkg/db
sed -n '1,120p' internal/store/postgres/query.go 2>/dev/null || true
sed -n '1,220p' pkg/db/client.go 2>/dev/null || true

Repository: raystack/frontier

Length of output: 14266


Lock the live customer in the credit transaction.

CreateEntry checks the target customer before it starts its transaction. credit.Service.Add then inserts the customer credit without reading or locking that customer inside the transaction.

If the pre-check succeeds and BillingCustomerRepository.Delete commits before the insert transaction starts, createTransactionEntry can insert a new live transaction for the soft-deleted account. The deletion hides only transactions that exist when its update runs. This leaves a live transaction outside the deletion totals.

Add a transaction-scoped live-customer check with FOR UPDATE before the balance check and transaction inserts. Return customer.ErrNotFound when the row is already soft-deleted.

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.

@rohilsurana This is the gap the description lists. Can we close it in a follow-up with a FOR KEY SHARE read of the live customer inside CreateEntry's transaction? It only has to clash with the delete's FOR UPDATE.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, opened #2010 for it with the FOR KEY SHARE approach. Leaving it out of this PR. The description points to it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real, but not a fix for this PR. Tracked in #2010: the credit insert will take a FOR KEY SHARE read of the live customer inside its transaction, so it conflicts with the delete lock.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread core/deleter/delete_notice.go
Comment thread core/deleter/service.go
type CreditService interface {
GetBalance(ctx context.Context, accountID string) (int64, error)
List(ctx context.Context, flt credit.Filter) ([]credit.Transaction, error)
DeleteByAccountID(ctx context.Context, accountID string) error

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.

credit.Service.DeleteByAccountID has no callers after this change. Can we remove it, along with the repository method?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in e778039, along with the repository method, its test and the mock.

@AmanGIT07

Copy link
Copy Markdown
Contributor

The description still says the forfeit goes to app.billing.tokens.forfeited after the delete and that core/deleter is not changed. Can we update it to the new flow, including that this removes app.billing.tokens.forfeited, which has shipped since v0.116.0?

@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: b586b7de-26b5-4bc9-9f41-9ab440e0527f
📥 Commits

Reviewing files that changed from the base of the PR and between 7f9267e and fc474d7.

📒 Files selected for processing: 19
  • billing/credit/mocks/transaction_repository.go
  • billing/credit/service.go
  • billing/customer/customer.go
  • billing/customer/mocks/repository.go
  • billing/customer/service.go
  • billing/customer/service_test.go
  • cmd/serve.go
  • core/deleter/delete_notice.go
  • core/deleter/delete_notice_test.go
  • core/deleter/mocks/customer_service.go
  • core/deleter/service.go
  • core/deleter/service_test.go
  • internal/api/v1beta1connect/billing_customer.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/customer_service.go
  • internal/store/postgres/billing_customer_repository.go
  • internal/store/postgres/billing_customer_repository_pg_test.go
  • internal/store/postgres/billing_transactions_repository.go
  • internal/store/postgres/billing_transactions_repository_pg_test.go
💤 Files with no reviewable changes: 4
  • internal/store/postgres/billing_transactions_repository.go
  • billing/credit/service.go
  • internal/store/postgres/billing_transactions_repository_pg_test.go
  • billing/credit/mocks/transaction_repository.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 on lines +132 to +135
records, err := d.auditRecordService.List(ctx, utils.NewRQLQuery("", 0, forfeitRecordsLimit, []rql.Filter{
{Name: "event", Operator: "eq", Value: auditrecord.BillingCustomerDeletedEvent.String()},
{Name: "org_id", Operator: "eq", Value: orgID},
}, []rql.Sort{{Name: "occurred_at", Order: "desc"}}, nil))

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '110,170p' core/deleter/delete_notice.go
rg -n 'forfeitRecordsLimit|recoverForfeitFromAudit|\\.List\\(ctx,.*query|Limit\\(' core/deleter internal/store/postgres/audit_record_repository.go

Repository: raystack/frontier

Length of output: 2917


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- identifiers and call sites ---'
rg -n -F -- 'forfeitRecordsLimit' core internal || true
rg -n -F -- 'recoverForfeitFromAudit' . || true
rg -n -F -- 'DeleteRecordOrgTeardownKey' . || true
rg -n -F -- 'org_teardown' . || true
printf '%s\n' '--- delete notice source ---'
sed -n '1,260p' core/deleter/delete_notice.go
printf '%s\n' '--- delete notice callers ---'
rg -n -F -- 'deleteNotice' core/deleter || true
rg -n -F -- 'BuildDeleteNotice' core internal || true
rg -n -F -- 'DeleteNotice' core/deleter || true
printf '%s\n' '--- audit service and repository references ---'
rg -n -F -- 'List(ctx context.Context' internal core | grep -Ei 'audit|Audit' || true
rg -n -F -- 'NewRQLQuery' internal/store/postgres core | grep -Ei 'audit|Audit' || true
rg -n -F -- 'AuditRecord' internal/store/postgres | head -160 || true
printf '%s\n' '--- relevant diff ---'
git diff --stat 43d85db2d7cee24e0ba43851eb5cdeb3ae6f848f fc474d71e12f3bf1a8d811f34c807055f5369871 -- core/deleter internal/store/postgres billing
git diff --unified=35 43d85db2d7cee24e0ba43851eb5cdeb3ae6f848f fc474d71e12f3bf1a8d811f34c807055f5369871 -- core/deleter/delete_notice.go

Repository: raystack/frontier

Length of output: 41723


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- identifiers and call sites ---'
rg -n -F -- 'forfeitRecordsLimit' core internal || true
rg -n -F -- 'recoverForfeitFromAudit' . || true
rg -n -F -- 'DeleteRecordOrgTeardownKey' . || true
rg -n -F -- 'org_teardown' . || true
printf '%s\n' '--- delete notice source ---'
sed -n '1,280p' core/deleter/delete_notice.go
printf '%s\n' '--- delete notice callers ---'
rg -n -F -- 'deleteNotice' core/deleter || true
rg -n -F -- 'BuildDeleteNotice' core internal || true
rg -n -F -- 'DeleteNotice' core/deleter || true
printf '%s\n' '--- audit service and repository references ---'
rg -n -F -- 'List(ctx context.Context' internal core | grep -Ei 'audit|Audit' || true
rg -n -F -- 'NewRQLQuery' internal/store/postgres core | grep -Ei 'audit|Audit' || true
rg -n -F -- 'AuditRecord' internal/store/postgres | head -160 || true
printf '%s\n' '--- relevant diff ---'
git diff --stat 43d85db2d7cee24e0ba43851eb5cdeb3ae6f848f fc474d71e12f3bf1a8d811f34c807055f5369871 -- core/deleter internal/store/postgres billing
git diff --unified=35 43d85db2d7cee24e0ba43851eb5cdeb3ae6f848f fc474d71e12f3bf1a8d811f34c807055f5369871 -- core/deleter/delete_notice.go

Repository: raystack/frontier

Length of output: 42060


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- organization delete lifecycle ---'
sed -n '250,455p' core/deleter/service.go
printf '%s\n' '--- customer deletion and teardown metadata ---'
sed -n '470,625p' internal/store/postgres/billing_customer_repository.go
printf '%s\n' '--- audit record service and repository ---'
sed -n '175,235p' core/auditrecord/service.go
sed -n '145,225p' internal/store/postgres/audit_record_repository.go
printf '%s\n' '--- RQL query helper and SQL construction ---'
rg -n -F -- 'func NewRQLQuery' .
rg -n -F -- 'ApplyRQL' internal/store/postgres core pkg || true
rg -n -F -- 'rqlQuery.Limit' internal/store/postgres core pkg || true
rg -n -F -- 'rqlQuery.Offset' internal/store/postgres core pkg || true

Repository: raystack/frontier

Length of output: 17793


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- deleteCustomers continuation and options ---'
sed -n '430,505p' core/deleter/service.go
rg -n -F -- 'OrgTeardown:' . || true
rg -n -F -- 'DeleteOptions{' . || true
printf '%s\n' '--- standalone customer deletion callers ---'
rg -n -F -- 'customerService.Delete' core internal billing | head -100 || true
rg -n -F -- 'Delete(ctx context.Context, id string' billing/customer core internal | head -80 || true
printf '%s\n' '--- customer listing behavior ---'
rg -n -F -- 'func (.* List' internal/store/postgres/billing_customer_repository.go billing/customer core | head -80 || true
sed -n '1,180p' billing/customer/service.go
printf '%s\n' '--- audit filtered query and pagination ---'
rg -n -F -- 'func (r AuditRecordRepository) buildFilteredQuery' internal/store/postgres/audit_record_repository.go
sed -n '1,160p' internal/store/postgres/audit_record_repository.go
sed -n '1,120p' pkg/utils/rql.go
rg -n -F -- 'func AddRQLPaginationInQuery' pkg internal
rg -n -F -- 'func AddRQLFiltersInQuery' pkg internal

Repository: raystack/frontier

Length of output: 29945


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- audit filtering and pagination implementation ---'
sed -n '130,225p' pkg/utils/rql.go
sed -n '335,435p' internal/store/postgres/audit_record_repository.go
printf '%s\n' '--- live customer listing implementation ---'
rg -n -F -- 'func (r BillingCustomerRepository) List' internal/store/postgres/billing_customer_repository.go
sed -n '120,205p' internal/store/postgres/billing_customer_repository.go
printf '%s\n' '--- customer service delete binding ---'
sed -n '255,300p' billing/customer/service.go
sed -n '145,175p' internal/api/v1beta1connect/billing_customer.go

Repository: raystack/frontier

Length of output: 12334


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- exact customer List implementation and live scope ---'
sed -n '210,275p' internal/store/postgres/billing_customer_repository.go
rg -n -F -- 'func fromLive' internal/store/postgres
sed -n '1,45p' internal/store/postgres/billing_customer_repository.go
printf '%s\n' '--- audit list response shape ---'
rg -n -F -- 'type AuditRecordsList' core/auditrecord pkg
sed -n '1,55p' core/auditrecord/service.go
rg -n -F -- 'AuditRecordsList struct' .

Repository: raystack/frontier

Length of output: 5086


🏁 Script executed:

set -o pipefail
sed -n '34,48p' internal/store/postgres/postgres.go
sed -n '40,62p' core/auditrecord/models/models.go

Repository: raystack/frontier

Length of output: 1720


Paginate audit recovery before applying org_teardown.

A retry can omit forfeited and purchased amounts. The helper reads only the newest 1,000 billing_customer.deleted records for the organization. org_teardown is not an RQL-filterable field, so standalone deletions can fill that page before the helper checks the metadata. If more than 1,000 standalone accounts were deleted after a failed organization teardown, the older teardown record is not recovered. The retry then sends an understated notice because deleted accounts are excluded from the live customer list.

Suggested fix
--- "a/core/deleter/delete_notice.go"
+++ "b/core/deleter/delete_notice.go"
@@ -128,17 +128,25 @@
 // audit records hold for accounts a failed earlier attempt already deleted,
 // so the retry still reports them in the owner notice. Best-effort: without
 // a readable audit store the notice keeps only the live amounts.
 func (d Service) recoverForfeitFromAudit(ctx context.Context, orgID string, notice *deleteNotice) {
-	records, err := d.auditRecordService.List(ctx, utils.NewRQLQuery("", 0, forfeitRecordsLimit, []rql.Filter{
+	filters := []rql.Filter{
 		{Name: "event", Operator: "eq", Value: auditrecord.BillingCustomerDeletedEvent.String()},
 		{Name: "org_id", Operator: "eq", Value: orgID},
-	}, []rql.Sort{{Name: "occurred_at", Order: "desc"}}, nil))
-	if err != nil {
-		slog.WarnContext(ctx, "failed to check audit records for forfeited tokens", "org_id", orgID, "error", err)
-		return
+	}
+	var recovered []auditrecord.AuditRecord
+	for offset := 0; ; offset += forfeitRecordsLimit {
+		page, err := d.auditRecordService.List(ctx, utils.NewRQLQuery("", offset, forfeitRecordsLimit, filters, []rql.Sort{{Name: "occurred_at", Order: "desc"}}, nil))
+		if err != nil {
+			slog.WarnContext(ctx, "failed to check audit records for forfeited tokens", "org_id", orgID, "error", err)
+			return
+		}
+		recovered = append(recovered, page.AuditRecords...)
+		if len(recovered) >= int(page.Page.TotalCount) || len(page.AuditRecords) < forfeitRecordsLimit {
+			break
+		}
 	}
 	seen := map[string]struct{}{}
-	for _, record := range records.AuditRecords {
+	for _, record := range recovered {
 		accountID := record.Resource.ID
 		if accountID == "" {
 			continue

This branch was successfully deployed

1 active deployment
Preview — fc474d71 Deployed Oct 20, 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