[PM-38422] feat: Stack customer-level discount with churn and migration discounts#7831
Conversation
…on discounts Subscription- and schedule-phase-level discounts override a customer-level discount in Stripe, so applying a churn-mitigation, proactive, or milestone-migration coupon silently dropped a pre-existing customer.discount from billing. A shared DiscountExtensions.MergeDiscountCouponIds extension now carries the customer coupon into the subscription/phase discounts array so the two stack (deduped, customer coupon first). Unblocks migration cohorts that include customers with existing discounts.
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed this billing change that stacks a pre-existing customer-level discount ( Code Review DetailsNo findings. The change is well-structured with comprehensive unit coverage:
Null-safety ( |
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7831 +/- ##
==========================================
+ Coverage 61.22% 61.23% +0.01%
==========================================
Files 2209 2209
Lines 97716 97723 +7
Branches 8815 8818 +3
==========================================
+ Hits 59824 59841 +17
+ Misses 35768 35760 -8
+ Partials 2124 2122 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
kdenney
left a comment
There was a problem hiding this comment.
🔥 Very clean design; nice!



🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-38422
📔 Objective
When a churn-mitigation, proactive, or milestone-migration coupon is applied to a subscription that already carries a customer-level discount (
customer.discount), the new coupon was written into the subscription's (or schedule phase's)discountsarray on its own. Because Stripe lets a subscription/phase-level discount override the customer-level one, the pre-existing customer discount was silently dropped from billing.This adds a shared
DiscountExtensions.MergeDiscountCouponIdsextension (customer coupon first, deduped by id withStringComparison.Ordinal, order-preserving) that merges the customer-level discount with the existing subscription/phase discounts and the newly applied coupon so they stack. It is applied wherever adiscountsarray is written and a pre-existing customer discount could be shadowed:UpcomingInvoiceHandlerautomatic-tax rebuild — future phases only (active phases are mirrored verbatim so a discount is not re-applied on the already-billed period)GetBitwardenSubscriptionQuerydedups the carried coupon so the tax preview does not double-count itThe customer coupon is carried regardless of
Coupon.Valid(an activecustomer.discountis one Stripe is already applying). Unblocks migration cohorts that include customers with pre-existing discounts. Unit tests cover the helper, the stacking/repro/self-heal cases, and the active-vs-future-phase boundary at each site.