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(organizations): update is_mmo to identify mmo subscriptions TASK-1231 #5289

Merged
merged 8 commits into from
Nov 28, 2024
Merged
9 changes: 8 additions & 1 deletion kobo/apps/organizations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,14 @@ def is_mmo(self):

If the override is enabled, it takes precedence over the subscription status
"""
return self.mmo_override or bool(self.active_subscription_billing_details())
if self.mmo_override:
return True

if billing_details := self.active_subscription_billing_details():
if product_metadata := billing_details.get('product_metadata'):
return product_metadata.get('mmo_enabled') == 'true'

return False
Comment on lines +183 to +190
Copy link
Contributor

Choose a reason for hiding this comment

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

@jamesrkiger we may change the docstring then ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. I have updated the text.


@cache_for_request
def is_admin_only(self, user: 'User') -> bool:
Expand Down
9 changes: 5 additions & 4 deletions kobo/apps/organizations/tests/test_organizations_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class OrganizationApiTestCase(BaseTestCase):
'current_period_start': '2024-01-01',
'current_period_end': '2024-12-31'
}
MMO_SUBSCRIPTION_DETAILS = {'product_metadata': {'mmo_enabled': 'true'}}

def setUp(self):
self.user = User.objects.get(username='someuser')
Expand Down Expand Up @@ -116,13 +117,13 @@ def test_api_response_includes_is_mmo_with_mmo_override(self):
@patch.object(
Organization,
'active_subscription_billing_details',
return_value=DEFAULT_SUBSCRIPTION_DETAILS
return_value=MMO_SUBSCRIPTION_DETAILS,
)
def test_api_response_includes_is_mmo_with_subscription(
self, mock_active_subscription
):
"""
Test that is_mmo is True when there is an active subscription.
Test that is_mmo is True when there is an active MMO subscription.
"""
self._insert_data(mmo_override=False)
response = self.client.get(self.url_detail)
Expand All @@ -149,14 +150,14 @@ def test_api_response_includes_is_mmo_with_no_override_and_no_subscription(
@patch.object(
Organization,
'active_subscription_billing_details',
return_value=DEFAULT_SUBSCRIPTION_DETAILS
return_value=MMO_SUBSCRIPTION_DETAILS,
)
def test_api_response_includes_is_mmo_with_override_and_subscription(
self, mock_active_subscription
):
"""
Test that is_mmo is True when both mmo_override and active
subscription is present.
MMO subscription is present.
"""
self._insert_data(mmo_override=True)
response = self.client.get(self.url_detail)
Expand Down
7 changes: 0 additions & 7 deletions kobo/apps/organizations/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import QuerySet
from django.utils.decorators import method_decorator
from django.utils.http import http_date
from django.views.decorators.cache import cache_page
from django_dont_vary_on.decorators import only_vary_on
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.request import Request
Expand Down Expand Up @@ -63,10 +60,6 @@ def get_queryset(self, *args, **kwargs):
raise NotImplementedError


@method_decorator(cache_page(settings.ENDPOINT_CACHE_DURATION), name='service_usage')
# django uses the Vary header in its caching, and each middleware can potentially add more Vary headers
# we use this decorator to remove any Vary headers except 'origin' (we don't want to cache between different installs)
@method_decorator(only_vary_on('Origin'), name='service_usage')
class OrganizationViewSet(viewsets.ModelViewSet):
"""
Organizations are groups of users with assigned permissions and configurations
Expand Down
57 changes: 41 additions & 16 deletions kobo/apps/stripe/tests/test_organization_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from kobo.apps.organizations.models import Organization, OrganizationUser
from kobo.apps.stripe.constants import USAGE_LIMIT_MAP
from kobo.apps.stripe.tests.utils import (
generate_enterprise_subscription,
generate_mmo_subscription,
generate_plan_subscription,
)
from kobo.apps.stripe.utils import get_organization_plan_limit
Expand Down Expand Up @@ -107,7 +107,7 @@ def test_usage_for_plans_with_org_access(self):
when viewing /service_usage/{organization_id}/
"""

generate_enterprise_subscription(self.organization)
generate_mmo_subscription(self.organization)

# the user should see usage for everyone in their org
response = self.client.get(self.detail_url)
Expand Down Expand Up @@ -138,7 +138,7 @@ def test_endpoint_speed(self):
# get the average request time for 10 hits to the endpoint
single_user_time = timeit.timeit(lambda: self.client.get(self.detail_url), number=10)

generate_enterprise_subscription(self.organization)
generate_mmo_subscription(self.organization)

# get the average request time for 10 hits to the endpoint
multi_user_time = timeit.timeit(lambda: self.client.get(self.detail_url), number=10)
Expand All @@ -151,7 +151,7 @@ def test_endpoint_is_cached(self):
"""
Test that multiple hits to the endpoint from the same origin are properly cached
"""
generate_enterprise_subscription(self.organization)
generate_mmo_subscription(self.organization)

first_response = self.client.get(self.detail_url)
assert first_response.data['total_submission_count']['current_month'] == self.expected_submissions_multi
Expand Down Expand Up @@ -438,31 +438,23 @@ def test_user_not_member_of_organization(self):
assert response.status_code == status.HTTP_400_BAD_REQUEST

def test_successful_retrieval(self):
generate_enterprise_subscription(self.organization)
generate_mmo_subscription(self.organization)
create_mock_assets([self.anotheruser])
response = self.client.get(self.detail_url)
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 1
assert response.data['results'][0]['asset__name'] == 'test'
assert response.data['results'][0]['deployment_status'] == 'deployed'

def test_aggregates_usage_for_enterprise_org(self):
generate_enterprise_subscription(self.organization)
def test_aggregates_usage_for_mmo(self):
generate_mmo_subscription(self.organization)
self.organization.add_user(self.newuser)
# create 2 additional assets, one per user
create_mock_assets([self.anotheruser, self.newuser])
response = self.client.get(self.detail_url)
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 2

def test_users_without_enterprise_see_only_their_usage(self):
noliveleger marked this conversation as resolved.
Show resolved Hide resolved
generate_plan_subscription(self.organization)
self.organization.add_user(self.newuser)
create_mock_assets([self.anotheruser, self.newuser])
response = self.client.get(self.detail_url)
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 1


@ddt
class OrganizationsUtilsTestCase(BaseTestCase):
Expand All @@ -478,7 +470,7 @@ def setUp(self):
self.organization.add_user(self.anotheruser, is_admin=True)

def test_get_plan_community_limit(self):
generate_enterprise_subscription(self.organization)
generate_mmo_subscription(self.organization)
limit = get_organization_plan_limit(self.organization, 'seconds')
assert limit == 2000 # TODO get the limits from the community plan, overrides
limit = get_organization_plan_limit(self.organization, 'characters')
Expand Down Expand Up @@ -509,3 +501,36 @@ def test_get_suscription_limit_unlimited(self, usage_type):
generate_plan_subscription(self.organization, metadata=product_metadata)
limit = get_organization_plan_limit(self.organization, usage_type)
assert limit == float('inf')


@override_settings(STRIPE_ENABLED=True)
class OrganizationsModelIntegrationTestCase(BaseTestCase):
fixtures = ['test_data']

def setUp(self):
self.someuser = User.objects.get(username='someuser')
self.organization = self.someuser.organization

def test_is_mmo_subscription_logic(self):
self.organization.mmo_override = True
self.organization.save()
assert self.organization.is_mmo is True

self.organization.mmo_override = False
self.organization.save()
noliveleger marked this conversation as resolved.
Show resolved Hide resolved
assert self.organization.is_mmo is False

product_metadata = {
'mmo_enabled': 'false',
}
subscription = generate_plan_subscription(
self.organization, metadata=product_metadata
)
assert self.organization.is_mmo is False
subscription.status = 'canceled'
subscription.ended_at = timezone.now()
subscription.save()

product_metadata['mmo_enabled'] = 'true'
generate_plan_subscription(self.organization, metadata=product_metadata)
assert self.organization.is_mmo is True
noliveleger marked this conversation as resolved.
Show resolved Hide resolved
22 changes: 10 additions & 12 deletions kobo/apps/stripe/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from dateutil.relativedelta import relativedelta
from django.utils import timezone
from djstripe.models import Customer, Product, SubscriptionItem, Subscription, Price
from djstripe.models import Customer, Price, Product, Subscription, SubscriptionItem
from model_bakery import baker

from kobo.apps.organizations.models import Organization
Expand All @@ -17,7 +17,6 @@ def generate_plan_subscription(
) -> Subscription:
"""Create a subscription for a product with custom metadata"""
created_date = timezone.now() - relativedelta(days=age_days)
price_id = 'price_sfmOFe33rfsfd36685657'

if not customer:
customer = baker.make(Customer, subscriber=organization, livemode=False)
Expand All @@ -30,14 +29,12 @@ def generate_plan_subscription(
product_metadata = {**product_metadata, **metadata}
product = baker.make(Product, active=True, metadata=product_metadata)

if not (price := Price.objects.filter(id=price_id).first()):
noliveleger marked this conversation as resolved.
Show resolved Hide resolved
price = baker.make(
Price,
active=True,
id=price_id,
recurring={'interval': interval},
product=product,
)
price = baker.make(
Price,
active=True,
recurring={'interval': interval},
product=product,
)

period_offset = relativedelta(weeks=2)

Expand All @@ -59,5 +56,6 @@ def generate_plan_subscription(
)


def generate_enterprise_subscription(organization: Organization, customer: Customer = None):
return generate_plan_subscription(organization, {'plan_type': 'enterprise'}, customer)
def generate_mmo_subscription(organization: Organization, customer: Customer = None):
product_metadata = {'mmo_enabled': 'true', 'plan_type': 'enterprise'}
return generate_plan_subscription(organization, product_metadata, customer)
4 changes: 2 additions & 2 deletions kpi/tests/test_usage_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from kobo.apps.kobo_auth.shortcuts import User
from kobo.apps.organizations.models import Organization
from kobo.apps.stripe.constants import USAGE_LIMIT_MAP
from kobo.apps.stripe.tests.utils import generate_enterprise_subscription
from kobo.apps.stripe.tests.utils import generate_mmo_subscription
from kobo.apps.trackers.models import NLPUsageCounter
from kpi.models import Asset
from kpi.tests.base_test_case import BaseAssetTestCase
Expand Down Expand Up @@ -201,7 +201,7 @@ def test_organization_setup(self):
organization = baker.make(Organization, id='org_abcd1234', mmo_override=True)
organization.add_user(user=self.anotheruser, is_admin=True)
organization.add_user(user=self.someuser, is_admin=True)
generate_enterprise_subscription(organization)
generate_mmo_subscription(organization)

calculator = ServiceUsageCalculator(self.someuser, organization)
submission_counters = calculator.get_submission_counters()
Expand Down