Skip to content

[PM-36225] Upgrade Stripe SDK to 52.1.0 - #7643

Merged
sbrown-livefront merged 117 commits into
mainfrom
billing/pm-36225/upgrade-stripe-sdk-to-51.1.0
Aug 14, 2026
Merged

[PM-36225] Upgrade Stripe SDK to 52.1.0#7643
sbrown-livefront merged 117 commits into
mainfrom
billing/pm-36225/upgrade-stripe-sdk-to-51.1.0

Conversation

@sbrown-livefront

@sbrown-livefront sbrown-livefront commented May 15, 2026

Copy link
Copy Markdown
Collaborator

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-36225

📔 Objective

Upgrades the Stripe.net SDK 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):

  • Bumped Stripe.net from 51.1.0 to 52.1.0 (single reference in src/Core/Core.csproj; all other projects resolve it transitively). This moves the SDK's pinned Stripe API version from 2026-04-22.dahlia to 2026-06-24.dahlia.
  • Registered the new pinned version end-to-end so webhooks aren't silently dropped: added StripeWebhookSecret20260624Dahlia to BillingSettings, added the matching case to StripeController.PickStripeWebhookSecret, and removed the superseded 2026-04-22.dahlia wiring.
  • Hardened the webhook integration tests: SendStripeWebhookAsync now asserts the event was actually Processed (a bare HTTP 200 masked silently-dropped events). This surfaced and fixed a payment_method.attached test that used the shared pm_card_visa token — re-fetched with a null customer and dropped as out-of-region — so it now attaches a real payment method.

Stripe Discount Object Updates:

  • Updated all references to access coupons via discount.Source.Coupon instead of the deprecated discount.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:

  • Added the BillingMode property with Type = StripeConstants.BillingMode.Classic to 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:

  • Introduced StripeConstants.BillingMode with Classic and Flexible options for consistent usage throughout the codebase.

Stripe Object Null Property Settings

  • Double-checked that no flows set properties to null that should not be cleared on updates.

Updated Expand parameters and coupon lookups to use the new discount structure in queries and commands.

⚠️ Deploy note

The SDK now pins Stripe API version 2026-06-24.dahlia and the webhook controller rejects any other api_version. Each environment must, at deploy time: (a) set its Stripe webhook endpoint to 2026-06-24.dahlia, and (b) configure BillingSettings:StripeWebhookSecret20260624Dahlia. A mismatch fails silently (HTTP 200 with Processed=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:

  • Make coupon seeding less invasive. StripeTestsFixture.CreateStripeCouponAsync deletes-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 like sm-standalone) and delete them on teardown.
  • Clean up the accumulated test coupons. stripe coupons list on 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-20 is duration=once / duration_in_months=null, so UpcomingInvoiceHandler resolves ProactiveDiscountMonths=0 and 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 via RUN_STRIPE_INTEGRATION_TESTS). ⬜ = no integration coverage, reason noted (many ⬜ rows still name the unit test that guards the code site).

Discount display

