mirror of https://github.com/zulip/zulip.git
billing: Prepare for moving Plan to CustomerPlan.billing_schedule.
This commit is contained in:
parent
8176d112fe
commit
7ab1406962
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue