billing: Show estimated subscription revenue on /activity.

[Substantial edits by Rishi Gupta]
This commit is contained in:
Vishnu Ks
2018-11-16 21:38:09 +05:30
committed by Rishi Gupta
parent a7c33e12cb
commit 2e04cdbe5e
5 changed files with 60 additions and 3 deletions

View File

@@ -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='',

View File

@@ -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:

View File

@@ -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:

View File

@@ -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 }}">

View File

@@ -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"))