Path Code site Test
Org sub — customer-level percent-off GetBitwardenSubscriptionQuery RetrievingOrganizationBillingTests.Subscription_WithCustomerLevelPercentOffCoupon_ReturnsDiscountPercentage
Premium — plain percent-off (cart level) GetBitwardenSubscriptionQuery RetrievingPremiumSubscriptionTests.Subscription_WithPlainPercentOffSubscriptionCoupon_AttributesDiscountToCartLevel
Premium — product-scoped coupon under line item GetBitwardenSubscriptionQuery RetrievingPremiumSubscriptionTests.Subscription_WithProductScopedSubscriptionCoupon_AttributesDiscountToSeatsCartItem
Premium — phase-2 schedule coupon still displays GetBitwardenSubscriptionQuery (schedule phase-2) RetrievingPremiumSubscriptionTests.Subscription_WithProductScopedPhase2ScheduleCoupon_AttributesDiscountToSeatsCartItem
SM-standalone detection — billing-page/metadata GetOrganizationMetadataQuery RetrievingOrganizationBillingTests.Metadata_WithSecretsManagerStandaloneCoupon_ReportsIsOnSecretsManagerStandalone
SM-standalone detection — invite/import gating StripePaymentService.HasSecretsManagerStandalone ⬜ unit-tested only (driven by AdminConsole invite/import flows, out of this suite's scope)
Provider sub — customer-level percent-off ProviderBillingController / ProviderSubscriptionResponse RetrievingProviderBillingTests.Subscription_WithCustomerLevelPercentOffCoupon_ReturnsDiscountPercentage

Subscription creation (classic billing mode)

Path Code site Test
New org subscription OrganizationBillingService RetrievingOrganizationBillingTests.Subscription_IsCreatedWithClassicBillingMode
New provider subscription ProviderBillingService.SetupSubscription RetrievingProviderBillingTests.Subscription_IsCreatedWithClassicBillingMode
Remove org from provider → standalone sub RemoveOrganizationFromProviderCommand PreExistingStateTests.RemoveOrganizationFromProvider_CreatesStandaloneSubscriptionWithClassicBillingMode
Premium cloud-hosted creation CreatePremiumCloudHostedSubscriptionCommand RetrievingPremiumSubscriptionTests.Subscription_IsCreatedWithClassicBillingMode, CreatingPremiumForExistingCustomerTests.RecreatingPremium_AfterCancellation_HitsExistingCustomerBranch
Premium checkout session CreatePremiumCheckoutSessionCommand ⬜ out of scope — a hosted Checkout Session can't be completed in an integration test
Restart a canceled subscription RestartSubscriptionCommand UpdatingOrganizationBillingTests.RestartSubscription_CreatesReplacementWithClassicBillingMode

Invoice previews / tax

Path Code site Test
Org preview — new org, no sub PreviewOrganizationTaxCommand SubscriptionPreviewTests.Purchase_ForTeamsAnnually_ReturnsTaxAndTotal
Org preview — existing sub, no coupon PreviewOrganizationTaxCommand SubscriptionPreviewTests.PlanChange_ToTeamsAnnually_ReturnsTaxAndTotal, SubscriptionUpdate_WithAdditionalSeats_ReturnsTaxAndTotal
Org preview — existing sub, customer coupon PreviewOrganizationTaxCommand SubscriptionPreviewTests.PlanChange_WithCustomerLevelCoupon_AppliesCouponToPreviewTotal
Premium tax preview (with/without PM, no sub) PreviewPremiumTaxCommand SubscriptionPreviewTests.PremiumPurchase_ReturnsTaxAndTotal (preview carries no token, so both are the same path)
Premium tax preview — existing sub PreviewPremiumTaxCommand ⬜ not covered — near-duplicate, low value
Premium upgrade proration preview PreviewPremiumUpgradeProrationCommand UpdatingPremiumSubscriptionTests.UpgradePreview_ToFamiliesPlan_ReturnsTheProratedPricing, UpgradePreview_WithCustomerLevelCoupon_ReturnsProratedPricing

Webhook handlers

Path Code site Test
SM trial removed — customer-level discount SubscriptionUpdatedHandler StripeWebhookTests.SubscriptionUpdated_WhenSecretsManagerTrialRemoved_RemovesTheCustomerLevelStandaloneCoupon
SM trial removed — subscription-level discount SubscriptionUpdatedHandler StripeWebhookTests.SubscriptionUpdated_WhenSecretsManagerTrialRemoved_RemovesTheStandaloneCoupon
Discounted provider invoice → correct PercentOff ProviderEventService StripeWebhookTests.InvoiceCreated_WithSubscriptionLevelPercentOffCoupon_RecordsDiscountedLineItemTotals

Metadata preservation

Path Code site Test
Cancel sub — existing metadata survives SubscriberService.CancelSubscription CancellingAndReinstatingSubscriptionTests.CancelPremium_PreservesSubscriptionMetadata
Storage update — existing metadata not cleared UpdatePremiumStorageCommand UpdatingPremiumSubscriptionTests.Storage_WhenUpdated_PreservesSubscriptionMetadata

Discount deletion

Path Code site Test
Remove org from provider w/ active discount → deleted RemoveOrganizationFromProviderCommand PreExistingStateTests.RemoveOrganizationFromProvider_WhenCustomerHasActiveDiscount_DeletesTheDiscount

Churn mitigation offer

Path Code site Test
Offer ineligible — coupon on sub-level discount GetChurnMitigationOfferQuery MigrationCohortTests.ChurnMitigationOffer_WhenSubscriptionAlreadyHasCoupon_IsIneligible
Offer ineligible — coupon on customer-level discount GetChurnMitigationOfferQuery MigrationCohortTests.ChurnMitigationOffer_WhenCustomerAlreadyHasCoupon_IsIneligible
Redeem — merge preserves existing discounts RedeemChurnMitigationOfferCommand MigrationCohortTests.RedeemChurnMitigationOffer_WithExistingSubscriptionDiscount_MergesInsteadOfNREing, RedeemChurnMitigationOffer_WithCustomerDiscount_PreservesItInMergedSet
Redeem — migration cohort phase-2 append RedeemChurnMitigationOfferCommand ⬜ not covered — phase-2 discounts use .CouponId (not .Source.Coupon), unaffected by the SDK change; unit test is the better fit

Discount 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.coupon expand). The premium and Families (personal-tier) paths are covered end-to-end below, because their PriceIncreaseScheduler branch 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, the PM35215 flag, 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.

