From cf5cbfaa3cb51b67db3c3cff27c0c7b60fd0ca27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Sun, 5 Nov 2017 14:25:39 +0200 Subject: [PATCH] Annual subscriptions, updated Braintree Drop-in integration --- hc/payments/models.py | 12 ++ hc/payments/tests/test_create_plan.py | 30 +++- hc/payments/views.py | 17 +- static/css/pricing.css | 29 ++- static/js/pricing.js | 57 ++++-- templates/payments/pricing.html | 245 ++++++++++++++++++++------ 6 files changed, 304 insertions(+), 86 deletions(-) diff --git a/hc/payments/models.py b/hc/payments/models.py index a73ffdaa..ad55a9dd 100644 --- a/hc/payments/models.py +++ b/hc/payments/models.py @@ -31,9 +31,21 @@ class Subscription(models.Model): return 5 elif self.plan_id == "P50": return 50 + elif self.plan_id == "Y48": + return 48 + elif self.plan_id == "Y480": + return 480 return 0 + def period(self): + if self.plan_id.startswith("P"): + return "month" + elif self.plan_id.startswith("Y"): + return "year" + + raise NotImplementedError("Unexpected plan: %s" % self.plan_id) + def _get_braintree_payment_method(self): if not hasattr(self, "_pm"): self._pm = braintree.PaymentMethod.find(self.payment_method_token) diff --git a/hc/payments/tests/test_create_plan.py b/hc/payments/tests/test_create_plan.py index 517112f2..fb255a7e 100644 --- a/hc/payments/tests/test_create_plan.py +++ b/hc/payments/tests/test_create_plan.py @@ -1,6 +1,5 @@ from mock import patch -from hc.accounts.models import Profile from hc.payments.models import Subscription from hc.test import BaseTestCase @@ -53,6 +52,35 @@ class CreatePlanTestCase(BaseTestCase): # braintree.Subscription.cancel should have not been called assert not mock.Subscription.cancel.called + @patch("hc.payments.views.braintree") + def test_yearly_works(self, mock): + self._setup_mock(mock) + + self.profile.sms_limit = 0 + self.profile.sms_sent = 1 + self.profile.save() + + r = self.run_create_plan("Y48") + self.assertRedirects(r, "/pricing/") + + # Subscription should be filled out: + sub = Subscription.objects.get(user=self.alice) + self.assertEqual(sub.customer_id, "test-customer-id") + self.assertEqual(sub.payment_method_token, "t-token") + self.assertEqual(sub.subscription_id, "t-sub-id") + self.assertEqual(sub.plan_id, "Y48") + + # User's profile should have a higher limits + self.profile.refresh_from_db() + self.assertEqual(self.profile.ping_log_limit, 1000) + self.assertEqual(self.profile.check_limit, 500) + self.assertEqual(self.profile.team_limit, 9) + self.assertEqual(self.profile.sms_limit, 50) + self.assertEqual(self.profile.sms_sent, 0) + + # braintree.Subscription.cancel should have not been called + assert not mock.Subscription.cancel.called + def test_bad_plan_id(self): r = self.run_create_plan(plan_id="this-is-wrong") self.assertEqual(r.status_code, 400) diff --git a/hc/payments/views.py b/hc/payments/views.py index e0e173de..d7030d16 100644 --- a/hc/payments/views.py +++ b/hc/payments/views.py @@ -38,9 +38,14 @@ def pricing(request): # subscription object is not created just by viewing a page. sub = Subscription.objects.filter(user_id=request.user.id).first() + period = "monthly" + if sub and sub.plan_id.startswith("Y"): + period = "annual" + ctx = { "page": "pricing", "sub": sub, + "period": period, "first_charge": request.session.pop("first_charge", False) } @@ -48,9 +53,13 @@ def pricing(request): def log_and_bail(request, result): + logged_deep_error = False + for error in result.errors.deep_errors: messages.error(request, error.message) - else: + logged_deep_error = True + + if not logged_deep_error: messages.error(request, result.message) return redirect("hc-pricing") @@ -60,7 +69,7 @@ def log_and_bail(request, result): @require_POST def create_plan(request): plan_id = request.POST["plan_id"] - if plan_id not in ("P5", "P50"): + if plan_id not in ("P5", "P50", "Y48", "Y480"): return HttpResponseBadRequest() sub = Subscription.objects.for_user(request.user) @@ -111,14 +120,14 @@ def create_plan(request): # Update user's profile profile = request.user.profile - if plan_id == "P5": + if plan_id in ("P5", "Y48"): profile.ping_log_limit = 1000 profile.check_limit = 500 profile.team_limit = 9 profile.sms_limit = 50 profile.sms_sent = 0 profile.save() - elif plan_id == "P50": + elif plan_id in ("P50", "Y480"): profile.ping_log_limit = 1000 profile.check_limit = 500 profile.team_limit = 500 diff --git a/static/css/pricing.css b/static/css/pricing.css index 124e9108..33069c25 100644 --- a/static/css/pricing.css +++ b/static/css/pricing.css @@ -1,9 +1,5 @@ -.panel-pricing .panel-heading { - padding: 20px 10px; -} .panel-pricing .list-group-item { color: #777777; - border-bottom: 1px solid rgba(250, 250, 250, 0.5); } .panel-pricing .list-group-item:last-child { border-bottom-right-radius: 0px; @@ -46,10 +42,29 @@ font-weight: bold; } -.panel-pricing .free { -} - .mo { font-size: 18px; color: #888; +} + +#period-controls { + text-align: center; + padding: 24px; +} + +#period-controls .btn { + width: 100px; +} + +#plans .panel-footer { + background: transparent; +} + +#annual-note { + margin: 10px 0; + color: #AAA; +} + +#payment-method-modal .modal-header { + text-align: center; } \ No newline at end of file diff --git a/static/js/pricing.js b/static/js/pricing.js index 24cde955..9edfedad 100644 --- a/static/js/pricing.js +++ b/static/js/pricing.js @@ -1,29 +1,48 @@ $(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); + }); + } + + $(".btn-create-payment-method").hover(requestClientToken); + $(".btn-update-payment-method").hover(requestClientToken); $(".btn-create-payment-method").click(function() { - var planId = $(this).data("plan-id"); - $("#plan_id").val(planId); - $.getJSON("/pricing/get_client_token/", function(data) { - var $modal = $("#payment-method-modal"); - braintree.setup(data.client_token, "dropin", { - container: "payment-form" - }); - $modal.modal("show"); - }) + 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() { - $.getJSON("/pricing/get_client_token/", function(data) { - var $modal = $("#update-payment-method-modal"); - braintree.setup(data.client_token, "dropin", { - container: "update-payment-form" - }); - $modal.modal("show"); - }) + requestClientToken(); + $("#payment-form").attr("action", this.dataset.action); + $("#payment-form-submit").text("Update Payment Method"); + $("#payment-method-modal").modal("show"); }); - $(".pm-modal").on("hidden.bs.modal", function() { - location.reload(); - }) + $("#period-controls :input").change(function() { + $("#monthly").toggleClass("hide", this.value != "monthly"); + $("#annual").toggleClass("hide", this.value != "annual"); + }); }); \ No newline at end of file diff --git a/templates/payments/pricing.html b/templates/payments/pricing.html index 711a33ef..46b1f74e 100644 --- a/templates/payments/pricing.html +++ b/templates/payments/pricing.html @@ -1,12 +1,12 @@ {% extends "base.html" %} -{% load staticfiles compress %} +{% load staticfiles compress hc_extras %} {% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} {% block content %} -
+
{% if messages %}
@@ -28,7 +28,7 @@ {% if first_charge %} Success! You just paid ${{ sub.price }}. {% else %} - You are currently paying ${{ sub.price }}/month + You are currently paying ${{ sub.price }}/{{ sub.period }} {% if sub.pm_is_credit_card %} using {{ sub.card_type }} card @@ -47,7 +47,9 @@

