fix(subscriptions): update existing charge on overridden plan in place (#5549)#5557
Open
nlenepveu wants to merge 1 commit into
Conversation
When a subscription is updated through `PUT /subscriptions/{external_id}
/charges/{code}` and its overridden plan already carries a charge for
the same billable metric — e.g. one created earlier by
`Plans::UpdateService#process_charges` via the legacy `plan_overrides`
PATCH path, which sits directly on the overridden plan with
`parent_id: nil` — `UpdateOrOverrideChargeService` would walk up to
`find_parent_charge`, fail to find an existing override with that
parent, and create a second override on top. The overridden plan
ended up holding two charges with the same `billable_metric_id` and
`code`, both contributing fees over the same events.
Short-circuit when the resolved charge already lives on the overridden
plan: update it in place instead of creating a new override. Existing
behaviour is preserved in the other branches:
- Fresh subscription on the base plan: charge.plan_id != target_plan.id,
the short-circuit doesn't fire, `ensure_plan_override` populates the
overridden plan via `Charges::OverrideService`, and the existing
`parent_id` lookup finds and updates the freshly-created override.
- Subscription whose overridden plan already carries a PUT-managed
override (`parent_id` pointing at a base-plan charge): the short-
circuit now updates that charge directly — same effective behaviour
as before, different code path.
Closes getlago#5549.
Welcome, @nlenepveu!Thanks for your first contribution! Before we proceed with the review, please sign the Fiduciary License Agreement: Once signed, this PR will be automatically updated. |
Thanks, @nlenepveu! 🎉Your CLA has been signed and is now on file. We'll proceed with the review shortly. |
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.
Context
Closes #5549.
When a subscription has been updated through the legacy
PATCH /subscriptions/{external_id}plan_overridespath and is later updated through the new per-chargePUT /subscriptions/{external_id}/charges/{code}endpoint, the overridden plan ends up holding two charges with the samebillable_metric_idandcode. Both are enumerated by the canonical fee-creation path (app/services/invoices/calculate_fees_service.rb#create_charges_fees) with no de-duplication, so each event produces twoFeerows. The full source trace and a reproduction sequence onapi.eu.getlago.comare in #5549.Implements option 2 from the issue, per @rsempe's suggestion:
Change
In
Subscriptions::UpdateOrOverrideChargeService#find_or_update_charge_override(target_plan), short-circuit when the resolved charge already lives on the overridden plan — update it in place rather than walking up tofind_parent_chargeand creating a new override on top.Behaviour across the three scenarios
charge.plan_idvs.target_plan.idbase_plan.id != overridden_plan.idensure_plan_overridecreates the overridden plan and populates it viaCharges::OverrideService. Existing branch finds the freshly-created override viaparent_idmatch and updates it. Unchanged.parent_idpoints at base-plan chargeoverridden_plan.id == overridden_plan.idplan_overridesPATCH —parent_id: nil, charge sits directly on the overridden plan viaPlans::UpdateService#process_chargesoverridden_plan.id == overridden_plan.idTests
Existing spec contexts in
spec/services/subscriptions/update_or_override_charge_service_spec.rbcontinue to pass; new context"when the charge passed lives directly on the overridden plan with parent_id: nil"covers scenario 3 explicitly:Charge.countdoes not change (no second override).parent_idstill nil, sameplan_id, override params applied).billable_metricafter the call.Out of scope
DELETE /subscriptions/{external_id}/charges/{code}endpoint (option 1 from the issue). Left for a follow-up.cc @rsempe — thanks for the quick triage on #5549.