Private
Public Access
1
0

buy now flow complete except email confirmation

This commit is contained in:
Sander Roosendaal
2018-12-19 17:23:34 +01:00
parent fa61957be7
commit d70725f2b3
12 changed files with 512 additions and 113 deletions

View File

@@ -1,4 +1,6 @@
import braintree
from django.utils import timezone
import datetime
from rowsandall_app.settings import (
BRAINTREE_MERCHANT_ID,BRAINTREE_PUBLIC_KEY,BRAINTREE_PRIVATE_KEY
@@ -16,8 +18,8 @@ gateway = braintree.BraintreeGateway(
from rowers.models import Rower,PaidPlan
from rowers.utils import ProcessorCustomerError
def create_customer(rower):
if not rower.customer_id:
def create_customer(rower,force=False):
if not rower.customer_id or force:
result = gateway.customer.create(
{
'first_name':rower.user.first_name,
@@ -28,14 +30,25 @@ def create_customer(rower):
raise ProcessorCustomerError
else:
rower.customer_id = result.customer.id
rower.paymentprocessor = 'braintree'
rower.save()
return rower.customer_id
else:
return rower.customer_id
def get_client_token(rower):
try:
client_token = gateway.client_token.generate({
"customer_id":rower.customer_id,
})
except ValueError:
customer_id = create_customer(rower,force=True)
client_token = gateway.client_token.generate({
"customer_id": customer_id,
})
return client_token
@@ -51,3 +64,125 @@ def get_plans_costs():
plan.save()
return plans
def make_payment(rower,data):
nonce_from_the_client = data['payment_method_nonce']
amount = data['amount']
amount = str(amount)
result = gateway.transaction.sale({
"amount": amount,
"payment_method_nonce": nonce_from_the_client,
"options": {
"submit_for_settlement": True
}
})
if result.is_success:
transaction = result.transaction
amount = transaction.amount
return amount
else:
return 0,''
def create_subscription(rower,data):
planid = data['plan']
plan = PaidPlan.objects.get(id=planid)
nonce_from_the_client = data['payment_method_nonce']
amount = data['amount']
# create or find payment method
result = gateway.payment_method.create({
"customer_id": rower.customer_id,
"payment_method_nonce": nonce_from_the_client
})
if result.is_success:
payment_method_token = result.payment_method.token
else:
return False
result = gateway.subscription.create({
"payment_method_token": payment_method_token,
"plan_id": plan.external_id
})
if result.is_success:
rower.paidplan = plan
rower.planexpires = timezone.now()+datetime.timedelta(days=365)
rower.teamplanexpires = timezone.now()+datetime.timedelta(days=365)
rower.clubsize = plan.clubsize
rower.paymenttype = plan.paymenttype
rower.rowerplan = plan.shortname
rower.save()
return True
else:
return False
return False
def cancel_subscription(rower,id):
themessages = []
errormessages = []
try:
result = gateway.subscription.cancel(id)
messages.append("Subscription canceled")
except:
errormessages.append("We could not find the subscription record in our customer database")
return False, themessages, errormessages
rower.paidplan = None
rower.teamplanexpires = timezone.now()
rower.planexpires = timezone.now()
rower.clubsize = 0
rower.rowerplan = 'basic'
rower.save()
themessages.append("Your plan was reset to basic")
return True, themessages,errormessages
def find_subscriptions(rower):
try:
result = gateway.customer.find(rower.customer_id)
except:
raise ProcessorCustomerError("We could not find the customer in the database")
active_subscriptions = []
cards = result.credit_cards
for card in cards:
for subscription in card.subscriptions:
if subscription.status == 'Active':
active_subscriptions.append(subscription)
try:
paypal_accounts = result.paypal_accounts
for account in accuonts:
for subscription in account.subscriptions:
if subscription.status == 'Active':
active_subscriptions.append(subscription)
except AttributeError:
pass
result = []
for subscription in active_subscriptions:
plan = PaidPlan.objects.filter(paymentprocessor="braintree",
external_id=subscription.plan_id)[0]
thedict = {
'end_date': subscription.billing_period_end_date,
'plan_id': subscription.plan_id,
'price': subscription.price,
'id': subscription.id,
'plan': plan.name
}
result.append(thedict)
return result

View File

@@ -22,7 +22,8 @@ from metrics import axes
# BillingForm form
class BillingForm(forms.Form):
amount = forms.FloatField(required=True)
payment_method_nonce = forms.CharField(max_length=255)
plan = forms.IntegerField(widget=forms.HiddenInput())
payment_method_nonce = forms.CharField(max_length=255,required=True)
# login form

View File

@@ -558,7 +558,7 @@ paymentprocessors = (
class PaidPlan(models.Model):
shortname = models.CharField(max_length=50,choices=plans)
name = models.CharField(max_length=200)
external_id = models.IntegerField(blank=True,null=True,default=None)
external_id = models.CharField(blank=True,null=True,default=None,max_length=200)
price = models.FloatField(blank=True,null=True,default=None)
paymentprocessor = models.CharField(
max_length=50,choices=paymentprocessors,default='braintree')
@@ -633,7 +633,8 @@ class Rower(models.Model):
)
paymentprocessor = models.CharField(max_length=50,
choices=paymentprocessors,
default='braintree')
null=True,blank=True,
default=None)
paidplan = models.ForeignKey(PaidPlan,null=True,default=None)

View File

@@ -28,7 +28,7 @@ def setrowerplans():
print 'Could not set plan for ',r
def is_existing_customer(rower):
if rower.country is not None and rower.customer_id is not None and r.country != '':
if rower.country is not None and rower.customer_id is not None and rower.country != '':
return True
return False

View File

@@ -31,6 +31,6 @@
{% endblock %}
{% block sidebar %}
{% include 'menu_help.html' %}
{% include 'menu_profile.html' %}
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "newbase.html" %}
{% block title %}Rowsandall Paid Membership{% endblock title %}
{% load rowerfilters %}
{% block main %}
<h1>Your Payment was completed</h1>
<p>
Thank you for registering to {{ user.rower.paidplan.name }}. You have paid for 12 months
membership.
</p>
<p>
{% if user.rower.paymenttype == 'recurring' %}
Your next payment will be automatically processed on {{ user.rower.planexpires }}
{% else %}
Your plan will end automatically on {{ user.rower.planexpires }}
{% endif %}
</p>
{% endblock %}
{% block sidebar %}
{% include 'menu_profile.html' %}
{% endblock %}

View File

@@ -0,0 +1,113 @@
{% extends "newbase.html" %}
{% block title %}Rowsandall Paid Membership{% endblock title %}
{% load rowerfilters %}
{% block main %}
<h1>Confirm Your Payment</h1>
<h2>Order Overview</h2>
<ul class="main-content">
<li class="grid_2">
<p>
<table class="plantable shortpadded" width="80%">
<tbody>
<tr>
<th>Plan</th><td>{{ plan.name }}</td>
</tr>
<tr>
<th>Payment Type</th><td>{{ plan.paymenttype }}</td>
</tr>
<tr>
<th>Plan Duration</th><td>1 year starting today</td>
</tr>
<tr>
<th>Total</th><td>&euro; {{ plan.price|currency }}
{% if plan.paymenttype == 'recurring' %}
/year
{% endif %}
</td>
</tr>
</tbody>
</table>
</p>
<p>
<table class="plantable shortpadded" width="80%">
<tbody>
<tr>
<th>Street Address</th><td>{{ user.rower.street_address }}</td>
</tr>
<tr>
<th>City</th><td>{{ user.rower.city }}</td>
</tr>
<tr>
<th>Posstal Code</th><td>{{ user.rower.postal_code }}</td>
</tr>
<tr>
<th>Country</th><td>{{ user.rower.country }}
</td>
</tr>
</tbody>
</table>
</p>
</li>
<li class="grid_2">
<p>
<a href="/rowers/billing">Change Order</a>
</li>
<li class="grid_4">
<form id="payment-form" method="post" action="/rowers/checkouts"
autocomplete="off">
<section>
<label for="amount">
<div class="input-wrapper amount-wrapper">
<input id="amount" name="amount" type="hidden" min="1" placeholder="Amount"
value="{{ plan.price }}" readonly>
</div>
</label>
<div class="bt-drop-in-wrapper">
<div id="bt-dropin"></div>
</div>
</section>
<input type="hidden" id="nonce" name="payment_method_nonce" />
<input type="hidden" id="plan" name="plan" value="{{ plan.id }}">
{% csrf_token %}
<button type="submit" id="submit-button"><span>Pay &euro; {{ plan.price|currency }}</span></button>
</form>
</li>
</ul>
<script src="https://js.braintreegateway.com/web/dropin/1.14.1/js/dropin.min.js"></script>
<script>
var form = document.querySelector('#payment-form');
var client_token = '{{ client_token }}';
braintree.dropin.create({
authorization: client_token,
container: '#bt-dropin',
paypal: {
flow: 'checkout'
}
}, function (createErr, instance) {
form.addEventListener('submit', function (event) {
event.preventDefault();
instance.requestPaymentMethod(function (err, payload) {
if (err) {
console.log('Error', err);
return;
}
// Add the nonce to the form and submit
document.querySelector('#nonce').value = payload.nonce;
form.submit();
});
});
});
</script>
{% endblock %}
{% block sidebar %}
{% include 'menu_profile.html' %}
{% endblock %}

View File

@@ -14,7 +14,7 @@
<h2>Account Information</h2>
<p>
{% if rower.user == user %}
<a class="button blue small" href="/password_change/">Password Change</a>
<a href="/password_change/">Password Change</a>
{% else %}
&nbsp;
{% endif %}
@@ -34,29 +34,49 @@
{{ userform.as_table }}
{{ accountform.as_table }}
<tr>
<th>Plan</th><td>{{ rower.rowerplan }}</td>
<th>&nbsp;</th><td></td>
</tr>
<tr>
<th>Plan Expiry</th><td>{{ rower.planexpires }}</td>
<th>Plan</th><td>{{ rower.paidplan.name }}</td>
</tr>
{% if rower.rowerplan != 'basic' %}
<tr>
<th>
{% if rower.paymenttype != 'recurring' %}
Plan Expiry
{% else %}
Next Payment Due
{% endif %}
</th><td>{{ rower.planexpires }}</td>
</tr>
{% endif %}
</table>
{% csrf_token %}
{% if rower.rowerplan == 'basic' and rower.user == user %}
<a class="button blue" href="/rowers/promembership">Upgrade</a>
{% if rower.rowerplan != 'coach' and rower.user == user %}
<p>
<a href="/rowers/paidplans">Upgrade</a>
</p>
{% else %}
<p>
&nbsp;
</p>
{% endif %}
<input class="button green" type="submit" value="Save">
{% if rower.rowerplan != 'basic' and rower.user == user %}
<p>
<a href="/rowers/me/cancelsubscriptions">Cancel Subscription</a>
</p>
{% endif %}
<input type="submit" value="Save">
</form>
</li>
{% if rower.user == user %}
<li class="grid_2">
<h2>GDPR - Data Protection</h2>
<p>
<a class="button blue small" href="/rowers/exportallworkouts">Download your data</a>
<a href="/rowers/exportallworkouts">Download your data</a>
</p>
<p>
<a class="button blue small" href="/rowers/me/deactivate">Deactivate Account</a>
<a href="/rowers/me/deactivate">Deactivate Account</a>
</p>
<p>
<a class="button red small" href="/rowers/me/delete">Delete Account</a>
@@ -79,7 +99,7 @@
<td>{{ grant.application }}</td>
<td>{{ grant.scope }}</td>
<td>
<a class="button red small" href="/rowers/me/revokeapp/{{ grant.application.id }}">Revoke</a>
<a href="/rowers/me/revokeapp/{{ grant.application.id }}">Revoke</a>
</td>
</tr>
{% endfor %}

View File

@@ -0,0 +1,64 @@
{% extends "newbase.html" %}
{% block title %}Rowsandall Paid Membership{% endblock title %}
{% load rowerfilters %}
{% block main %}
<h1>Cancel Subscriptions</h1>
<ul class="main-content">
<li class="grid_4">
{% if subscriptions %}
<p>
<table class="plantable shortpadded" width="80%">
<thead>
<tr>
<th>Subscription</th><th>Next Billing Date</th><th>Price</th><th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for subscription in subscriptions %}
<tr>
<td>
{{ subscription|lookup:"plan" }}
</td>
<td>
{{ subscription|lookup:"end_date" }}
</td>
<td>
{{ subscription|lookup:"price" }} &euro;
</td>
<td>
<a href="/rowers/me/cancelsubscription/{{ subscription|lookup:'id' }}">Stop this plan</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>
<p>
By clicking on the link to stop the plan, you will downgrade to the Basic plan.
Future payments will be stopped.
</p>
{% else %}
<p>
You don't have any subscriptions or your subscriptions cannot be automatically stopped
from the site.
</p>
<p>
If you have paid through PayPal, log in to your PayPal account and cancel the recurring payment
there. We will manually downgrade your subscription.
</p>
<p>
If you have questions, don't hesitate to contact us.
</p>
{% endif %}
</li>
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_profile.html' %}
{% endblock %}

View File

@@ -126,6 +126,15 @@ def c2userid(user):
return c2userid
@register.filter
def currency(word):
try:
amount = float(word)
except ValueError:
return word
return '{amount:.2f}'.format(amount=amount)
@register.filter
def rkuserid(user):
try:

View File

@@ -438,10 +438,13 @@ urlpatterns = [
url(r'^analysis/$', views.analysis_view,name='analysis'),
url(r'^laboratory/$', views.laboratory_view,name='laboratory'),
url(r'^promembership', TemplateView.as_view(template_name='promembership.html'),name='promembership'),
url(r'^checkout/(?P<planid>\d+)$',views.payment_confirm_view),
url(r'^billing',views.billing_view,name='billing'),
url(r'^paymentcompleted',views.payment_completed_view),
url(r'^paidplans',views.paidplans_view,name='paidplans'),
url(r'^me/cancelsubscriptions',views.plan_stop_view),
url(r'^me/cancelsubscription/(?P<id>[\w\ ]+.*)$',views.plan_tobasic_view),
url(r'^checkouts',views.checkouts_view,name='checkouts'),
url(r'^payments',views.payments_view,name='payments'),
url(r'^planrequired',views.planrequired_view),
url(r'^starttrial$',views.start_trial_view),
url(r'^startplantrial$',views.start_plantrial_view),

View File

@@ -84,7 +84,7 @@ from rowers.models import (
createmicrofillers, createmesofillers,
microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates,
TrainingMesoCycleForm, TrainingMicroCycleForm,
RaceLogo,RowerBillingAddressForm,
RaceLogo,RowerBillingAddressForm,PaidPlan,
)
from rowers.models import (
RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm,
@@ -738,8 +738,27 @@ def deactivate_user(request):
if request.method == "POST":
user_form = DeactivateUserForm(request.POST, instance=user)
if user_form.is_valid():
r = Rower.objects.get(user=user)
if r.paidplan is not None and r.paidplan.paymentprocessor == 'braintree':
try:
subscriptions = braintreestuff.find_subscriptions(r)
for subscription in subscriptions:
success, themessages,errormessages = braintreestuff.cancel_subscription(r,id)
for message in themessages:
messages.info(request,message)
except ProcessorCustomerError:
pass
r.paidplan = None
r.teamplanexpires = timezone.now()
r.planexpires = timezone.now()
r.clubsize = 0
r.rowerplan = 'basic'
r.save()
deactivate_user = user_form.save(commit=False)
user.is_active = False
user.save()
deactivate_user.save()
# url = reverse(auth_views.logout_then_login)
url = '/logout/?next=/login'
@@ -790,6 +809,16 @@ def remove_user(request):
name = user.first_name+' '+user.last_name
email = user.email
r = Rower.objects.get(user=user)
if r.paidplan is not None and r.paidplan.paymentprocessor == 'braintree':
try:
subscriptions = braintreestuff.find_subscriptions(r)
for subscription in subscriptions:
success, themessages,errormessages = braintreestuff.cancel_subscription(r,id)
for message in themessages:
messages.info(request,message)
except ProcessorCustomerError:
pass
if cd['delete_user']:
user.delete()
@@ -998,7 +1027,7 @@ def hasplannedsessions(user):
return result
from rowers.utils import isprorower
from rowers.utils import isprorower,ProcessorCustomerError
# Check if a user is a Pro member
def ispromember(user):
@@ -1056,6 +1085,19 @@ def billing_view(request):
if planselectform.is_valid():
plan = planselectform.cleaned_data['plan']
if billingaddressform.is_valid():
try:
customer_id = braintreestuff.create_customer(r)
except ProcessorCustomerError:
messages.error(request,"Something went wrong registering you as a customer.")
url = reverse(billing_view)
return HttpResponseRedirect(url)
url = reverse(payment_confirm_view,
kwargs={
'planid':plan.id
})
return HttpResponseRedirect(url)
else:
billingaddressform = RowerBillingAddressForm(instance=r)
planselectform = PlanSelectForm(paymentprocessor='braintree')
@@ -1067,48 +1109,64 @@ def billing_view(request):
'planselectform':planselectform,
})
# Experimental - Payments
@login_required()
def payments_view(request):
def plan_stop_view(request):
r = getrequestrower(request)
subscriptions = []
if r.paidplan is not None and r.paidplan.paymentprocessor == 'braintree':
try:
subscriptions = braintreestuff.find_subscriptions(r)
except ProcessorCustomerError:
r.paymentprocessor = None
r.save()
return render(request,
'subscriptions_cancel.html',
{'rower':r,
'subscriptions':subscriptions
})
@login_required()
def plan_tobasic_view(request,id=0):
r = getrequestrower(request)
if r.paidplan.paymentprocessor == 'braintree':
success, themessages,errormessages = braintreestuff.cancel_subscription(r,id)
for message in themessages:
messages.info(request,message)
for message in errormessages:
messages.error(request,message)
url = reverse(plan_stop_view)
return HttpResponseRedirect(url)
@login_required()
def payment_confirm_view(request,planid = 0):
try:
plan = PaidPlan.objects.get(id=planid)
except PaidPlan.DoesNotExist:
messages.error(request,"Something went wrong. Please try again.")
url = reverse(billing_view)
return HttpResponseRedirect(url)
r = getrequestrower(request)
gateway = braintree.BraintreeGateway(
braintree.Configuration(
braintree.Environment.Sandbox,
merchant_id=BRAINTREE_MERCHANT_ID,
public_key=BRAINTREE_PUBLIC_KEY,
private_key=BRAINTREE_PRIVATE_KEY,
)
)
# add code to store customer_id
if not r.customer_id:
result = gateway.customer.create(
{
'first_name':r.user.first_name,
'last_name':r.user.last_name,
'email':r.user.email,
})
if not result.is_success:
messages.error(request,'Failed to create customer. Please try again later')
return render(request,
"payments.html")
else:
r.customer_id = result.customer.id
r.save()
client_token = gateway.client_token.generate({
"customer_id": r.customer_id,
})
client_token = braintreestuff.get_client_token(r)
return render(request,
"payments.html",
"paymentconfirm.html",
{
'plan':plan,
'client_token':client_token,
'rower':r,
})
@@ -1118,72 +1176,41 @@ def checkouts_view(request):
r = getrequestrower(request)
if request.method != 'POST':
url = reverse(payments_view)
url = reverse(paidplans_view)
return HttpResponseRedirect(url)
# we're still here
gateway = braintree.BraintreeGateway(
braintree.Configuration(
braintree.Environment.Sandbox,
merchant_id=BRAINTREE_MERCHANT_ID,
public_key=BRAINTREE_PUBLIC_KEY,
private_key=BRAINTREE_PRIVATE_KEY,
)
)
form = BillingForm(request.POST)
if form.is_valid():
nonce_from_the_client = form.cleaned_data['payment_method_nonce']
amount = form.cleaned_data['amount']
amount = str(amount)
#for testing
#nonce_from_the_client = 'fake-processor-declined-visa-none'
result = gateway.transaction.sale({
"amount": amount,
"payment_method_nonce": nonce_from_the_client,
"options": {
"submit_for_settlement": True
}
})
if result.is_success:
transaction = result.transaction
if transaction.payment_instrument_type == "credit_card":
country = transaction.credit_card_details.country_of_issuance
if country == 'Unknown':
bin = transaction.credit_card_details.bin
url = "https://lookup.binlist.net/"+str(bin)
headers = {
'Accept-Version':'3',
}
binresult = requests.get(url,headers=headers)
print binresult.status_code
if binresult.status_code == 200:
js = binresult.json()
country = js['country']['name']
print country
amount = transaction.amount
messages.info(request,
"We have successfully received your payment of {amount} Euro".format(
amount=amount
)
)
data = form.cleaned_data
success = braintreestuff.create_subscription(r,data)
if success:
messages.info(request,"Your payment has succeeded and your plan has been updated")
url = reverse(payment_completed_view)
return HttpResponseRedirect(url)
else:
messages.error(request,"We are sorry but there was an error with the payment")
url = reverse(payments_view)
messages.error(request,"There was a problem with your payment")
url = reverse(billing_view)
return HttpResponseRedirect(url)
else:
messages.error(request,"There was an error in the payment form")
url = reverse(payments_view)
url = reverse(billing_view)
return HttpResponseRedirect(url)
url = reverse(payments_view)
return HttpResponseRedirect(url)
@login_required()
def payment_completed_view(request):
r = getrequestrower(request)
return render(request,
"payment_completed.html",
{
'rower':r
})
# User registration
def rower_register_view(request):