[PM-36225] Upgrade Stripe SDK to 52.1.0 - #7643
Merged
sbrown-livefront merged 117 commits intoAug 14, 2026
Merged
Conversation
…h Source.Coupon path
|
…ade-stripe-sdk-to-51.1.0
…iews Stripe API version 2026-04-22.dahlia (pinned by Stripe.net 51.1.0) rejects billing_mode overrides on invoice previews for existing subscriptions and uses the subscription's own stored billing_mode instead — see the 2025-09-30.clover breaking change "Makes flexible billing mode the default for new subscriptions." The override was already semantically a no-op, so removing it just stops the request from failing under the newer API version. New-sub preview sites keep the Classic override so their tax/total match the CREATE calls that pin Classic explicitly. Adds two integration tests covering the previously uncovered new-sub preview paths (org purchase, premium purchase).
…and cap Stripe's 4-level expand cap plus the 2025-09-30.clover Discount.coupon → Discount.source.coupon wrap makes customer.discount.source.coupon.applies_to (and the equivalent phases.discounts.source.coupon.applies_to) unreachable in a single fetch — the path is 5 levels. Truncating at `.coupon` compiles and the request succeeds, but coupon.applies_to is expandable, not inline, so the returned Coupon comes back with AppliesTo = null. Every product- scoped customer- or phase-level coupon then silently misclassifies as cart-level in PartitionCouponsByScope and lands in the wrong slot of the cart response. GetRelevantCouponsAsync now collects coupon IDs from the customer, subscription, and phase-2 sources and refetches each unique ID with Expand = ["applies_to"] — rooted at Coupons.Retrieve, applies_to sits 1 level deep. StripePaymentService gets the same treatment via EnsureDiscountCouponAppliesToAsync and FetchCouponWithAppliesToAsync. Adds two integration tests: a direct-Stripe probe that codifies the expandable-not-inline rule, and an end-to-end cart-attribution test that seeds a product-scoped subscription coupon and asserts it lands on cart.passwordManager.seats.discount.
amorask-bitwarden
approved these changes
Aug 14, 2026
dani-garcia
approved these changes
Aug 14, 2026
JaredScar
approved these changes
Aug 14, 2026
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-36225
📔 Objective
Upgrades the
Stripe.netSDK to 52.1.0 and introduces updates across the billing codebase to support Stripe's new discount object structure and to explicitly set the billing mode to "classic" for all relevant Stripe API calls. The changes ensure compatibility with Stripe's evolving API and prevent potential issues with discount handling and subscription creation. The most important changes are grouped below:Stripe SDK Upgrade (51.1.0 → 52.1.0):
Stripe.netfrom 51.1.0 to 52.1.0 (single reference insrc/Core/Core.csproj; all other projects resolve it transitively). This moves the SDK's pinned Stripe API version from2026-04-22.dahliato2026-06-24.dahlia.StripeWebhookSecret20260624DahliatoBillingSettings, added the matching case toStripeController.PickStripeWebhookSecret, and removed the superseded2026-04-22.dahliawiring.SendStripeWebhookAsyncnow asserts the event was actuallyProcessed(a bare HTTP 200 masked silently-dropped events). This surfaced and fixed apayment_method.attachedtest that used the sharedpm_card_visatoken — re-fetched with a null customer and dropped as out-of-region — so it now attaches a real payment method.Stripe Discount Object Updates:
discount.Source.Couponinstead of the deprecateddiscount.Coupon, aligning with Stripe's new discount object structure. This affects discount application, validation, and coupon ID/percent retrieval across services, extensions, and tests.Explicit Billing Mode Configuration:
BillingModeproperty withType = StripeConstants.BillingMode.Classicto all Stripe subscription, invoice, and session creation options to ensure the correct billing mode is used and to prepare for future Stripe API requirements.Constants and Supporting Types:
StripeConstants.BillingModewithClassicandFlexibleoptions for consistent usage throughout the codebase.Stripe Object Null Property Settings
Updated
Expandparameters and coupon lookups to use the new discount structure in queries and commands.The SDK now pins Stripe API version
2026-06-24.dahliaand the webhook controller rejects any otherapi_version. Each environment must, at deploy time: (a) set its Stripe webhook endpoint to2026-06-24.dahlia, and (b) configureBillingSettings:StripeWebhookSecret20260624Dahlia. A mismatch fails silently (HTTP 200 withProcessed=false) — recommend alerting on that.Notes:
This needs to merge into a long-lived branch not main
Follow-up: test-account coupon hygiene
The shared QA Stripe test account has accumulated a lot of stale coupon state that we should follow up on:
StripeTestsFixture.CreateStripeCouponAsyncdeletes-first at setup but never removes coupons at teardown (DisposeAsync), so every run leaves coupons behind on the shared account. We should seed with randomized ids everywhere (never reserved prod ids likesm-standalone) and delete them on teardown.stripe coupons liston the QA account shows dozens of orphaned fixture coupons ("IT Customer Coupon", "Integration Test Churn Coupon", etc.) that should be purged.For reference, this surfaced while investigating PM-40438: the QA coupon
proactive-org-migration-cohort-test-20isduration=once/duration_in_months=null, soUpcomingInvoiceHandlerresolvesProactiveDiscountMonths=0and the loyalty copy is (correctly) suppressed. That's a test-data config issue rather than a code bug, but it's another symptom of the untended coupon state above.Paths Tested
Each row maps a path → the code site it exercises → the test. ✅ = covered by a real-Stripe integration test in
test/Billing.IntegrationTest/(opt-in viaRUN_STRIPE_INTEGRATION_TESTS). ⬜ = no integration coverage, reason noted (many ⬜ rows still name the unit test that guards the code site).Discount display
GetBitwardenSubscriptionQueryRetrievingOrganizationBillingTests.Subscription_WithCustomerLevelPercentOffCoupon_ReturnsDiscountPercentageGetBitwardenSubscriptionQueryRetrievingPremiumSubscriptionTests.Subscription_WithPlainPercentOffSubscriptionCoupon_AttributesDiscountToCartLevelGetBitwardenSubscriptionQueryRetrievingPremiumSubscriptionTests.Subscription_WithProductScopedSubscriptionCoupon_AttributesDiscountToSeatsCartItemGetBitwardenSubscriptionQuery(schedule phase-2)RetrievingPremiumSubscriptionTests.Subscription_WithProductScopedPhase2ScheduleCoupon_AttributesDiscountToSeatsCartItemGetOrganizationMetadataQueryRetrievingOrganizationBillingTests.Metadata_WithSecretsManagerStandaloneCoupon_ReportsIsOnSecretsManagerStandaloneStripePaymentService.HasSecretsManagerStandaloneProviderBillingController/ProviderSubscriptionResponseRetrievingProviderBillingTests.Subscription_WithCustomerLevelPercentOffCoupon_ReturnsDiscountPercentageSubscription creation (classic billing mode)
OrganizationBillingServiceRetrievingOrganizationBillingTests.Subscription_IsCreatedWithClassicBillingModeProviderBillingService.SetupSubscriptionRetrievingProviderBillingTests.Subscription_IsCreatedWithClassicBillingModeRemoveOrganizationFromProviderCommandPreExistingStateTests.RemoveOrganizationFromProvider_CreatesStandaloneSubscriptionWithClassicBillingModeCreatePremiumCloudHostedSubscriptionCommandRetrievingPremiumSubscriptionTests.Subscription_IsCreatedWithClassicBillingMode,CreatingPremiumForExistingCustomerTests.RecreatingPremium_AfterCancellation_HitsExistingCustomerBranchCreatePremiumCheckoutSessionCommandRestartSubscriptionCommandUpdatingOrganizationBillingTests.RestartSubscription_CreatesReplacementWithClassicBillingModeInvoice previews / tax
PreviewOrganizationTaxCommandSubscriptionPreviewTests.Purchase_ForTeamsAnnually_ReturnsTaxAndTotalPreviewOrganizationTaxCommandSubscriptionPreviewTests.PlanChange_ToTeamsAnnually_ReturnsTaxAndTotal,SubscriptionUpdate_WithAdditionalSeats_ReturnsTaxAndTotalPreviewOrganizationTaxCommandSubscriptionPreviewTests.PlanChange_WithCustomerLevelCoupon_AppliesCouponToPreviewTotalPreviewPremiumTaxCommandSubscriptionPreviewTests.PremiumPurchase_ReturnsTaxAndTotal(preview carries no token, so both are the same path)PreviewPremiumTaxCommandPreviewPremiumUpgradeProrationCommandUpdatingPremiumSubscriptionTests.UpgradePreview_ToFamiliesPlan_ReturnsTheProratedPricing,UpgradePreview_WithCustomerLevelCoupon_ReturnsProratedPricingWebhook handlers
SubscriptionUpdatedHandlerStripeWebhookTests.SubscriptionUpdated_WhenSecretsManagerTrialRemoved_RemovesTheCustomerLevelStandaloneCouponSubscriptionUpdatedHandlerStripeWebhookTests.SubscriptionUpdated_WhenSecretsManagerTrialRemoved_RemovesTheStandaloneCouponPercentOffProviderEventServiceStripeWebhookTests.InvoiceCreated_WithSubscriptionLevelPercentOffCoupon_RecordsDiscountedLineItemTotalsMetadata preservation
SubscriberService.CancelSubscriptionCancellingAndReinstatingSubscriptionTests.CancelPremium_PreservesSubscriptionMetadataUpdatePremiumStorageCommandUpdatingPremiumSubscriptionTests.Storage_WhenUpdated_PreservesSubscriptionMetadataDiscount deletion
RemoveOrganizationFromProviderCommandPreExistingStateTests.RemoveOrganizationFromProvider_WhenCustomerHasActiveDiscount_DeletesTheDiscountChurn mitigation offer
GetChurnMitigationOfferQueryMigrationCohortTests.ChurnMitigationOffer_WhenSubscriptionAlreadyHasCoupon_IsIneligibleGetChurnMitigationOfferQueryMigrationCohortTests.ChurnMitigationOffer_WhenCustomerAlreadyHasCoupon_IsIneligibleRedeemChurnMitigationOfferCommandMigrationCohortTests.RedeemChurnMitigationOffer_WithExistingSubscriptionDiscount_MergesInsteadOfNREing,RedeemChurnMitigationOffer_WithCustomerDiscount_PreservesItInMergedSetRedeemChurnMitigationOfferCommand.CouponId(not.Source.Coupon), unaffected by the SDK change; unit test is the better fitDiscount preserved through a schedule rebuild
When a subscriber has an active deferred price-migration schedule, several mutation commands rebuild the schedule's future phase and must carry the customer-level coupon into it (via the
customer.discount.source.coupon/discount.source.couponexpand). The premium and Families (personal-tier) paths are covered end-to-end below, because theirPriceIncreaseSchedulerbranch only needs the subscriber moved onto a legacy price. The business (Teams/Enterprise) paths exercise the same expand contract but their scheduler branch additionally requires a migration cohort + assignment, thePM35215flag, and the org on a legacy source plan — so rather than re-validate an identical contract through that scaffolding, they're guarded by unit tests asserting the expand is present. Each ⬜ row names that unit test.UpdatePremiumStorageCommandPreservingDiscountsThroughScheduleTests.StorageChange_WithActiveScheduleAndCustomerCoupon_PreservesCouponInFuturePhaseUpdateBillingAddressCommand(personal path)PreservingDiscountsThroughScheduleTests.PersonalAddressChange_WithActiveScheduleAndCustomerCoupon_PreservesCouponInFuturePhaseUpdateBillingAddressCommand(business path)UpdateBillingAddressCommandTests.Run_BusinessOrganization_FetchesCustomerWithDiscountSourceCouponExpandedUpdateOrganizationSubscriptionCommandUpdateOrganizationSubscriptionCommandTests.Run_FetchesSubscriptionWithCustomerDiscountSourceCouponExpandedUpdateSecretsManagerSubscriptionCommandUpdateSecretsManagerSubscriptionCommandTests.UpdateSubscriptionAsync_WithFeatureFlag_FetchesSubscriptionWithCustomerDiscountSourceCouponExpanded