billing: Update /billing to work with new subscription model.

This commit is contained in:
Rishi Gupta
2018-12-23 00:10:57 -08:00
parent e7220fd71f
commit 653416ab23
13 changed files with 348 additions and 63 deletions

View File

@@ -0,0 +1,85 @@
{
"account_balance": 0,
"created": 1000000000,
"currency": "usd",
"default_source": {
"address_city": "Pacific",
"address_country": "United States",
"address_line1": "Under the sea,",
"address_line1_check": "pass",
"address_line2": null,
"address_state": null,
"address_zip": "33333",
"address_zip_check": "pass",
"brand": "Visa",
"country": "US",
"customer": "cus_NORMALIZED0001",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 3,
"exp_year": 2033,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"id": "card_NORMALIZED00000000000001",
"last4": "4242",
"metadata": {},
"name": "Ada Starr",
"object": "card",
"tokenization_method": null
},
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"object": "customer",
"shipping": null,
"sources": {
"data": [
{
"address_city": "Pacific",
"address_country": "United States",
"address_line1": "Under the sea,",
"address_line1_check": "pass",
"address_line2": null,
"address_state": null,
"address_zip": "33333",
"address_zip_check": "pass",
"brand": "Visa",
"country": "US",
"customer": "cus_NORMALIZED0001",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 3,
"exp_year": 2033,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"id": "card_NORMALIZED00000000000001",
"last4": "4242",
"metadata": {},
"name": "Ada Starr",
"object": "card",
"tokenization_method": null
}
],
"has_more": false,
"object": "list",
"total_count": 1,
"url": "/v1/customers/cus_NORMALIZED0001/sources"
},
"subscriptions": {
"data": [],
"has_more": false,
"object": "list",
"total_count": 0,
"url": "/v1/customers/cus_NORMALIZED0001/subscriptions"
},
"tax_info": null,
"tax_info_verification": null
}

View File

@@ -0,0 +1,85 @@
{
"account_balance": 0,
"created": 1000000000,
"currency": "usd",
"default_source": {
"address_city": "Pacific",
"address_country": "United States",
"address_line1": "Under the sea,",
"address_line1_check": "pass",
"address_line2": null,
"address_state": null,
"address_zip": "33333",
"address_zip_check": "pass",
"brand": "Visa",
"country": "US",
"customer": "cus_NORMALIZED0001",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 3,
"exp_year": 2033,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"id": "card_NORMALIZED00000000000001",
"last4": "4242",
"metadata": {},
"name": "Ada Starr",
"object": "card",
"tokenization_method": null
},
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"object": "customer",
"shipping": null,
"sources": {
"data": [
{
"address_city": "Pacific",
"address_country": "United States",
"address_line1": "Under the sea,",
"address_line1_check": "pass",
"address_line2": null,
"address_state": null,
"address_zip": "33333",
"address_zip_check": "pass",
"brand": "Visa",
"country": "US",
"customer": "cus_NORMALIZED0001",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 3,
"exp_year": 2033,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"id": "card_NORMALIZED00000000000001",
"last4": "4242",
"metadata": {},
"name": "Ada Starr",
"object": "card",
"tokenization_method": null
}
],
"has_more": false,
"object": "list",
"total_count": 1,
"url": "/v1/customers/cus_NORMALIZED0001/sources"
},
"subscriptions": {
"data": [],
"has_more": false,
"object": "list",
"total_count": 0,
"url": "/v1/customers/cus_NORMALIZED0001/subscriptions"
},
"tax_info": null,
"tax_info_verification": null
}

View File

