From 9bb338be111cdda1c569d250984a26c63cc370e9 Mon Sep 17 00:00:00 2001 From: Vishnu Ks Date: Fri, 10 Aug 2018 01:08:22 +0530 Subject: [PATCH] models: Add plan_type to Realm. --- zerver/lib/actions.py | 11 +++++++++++ zerver/migrations/0185_realm_plan_type.py | 22 ++++++++++++++++++++++ zerver/models.py | 10 ++++++++++ zerver/tests/test_realm.py | 7 +++++++ zilencer/lib/stripe.py | 2 ++ zilencer/tests/test_stripe.py | 15 +++++++++++++-- zproject/dev_settings.py | 2 ++ zproject/settings.py | 4 ++++ 8 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 zerver/migrations/0185_realm_plan_type.py diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 4ba2720364..d567f47add 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -3018,6 +3018,15 @@ def do_change_icon_source(realm: Realm, icon_source: str, log: bool=True) -> Non icon_url=realm_icon_url(realm))), active_user_ids(realm.id)) +def do_change_plan_type(user: UserProfile, plan_type: int) -> None: + realm = user.realm + old_value = realm.plan_type + realm.plan_type = plan_type + realm.save(update_fields=['plan_type']) + RealmAuditLog.objects.create(event_type=RealmAuditLog.REALM_PLAN_TYPE_CHANGED, + realm=realm, acting_user=user, event_time=timezone_now(), + extra_data={'old_value': old_value, 'new_value': plan_type}) + def do_change_default_sending_stream(user_profile: UserProfile, stream: Optional[Stream], log: bool=True) -> None: user_profile.default_sending_stream = stream @@ -3194,6 +3203,8 @@ def do_create_realm(string_id: str, name: str, kwargs = {} # type: Dict[str, Any] if emails_restricted_to_domains is not None: kwargs['emails_restricted_to_domains'] = emails_restricted_to_domains + if settings.BILLING_ENABLED: + kwargs['plan_type'] = Realm.LIMITED realm = Realm(string_id=string_id, name=name, **kwargs) realm.save() diff --git a/zerver/migrations/0185_realm_plan_type.py b/zerver/migrations/0185_realm_plan_type.py new file mode 100644 index 0000000000..13701f70c3 --- /dev/null +++ b/zerver/migrations/0185_realm_plan_type.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.14 on 2018-08-10 21:36 +from __future__ import unicode_literals + +from django.db import migrations, models +import zerver.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('zerver', '0184_rename_custom_field_types'), + ] + + operations = [ + migrations.AddField( + model_name='realm', + name='plan_type', + # Realm.SELF_HOSTED + field=models.PositiveSmallIntegerField(default=1), + ), + ] diff --git a/zerver/models.py b/zerver/models.py index 4f3f045d36..6e5b3534a5 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -223,6 +223,15 @@ class Realm(models.Model): COMMUNITY = 2 org_type = models.PositiveSmallIntegerField(default=CORPORATE) # type: int + # plan_type controls various features around resource/feature + # limitations for a Zulip organization on multi-tenant servers + # like zulipchat.com. + SELF_HOSTED = 1 + LIMITED = 2 + PREMIUM = 3 + PREMIUM_FREE = 4 + plan_type = models.PositiveSmallIntegerField(default=SELF_HOSTED) # type: int + # This value is also being used in static/js/settings_bots.bot_creation_policy_values. # On updating it here, update it there as well. BOT_CREATION_EVERYONE = 1 @@ -2189,6 +2198,7 @@ class RealmAuditLog(models.Model): REALM_DEACTIVATED = 'realm_deactivated' REALM_REACTIVATED = 'realm_reactivated' + REALM_PLAN_TYPE_CHANGED = 'realm_plan_type_changed' SUBSCRIPTION_CREATED = 'subscription_created' SUBSCRIPTION_ACTIVATED = 'subscription_activated' diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index baaa2db702..f2328c0d2e 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -11,6 +11,7 @@ from zerver.lib.actions import ( do_set_realm_property, do_deactivate_realm, do_deactivate_stream, + do_create_realm, ) from zerver.lib.send_email import send_future_email @@ -334,6 +335,12 @@ class RealmTest(ZulipTestCase): self.assert_json_success(result) self.assertEqual(get_realm('zulip').video_chat_provider, "Jitsi") + def test_initial_plan_type(self) -> None: + with self.settings(BILLING_ENABLED=True): + self.assertEqual(Realm.LIMITED, do_create_realm('hosted', 'hosted').plan_type) + with self.settings(BILLING_ENABLED=False): + self.assertEqual(Realm.SELF_HOSTED, do_create_realm('onpremise', 'onpremise').plan_type) + class RealmAPITest(ZulipTestCase): def setUp(self) -> None: diff --git a/zilencer/lib/stripe.py b/zilencer/lib/stripe.py index ce356582d9..ee26ef33fb 100644 --- a/zilencer/lib/stripe.py +++ b/zilencer/lib/stripe.py @@ -16,6 +16,7 @@ from zerver.lib.exceptions import JsonableError from zerver.lib.logging_util import log_to_file from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.utils import generate_random_token +from zerver.lib.actions import do_change_plan_type from zerver.models import Realm, UserProfile, RealmAuditLog from zilencer.models import Customer, Plan, BillingProcessor from zproject.settings import get_secret @@ -228,6 +229,7 @@ def process_initial_upgrade(user: UserProfile, plan: Plan, seat_count: int, stri # TODO: billing address details are passed to us in the request; # use that to calculate taxes. tax_percent=0) + do_change_plan_type(user, Realm.PREMIUM) ## Process RealmAuditLog diff --git a/zilencer/tests/test_stripe.py b/zilencer/tests/test_stripe.py index 6cb85911d4..72c46c44c6 100644 --- a/zilencer/tests/test_stripe.py +++ b/zilencer/tests/test_stripe.py @@ -50,6 +50,12 @@ def mock_customer_with_cancel_at_period_end_subscription(*args: Any, **kwargs: A def mock_upcoming_invoice(*args: Any, **kwargs: Any) -> stripe.Invoice: return stripe.util.convert_to_stripe_object(fixture_data["upcoming_invoice"]) +# A Kandra is a fictional character that can become anything. Used as a +# wildcard when testing for equality. +class Kandra(object): + def __eq__(self, other: Any) -> bool: + return True + class StripeTest(ZulipTestCase): def setUp(self) -> None: self.token = 'token' @@ -103,6 +109,7 @@ class StripeTest(ZulipTestCase): response = self.client_get("/upgrade/") self.assert_in_success_response(['We can also bill by invoice'], response) self.assertFalse(user.realm.has_seat_based_plan) + self.assertNotEqual(user.realm.plan_type, Realm.PREMIUM) # Click "Make payment" in Stripe Checkout self.client_post("/upgrade/", { @@ -135,10 +142,12 @@ class StripeTest(ZulipTestCase): (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(self.customer_created)), (RealmAuditLog.STRIPE_CARD_ADDED, timestamp_to_datetime(self.customer_created)), (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(self.subscription_created)), + (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertTrue(realm.has_seat_based_plan) + self.assertEqual(realm.plan_type, Realm.PREMIUM) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) @@ -202,12 +211,13 @@ class StripeTest(ZulipTestCase): # correctly handled the requires_billing_update field audit_log_entries = list(RealmAuditLog.objects.order_by('-id') .values_list('event_type', 'event_time', - 'requires_billing_update')[:4])[::-1] + 'requires_billing_update')[:5])[::-1] self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(self.customer_created), False), (RealmAuditLog.STRIPE_CARD_ADDED, timestamp_to_datetime(self.customer_created), False), (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(self.subscription_created), False), (RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET, timestamp_to_datetime(self.subscription_created), True), + (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra(), False), ]) self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( event_type=RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET).values_list('extra_data', flat=True).first()), @@ -261,7 +271,8 @@ class StripeTest(ZulipTestCase): self.assertEqual(audit_log_entries, [RealmAuditLog.STRIPE_CUSTOMER_CREATED, RealmAuditLog.STRIPE_CARD_ADDED, RealmAuditLog.STRIPE_CARD_ADDED, - RealmAuditLog.STRIPE_PLAN_CHANGED]) + RealmAuditLog.STRIPE_PLAN_CHANGED, + RealmAuditLog.REALM_PLAN_TYPE_CHANGED]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertTrue(realm.has_seat_based_plan) diff --git a/zproject/dev_settings.py b/zproject/dev_settings.py index 476114936b..c68525301a 100644 --- a/zproject/dev_settings.py +++ b/zproject/dev_settings.py @@ -123,3 +123,5 @@ if FAKE_LDAP_MODE: THUMBOR_URL = 'http://127.0.0.1:9995' SEARCH_PILLS_ENABLED = os.getenv('SEARCH_PILLS_ENABLED', False) + +BILLING_ENABLED = True diff --git a/zproject/settings.py b/zproject/settings.py index 6ea08e1a60..e6eb25bb35 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -437,6 +437,10 @@ DEFAULT_SETTINGS.update({ # DEFAULT_SETTINGS, since it likely isn't usefully user-configurable. 'OFFLINE_THRESHOLD_SECS': 5 * 60, + # Enables billing pages and plan-based feature gates. If False, all features + # are available to all realms. + 'BILLING_ENABLED': False, + # Controls whether we run the worker that syncs billing-related updates # into Stripe. Should be True on at most one machine. 'BILLING_PROCESSOR_ENABLED': False,