Browse Source

Billing overhaul.

pull/149/head
Pēteris Caune 7 years ago
parent
commit
01c3a13922
27 changed files with 1418 additions and 779 deletions
  1. +20
    -0
      hc/payments/migrations/0003_subscription_address_id.py
  2. +132
    -18
      hc/payments/models.py
  3. +69
    -0
      hc/payments/tests/test_address.py
  4. +4
    -12
      hc/payments/tests/test_billing_history.py
  5. +0
    -38
      hc/payments/tests/test_cancel_plan.py
  6. +1
    -1
      hc/payments/tests/test_get_client_token.py
  7. +0
    -56
      hc/payments/tests/test_invoice.py
  8. +94
    -0
      hc/payments/tests/test_payment_method.py
  9. +3
    -3
      hc/payments/tests/test_pdf_invoice.py
  10. +47
    -49
      hc/payments/tests/test_set_plan.py
  11. +15
    -15
      hc/payments/urls.py
  12. +80
    -136
      hc/payments/views.py
  13. +0
    -4
      static/css/pricing.css
  14. +21
    -0
      static/css/profile.css
  15. +46
    -0
      static/js/billing.js
  16. +8
    -43
      static/js/pricing.js
  17. +3
    -0
      templates/accounts/badges.html
  18. +428
    -0
      templates/accounts/billing.html
  19. +3
    -0
      templates/accounts/notifications.html
  20. +3
    -0
      templates/accounts/profile.html
  21. +41
    -0
      templates/payments/address.html
  22. +8
    -0
      templates/payments/address_plain.html
  23. +0
    -98
      templates/payments/billing.html
  24. +42
    -0
      templates/payments/billing_history.html
  25. +250
    -0
      templates/payments/countries.html
  26. +14
    -0
      templates/payments/payment_method.html
  27. +86
    -306
      templates/payments/pricing.html

+ 20
- 0
hc/payments/migrations/0003_subscription_address_id.py View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-01-07 16:29
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payments', '0002_subscription_plan_id'),
]
operations = [
migrations.AddField(
model_name='subscription',
name='address_id',
field=models.CharField(blank=True, max_length=2),
),
]

+ 132
- 18
hc/payments/models.py View File

@ -10,6 +10,11 @@ else:
braintree = None
ADDRESS_KEYS = ("first_name", "last_name", "company", "street_address",
"extended_address", "locality", "region", "postal_code",
"country_code_alpha2")
class SubscriptionManager(models.Manager):
def for_user(self, user):
@ -23,6 +28,7 @@ class Subscription(models.Model):
payment_method_token = models.CharField(max_length=35, blank=True)
subscription_id = models.CharField(max_length=10, blank=True)
plan_id = models.CharField(max_length=10, blank=True)
address_id = models.CharField(max_length=2, blank=True)
objects = SubscriptionManager()
@ -46,11 +52,99 @@ class Subscription(models.Model):
raise NotImplementedError("Unexpected plan: %s" % self.plan_id)
def _get_braintree_payment_method(self):
@property
def payment_method(self):
if not self.payment_method_token:
return None
if not hasattr(self, "_pm"):
self._pm = braintree.PaymentMethod.find(self.payment_method_token)
return self._pm
def _get_braintree_subscription(self):
if not hasattr(self, "_sub"):
self._sub = braintree.Subscription.find(self.subscription_id)
return self._sub
def get_client_token(self):
return braintree.ClientToken.generate({
"customer_id": self.customer_id
})
def update_payment_method(self, nonce):
# Create customer record if it does not exist:
if not self.customer_id:
result = braintree.Customer.create({
"email": self.user.email
})
if not result.is_success:
return result
self.customer_id = result.customer.id
self.save()
# Create payment method
result = braintree.PaymentMethod.create({
"customer_id": self.customer_id,
"payment_method_nonce": nonce,
"options": {"make_default": True}
})
if not result.is_success:
return result
self.payment_method_token = result.payment_method.token
self.save()
# Update an existing subscription to use this payment method
if self.subscription_id:
result = braintree.Subscription.update(self.subscription_id, {
"payment_method_token": self.payment_method_token
})
if not result.is_success:
return result
def update_address(self, post_data):
# Create customer record if it does not exist:
if not self.customer_id:
result = braintree.Customer.create({
"email": self.user.email
})
if not result.is_success:
return result
self.customer_id = result.customer.id
self.save()
payload = {key: str(post_data.get(key)) for key in ADDRESS_KEYS}
if self.address_id:
result = braintree.Address.update(self.customer_id,
self.address_id,
payload)
else:
payload["customer_id"] = self.customer_id
result = braintree.Address.create(payload)
if result.is_success:
self.address_id = result.address.id
self.save()
if not result.is_success:
return result
def setup(self, plan_id):
result = braintree.Subscription.create({
"payment_method_token": self.payment_method_token,
"plan_id": plan_id
})
if result.is_success:
self.subscription_id = result.subscription.id
self.plan_id = plan_id
self.save()
return result
def cancel(self):
if self.subscription_id:
braintree.Subscription.cancel(self.subscription_id)
@ -59,22 +153,42 @@ class Subscription(models.Model):
self.plan_id = ""
self.save()
def pm_is_credit_card(self):
return isinstance(self._get_braintree_payment_method(),
braintree.credit_card.CreditCard)
def pm_is_card(self):
pm = self.payment_method
return isinstance(pm, braintree.credit_card.CreditCard)
def pm_is_paypal(self):
return isinstance(self._get_braintree_payment_method(),
braintree.paypal_account.PayPalAccount)
def card_type(self):
o = self._get_braintree_payment_method()
return o.card_type
def last_4(self):
o = self._get_braintree_payment_method()
return o.last_4
def paypal_email(self):
o = self._get_braintree_payment_method()
return o.email
pm = self.payment_method
return isinstance(pm, braintree.paypal_account.PayPalAccount)
def next_billing_date(self):
o = self._get_braintree_subscription()
return o.next_billing_date
@property
def address(self):
if not hasattr(self, "_address"):
try:
self._address = braintree.Address.find(self.customer_id,
self.address_id)
except braintree.exceptions.NotFoundError:
self._address = None
return self._address
@property
def transactions(self):
if not hasattr(self, "_tx"):
if not self.customer_id:
self._tx = []
else:
self._tx = list(braintree.Transaction.search(braintree.TransactionSearch.customer_id == self.customer_id))
return self._tx
def get_transaction(self, transaction_id):
tx = braintree.Transaction.find(transaction_id)
if tx.customer_details.id != self.customer_id:
return None
return tx

+ 69
- 0
hc/payments/tests/test_address.py View File

@ -0,0 +1,69 @@
from mock import patch
from hc.payments.models import Subscription
from hc.test import BaseTestCase
class AddressTestCase(BaseTestCase):
@patch("hc.payments.models.braintree")
def test_it_retrieves_address(self, mock):
mock.Address.find.return_value = {"company": "FooCo"}
self.sub = Subscription(user=self.alice)
self.sub.address_id = "aa"
self.sub.save()
self.client.login(username="[email protected]", password="password")
r = self.client.get("/accounts/profile/billing/address/")
self.assertContains(r, "FooCo")
@patch("hc.payments.models.braintree")
def test_it_creates_address(self, mock):
mock.Address.create.return_value.is_success = True
mock.Address.create.return_value.address.id = "bb"
self.sub = Subscription(user=self.alice)
self.sub.customer_id = "test-customer"
self.sub.save()
self.client.login(username="[email protected]", password="password")
form = {"company": "BarCo"}
r = self.client.post("/accounts/profile/billing/address/", form)
self.assertRedirects(r, "/accounts/profile/billing/")
self.sub.refresh_from_db()
self.assertEqual(self.sub.address_id, "bb")
@patch("hc.payments.models.braintree")
def test_it_updates_address(self, mock):
mock.Address.update.return_value.is_success = True
self.sub = Subscription(user=self.alice)
self.sub.customer_id = "test-customer"
self.sub.address_id = "aa"
self.sub.save()
self.client.login(username="[email protected]", password="password")
form = {"company": "BarCo"}
r = self.client.post("/accounts/profile/billing/address/", form)
self.assertRedirects(r, "/accounts/profile/billing/")
@patch("hc.payments.models.braintree")
def test_it_creates_customer(self, mock):
mock.Address.create.return_value.is_success = True
mock.Address.create.return_value.address.id = "bb"
mock.Customer.create.return_value.is_success = True
mock.Customer.create.return_value.customer.id = "test-customer-id"
self.sub = Subscription(user=self.alice)
self.sub.save()
self.client.login(username="[email protected]", password="password")
form = {"company": "BarCo"}
self.client.post("/accounts/profile/billing/address/", form)
self.sub.refresh_from_db()
self.assertEqual(self.sub.customer_id, "test-customer-id")