See Billing History {% if not first_charge %} - {% endif %} @@ -55,22 +57,51 @@

+ {% else %} +
+
+

{% site_name %} Pricing Plans

+
+
{% endif %} -
+
+
+ + +
+

(20% off on annual plan)

+
+
+ +
-
+
-

free

+

Free

+

$0/mo

  • 20 Checks
  • -
  • 100 log entries per check
  • -
  • Personal or Commercial use
  • 3 Team Members
  • +
  • 100 log entries per check
  •  
  •  
@@ -96,16 +127,16 @@
-
+
-

$5/mo

+

Standard

+

$5/mo

  • Unlimited Checks
  • -
  • 1000 log entries per check
  • -
  • Personal or Commercial use
  • 10 Team Members
  • +
  • 1000 log entries per check
  • 50 SMS alerts per month
  • Email Support
@@ -118,11 +149,13 @@ {% else %} {% endif %} @@ -138,16 +171,16 @@
-
+
-

$50/mo

+

Plus

+

$50/mo

  • Unlimited Checks
  • -
  • 1000 log entries per check
  • -
  • Personal or Commercial use
  • Unlimited Team Members
  • +
  • 1000 log entries per check
  • 500 SMS alerts per month
  • Priority Email Support
@@ -160,8 +193,94 @@ {% else %} + {% endif %} + {% else %} + + Get Started + + {% endif %} +
+
+
+ +
+ +
+ +
+
+
+

Free

+

$0/mo

+
+
    +
  • 20 Checks
  • +
  • 3 Team Members
  • +
  • 100 log entries per check
  • +
  •  
  • +
  •  
  • +
+ +
+
+ + + +
+
+
+

Standard

+

$4/mo

+
+ +
    +
  • Unlimited Checks
  • +
  • 10 Team Members
  • +
  • 1000 log entries per check
  • +
  • 50 SMS alerts per month
  • +
  • Email Support
  • +
+ + +
+
+
+

Plus

+

$40/mo

+
+ +
    +
  • Unlimited Checks
  • +
  • Unlimited Team Members
  • +
  • 1000 log entries per check
  • +
  • 500 SMS alerts per month
  • +
  • Priority Email Support
  • +
+ +
+
+
@@ -257,62 +419,35 @@ - - - {% endblock %} {% block scripts %} - + {% compress js %}