support: Add active plan information to remote server activity.

Adds three columns to the remote server activity chart and updates
the chart key for the third of those columns.

The first is the plan name. If there are multiple plans with a
status under the live threshhold, then we send "See support view".

The second is the plan status. If there are multiple plans, then
we send "Multiple plans".

The third is the estimated annual revenue for the plan. Note that
for free trials, this will be calculated as if the plan was paid
for 12 months (so a full year).

If there is no plan for the server under the live threshold or at
all then "---" is inserted into the table row. Note that 100%
sponsored servers/realms would fall into this category.
This commit is contained in:
Lauryn Menard 2023-12-12 19:48:15 +01:00 committed by Tim Abbott
parent 484c0df076
commit a897d68d93
4 changed files with 109 additions and 6 deletions

View File

@ -1,11 +1,18 @@
import uuid
from datetime import timedelta
from unittest import mock
from django.utils.timezone import now as timezone_now
from corporate.lib.stripe import add_months
from corporate.models import Customer, CustomerPlan, LicenseLedger
from zerver.lib.test_classes import ZulipTestCase
from zerver.models import Client, UserActivity, UserProfile
from zilencer.models import RemoteRealmAuditLog, get_remote_server_guest_and_non_guest_count
from zilencer.models import (
RemoteRealmAuditLog,
RemoteZulipServer,
get_remote_server_guest_and_non_guest_count,
)
event_time = timezone_now() - timedelta(days=3)
data_list = [
@ -99,8 +106,32 @@ class ActivityTest(ZulipTestCase):
result = self.client_get("/activity")
self.assertEqual(result.status_code, 200)
# Add data for remote activity page
RemoteRealmAuditLog.objects.bulk_create([RemoteRealmAuditLog(**data) for data in data_list])
with self.assert_database_query_count(6):
remote_server = RemoteZulipServer.objects.get(id=1)
customer = Customer.objects.create(remote_server=remote_server)
plan = CustomerPlan.objects.create(
customer=customer,
billing_cycle_anchor=timezone_now(),
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
price_per_license=8000,
next_invoice_date=add_months(timezone_now(), 12),
)
LicenseLedger.objects.create(
licenses=10,
licenses_at_next_renewal=10,
event_time=timezone_now(),
is_renewal=True,
plan=plan,
)
RemoteZulipServer.objects.create(
uuid=str(uuid.uuid4()),
api_key="magic_secret_api_key",
hostname="demo.example.com",
contact_email="email@example.com",
)
with self.assert_database_query_count(10):
result = self.client_get("/activity/remote")
self.assertEqual(result.status_code, 200)

View File

@ -10,6 +10,7 @@ from analytics.views.activity_common import (
remote_installation_stats_link,
remote_installation_support_link,
)
from corporate.lib.analytics import get_plan_data_by_remote_server
from zerver.decorator import require_server_admin
from zilencer.models import get_remote_server_guest_and_non_guest_count
@ -83,6 +84,9 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
"Mobile users",
"Last update time",
"Mobile pushes forwarded",
"Plan name",
"Plan status",
"ARR",
"Non guest users",
"Guest users",
"Links",
@ -91,13 +95,27 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
rows = get_query_data(query)
total_row = []
totals_columns = [4, 5]
plan_data_by_remote_server = get_plan_data_by_remote_server()
for row in rows:
stats = remote_installation_stats_link(row[0])
support = remote_installation_support_link(row[1])
links = stats + " " + support
# Add estimated revenue for server
server_plan_data = plan_data_by_remote_server.get(row[0])
if server_plan_data is None:
row.append("---")
row.append("---")
row.append("---")
else:
row.append(server_plan_data.current_plan_name)
row.append(server_plan_data.current_status)
row.append(server_plan_data.annual_revenue)
# Add user counts
remote_server_counts = get_remote_server_guest_and_non_guest_count(row[0])
row.append(remote_server_counts.non_guest_user_count)
row.append(remote_server_counts.guest_user_count)
# Add links
stats = remote_installation_stats_link(row[0])
support = remote_installation_support_link(row[1])
links = stats + " " + support
row.append(links)
for i, col in enumerate(cols):
if col == "Last update time":

View File

@ -1,13 +1,25 @@
from dataclasses import dataclass
from decimal import Decimal
from typing import Any, Dict
from django.utils.timezone import now as timezone_now
from corporate.lib.stripe import RealmBillingSession
from corporate.lib.stripe import (
RealmBillingSession,
RemoteRealmBillingSession,
RemoteServerBillingSession,
)
from corporate.models import Customer, CustomerPlan
from zerver.lib.utils import assert_is_not_none
@dataclass
class RemoteActivityPlanData:
current_status: str
current_plan_name: str
annual_revenue: int
def get_realms_with_default_discount_dict() -> Dict[str, Decimal]:
realms_with_default_discount: Dict[str, Any] = {}
customers = (
@ -39,3 +51,44 @@ def estimate_annual_recurring_revenue_by_realm() -> Dict[str, int]: # nocoverag
# TODO: Decimal stuff
annual_revenue[plan.customer.realm.string_id] = int(renewal_cents / 100)
return annual_revenue
def get_plan_data_by_remote_server() -> Dict[int, RemoteActivityPlanData]: # nocoverage
remote_server_plan_data: Dict[int, RemoteActivityPlanData] = {}
for plan in CustomerPlan.objects.filter(
status__lt=CustomerPlan.LIVE_STATUS_THRESHOLD
).select_related("customer__remote_server", "customer__remote_realm"):
renewal_cents = 0
server_id = None
if plan.customer.remote_server is not None:
server_id = plan.customer.remote_server.id
renewal_cents = RemoteServerBillingSession(
remote_server=plan.customer.remote_server
).get_customer_plan_renewal_amount(plan, timezone_now())
elif plan.customer.remote_realm is not None:
server_id = plan.customer.remote_realm.server.id
renewal_cents = RemoteRealmBillingSession(
remote_realm=plan.customer.remote_realm
).get_customer_plan_renewal_amount(plan, timezone_now())
assert server_id is not None
if plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY:
renewal_cents *= 12
current_data = remote_server_plan_data.get(server_id)
if current_data is not None:
current_revenue = remote_server_plan_data[server_id].annual_revenue
remote_server_plan_data[server_id] = RemoteActivityPlanData(
current_status="Multiple plans",
current_plan_name="See support view",
annual_revenue=current_revenue + int(renewal_cents / 100),
)
else:
remote_server_plan_data[server_id] = RemoteActivityPlanData(
current_status=plan.get_plan_status_as_text(),
current_plan_name=plan.name,
annual_revenue=int(renewal_cents / 100),
)
return remote_server_plan_data

View File

@ -2,6 +2,7 @@
<ul>
<li><strong>Zulip version</strong> - as of last update time</li>
<li><strong>Mobile pushes forwarded</strong> - last 7 days, including today's current count</li>
<li><strong>ARR</strong> (annual recurring revenue) - the number of users they are paying for * annual price/user</li>
<li><strong>Links</strong>
<ul>
<li><strong><i class="fa fa-pie-chart"></i></strong> - remote server's stats page</li>