Skip to content

Commit

Permalink
feat: Use get-metadata-subscription to get max_api_calls (#2279)
Browse files Browse the repository at this point in the history
  • Loading branch information
novakzaballa committed Aug 22, 2023
1 parent 49754e5 commit 42049fc
Show file tree
Hide file tree
Showing 22 changed files with 482 additions and 153 deletions.
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

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

3 comments on commit 42049fc

@vercel
Copy link

@vercel vercel bot commented on 42049fc Aug 22, 2023

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on 42049fc Aug 22, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

docs – ./docs

docs-git-main-flagsmith.vercel.app
docs-flagsmith.vercel.app
docs.bullet-train.io
docs.flagsmith.com

@vercel
Copy link

@vercel vercel bot commented on 42049fc Aug 22, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.