Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use get-metadata-subscription to get max_api_calls #2279

Merged
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
09567a6
Use get-metadata-subscription to get max_api_calls
novakzaballa Jun 8, 2023
659e637
Merge branch 'main' into fix/use-get-metadata-subscription-for-max-ap…
novakzaballa Jun 27, 2023
3c5c924
Use CB webhook, Update the get-subscription-metadata endpoint and upd…
novakzaballa Jun 27, 2023
88f1cf1
Update tests
novakzaballa Jun 28, 2023
fd12f20
clean logs
novakzaballa Jun 28, 2023
b4b856b
Solve observations
novakzaballa Jun 30, 2023
1eddc26
Solve CB web hook errors
novakzaballa Jun 30, 2023
0d635d4
Add constants
novakzaballa Jul 3, 2023
52d9c65
Update get_subscription_metadata from chargebee and solve obs
novakzaballa Jul 7, 2023
28043b6
Update tests
novakzaballa Jul 7, 2023
e424640
Update test
novakzaballa Jul 9, 2023
28a500d
Merge branch 'main' into fix/use-get-metadata-subscription-for-max-ap…
novakzaballa Jul 9, 2023
5c74b28
test correction and move dict_to_class function to utils
novakzaballa Jul 10, 2023
85983c4
Solve observations
novakzaballa Jul 11, 2023
c6cb078
Adjust type hints
novakzaballa Jul 11, 2023
c99dc86
create _convert_chargebee_subscription_to_dictionary function
novakzaballa Jul 13, 2023
5fef7cc
Update get_metadata_subscription
novakzaballa Jul 14, 2023
d35157a
Update get_metadata_subscription
novakzaballa Jul 14, 2023
a51c973
Merge branch 'main' into fix/use-get-metadata-subscription-for-max-ap…
novakzaballa Jul 14, 2023
528835e
Recreate migration 0043
novakzaballa Jul 14, 2023
e74a1ca
Recreate migration 0043
novakzaballa Jul 14, 2023
74e4744
Tests
novakzaballa Jul 19, 2023
0058673
Update sales_dashboard page, organisationsubscriptioninformationcache
novakzaballa Jul 19, 2023
2b367a5
Update tests
novakzaballa Jul 20, 2023
358d47f
Update tests
novakzaballa Jul 20, 2023
94cd1ff
Update tests
novakzaballa Jul 24, 2023
f5e00dc
Update tests, and migrations
novakzaballa Aug 2, 2023
937cd76
Merge branch 'main' into fix/use-get-metadata-subscription-for-max-ap…
novakzaballa Aug 2, 2023
af05c5a
recreate migrations
novakzaballa Aug 2, 2023
6f340c3
Update test
novakzaballa Aug 2, 2023
f88c5b6
Updated test to use parameterization
novakzaballa Aug 9, 2023
01db4d0
fixes in test_when_plan_is_changed_max_seats_and_max_api_calls_are_up…
novakzaballa Aug 10, 2023
e2c8fd6
test swapped
novakzaballa Aug 18, 2023
457d411
Merge branch 'main' into fix/use-get-metadata-subscription-for-max-ap…
novakzaballa Aug 21, 2023
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
4 changes: 2 additions & 2 deletions api/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import typing
from typing import Tuple
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really an issue, but I'm not sure why these changes have snuck into this PR.


import pytest
from django.contrib.contenttypes.models import ContentType
Expand Down Expand Up @@ -292,7 +292,7 @@ def environment_api_key(environment):


