feat(billing): grant prorated Deploy credit top-ups on mid-cycle upgrades#6422
Conversation
d2d8a6a to
c560a00
Compare
f8babdd to
6a9b755
Compare
c560a00 to
4fe1d8e
Compare
6a9b755 to
79f2063
Compare
4fe1d8e to
263f020
Compare
7aed52a to
f2dd6dd
Compare
f3d73b1 to
90233a8
Compare
f2dd6dd to
edd63bc
Compare
90233a8 to
7a4870f
Compare
edd63bc to
8cb9617
Compare
7a4870f to
5b7f425
Compare
8cb9617 to
b14c61b
Compare
5b7f425 to
4769cb5
Compare
b14c61b to
08d6847
Compare
4769cb5 to
995a78c
Compare
08d6847 to
ad349bc
Compare
995a78c to
d6bb1e4
Compare
There was a problem hiding this comment.
ℹ️ No critical issues — two minor labeling/test nits inline.
Reviewed changes — mid-cycle Deploy plan changes now grant the correct usage credits by reusing the existing invoice-paid credit-grant webhook, with netDeployFee extracted as a pure, unit-tested function.
- Immediate prorated charge on upgrades —
changeDeployPlan.tsswitchesproration_behaviortoalways_invoicefor upgrades andnonefor downgrades, deriving direction fromDEPLOY_PLANS.indexOf(the equal-plan case returns earlier, so<is sufficient). - Self-correcting credit top-up —
netDeployFeesums the invoice's Deploy plan-fee lines; the upgrade proration pair (+new fee,-unused old fee) nets to exactly the top-up, a downgrade nets negative (no grant, no clawback), and a renewal grants the full fee. - Refactor of
grantDeployCreditsForInvoice— fee summing, period-end, and plan resolution now flow throughnetDeployFee; behavior preserved. - Unit tests —
deployCredits.test.tscovers subscribe/renewal, upgrade netting, downgrade netting, unrelated lines, latest-period-end, and missing price details. - Modal copy —
deploy-product-card.tsxfootnote and downgrade overage warning now describe the immediate-charge / keep-current-credits semantics.
I verified the load-bearing Stripe assumptions against current docs: always_invoice creates and collects a proration invoice synchronously, proration lines carry pricing.price_details.price and a period.end equal to the subscription period end (so the top-up expires with the current period), error_if_incomplete throws synchronously on payment failure, and invoice.payment_succeeded fires for the proration invoice. All correct. The money paths net correctly and the grant is idempotent per invoice; the only findings are cosmetic.
Claude Opus | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the delta since the prior pullfrog review (3d91734) cleanly addresses both earlier nits via the netDeployFee extraction and added test coverage; the money paths are unchanged and remain correct.
- Plan labeled from the charge line —
netDeployFeenow derivesplanfrom the largest-amount fee line (chargeLineviareduce), so an upgrade's credit name anddeploy_planmetadata reflect the plan the customer ends up on regardless of Stripe's proration line ordering. - Test coverage for the previously-uncovered paths — split the old test into
"ignores metered lines"plus a genuine null-pricingcase ("ignores lines without price details"), added"labels the plan from the charge line regardless of proration order", and added adiscount_amountscoupon test. - Behavior-preserving refactor —
grantDeployCreditsForInvoicenow delegates fee summing, period-end (Math.max), and plan resolution to the purenetDeployFee; the net amount, discount subtraction, and expiry logic are identical to the inlined version.
The chargeLine heuristic is a labeling-only concern — amountCents is still summed independently over all fee lines, so the net top-up math is unaffected by which line wins the reduce. Both prior review threads have been resolved.
Claude Opus | 𝕏
…ades Included usage credits are granted once per period, off the plan fee paid at the start of it. A mid-cycle upgrade charges more fee right away, but before this change the credit allowance stayed flat until the next renewal: the customer paid the higher fee immediately and still hit overage the upgrade was meant to cover. An upgrade has to buy more headroom now, not next month. Rather than add a separate top-up path, we lean on the credit-grant webhook from eng-2870. changeDeployPlan reprices the plan-fee item with always_invoice, so the upgrade produces a proration invoice immediately. That invoice carries the pair (+new prorated fee, -unused old fee), and the webhook's net-fee summing collapses it to exactly the difference, granted as credits that expire with the current period. Summing the invoice lines instead of reading the catalog price is what makes this work with no new logic: the same code that grants on subscribe and renewal grants the right top-up on upgrade. Downgrades are deliberately the opposite: proration_behavior none, so no refund of the fee difference and no clawback of the credits that fee already granted. The alternative, refunding the downgrade, would force us to partially revoke an existing credit grant to stay consistent, and Stripe credit grants only void in full. Keeping the period as bought sidesteps that entirely, and it is the fairer default: the customer rides out the period at the level they paid for, and the lower fee with its smaller credits starts at the next renewal. Direction is derived from DEPLOY_PLANS order (lowest to highest), so there is no separate upgrade flag to keep in sync. Supporting changes: - netDeployFee extracted as a pure function with unit tests covering subscribe/renewal, upgrade netting, downgrade netting, unrelated lines, multi-line period ends, and both webhook payload shapes. - Compute plan modal: footnote describes the immediate-charge / keep-credits semantics, and the downgrade overage warning now says the current period keeps its credits. ENG-2871

Upgrades: changeDeployPlan uses always_invoice, so a mid-cycle upgrade
charges the prorated fee difference immediately. Paying that invoice triggers
the existing credit grant webhook, whose net-fee summing turns the proration
pair (+new prorated fee, -unused old fee) into exactly the right top-up,
expiring with the current period. No new grant logic needed.
Downgrades keep the period as bought (proration_behavior none): no refund of
the fee difference AND no clawback of the usage credits that fee already
granted. The customer rides out the period at the level they paid for; the
lower fee and its smaller credits start at the next renewal. Direction is
derived from DEPLOY_PLANS order (lowest to highest).
Supporting changes:
subscribe/renewal, upgrade netting, downgrade netting, unrelated lines,
multi-line period ends, and both webhook payload shapes.
semantics, and the downgrade overage warning now says the current period
keeps its credits.
ENG-2871