mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 03:53:50 +00:00 
			
		
		
		
	billing: Add do_change_remote_server_plan_type.
This is a part of the plumbing we need to support billing for self-hosted customers. With documentation changes from tabbott.
This commit is contained in:
		| @@ -32,6 +32,7 @@ from zerver.lib.send_email import FromAddress, send_email_to_billing_admins_and_ | |||||||
| from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime | from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime | ||||||
| from zerver.lib.utils import assert_is_not_none | from zerver.lib.utils import assert_is_not_none | ||||||
| from zerver.models import Realm, RealmAuditLog, UserProfile, get_system_bot | from zerver.models import Realm, RealmAuditLog, UserProfile, get_system_bot | ||||||
|  | from zilencer.models import RemoteZulipServer, RemoteZulipServerAuditLog | ||||||
| from zproject.config import get_secret | from zproject.config import get_secret | ||||||
|  |  | ||||||
| stripe.api_key = get_secret("stripe_secret_key") | stripe.api_key = get_secret("stripe_secret_key") | ||||||
| @@ -619,6 +620,19 @@ def ensure_realm_does_not_have_active_plan(realm: Customer) -> None: | |||||||
|         raise UpgradeWithExistingPlanError() |         raise UpgradeWithExistingPlanError() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @transaction.atomic | ||||||
|  | def do_change_remote_server_plan_type(remote_server: RemoteZulipServer, plan_type: int) -> None: | ||||||
|  |     old_value = remote_server.plan_type | ||||||
|  |     remote_server.plan_type = plan_type | ||||||
|  |     remote_server.save(update_fields=["plan_type"]) | ||||||
|  |     RemoteZulipServerAuditLog.objects.create( | ||||||
|  |         event_type=RealmAuditLog.REMOTE_SERVER_PLAN_TYPE_CHANGED, | ||||||
|  |         server=remote_server, | ||||||
|  |         event_time=timezone_now(), | ||||||
|  |         extra_data={"old_value": old_value, "new_value": plan_type}, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| # Only used for cloud signups | # Only used for cloud signups | ||||||
| @catch_stripe_errors | @catch_stripe_errors | ||||||
| def process_initial_upgrade( | def process_initial_upgrade( | ||||||
|   | |||||||
| @@ -36,6 +36,7 @@ from corporate.lib.stripe import ( | |||||||
|     catch_stripe_errors, |     catch_stripe_errors, | ||||||
|     compute_plan_parameters, |     compute_plan_parameters, | ||||||
|     customer_has_credit_card_as_default_payment_method, |     customer_has_credit_card_as_default_payment_method, | ||||||
|  |     do_change_remote_server_plan_type, | ||||||
|     do_create_stripe_customer, |     do_create_stripe_customer, | ||||||
|     downgrade_small_realms_behind_on_payments_as_needed, |     downgrade_small_realms_behind_on_payments_as_needed, | ||||||
|     get_discount_for_realm, |     get_discount_for_realm, | ||||||
| @@ -97,6 +98,7 @@ from zerver.models import ( | |||||||
|     get_realm, |     get_realm, | ||||||
|     get_system_bot, |     get_system_bot, | ||||||
| ) | ) | ||||||
|  | from zilencer.models import RemoteZulipServer, RemoteZulipServerAuditLog | ||||||
|  |  | ||||||
| CallableT = TypeVar("CallableT", bound=Callable[..., Any]) | CallableT = TypeVar("CallableT", bound=Callable[..., Any]) | ||||||
|  |  | ||||||
| @@ -4418,6 +4420,30 @@ class BillingHelpersTest(ZulipTestCase): | |||||||
|         realm.save() |         realm.save() | ||||||
|         self.assertTrue(is_sponsored_realm(realm)) |         self.assertTrue(is_sponsored_realm(realm)) | ||||||
|  |  | ||||||
|  |     def test_change_remote_server_plan_type(self) -> None: | ||||||
|  |         server_uuid = "demo-1234" | ||||||
|  |         remote_server = RemoteZulipServer.objects.create( | ||||||
|  |             uuid=server_uuid, | ||||||
|  |             api_key="magic_secret_api_key", | ||||||
|  |             hostname="demo.example.com", | ||||||
|  |             contact_email="email@example.com", | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_HOSTED) | ||||||
|  |  | ||||||
|  |         do_change_remote_server_plan_type(remote_server, RemoteZulipServer.PLAN_TYPE_STANDARD) | ||||||
|  |  | ||||||
|  |         remote_server = RemoteZulipServer.objects.get(uuid=server_uuid) | ||||||
|  |         remote_realm_audit_log = RemoteZulipServerAuditLog.objects.filter( | ||||||
|  |             event_type=RealmAuditLog.REMOTE_SERVER_PLAN_TYPE_CHANGED | ||||||
|  |         ).last() | ||||||
|  |         assert remote_realm_audit_log is not None | ||||||
|  |         expected_extra_data = { | ||||||
|  |             "old_value": RemoteZulipServer.PLAN_TYPE_SELF_HOSTED, | ||||||
|  |             "new_value": RemoteZulipServer.PLAN_TYPE_STANDARD, | ||||||
|  |         } | ||||||
|  |         self.assertEqual(remote_realm_audit_log.extra_data, str(expected_extra_data)) | ||||||
|  |         self.assertEqual(remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_STANDARD) | ||||||
|  |  | ||||||
|  |  | ||||||
| class LicenseLedgerTest(StripeTestCase): | class LicenseLedgerTest(StripeTestCase): | ||||||
|     def test_add_plan_renewal_if_needed(self) -> None: |     def test_add_plan_renewal_if_needed(self) -> None: | ||||||
|   | |||||||
| @@ -3883,6 +3883,11 @@ class AbstractRealmAuditLog(models.Model): | |||||||
|     STREAM_NAME_CHANGED = 603 |     STREAM_NAME_CHANGED = 603 | ||||||
|     STREAM_REACTIVATED = 604 |     STREAM_REACTIVATED = 604 | ||||||
|  |  | ||||||
|  |     # The following values are only for RemoteZulipServerAuditLog | ||||||
|  |     # Values are chosen to be 10000 greater than the value in RealmAuditLog. | ||||||
|  |     REMOTE_SERVER_CREATED = 10215 | ||||||
|  |     REMOTE_SERVER_PLAN_TYPE_CHANGED = 10204 | ||||||
|  |  | ||||||
|     event_type: int = models.PositiveSmallIntegerField() |     event_type: int = models.PositiveSmallIntegerField() | ||||||
|  |  | ||||||
|     # event_types synced from on-prem installations to Zulip Cloud when |     # event_types synced from on-prem installations to Zulip Cloud when | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								zilencer/migrations/0019_remotezulipserver_plan_type.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								zilencer/migrations/0019_remotezulipserver_plan_type.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | # Generated by Django 3.2.9 on 2021-12-01 16:24 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("zilencer", "0018_remoterealmauditlog"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="remotezulipserver", | ||||||
|  |             name="plan_type", | ||||||
|  |             field=models.PositiveSmallIntegerField(default=1), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										38
									
								
								zilencer/migrations/0020_remotezulipserverauditlog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								zilencer/migrations/0020_remotezulipserverauditlog.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | # Generated by Django 3.2.9 on 2021-12-06 18:35 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("zilencer", "0019_remotezulipserver_plan_type"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name="RemoteZulipServerAuditLog", | ||||||
|  |             fields=[ | ||||||
|  |                 ( | ||||||
|  |                     "id", | ||||||
|  |                     models.AutoField( | ||||||
|  |                         auto_created=True, primary_key=True, serialize=False, verbose_name="ID" | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ("event_time", models.DateTimeField(db_index=True)), | ||||||
|  |                 ("backfilled", models.BooleanField(default=False)), | ||||||
|  |                 ("extra_data", models.TextField(null=True)), | ||||||
|  |                 ("event_type", models.PositiveSmallIntegerField()), | ||||||
|  |                 ( | ||||||
|  |                     "server", | ||||||
|  |                     models.ForeignKey( | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver" | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 "abstract": False, | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -15,17 +15,34 @@ def get_remote_server_by_uuid(uuid: str) -> "RemoteZulipServer": | |||||||
|  |  | ||||||
|  |  | ||||||
| class RemoteZulipServer(models.Model): | class RemoteZulipServer(models.Model): | ||||||
|  |     """Each object corresponds to a single remote Zulip server that is | ||||||
|  |     registered for the Mobile Push Notifications Service via | ||||||
|  |     `manage.py register_server`. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|     UUID_LENGTH = 36 |     UUID_LENGTH = 36 | ||||||
|     API_KEY_LENGTH = 64 |     API_KEY_LENGTH = 64 | ||||||
|     HOSTNAME_MAX_LENGTH = 128 |     HOSTNAME_MAX_LENGTH = 128 | ||||||
|  |  | ||||||
|  |     # The unique UUID (`zulip_org_id`) and API key (`zulip_org_key`) | ||||||
|  |     # for this remote server registration. | ||||||
|     uuid: str = models.CharField(max_length=UUID_LENGTH, unique=True) |     uuid: str = models.CharField(max_length=UUID_LENGTH, unique=True) | ||||||
|     api_key: str = models.CharField(max_length=API_KEY_LENGTH) |     api_key: str = models.CharField(max_length=API_KEY_LENGTH) | ||||||
|  |  | ||||||
|  |     # The hostname and contact details are not verified/trusted. Thus, | ||||||
|  |     # they primarily exist so that we can communicate with the | ||||||
|  |     # maintainer of a server about abuse problems. | ||||||
|     hostname: str = models.CharField(max_length=HOSTNAME_MAX_LENGTH) |     hostname: str = models.CharField(max_length=HOSTNAME_MAX_LENGTH) | ||||||
|     contact_email: str = models.EmailField(blank=True, null=False) |     contact_email: str = models.EmailField(blank=True, null=False) | ||||||
|     last_updated: datetime.datetime = models.DateTimeField("last updated", auto_now=True) |     last_updated: datetime.datetime = models.DateTimeField("last updated", auto_now=True) | ||||||
|  |  | ||||||
|  |     # Plan types for self-hosted customers | ||||||
|  |     PLAN_TYPE_SELF_HOSTED = 1 | ||||||
|  |     PLAN_TYPE_STANDARD = 102 | ||||||
|  |  | ||||||
|  |     # The current billing plan for the remote server, similar to Realm.plan_type. | ||||||
|  |     plan_type: int = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_HOSTED) | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|         return f"<RemoteZulipServer {self.hostname} {self.uuid[0:12]}>" |         return f"<RemoteZulipServer {self.hostname} {self.uuid[0:12]}>" | ||||||
|  |  | ||||||
| @@ -33,8 +50,9 @@ class RemoteZulipServer(models.Model): | |||||||
|         return "zulip-server:" + self.uuid |         return "zulip-server:" + self.uuid | ||||||
|  |  | ||||||
|  |  | ||||||
| # Variant of PushDeviceToken for a remote server. |  | ||||||
| class RemotePushDeviceToken(AbstractPushDeviceToken): | class RemotePushDeviceToken(AbstractPushDeviceToken): | ||||||
|  |     """Like PushDeviceToken, but for a device connected to a remote server.""" | ||||||
|  |  | ||||||
|     server: RemoteZulipServer = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE) |     server: RemoteZulipServer = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE) | ||||||
|     # The user id on the remote server for this device device this is |     # The user id on the remote server for this device device this is | ||||||
|     user_id: int = models.BigIntegerField(db_index=True) |     user_id: int = models.BigIntegerField(db_index=True) | ||||||
| @@ -46,6 +64,22 @@ class RemotePushDeviceToken(AbstractPushDeviceToken): | |||||||
|         return f"<RemotePushDeviceToken {self.server} {self.user_id}>" |         return f"<RemotePushDeviceToken {self.server} {self.user_id}>" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RemoteZulipServerAuditLog(AbstractRealmAuditLog): | ||||||
|  |     """Audit data associated with a remote Zulip server (not specific to a | ||||||
|  |     realm).  Used primarily for tracking registration and billing | ||||||
|  |     changes for self-hosted customers. | ||||||
|  |  | ||||||
|  |     In contrast with RemoteRealmAuditLog, which has a copy of data | ||||||
|  |     that is generated on the client Zulip server, this table is the | ||||||
|  |     authoritative storage location for the server's history. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     server: RemoteZulipServer = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"<RemoteZulipServerAuditLog: {self.server} {self.event_type} {self.event_time} {self.id}>" | ||||||
|  |  | ||||||
|  |  | ||||||
| class RemoteRealmAuditLog(AbstractRealmAuditLog): | class RemoteRealmAuditLog(AbstractRealmAuditLog): | ||||||
|     """Synced audit data from a remote Zulip server, used primarily for |     """Synced audit data from a remote Zulip server, used primarily for | ||||||
|     billing.  See RealmAuditLog and AbstractRealmAuditLog for details. |     billing.  See RealmAuditLog and AbstractRealmAuditLog for details. | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user