Path Code site Test
Storage change w/ active schedule → coupon survives into phase 2 UpdatePremiumStorageCommand PreservingDiscountsThroughScheduleTests.StorageChange_WithActiveScheduleAndCustomerCoupon_PreservesCouponInFuturePhase
Personal (Families) address change w/ active schedule → coupon survives UpdateBillingAddressCommand (personal path) PreservingDiscountsThroughScheduleTests.PersonalAddressChange_WithActiveScheduleAndCustomerCoupon_PreservesCouponInFuturePhase
Business (Teams/Enterprise) address change w/ active schedule UpdateBillingAddressCommand (business path) ⬜ unit-tested — UpdateBillingAddressCommandTests.Run_BusinessOrganization_FetchesCustomerWithDiscountSourceCouponExpanded
Org subscription change (seats/plan) w/ active schedule UpdateOrganizationSubscriptionCommand ⬜ unit-tested — UpdateOrganizationSubscriptionCommandTests.Run_FetchesSubscriptionWithCustomerDiscountSourceCouponExpanded
SM seat change w/ active schedule (reused-subscription path) UpdateSecretsManagerSubscriptionCommand ⬜ unit-tested — UpdateSecretsManagerSubscriptionCommandTests.UpdateSubscriptionAsync_WithFeatureFlag_FetchesSubscriptionWithCustomerDiscountSourceCouponExpanded

@sbrown-livefront sbrown-livefront changed the title Billing/pm 36225/upgrade stripe sdk to 51.1.0 [PM-36225] Upgrade Stripe SDK to 51.1.0 May 15, 2026
@codecov

codecov Bot commented May 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.52941% with 63 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.53%. Comparing base (6fbf0f2) to head (0d44704).

Files with missing lines Patch % Lines
...UpgradeOffer/Queries/GetAnnualUpgradeOfferQuery.cs 20.00% 13 Missing and 3 partials ⚠️
...g/Services/Implementations/StripePaymentService.cs 65.78% 8 Missing and 5 partials ⚠️
...criptions/Queries/GetBitwardenSubscriptionQuery.cs 80.55% 6 Missing and 1 partial ⚠️
src/Core/Models/Business/SubscriptionInfo.cs 0.00% 0 Missing and 5 partials ⚠️
...nualUpgradeOffer/AnnualUpgradeSavingsCalculator.cs 57.14% 0 Missing and 3 partials ⚠️
...g/Services/Implementations/ProviderEventService.cs 33.33% 0 Missing and 2 partials ⚠️
src/Core/Billing/Extensions/DiscountExtensions.cs 0.00% 0 Missing and 2 partials ⚠️
...tion/Commands/RedeemChurnMitigationOfferCommand.cs 86.66% 0 Missing and 2 partials ⚠️
...nMigration/Queries/GetChurnMitigationOfferQuery.cs 66.66% 0 Missing and 2 partials ⚠️
...Services/BusinessPlanRenewalNotificationService.cs 33.33% 1 Missing and 1 partial ⚠️
... and 9 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7643      +/-   ##
==========================================
+ Coverage   63.22%   68.53%   +5.31%     
==========================================
  Files        2381     2381              
  Lines      103757   103892     +135     
  Branches     9385     9404      +19     
==========================================
+ Hits        65604    71207    +5603     
+ Misses      35924    30338    -5586     
- Partials     2229     2347     +118     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sonarqubecloud

Copy link
Copy Markdown

…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.
Comment thread src/Core/Billing/Pricing/PriceIncreaseScheduler.cs
Comment thread test/Billing.IntegrationTest/BillingApplicationFactory.cs Outdated
Comment thread src/Billing/BillingSettings.cs
@amorask-bitwarden amorask-bitwarden changed the title [DNM][PM-36225] Upgrade Stripe SDK to 52.1.0 [PM-36225] Upgrade Stripe SDK to 52.1.0 Aug 14, 2026
@sbrown-livefront
sbrown-livefront merged commit 91c1aa0 into main Aug 14, 2026
55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:breaking-change Change Type - Breaking Change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants