@ -0,0 +1,8 @@ | |||||
from django.contrib import admin | |||||
from .models import Subscription | |||||
@admin.register(Subscription) | |||||
class SubsAdmin(admin.ModelAdmin): | |||||
list_display = ("id", "user") |
@ -0,0 +1,5 @@ | |||||
from django.conf import settings | |||||
def payments(request): | |||||
return {'USE_PAYMENTS': settings.USE_PAYMENTS} |
@ -0,0 +1,25 @@ | |||||
# -*- coding: utf-8 -*- | |||||
from __future__ import unicode_literals | |||||
from django.db import migrations, models | |||||
from django.conf import settings | |||||
class Migration(migrations.Migration): | |||||
dependencies = [ | |||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | |||||
] | |||||
operations = [ | |||||
migrations.CreateModel( | |||||
name='Subscription', | |||||
fields=[ | |||||
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), | |||||
('customer_id', models.CharField(blank=True, max_length=36)), | |||||
('payment_method_token', models.CharField(blank=True, max_length=35)), | |||||
('subscription_id', models.CharField(blank=True, max_length=10)), | |||||
('user', models.OneToOneField(blank=True, null=True, to=settings.AUTH_USER_MODEL)), | |||||
], | |||||
), | |||||
] |
@ -0,0 +1,32 @@ | |||||
import braintree | |||||
from django.contrib.auth.models import User | |||||
from django.db import models | |||||
class Subscription(models.Model): | |||||
user = models.OneToOneField(User, blank=True, null=True) | |||||
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) | |||||
def _get_braintree_sub(self): | |||||
if not hasattr(self, "_sub"): | |||||
print("getting subscription over network") | |||||
self._sub = braintree.Subscription.find(self.subscription_id) | |||||
return self._sub | |||||
def is_active(self): | |||||
if not self.subscription_id: | |||||
return False | |||||
o = self._get_braintree_sub() | |||||
return o.status == "Active" | |||||
def price(self): | |||||
o = self._get_braintree_sub() | |||||
return int(o.price) | |||||
def next_billing_date(self): | |||||
o = self._get_braintree_sub() | |||||
return o.next_billing_date |
@ -0,0 +1,3 @@ | |||||
from django.test import TestCase | |||||
# Create your tests here. |
@ -0,0 +1,22 @@ | |||||
from django.conf.urls import url | |||||
from . import views | |||||
urlpatterns = [ | |||||
url(r'^pricing/$', | |||||
views.pricing, | |||||
name="hc-pricing"), | |||||
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"), | |||||
] |
@ -0,0 +1,101 @@ | |||||
import braintree | |||||
from django.conf import settings | |||||
from django.contrib.auth.decorators import login_required | |||||
from django.shortcuts import redirect, render | |||||
from django.views.decorators.http import require_POST | |||||
from .models import Subscription | |||||
def setup_braintree(): | |||||
kw = { | |||||
"merchant_id": settings.BRAINTREE_MERCHANT_ID, | |||||
"public_key": settings.BRAINTREE_PUBLIC_KEY, | |||||
"private_key": settings.BRAINTREE_PRIVATE_KEY | |||||
} | |||||
braintree.Configuration.configure(settings.BRAINTREE_ENV, **kw) | |||||
def pricing(request): | |||||
setup_braintree() | |||||
try: | |||||
sub = Subscription.objects.get(user=request.user) | |||||
except Subscription.DoesNotExist: | |||||
sub = Subscription(user=request.user) | |||||
sub.save() | |||||
ctx = { | |||||
"page": "pricing", | |||||
"sub": sub, | |||||
"client_token": braintree.ClientToken.generate() | |||||
} | |||||
return render(request, "payments/pricing.html", ctx) | |||||
@login_required | |||||
@require_POST | |||||
def create_plan(request): | |||||
setup_braintree() | |||||
sub = Subscription.objects.get(user=request.user) | |||||
if not sub.customer_id: | |||||
result = braintree.Customer.create({}) | |||||
assert result.is_success | |||||
sub.customer_id = result.customer.id | |||||
sub.save() | |||||
if "payment_method_nonce" in request.POST: | |||||
result = braintree.PaymentMethod.create({ | |||||
"customer_id": sub.customer_id, | |||||
"payment_method_nonce": request.POST["payment_method_nonce"] | |||||
}) | |||||
assert result.is_success | |||||
sub.payment_method_token = result.payment_method.token | |||||
sub.save() | |||||
price = int(request.POST["price"]) | |||||
assert price in (2, 5, 10, 15, 20, 25, 50, 100) | |||||
result = braintree.Subscription.create({ | |||||
"payment_method_token": sub.payment_method_token, | |||||
"plan_id": "P%d" % price, | |||||
"price": price | |||||
}) | |||||
sub.subscription_id = result.subscription.id | |||||
sub.save() | |||||
return redirect("hc-pricing") | |||||
@login_required | |||||
@require_POST | |||||
def update_plan(request): | |||||
setup_braintree() | |||||
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.update(sub.subscription_id, fields) | |||||
return redirect("hc-pricing") | |||||
@login_required | |||||
@require_POST | |||||
def cancel_plan(request): | |||||
setup_braintree() | |||||
sub = Subscription.objects.get(user=request.user) | |||||
braintree.Subscription.cancel(sub.subscription_id) | |||||
sub.subscription_id = "" | |||||
sub.save() | |||||
return redirect("hc-pricing") |
@ -20,6 +20,7 @@ SECRET_KEY = "---" | |||||
DEBUG = True | DEBUG = True | ||||
ALLOWED_HOSTS = [] | ALLOWED_HOSTS = [] | ||||
DEFAULT_FROM_EMAIL = '[email protected]' | DEFAULT_FROM_EMAIL = '[email protected]' | ||||
USE_PAYMENTS = False | |||||
INSTALLED_APPS = ( | INSTALLED_APPS = ( | ||||
@ -62,6 +63,7 @@ TEMPLATES = [ | |||||
'django.template.context_processors.request', | 'django.template.context_processors.request', | ||||
'django.contrib.auth.context_processors.auth', | 'django.contrib.auth.context_processors.auth', | ||||
'django.contrib.messages.context_processors.messages', | 'django.contrib.messages.context_processors.messages', | ||||
'hc.payments.context_processors.payments' | |||||
], | ], | ||||
}, | }, | ||||
}, | }, | ||||
@ -131,7 +133,7 @@ PUSHOVER_SUBSCRIPTION_URL = None | |||||
PUSHOVER_EMERGENCY_RETRY_DELAY = 300 | PUSHOVER_EMERGENCY_RETRY_DELAY = 300 | ||||
PUSHOVER_EMERGENCY_EXPIRATION = 86400 | PUSHOVER_EMERGENCY_EXPIRATION = 86400 | ||||
try: | |||||
if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")): | |||||
from .local_settings import * | from .local_settings import * | ||||
except ImportError as e: | |||||
else: | |||||
warnings.warn("local_settings.py not found, using defaults") | warnings.warn("local_settings.py not found, using defaults") |
@ -0,0 +1,57 @@ | |||||
$(function () { | |||||
var prices = [2, 5, 10, 15, 20, 25, 50, 100]; | |||||
var initialPrice = parseInt($("#pricing-value").text()); | |||||
var priceIdx = prices.indexOf(initialPrice); | |||||
function updateDisplayPrice(price) { | |||||
$("#pricing-value").text(price); | |||||
$(".selected-price").val(price); | |||||
$("#pww-switch-btn").text("Switch to $" + price + " / mo"); | |||||
if (price == initialPrice) { | |||||
$("#pww-selected-btn").show(); | |||||
$("#pww-switch-btn").hide(); | |||||
} else { | |||||
$("#pww-selected-btn").hide(); | |||||
$("#pww-switch-btn").show(); | |||||
} | |||||
} | |||||
$("#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(){ | |||||
var $modal = $("#payment-method-modal"); | |||||
var clientToken = $modal.attr("data-client-token"); | |||||
braintree.setup(clientToken, "dropin", { | |||||
container: "payment-form" | |||||
}); | |||||
$modal.modal("show"); | |||||
}); | |||||
}); |
@ -1,41 +0,0 @@ | |||||
{% extends "base.html" %} | |||||
{% load staticfiles %} | |||||
{% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} | |||||
{% block content %} | |||||
<!-- Plans --> | |||||
<section id="plans"> | |||||
<div class="container"> | |||||
<div class="row"> | |||||
<!-- item --> | |||||
<div class="col-md-4 text-center col-md-offset-4"> | |||||
<div class="panel panel-success panel-pricing"> | |||||
<div class="panel-heading"> | |||||
<i class="glyphicon glyphicon-heart-empty"></i> | |||||
<h3>Free Plan</h3> | |||||
</div> | |||||
<div class="panel-body text-center"> | |||||
<p><strong>€0 / Month</strong></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> | |||||
</ul> | |||||
<div class="panel-footer"> | |||||
<a class="btn btn-lg btn-block btn-success" href="{% url 'hc-login' %}">Get Started</a> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- /item --> | |||||
</div> | |||||
</div> | |||||
</section> | |||||
<!-- /Plans --> | |||||
{% endblock %} |
@ -0,0 +1,190 @@ | |||||
{% extends "base.html" %} | |||||
{% load staticfiles compress %} | |||||
{% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} | |||||
{% block content %} | |||||
<!-- Plans --> | |||||
<section id="plans"> | |||||
<div class="container"> | |||||
{% if sub.is_active %} | |||||
<div class="row"> | |||||
<div class="col-md-12"> | |||||
<div id="subscription-status" class="jumbotron"> | |||||
<p> | |||||
You are currently paying <strong>${{ sub.price }}/month</strong>. | |||||
Next billing date will be {{ sub.next_billing_date }}.</p> | |||||
<p> | |||||
Thank you for supporting healthchecks.io! | |||||
</p> | |||||
<a class="btn btn-default" href="#">Update Payment Method</a> | |||||
<form method="post" action="{% url 'hc-cancel-plan' %}"> | |||||
{% csrf_token %} | |||||
<button type="submit" class="btn btn-default"> | |||||
Cancel Subscription | |||||
</button> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
{% endif %} | |||||
<div class="row"> | |||||
<!-- item --> | |||||
<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> | |||||
<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"> </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 --> | |||||
<!-- 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> | |||||
<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> | |||||
<span class="btn-group" role="group"> | |||||
<button id="pay-minus" type="button" class="btn btn-default"> | |||||
<i class="glyphicon glyphicon-chevron-down"></i> | |||||
</button> | |||||
<button id="pay-plus" type="button" class="btn btn-default"> | |||||
<i class="glyphicon glyphicon-chevron-up"></i> | |||||
</button> | |||||
</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> | |||||
</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> | |||||
{% else %} | |||||
{% if sub.payment_method_token %} | |||||
<form method="post" action="{% url 'hc-create-plan' %}"> | |||||
{% csrf_token %} | |||||
<input class="selected-price" type="hidden" name="price" value="10" /> | |||||
<button | |||||
type="submit" | |||||
class="btn btn-lg btn-default"> | |||||
Select (direct) | |||||
</button> | |||||
</form> | |||||
{% else %} | |||||
<button | |||||
id="pww-create-payment-method" | |||||
class="btn btn-lg btn-default">Select (form)</button> | |||||
{% endif %} | |||||
{% endif %} | |||||
{% else %} | |||||
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">Get Started</a> | |||||
{% endif %} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- /item --> | |||||
</div> | |||||
</div> | |||||
</section> | |||||
<!-- /Plans --> | |||||
<div id="payment-method-modal" class="modal" data-client-token="{{ client_token }}"> | |||||
<div class="modal-dialog"> | |||||
<form method="post" action="{% url 'hc-create-plan' %}"> | |||||
{% csrf_token %} | |||||
<input class="selected-price" type="hidden" name="price" value="10" /> | |||||
<div class="modal-content"> | |||||
<div class="modal-header"> | |||||
<button type="button" class="close" data-dismiss="modal">×</span></button> | |||||
<h4>Set Up Subscription</h4> | |||||
</div> | |||||
<div class="modal-body" id="payment-method-body"> | |||||
<div id="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">Set Up Subscription</button> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
{% endblock %} | |||||
{% block scripts %} | |||||
<script src="https://js.braintreegateway.com/v2/braintree.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/pricing.js' %}"></script> | |||||
{% endcompress %} | |||||
{% endblock %} |