Browse Source

Annual subscriptions, updated Braintree Drop-in integration

pull/142/head
Pēteris Caune 7 years ago
parent
commit
cf5cbfaa3c
6 changed files with 304 additions and 86 deletions
  1. +12
    -0
      hc/payments/models.py
  2. +29
    -1
      hc/payments/tests/test_create_plan.py
  3. +13
    -4
      hc/payments/views.py
  4. +22
    -7
      static/css/pricing.css
  5. +38
    -19
      static/js/pricing.js
  6. +190
    -55
      templates/payments/pricing.html

+ 12
- 0
hc/payments/models.py View File

@ -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)


+ 29
- 1
hc/payments/tests/test_create_plan.py View File

@ -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)


+ 13
- 4
hc/payments/views.py View File

@ -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


+ 22
- 7
static/css/pricing.css View File

@ -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;
}

+ 38
- 19
static/js/pricing.js View File

@ -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");
});
});

+ 190
- 55
templates/payments/pricing.html View File

@ -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 %}
<!-- Plans -->
<section id="plans">
<section id="plans" {% if request.user.is_authenticated %} data- {% endif %}>
<div class="container">
{% if messages %}
<div class="alert alert-danger">
@ -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 @@
<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">
<button
class="btn btn-default btn-update-payment-method"
data-action="{% url 'hc-update-payment-method' %}">
Change Payment Method
</button>
{% endif %}
@ -55,22 +57,51 @@
</div>
</div>
</div>
{% else %}
<div class="row">
<div class="col-sm-12 text-center">
<h1>{% site_name %} Pricing Plans</h1>
</div>
</div>
{% endif %}
<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 %}">
<input
type="radio"
name="period"
value="monthly"
{% if period == "monthly" %} checked {% endif %}
autocomplete="off"> Monthly
</label>
<label class="btn btn-default {% if period == "annual" %} active {% endif %}">
<input
type="radio"
name="period"
value="annual"
{% if period == "annual" %} checked {% endif %}
autocomplete="off"> Annual
</label>
</div>
<p id="annual-note">(20% off on annual plan)</p>
</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 panel-pricing">
<div class="panel panel-default">
<div class="panel-body text-center free">
<p>free</p>
<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">100 log entries per check</li>
<li class="list-group-item"><i class="fa fa-check"></i> Personal or Commercial use</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>
@ -96,16 +127,16 @@
<!-- P5 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default panel-pricing">
<div class="panel panel-default">
<div class="panel-body text-center">
<p>$5<span class="mo">/mo</span></p>
<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">1000 log entries per check</li>
<li class="list-group-item">Personal or Commercial use</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>
@ -118,11 +149,13 @@
{% 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 sub.plan_id == "P50" %}
Switch to $5/mo
{% else %}
{% if not sub.subscription_id %}
Upgrade your Account
{% else %}
Switch to $5/mo
{% endif %}
</button>
{% endif %}
@ -138,16 +171,16 @@
<!-- P50 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default panel-pricing">
<div class="panel panel-default">
<div class="panel-body text-center">
<p>$50<span class="mo">/mo</span></p>
<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">1000 log entries per check</li>
<li class="list-group-item">Personal or Commercial use</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>
@ -160,8 +193,94 @@
{% 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>
</div>
</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>
</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">
Upgrade Your Account
{% if not sub.subscription_id %}
Upgrade your Account
{% else %}
Switch to $4/mo
{% endif %}
</button>
{% endif %}
{% else %}
@ -174,6 +293,49 @@
</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>
<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>
</div>
</div>
<!-- /item -->
</div>
</div>
@ -257,62 +419,35 @@
<div id="payment-method-modal" class="modal pm-modal">
<div class="modal-dialog">
<form method="post" action="{% url 'hc-create-plan' %}">
<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>Set Up Subscription
<small>for {{ request.user.profile }}</small>
</h4>
<h4>Subscription for {{ request.user.profile }}</h4>
</div>
<div class="modal-body">
<div id="payment-form"></div>
<div id="dropin"></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">
Set Up Subscription
<button id="payment-form-submit" type="button" class="btn btn-primary" disabled>
Submit
</button>
</div>
</div>
</form>
</div>
</div>
<div id="update-payment-method-modal" class="modal pm-modal">
<div class="modal-dialog">
<form method="post" action="{% url 'hc-update-payment-method' %}">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>Your Payment Method</h4>
</div>
<div class="modal-body">
<div id="update-payment-form"></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">
Confirm Payment Method
</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://js.braintreegateway.com/v2/braintree.js"></script>
<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