hc/payments/tests/test_billing.py → hc/payments/tests/test_billing_history.py View File


+ 0
- 38
hc/payments/tests/test_cancel_plan.py View File

@ -1,38 +0,0 @@
from mock import patch
from hc.accounts.models import Profile
from hc.payments.models import Subscription
from hc.test import BaseTestCase
class CancelPlanTestCase(BaseTestCase):
def setUp(self):
super(CancelPlanTestCase, self).setUp()
self.sub = Subscription(user=self.alice)
self.sub.subscription_id = "test-id"
self.sub.plan_id = "P5"
self.sub.save()
self.profile.ping_log_limit = 1000
self.profile.check_limit = 500
self.profile.sms_limit = 50
self.profile.save()
@patch("hc.payments.models.braintree")
def test_it_works(self, mock_braintree):
self.client.login(username="[email protected]", password="password")
r = self.client.post("/pricing/cancel_plan/")
self.assertRedirects(r, "/pricing/")
self.sub.refresh_from_db()
self.assertEqual(self.sub.subscription_id, "")
self.assertEqual(self.sub.plan_id, "")
# User's profile should have standard limits
profile = Profile.objects.get(user=self.alice)
self.assertEqual(profile.ping_log_limit, 100)
self.assertEqual(profile.check_limit, 20)
self.assertEqual(profile.team_limit, 2)
self.assertEqual(profile.sms_limit, 0)

+ 1
- 1
hc/payments/tests/test_get_client_token.py View File

@ -6,7 +6,7 @@ from hc.test import BaseTestCase
class GetClientTokenTestCase(BaseTestCase):
@patch("hc.payments.views.braintree")
@patch("hc.payments.models.braintree")
def test_it_works(self, mock_braintree):
mock_braintree.ClientToken.generate.return_value = "test-token"
self.client.login(username="[email protected]", password="password")


+ 0
- 56
hc/payments/tests/test_invoice.py View File

@ -1,56 +0,0 @@
from mock import Mock, patch
from hc.payments.models import Subscription
from hc.test import BaseTestCase
class InvoiceTestCase(BaseTestCase):
def setUp(self):
super(InvoiceTestCase, self).setUp()
self.sub = Subscription(user=self.alice)
self.sub.subscription_id = "test-id"
self.sub.customer_id = "test-customer-id"
self.sub.save()
@patch("hc.payments.views.braintree")
def test_it_works(self, mock_braintree):
tx = Mock()
tx.id = "abc123"
tx.customer_details.id = "test-customer-id"
tx.created_at = None
mock_braintree.Transaction.find.return_value = tx
self.client.login(username="[email protected]", password="password")
r = self.client.get("/invoice/abc123/")
self.assertContains(r, "ABC123") # tx.id in uppercase
self.assertContains(r, "[email protected]") # bill to
@patch("hc.payments.views.braintree")
def test_it_checks_customer_id(self, mock_braintree):
tx = Mock()
tx.id = "abc123"
tx.customer_details.id = "test-another-customer-id"
tx.created_at = None
mock_braintree.Transaction.find.return_value = tx
self.client.login(username="[email protected]", password="password")
r = self.client.get("/invoice/abc123/")
self.assertEqual(r.status_code, 403)
@patch("hc.payments.views.braintree")
def test_it_shows_company_data(self, mock_braintree):
self.profile.bill_to = "Alice and Partners"
self.profile.save()
tx = Mock()
tx.id = "abc123"
tx.customer_details.id = "test-customer-id"
tx.created_at = None
mock_braintree.Transaction.find.return_value = tx
self.client.login(username="[email protected]", password="password")
r = self.client.get("/invoice/abc123/")
self.assertContains(r, "Alice and Partners")

+ 94
- 0
hc/payments/tests/test_payment_method.py View File

@ -0,0 +1,94 @@
from mock import patch
from hc.payments.models import Subscription
from hc.test import BaseTestCase
class UpdatePaymentMethodTestCase(BaseTestCase):
def _setup_mock(self, mock):
""" Set up Braintree calls that the controller will use. """
mock.PaymentMethod.create.return_value.is_success = True
mock.PaymentMethod.create.return_value.payment_method.token = "fake"
@patch("hc.payments.models.braintree")
def test_it_retrieves_paypal(self, mock):
self._setup_mock(mock)
mock.paypal_account.PayPalAccount = dict
mock.credit_card.CreditCard = list
mock.PaymentMethod.find.return_value = {"email": "[email protected]"}
self.sub = Subscription(user=self.alice)
self.sub.payment_method_token = "fake-token"
self.sub.save()
self.client.login(username="[email protected]", password="password")
r = self.client.get("/accounts/profile/billing/payment_method/")
self.assertContains(r, "[email protected]")
@patch("hc.payments.models.braintree")
def test_it_retrieves_cc(self, mock):
self._setup_mock(mock)
mock.paypal_account.PayPalAccount = list
mock.credit_card.CreditCard = dict
mock.PaymentMethod.find.return_value = {"masked_number": "1***2"}
self.sub = Subscription(user=self.alice)
self.sub.payment_method_token = "fake-token"
self.sub.save()
self.client.login(username="[email protected]", password="password")
r = self.client.get("/accounts/profile/billing/payment_method/")
self.assertContains(r, "1***2")
@patch("hc.payments.models.braintree")
def test_it_creates_payment_method(self, mock):
self._setup_mock(mock)
self.sub = Subscription(user=self.alice)
self.sub.customer_id = "test-customer"
self.sub.save()
self.client.login(username="[email protected]", password="password")
form = {"payment_method_nonce": "test-nonce"}
r = self.client.post("/accounts/profile/billing/payment_method/", form)
self.assertRedirects(r, "/accounts/profile/billing/")
@patch("hc.payments.models.braintree")
def test_it_creates_customer(self, mock):
self._setup_mock(mock)
mock.Customer.create.return_value.is_success = True
mock.Customer.create.return_value.customer.id = "test-customer-id"
self.sub = Subscription(user=self.alice)
self.sub.save()
self.client.login(username="[email protected]", password="password")
form = {"payment_method_nonce": "test-nonce"}
self.client.post("/accounts/profile/billing/payment_method/", form)
self.sub.refresh_from_db()
self.assertEqual(self.sub.customer_id, "test-customer-id")
@patch("hc.payments.models.braintree")
def test_it_updates_subscription(self, mock):
self._setup_mock(mock)
self.sub = Subscription(user=self.alice)
self.sub.customer_id = "test-customer"
self.sub.subscription_id = "fake-id"
self.sub.save()
mock.Customer.create.return_value.is_success = True
mock.Customer.create.return_value.customer.id = "test-customer-id"
self.client.login(username="[email protected]", password="password")
form = {"payment_method_nonce": "test-nonce"}
self.client.post("/accounts/profile/billing/payment_method/", form)
self.assertTrue(mock.Subscription.update.called)

+ 3
- 3
hc/payments/tests/test_pdf_invoice.py View File

@ -31,7 +31,7 @@ class PdfInvoiceTestCase(BaseTestCase):
self.tx.subscription_details.billing_period_end_date = now()
@skipIf(reportlab is None, "reportlab not installed")
@patch("hc.payments.views.braintree")
@patch("hc.payments.models.braintree")
def test_it_works(self, mock_braintree):
mock_braintree.Transaction.find.return_value = self.tx
@ -40,7 +40,7 @@ class PdfInvoiceTestCase(BaseTestCase):
self.assertTrue(six.b("ABC123") in r.content)
self.assertTrue(six.b("[email protected]") in r.content)
@patch("hc.payments.views.braintree")
@patch("hc.payments.models.braintree")
def test_it_checks_customer_id(self, mock_braintree):
tx = Mock()
@ -54,7 +54,7 @@ class PdfInvoiceTestCase(BaseTestCase):
self.assertEqual(r.status_code, 403)
@skipIf(reportlab is None, "reportlab not installed")
@patch("hc.payments.views.braintree")
@patch("hc.payments.models.braintree")
def test_it_shows_company_data(self, mock_braintree):
self.profile.bill_to = "Alice and Partners"
self.profile.save()


hc/payments/tests/test_create_plan.py → hc/payments/tests/test_set_plan.py View File


+ 15
- 15
hc/payments/urls.py View File

@ -7,29 +7,29 @@ urlpatterns = [
views.pricing,
name="hc-pricing"),
url(r'^billing/$',
url(r'^accounts/profile/billing/$',
views.billing,
name="hc-billing"),
url(r'^invoice/([\w-]+)/$',
views.invoice,
name="hc-invoice"),
url(r'^accounts/profile/billing/history/$',
views.billing_history,
name="hc-billing-history"),
url(r'^accounts/profile/billing/address/$',
views.address,
name="hc-billing-address"),
url(r'^accounts/profile/billing/payment_method/$',
views.payment_method,
name="hc-payment-method"),
url(r'^invoice/pdf/([\w-]+)/$',
views.pdf_invoice,
name="hc-invoice-pdf"),
url(r'^pricing/create_plan/$',
views.create_plan,
name="hc-create-plan"),
url(r'^pricing/update_payment_method/$',
views.update_payment_method,
name="hc-update-payment-method"),
url(r'^pricing/cancel_plan/$',
views.cancel_plan,
name="hc-cancel-plan"),
url(r'^pricing/set_plan/$',
views.set_plan,
name="hc-set-plan"),
url(r'^pricing/get_client_token/$',
views.get_client_token,


+ 80
- 136
hc/payments/views.py View File

@ -1,31 +1,20 @@
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import (HttpResponseBadRequest, HttpResponseForbidden,
JsonResponse, HttpResponse)
from django.shortcuts import redirect, render
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.views.decorators.http import require_POST
from hc.payments.forms import BillToForm
from hc.api.models import Check
from hc.payments.invoices import PdfInvoice
from hc.payments.models import Subscription
if settings.USE_PAYMENTS:
import braintree
else:
# hc.payments tests mock this object, so tests should
# still be able to run:
braintree = None
@login_required
def get_client_token(request):
sub = Subscription.objects.for_user(request.user)
client_token = braintree.ClientToken.generate({
"customer_id": sub.customer_id
})
return JsonResponse({"client_token": client_token})
return JsonResponse({"client_token": sub.get_client_token()})
def pricing(request):
@ -33,24 +22,31 @@ def pricing(request):
ctx = {"page": "pricing"}
return render(request, "payments/pricing_not_owner.html", ctx)
sub = None
if request.user.is_authenticated:
# Don't use Subscription.objects.for_user method here, so a
# subscription object is not created just by viewing a page.
sub = Subscription.objects.filter(user_id=request.user.id).first()
ctx = {"page": "pricing"}
return render(request, "payments/pricing.html", ctx)
period = "monthly"
if sub and sub.plan_id.startswith("Y"):
period = "annual"
@login_required
def billing(request):
if request.team != request.profile:
request.team = request.profile
request.profile.current_team = request.profile
request.profile.save()
# Don't use Subscription.objects.for_user method here, so a
# subscription object is not created just by viewing a page.
sub = Subscription.objects.filter(user_id=request.user.id).first()
ctx = {
"page": "pricing",
"page": "billing",
"profile": request.profile,
"sub": sub,
"period": period,
"first_charge": request.session.pop("first_charge", False)
"num_checks": Check.objects.filter(user=request.user).count(),
"team_size": request.profile.member_set.count() + 1,
"team_max": request.profile.team_limit + 1
}
return render(request, "payments/pricing.html", ctx)
return render(request, "accounts/billing.html", ctx)
def log_and_bail(request, result):
@ -63,63 +59,35 @@ def log_and_bail(request, result):
if not logged_deep_error:
messages.error(request, result.message)
return redirect("hc-pricing")
return redirect("hc-billing")
@login_required
@require_POST
def create_plan(request):
def set_plan(request):
plan_id = request.POST["plan_id"]
if plan_id not in ("P5", "P50", "Y48", "Y480"):
if plan_id not in ("", "P5", "P50", "Y48", "Y480"):
return HttpResponseBadRequest()
sub = Subscription.objects.for_user(request.user)
if sub.plan_id == plan_id:
return redirect("hc-billing")
# Cancel the previous plan
if sub.subscription_id:
braintree.Subscription.cancel(sub.subscription_id)
sub.subscription_id = ""
sub.plan_id = ""
sub.save()
# Create Braintree customer record
if not sub.customer_id:
result = braintree.Customer.create({
"email": request.user.email
})
if not result.is_success:
return log_and_bail(request, result)
sub.customer_id = result.customer.id
sub.save()
# Create Braintree payment method
if "payment_method_nonce" in request.POST:
result = braintree.PaymentMethod.create({
"customer_id": sub.customer_id,
"payment_method_nonce": request.POST["payment_method_nonce"],
"options": {"make_default": True}
})
if not result.is_success:
return log_and_bail(request, result)
sub.payment_method_token = result.payment_method.token
sub.save()
# Create Braintree subscription
result = braintree.Subscription.create({
"payment_method_token": sub.payment_method_token,
"plan_id": plan_id,
})
sub.cancel()
if plan_id == "":
profile = request.user.profile
profile.ping_log_limit = 100
profile.check_limit = 20
profile.team_limit = 2
profile.sms_limit = 0
profile.save()
return redirect("hc-billing")
result = sub.setup(plan_id)
if not result.is_success:
return log_and_bail(request, result)
sub.subscription_id = result.subscription.id
sub.plan_id = plan_id
sub.save()
# Update user's profile
profile = request.user.profile
if plan_id in ("P5", "Y48"):
@ -138,100 +106,76 @@ def create_plan(request):
profile.save()
request.session["first_charge"] = True
return redirect("hc-pricing")
return redirect("hc-billing")
@login_required
@require_POST
def update_payment_method(request):
def address(request):
sub = Subscription.objects.for_user(request.user)
if request.method == "POST":
error = sub.update_address(request.POST)
if error:
return log_and_bail(request, error)
if not sub.customer_id or not sub.subscription_id:
return HttpResponseBadRequest()
if "payment_method_nonce" not in request.POST:
return HttpResponseBadRequest()
result = braintree.PaymentMethod.create({
"customer_id": sub.customer_id,
"payment_method_nonce": request.POST["payment_method_nonce"],
"options": {"make_default": True}
})
if not result.is_success:
return log_and_bail(request, result)
payment_method_token = result.payment_method.token
result = braintree.Subscription.update(sub.subscription_id, {
"payment_method_token": payment_method_token
})
if not result.is_success:
return log_and_bail(request, result)
sub.payment_method_token = payment_method_token
sub.save()
return redirect("hc-billing")
return redirect("hc-pricing")
ctx = {"a": sub.address}
return render(request, "payments/address.html", ctx)
@login_required
@require_POST
def cancel_plan(request):
sub = Subscription.objects.get(user=request.user)
sub.cancel()
def payment_method(request):
sub = get_object_or_404(Subscription, user=request.user)
# Revert to default limits--
profile = request.user.profile
profile.ping_log_limit = 100
profile.check_limit = 20
profile.team_limit = 2
profile.sms_limit = 0
profile.save()
return redirect("hc-pricing")
@login_required
def billing(request):
if request.method == "POST":
form = BillToForm(request.POST)
if form.is_valid():
request.user.profile.bill_to = form.cleaned_data["bill_to"]
request.user.profile.save()
return redirect("hc-billing")
if "payment_method_nonce" not in request.POST:
return HttpResponseBadRequest()
sub = Subscription.objects.get(user=request.user)
nonce = request.POST["payment_method_nonce"]
error = sub.update_payment_method(nonce)
if error:
return log_and_bail(request, error)
transactions = braintree.Transaction.search(
braintree.TransactionSearch.customer_id == sub.customer_id)
return redirect("hc-billing")
ctx = {"transactions": transactions}
return render(request, "payments/billing.html", ctx)
ctx = {
"sub": sub,
"pm": sub.payment_method
}
return render(request, "payments/payment_method.html", ctx)
@login_required
def invoice(request, transaction_id):
sub = Subscription.objects.get(user=request.user)
transaction = braintree.Transaction.find(transaction_id)
if transaction.customer_details.id != sub.customer_id:
return HttpResponseForbidden()
def billing_history(request):
try:
sub = Subscription.objects.get(user=request.user)
transactions = sub.transactions
except Subscription.DoesNotExist:
transactions = []
ctx = {"tx": transaction}
return render(request, "payments/invoice.html", ctx)
ctx = {"transactions": transactions}
return render(request, "payments/billing_history.html", ctx)
@login_required
def pdf_invoice(request, transaction_id):
sub = Subscription.objects.get(user=request.user)
transaction = braintree.Transaction.find(transaction_id)
if transaction.customer_details.id != sub.customer_id:
transaction = sub.get_transaction(transaction_id)
if transaction is None:
return HttpResponseForbidden()
response = HttpResponse(content_type='application/pdf')
filename = "MS-HC-%s.pdf" % transaction.id.upper()
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
bill_to = request.user.profile.bill_to or request.user.email
bill_to = []
if sub.address_id:
ctx = {"a": sub.address}
bill_to = render_to_string("payments/address_plain.html", ctx)
elif request.user.profile.bill_to:
bill_to = request.user.profile.bill_to
else:
bill_to = request.user.email
PdfInvoice(response).render(transaction, bill_to)
return response

+ 0
- 4
static/css/pricing.css View File

@ -64,7 +64,3 @@
margin: 10px 0;
color: #AAA;
}
#payment-method-modal .modal-header {
text-align: center;
}

+ 21
- 0
static/css/profile.css View File

@ -16,3 +16,24 @@
padding: 8px 15px;
}
span.loading {
color: #888;
font-style: italic;
}
#billing-address-modal label {
font-weight: normal;
}
#billing-address-modal .modal-body {
margin-left: 30px;
margin-right: 30px;
}
.masked_number {
padding-left: 8px;
}
.billing-empty {
color: #888;
}

+ 46
- 0
static/js/billing.js View File

@ -0,0 +1,46 @@
$(function () {
var clientTokenRequested = false;
function requestClientToken() {
if (!clientTokenRequested) {
clientTokenRequested = true;
$.getJSON("/pricing/get_client_token/", setupDropin);
}
}
function setupDropin(data) {
braintree.dropin.create({
authorization: data.client_token,
container: "#dropin",
paypal: { flow: 'vault' }
}, function(createErr, instance) {
$("#payment-form-submit").click(function() {
instance.requestPaymentMethod(function (requestPaymentMethodErr, payload) {
$("#pmm-nonce").val(payload.nonce);
$("#payment-form").submit();
});
}).prop("disabled", false);
});
}
$("#update-payment-method").hover(requestClientToken);
$("#update-payment-method").click(function() {
requestClientToken();
$("#payment-form").attr("action", this.dataset.action);
$("#payment-form-submit").text("Update Payment Method");
$("#payment-method-modal").modal("show");
});
$("#billing-history").load( "/accounts/profile/billing/history/" );
$("#billing-address").load( "/accounts/profile/billing/address/", function() {
$("#billing-address input").each(function(idx, obj) {
$("#" + obj.name).val(obj.value);
});
});
$("#payment-method").load( "/accounts/profile/billing/payment_method/", function() {
$("#next-billing-date").text($("#nbd").val());
});
});

+ 8
- 43
static/js/pricing.js View File

