From 1b37c99d93471def56a5042afb78e15cc2d7727e Mon Sep 17 00:00:00 2001 From: Matthew Elwell Date: Thu, 31 Aug 2023 14:38:28 +0100 Subject: [PATCH] fix: make `OrganisationSubscriptionInformationCache.allowed_projects` nullable (#2716) --- .../0046_allow_allowed_projects_to_be_null.py | 47 +++++++++++++++++++ api/organisations/models.py | 2 +- api/organisations/tests/test_views.py | 40 ++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 api/organisations/migrations/0046_allow_allowed_projects_to_be_null.py diff --git a/api/organisations/migrations/0046_allow_allowed_projects_to_be_null.py b/api/organisations/migrations/0046_allow_allowed_projects_to_be_null.py new file mode 100644 index 000000000000..d0fe544843fd --- /dev/null +++ b/api/organisations/migrations/0046_allow_allowed_projects_to_be_null.py @@ -0,0 +1,47 @@ +# Generated by Django 3.2.20 on 2023-08-31 11:31 +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def update_allowed_projects_for_all_paid_subscriptions( + apps: Apps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + organisation_subscription_information_cache_model = apps.get_model( + "organisations", "organisationsubscriptioninformationcache" + ) + organisation_subscription_information_cache_model.objects.exclude( + organisation__subscription__plan="free" + ).update(allowed_projects=None) + + +def reverse(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + """ + Set the values for the OrganisationSubscriptionInformationCache objects back to 1 (which + is incorrect for paid subscriptions, but necessary to avoid IntegrityError when reversing + migrations) + """ + + organisation_subscription_information_cache_model = apps.get_model( + "organisations", "organisationsubscriptioninformationcache" + ) + organisation_subscription_information_cache_model.objects.exclude( + organisation__subscription__plan="free" + ).update(allowed_projects=1) + + +class Migration(migrations.Migration): + dependencies = [ + ("organisations", "0045_auto_20230802_1956"), + ] + + operations = [ + migrations.AlterField( + model_name="organisationsubscriptioninformationcache", + name="allowed_projects", + field=models.IntegerField(default=1, blank=True, null=True), + ), + migrations.RunPython( + update_allowed_projects_for_all_paid_subscriptions, reverse_code=reverse + ), + ] diff --git a/api/organisations/models.py b/api/organisations/models.py index dbea4a7fa885..fb4b596b2ebe 100644 --- a/api/organisations/models.py +++ b/api/organisations/models.py @@ -295,6 +295,6 @@ class OrganisationSubscriptionInformationCache(models.Model): allowed_seats = models.IntegerField(default=1) allowed_30d_api_calls = models.IntegerField(default=50000) - allowed_projects = models.IntegerField(default=1) + allowed_projects = models.IntegerField(default=1, blank=True, null=True) chargebee_email = models.EmailField(blank=True, max_length=254, null=True) diff --git a/api/organisations/tests/test_views.py b/api/organisations/tests/test_views.py index 5920afc99047..5aefb206c8d6 100644 --- a/api/organisations/tests/test_views.py +++ b/api/organisations/tests/test_views.py @@ -1,6 +1,7 @@ import json from datetime import datetime, timedelta from unittest import TestCase, mock +from unittest.mock import MagicMock import pytest from django.conf import settings @@ -582,6 +583,45 @@ def setUp(self) -> None: ) self.subscription = Subscription.objects.get(organisation=self.organisation) + @mock.patch("organisations.views.extract_subscription_metadata") + def test_chargebee_webhook( + self, mock_extract_subscription_metadata: MagicMock + ) -> None: + # Given + seats = 3 + api_calls = 100 + mock_extract_subscription_metadata.return_value = ChargebeeObjMetadata( + seats=seats, + api_calls=api_calls, + projects=None, + chargebee_email=self.cb_user.email, + ) + data = { + "content": { + "subscription": { + "status": "active", + "id": self.subscription_id, + }, + "customer": {"email": self.cb_user.email}, + } + } + + # When + response = self.client.post( + self.url, data=json.dumps(data), content_type="application/json" + ) + + # Then + assert response.status_code == status.HTTP_200_OK + + self.subscription.refresh_from_db() + subscription_cache = OrganisationSubscriptionInformationCache.objects.get( + organisation=self.subscription.organisation + ) + assert subscription_cache.allowed_projects is None + assert subscription_cache.allowed_30d_api_calls == api_calls + assert subscription_cache.allowed_seats == seats + @mock.patch("organisations.models.cancel_chargebee_subscription") def test_when_subscription_is_set_to_non_renewing_then_cancellation_date_set_and_alert_sent( self, mocked_cancel_chargebee_subscription