@@ -18,9 +18,9 @@
"due_date": 1000000000,
"ending_balance": 0,
"finalized_at": 1000000000,
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_daf4CWkp5EbV5fMjyaF0P8Yu3h",
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001",
"id": "in_NORMALIZED00000000000001",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_daf4CWkp5EbV5fMjyaF0P8Yu3h/pdf",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf",
"lines": {
"data": [
{

View File

@@ -0,0 +1,85 @@
{
"account_balance": 0,
"created": 1010000002,
"currency": "usd",
"default_source": {
"address_city": "Pacific",
"address_country": "United States",
"address_line1": "Under the sea,",
"address_line1_check": "pass",
"address_line2": null,
"address_state": null,
"address_zip": "33333",
"address_zip_check": "pass",
"brand": "Visa",
"country": "US",
"customer": "cus_NORMALIZED0001",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 3,
"exp_year": 2033,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"id": "card_NORMALIZED00000000000001",
"last4": "4242",
"metadata": {},
"name": "Ada Starr",
"object": "card",
"tokenization_method": null
},
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"object": "customer",
"shipping": null,
"sources": {
"data": [
{
"address_city": "Pacific",
"address_country": "United States",
"address_line1": "Under the sea,",
"address_line1_check": "pass",
"address_line2": null,
"address_state": null,
"address_zip": "33333",
"address_zip_check": "pass",
"brand": "Visa",
"country": "US",
"customer": "cus_NORMALIZED0001",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 3,
"exp_year": 2033,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"id": "card_NORMALIZED00000000000001",
"last4": "4242",
"metadata": {},
"name": "Ada Starr",
"object": "card",
"tokenization_method": null
}
],
"has_more": false,
"object": "list",
"total_count": 1,
"url": "/v1/customers/cus_NORMALIZED0001/sources"
},
"subscriptions": {
"data": [],
"has_more": false,
"object": "list",
"total_count": 0,
"url": "/v1/customers/cus_NORMALIZED0001/subscriptions"
},
"tax_info": null,
"tax_info_verification": null
}

View File

@@ -18,9 +18,9 @@
"due_date": 1000000000,
"ending_balance": 0,
"finalized_at": 1000000000,
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_sh6wGq3YCdgkwFxDfHonbxi1JB",
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001",
"id": "in_NORMALIZED00000000000001",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_sh6wGq3YCdgkwFxDfHonbxi1JB/pdf",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf",
"lines": {
"data": [
{

View File

@@ -20,9 +20,9 @@
"due_date": 1000000000,
"ending_balance": 0,
"finalized_at": 1000000000,
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_sh6wGq3YCdgkwFxDfHonbxi1JB",
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001",
"id": "in_NORMALIZED00000000000001",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_sh6wGq3YCdgkwFxDfHonbxi1JB/pdf",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf",
"lines": {
"data": [
{

View File

@@ -0,0 +1,35 @@
{
"account_balance": 0,
"created": 1010000001,
"currency": "usd",
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"object": "customer",
"shipping": null,
"sources": {
"data": [],
"has_more": false,
"object": "list",
"total_count": 0,
"url": "/v1/customers/cus_NORMALIZED0001/sources"
},
"subscriptions": {
"data": [],
"has_more": false,
"object": "list",
"total_count": 0,
"url": "/v1/customers/cus_NORMALIZED0001/subscriptions"
},
"tax_info": null,
"tax_info_verification": null
}

View File

@@ -18,9 +18,9 @@
"due_date": 1000000000,
"ending_balance": 0,
"finalized_at": 1000000000,
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_O7KxfsK8GxMVb8EGiyIlGF9OUe",
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001",
"id": "in_NORMALIZED00000000000001",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_O7KxfsK8GxMVb8EGiyIlGF9OUe/pdf",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf",
"lines": {
"data": [
{

View File

@@ -20,9 +20,9 @@
"due_date": 1000000000,
"ending_balance": 0,
"finalized_at": 1000000000,
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_O7KxfsK8GxMVb8EGiyIlGF9OUe",
"hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001",
"id": "in_NORMALIZED00000000000001",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_O7KxfsK8GxMVb8EGiyIlGF9OUe/pdf",
"invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf",
"lines": {
"data": [
{

View File

@@ -118,7 +118,7 @@ def normalize_fixture_data(decorated_function: CallableT,
id_lengths = [
('cus', 14), ('sub', 14), ('si', 14), ('sli', 14), ('req', 14), ('tok', 24), ('card', 24),
('txn', 24), ('ch', 24), ('in', 24), ('ii', 24), ('test', 12), ('src_client_secret', 24),
('src', 24)]
('src', 24), ('invst', 26)]
# We'll replace cus_D7OT2jf5YAtZQ2 with something like cus_NORMALIZED0001
pattern_translations = {
"%s_[A-Za-z0-9]{%d}" % (prefix, length): "%s_NORMALIZED%%0%dd" % (prefix, length - 10)
@@ -399,12 +399,15 @@ class StripeTest(ZulipTestCase):
self.assertEqual(response.status_code, 302)
self.assertEqual('/billing/', response.url)
# TODO: Check /billing has the correct information
# response = self.client_get("/billing/")
# self.assert_not_in_success_response(['Pay annually'], response)
# for substring in ['Your plan will renew on', '$%s.00' % (80 * self.seat_count,),
# 'Card ending in 4242', 'Update card']:
# self.assert_in_response(substring, response)
# Check /billing has the correct information
response = self.client_get("/billing/")
self.assert_not_in_success_response(['Pay annually'], response)
for substring in [
'Zulip Standard', str(self.seat_count),
'Your plan will renew on', 'January 2, 2013', '$%s.00' % (80 * self.seat_count,),
'Visa ending in 4242',
'Update card']:
self.assert_in_response(substring, response)
@mock_stripe(tested_timestamp_fields=["created"])
def test_upgrade_by_invoice(self, *mocks: Mock) -> None:
@@ -477,12 +480,14 @@ class StripeTest(ZulipTestCase):
self.assertEqual(response.status_code, 302)
self.assertEqual('/billing/', response.url)
# TODO: Check /billing has the correct information
# response = self.client_get("/billing/")
# self.assert_not_in_success_response(['Pay annually'], response)
# for substring in ['Your plan will renew on', '$%s.00' % (80 * self.seat_count,),
# 'Card ending in 4242', 'Update card']:
# self.assert_in_response(substring, response)
# Check /billing has the correct information
response = self.client_get("/billing/")
self.assert_not_in_success_response(['Pay annually', 'Update card'], response)
for substring in [
'Zulip Standard', str(123),
'Your plan will renew on', 'January 2, 2013', '$9,840.00', # 9840 = 80 * 123
'Billed by invoice']:
self.assert_in_response(substring, response)
@mock_stripe()
def test_billing_page_permissions(self, *mocks: Mock) -> None:
@@ -727,10 +732,6 @@ class StripeTest(ZulipTestCase):
# card on file, and should show it
# TODO
# If you signup via invoice, and then downgrade immediately, the
# default_source is in a weird intermediate state.
# TODO
@mock_stripe()
def test_attach_discount_to_realm(self, *mocks: Mock) -> None:
# Attach discount before Stripe customer exists

View File

@@ -54,24 +54,20 @@ def check_upgrade_parameters(
raise BillingError('not enough licenses',
_("You must invoice for at least {} users.".format(min_licenses)))
# TODO
def payment_method_string(stripe_customer: stripe.Customer) -> str: # nocoverage: TODO
subscription = None # extract_current_subscription(stripe_customer)
if subscription is not None and subscription.billing == "send_invoice":
return _("Billed by invoice")
# Should only be called if the customer is being charged automatically
def payment_method_string(stripe_customer: stripe.Customer) -> str:
stripe_source = stripe_customer.default_source
# In case of e.g. an expired card
if stripe_source is None: # nocoverage
return _("No payment method on file")
if stripe_source.object == "card":
return _("Card ending in %(last4)s" % {'last4': cast(stripe.Card, stripe_source).last4})
# You can get here if e.g. you sign up to pay by invoice, and then
# immediately downgrade. In that case, stripe_source.object == 'source',
# and stripe_source.type = 'ach_credit_transfer'.
# Using a catch-all error message here since there might be one-off stuff we
# do for a particular customer that would land them here. E.g. by default we
# don't support ACH for automatic payments, but in theory we could add it for
# a customer via the Stripe dashboard.
return _("%(brand)s ending in %(last4)s" % {
'brand': cast(stripe.Card, stripe_source).brand,
'last4': cast(stripe.Card, stripe_source).last4})
# There might be one-off stuff we do for a particular customer that
# would land them here. E.g. by default we don't support ACH for
# automatic payments, but in theory we could add it for a customer via
# the Stripe dashboard.
return _("Unknown payment method. Please contact %s." % (settings.ZULIP_ADMINISTRATOR,)) # nocoverage
@has_request_variables
@@ -163,7 +159,7 @@ def billing_home(request: HttpRequest) -> HttpResponse:
return render(request, 'corporate/billing.html', context=context)
context = {'admin_access': True}
charge_automatically = False
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
plan = get_active_plan(customer)
if plan is not None:
plan_name = {
@@ -171,16 +167,14 @@ def billing_home(request: HttpRequest) -> HttpResponse:
CustomerPlan.PLUS: 'Zulip Plus',
}[plan.tier]
licenses = plan.licenses
# Need user's timezone to do this properly
# Should do this in javascript, using the user's timezone
renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format(dt=next_renewal_date(plan))
renewal_cents = renewal_amount(plan)
charge_automatically = plan.charge_automatically
if charge_automatically: # nocoverage: TODO
# TODO get last4
payment_method = 'Card on file'
else: # nocoverage: TODO
if charge_automatically:
payment_method = payment_method_string(stripe_customer)
else:
payment_method = 'Billed by invoice'
billed_by_invoice = not plan.charge_automatically
# Can only get here by subscribing and then downgrading. We don't support downgrading
# yet, but keeping this code here since we will soon.
else: # nocoverage
@@ -189,6 +183,7 @@ def billing_home(request: HttpRequest) -> HttpResponse:
renewal_date = ''
renewal_cents = 0
payment_method = ''
charge_automatically = False
context.update({
'plan_name': plan_name,
@@ -196,13 +191,10 @@ def billing_home(request: HttpRequest) -> HttpResponse:
'renewal_date': renewal_date,
'renewal_amount': '{:,.2f}'.format(renewal_cents / 100.),
'payment_method': payment_method,
# TODO: Rename to charge_automatically
'billed_by_invoice': billed_by_invoice,
'charge_automatically': charge_automatically,
'publishable_key': STRIPE_PUBLISHABLE_KEY,
# TODO: get actual stripe email?
'stripe_email': user.email,
'stripe_email': stripe_customer.email,
})
return render(request, 'corporate/billing.html', context=context)
@require_billing_access

View File

@@ -116,6 +116,7 @@ class Source:
class Card:
id: str
brand: str
last4: str
object: str

View File

@@ -29,16 +29,15 @@
<div class="tab-content">
<div class="tab-pane active" id="overview">
<p>Your current plan is <strong>{{ plan_name }}</strong></p>
<p>Your current plan is <strong>{{ plan_name }}</strong>.</p>
<p>You are paying for <strong>{{ licenses }} users</strong>.</p>
<p>Your plan will renew on <strong>{{ renewal_date }}</strong> for <strong>${{ renewal_amount }}</strong>.</p>
{% if account_charges %}
<p>You have <strong>${{ account_charges }}</strong> in charges that will be added to your next bill.</p>
{% elif account_credits %}
<p>You have <strong>${{ account_credits }}</strong> in credits that will be automatically applied to your next bill.</p>
{% endif %}
<p>
Your plan will renew on <strong>{{ renewal_date }}</strong> for
<strong>${{ renewal_amount }}</strong>.
</p>
</div>
<div class="tab-pane" id="payment-method" data-email="{{stripe_email}}" data-csrf="{{csrf_token}}" data-key="{{publishable_key}}">
<div class="tab-pane" id="payment-method" data-email="{{stripe_email}}"
data-csrf="{{csrf_token}}" data-key="{{publishable_key}}">
<div id="payment-section">
<p>Current payment method: <strong>{{ payment_method }}</strong></p>
{% if charge_automatically %}
@@ -67,13 +66,15 @@
</div>
<div class="support-link">
<p>Contact <a href="mailto:support@zulipchat.com">support@zulipchat.com</a> for billing history or to make changes to your subscription.</p>
<p>
Contact <a href="mailto:support@zulipchat.com">support@zulipchat.com</a>
for billing history or to make changes to your subscription.
</p>
</div>
{% else %}
<p>
You must be an organization administrator or a
billing administrator to view this page.
You must be an organization administrator or a billing administrator to view this page.
</p>
{% endif %}
</div>