Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions api/organisations/chargebee/chargebee.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime
from typing import Any

import structlog
from chargebee.api_error import ( # type: ignore[import-untyped]
APIError as ChargebeeAPIError,
)
Expand Down Expand Up @@ -31,9 +32,11 @@
from organisations.chargebee.constants import (
ADDITIONAL_API_SCALE_UP_ADDON_ID,
ADDITIONAL_API_START_UP_ADDON_ID,
SEAT_ADDON_BY_PLAN_PREFIX,
SEAT_SCALE_UP_V2_ADDON_BY_BILLING_PERIOD,
)
from organisations.chargebee.metadata import ChargebeeObjMetadata
from organisations.subscriptions.constants import CHARGEBEE
from organisations.subscriptions.constants import CHARGEBEE, SubscriptionPlanFamily
from organisations.subscriptions.exceptions import (
CannotCancelChargebeeSubscription,
UpgradeAPIUsageError,
Expand All @@ -43,6 +46,7 @@
)

logger = logging.getLogger(__name__)
log = structlog.get_logger("billing")

CHARGEBEE_PAYMENT_ERROR_CODES = [
"payment_processing_failed",
Expand Down Expand Up @@ -206,7 +210,7 @@ def cancel_subscription(subscription_id: str) -> None:
raise CannotCancelChargebeeSubscription(msg) from e


def add_single_seat(subscription_id: str) -> None:
def add_single_seat(subscription_id: str, organisation_id: int) -> None:
try:
subscription = chargebee_client.Subscription.retrieve(
subscription_id
Expand All @@ -224,7 +228,7 @@ def add_single_seat(subscription_id: str) -> None:
SubscriptionOps.UpdateParams(
addons=[
SubscriptionOps.UpdateAddonParams(
id=_get_additional_seat_addon_id(subscription),
id=addon_id,
quantity=current_seats + 1,
)
],
Expand All @@ -233,6 +237,15 @@ def add_single_seat(subscription_id: str) -> None:
),
)

log.info(
"seat.added",
organisation__id=organisation_id,
subscription__id=subscription_id,
addon__id=addon_id,
seats__previous=current_seats,
seats__new=current_seats + 1,
)

except ChargebeeAPIError as e:
api_error_code = e.json_obj["api_error_code"]
if api_error_code in CHARGEBEE_PAYMENT_ERROR_CODES:
Expand All @@ -252,16 +265,24 @@ def add_single_seat(subscription_id: str) -> None:


def _get_additional_seat_addon_id(subscription: SubscriptionOps) -> str:
addon_id_prefix = "additional-team-members-scale-up-v2"
addon_suffixes_by_billing_period = {1: "monthly", 6: "semiannual", 12: "annual"}
suffix = addon_suffixes_by_billing_period.get(subscription.billing_period)
if not suffix:
logger.warning(
"Unexpected billing period for subscription ID %s",
subscription.id,
)
suffix = "monthly"
return "-".join([addon_id_prefix, suffix])
normalised_plan = SubscriptionPlanFamily.normalise_plan_id(
str(getattr(subscription, "plan_id", ""))
)
for prefix, addon_id in SEAT_ADDON_BY_PLAN_PREFIX.items():
if normalised_plan.startswith(prefix):
return addon_id

v2_addon_id = SEAT_SCALE_UP_V2_ADDON_BY_BILLING_PERIOD.get(
subscription.billing_period
)
if v2_addon_id:
return v2_addon_id

logger.warning(
"Unexpected billing period for subscription ID %s",
subscription.id,
)
return SEAT_SCALE_UP_V2_ADDON_BY_BILLING_PERIOD[1]


def add_100k_api_calls_start_up(
Expand Down
10 changes: 10 additions & 0 deletions api/organisations/chargebee/constants.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
ADDITIONAL_API_START_UP_ADDON_ID = "additional-api-start-up-monthly"
ADDITIONAL_API_SCALE_UP_ADDON_ID = "additional-api-scale-up-monthly"

SEAT_ADDON_BY_PLAN_PREFIX: dict[str, str] = {
"scaleupv4": "Additional-Team-Members-Scale-Up-v4",
}

SEAT_SCALE_UP_V2_ADDON_BY_BILLING_PERIOD: dict[int, str] = {
1: "additional-team-members-scale-up-v2-monthly",
6: "additional-team-members-scale-up-v2-semiannual",
12: "additional-team-members-scale-up-v2-annual",
}
2 changes: 1 addition & 1 deletion api/organisations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ def add_single_seat(self): # type: ignore[no-untyped-def]
if not self.can_auto_upgrade_seats:
raise SubscriptionDoesNotSupportSeatUpgrade()

add_single_seat(self.subscription_id) # type: ignore[arg-type]
add_single_seat(self.subscription_id, organisation_id=self.organisation_id) # type: ignore[arg-type]

def is_in_trial(self) -> bool:
return self.subscription_id == TRIAL_SUBSCRIPTION_ID
Expand Down
6 changes: 5 additions & 1 deletion api/organisations/subscriptions/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@ class SubscriptionPlanFamily(Enum):
SCALE_UP = "SCALE_UP"
ENTERPRISE = "ENTERPRISE"

@staticmethod
def normalise_plan_id(plan_id: str) -> str:
return str(plan_id).replace("-", "").lower()

@classmethod
def get_by_plan_id(cls, plan_id: str) -> "SubscriptionPlanFamily":
match str(plan_id).replace("-", "").lower():
match cls.normalise_plan_id(plan_id):
case p if p.startswith("scaleup"):
return cls.SCALE_UP
case p if p.startswith("startup"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ def test_add_single_seat__existing_addon__increments_quantity(mocker) -> None:
)

# When
add_single_seat(subscription_id)
add_single_seat(subscription_id, organisation_id=1)

# Then
mocked_chargebee.Subscription.update.assert_called_once_with(
Expand Down Expand Up @@ -585,7 +585,7 @@ def test_add_single_seat__no_existing_addon__creates_addon_with_quantity_one(
)

# When
add_single_seat(subscription_id)
add_single_seat(subscription_id, organisation_id=1)

# Then
mocked_chargebee.Subscription.update.assert_called_once_with(
Expand All @@ -600,6 +600,42 @@ def test_add_single_seat__no_existing_addon__creates_addon_with_quantity_one(
)


def test_add_single_seat__scale_up_v4_plan__uses_v4_addon(
mocker: MockerFixture,
) -> None:
# Given
subscription_id = "subscription-id"
expected_addon_id = "Additional-Team-Members-Scale-Up-v4"

mocked_subscription = mocker.MagicMock(
id=subscription_id,
plan_id="Scale-Up-v4",
addons=[],
billing_period=1,
)
mocked_chargebee = mocker.patch(
"organisations.chargebee.chargebee.chargebee_client", autospec=True
)
mocked_chargebee.Subscription.retrieve.return_value.subscription = (
mocked_subscription
)

# When
add_single_seat(subscription_id, organisation_id=1)

# Then
mocked_chargebee.Subscription.update.assert_called_once_with(
subscription_id,
SubscriptionOps.UpdateParams(
addons=[
SubscriptionOps.UpdateAddonParams(id=expected_addon_id, quantity=1)
],
prorate=True,
invoice_immediately=True,
),
)


def test_add_single_seat__api_error__raises_upgrade_seats_error( # type: ignore[no-untyped-def]
mocker, caplog
) -> None:
Expand Down Expand Up @@ -636,7 +672,7 @@ def test_add_single_seat__api_error__raises_upgrade_seats_error( # type: ignore

# When
with pytest.raises(UpgradeSeatsError):
add_single_seat(subscription_id)
add_single_seat(subscription_id, organisation_id=1)

# Then
mocked_chargebee.Subscription.update.assert_called_once_with(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,9 @@ def test_add_single_seat__upgradable_plan__calls_chargebee_add_single_seat(
subscription.add_single_seat() # type: ignore[no-untyped-call]

# Then
mocked_add_single_seat.assert_called_once_with(subscription_id)
mocked_add_single_seat.assert_called_once_with(
subscription_id, organisation_id=subscription.organisation_id
)


def test_add_single_seat__non_upgradable_plan__raises_error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ Logged at `warning` from:
Attributes:
- `details`

### `billing.seat.added`

Logged at `info` from:
- `api/organisations/chargebee/chargebee.py:240`

Attributes:
- `addon.id`
- `organisation.id`
- `seats.new`
- `seats.previous`
- `subscription.id`

### `code_references.cleanup_issues.created`

Logged at `info` from:
Expand Down
Loading