@@ -276,40 +276,70 @@ def __init__(
276276
277277class BelowMinimumSeats (PolarRequestValidationError ):
278278 def __init__ (
279- self , subscription : Subscription , minimum_seats : int , requested_seats : int
279+ self ,
280+ subscription : Subscription ,
281+ minimum_seats : int ,
282+ requested_seats : int ,
283+ * ,
284+ product_id : uuid .UUID | None = None ,
280285 ) -> None :
281286 self .subscription = subscription
282287 self .minimum_seats = minimum_seats
283288 self .requested_seats = requested_seats
284- super ().__init__ (
285- [
286- {
287- "type" : "value_error" ,
288- "loc" : ("body" , "seats" ),
289- "msg" : f"Minimum { minimum_seats } seats required." ,
290- "input" : requested_seats ,
291- }
292- ]
293- )
289+ # A product change keeps the live seat count, so the offending input is
290+ # the target product, not a submitted seat count.
291+ error : ValidationError
292+ if product_id is not None :
293+ error = {
294+ "type" : "value_error" ,
295+ "loc" : ("body" , "product_id" ),
296+ "msg" : (
297+ f"Current seat count of { requested_seats } is below the "
298+ f"minimum of { minimum_seats } seats for this product."
299+ ),
300+ "input" : product_id ,
301+ }
302+ else :
303+ error = {
304+ "type" : "value_error" ,
305+ "loc" : ("body" , "seats" ),
306+ "msg" : f"Minimum { minimum_seats } seats required." ,
307+ "input" : requested_seats ,
308+ }
309+ super ().__init__ ([error ])
294310
295311
296312class AboveMaximumSeats (PolarRequestValidationError ):
297313 def __init__ (
298- self , subscription : Subscription , maximum_seats : int , requested_seats : int
314+ self ,
315+ subscription : Subscription ,
316+ maximum_seats : int ,
317+ requested_seats : int ,
318+ * ,
319+ product_id : uuid .UUID | None = None ,
299320 ) -> None :
300321 self .subscription = subscription
301322 self .maximum_seats = maximum_seats
302323 self .requested_seats = requested_seats
303- super ().__init__ (
304- [
305- {
306- "type" : "value_error" ,
307- "loc" : ("body" , "seats" ),
308- "msg" : f"Maximum { maximum_seats } seats allowed." ,
309- "input" : requested_seats ,
310- }
311- ]
312- )
324+ error : ValidationError
325+ if product_id is not None :
326+ error = {
327+ "type" : "value_error" ,
328+ "loc" : ("body" , "product_id" ),
329+ "msg" : (
330+ f"Current seat count of { requested_seats } is above the "
331+ f"maximum of { maximum_seats } seats for this product."
332+ ),
333+ "input" : product_id ,
334+ }
335+ else :
336+ error = {
337+ "type" : "value_error" ,
338+ "loc" : ("body" , "seats" ),
339+ "msg" : f"Maximum { maximum_seats } seats allowed." ,
340+ "input" : requested_seats ,
341+ }
342+ super ().__init__ ([error ])
313343
314344
315345class NotAUnitBasedSubscription (PolarRequestValidationError ):
@@ -1945,7 +1975,7 @@ async def update_product(
19451975 proration_behavior = organization .proration_behavior
19461976
19471977 is_initial_seat_transition = self ._promote_seats_for_seat_transition (
1948- subscription , currency_prices , proration_behavior
1978+ subscription , currency_prices , proration_behavior , product_id
19491979 )
19501980 self ._promote_units_for_unit_transition (
19511981 subscription , currency_prices , proration_behavior , product_id
@@ -3165,7 +3195,7 @@ async def _compute_change_preview(
31653195 allowed_visibilities = allowed_visibilities ,
31663196 )
31673197 self ._promote_seats_for_seat_transition (
3168- subscription , currency_prices , proration_behavior
3198+ subscription , currency_prices , proration_behavior , product_id
31693199 )
31703200 self ._promote_units_for_unit_transition (
31713201 subscription , currency_prices , proration_behavior , product_id
@@ -3283,22 +3313,36 @@ def _promote_seats_for_seat_transition(
32833313 subscription : Subscription ,
32843314 currency_prices : PriceSet ,
32853315 proration_behavior : SubscriptionProrationBehavior ,
3316+ product_id : uuid .UUID ,
32863317 ) -> bool :
32873318 """Promote `subscription.seats` to the new product's first seat-price tier
32883319 minimum, so the proration debit and `apply_update`'s product-branch rebuild
32893320 both see a valid seat count. `next_period` is blocked because the post-apply
32903321 seat auto-claim has to run immediately, or the billing customer loses benefit
32913322 access. Returns whether this was a non-seat → seat transition.
32923323 """
3293- if any (is_seat_price (price ) for price in subscription .prices ):
3294- return False
3295-
32963324 seat_price = next (
32973325 (price for price in currency_prices if is_seat_price (price )), None
32983326 )
32993327 if seat_price is None :
33003328 return False
33013329
3330+ if any (is_seat_price (price ) for price in subscription .prices ):
3331+ seats = subscription .seats
3332+ if seats is None :
3333+ return False
3334+ minimum_seats = seat_price .get_minimum_seats ()
3335+ if seats < minimum_seats :
3336+ raise BelowMinimumSeats (
3337+ subscription , minimum_seats , seats , product_id = product_id
3338+ )
3339+ maximum_seats = seat_price .get_maximum_seats ()
3340+ if maximum_seats is not None and seats > maximum_seats :
3341+ raise AboveMaximumSeats (
3342+ subscription , maximum_seats , seats , product_id = product_id
3343+ )
3344+ return False
3345+
33023346 if proration_behavior == SubscriptionProrationBehavior .next_period :
33033347 raise PolarRequestValidationError (
33043348 [
0 commit comments