mirror of https://github.com/zulip/zulip.git
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:
parent
f533097487
commit
79e9ba13e2
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue