Browse Source

Updated pricing page, added billing history and invoices.

pull/25/head
Pēteris Caune 9 years ago
parent
commit
31c10d357e
14 changed files with 320 additions and 216 deletions
  1. +19
    -0
      hc/accounts/migrations/0002_profile_ping_log_limit.py
  2. +1
    -0
      hc/accounts/models.py
  3. +6
    -3
      hc/front/views.py
  4. +2
    -1
      hc/payments/admin.py
  5. +19
    -0
      hc/payments/migrations/0002_subscription_plan_id.py
  6. +7
    -2
      hc/payments/models.py
  7. +8
    -4
      hc/payments/urls.py
  8. +46
    -20
      hc/payments/views.py
  9. +7
    -81
      static/css/pricing.css
  10. +4
    -28
      static/js/pricing.js
  11. +25
    -0
      templates/base_bare.html
  12. +47
    -0
      templates/payments/billing.html
  13. +62
    -0
      templates/payments/invoice.html
  14. +67
    -77
      templates/payments/pricing.html

+ 19
- 0
hc/accounts/migrations/0002_profile_ping_log_limit.py View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='profile',
name='ping_log_limit',
field=models.IntegerField(default=100),
),
]

+ 1
- 0
hc/accounts/models.py View File

@ -25,6 +25,7 @@ class Profile(models.Model):
user = models.OneToOneField(User, blank=True, null=True)
next_report_date = models.DateTimeField(null=True, blank=True)
reports_allowed = models.BooleanField(default=True)
ping_log_limit = models.IntegerField(default=100)
objects = ProfileManager()


+ 6
- 3
hc/front/views.py View File

@ -2,13 +2,14 @@ from collections import Counter
from datetime import timedelta as td
from django.conf import settings
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.six.moves.urllib.parse import urlencode
from django.utils.crypto import get_random_string
from django.utils.six.moves.urllib.parse import urlencode
from hc.accounts.models import Profile
from hc.api.decorators import uuid_or_400
from hc.api.models import Channel, Check, Ping
from hc.front.forms import AddChannelForm, NameTagsForm, TimeoutForm
@ -182,7 +183,9 @@ def log(request, code):
if check.user != request.user:
return HttpResponseForbidden()
pings = Ping.objects.filter(owner=check).order_by("-created")[:100]
profile = Profile.objects.for_user(request.user)
limit = profile.ping_log_limit
pings = Ping.objects.filter(owner=check).order_by("-created")[:limit]
# Now go through pings, calculate time gaps, and decorate
# the pings list for convenient use in template


+ 2
- 1
hc/payments/admin.py View File

@ -5,7 +5,8 @@ from .models import Subscription
@admin.register(Subscription)
class SubsAdmin(admin.ModelAdmin):
list_display = ("id", "email", "customer_id", "payment_method_token", "subscription_id")
list_display = ("id", "email", "customer_id",
"payment_method_token", "subscription_id", "plan_id")
def email(self, obj):
return obj.user.email if obj.user else None

+ 19
- 0
hc/payments/migrations/0002_subscription_plan_id.py View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payments', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='subscription',
name='plan_id',
field=models.CharField(blank=True, max_length=10),
),
]

+ 7
- 2
hc/payments/models.py View File

@ -8,6 +8,7 @@ class Subscription(models.Model):
customer_id = models.CharField(max_length=36, blank=True)
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)
def _get_braintree_sub(self):
if not hasattr(self, "_sub"):
@ -29,8 +30,12 @@ class Subscription(models.Model):
return o.status == "Active"
def price(self):
o = self._get_braintree_sub()
return int(o.price)
if self.plan_id == "P5":
return 5
elif self.plan_id == "P20":
return 20
return 0
def next_billing_date(self):
o = self._get_braintree_sub()


+ 8
- 4
hc/payments/urls.py View File

@ -7,14 +7,18 @@ urlpatterns = [
views.pricing,
name="hc-pricing"),
url(r'^billing/$',
views.billing,
name="hc-billing"),
url(r'^invoice/([\w-]+)/$',
views.invoice,
name="hc-invoice"),
url(r'^pricing/create_plan/$',
views.create_plan,
name="hc-create-plan"),
url(r'^pricing/update_plan/$',
views.update_plan,
name="hc-update-plan"),
url(r'^pricing/cancel_plan/$',
views.cancel_plan,
name="hc-cancel-plan"),


+ 46
- 20
hc/payments/views.py View File

@ -1,10 +1,11 @@
import braintree
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import JsonResponse
from django.http import HttpResponseForbidden, JsonResponse
from django.shortcuts import redirect, render
from django.views.decorators.http import require_POST
from hc.accounts.models import Profile
from .models import Subscription
@ -53,10 +54,19 @@ def log_and_bail(request, result):
@login_required
@require_POST
def create_plan(request):
price = int(request.POST["price"])
assert price in (2, 5, 10, 15, 20, 25, 50, 100)
plan_id = request.POST["plan_id"]
assert plan_id in ("P5", "P20")
sub = Subscription.objects.get(user=request.user)
# 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
@ -67,6 +77,7 @@ def create_plan(request):
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,
@ -79,46 +90,61 @@ def create_plan(request):
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": "P%d" % price,
"price": price
"plan_id": 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 = Profile.objects.for_user(request.user)
if plan_id == "P5":
profile.ping_log_limit = 1000
profile.save()
elif plan_id == "P20":
profile.ping_log_limit = 10000
profile.save()
request.session["first_charge"] = True
return redirect("hc-pricing")
@login_required
@require_POST
def update_plan(request):
def cancel_plan(request):
sub = Subscription.objects.get(user=request.user)
price = int(request.POST["price"])
assert price in (2, 5, 10, 15, 20, 25, 50, 100)
fields = {
"plan_id": "P%s" % price,
"price": price
}
braintree.Subscription.cancel(sub.subscription_id)
sub.subscription_id = ""
sub.plan_id = ""
sub.save()
braintree.Subscription.update(sub.subscription_id, fields)
return redirect("hc-pricing")
@login_required
@require_POST
def cancel_plan(request):
def billing(request):
sub = Subscription.objects.get(user=request.user)
braintree.Subscription.cancel(sub.subscription_id)
sub.subscription_id = ""
sub.save()
transactions = braintree.Transaction.search(braintree.TransactionSearch.customer_id == sub.customer_id)
ctx = {"transactions": transactions}
return redirect("hc-pricing")
return render(request, "payments/billing.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()
ctx = {"tx": transaction}
return render(request, "payments/invoice.html", ctx)

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

@ -25,86 +25,6 @@
background-color: #f0f0f0;
}
.panel-slider {
padding: 20px 20px 40px 20px;
height: 78px;
background-color: #f0f0f0;
}
.animated {
animation-duration: 1s;
animation-fill-mode: both;
}
@keyframes tada {
from {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%, 20% {
-webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
}
30%, 50%, 70%, 90% {
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
}
40%, 60%, 80% {
-webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
}
to {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
.tada {
-webkit-animation-name: tada;
animation-name: tada;
}
@keyframes tadaIn {
from {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%, 20% {
-webkit-transform: scale3d(1.05, 1.05, 1.05) rotate3d(0, 0, 1, -3deg);
transform: scale3d(1.05, 1.05, 1.05) rotate3d(0, 0, 1, -3deg);
}
30%, 50%, 70%, 90% {
-webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, 3deg);
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, 3deg);
}
40%, 60%, 80% {
-webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
}
to {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
.tadaIn {
-webkit-animation-name: tadaIn;
animation-name: tadaIn;
}
#pww-switch-btn {
display: none;
}
#subscription-status form {
display: inline-block;
}
@ -117,7 +37,6 @@
border-top: 0;
}
.error-message {
font-family: monospace;
}
@ -126,3 +45,10 @@
margin: 40px 0;
}
.panel-pricing .free {
}
.mo {
font-size: 18px;
color: #888;
}

+ 4
- 28
static/js/pricing.js View File

@ -5,7 +5,6 @@ $(function () {
function updateDisplayPrice(price) {
$("#pricing-value").text(price);
$(".selected-price").val(price);
$("#pww-switch-btn").text("Switch to $" + price + " / mo");
if (price == initialPrice) {
@ -17,33 +16,10 @@ $(function () {
}
}
$("#pay-plus").click(function() {
if (priceIdx > 6)
return;
priceIdx += 1;
updateDisplayPrice(prices[priceIdx]);
$("#piggy").removeClass().addClass("tada animated").one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){
$(this).removeClass();
});;
});
$("#pay-minus").click(function() {
if (priceIdx <= 0)
return;
priceIdx -= 1;
updateDisplayPrice(prices[priceIdx]);
$("#piggy").removeClass().addClass("tadaIn animated").one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){
$(this).removeClass();
});;
});
$("#pww-create-payment-method").click(function() {
$(".btn-create-payment-method").click(function() {
var planId = $(this).data("plan-id");
console.log(planId);
$("#plan_id").val(planId);
$.getJSON("/pricing/get_client_token/", function(data) {
var $modal = $("#payment-method-modal");
braintree.setup(data.client_token, "dropin", {


+ 25
- 0
templates/base_bare.html View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}healthchecks.io - Monitor Cron Jobs. Get Notified When Your Cron Jobs Fail{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,600' rel='stylesheet' type='text/css'>
{% load compress staticfiles %}
<link rel="icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}">
{% compress css %}
<link rel="stylesheet" href="{% static 'css/bootstrap.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/base.css' %}" type="text/css">
{% endcompress %}
</head>
<body class="page-{{ page }}">
{% block containers %}
<div class="container">
{% block content %}{% endblock %}
</div>
{% endblock %}
</body>
</html>

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

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Billing History - healthchecks.io{% endblock %}
{% block content %}
<h1>Billing History</h1>
<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>
{{ tx.credit_card.card_type }} ending in {{ tx.credit_card.last_4 }}
</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' tx.id %}">View Invoice</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5">
No past transactions to display here
</td>
</tr>
{% endfor%}
</table>
{% endblock %}

+ 62
- 0
templates/payments/invoice.html View File

@ -0,0 +1,62 @@
{% extends "base_bare.html" %}
{% block title %}Invoice MS-HC-{{ tx.id|upper }} - healthchecks.io{% endblock %}
{% block content %}
<h1>SIA Monkey See Monkey Do</h1>
<p>
Gaujas iela 4-2<br />
Valmiera, LV-4201, Latvia<br />
VAT: LV44103100701
</p>
<p class="text-right">Date Issued: {{ tx.created_at|date }}</p>
<p class="text-right">Invoice Id: MS-HC-{{ tx.id|upper }}</p>
<table class="table">
<tr>
<th>Description</th>
<th>Start</th>
<th>End</th>
<th class="text-right">{{ tx.currency_iso_code }}</th>
</tr>
<tr>
<td>healthchecks.io paid plan</td>
<td>{{ tx.subscription_details.billing_period_start_date }}</td>
<td>{{ tx.subscription_details.billing_period_end_date }}</td>
<td class="text-right">
{% 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>
</tr>
<tr>
<td colspan="4" class="text-right">
<strong>
Total:
{% if tx.currency_iso_code == "USD" %}
${{ tx.amount }}
{% elif tx.currency_iso_code == "EUR" %}
€{{ tx.amount }}
{% else %}
{{ tx.currency_iso_code }} {{ tx.amount }}
{% endif %}
</strong>
</td>
</tr>
</table>
<p><strong>Bill to:</strong></p>
<p>{{ request.user.email }}</p>
<p class="text-center">
If you have a credit card on file it will be automatically charged within 24 hours.
</p>
{% endblock %}

+ 67
- 77
templates/payments/pricing.html View File

@ -20,36 +20,21 @@
</div>
{% endif %}
{% if sub.is_active %}
{% if sub.plan_id %}
<div class="row">
<div class="col-md-12">
<div id="subscription-status" class="jumbotron">
<p>
{% if first_charge %}
You just paid <strong>${{ sub.price }}</strong>
Success! You just paid ${{ sub.price }}.
{% else %}
You are currently paying <strong>${{ sub.price }}/month</strong>
{% endif %}
{% 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 }}.
You are currently paying ${{ sub.price }}/month.
{% endif %}
<a href="{% url 'hc-billing' %}">See Billing History</a>.
</p>
<p>
Next billing date will be {{ sub.next_billing_date }}.
Thank you for supporting healthchecks.io!
</p>
<form method="post" action="{% url 'hc-cancel-plan' %}">
{% csrf_token %}
<button type="submit" class="btn btn-default">
Cancel Subscription
</button>
</form>
</div>
</div>
</div>
@ -58,21 +43,17 @@
<div class="row">
<!-- item -->
<!-- Free -->
<div class="col-sm-4 text-center">
<div class="panel panel-success panel-pricing">
<div class="panel-heading">
<i class="glyphicon glyphicon-heart"></i>
<h3>Free Plan</h3>
</div>
<div class="panel-body text-center">
<p><strong>$0 / Month</strong></p>
<div class="panel panel-default panel-pricing">
<div class="panel-body text-center free">
<p>free</p>
</div>
<ul class="list-group text-center">
<li class="list-group-item"><i class="fa fa-check"></i> Personal or Commercial use</li>
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Checks</li>
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Alerts</li>
<li class="list-group-item">&nbsp;</li>
<li class="list-group-item">100 log entries / check</li>
</ul>
<div class="panel-footer">
{% if request.user.is_authenticated %}
@ -94,73 +75,82 @@
</div>
<!-- /item -->
<!-- item -->
<div class="col-sm-8 text-center">
<div class="panel panel-success panel-pricing">
<div class="panel-heading">
<div id="piggy">
<i class="glyphicon glyphicon-piggy-bank"></i>
</div>
<h3>Pay What You Want Plan</h3>
</div>
<!-- P5 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default panel-pricing">
<div class="panel-body text-center">
<p>
<strong>
{% if sub.is_active %}
$<span id="pricing-value">{{ sub.price }}</span> / Month
{% else %}
$<span id="pricing-value">10</span> / Month
{% endif %}
</strong>
<p>$5<span class="mo">/mo</span></p>
</div>
<span class="btn-group" role="group">
<button id="pay-minus" type="button" class="btn btn-default">
<i class="glyphicon glyphicon-chevron-down"></i>
<ul class="list-group text-center">
<li class="list-group-item">Personal or Commercial use</li>
<li class="list-group-item">Unlimited Checks</li>
<li class="list-group-item">Unlimited Alerts</li>
<li class="list-group-item">1000 log entries / check</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>
<button id="pay-plus" type="button" class="btn btn-default">
<i class="glyphicon glyphicon-chevron-up"></i>
{% else %}
<button
data-plan-id="P5"
class="btn btn-lg btn-default btn-create-payment-method">
{% if sub.plan_id == "P20" %}
Switch to $5/mo
{% else %}
Upgrade your Account
{% endif %}
</button>
</span>
{% endif %}
{% else %}
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">
Get Started
</a>
{% endif %}
</div>
</div>
</div>
<!-- /item -->
</p>
<!-- P20 -->
<div class="col-sm-4 text-center">
<div class="panel panel-default panel-pricing">
<div class="panel-body text-center">
<p>$20<span class="mo">/mo</span></p>
</div>
<ul class="list-group text-center">
<li class="list-group-item"><i class="fa fa-check"></i> Personal or Commercial use</li>
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Checks</li>
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Alerts</li>
<li class="list-group-item"><i class="fa fa-check"></i> Priority Support</li>
<li class="list-group-item">Personal or Commercial use</li>
<li class="list-group-item">Unlimited Checks</li>
<li class="list-group-item">Unlimited Alerts</li>
<li class="list-group-item">10'000 log entries / check</li>
</ul>
<div class="panel-footer">
{% if request.user.is_authenticated %}
{% if sub.is_active %}
<button id="pww-selected-btn"
class="btn btn-lg btn-success disabled">Selected</button>
<form method="post" action="{% url 'hc-update-plan' %}">
{% csrf_token %}
<input class="selected-price" type="hidden" name="price" />
<button
id="pww-switch-btn"
type="submit"
class="btn btn-lg btn-default">
Switch To
</button>
</form>
{% if sub.plan_id == "P20" %}
<button class="btn btn-lg btn-success disabled">
Selected
</button>
{% else %}
<button
id="pww-create-payment-method"
class="btn btn-lg btn-default">Select</button>
<button
data-plan-id="P20"
class="btn btn-lg btn-default btn-create-payment-method">
Upgrade Your Account
</button>
{% endif %}
{% else %}
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">Get Started</a>
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">
Get Started
</a>
{% endif %}
</div>
</div>
</div>
<!-- /item -->
</div>
</div>
@ -171,7 +161,7 @@
<div class="modal-dialog">
<form method="post" action="{% url 'hc-create-plan' %}">
{% csrf_token %}
<input class="selected-price" type="hidden" name="price" value="10" />
<input id="plan_id" type="hidden" name="plan_id" value="" />
<div class="modal-content">
<div class="modal-header">


Loading…
Cancel
Save