forked from GithubBackups/healthchecks
Prepare for 3DS 2
This commit is contained in:
parent
33dece4ad2
commit
fa16bd4e42
@ -66,11 +66,9 @@ class Subscription(models.Model):
|
||||
|
||||
@property
|
||||
def payment_method(self):
|
||||
if not self.payment_method_token:
|
||||
return None
|
||||
|
||||
if not hasattr(self, "_pm"):
|
||||
self._pm = braintree.PaymentMethod.find(self.payment_method_token)
|
||||
o = self._get_braintree_subscription()
|
||||
self._pm = braintree.PaymentMethod.find(o.payment_method_token)
|
||||
return self._pm
|
||||
|
||||
def _get_braintree_subscription(self):
|
||||
@ -79,43 +77,19 @@ class Subscription(models.Model):
|
||||
return self._sub
|
||||
|
||||
def get_client_token(self):
|
||||
assert self.customer_id
|
||||
return braintree.ClientToken.generate({"customer_id": self.customer_id})
|
||||
|
||||
def update_payment_method(self, nonce):
|
||||
# Create customer record if it does not exist:
|
||||
if not self.customer_id:
|
||||
result = braintree.Customer.create({"email": self.user.email})
|
||||
if not result.is_success:
|
||||
return result
|
||||
assert self.subscription_id
|
||||
|
||||
self.customer_id = result.customer.id
|
||||
self.save()
|
||||
|
||||
# Create payment method
|
||||
result = braintree.PaymentMethod.create(
|
||||
{
|
||||
"customer_id": self.customer_id,
|
||||
"payment_method_nonce": nonce,
|
||||
"options": {"make_default": True},
|
||||
}
|
||||
result = braintree.Subscription.update(
|
||||
self.subscription_id, {"payment_method_nonce": nonce}
|
||||
)
|
||||
|
||||
if not result.is_success:
|
||||
return result
|
||||
|
||||
self.payment_method_token = result.payment_method.token
|
||||
self.save()
|
||||
|
||||
# Update an existing subscription to use this payment method
|
||||
if self.subscription_id:
|
||||
result = braintree.Subscription.update(
|
||||
self.subscription_id,
|
||||
{"payment_method_token": self.payment_method_token},
|
||||
)
|
||||
|
||||
if not result.is_success:
|
||||
return result
|
||||
|
||||
def update_address(self, post_data):
|
||||
# Create customer record if it does not exist:
|
||||
if not self.customer_id:
|
||||
@ -141,9 +115,9 @@ class Subscription(models.Model):
|
||||
if not result.is_success:
|
||||
return result
|
||||
|
||||
def setup(self, plan_id):
|
||||
def setup(self, plan_id, nonce):
|
||||
result = braintree.Subscription.create(
|
||||
{"payment_method_token": self.payment_method_token, "plan_id": plan_id}
|
||||
{"payment_method_nonce": nonce, "plan_id": plan_id}
|
||||
)
|
||||
|
||||
if result.is_success:
|
||||
|
@ -7,10 +7,14 @@ from hc.test import BaseTestCase
|
||||
class GetClientTokenTestCase(BaseTestCase):
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_it_works(self, mock_braintree):
|
||||
sub = Subscription(user=self.alice)
|
||||
sub.customer_id = "fake-customer-id"
|
||||
sub.save()
|
||||
|
||||
mock_braintree.ClientToken.generate.return_value = "test-token"
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
r = self.client.get("/pricing/get_client_token/")
|
||||
r = self.client.get("/pricing/token/")
|
||||
self.assertContains(r, "test-token", status_code=200)
|
||||
|
||||
# A subscription object should have been created
|
||||
|
@ -5,23 +5,13 @@ from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class UpdatePaymentMethodTestCase(BaseTestCase):
|
||||
def _setup_mock(self, mock):
|
||||
""" Set up Braintree calls that the controller will use. """
|
||||
|
||||
mock.PaymentMethod.create.return_value.is_success = True
|
||||
mock.PaymentMethod.create.return_value.payment_method.token = "fake"
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_it_retrieves_paypal(self, mock):
|
||||
self._setup_mock(mock)
|
||||
|
||||
mock.paypal_account.PayPalAccount = dict
|
||||
mock.credit_card.CreditCard = list
|
||||
mock.PaymentMethod.find.return_value = {"email": "foo@example.org"}
|
||||
|
||||
self.sub = Subscription(user=self.alice)
|
||||
self.sub.payment_method_token = "fake-token"
|
||||
self.sub.save()
|
||||
Subscription.objects.create(user=self.alice)
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get("/accounts/profile/billing/payment_method/")
|
||||
@ -29,65 +19,12 @@ class UpdatePaymentMethodTestCase(BaseTestCase):
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_it_retrieves_cc(self, mock):
|
||||
self._setup_mock(mock)
|
||||
|
||||
mock.paypal_account.PayPalAccount = list
|
||||
mock.credit_card.CreditCard = dict
|
||||
mock.PaymentMethod.find.return_value = {"masked_number": "1***2"}
|
||||
|
||||
self.sub = Subscription(user=self.alice)
|
||||
self.sub.payment_method_token = "fake-token"
|
||||
self.sub.save()
|
||||
Subscription.objects.create(user=self.alice)
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get("/accounts/profile/billing/payment_method/")
|
||||
self.assertContains(r, "1***2")
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_it_creates_payment_method(self, mock):
|
||||
self._setup_mock(mock)
|
||||
|
||||
self.sub = Subscription(user=self.alice)
|
||||
self.sub.customer_id = "test-customer"
|
||||
self.sub.save()
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
form = {"payment_method_nonce": "test-nonce"}
|
||||
r = self.client.post("/accounts/profile/billing/payment_method/", form)
|
||||
|
||||
self.assertRedirects(r, "/accounts/profile/billing/")
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_it_creates_customer(self, mock):
|
||||
self._setup_mock(mock)
|
||||
|
||||
mock.Customer.create.return_value.is_success = True
|
||||
mock.Customer.create.return_value.customer.id = "test-customer-id"
|
||||
|
||||
self.sub = Subscription(user=self.alice)
|
||||
self.sub.save()
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
form = {"payment_method_nonce": "test-nonce"}
|
||||
self.client.post("/accounts/profile/billing/payment_method/", form)
|
||||
|
||||
self.sub.refresh_from_db()
|
||||
self.assertEqual(self.sub.customer_id, "test-customer-id")
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_it_updates_subscription(self, mock):
|
||||
self._setup_mock(mock)
|
||||
|
||||
self.sub = Subscription(user=self.alice)
|
||||
self.sub.customer_id = "test-customer"
|
||||
self.sub.subscription_id = "fake-id"
|
||||
self.sub.save()
|
||||
|
||||
mock.Customer.create.return_value.is_success = True
|
||||
mock.Customer.create.return_value.customer.id = "test-customer-id"
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
form = {"payment_method_nonce": "test-nonce"}
|
||||
self.client.post("/accounts/profile/billing/payment_method/", form)
|
||||
|
||||
self.assertTrue(mock.Subscription.update.called)
|
||||
|
@ -4,17 +4,17 @@ from hc.payments.models import Subscription
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class SetPlanTestCase(BaseTestCase):
|
||||
class UpdateSubscriptionTestCase(BaseTestCase):
|
||||
def _setup_mock(self, mock):
|
||||
""" Set up Braintree calls that the controller will use. """
|
||||
|
||||
mock.Subscription.create.return_value.is_success = True
|
||||
mock.Subscription.create.return_value.subscription.id = "t-sub-id"
|
||||
|
||||
def run_set_plan(self, plan_id="P20"):
|
||||
form = {"plan_id": plan_id}
|
||||
def run_update(self, plan_id="P20", nonce="fake-nonce"):
|
||||
form = {"plan_id": plan_id, "nonce": nonce}
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
return self.client.post("/pricing/set_plan/", form, follow=True)
|
||||
return self.client.post("/pricing/update/", form, follow=True)
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_it_works(self, mock):
|
||||
@ -24,8 +24,9 @@ class SetPlanTestCase(BaseTestCase):
|
||||
self.profile.sms_sent = 1
|
||||
self.profile.save()
|
||||
|
||||
r = self.run_set_plan()
|
||||
r = self.run_update()
|
||||
self.assertRedirects(r, "/accounts/profile/billing/")
|
||||
self.assertContains(r, "Your billing plan has been updated!")
|
||||
|
||||
# Subscription should be filled out:
|
||||
sub = Subscription.objects.get(user=self.alice)
|
||||
@ -42,7 +43,10 @@ class SetPlanTestCase(BaseTestCase):
|
||||
self.assertEqual(self.profile.sms_sent, 0)
|
||||
|
||||
# braintree.Subscription.cancel should have not been called
|
||||
assert not mock.Subscription.cancel.called
|
||||
# because there was no previous subscription
|
||||
self.assertFalse(mock.Subscription.cancel.called)
|
||||
|
||||
self.assertTrue(mock.Subscription.create.called)
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_yearly_works(self, mock):
|
||||
@ -52,7 +56,7 @@ class SetPlanTestCase(BaseTestCase):
|
||||
self.profile.sms_sent = 1
|
||||
self.profile.save()
|
||||
|
||||
r = self.run_set_plan("Y192")
|
||||
r = self.run_update("Y192")
|
||||
self.assertRedirects(r, "/accounts/profile/billing/")
|
||||
|
||||
# Subscription should be filled out:
|
||||
@ -80,7 +84,7 @@ class SetPlanTestCase(BaseTestCase):
|
||||
self.profile.sms_sent = 1
|
||||
self.profile.save()
|
||||
|
||||
r = self.run_set_plan("P80")
|
||||
r = self.run_update("P80")
|
||||
self.assertRedirects(r, "/accounts/profile/billing/")
|
||||
|
||||
# Subscription should be filled out:
|
||||
@ -114,8 +118,9 @@ class SetPlanTestCase(BaseTestCase):
|
||||
self.profile.sms_sent = 1
|
||||
self.profile.save()
|
||||
|
||||
r = self.run_set_plan("")
|
||||
r = self.run_update("")
|
||||
self.assertRedirects(r, "/accounts/profile/billing/")
|
||||
self.assertContains(r, "Your billing plan has been updated!")
|
||||
|
||||
# Subscription should be cleared
|
||||
sub = Subscription.objects.get(user=self.alice)
|
||||
@ -130,10 +135,10 @@ class SetPlanTestCase(BaseTestCase):
|
||||
self.assertEqual(self.profile.team_limit, 2)
|
||||
self.assertEqual(self.profile.sms_limit, 0)
|
||||
|
||||
assert mock.Subscription.cancel.called
|
||||
self.assertTrue(mock.Subscription.cancel.called)
|
||||
|
||||
def test_bad_plan_id(self):
|
||||
r = self.run_set_plan(plan_id="this-is-wrong")
|
||||
r = self.run_update(plan_id="this-is-wrong")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
@ -144,16 +149,16 @@ class SetPlanTestCase(BaseTestCase):
|
||||
sub.subscription_id = "prev-sub"
|
||||
sub.save()
|
||||
|
||||
r = self.run_set_plan()
|
||||
r = self.run_update()
|
||||
self.assertRedirects(r, "/accounts/profile/billing/")
|
||||
assert mock.Subscription.cancel.called
|
||||
self.assertTrue(mock.Subscription.cancel.called)
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_subscription_creation_failure(self, mock):
|
||||
mock.Subscription.create.return_value.is_success = False
|
||||
mock.Subscription.create.return_value.message = "sub failure"
|
||||
|
||||
r = self.run_set_plan()
|
||||
r = self.run_update()
|
||||
self.assertRedirects(r, "/accounts/profile/billing/")
|
||||
self.assertContains(r, "sub failure")
|
||||
|
||||
@ -171,7 +176,7 @@ class SetPlanTestCase(BaseTestCase):
|
||||
mock.Subscription.create.return_value.is_success = False
|
||||
mock.Subscription.create.return_value.message = "sub failure"
|
||||
|
||||
r = self.run_set_plan()
|
||||
r = self.run_update()
|
||||
|
||||
# It should cancel the current plan
|
||||
self.assertTrue(mock.Subscription.cancel.called)
|
||||
@ -183,3 +188,18 @@ class SetPlanTestCase(BaseTestCase):
|
||||
# And it should show the error message from API:
|
||||
self.assertRedirects(r, "/accounts/profile/billing/")
|
||||
self.assertContains(r, "sub failure")
|
||||
|
||||
@patch("hc.payments.models.braintree")
|
||||
def test_it_updates_payment_method(self, mock):
|
||||
# Initial state: the user has a subscription and a high check limit:
|
||||
sub = Subscription.objects.for_user(self.alice)
|
||||
sub.plan_id = "P20"
|
||||
sub.subscription_id = "old-sub-id"
|
||||
sub.save()
|
||||
|
||||
r = self.run_update()
|
||||
|
||||
# It should update the existing subscription
|
||||
self.assertTrue(mock.Subscription.update.called)
|
||||
self.assertRedirects(r, "/accounts/profile/billing/")
|
||||
self.assertContains(r, "Your payment method has been updated!")
|
@ -19,9 +19,7 @@ urlpatterns = [
|
||||
path(
|
||||
"invoice/pdf/<slug:transaction_id>/", views.pdf_invoice, name="hc-invoice-pdf"
|
||||
),
|
||||
path("pricing/set_plan/", views.set_plan, name="hc-set-plan"),
|
||||
path(
|
||||
"pricing/get_client_token/", views.get_client_token, name="hc-get-client-token"
|
||||
),
|
||||
path("pricing/update/", views.update, name="hc-update-subscription"),
|
||||
path("pricing/token/", views.token, name="hc-get-client-token"),
|
||||
path("pricing/charge/", views.charge_webhook),
|
||||
]
|
||||
|
@ -20,7 +20,7 @@ from hc.payments.models import Subscription
|
||||
|
||||
|
||||
@login_required
|
||||
def get_client_token(request):
|
||||
def token(request):
|
||||
sub = Subscription.objects.for_user(request.user)
|
||||
return JsonResponse({"client_token": sub.get_client_token()})
|
||||
|
||||
@ -96,13 +96,21 @@ def log_and_bail(request, result):
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def set_plan(request):
|
||||
def update(request):
|
||||
plan_id = request.POST["plan_id"]
|
||||
nonce = request.POST["nonce"]
|
||||
|
||||
if plan_id not in ("", "P20", "P80", "Y192", "Y768"):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
sub = Subscription.objects.for_user(request.user)
|
||||
if sub.plan_id == plan_id:
|
||||
# If plan_id has not changed then just update the payment method:
|
||||
if plan_id == sub.plan_id:
|
||||
error = sub.update_payment_method(nonce)
|
||||
if error:
|
||||
return log_and_bail(request, error)
|
||||
|
||||
request.session["payment_method_status"] = "success"
|
||||
return redirect("hc-billing")
|
||||
|
||||
# Cancel the previous plan and reset limits:
|
||||
@ -116,9 +124,10 @@ def set_plan(request):
|
||||
profile.save()
|
||||
|
||||
if plan_id == "":
|
||||
request.session["set_plan_status"] = "success"
|
||||
return redirect("hc-billing")
|
||||
|
||||
result = sub.setup(plan_id)
|
||||
result = sub.setup(plan_id, nonce)
|
||||
if not result.is_success:
|
||||
return log_and_bail(request, result)
|
||||
|
||||
@ -161,19 +170,6 @@ def address(request):
|
||||
@login_required
|
||||
def payment_method(request):
|
||||
sub = get_object_or_404(Subscription, user=request.user)
|
||||
|
||||
if request.method == "POST":
|
||||
if "payment_method_nonce" not in request.POST:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
nonce = request.POST["payment_method_nonce"]
|
||||
error = sub.update_payment_method(nonce)
|
||||
if error:
|
||||
return log_and_bail(request, error)
|
||||
|
||||
request.session["payment_method_status"] = "success"
|
||||
return redirect("hc-billing")
|
||||
|
||||
ctx = {"sub": sub, "pm": sub.payment_method}
|
||||
return render(request, "payments/payment_method.html", ctx)
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
#change-billing-plan-modal .modal-dialog {
|
||||
#change-billing-plan-modal .modal-dialog, #payment-method-modal .modal-dialog, #please-wait-modal .modal-dialog {
|
||||
width: 850px;
|
||||
}
|
||||
}
|
||||
@ -86,3 +86,8 @@
|
||||
color: #777;
|
||||
}
|
||||
|
||||
#please-wait-modal .modal-body {
|
||||
text-align: center;
|
||||
padding: 100px;
|
||||
font-size: 18px;
|
||||
}
|
@ -1,45 +1,92 @@
|
||||
$(function () {
|
||||
var clientTokenRequested = false;
|
||||
function requestClientToken() {
|
||||
if (!clientTokenRequested) {
|
||||
clientTokenRequested = true;
|
||||
$.getJSON("/pricing/get_client_token/", setupDropin);
|
||||
var preloadedToken = null;
|
||||
function getToken(callback) {
|
||||
if (preloadedToken) {
|
||||
callback(preloadedToken);
|
||||
} else {
|
||||
$.getJSON("/pricing/token/", function(response) {
|
||||
preloadedToken = response.client_token;
|
||||
callback(response.client_token);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
// Preload client token:
|
||||
if ($("#billing-address").length) {
|
||||
getToken(function(token){});
|
||||
}
|
||||
|
||||
function getAmount(planId) {
|
||||
return planId.substr(1);
|
||||
}
|
||||
|
||||
function showPaymentMethodForm(planId) {
|
||||
$("#plan-id").val(planId);
|
||||
$("#nonce").val("");
|
||||
|
||||
if (planId == "") {
|
||||
// Don't need a payment method when switching to the free plan
|
||||
// -- can submit the form right away:
|
||||
$("#update-subscription-form").submit();
|
||||
return;
|
||||
}
|
||||
|
||||
$("#payment-form-submit").prop("disabled", true);
|
||||
$("#payment-method-modal").modal("show");
|
||||
|
||||
getToken(function(token) {
|
||||
braintree.dropin.create({
|
||||
authorization: token,
|
||||
container: "#dropin",
|
||||
threeDSecure: {
|
||||
amount: getAmount(planId),
|
||||
},
|
||||
paypal: { flow: 'vault' },
|
||||
preselectVaultedPaymentMethod: false
|
||||
}, function(createErr, instance) {
|
||||
$("#payment-form-submit").off().click(function() {
|
||||
instance.requestPaymentMethod(function (err, payload) {
|
||||
$("#payment-method-modal").modal("hide");
|
||||
$("#please-wait-modal").modal("show");
|
||||
|
||||
$("#nonce").val(payload.nonce);
|
||||
$("#update-subscription-form").submit();
|
||||
});
|
||||
});
|
||||
}).prop("disabled", false);
|
||||
|
||||
$("#payment-method-modal").off("hidden.bs.modal").on("hidden.bs.modal", function() {
|
||||
instance.teardown();
|
||||
});
|
||||
|
||||
instance.on("paymentMethodRequestable", function() {
|
||||
$("#payment-form-submit").prop("disabled", false);
|
||||
});
|
||||
|
||||
instance.on("noPaymentMethodRequestable", function() {
|
||||
$("#payment-form-submit").prop("disabled", true);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$("#update-payment-method").hover(requestClientToken);
|
||||
|
||||
$("#update-payment-method").click(function() {
|
||||
requestClientToken();
|
||||
$("#payment-form").attr("action", this.dataset.action);
|
||||
$("#payment-form-submit").text("Update Payment Method");
|
||||
$("#payment-method-modal").modal("show");
|
||||
$("#change-plan-btn").click(function() {
|
||||
$("#change-billing-plan-modal").modal("hide");
|
||||
showPaymentMethodForm(this.dataset.planId);
|
||||
});
|
||||
|
||||
$("#update-payment-method").click(function() {
|
||||
showPaymentMethodForm($("#old-plan-id").val());
|
||||
});
|
||||
|
||||
$("#billing-history").load( "/accounts/profile/billing/history/" );
|
||||
$("#billing-address").load( "/accounts/profile/billing/address/", function() {
|
||||
$("#billing-history").load("/accounts/profile/billing/history/");
|
||||
$("#billing-address").load("/accounts/profile/billing/address/", function() {
|
||||
$("#billing-address input").each(function(idx, obj) {
|
||||
$("#" + obj.name).val(obj.value);
|
||||
});
|
||||
});
|
||||
|
||||
$("#payment-method").load( "/accounts/profile/billing/payment_method/", function() {
|
||||
$("#payment-method").load("/accounts/profile/billing/payment_method/", function() {
|
||||
$("#next-billing-date").text($("#nbd").val());
|
||||
});
|
||||
|
||||
@ -94,9 +141,7 @@ $(function () {
|
||||
if ($("#plan-business-plus").hasClass("selected")) {
|
||||
planId = period == "monthly" ? "P80" : "Y768";
|
||||
}
|
||||
|
||||
$("#plan-id").val(planId);
|
||||
|
||||
|
||||
if (planId == $("#old-plan-id").val()) {
|
||||
$("#change-plan-btn")
|
||||
.attr("disabled", "disabled")
|
||||
@ -105,10 +150,14 @@ $(function () {
|
||||
} else {
|
||||
var caption = "Change Billing Plan";
|
||||
if (planId) {
|
||||
caption += " And Pay $" + planId.substr(1) + " Now";
|
||||
var amount = planId.substr(1);
|
||||
caption += " And Pay $" + amount + " Now";
|
||||
}
|
||||
|
||||
$("#change-plan-btn").removeAttr("disabled").text(caption);
|
||||
$("#change-plan-btn")
|
||||
.removeAttr("disabled")
|
||||
.text(caption)
|
||||
.attr("data-plan-id", planId);
|
||||
}
|
||||
}
|
||||
updateChangePlanForm();
|
||||
|
@ -76,16 +76,13 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if sub.subscription_id %}
|
||||
<div class="panel panel-{{ payment_method_status }}">
|
||||
<div class="panel-body settings-block">
|
||||
<h2>Payment Method</h2>
|
||||
{% if sub.payment_method_token %}
|
||||
<p id="payment-method">
|
||||
<span class="loading">loading…</span>
|
||||
</p>
|
||||
{% else %}
|
||||
<p id="payment-method-missing" class="billing-empty">Not set</p>
|
||||
{% endif %}
|
||||
<button
|
||||
id="update-payment-method"
|
||||
class="btn btn-default pull-right">
|
||||
@ -97,6 +94,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-{{ address_status }}">
|
||||
@ -170,110 +168,104 @@
|
||||
|
||||
<div id="change-billing-plan-modal" class="modal">
|
||||
<div class="modal-dialog">
|
||||
{% if sub.payment_method_token and sub.address_id %}
|
||||
<form method="post" class="form-horizontal" autocomplete="off" action="{% url 'hc-set-plan' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="old-plan-id" value="{{ sub.plan_id }}">
|
||||
<input type="hidden" id="plan-id" name="plan_id" value="{{ sub.plan_id }}">
|
||||
{% if sub.address_id %}
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4>Change Billing Plan</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div id="plan-hobbyist" class="panel plan {% if sub.plan_id == "" %}selected{% endif %}">
|
||||
<div class="marker">Selected Plan</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4>Change Billing Plan</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div id="plan-hobbyist" class="panel plan {% if sub.plan_id == "" %}selected{% endif %}">
|
||||
<div class="marker">Selected Plan</div>
|
||||
|
||||
<h2>Hobbyist</h2>
|
||||
<ul>
|
||||
<li>Checks: 20</li>
|
||||
<li>Team members: 3</li>
|
||||
<li>Log entries: 100</li>
|
||||
</ul>
|
||||
<h3>Free</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div id="plan-business" class="panel plan {% if sub.plan_id == "P20" or sub.plan_id == "Y192" %}selected{% endif %}">
|
||||
<div class="marker">Selected Plan</div>
|
||||
|
||||
<h2>Business</h2>
|
||||
<ul>
|
||||
<li>Checks: 100</li>
|
||||
<li>Team members: 10</li>
|
||||
<li>Log entries: 1000</li>
|
||||
</ul>
|
||||
<h3>
|
||||
<span id="business-price"></span>
|
||||
<small>/ month</small>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div id="plan-business-plus" class="panel plan {% if sub.plan_id == "P80" or sub.plan_id == "Y768" %}selected{% endif %}">
|
||||
<div class="marker">Selected Plan</div>
|
||||
|
||||
<h2>Business Plus</h2>
|
||||
<ul>
|
||||
<li>Checks: 1000</li>
|
||||
<li>Team members: Unlimited</li>
|
||||
<li>Log entries: 1000</li>
|
||||
</ul>
|
||||
<h3>
|
||||
<span id="business-plus-price"></span>
|
||||
<small>/ month</small>
|
||||
</h3>
|
||||
</div>
|
||||
<h2>Hobbyist</h2>
|
||||
<ul>
|
||||
<li>Checks: 20</li>
|
||||
<li>Team members: 3</li>
|
||||
<li>Log entries: 100</li>
|
||||
</ul>
|
||||
<h3>Free</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div id="plan-business" class="panel plan {% if sub.plan_id == "P20" or sub.plan_id == "Y192" %}selected{% endif %}">
|
||||
<div class="marker">Selected Plan</div>
|
||||
|
||||
<div class="row">
|
||||
<div id="billing-periods" class="col-sm-6">
|
||||
<p>Billing Period</p>
|
||||
|
||||
<label class="radio-container">
|
||||
<input
|
||||
id="billing-monthly"
|
||||
type="radio"
|
||||
name="billing_period"
|
||||
value="monthly"
|
||||
{% if sub.plan_id == "Y192" or sub.plan_id == "Y768" %}{% else %}checked{% endif %}>
|
||||
<span class="radiomark"></span>
|
||||
Monthly
|
||||
</label>
|
||||
<label class="radio-container">
|
||||
<input
|
||||
id="billing-annual"
|
||||
type="radio"
|
||||
name="billing_period"
|
||||
value="annual"
|
||||
{% if sub.plan_id == "Y192" or sub.plan_id == "Y768" %} checked {% endif %}>
|
||||
<span class="radiomark"></span>
|
||||
Annual, 20% off
|
||||
</label>
|
||||
<h2>Business</h2>
|
||||
<ul>
|
||||
<li>Checks: 100</li>
|
||||
<li>Team members: 10</li>
|
||||
<li>Log entries: 1000</li>
|
||||
</ul>
|
||||
<h3>
|
||||
<span id="business-price"></span>
|
||||
<small>/ month</small>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div id="plan-business-plus" class="panel plan {% if sub.plan_id == "P80" or sub.plan_id == "Y768" %}selected{% endif %}">
|
||||
<div class="marker">Selected Plan</div>
|
||||
|
||||
<div class="text-warning">
|
||||
<strong>No proration.</strong> We currently do not
|
||||
support proration when changing billing plans.
|
||||
Changing the plan starts a new billing cycle
|
||||
and charges your payment method.
|
||||
<h2>Business Plus</h2>
|
||||
<ul>
|
||||
<li>Checks: 1000</li>
|
||||
<li>Team members: Unlimited</li>
|
||||
<li>Log entries: 1000</li>
|
||||
</ul>
|
||||
<h3>
|
||||
<span id="business-plus-price"></span>
|
||||
<small>/ month</small>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div id="billing-periods" class="col-sm-6">
|
||||
<p>Billing Period</p>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button id="change-plan-btn" type="submit" class="btn btn-primary" disabled="disabled">
|
||||
Change Billing Plan
|
||||
</button>
|
||||
<label class="radio-container">
|
||||
<input
|
||||
id="billing-monthly"
|
||||
type="radio"
|
||||
name="billing_period"
|
||||
value="monthly"
|
||||
{% if sub.plan_id == "Y192" or sub.plan_id == "Y768" %}{% else %}checked{% endif %}>
|
||||
<span class="radiomark"></span>
|
||||
Monthly
|
||||
</label>
|
||||
<label class="radio-container">
|
||||
<input
|
||||
id="billing-annual"
|
||||
type="radio"
|
||||
name="billing_period"
|
||||
value="annual"
|
||||
{% if sub.plan_id == "Y192" or sub.plan_id == "Y768" %} checked {% endif %}>
|
||||
<span class="radiomark"></span>
|
||||
Annual, 20% off
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-warning">
|
||||
<strong>No proration.</strong> We currently do not
|
||||
support proration when changing billing plans.
|
||||
Changing the plan starts a new billing cycle
|
||||
and charges your payment method.
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button id="change-plan-btn" type="button" class="btn btn-primary" disabled="disabled">
|
||||
Change Billing Plan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@ -281,14 +273,6 @@
|
||||
<h4>Some details are missing…</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% if not sub.payment_method_token %}
|
||||
<div id="no-payment-method">
|
||||
<h4>No payment method.</h4>
|
||||
<p>Please add a payment method before changing the billing
|
||||
plan.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not sub.address_id %}
|
||||
<div id="no-billing-address">
|
||||
<h4>Country not specified.</h4>
|
||||
@ -315,28 +299,23 @@
|
||||
|
||||
<div id="payment-method-modal" class="modal pm-modal">
|
||||
<div class="modal-dialog">
|
||||
<form id="payment-form" method="post" action="{% url 'hc-payment-method' %}">
|
||||
{% csrf_token %}
|
||||
<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">×</button>
|
||||
<h4>Payment Method</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="dropin"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="payment-form-submit" type="button" class="btn btn-primary" disabled>
|
||||
Confirm Payment Method
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4>Payment Method</h4>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-body">
|
||||
<div id="dropin"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="payment-form-submit" type="button" class="btn btn-primary" disabled>
|
||||
Confirm Payment Method
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -510,11 +489,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="please-wait-modal" class="modal pm-modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>Payment Method</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Processing, please wait…
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" disabled>
|
||||
Confirm Payment Method
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="update-subscription-form" method="post" action="{% url 'hc-update-subscription' %}">
|
||||
{% csrf_token %}
|
||||
<input id="nonce" type="hidden" name="nonce" />
|
||||
<input type="hidden" id="old-plan-id" value="{{ sub.plan_id }}">
|
||||
<input id="plan-id" type="hidden" name="plan_id" />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://js.braintreegateway.com/web/dropin/1.17.1/js/dropin.min.js"></script>
|
||||
<script src="https://js.braintreegateway.com/web/dropin/1.20.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…
x
Reference in New Issue
Block a user