[PM-36613] Void open invoices for unpaid subscriptions#7589
Open
amorask-bitwarden wants to merge 2 commits intomainfrom
Open
[PM-36613] Void open invoices for unpaid subscriptions#7589amorask-bitwarden wants to merge 2 commits intomainfrom
amorask-bitwarden wants to merge 2 commits intomainfrom
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7589 +/- ##
=======================================
Coverage 59.75% 59.75%
=======================================
Files 2102 2101 -1
Lines 92722 92716 -6
Branches 8261 8256 -5
=======================================
- Hits 55409 55406 -3
+ Misses 35355 35353 -2
+ Partials 1958 1957 -1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-36613
📔 Objective
Restores the void-open-invoices behavior on canceled Stripe subscriptions that was lost in #6918 (PM-31140). Since
v2026.2.1shipped on 2026-03-03, invoices on subscriptions whose payment fails and lapse tounpaidhave been stuck inopenstatus indefinitely — Stripe does not auto-void them when the subscription cancels.The fix adds
VoidOpenInvoicesAsyncandTryVoidInvoiceAsyncprivate helpers toSubscriptionUpdatedHandlerand calls the helper from the existingSubscriptionWentUnpaid || SubscriptionWentIncompleteExpiredbranch, afterDisableSubscriberAsyncandSetSubscriptionToCancelAsync. The orphanedSubscriptionCancellationJob(dead code since PM-31140 — nothing scheduled it anymore), its test class, and its DI registration are deleted in the same commit.Voiding scope is intentionally narrow. Only cancellations driven through the platform-managed unpaid lifecycle void their open invoices. Cancellations that arrive through other paths — voluntary user cancel, off-platform negotiated cancel, provider migration — leave open invoices intact. Per ops: those invoices are intentional artifacts that need to be preserved for manual reconciliation through other channels. Voiding indiscriminately on
customer.subscription.deleted(the first iteration of this PR) would have erased that surface.The cleanup is best-effort:
ListInvoicesAsynclogs atErrorand returns. The disable + cancel_at calls that already ran are not rolled back.VoidInvoiceAsyncfailure is logged and the loop continues to the next invoice.StripeException(the expected webhook re-delivery race against an already-voided invoice) logs atWarning; any other exception (transport-level failures likeHttpRequestExceptionorTaskCanceledException) logs atError.Pagination is handled by
IStripeAdapter.ListInvoicesAsyncwithSelectAll = true(auto-paginating via Stripe SDK'sListAutoPagingAsync) — no manual cursor management on our side.Out of scope: Backfill of invoices that went
openbetween v2026.2.1 deploy and this fix's deploy. Those will not be retroactively voided. Ops decided against a polling background job for backfill; finance/ops can resolve historical leaked invoices manually if/when surfaced.Test coverage
SubscriptionUpdatedHandlerTestsadds:StripeException(webhook re-delivery against already-voided invoice) → loop continues.HttpRequestException) → loop continues.ListInvoicesAsyncfailure → does not block subscriber-disable.Active → Activequantity change) → no list, no void calls. Locks in the narrow-scoping decision.One existing test (
HandleAsync_UnpaidUserSubscription_DisablesPremiumAndSetsCancellation) had itsDidNotReceive(ListInvoicesAsync)assertion flipped toReceived(1)with the expected options — that assertion was locking in the post-PM-31140 absence we are now reversing.A default
_stripeAdapter.ListInvoicesAsync(...).Returns(empty list)mock was added to the test constructor so existing Unpaid-path tests don't NRE through the new void loop.📸 Screenshots
N/A — no UI changes.