From 31c10d357eeb6a651997a866bf24762413fa07b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Mon, 28 Dec 2015 18:55:46 +0200 Subject: [PATCH] Updated pricing page, added billing history and invoices. --- .../migrations/0002_profile_ping_log_limit.py | 19 +++ hc/accounts/models.py | 1 + hc/front/views.py | 9 +- hc/payments/admin.py | 3 +- .../migrations/0002_subscription_plan_id.py | 19 +++ hc/payments/models.py | 9 +- hc/payments/urls.py | 12 +- hc/payments/views.py | 66 +++++--- static/css/pricing.css | 88 +---------- static/js/pricing.js | 32 +--- templates/base_bare.html | 25 +++ templates/payments/billing.html | 47 ++++++ templates/payments/invoice.html | 62 ++++++++ templates/payments/pricing.html | 144 ++++++++---------- 14 files changed, 320 insertions(+), 216 deletions(-) create mode 100644 hc/accounts/migrations/0002_profile_ping_log_limit.py create mode 100644 hc/payments/migrations/0002_subscription_plan_id.py create mode 100644 templates/base_bare.html create mode 100644 templates/payments/billing.html create mode 100644 templates/payments/invoice.html diff --git a/hc/accounts/migrations/0002_profile_ping_log_limit.py b/hc/accounts/migrations/0002_profile_ping_log_limit.py new file mode 100644 index 00000000..5c2fd48f --- /dev/null +++ b/hc/accounts/migrations/0002_profile_ping_log_limit.py @@ -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), + ), + ] diff --git a/hc/accounts/models.py b/hc/accounts/models.py index 086febd0..65cdbcef 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -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() diff --git a/hc/front/views.py b/hc/front/views.py index ccf72289..17d8e48c 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -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 diff --git a/hc/payments/admin.py b/hc/payments/admin.py index 82dfb0a0..4b95c3c2 100644 --- a/hc/payments/admin.py +++ b/hc/payments/admin.py @@ -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 diff --git a/hc/payments/migrations/0002_subscription_plan_id.py b/hc/payments/migrations/0002_subscription_plan_id.py new file mode 100644 index 00000000..03281f34 --- /dev/null +++ b/hc/payments/migrations/0002_subscription_plan_id.py @@ -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), + ), + ] diff --git a/hc/payments/models.py b/hc/payments/models.py index 5d392340..fd775afa 100644 --- a/hc/payments/models.py +++ b/hc/payments/models.py @@ -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() diff --git a/hc/payments/urls.py b/hc/payments/urls.py index 07863d14..882baa26 100644 --- a/hc/payments/urls.py +++ b/hc/payments/urls.py @@ -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"), diff --git a/hc/payments/views.py b/hc/payments/views.py index a5ad7761..2554551c 100644 --- a/hc/payments/views.py +++ b/hc/payments/views.py @@ -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) diff --git a/static/css/pricing.css b/static/css/pricing.css index 197f33e0..baaff32a 100644 --- a/static/css/pricing.css +++ b/static/css/pricing.css @@ -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; +} \ No newline at end of file diff --git a/static/js/pricing.js b/static/js/pricing.js index 328a2ab5..28fc8d8b 100644 --- a/static/js/pricing.js +++ b/static/js/pricing.js @@ -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", { diff --git a/templates/base_bare.html b/templates/base_bare.html new file mode 100644 index 00000000..9b55be37 --- /dev/null +++ b/templates/base_bare.html @@ -0,0 +1,25 @@ + + + + + {% block title %}healthchecks.io - Monitor Cron Jobs. Get Notified When Your Cron Jobs Fail{% endblock %} + + + + {% load compress staticfiles %} + + + {% compress css %} + + + + {% endcompress %} + + + {% block containers %} +
+ {% block content %}{% endblock %} +
+ {% endblock %} + + \ No newline at end of file diff --git a/templates/payments/billing.html b/templates/payments/billing.html new file mode 100644 index 00000000..777a74ce --- /dev/null +++ b/templates/payments/billing.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} + +{% block title %}Billing History - healthchecks.io{% endblock %} + + +{% block content %} + +

Billing History

+ + + + + + + + + + {% for tx in transactions %} + + + + + + + + {% empty %} + + + + {% endfor%} +
DatePayment MethodAmountStatus
{{ tx.created_at }} + {{ tx.credit_card.card_type }} ending in {{ tx.credit_card.last_4 }} + + {% if tx.currency_iso_code == "USD" %} + ${{ tx.amount }} + {% elif tx.currency_iso_code == "EUR" %} + €{{ tx.amount }} + {% else %} + {{ tx.currency_iso_code }} {{ tx.amount }} + {% endif %} + {{ tx.status }} + View Invoice +
+ No past transactions to display here +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/payments/invoice.html b/templates/payments/invoice.html new file mode 100644 index 00000000..345cdeeb --- /dev/null +++ b/templates/payments/invoice.html @@ -0,0 +1,62 @@ +{% extends "base_bare.html" %} + +{% block title %}Invoice MS-HC-{{ tx.id|upper }} - healthchecks.io{% endblock %} + + +{% block content %} + +

SIA Monkey See Monkey Do

+

+Gaujas iela 4-2
+Valmiera, LV-4201, Latvia
+VAT: LV44103100701 +

+ +

Date Issued: {{ tx.created_at|date }}

+

Invoice Id: MS-HC-{{ tx.id|upper }}

+ + + + + + + + + + + + + + + + + +
DescriptionStartEnd{{ tx.currency_iso_code }}
healthchecks.io paid plan{{ tx.subscription_details.billing_period_start_date }}{{ tx.subscription_details.billing_period_end_date }} + {% if tx.currency_iso_code == "USD" %} + ${{ tx.amount }} + {% elif tx.currency_iso_code == "EUR" %} + €{{ tx.amount }} + {% else %} + {{ tx.currency_iso_code }} {{ tx.amount }} + {% endif %} +
+ + 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 %} + +
+ +

Bill to:

+

{{ request.user.email }}

+ +

+ If you have a credit card on file it will be automatically charged within 24 hours. +

+ +{% endblock %} \ No newline at end of file diff --git a/templates/payments/pricing.html b/templates/payments/pricing.html index 4d3781de..38c4b609 100644 --- a/templates/payments/pricing.html +++ b/templates/payments/pricing.html @@ -20,36 +20,21 @@ {% endif %} - {% if sub.is_active %} + {% if sub.plan_id %}

{% if first_charge %} - You just paid ${{ sub.price }} + Success! You just paid ${{ sub.price }}. {% else %} - You are currently paying ${{ sub.price }}/month - {% 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 %} + See Billing History.

- Next billing date will be {{ sub.next_billing_date }}. Thank you for supporting healthchecks.io!

-
- {% csrf_token %} - -
@@ -58,21 +43,17 @@
- +
-
-
- -

Free Plan

-
-
-

$0 / Month

+
+
+

free

  • Personal or Commercial use
  • Unlimited Checks
  • Unlimited Alerts
  • -
  •  
  • +
  • 100 log entries / check
- -
-
-
-
- -
-

Pay What You Want Plan

-
+ +
+
-

- - {% if sub.is_active %} - ${{ sub.price }} / Month - {% else %} - $10 / Month - {% endif %} - +

$5/mo

+
- - - - - + {% endif %} + {% else %} + + Get Started + + {% endif %} +
+
+
+ -

+ +
+
+
+

$20/mo

    -
  • Personal or Commercial use
  • -
  • Unlimited Checks
  • -
  • Unlimited Alerts
  • -
  • Priority Support
  • +
  • Personal or Commercial use
  • +
  • Unlimited Checks
  • +
  • Unlimited Alerts
  • +
  • 10'000 log entries / check
+
@@ -171,7 +161,7 @@