Skip to content

Commit c3c837a

Browse files
emyllerclaude
andauthored
feat(Billing): Cap Scale-Up seats at 20 (#7217)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 556b114 commit c3c837a

3 files changed

Lines changed: 47 additions & 17 deletions

File tree

api/organisations/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
FREE_PLAN_SUBSCRIPTION_METADATA,
4343
MAX_API_CALLS_IN_FREE_PLAN,
4444
MAX_SEATS_IN_FREE_PLAN,
45+
MAX_SEATS_IN_SCALE_UP_PLAN,
4546
SUBSCRIPTION_BILLING_STATUSES,
4647
SUBSCRIPTION_PAYMENT_METHODS,
4748
TRIAL_SUBSCRIPTION_ID,
@@ -272,6 +273,7 @@ def can_auto_upgrade_seats(self) -> bool:
272273
return (
273274
is_saas()
274275
and self.subscription_plan_family == SubscriptionPlanFamily.SCALE_UP
276+
and self.organisation.num_seats < MAX_SEATS_IN_SCALE_UP_PLAN
275277
)
276278

277279
@property

api/organisations/subscriptions/constants.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
settings.MAX_PROJECTS_IN_FREE_PLAN,
1313
)
1414

15+
MAX_SEATS_IN_SCALE_UP_PLAN = 20
16+
1517
CHARGEBEE = "CHARGEBEE"
1618
XERO = "XERO"
1719
AWS_MARKETPLACE = "AWS_MARKETPLACE"
@@ -41,12 +43,7 @@
4143
FREE_PLAN_ID = "free"
4244
TRIAL_SUBSCRIPTION_ID = "trial"
4345
SCALE_UP = "scale-up"
44-
SCALE_UP_12_MONTHS_V2 = "scale-up-12-months-v2"
45-
SCALE_UP_QUARTERLY_V2_SEMIANNUAL = "scale-up-quarterly-v2-semiannual"
46-
SCALE_UP_V2 = "scale-up-v2"
4746
STARTUP = "startup"
48-
STARTUP_ANNUAL_V2 = "startup-annual-v2"
49-
STARTUP_V2 = "startup-v2"
5047
ENTERPRISE = "enterprise"
5148

5249

api/tests/unit/organisations/test_unit_organisations_models.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -209,22 +209,53 @@ def test_over_plan_seats_limit__no_subscription_metadata__returns_true( # type:
209209

210210

211211
@pytest.mark.saas_mode
212-
def test_is_auto_seat_upgrade_available__scale_up_plan__returns_true(
212+
@pytest.mark.parametrize(
213+
"plan, num_seats, expected",
214+
[
215+
("scale-up-v2", 19, True),
216+
("scale-up-v2", 20, False),
217+
("scale-up-v2", 21, False),
218+
("startup-v2", 1, False),
219+
],
220+
)
221+
def test_is_auto_seat_upgrade_available__given_plan_and_seat_count__returns_expected(
213222
organisation: Organisation,
223+
plan: str,
224+
num_seats: int,
225+
expected: bool,
214226
) -> None:
215227
# Given
216-
plan = "Scale-Up"
217-
subscription_id = "subscription-id"
228+
subscription = organisation.subscription
229+
subscription.plan = plan
230+
subscription.subscription_id = "subscription-id"
231+
subscription.save()
218232

219-
Subscription.objects.filter(organisation=organisation).update(
220-
subscription_id=subscription_id, plan=plan
221-
)
233+
organisation.users.all().delete()
234+
for i in range(num_seats):
235+
user = FFAdminUser.objects.create(email=f"seat-{i}@test.com")
236+
user.add_organisation(organisation)
222237

223238
# When
224-
organisation.refresh_from_db()
239+
result = organisation.is_auto_seat_upgrade_available()
240+
241+
# Then
242+
assert result is expected
243+
244+
245+
def test_is_auto_seat_upgrade_available__not_saas__returns_false(
246+
organisation: Organisation,
247+
) -> None:
248+
# Given
249+
subscription = organisation.subscription
250+
subscription.plan = "scale-up-v2"
251+
subscription.subscription_id = "subscription-id"
252+
subscription.save()
253+
254+
# When
255+
result = organisation.is_auto_seat_upgrade_available()
225256

226257
# Then
227-
assert organisation.is_auto_seat_upgrade_available() is True
258+
assert result is False
228259

229260

230261
def test_subscription__default_for_new_organisation__has_one_max_seat(
@@ -458,16 +489,16 @@ def test_get_subscription_metadata__self_hosted_open_source__returns_free_plan_m
458489

459490

460491
@pytest.mark.saas_mode
461-
def test_add_single_seat__upgradable_plan__calls_chargebee_add_seat(
492+
def test_add_single_seat__upgradable_plan__calls_chargebee_add_single_seat(
493+
organisation: Organisation,
462494
mocker: MockerFixture,
463495
) -> None:
464496
# Given
465497
subscription_id = "subscription-id"
466498
subscription = Subscription(subscription_id=subscription_id, plan="scale-up")
499+
mocker.patch.object(Subscription, "can_auto_upgrade_seats", new=True)
500+
mocked_add_single_seat = mocker.patch("organisations.models.add_single_seat")
467501

468-
mocked_add_single_seat = mocker.patch(
469-
"organisations.models.add_single_seat", autospec=True
470-
)
471502
# When
472503
subscription.add_single_seat() # type: ignore[no-untyped-call]
473504

0 commit comments

Comments
 (0)