Skip to content

improvement(billing): move overage calculations out of txes #4595

Merged
icecrasher321 merged 5 commits into
stagingfrom
improvement/billing-txes
May 14, 2026
Merged

improvement(billing): move overage calculations out of txes #4595
icecrasher321 merged 5 commits into
stagingfrom
improvement/billing-txes

Conversation

@icecrasher321
Copy link
Copy Markdown
Collaborator

Summary

Will catch missed overages in next check.

Type of Change

  • Other: Performance Improvement

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 14, 2026 6:44am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 14, 2026

PR Summary

Medium Risk
Changes threshold overage billing and invoice usage resets to use new locking/snapshot logic, which can affect when/if invoices are enqueued and how credits/trackers are updated under concurrency. While largely a performance/concurrency improvement, it touches billing-critical flows (Stripe invoicing + credit deduction) and relies on new retry/skip behavior when usage changes mid-flight.

Overview
Refactors checkAndBillOverageThreshold (personal + org) to compute overage and validate Stripe identifiers before opening DB transactions, reducing time spent holding FOR UPDATE locks.

Adds usage snapshot + recheck logic (with a lock timeout) so threshold billing skips and retries later if relevant usage inputs, membership/owner, or billed trackers change while locked—preventing stale calculations from enqueueing invoices.

Updates org-scoped resetUsageForSubscription and invoice-finalization handling to coordinate locks (org owner/tracker + organization rows) and to perform bulk member stat resets in a single transaction; expands tests to cover these new concurrency/locking guarantees, including a new threshold-billing.test.ts suite.

Reviewed by Cursor Bugbot for commit 390eef5. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 14, 2026

Greptile Summary

This PR moves the expensive overage calculations (calculateSubscriptionOverage, computeOrgOverageAmount) outside the database transaction so that lock-hold time is minimized. A pre-transaction snapshot is captured, the overage is computed, and a short locked transaction then validates the snapshot is still current before writing — aborting if inputs changed, to be caught on the next check cycle.

  • threshold-billing.ts: Pre-tx snapshot + overage calculation added before opening the critical section; inside the transaction only a snapshot-equality check, credit application, and tracker bump remain. Lock order (member → userStats → org) is now uniform across both personal and org paths.
  • invoices.ts: resetUsageForSubscription org-path wrapped in a transaction with lock_timeout and consistent locking; bulk UPDATE via per-user CASE expressions replaces the per-member loop. Owner resolution in handleInvoiceFinalized moved inside the transaction.
  • membership.ts: Departed-usage capture refactored into a captureDepartedUsage closure with explicit FOR UPDATE locks, but acquires the org lock before the departing user's userStats — reversing the lock order used everywhere else and creating a deadlock risk when the org owner is removed concurrently with billing (see inline comment).

Confidence Score: 4/5

Safe to merge after fixing the lock-ordering inversion in captureDepartedUsage; all other changes are well-structured.

The billing and invoice paths are carefully reworked with consistent lock ordering and timeout guards. The one concrete defect is in membership.ts: captureDepartedUsage locks the org row before the departing user's userStats row, while threshold billing and resetUsageForSubscription lock userStats before org. When the org owner is removed while threshold billing is running, Postgres will detect the AB/BA cycle and abort one of the transactions.

apps/sim/lib/billing/organizations/membership.ts — lock ordering in captureDepartedUsage needs to be inverted (userStats first, then org) to match the ordering used in threshold-billing.ts and invoices.ts.

Important Files Changed

Filename Overview
apps/sim/lib/billing/organizations/membership.ts Refactored departed-usage capture into a captureDepartedUsage closure with explicit FOR UPDATE locks, but acquires the org lock before the userStats lock — opposite of the order used in threshold billing and usage reset, creating a deadlock when the org owner is removed concurrently with billing.
apps/sim/lib/billing/threshold-billing.ts Major refactor: moves calculateSubscriptionOverage / computeOrgOverageAmount outside the transaction with pre-tx + locked snapshot comparison to detect stale reads, significantly shrinking the critical section. Lock order (member → userStats → org) is consistent and correct.
apps/sim/lib/billing/webhooks/invoices.ts Wraps resetUsageForSubscription org-path in a transaction with lock_timeout and consistent lock order (member → owner stats → org); switches to a single bulk UPDATE with per-user CASE expressions. Moves owner resolution inside the handleInvoiceFinalized transaction. All lock orderings are consistent.
apps/sim/lib/billing/threshold-billing.test.ts New test file with comprehensive coverage of the pre-tx snapshot / locked-snapshot staleness detection for both personal and org billing paths.
apps/sim/lib/billing/webhooks/invoices.test.ts Adds a test that verifies resetUsageForSubscription acquires owner-tracker and org locks and emits the correct CASE/GREATEST SQL for the bulk stats reset.
apps/sim/lib/billing/constants.ts Adds BILLING_LOCK_TIMEOUT_MS = 5_000 constant shared across threshold billing and invoice handlers.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant ThresholdBilling
    participant DB

    Caller->>ThresholdBilling: checkAndBillOverageThreshold(userId)
    ThresholdBilling->>DB: SELECT userStats snapshot (no lock)
    ThresholdBilling->>DB: calculateSubscriptionOverage() [outside tx]
    alt "overage < threshold"
        ThresholdBilling-->>Caller: return (no tx opened)
    else "overage >= threshold"
        ThresholdBilling->>DB: "BEGIN tx + SET LOCAL lock_timeout=5s"
        ThresholdBilling->>DB: SELECT member FOR UPDATE (owner)
        ThresholdBilling->>DB: SELECT userStats FOR UPDATE (owner)
        ThresholdBilling->>DB: SELECT organization FOR UPDATE
        ThresholdBilling->>DB: SELECT member+userStats+org (locked read)
        alt snapshot changed
            ThresholdBilling-->>Caller: return (retry next cycle)
        else snapshot stable
            ThresholdBilling->>DB: UPDATE userStats billedOverageThisPeriod
            ThresholdBilling->>DB: enqueueOutboxEvent (Stripe invoice)
            ThresholdBilling->>DB: COMMIT
        end
    end
Loading

Reviews (3): Last reviewed commit: "share timeout const" | Re-trigger Greptile

Comment thread apps/sim/lib/billing/threshold-billing.ts Outdated
Comment thread apps/sim/lib/billing/threshold-billing.ts
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/lib/billing/webhooks/invoices.ts
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/lib/billing/organizations/membership.ts
@icecrasher321 icecrasher321 merged commit b1a9443 into staging May 14, 2026
13 checks passed
@icecrasher321 icecrasher321 deleted the improvement/billing-txes branch May 14, 2026 06:52
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.

1 participant