mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 16:14:02 +00:00
billing: Show estimated subscription revenue on /activity.
[Substantial edits by Rishi Gupta]
This commit is contained in:
@@ -21,6 +21,7 @@ from django.template import RequestContext, loader
|
||||
from django.utils.timezone import now as timezone_now, utc as timezone_utc
|
||||
from django.utils.translation import ugettext as _
|
||||
from jinja2 import Markup as mark_safe
|
||||
import stripe
|
||||
|
||||
from analytics.lib.counts import COUNT_STATS, CountStat, process_count_stat
|
||||
from analytics.lib.time_utils import time_range
|
||||
@@ -36,6 +37,7 @@ from zerver.lib.timestamp import ceiling_to_day, \
|
||||
ceiling_to_hour, convert_to_UTC, timestamp_to_datetime
|
||||
from zerver.models import Client, get_realm, Realm, \
|
||||
UserActivity, UserActivityInterval, UserProfile
|
||||
from zproject.settings import get_secret
|
||||
|
||||
def render_stats(request: HttpRequest, data_url_suffix: str, target_name: str,
|
||||
for_installation: bool=False) -> HttpRequest:
|
||||
@@ -490,6 +492,25 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
|
||||
except Exception:
|
||||
row['history'] = ''
|
||||
|
||||
# estimate annual subscription revenue
|
||||
total_amount = 0
|
||||
if settings.BILLING_ENABLED:
|
||||
from corporate.lib.stripe import estimate_customer_arr
|
||||
from corporate.models import Customer
|
||||
stripe.api_key = get_secret('stripe_secret_key')
|
||||
estimated_arr = {}
|
||||
try:
|
||||
for stripe_customer in stripe.Customer.list(limit=100):
|
||||
# TODO: could do a select_related to get the realm.string_id, potentially
|
||||
customer = Customer.objects.filter(stripe_customer_id=stripe_customer.id).first()
|
||||
if customer is not None:
|
||||
estimated_arr[customer.realm.string_id] = estimate_customer_arr(stripe_customer)
|
||||
except stripe.error.StripeError:
|
||||
pass
|
||||
for row in rows:
|
||||
row['amount'] = estimated_arr.get(row['string_id'], None)
|
||||
total_amount = sum(estimated_arr.values())
|
||||
|
||||
# augment data with realm_minutes
|
||||
total_hours = 0.0
|
||||
for row in rows:
|
||||
@@ -528,6 +549,7 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
|
||||
total_row = dict(
|
||||
string_id='Total',
|
||||
plan_type_string="",
|
||||
amount=total_amount,
|
||||
stats_link = '',
|
||||
date_created_day='',
|
||||
realm_admin_email='',
|
||||
|
@@ -164,6 +164,18 @@ def extract_current_subscription(stripe_customer: stripe.Customer) -> Any:
|
||||
return stripe_subscription
|
||||
return None
|
||||
|
||||
def estimate_customer_arr(stripe_customer: stripe.Customer) -> int: # nocoverage
|
||||
stripe_subscription = extract_current_subscription(stripe_customer)
|
||||
if stripe_subscription is None:
|
||||
return 0
|
||||
# This is an overestimate for those paying by invoice
|
||||
estimated_arr = stripe_subscription.plan.amount * stripe_subscription.quantity / 100.
|
||||
if stripe_subscription.plan_interval == 'month':
|
||||
estimated_arr *= 12
|
||||
if stripe_customer.discount is not None:
|
||||
estimated_arr *= 1 - stripe_customer.discount.coupon.percent_off/100.
|
||||
return int(estimated_arr)
|
||||
|
||||
@catch_stripe_errors
|
||||
def do_create_customer(user: UserProfile, stripe_token: Optional[str]=None,
|
||||
coupon: Optional[Coupon]=None) -> stripe.Customer:
|
||||
|
@@ -17,7 +17,7 @@ class Customer:
|
||||
account_balance: int
|
||||
email: str
|
||||
description: str
|
||||
discount: Optional[Dict[str, Any]]
|
||||
discount: Optional[Discount]
|
||||
metadata: Dict[str, str]
|
||||
|
||||
@staticmethod
|
||||
@@ -33,6 +33,15 @@ class Customer:
|
||||
def save(customer: Customer) -> Customer:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def delete_discount(customer: Customer) -> None:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def list(limit: Optional[int]=...) -> List[Customer]:
|
||||
...
|
||||
|
||||
|
||||
class Invoice:
|
||||
amount_due: int
|
||||
total: int
|
||||
@@ -74,8 +83,12 @@ class Product:
|
||||
def create(name: str=..., type: str=..., statement_descriptor: str=..., unit_label: str=...) -> Product:
|
||||
...
|
||||
|
||||
class Discount:
|
||||
coupon: Coupon
|
||||
|
||||
class Coupon:
|
||||
id: str
|
||||
percent_off: int
|
||||
|
||||
@staticmethod
|
||||
def create(duration: str=..., name: str=..., percent_off: int=...) -> Coupon:
|
||||
|
@@ -24,6 +24,7 @@
|
||||
<li>sites are listed if ≥1 users active in last 2 weeks</li>
|
||||
<li><strong>user</strong> - registered user, not deactivated, not a bot</li>
|
||||
<li><strong>active (user)</strong> - sent a message, or advanced the pointer (reading messages doesn't count unless advances the pointer)</li>
|
||||
<li><strong>ARR</strong> (Annual recurring revenue) - the number of users they are paying for * annual price/user.</li>
|
||||
<li><strong><th><i class="fa fa-envelope"></i></th></strong> - copies realm admin emails to clipboard</li>
|
||||
<li><strong>DAU</strong> (Daily Active Users) - users active in last 24hr</li>
|
||||
<li><strong>WAU</strong> (Weekly Active Users) - users active in last 7 * 24hr</li>
|
||||
@@ -39,6 +40,7 @@
|
||||
<th>Realm</th>
|
||||
<th>Created (green if ≤12wk)</th>
|
||||
<th>Plan Type</th>
|
||||
<th>ARR</th>
|
||||
<th></th>
|
||||
<th>DAU</th>
|
||||
<th>WAU</th>
|
||||
@@ -69,6 +71,12 @@
|
||||
{{ row.plan_type_string }}
|
||||
</td>
|
||||
|
||||
<td class="number">
|
||||
{% if row.amount %}
|
||||
{{ row.amount }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if not loop.last %}
|
||||
<a class="envelope-link" data-value="{{ row.realm_admin_email }}">
|
||||
|
@@ -32,7 +32,8 @@ from zerver.models import (
|
||||
import datetime
|
||||
|
||||
class ActivityTest(ZulipTestCase):
|
||||
def test_activity(self) -> None:
|
||||
@mock.patch("stripe.Customer.list", return_value=[])
|
||||
def test_activity(self, unused_mock: mock.Mock) -> None:
|
||||
self.login(self.example_email("hamlet"))
|
||||
client, _ = Client.objects.get_or_create(name='website')
|
||||
query = '/json/users/me/pointer'
|
||||
@@ -198,7 +199,8 @@ class UserPresenceTests(ZulipTestCase):
|
||||
self.assertEqual(json['presences'][email][client]['status'], 'active')
|
||||
self.assertEqual(json['presences'][self.example_email("hamlet")][client]['status'], 'idle')
|
||||
|
||||
def test_new_user_input(self) -> None:
|
||||
@mock.patch("stripe.Customer.list", return_value=[])
|
||||
def test_new_user_input(self, unused_mock: mock.Mock) -> None:
|
||||
"""Mostly a test for UserActivityInterval"""
|
||||
user_profile = self.example_user("hamlet")
|
||||
self.login(self.example_email("hamlet"))
|
||||
|
Reference in New Issue
Block a user