billing: Prepare for moving Plan to CustomerPlan.billing_schedule.

This commit is contained in:
Rishi Gupta 2018-12-12 14:23:15 -08:00
parent 8176d112fe
commit 7ab1406962
6 changed files with 50 additions and 49 deletions

View File

@ -19,7 +19,7 @@ from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
from zerver.lib.utils import generate_random_token
from zerver.lib.actions import do_change_plan_type
from zerver.models import Realm, UserProfile, RealmAuditLog
from corporate.models import Customer, Plan, Coupon
from corporate.models import Customer, CustomerPlan, Plan, Coupon
from zproject.settings import get_secret
STRIPE_PUBLISHABLE_KEY = get_secret('stripe_publishable_key')
@ -227,8 +227,12 @@ def do_subscribe_customer_to_plan(user: UserProfile, stripe_customer: stripe.Cus
requires_billing_update=True,
extra_data=ujson.dumps({'quantity': current_seat_count}))
def process_initial_upgrade(user: UserProfile, plan: Plan, seat_count: int,
def process_initial_upgrade(user: UserProfile, seat_count: int, schedule: int,
stripe_token: Optional[str]) -> None:
if schedule == CustomerPlan.ANNUAL:
plan = Plan.objects.get(nickname=Plan.CLOUD_ANNUAL)
else: # schedule == CustomerPlan.MONTHLY:
plan = Plan.objects.get(nickname=Plan.CLOUD_MONTHLY)
customer = Customer.objects.filter(realm=user.realm).first()
if customer is None:
stripe_customer = do_create_customer(user, stripe_token=stripe_token)

View File

@ -17,6 +17,12 @@ class Customer(models.Model):
def __str__(self) -> str:
return "<Customer %s %s>" % (self.realm, self.stripe_customer_id)
class CustomerPlan(object):
ANNUAL = 1
MONTHLY = 2
# Everything below here is legacy
class Plan(models.Model):
# The two possible values for nickname
CLOUD_MONTHLY = 'monthly'
@ -25,8 +31,6 @@ class Plan(models.Model):
stripe_plan_id = models.CharField(max_length=255, unique=True) # type: str
# Everything below here is legacy
class Coupon(models.Model):
percent_off = models.SmallIntegerField(unique=True) # type: int
stripe_coupon_id = models.CharField(max_length=255, unique=True) # type: str

View File

@ -29,7 +29,7 @@ from corporate.lib.stripe import catch_stripe_errors, \
get_seat_count, extract_current_subscription, sign_string, unsign_string, \
BillingError, StripeCardError, StripeConnectionError, stripe_get_customer, \
DEFAULT_INVOICE_DAYS_UNTIL_DUE, MIN_INVOICED_SEAT_COUNT, do_create_customer
from corporate.models import Customer, Plan, Coupon
from corporate.models import Customer, CustomerPlan, Plan, Coupon
from corporate.views import payment_method_string
import corporate.urls
@ -247,7 +247,7 @@ class StripeTest(ZulipTestCase):
params = {
'signed_seat_count': self.get_signed_seat_count_from_response(response),
'salt': self.get_salt_from_response(response),
'plan': Plan.CLOUD_ANNUAL} # type: Dict[str, Any]
'schedule': 'annual'} # type: Dict[str, Any]
if invoice: # send_invoice
params.update({
'invoiced_seat_count': 123,
@ -469,16 +469,16 @@ class StripeTest(ZulipTestCase):
self.assert_json_error_contains(response, "Something went wrong. Please contact")
self.assertEqual(ujson.loads(response.content)['error_description'], 'tampered seat count')
def test_upgrade_with_tampered_plan(self) -> None:
def test_upgrade_with_tampered_schedule(self) -> None:
# Test with an unknown plan
self.login(self.example_email("hamlet"))
response = self.upgrade(talk_to_stripe=False, plan='badplan')
response = self.upgrade(talk_to_stripe=False, schedule='biweekly')
self.assert_json_error_contains(response, "Something went wrong. Please contact")
self.assertEqual(ujson.loads(response.content)['error_description'], 'tampered plan')
self.assertEqual(ujson.loads(response.content)['error_description'], 'tampered schedule')
# Test with a plan that's valid, but not if you're paying by invoice
response = self.upgrade(invoice=True, talk_to_stripe=False, plan=Plan.CLOUD_MONTHLY)
response = self.upgrade(invoice=True, talk_to_stripe=False, schedule='monthly')
self.assert_json_error_contains(response, "Something went wrong. Please contact")
self.assertEqual(ujson.loads(response.content)['error_description'], 'tampered plan')
self.assertEqual(ujson.loads(response.content)['error_description'], 'tampered schedule')
def test_upgrade_with_insufficient_invoiced_seat_count(self) -> None:
self.login(self.example_email("hamlet"))

View File

@ -22,30 +22,29 @@ from corporate.lib.stripe import STRIPE_PUBLISHABLE_KEY, \
extract_current_subscription, process_initial_upgrade, sign_string, \
unsign_string, BillingError, process_downgrade, do_replace_payment_source, \
MIN_INVOICED_SEAT_COUNT, DEFAULT_INVOICE_DAYS_UNTIL_DUE
from corporate.models import Customer, Plan
from corporate.models import Customer, CustomerPlan, Plan
billing_logger = logging.getLogger('corporate.stripe')
def unsign_and_check_upgrade_parameters(user: UserProfile, plan_nickname: str,
def unsign_and_check_upgrade_parameters(user: UserProfile, schedule: str,
signed_seat_count: str, salt: str,
billing_modality: str) -> Tuple[Plan, int]:
provided_plans = {
'charge_automatically': [Plan.CLOUD_ANNUAL, Plan.CLOUD_MONTHLY],
'send_invoice': [Plan.CLOUD_ANNUAL],
billing_modality: str) -> Tuple[int, int]:
provided_schedules = {
'charge_automatically': ['annual', 'monthly'],
'send_invoice': ['annual'],
}
if plan_nickname not in provided_plans[billing_modality]:
billing_logger.warning("Tampered plan during realm upgrade. user: %s, realm: %s (%s)."
if schedule not in provided_schedules[billing_modality]:
billing_logger.warning("Tampered schedule during realm upgrade. user: %s, realm: %s (%s)."
% (user.id, user.realm.id, user.realm.string_id))
raise BillingError('tampered plan', BillingError.CONTACT_SUPPORT)
plan = Plan.objects.get(nickname=plan_nickname)
raise BillingError('tampered schedule', BillingError.CONTACT_SUPPORT)
billing_schedule = {'annual': CustomerPlan.ANNUAL, 'monthly': CustomerPlan.MONTHLY}[schedule]
try:
seat_count = int(unsign_string(signed_seat_count, salt))
except signing.BadSignature:
billing_logger.warning("Tampered seat count during realm upgrade. user: %s, realm: %s (%s)."
% (user.id, user.realm.id, user.realm.string_id))
raise BillingError('tampered seat count', BillingError.CONTACT_SUPPORT)
return plan, seat_count
return seat_count, billing_schedule
def payment_method_string(stripe_customer: stripe.Customer) -> str:
subscription = extract_current_subscription(stripe_customer)
@ -68,7 +67,7 @@ def payment_method_string(stripe_customer: stripe.Customer) -> str:
@has_request_variables
def upgrade(request: HttpRequest, user: UserProfile,
plan: str=REQ(validator=check_string),
schedule: str=REQ(validator=check_string),
license_management: str=REQ(validator=check_string, default=None),
signed_seat_count: str=REQ(validator=check_string),
salt: str=REQ(validator=check_string),
@ -76,8 +75,8 @@ def upgrade(request: HttpRequest, user: UserProfile,
invoiced_seat_count: int=REQ(validator=check_int, default=None),
stripe_token: str=REQ(validator=check_string, default=None)) -> HttpResponse:
try:
plan, seat_count = unsign_and_check_upgrade_parameters(user, plan, signed_seat_count,
salt, billing_modality)
seat_count, billing_schedule = unsign_and_check_upgrade_parameters(
user, schedule, signed_seat_count, salt, billing_modality)
if billing_modality == 'send_invoice':
min_required_seat_count = max(seat_count, MIN_INVOICED_SEAT_COUNT)
if invoiced_seat_count < min_required_seat_count:
@ -85,7 +84,7 @@ def upgrade(request: HttpRequest, user: UserProfile,
'lowball seat count',
"You must invoice for at least %d users." % (min_required_seat_count,))
seat_count = invoiced_seat_count
process_initial_upgrade(user, plan, seat_count, stripe_token)
process_initial_upgrade(user, seat_count, billing_schedule, stripe_token)
except BillingError as e:
return json_error(e.message, data={'error_description': e.description})
except Exception as e:
@ -121,12 +120,8 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
'min_seat_count_for_invoice': max(seat_count, MIN_INVOICED_SEAT_COUNT),
'default_invoice_days_until_due': DEFAULT_INVOICE_DAYS_UNTIL_DUE,
'plan': "Zulip Standard",
'nickname_monthly': Plan.CLOUD_MONTHLY,
'nickname_annual': Plan.CLOUD_ANNUAL,
'page_params': JSONEncoderForHTML().encode({
'seat_count': seat_count,
'nickname_annual': Plan.CLOUD_ANNUAL,
'nickname_monthly': Plan.CLOUD_MONTHLY,
'annual_price': 8000,
'monthly_price': 800,
'percent_off': float(percent_off),

View File

@ -111,7 +111,7 @@ $(function () {
csrfmiddlewaretoken: $("#autopay-form input[name='csrf']").val(),
signed_seat_count: get_form_input("autopay", "signed_seat_count"),
salt: get_form_input("autopay", "salt"),
plan: get_form_input("autopay", "plan"),
schedule: get_form_input("autopay", "schedule"),
license_management: JSON.stringify(license_management),
invoiced_seat_count: $("#" + license_management + "_license_count").val(),
billing_modality: get_form_input("autopay", "billing_modality"),
@ -165,7 +165,7 @@ $(function () {
csrfmiddlewaretoken: get_form_input("invoice", "csrfmiddlewaretoken", false),
signed_seat_count: get_form_input("invoice", "signed_seat_count"),
salt: get_form_input("invoice", "salt"),
plan: get_form_input("invoice", "plan"),
schedule: get_form_input("invoice", "schedule"),
billing_modality: get_form_input("invoice", "billing_modality"),
invoiced_seat_count: get_form_input("invoice", "invoiced_seat_count", false),
},
@ -184,14 +184,12 @@ $(function () {
});
var prices = {};
prices[page_params.nickname_annual] =
page_params.annual_price * (1 - page_params.percent_off / 100);
prices[page_params.nickname_monthly] =
page_params.monthly_price * (1 - page_params.percent_off / 100);
prices.annual = page_params.annual_price * (1 - page_params.percent_off / 100);
prices.monthly = page_params.monthly_price * (1 - page_params.percent_off / 100);
function update_charged_amount(plan_nickname) {
function update_charged_amount(schedule) {
$("#charged_amount").text(
format_money(page_params.seat_count * prices[plan_nickname])
format_money(page_params.seat_count * prices[schedule])
);
}
@ -208,17 +206,17 @@ $(function () {
show_license_section($(this).val());
});
$('input[type=radio][name=plan]').change(function () {
$('input[type=radio][name=schedule]').change(function () {
update_charged_amount($(this).val());
});
$("#autopay_annual_price").text(format_money(prices[page_params.nickname_annual]));
$("#autopay_annual_price_per_month").text(format_money(prices[page_params.nickname_annual] / 12));
$("#autopay_monthly_price").text(format_money(prices[page_params.nickname_monthly]));
$("#invoice_annual_price").text(format_money(prices[page_params.nickname_annual]));
$("#invoice_annual_price_per_month").text(format_money(prices[page_params.nickname_annual] / 12));
$("#autopay_annual_price").text(format_money(prices.annual));
$("#autopay_annual_price_per_month").text(format_money(prices.annual / 12));
$("#autopay_monthly_price").text(format_money(prices.monthly));
$("#invoice_annual_price").text(format_money(prices.annual));
$("#invoice_annual_price_per_month").text(format_money(prices.annual / 12));
show_license_section($('input[type=radio][name=license_management]:checked').val());
update_charged_amount($('input[type=radio][name=plan]:checked').val());
update_charged_amount($('input[type=radio][name=schedule]:checked').val());
}
});

View File

@ -54,7 +54,7 @@
<div id="autopay-error" class="alert alert-danger"></div>
<h3>{{ _("Payment schedule") }}</h3>
<label>
<input type="radio" name="plan" value="{{ nickname_annual }}" checked />
<input type="radio" name="schedule" value="annual" checked />
<div class="box">
<div class="schedule-time annually">{{ _("Pay annually") }}</div>
<div class="schedule-amount">
@ -66,7 +66,7 @@
</div>
</label>
<label>
<input type="radio" name="plan" value="{{ nickname_monthly }}" />
<input type="radio" name="schedule" value="monthly" />
<div class="box">
<div class="schedule-time">{{ _("Pay monthly") }}</div>
<div class="schedule-amount">$<span id="autopay_monthly_price"></span>/user/month</div>
@ -163,7 +163,7 @@
<div class="payment-schedule">
<h3>{{ _("Payment schedule") }}</h3>
<label>
<input type="radio" name="plan" value="{{ nickname_annual }}" checked />
<input type="radio" name="schedule" value="annual" checked />
<div class="box">
<div class="schedule-time annually">{{ _("Pay annually") }}</div>
<div class="schedule-amount">