Skip to content

Fix billing downgrade recharge and suspension race#2397

Draft
niemyjski wants to merge 7 commits into
mainfrom
issue/billing-downgrade-race
Draft

Fix billing downgrade recharge and suspension race#2397
niemyjski wants to merge 7 commits into
mainfrom
issue/billing-downgrade-race

Conversation

@niemyjski

@niemyjski niemyjski commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Prorate and finalize paid-to-free cancellations so unused yearly time becomes customer credit before a later re-upgrade.
  • Serialize billing requests and subscription webhooks per organization, correlate persisted subscription and operation IDs, and make provider mutations idempotent.
  • Prevent stale subscription events from suspending a free or newly paid organization, while fail-closed reconciliation cancels any authoritative paid subscription still live after a settled Free transition.
  • Persist and retry incomplete billing operations and PlanChanged publication without repeating provider mutations.
  • Preserve the original public billing constructors and safely fall back for custom clients that implement only the original IStripeBillingClient surface.
  • Add locally signed endpoint, ordering, concurrency, response-loss, compatibility, persistence, and replay coverage.

Root cause

Paid-to-free changes immediately canceled the active subscription without proration. A later upgrade therefore created a new full-price subscription without credit for unused time. Asynchronous deletion/update webhooks could also overwrite the free or newly paid state because billing writes were neither serialized nor correlated to the controlling subscription and operation.

Verification

  • dotnet build tests/Exceptionless.Tests/Exceptionless.Tests.csproj --no-restore --disable-build-servers -m:1 — 0 warnings, 0 errors
  • BillingLifecycleEndpointTests — 143/143 passed
  • Focused billing dependency/endpoints/persistence/serialization/compatibility matrix — 251/251 passed
  • Full backend suite — 2,724 passed, 0 failed, 2 intentional performance-data skips
  • Svelte unit suite — 323/323 passed
  • Svelte production build — passed
  • Focused line coverage: webhook processor 85.5%, subscription workflow 90.2%, plan-change service 91.8%, operation policy 94.0%, billing-state policy 100%
  • Final-commit local Aspire dogfood (101b6d236) — API health 200 and locally HMAC-signed livemode:false webhook 200
  • Dogfood trace contained only localhost API, Redis, and Elasticsearch spans; api.stripe.com telemetry search returned no spans
  • Independent thermo-nuclear review — no remaining P0-P2 race, compatibility, replay, lock-cleanup, or code-quality findings
  • Local-only Stripe boundary — Stripe API/publishable keys explicitly blank; no Stripe API/dashboard, Stripe.js, real/test keys, or production URLs used

Breaking changes

None. The original BillingManager and StripeEventHandler public constructors remain available.

@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.Core 75% 67% 10230
Exceptionless.AppHost 38% 40% 139
Exceptionless.Insulation 23% 23% 205
Exceptionless.Web 85% 68% 7019
Summary 78% (24355 / 31336) 67% (11075 / 16654) 17593

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