fix(checkout): preserve locked unit count when switching products - #14196
Merged
strandhvilliam merged 2 commits intoSep 8, 2026
Merged
strandhvilliam merged 2 commits into
strandhvilliam merged 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
OpenAPI ChangesNo changes detected in the OpenAPI schema. |
detail-app
Bot
force-pushed
the
detail/bug-fix/fix-checkout-preserve-locked-unit-count-when-switc-be989e
branch
from
September 8, 2026 01:14
5c07c6a to
5e9ffe8
Compare
Remove the product-switch override test (already enforced on any units update) and the extra min/max fixture in favor of product_unit_based_with_min. Co-authored-by: Villiam Strandh <villiam.strandh@outlook.com>
strandhvilliam
approved these changes
Sep 8, 2026
strandhvilliam
left a comment
Contributor
There was a problem hiding this comment.
Same pattern as seats, so checks out
strandhvilliam
deleted the
detail/bug-fix/fix-checkout-preserve-locked-unit-count-when-switc-be989e
branch
September 8, 2026 08:17
This branch was successfully deployed
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.
Detail bug report: View on Detail
Summary
Related Issue: polarsource/feedback#463
Fixes a checkout bypass where switching a unit-priced product on a multi-product checkout link ignored the merchant's locked unit count (
min_units/max_units), collapsing the quantity to the new product's minimum (typically1) and letting the customer underpay.What
CheckoutService._update_price(server/polar/checkout/service.py): the unit branch now mirrors the existing seat branch — captures the previous unit count, detects amin_units/max_unitslock, preserves & clamps the unit count across product switches when unlocked, and passescheckout_min_units/checkout_max_unitsinto_validate_unit_limits(kwargs that already existed but went unused here).server/tests/checkout/test_service.py: adds two fixtures (product_unit_based_with_max,product_unit_based_with_min_max) and fourTestUpdatetests mirroring the seat-switch coverage (test_switching_products_preserves_units,_clamps_units_to_new_bounds,_preserves_locked_units,_enforces_locked_units_on_override).server/tests/fixtures/random_objects.py:create_checkoutnow acceptsmin_units/max_units, mirroring the existingmin_seats/max_seats.Why
On the embedded checkout, switching products submits only
product_id(neverunits). The unit branch of_update_pricealways fell back tounits = checkout_update.units or unit_price.get_minimum_purchasable_units()and called_validate_unit_limitswithout the checkout-level lock. A merchant who locked units via a checkout link (min_units == max_units == N, the shape_create_from_linkproduces) could have a customer switch to a cheaper unit product and pay for1unit instead of the lockedN.The sibling seat branch was fixed for this same bypass class in #13749, but the adjacent unit branch was missed — even though
_validate_unit_limitsalready supported the lock kwargs. There is no downstream re-validation of the lock on the customer-reachable switch path, so_update_priceis the only enforcement point.How
Mirror the seat branch in shape: capture
previous_unitsbefore resetting, branch onis_unit_count_locked, preserve-and-clamp across an unlocked switch, fall back to the previous or minimum count when locked, then call_validate_unit_limitswithcheckout_min_units/checkout_max_units. The trailingcalculate_upfront_amount(..., units=units)already referenced the localunitsvariable, so it picks up the corrected value with no further change.Testing
TestUpdatetests mirroring the existing seat-switch regression suite (preserve, clamp, locked-preserve, locked-enforce-on-override); all four pass.TestUpdate+TestCreateUnitBasedCheckout(90 tests) andtests/checkout/test_endpoints.py(86 tests) pass; the fulltests/checkout/suite passes (465 tests).units=5, switch to a cheaper 2000/unit product carrying onlyproduct_id) now preservesunits=5and computesamount=14500instead of collapsing tounits=1, amount=2000. This was checked with a throwaway local test asserting the old buggy behavior, which now fails withAssertionError: assert 5 == 1— the throwaway test was not committed.ruff format --check,ruff check, andmypyon the changed files pass;py_compileclean.Checklist
uv run task lint && uv run task lint_types)Automatic Fixes PRs can be configured here.