From d70725f2b3e23549c03842f54905e5628ddfffe7 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 19 Dec 2018 17:23:34 +0100 Subject: [PATCH] buy now flow complete except email confirmation --- rowers/braintreestuff.py | 143 +++++++++++++- rowers/forms.py | 3 +- rowers/models.py | 5 +- rowers/payments.py | 2 +- rowers/templates/billing.html | 2 +- rowers/templates/payment_completed.html | 26 +++ rowers/templates/paymentconfirm.html | 113 +++++++++++ rowers/templates/rower_form.html | 40 +++- rowers/templates/subscriptions_cancel.html | 64 +++++++ rowers/templatetags/rowerfilters.py | 11 +- rowers/urls.py | 5 +- rowers/views.py | 211 ++++++++++++--------- 12 files changed, 512 insertions(+), 113 deletions(-) create mode 100644 rowers/templates/payment_completed.html create mode 100644 rowers/templates/paymentconfirm.html create mode 100644 rowers/templates/subscriptions_cancel.html diff --git a/rowers/braintreestuff.py b/rowers/braintreestuff.py index 7efd6345..fb1f22a8 100644 --- a/rowers/braintreestuff.py +++ b/rowers/braintreestuff.py @@ -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): - client_token = gateway.client_token.generate({ - "customer_id":rower.customer_id, + 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 + + diff --git a/rowers/forms.py b/rowers/forms.py index b4f03bb9..c7a35b9d 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -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 diff --git a/rowers/models.py b/rowers/models.py index 50fb9cc1..766c9caf 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -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) diff --git a/rowers/payments.py b/rowers/payments.py index ecc5691d..692852af 100644 --- a/rowers/payments.py +++ b/rowers/payments.py @@ -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 diff --git a/rowers/templates/billing.html b/rowers/templates/billing.html index ea725daa..bf4a358f 100644 --- a/rowers/templates/billing.html +++ b/rowers/templates/billing.html @@ -31,6 +31,6 @@ {% endblock %} {% block sidebar %} -{% include 'menu_help.html' %} +{% include 'menu_profile.html' %} {% endblock %} diff --git a/rowers/templates/payment_completed.html b/rowers/templates/payment_completed.html new file mode 100644 index 00000000..b29fbaea --- /dev/null +++ b/rowers/templates/payment_completed.html @@ -0,0 +1,26 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Paid Membership{% endblock title %} +{% load rowerfilters %} +{% block main %} + +

Your Payment was completed

+ +

+ Thank you for registering to {{ user.rower.paidplan.name }}. You have paid for 12 months + membership. +

+ +

+ {% 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 %} +

+ +{% endblock %} + +{% block sidebar %} +{% include 'menu_profile.html' %} +{% endblock %} + diff --git a/rowers/templates/paymentconfirm.html b/rowers/templates/paymentconfirm.html new file mode 100644 index 00000000..4bb14355 --- /dev/null +++ b/rowers/templates/paymentconfirm.html @@ -0,0 +1,113 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Paid Membership{% endblock title %} +{% load rowerfilters %} +{% block main %} + +

Confirm Your Payment

+ +

Order Overview

+ + + + + + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_profile.html' %} +{% endblock %} + diff --git a/rowers/templates/rower_form.html b/rowers/templates/rower_form.html index c5b82b61..ce5aea0c 100644 --- a/rowers/templates/rower_form.html +++ b/rowers/templates/rower_form.html @@ -14,7 +14,7 @@

Account Information

{% if rower.user == user %} - Password Change + Password Change {% else %}   {% endif %} @@ -34,29 +34,49 @@ {{ userform.as_table }} {{ accountform.as_table }} - Plan{{ rower.rowerplan }} +   - Plan Expiry{{ rower.planexpires }} + Plan{{ rower.paidplan.name }} + {% if rower.rowerplan != 'basic' %} + + + {% if rower.paymenttype != 'recurring' %} + Plan Expiry + {% else %} + Next Payment Due + {% endif %} + {{ rower.planexpires }} + + {% endif %} {% csrf_token %} - {% if rower.rowerplan == 'basic' and rower.user == user %} - Upgrade + {% if rower.rowerplan != 'coach' and rower.user == user %} +

+ Upgrade +

{% else %} -   +

+   +

{% endif %} - + {% if rower.rowerplan != 'basic' and rower.user == user %} +

+ Cancel Subscription +

+ {% endif %} + {% if rower.user == user %}
  • GDPR - Data Protection

    - Download your data + Download your data

    - Deactivate Account + Deactivate Account

    Delete Account @@ -79,7 +99,7 @@ {{ grant.application }} {{ grant.scope }} - Revoke + Revoke {% endfor %} diff --git a/rowers/templates/subscriptions_cancel.html b/rowers/templates/subscriptions_cancel.html new file mode 100644 index 00000000..aa360f37 --- /dev/null +++ b/rowers/templates/subscriptions_cancel.html @@ -0,0 +1,64 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Paid Membership{% endblock title %} +{% load rowerfilters %} +{% block main %} + +

    Cancel Subscriptions

    + + + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_profile.html' %} +{% endblock %} + diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index e2329849..253ced99 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -125,7 +125,16 @@ def c2userid(user): c2userid = c2stuff.get_userid(thetoken) 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: diff --git a/rowers/urls.py b/rowers/urls.py index 63504a03..73bb2bd9 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -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\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[\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), diff --git a/rowers/views.py b/rowers/views.py index 5aba8d49..dda94c0d 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -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,7 +809,17 @@ 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() res = myqueue(queuehigh, @@ -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,50 +1109,66 @@ 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, }) - + @login_required() def checkouts_view(request): @@ -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 - ) - ) - else: - messages.error(request,"We are sorry but there was an error with the payment") - url = reverse(payments_view) + 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,"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):