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.utils import assert_is_not_none
|
||||
from zerver.models import Realm, RealmAuditLog, UserProfile, get_system_bot
|
||||
from zilencer.models import RemoteZulipServer, RemoteZulipServerAuditLog
|
||||
from zproject.config import get_secret
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@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
|
||||
@catch_stripe_errors
|
||||
def process_initial_upgrade(
|
||||
|
|
|
@ -36,6 +36,7 @@ from corporate.lib.stripe import (
|
|||
catch_stripe_errors,
|
||||
compute_plan_parameters,
|
||||
customer_has_credit_card_as_default_payment_method,
|
||||
do_change_remote_server_plan_type,
|
||||
do_create_stripe_customer,
|
||||
downgrade_small_realms_behind_on_payments_as_needed,
|
||||
get_discount_for_realm,
|
||||
|
@ -97,6 +98,7 @@ from zerver.models import (
|
|||
get_realm,
|
||||
get_system_bot,
|
||||
)
|
||||
from zilencer.models import RemoteZulipServer, RemoteZulipServerAuditLog
|
||||
|
||||
CallableT = TypeVar("CallableT", bound=Callable[..., Any])
|
||||
|
||||
|
@ -4418,6 +4420,30 @@ class BillingHelpersTest(ZulipTestCase):
|
|||
realm.save()
|
||||
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):
|
||||
def test_add_plan_renewal_if_needed(self) -> None:
|
||||
|
|
|
@ -3883,6 +3883,11 @@ class AbstractRealmAuditLog(models.Model):
|
|||
STREAM_NAME_CHANGED = 603
|
||||
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_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):
|
||||
"""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
|
||||
API_KEY_LENGTH = 64
|
||||
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)
|
||||
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)
|
||||
contact_email: str = models.EmailField(blank=True, null=False)
|
||||
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:
|
||||
return f"<RemoteZulipServer {self.hostname} {self.uuid[0:12]}>"
|
||||
|
||||
|
@ -33,8 +50,9 @@ class RemoteZulipServer(models.Model):
|
|||
return "zulip-server:" + self.uuid
|
||||
|
||||
|
||||
# Variant of PushDeviceToken for a remote server.
|
||||
class RemotePushDeviceToken(AbstractPushDeviceToken):
|
||||
"""Like PushDeviceToken, but for a device connected to a remote server."""
|
||||
|
||||
server: RemoteZulipServer = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
||||
# The user id on the remote server for this device device this is
|
||||
user_id: int = models.BigIntegerField(db_index=True)
|
||||
|
@ -46,6 +64,22 @@ class RemotePushDeviceToken(AbstractPushDeviceToken):
|
|||
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):
|
||||
"""Synced audit data from a remote Zulip server, used primarily for
|
||||
billing. See RealmAuditLog and AbstractRealmAuditLog for details.
|
||||
|
|
Loading…
Reference in New Issue