@pytest.fixture()
def master_api_key(organisation) -> typing.Tuple[MasterAPIKey, str]:
def master_api_key(organisation) -> Tuple[MasterAPIKey, str]:
master_api_key, key = MasterAPIKey.objects.create_key(
name="test_key", organisation=organisation
)
Expand Down
3 changes: 2 additions & 1 deletion api/organisations/chargebee/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from .chargebee import ( # noqa
add_single_seat,
extract_subscription_metadata,
get_customer_id_from_subscription_id,
get_hosted_page_url_for_subscription_upgrade,
get_max_api_calls_for_plan,
get_max_seats_for_plan,
get_plan_meta_data,
get_portal_url,
get_subscription_data_from_hosted_page,
get_subscription_metadata,
get_subscription_metadata_from_id,
)
54 changes: 40 additions & 14 deletions api/organisations/chargebee/chargebee.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,28 @@ def get_hosted_page_url_for_subscription_upgrade(
return checkout_existing_response.hosted_page.url


def get_subscription_metadata(
def extract_subscription_metadata(
chargebee_subscription: dict,
customer_email: str,
) -> ChargebeeObjMetadata:
chargebee_addons = chargebee_subscription.get("addons", [])
chargebee_cache = ChargebeeCache()
subscription_metadata: ChargebeeObjMetadata = chargebee_cache.plans[
chargebee_subscription["plan_id"]
]
subscription_metadata.chargebee_email = customer_email

for addon in chargebee_addons:
quantity = getattr(addon, "quantity", None) or 1
addon_metadata: ChargebeeObjMetadata = (
chargebee_cache.addons[addon["id"]] * quantity
)
subscription_metadata = subscription_metadata + addon_metadata

return subscription_metadata


def get_subscription_metadata_from_id(
subscription_id: str,
) -> typing.Optional[ChargebeeObjMetadata]:
if not (subscription_id and subscription_id.strip() != ""):
Expand All @@ -112,20 +133,13 @@ def get_subscription_metadata(

with suppress(ChargebeeAPIError):
chargebee_result = chargebee.Subscription.retrieve(subscription_id)
subscription = chargebee_result.subscription
addons = subscription.addons or []

chargebee_cache = ChargebeeCache()
plan_metadata = chargebee_cache.plans[subscription.plan_id]
subscription_metadata = plan_metadata
subscription_metadata.chargebee_email = chargebee_result.customer.email

for addon in addons:
quantity = getattr(addon, "quantity", None) or 1
addon_metadata = chargebee_cache.addons[addon.id] * quantity
subscription_metadata = subscription_metadata + addon_metadata
chargebee_subscription = _convert_chargebee_subscription_to_dictionary(
chargebee_result.subscription
)

return subscription_metadata
return extract_subscription_metadata(
chargebee_subscription, chargebee_result.customer.email
)


def cancel_subscription(subscription_id: str):
Expand Down Expand Up @@ -169,3 +183,15 @@ def add_single_seat(subscription_id: str):
)
logger.error(msg)
raise UpgradeSeatsError(msg) from e


def _convert_chargebee_subscription_to_dictionary(
chargebee_subscription: chargebee.Subscription,
) -> dict:
chargebee_subscription_dict = vars(chargebee_subscription)
# convert the addons into a list of dictionaries since vars don't do it recursively
chargebee_subscription_dict["addons"] = [
vars(addon) for addon in chargebee_subscription.addons
]

return chargebee_subscription_dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-07-14 16:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('organisations', '0043_add_created_at_and_updated_at_to_organisationwebhook'),
]

operations = [
migrations.AddField(
model_name='organisationsubscriptioninformationcache',
name='allowed_projects',
field=models.IntegerField(default=1),
),
]
23 changes: 23 additions & 0 deletions api/organisations/migrations/0045_auto_20230802_1956.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.20 on 2023-08-02 19:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('organisations', '0044_organisationsubscriptioninformationcache_allowed_projects'),
]

operations = [
migrations.AddField(
model_name='organisationsubscriptioninformationcache',
name='chargebee_updated_at',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='organisationsubscriptioninformationcache',
name='influx_updated_at',
field=models.DateTimeField(null=True),
),
]
24 changes: 21 additions & 3 deletions api/organisations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
get_max_seats_for_plan,
get_plan_meta_data,
get_portal_url,
get_subscription_metadata,
get_subscription_metadata_from_id,
)
from organisations.chargebee.chargebee import add_single_seat
from organisations.chargebee.chargebee import (
cancel_subscription as cancel_chargebee_subscription,
)
from organisations.chargebee.metadata import ChargebeeObjMetadata
from organisations.subscriptions.constants import (
CHARGEBEE,
FREE_PLAN_ID,
Expand Down Expand Up @@ -95,6 +96,11 @@ def num_seats(self):
def has_subscription(self) -> bool:
return hasattr(self, "subscription") and bool(self.subscription.subscription_id)

def has_subscription_information_cache(self) -> bool:
return hasattr(self, "subscription_information_cache") and bool(
self.subscription_information_cache
)

@property
def is_paid(self):
return self.has_subscription() and self.subscription.cancellation_date is None
Expand Down Expand Up @@ -211,14 +217,23 @@ def get_portal_url(self, redirect_url):

def get_subscription_metadata(self) -> BaseSubscriptionMetadata:
metadata = None

if self.subscription_id == TRIAL_SUBSCRIPTION_ID:
metadata = BaseSubscriptionMetadata(
seats=self.max_seats, api_calls=self.max_api_calls
)

if self.payment_method == CHARGEBEE and self.subscription_id:
metadata = get_subscription_metadata(self.subscription_id)
if self.organisation.has_subscription_information_cache():
# Getting the data from the subscription information cache because
# data is guaranteed to be up to date by using a Chargebee webhook.
metadata = ChargebeeObjMetadata(
seats=self.organisation.subscription_information_cache.allowed_seats,
api_calls=self.organisation.subscription_information_cache.allowed_30d_api_calls,
projects=self.organisation.subscription_information_cache.allowed_projects,
chargebee_email=self.organisation.subscription_information_cache.chargebee_email,
)
else:
metadata = get_subscription_metadata_from_id(self.subscription_id)
elif self.payment_method == XERO and self.subscription_id:
metadata = XeroSubscriptionMetadata(
seats=self.max_seats, api_calls=self.max_api_calls, projects=None
Expand Down Expand Up @@ -271,12 +286,15 @@ class OrganisationSubscriptionInformationCache(models.Model):
on_delete=models.CASCADE,
)
updated_at = models.DateTimeField(auto_now=True)
chargebee_updated_at = models.DateTimeField(auto_now=False, null=True)
influx_updated_at = models.DateTimeField(auto_now=False, null=True)

api_calls_24h = models.IntegerField(default=0)
api_calls_7d = models.IntegerField(default=0)
api_calls_30d = models.IntegerField(default=0)

allowed_seats = models.IntegerField(default=1)
allowed_30d_api_calls = models.IntegerField(default=50000)
allowed_projects = models.IntegerField(default=1)

chargebee_email = models.EmailField(blank=True, max_length=254, null=True)
22 changes: 12 additions & 10 deletions api/organisations/subscription_info_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@

from app_analytics.influxdb_wrapper import get_top_organisations
from django.conf import settings
from django.utils import timezone

from .chargebee import get_subscription_metadata
from .chargebee import get_subscription_metadata_from_id
from .models import Organisation, OrganisationSubscriptionInformationCache
from .subscriptions.constants import CHARGEBEE
from .subscriptions.constants import CHARGEBEE, SubscriptionCacheEntity

OrganisationSubscriptionInformationCacheDict = typing.Dict[
int, OrganisationSubscriptionInformationCache
]


def update_caches():
def update_caches(update_cache_entities: typing.Tuple[SubscriptionCacheEntity, ...]):
"""
Update the cache objects for all active organisations in the database.
Update the cache objects for an update_cache_entity in the database.
"""

organisations = Organisation.objects.select_related(
Expand All @@ -30,14 +29,16 @@ def update_caches():
for org in organisations
}

_update_caches_with_influx_data(organisation_info_cache_dict)
_update_caches_with_chargebee_data(organisations, organisation_info_cache_dict)
if SubscriptionCacheEntity.INFLUX in update_cache_entities:
_update_caches_with_influx_data(organisation_info_cache_dict)

if SubscriptionCacheEntity.CHARGEBEE in update_cache_entities:
_update_caches_with_chargebee_data(organisations, organisation_info_cache_dict)

to_update = []
to_create = []

for subscription_info_cache in organisation_info_cache_dict.values():
subscription_info_cache.updated_at = timezone.now()
if subscription_info_cache.id:
to_update.append(subscription_info_cache)
else:
Expand All @@ -53,7 +54,8 @@ def update_caches():
"allowed_seats",
"allowed_30d_api_calls",
"chargebee_email",
"updated_at",
"chargebee_updated_at",
"influx_updated_at",
],
)

Expand Down Expand Up @@ -99,7 +101,7 @@ def _update_caches_with_chargebee_data(
):
continue

metadata = get_subscription_metadata(subscription.subscription_id)
metadata = get_subscription_metadata_from_id(subscription.subscription_id)
if not metadata:
continue

Expand Down
7 changes: 7 additions & 0 deletions api/organisations/subscriptions/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from enum import Enum

from organisations.subscriptions.metadata import BaseSubscriptionMetadata

MAX_SEATS_IN_FREE_PLAN = 1
Expand All @@ -24,3 +26,8 @@
projects=MAX_PROJECTS_IN_FREE_PLAN,
)
FREE_PLAN_ID = "free"


class SubscriptionCacheEntity(Enum):
INFLUX = "INFLUX"
CHARGEBEE = "CHARGEBEE"
4 changes: 3 additions & 1 deletion api/organisations/subscriptions/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def __str__(self):
return (
"%s Subscription Metadata (seats: %d, api_calls: %d, projects: %s, chargebee_email: %s)"
% (
self.payment_source.title(),
self.payment_source.title()
if self.payment_source is not None
else "unknown payment source",
self.seats,
self.api_calls,
str(self.projects) if self.projects is not None else "no limit",
Expand Down
6 changes: 2 additions & 4 deletions api/organisations/subscriptions/subscription_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from organisations.chargebee import (
get_subscription_metadata as get_chargebee_subscription_metadata,
)
from organisations.chargebee import get_subscription_metadata_from_id
from organisations.models import Organisation
from organisations.subscriptions.xero.metadata import XeroSubscriptionMetadata

Expand All @@ -14,7 +12,7 @@ def get_subscription_metadata(organisation: Organisation) -> BaseSubscriptionMet
seats=max_seats, api_calls=max_api_calls, projects=max_projects
)
if organisation.subscription.payment_method == CHARGEBEE:
chargebee_subscription_metadata = get_chargebee_subscription_metadata(
chargebee_subscription_metadata = get_subscription_metadata_from_id(
organisation.subscription.subscription_id
)
if chargebee_subscription_metadata is not None:
Expand Down
13 changes: 11 additions & 2 deletions api/organisations/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from task_processor.decorators import register_task_handler
from users.models import FFAdminUser

from .subscriptions.constants import SubscriptionCacheEntity

ALERT_EMAIL_MESSAGE = (
"Organisation %s has used %d seats which is over their plan limit of %d (plan: %s)"
)
Expand All @@ -30,5 +32,12 @@ def send_org_over_limit_alert(organisation_id):


@register_task_handler()
def update_organisation_subscription_information_caches():
subscription_info_cache.update_caches()
def update_organisation_subscription_information_influx_cache():
subscription_info_cache.update_caches((SubscriptionCacheEntity.INFLUX,))


@register_task_handler()
def update_organisation_subscription_information_cache():
subscription_info_cache.update_caches(
(SubscriptionCacheEntity.CHARGEBEE, SubscriptionCacheEntity.INFLUX)
)
20 changes: 12 additions & 8 deletions api/organisations/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,26 +243,30 @@ def test_organisation_is_paid_returns_false_if_cancelled_subscription_exists(


def test_subscription_get_subscription_metadata_returns_cb_metadata_for_cb_subscription(
organisation,
mocker,
):
# Given
subscription = Subscription(
payment_method=CHARGEBEE, subscription_id="cb-subscription"
seats = 10
api_calls = 50000000
OrganisationSubscriptionInformationCache.objects.create(
organisation=organisation, allowed_seats=seats, allowed_30d_api_calls=api_calls
)

expected_metadata = ChargebeeObjMetadata(seats=10, api_calls=50000000, projects=10)
expected_metadata = ChargebeeObjMetadata(
seats=seats, api_calls=api_calls, projects=10
)
mock_cb_get_subscription_metadata = mocker.patch(
"organisations.models.get_subscription_metadata"
"organisations.models.Subscription.get_subscription_metadata"
)
mock_cb_get_subscription_metadata.return_value = expected_metadata

# When
subscription_metadata = subscription.get_subscription_metadata()
subscription_metadata = organisation.subscription.get_subscription_metadata()

# Then
mock_cb_get_subscription_metadata.assert_called_once_with(
subscription.subscription_id
)
mock_cb_get_subscription_metadata.assert_called_once_with()

assert subscription_metadata == expected_metadata


Expand Down
Loading