@ -1,48 +1,13 @@
$(function () {
var clientTokenRequested = false;
function requestClientToken() {
if (!clientTokenRequested) {
clientTokenRequested = true;
$.getJSON("/pricing/get_client_token/", setupDropin);
$("#period-controls :input").change(function() {
if (this.value == "monthly") {
$("#s-price").text("$5");
$("#p-price").text("$50");
}
}
function setupDropin(data) {
braintree.dropin.create({
authorization: data.client_token,
container: "#dropin",
paypal: { flow: 'vault' }
}, function(createErr, instance) {
$("#payment-form-submit").click(function() {
instance.requestPaymentMethod(function (requestPaymentMethodErr, payload) {
$("#pmm-nonce").val(payload.nonce);
$("#payment-form").submit();
});
}).prop("disabled", false);
});
}
$(".btn-create-payment-method").hover(requestClientToken);
$(".btn-update-payment-method").hover(requestClientToken);
$(".btn-create-payment-method").click(function() {
requestClientToken();
$("#plan_id").val(this.dataset.planId);
$("#payment-form").attr("action", this.dataset.action);
$("#payment-form-submit").text("Set Up Subscription and Pay $" + this.dataset.planPay);
$("#payment-method-modal").modal("show");
});
$(".btn-update-payment-method").click(function() {
requestClientToken();
$("#payment-form").attr("action", this.dataset.action);
$("#payment-form-submit").text("Update Payment Method");
$("#payment-method-modal").modal("show");
});
$("#period-controls :input").change(function() {
$("#monthly").toggleClass("hide", this.value != "monthly");
$("#annual").toggleClass("hide", this.value != "annual");
if (this.value == "annual") {
$("#s-price").text("$4");
$("#p-price").text("$40");
}
});
});

+ 3
- 0
templates/accounts/badges.html View File

@ -14,6 +14,9 @@
<div class="col-sm-2">
<ul class="nav nav-pills nav-stacked">
<li><a href="{% url 'hc-profile' %}">Account</a></li>
{% if show_pricing %}
<li><a href="{% url 'hc-billing' %}">Billing</a></li>
{% endif %}
<li><a href="{% url 'hc-notifications' %}">Email Reports</a></li>
<li class="active"><a href="{% url 'hc-badges' %}">Badges</a></li>
</ul>


+ 428
- 0
templates/accounts/billing.html View File

@ -0,0 +1,428 @@
{% extends "base.html" %}
{% load compress staticfiles hc_extras %}
{% block title %}Account Settings - {% site_name %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<h1 class="settings-title">Settings</h1>
</div>
</div>
<div class="row">
<div class="col-sm-3">
<ul class="nav nav-pills nav-stacked">
<li><a href="{% url 'hc-profile' %}">Account</a></li>
<li class="active"><a href="{% url 'hc-billing' %}">Billing</a></li>
<li><a href="{% url 'hc-notifications' %}">Email Reports</a></li>
<li><a href="{% url 'hc-badges' %}">Badges</a></li>
</ul>
</div>
<div class="col-sm-9 col-md-9">
{% if messages %}
<div class="alert alert-danger">
<p>
<strong>We're sorry!</strong> There was a problem setting
up the subscription. Response from payment gateway:</p>
{% for message in messages %}
<p class="error-message">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
<div class="row">
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-body settings-block">
<h2>Billing Plan</h2>
<table class="table">
<tr>
<td>Current Plan</td>
<td>
{% if sub is None or sub.plan_id == "" %}
Free
{% else %}
{% if sub.plan_id == "P5" or sub.plan_id == "Y48" %}
Standard
{% endif %}
{% if sub.plan_id == "P50" or sub.plan_id == "Y480" %}
Plus
{% endif %}
(${{ sub.price }}/{{ sub.period }})
{% endif %}
</td>
</tr>
{% if sub.plan_id %}
<tr>
<td>Next Payment</td>
<td id="next-billing-date">
<span class="loading">loading…</span>
</td>
</tr>
{% endif %}
<tr>
<td>Checks Used</td>
<td>
{{ num_checks }} of
{% if sub.plan_id %}
unlimited
{% else %}
{{ profile.check_limit }}
{% endif %}
</td>
</tr>
<tr>
<td>Team Size</td>
<td>
{{ team_size }} of
{% if profile.team_limit == 500 %}
unlimited
{% else %}
{{ team_max }}
{% endif %}
</td>
</tr>
</table>
<button
data-toggle="modal"
data-target="#change-billing-plan-modal"
class="btn btn-default pull-right">
Change Billing Plan
</button>
</div>
</div>
<div class="panel panel-default">
<div class="panel-body settings-block">
<h2>Payment Method</h2>
{% if sub.payment_method_token %}
<p id="payment-method">
<span class="loading">loading…</span>
</p>
{% else %}
<p id="payment-method-missing" class="billing-empty">Not set</p>
{% endif %}
<button
id="update-payment-method"
class="btn btn-default pull-right">
Change Payment Method</button>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-body settings-block">
<h2>Billing Address</h2>
{% if sub.address_id %}
<div id="billing-address">
<span class="loading">loading…</span>
</div>
{% else %}
<p id="billing-address-missing" class="billing-empty">
Not set
</p>
{% endif %}
<button
data-toggle="modal"
data-target="#billing-address-modal"
class="btn btn-default pull-right">
Change Billing Address
</button>
</div>
{% if status == "info" %}
<div class="panel-footer">
Your billing address has been updated!
</div>
{% endif %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-body settings-block">
<h2>Billing History</h2>
<div id="billing-history">
<span class="loading">loading…</span>
</div>
</div>
</div>
</div>
</div>
<div id="change-billing-plan-modal" class="modal">
<div class="modal-dialog">
{% if sub.payment_method_token and sub.address_id %}
<form method="post" class="form-horizontal" action="{% url 'hc-set-plan' %}">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="remove-check-title">Change Billing Plan</h4>
</div>
<div class="modal-body">
<h2>Free <small>20 checks, 3 team members</small></h2>
<label class="radio-container">
<input
type="radio"
name="plan_id"
value=""
{% if sub.plan_id == "" %} checked {% endif %}>
<span class="radiomark"></span>
Enjoy free service.
</label>
<h2>Standard <small>Unlimited checks, 20 team members</small></h2>
<label class="radio-container">
<input
type="radio"
name="plan_id"
value="P5"
{% if sub.plan_id == "P5" %} checked {% endif %}>
<span class="radiomark"></span>
Monthly, $5/month
</label>
<label class="radio-container">
<input
type="radio"
name="plan_id"
value="Y48"
{% if sub.plan_id == "Y48" %} checked {% endif %}>
<span class="radiomark"></span>
Yearly, $48/year (20% off monthly)
</label>
<h2>Plus <small>Unlimited checks, unlimited team members</small></h2>
<label class="radio-container">
<input
type="radio"
name="plan_id"
value="P50"
{% if sub.plan_id == "P50" %} checked {% endif %}>
<span class="radiomark"></span>
Monthly, $50/month
</label>
<label class="radio-container">
<input
type="radio"
name="plan_id"
value="Y480"
{% if sub.plan_id == "Y480" %} checked {% endif %}>
<span class="radiomark"></span>
Yearly, $480/year (20% off monthly)
</label>
<div class="alert alert-warning">
<strong>No proration.</strong> We currently do not
support proration when changing billing plans.
Changing the plan starts a new billing cycle
and charges your payment method.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
Change Billing Plan
</button>
</div>
</div>
</form>
{% else %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>Some details are missing…</h4>
</div>
<div class="modal-body">
{% if not sub.payment_method_token %}
<div id="no-payment-method">
<h4>No payment method.</h4>
<p>Please add a payment method before changing the billing
plan.
</p>
</div>
{% endif %}
{% if not sub.address_id %}
<div id="no-billing-address">
<h4>No billing address.</h4>
<p>Please add a billing address before changing
the billing plan.
</p>
</div>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
</div>
</div>
{% endif %}
</div>
</div>
<div id="payment-method-modal" class="modal pm-modal">
<div class="modal-dialog">
<form id="payment-form" method="post" action="{% url 'hc-payment-method' %}">
{% csrf_token %}
<input id="pmm-nonce" type="hidden" name="payment_method_nonce" />
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>Payment Method</h4>
</div>
<div class="modal-body">
<div id="dropin"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
<button id="payment-form-submit" type="button" class="btn btn-primary" disabled>
Confirm Payment Method
</button>
</div>
</div>
</form>
</div>
</div>
<div id="billing-address-modal" class="modal">
<div class="modal-dialog">
<form action="{% url 'hc-billing-address' %}" method="post">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>Billing Address</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<input
id="first_name"
name="first_name"
type="text"
placeholder="First Name"
class="form-control" />
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<input
id="last_name"
name="last_name"
type="text"
placeholder="Last Name"
class="input-name form-control" />
</div>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<div class="form-group">
<input
id="company"
name="company"
placeholder="Company (optional)"
type="text"
class="form-control" />
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<input
id="extended_address"
name="extended_address"
placeholder="VAT ID (optional)"
type="text"
class="form-control" />
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<input
id="street_address"
name="street_address"
placeholder="Street Address"
type="text"
class="form-control" />
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<input
id="locality"
name="locality"
placeholder="City"
type="text"
class="form-control" />
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<input
id="region"
name="region"
placeholder="State / Region"
type="text"
class="form-control" />
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<input
id="postal_code"
name="postal_code"
placeholder="Postal Code"
type="text"
class="form-control" />
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<select id="country_code_alpha2" class="form-control" name="country_code_alpha2">
{% include "payments/countries.html" %}
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://js.braintreegateway.com/web/dropin/1.8.0/js/dropin.min.js"></script>
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/billing.js' %}"></script>
{% endcompress %}
{% endblock %}

+ 3
- 0
templates/accounts/notifications.html View File

@ -14,6 +14,9 @@
<div class="col-sm-3">
<ul class="nav nav-pills nav-stacked">
<li><a href="{% url 'hc-profile' %}">Account</a></li>
{% if show_pricing %}
<li><a href="{% url 'hc-billing' %}">Billing</a></li>
{% endif %}
<li class="active"><a href="{% url 'hc-notifications' %}">Email Reports</a></li>
<li><a href="{% url 'hc-badges' %}">Badges</a></li>
</ul>


+ 3
- 0
templates/accounts/profile.html View File

@ -22,6 +22,9 @@
<div class="col-sm-3">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="{% url 'hc-profile' %}">Account</a></li>
{% if show_pricing %}
<li><a href="{% url 'hc-billing' %}">Billing</a></li>
{% endif %}
<li><a href="{% url 'hc-notifications' %}">Email Reports</a></li>
<li><a href="{% url 'hc-badges' %}">Badges</a></li>
</ul>


+ 41
- 0
templates/payments/address.html View File

@ -0,0 +1,41 @@
{% if a.first_name or a.last_name %}
<p>{{ a.first_name|default:"" }} {{ a.last_name|default:"" }}</p>
{% endif %}
{% if a.company %}
<p>{{ a.company }}</p>
{% endif %}
{% if a.extended_address %}
<p>VAT: {{ a.extended_address }}</p>
{% endif %}
{% if a.street_address %}
<p>{{ a.street_address }}</p>
{% endif %}
{% if a.locality %}
<p>{{ a.locality }}</p>
{% endif %}
{% if a.region %}
<p>{{ a.region }}</p>
{% endif %}
{% if a.country_name %}
<p>{{ a.country_name }}</p>
{% endif %}
{% if a.postal_code %}
<p>{{ a.postal_code }}</p>
{% endif %}
<input type="hidden" name="first_name" value="{{ a.first_name|default:"" }}">
<input type="hidden" name="last_name" value="{{ a.last_name|default:"" }}">
<input type="hidden" name="company" value="{{ a.company|default:"" }}">
<input type="hidden" name="street_address" value="{{ a.street_address|default:"" }}">
<input type="hidden" name="extended_address" value="{{ a.extended_address|default:"" }}">
<input type="hidden" name="locality" value="{{ a.locality|default:"" }}">
<input type="hidden" name="region" value="{{ a.region|default:"" }}">
<input type="hidden" name="country_code_alpha2" value="{{ a.country_code_alpha2|default:"US" }}">
<input type="hidden" name="postal_code" value="{{ a.postal_code|default:"" }}">

+ 8
- 0
templates/payments/address_plain.html View File

@ -0,0 +1,8 @@
{% if a.first_name or a.last_name %}{{ a.first_name|default:"" }} {{ a.last_name|default:"" }}
{% endif %}{% if a.company %}{{ a.company }}
{% endif %}{% if a.extended_address %}VAT: {{ a.extended_address }}
{% endif %}{% if a.street_address %}{{ a.street_address }}
{% endif %}{% if a.locality %}{{ a.locality }}
{% endif %}{% if a.region %}{{ a.region }}
{% endif %}{% if a.country_name %}{{ a.country_name }}
{% endif %}{% if a.postal_code %}{{ a.postal_code }}{% endif %}

+ 0
- 98
templates/payments/billing.html View File

@ -1,98 +0,0 @@
{% extends "base.html" %}
{% block title %}Billing History - healthchecks.io{% endblock %}
{% block content %}
<h1>Billing History</h1>
<div class="row">
<div class="col-sm-9">
<table class="table">
<tr>
<th>Date</th>
<th>Payment Method</th>
<th>Amount</th>
<th>Status</th>
<th></th>
</tr>
{% for tx in transactions %}
<tr>
<td>{{ tx.created_at }}</td>
<td>
{% if tx.payment_instrument_type == "paypal_account" %}
Paypal from {{ tx.paypal.payer_email }}
{% endif %}
{% if tx.payment_instrument_type == "credit_card" %}
{{ tx.credit_card.card_type }} ending in {{ tx.credit_card.last_4 }}
{% endif %}
</td>
<td>
{% if tx.currency_iso_code == "USD" %}
${{ tx.amount }}
{% elif tx.currency_iso_code == "EUR" %}
€{{ tx.amount }}
{% else %}
{{ tx.currency_iso_code }} {{ tx.amount }}
{% endif %}
</td>
<td><code>{{ tx.status }}</code></td>
<td>
<a href="{% url 'hc-invoice-pdf' tx.id %}">PDF Invoice</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5">
No past transactions to display here
</td>
</tr>
{% endfor%}
</table>
</div>
<div class="col-sm-3">
<p><strong>Bill to:</strong></p>
<p>
{% if request.user.profile.bill_to %}
{{ request.user.profile.bill_to|linebreaksbr }}
{% else %}
{{ request.user.email }}
{% endif %}
</p>
<button
data-toggle="modal"
data-target="#bill-to-modal"
class="btn btn-default">
Edit Company Details…
</button>
</div>
</div>
<div id="bill-to-modal" class="modal">
<div class="modal-dialog">
<form id="bill-to-form" method="post">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>Company Details for Invoice</h4>
</div>
<div class="modal-body">
<textarea
name="bill_to"
class="form-control"
rows="5">{{ request.user.profile.bill_to|default:request.user.email }}</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

+ 42
- 0
templates/payments/billing_history.html View File

@ -0,0 +1,42 @@
{% if transactions %}
<table class="table">
<tr>
<th>Date</th>
<th>Payment Method</th>
<th>Amount</th>
<th>Status</th>
<th></th>
</tr>
{% for tx in transactions %}
<tr>
<td>{{ tx.created_at }}</td>
<td>
{% if tx.payment_instrument_type == "paypal_account" %}
Paypal from {{ tx.paypal.payer_email }}
{% endif %}
{% if tx.payment_instrument_type == "credit_card" %}
{{ tx.credit_card.card_type }} ending in {{ tx.credit_card.last_4 }}
{% endif %}
</td>
<td>
{% if tx.currency_iso_code == "USD" %}
${{ tx.amount }}
{% elif tx.currency_iso_code == "EUR" %}
€{{ tx.amount }}
{% else %}
{{ tx.currency_iso_code }} {{ tx.amount }}
{% endif %}
</td>
<td><code>{{ tx.status }}</code></td>
<td>
<a href="{% url 'hc-invoice-pdf' tx.id %}">PDF Invoice</a>
</td>
</tr>
{% endfor%}
</table>
{% else %}
<p class="billing-empty">
No past transactions to display here
</p>
{% endif %}

+ 250
- 0
templates/payments/countries.html View File

@ -0,0 +1,250 @@
<option value="US">United States of America</option>
<option value="AF">Afghanistan</option>
<option value="AX">Åland</option>
<option value="AL">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
<option value="AD">Andorra</option>
<option value="AO">Angola</option>
<option value="AI">Anguilla</option>
<option value="AQ">Antarctica</option>
<option value="AG">Antigua and Barbuda</option>
<option value="AR">Argentina</option>
<option value="AM">Armenia</option>
<option value="AW">Aruba</option>
<option value="AU">Australia</option>
<option value="AT">Austria</option>
<option value="AZ">Azerbaijan</option>
<option value="BS">Bahamas</option>
<option value="BH">Bahrain</option>
<option value="BD">Bangladesh</option>
<option value="BB">Barbados</option>
<option value="BY">Belarus</option>
<option value="BE">Belgium</option>
<option value="BZ">Belize</option>
<option value="BJ">Benin</option>
<option value="BM">Bermuda</option>
<option value="BT">Bhutan</option>
<option value="BO">Bolivia</option>
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="BW">Botswana</option>
<option value="BV">Bouvet Island</option>
<option value="BR">Brazil</option>
<option value="IO">British Indian Ocean Territory</option>
<option value="BN">Brunei Darussalam</option>
<option value="BG">Bulgaria</option>
<option value="BF">Burkina Faso</option>
<option value="BI">Burundi</option>
<option value="KH">Cambodia</option>
<option value="CM">Cameroon</option>
<option value="CA">Canada</option>
<option value="CV">Cape Verde</option>
<option value="KY">Cayman Islands</option>
<option value="CF">Central African Republic</option>
<option value="TD">Chad</option>
<option value="CL">Chile</option>
<option value="CN">China</option>
<option value="CX">Christmas Island</option>
<option value="CC">Cocos (Keeling) Islands</option>
<option value="CO">Colombia</option>
<option value="KM">Comoros</option>
<option value="CG">Congo (Brazzaville)</option>
<option value="CD">Congo (Kinshasa)</option>
<option value="CK">Cook Islands</option>
<option value="CR">Costa Rica</option>
<option value="CI">Côte d'Ivoire</option>
<option value="HR">Croatia</option>
<option value="CU">Cuba</option>
<option value="CW">Curaçao</option>
<option value="CY">Cyprus</option>
<option value="CZ">Czech Republic</option>
<option value="DK">Denmark</option>
<option value="DJ">Djibouti</option>
<option value="DM">Dominica</option>
<option value="DO">Dominican Republic</option>
<option value="EC">Ecuador</option>
<option value="EG">Egypt</option>
<option value="SV">El Salvador</option>
<option value="GQ">Equatorial Guinea</option>
<option value="ER">Eritrea</option>
<option value="EE">Estonia</option>
<option value="ET">Ethiopia</option>
<option value="FK">Falkland Islands</option>
<option value="FO">Faroe Islands</option>
<option value="FJ">Fiji</option>
<option value="FI">Finland</option>
<option value="FR">France</option>
<option value="GF">French Guiana</option>
<option value="PF">French Polynesia</option>
<option value="TF">French Southern Lands</option>
<option value="GA">Gabon</option>
<option value="GM">Gambia</option>
<option value="GE">Georgia</option>
<option value="DE">Germany</option>
<option value="GH">Ghana</option>
<option value="GI">Gibraltar</option>
<option value="GR">Greece</option>
<option value="GL">Greenland</option>
<option value="GD">Grenada</option>
<option value="GP">Guadeloupe</option>
<option value="GU">Guam</option>
<option value="GT">Guatemala</option>
<option value="GG">Guernsey</option>
<option value="GN">Guinea</option>
<option value="GW">Guinea-Bissau</option>
<option value="GY">Guyana</option>
<option value="HT">Haiti</option>
<option value="HM">Heard and McDonald Islands</option>
<option value="HN">Honduras</option>
<option value="HK">Hong Kong</option>
<option value="HU">Hungary</option>
<option value="IS">Iceland</option>
<option value="IN">India</option>
<option value="ID">Indonesia</option>
<option value="IR">Iran</option>
<option value="IQ">Iraq</option>
<option value="IE">Ireland</option>
<option value="IM">Isle of Man</option>
<option value="IL">Israel</option>
<option value="IT">Italy</option>
<option value="JM">Jamaica</option>
<option value="JP">Japan</option>
<option value="JE">Jersey</option>
<option value="JO">Jordan</option>
<option value="KZ">Kazakhstan</option>
<option value="KE">Kenya</option>
<option value="KI">Kiribati</option>
<option value="KP">Korea, North</option>
<option value="KR">Korea, South</option>
<option value="KW">Kuwait</option>
<option value="KG">Kyrgyzstan</option>
<option value="LA">Laos</option>
<option value="LV">Latvia</option>
<option value="LB">Lebanon</option>
<option value="LS">Lesotho</option>
<option value="LR">Liberia</option>
<option value="LY">Libya</option>
<option value="LI">Liechtenstein</option>
<option value="LT">Lithuania</option>
<option value="LU">Luxembourg</option>
<option value="MO">Macau</option>
<option value="MK">Macedonia</option>
<option value="MG">Madagascar</option>
<option value="MW">Malawi</option>
<option value="MY">Malaysia</option>
<option value="MV">Maldives</option>
<option value="ML">Mali</option>
<option value="MT">Malta</option>
<option value="MH">Marshall Islands</option>
<option value="MQ">Martinique</option>
<option value="MR">Mauritania</option>
<option value="MU">Mauritius</option>
<option value="YT">Mayotte</option>
<option value="MX">Mexico</option>
<option value="FM">Micronesia</option>
<option value="MD">Moldova</option>
<option value="MC">Monaco</option>
<option value="MN">Mongolia</option>
<option value="ME">Montenegro</option>
<option value="MS">Montserrat</option>
<option value="MA">Morocco</option>
<option value="MZ">Mozambique</option>
<option value="MM">Myanmar</option>
<option value="NA">Namibia</option>
<option value="NR">Nauru</option>
<option value="NP">Nepal</option>
<option value="NL">Netherlands</option>
<option value="AN">Netherlands Antilles</option>
<option value="NC">New Caledonia</option>
<option value="NZ">New Zealand</option>
<option value="NI">Nicaragua</option>
<option value="NE">Niger</option>
<option value="NG">Nigeria</option>
<option value="NU">Niue</option>
<option value="NF">Norfolk Island</option>
<option value="MP">Northern Mariana Islands</option>
<option value="NO">Norway</option>
<option value="OM">Oman</option>
<option value="PK">Pakistan</option>
<option value="PW">Palau</option>
<option value="PS">Palestine</option>
<option value="PA">Panama</option>
<option value="PG">Papua New Guinea</option>
<option value="PY">Paraguay</option>
<option value="PE">Peru</option>
<option value="PH">Philippines</option>
<option value="PN">Pitcairn</option>
<option value="PL">Poland</option>
<option value="PT">Portugal</option>
<option value="PR">Puerto Rico</option>
<option value="QA">Qatar</option>
<option value="RE">Reunion</option>
<option value="RO">Romania</option>
<option value="RU">Russian Federation</option>
<option value="RW">Rwanda</option>
<option value="BL">Saint Barthélemy</option>
<option value="SH">Saint Helena</option>
<option value="KN">Saint Kitts and Nevis</option>
<option value="LC">Saint Lucia</option>
<option value="MF">Saint Martin (French part)</option>
<option value="PM">Saint Pierre and Miquelon</option>
<option value="VC">Saint Vincent and the Grenadines</option>
<option value="WS">Samoa</option>
<option value="SM">San Marino</option>
<option value="ST">Sao Tome and Principe</option>
<option value="SA">Saudi Arabia</option>
<option value="SN">Senegal</option>
<option value="RS">Serbia</option>
<option value="SC">Seychelles</option>
<option value="SL">Sierra Leone</option>
<option value="SG">Singapore</option>
<option value="SX">Sint Maarten (Dutch part)</option>
<option value="SK">Slovakia</option>
<option value="SI">Slovenia</option>
<option value="SB">Solomon Islands</option>
<option value="SO">Somalia</option>
<option value="ZA">South Africa</option>
<option value="GS">South Georgia and South Sandwich Islands</option>
<option value="SS">South Sudan</option>
<option value="ES">Spain</option>
<option value="LK">Sri Lanka</option>
<option value="SD">Sudan</option>
<option value="SR">Suriname</option>
<option value="SJ">Svalbard and Jan Mayen Islands</option>
<option value="SZ">Swaziland</option>
<option value="SE">Sweden</option>
<option value="CH">Switzerland</option>
<option value="SY">Syria</option>
<option value="TW">Taiwan</option>
<option value="TJ">Tajikistan</option>
<option value="TZ">Tanzania</option>
<option value="TH">Thailand</option>
<option value="TL">Timor-Leste</option>
<option value="TG">Togo</option>
<option value="TK">Tokelau</option>
<option value="TO">Tonga</option>
<option value="TT">Trinidad and Tobago</option>
<option value="TN">Tunisia</option>
<option value="TR">Turkey</option>
<option value="TM">Turkmenistan</option>
<option value="TC">Turks and Caicos Islands</option>
<option value="TV">Tuvalu</option>
<option value="UG">Uganda</option>
<option value="UA">Ukraine</option>
<option value="AE">United Arab Emirates</option>
<option value="GB">United Kingdom</option>
<option value="UM">United States Minor Outlying Islands</option>
<option value="UY">Uruguay</option>
<option value="UZ">Uzbekistan</option>
<option value="VU">Vanuatu</option>
<option value="VA">Vatican City</option>
<option value="VE">Venezuela</option>
<option value="VN">Vietnam</option>
<option value="VG">Virgin Islands, British</option>
<option value="VI">Virgin Islands, U.S.</option>
<option value="WF">Wallis and Futuna Islands</option>
<option value="EH">Western Sahara</option>
<option value="YE">Yemen</option>
<option value="ZM">Zambia</option>
<option value="ZW">Zimbabwe</option>

+ 14
- 0
templates/payments/payment_method.html View File

@ -0,0 +1,14 @@
{% if sub.pm_is_card %}
<img src="{{ pm.image_url }}" height="30" /> <span class="masked_number">{{ pm.masked_number }}</span>
{% if pm.expired %}
<span class="text-danger">(expired)</span>
{% endif %}
{% endif %}
{% if sub.pm_is_paypal %}
<img src="{{ pm.image_url }}" height="30" /> {{ pm.email }}
{% endif %}
{% if sub.subscription_id %}
<input type="hidden" id="nbd" value="{{ sub.next_billing_date }}" />
{% endif %}

+ 86
- 306
templates/payments/pricing.html View File

@ -8,51 +8,32 @@
<!-- Plans -->
<section id="plans" {% if request.user.is_authenticated %} data- {% endif %}>
<div class="container">
{% if messages %}
<div class="alert alert-danger">
<p>
<strong>We're sorry!</strong> There was a problem setting
up the subscription. Response from payment gateway:</p>
{% for message in messages %}
<p class="error-message">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% if sub.plan_id %}
{% if request.user.is_authenticated %}
<div class="row">
<div class="col-md-12">
<div id="subscription-status" class="jumbotron">
<p>
{% if first_charge %}
Success! You just paid ${{ sub.price }}.
Your account is currently on the
{% if sub.plan_id == "P5" %}
<strong>Monthly Standard</strong>
{% elif sub.plan_id == "P50" %}
<strong>Monthly Plus</strong>
{% elif sub.plan_id == "Y48" %}
<strong>Yearly Standard</strong>
{% elif sub.plan_id == "Y480" %}
<strong>Yearly Plus</strong>
{% else %}
You are currently paying ${{ sub.price }}/{{ sub.period }}
{% if sub.pm_is_credit_card %}
using {{ sub.card_type }} card
ending with {{ sub.last_4 }}.
{% endif %}
{% if sub.pm_is_paypal %}
using PayPal account
{{ sub.paypal_email }}.
{% endif %}
<strong>Free</strong>
{% endif %}
plan.
{% if sub.plan_id %}
You are paying
<strong>${{ sub.price }}</strong> / {{ sub.period }}.
{% endif %}
</p>
<p>Thank you for supporting healthchecks.io!</p>
<p>
<a class="btn btn-default" href="{% url 'hc-billing' %}">See Billing History</a>
{% if not first_charge %}
<button
class="btn btn-default btn-update-payment-method"
data-action="{% url 'hc-update-payment-method' %}">
Change Payment Method
</button>
{% endif %}
<a class="btn btn-default" href="{% url 'hc-billing' %}">Billing Details</a>
</p>
</div>
</div>
@ -68,20 +49,19 @@
<div class="row">
<div class="col-sm-12" id="period-controls">
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-default {% if period == "monthly" %} active {% endif %}">
<label class="btn btn-default active">
<input
type="radio"
name="period"
value="monthly"
{% if period == "monthly" %} checked {% endif %}
checked
autocomplete="off"> Monthly
</label>
<label class="btn btn-default {% if period == "annual" %} active {% endif %}">
<label class="btn btn-default">
<input
type="radio"
name="period"
value="annual"
{% if period == "annual" %} checked {% endif %}
autocomplete="off"> Annual
</label>
</div>
@ -89,255 +69,84 @@
</div>
</div>
<div id="monthly" class="row {% if period != "monthly" %} hide {% endif %}">
<!-- Free -->
<div class="col-sm-4 text-center">
<div class="panel panel-default">
<div class="panel-body text-center free">
<h1>Free</h1>
<h2>$0<span class="mo">/mo</span></h2>
</div>
<ul class="list-group text-center">
<li class="list-group-item"><i class="fa fa-check"></i> 20 Checks</li>
<li class="list-group-item">3 Team Members</li>
<li class="list-group-item">100 log entries per check</li>
<li class="list-group-item">&nbsp;</li>
<li class="list-group-item">&nbsp;</li>
</ul>
<div class="panel-footer">
{% if request.user.is_authenticated %}
{% if sub.subscription_id %}
<form method="post" action="{% url 'hc-cancel-plan' %}">
{% csrf_token %}
<button type="submit" class="btn btn-lg btn-default">
Switch To Free
</button>
</form>
{% else %}
<a class="btn btn-lg btn-success disabled" href="#">Selected</a>
{% endif %}
{% else %}
<a class="btn btn-lg btn-success" href="{% url 'hc-login' %}">Get Started</a>
{% endif %}
</div>
<!-- Free -->
<div class="col-sm-4 text-center">
<div class="panel panel-default">
<div class="panel-body text-center free">
<h1>Free</h1>
<h2>$0<span class="mo">/mo</span></h2>
</div>
</div>
<!-- /item -->
<!-- P5 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default">
<div class="panel-body text-center">
<h1>Standard</h1>
<h2>$5<span class="mo">/mo</span></h2>
</div>
<ul class="list-group text-center">
<li class="list-group-item">Unlimited Checks</li>
<li class="list-group-item">10 Team Members</li>
<li class="list-group-item">1000 log entries per check</li>
<li class="list-group-item">50 SMS alerts per month</li>
<li class="list-group-item">Email Support</li>
</ul>
<div class="panel-footer">
{% if request.user.is_authenticated %}
{% if sub.plan_id == "P5" %}
<button class="btn btn-lg btn-success disabled">
Selected
</button>
{% else %}
<button
data-plan-id="P5"
data-plan-pay="5"
data-action="{% url 'hc-create-plan' %}"
class="btn btn-lg btn-default btn-create-payment-method">
{% if not sub.subscription_id %}
Upgrade your Account
{% else %}
Switch to $5/mo
{% endif %}
</button>
{% endif %}
{% else %}
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">
Get Started
</a>
{% endif %}
</div>
</div>
</div>
<!-- /item -->
<!-- P50 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default">
<div class="panel-body text-center">
<h1>Plus</h1>
<h2>$50<span class="mo">/mo</span></h2>
</div>
<ul class="list-group text-center">
<li class="list-group-item">Unlimited Checks</li>
<li class="list-group-item">Unlimited Team Members</li>
<li class="list-group-item">1000 log entries per check</li>
<li class="list-group-item">500 SMS alerts per month</li>
<li class="list-group-item">Priority Email Support</li>
</ul>
<div class="panel-footer">
{% if request.user.is_authenticated %}
{% if sub.plan_id == "P50" %}
<button class="btn btn-lg btn-success disabled">
Selected
</button>
{% else %}
<button
data-plan-id="P50"
data-plan-pay="50"
data-action="{% url 'hc-create-plan' %}"
class="btn btn-lg btn-default btn-create-payment-method">
{% if not sub.subscription_id %}
Upgrade your Account
{% else %}
Switch to $50/mo
{% endif %}
</button>
{% endif %}
{% else %}
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">
Get Started
</a>
{% endif %}
</div>
<ul class="list-group text-center">
<li class="list-group-item"><i class="fa fa-check"></i> 20 Checks</li>
<li class="list-group-item">3 Team Members</li>
<li class="list-group-item">100 log entries per check</li>
<li class="list-group-item">&nbsp;</li>
<li class="list-group-item">&nbsp;</li>
</ul>
{% if not request.user.is_authenticated %}
<div class="panel-footer">
<a class="btn btn-lg btn-success" href="{% url 'hc-login' %}">Get Started</a>
</div>
{% endif %}
</div>
<!-- /item -->
</div>
<div id="annual" class="row {% if period != "annual" %} hide {% endif %}">
<!-- Free -->
<div class="col-sm-4 text-center">
<div class="panel panel-default">
<div class="panel-body text-center free">
<h1>Free</h1>
<h2>$0<span class="mo">/mo</span></h2>
</div>
<ul class="list-group text-center">
<li class="list-group-item"><i class="fa fa-check"></i> 20 Checks</li>
<li class="list-group-item">3 Team Members</li>
<li class="list-group-item">100 log entries per check</li>
<li class="list-group-item">&nbsp;</li>
<li class="list-group-item">&nbsp;</li>
</ul>
<div class="panel-footer">
{% if request.user.is_authenticated %}
{% if sub.subscription_id %}
<form method="post" action="{% url 'hc-cancel-plan' %}">
{% csrf_token %}
<button type="submit" class="btn btn-lg btn-default">
Switch To Free
</button>
</form>
{% else %}
<a class="btn btn-lg btn-success disabled" href="#">Selected</a>
{% endif %}
{% else %}
<a class="btn btn-lg btn-success" href="{% url 'hc-login' %}">Get Started</a>
{% endif %}
</div>
<!-- /item -->
<!-- P5 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default">
<div class="panel-body text-center">
<h1>Standard</h1>
<h2>
<span id="s-price">$5</span><span class="mo">/mo</span>
</h2>
</div>
</div>
<!-- /item -->
<!-- Y48 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default">
<div class="panel-body text-center">
<h1>Standard</h1>
<h2>$4<span class="mo">/mo</span></h2>
</div>
<ul class="list-group text-center">
<li class="list-group-item">Unlimited Checks</li>
<li class="list-group-item">10 Team Members</li>
<li class="list-group-item">1000 log entries per check</li>
<li class="list-group-item">50 SMS alerts per month</li>
<li class="list-group-item">Email Support</li>
</ul>
<div class="panel-footer">
{% if request.user.is_authenticated %}
{% if sub.plan_id == "Y48" %}
<button class="btn btn-lg btn-success disabled">
Selected
</button>
{% else %}
<button
data-plan-id="Y48"
data-plan-pay="48"
data-action="{% url 'hc-create-plan' %}"
class="btn btn-lg btn-default btn-create-payment-method">
{% if not sub.subscription_id %}
Upgrade your Account
{% else %}
Switch to $4/mo
{% endif %}
</button>
{% endif %}
{% else %}
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">
Get Started
</a>
{% endif %}
</div>
<ul class="list-group text-center">
<li class="list-group-item">Unlimited Checks</li>
<li class="list-group-item">10 Team Members</li>
<li class="list-group-item">1000 log entries per check</li>
<li class="list-group-item">50 SMS alerts per month</li>
<li class="list-group-item">Email Support</li>
</ul>
{% if not request.user.is_authenticated %}
<div class="panel-footer">
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">
Get Started
</a>
</div>
{% endif %}
</div>
<!-- /item -->
<!-- Y480 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default">
<div class="panel-body text-center">
<h1>Plus</h1>
<h2>$40<span class="mo">/mo</span></h2>
</div>
</div>
<!-- /item -->
<!-- P50 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default">
<div class="panel-body text-center">
<h1>Plus</h1>
<h2>
<span id="p-price">$50</span><span class="mo">/mo</span>
</h2>
</div>
<ul class="list-group text-center">
<li class="list-group-item">Unlimited Checks</li>
<li class="list-group-item">Unlimited Team Members</li>
<li class="list-group-item">1000 log entries per check</li>
<li class="list-group-item">500 SMS alerts per month</li>
<li class="list-group-item">Priority Email Support</li>
</ul>
<div class="panel-footer">
{% if request.user.is_authenticated %}
{% if sub.plan_id == "Y480" %}
<button class="btn btn-lg btn-success disabled">
Selected
</button>
{% else %}
<button
data-plan-id="Y480"
data-plan-pay="480"
data-action="{% url 'hc-create-plan' %}"
class="btn btn-lg btn-default btn-create-payment-method">
{% if not sub.subscription_id %}
Upgrade your Account
{% else %}
Switch to $40/mo
{% endif %}
</button>
{% endif %}
{% else %}
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">
Get Started
</a>
{% endif %}
</div>
<ul class="list-group text-center">
<li class="list-group-item">Unlimited Checks</li>
<li class="list-group-item">Unlimited Team Members</li>
<li class="list-group-item">1000 log entries per check</li>
<li class="list-group-item">500 SMS alerts per month</li>
<li class="list-group-item">Priority Email Support</li>
</ul>
{% if not request.user.is_authenticated %}
<div class="panel-footer">
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">
Get Started
</a>
</div>
{% endif %}
</div>
<!-- /item -->
</div>
<!-- /item -->
</div>
</section>
<!-- /Plans -->
@ -416,38 +225,9 @@
</div>
</div>
</section>
<div id="payment-method-modal" class="modal pm-modal">
<div class="modal-dialog">
<form id="payment-form" method="post" action="{% url 'hc-create-plan' %}">
{% csrf_token %}
<input id="plan_id" type="hidden" name="plan_id" value="" />
<input id="pmm-nonce" type="hidden" name="payment_method_nonce" />
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>Subscription for {{ request.user.profile }}</h4>
</div>
<div class="modal-body">
<div id="dropin"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
<button id="payment-form-submit" type="button" class="btn btn-primary" disabled>
Submit
</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://js.braintreegateway.com/web/dropin/1.8.0/js/dropin.min.js"></script>
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>


Loading…
Cancel
Save