From a9091bfed78b77a08f320f6a7e46bf8bc92d6265 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 24 Mar 2021 06:35:25 +0100 Subject: [PATCH] works but cannot change address --- rowers/braintreestuff.py | 11 +- rowers/fakturoid.py | 7 +- rowers/forms.py | 13 ++ rowers/models.py | 2 +- rowers/templates/buy_trainingplan.html | 43 +++++ rowers/templates/confirm_trainingplan.html | 114 ++++++++++++ rowers/templates/instantplan.html | 13 ++ rowers/templates/instantplans.html | 5 + rowers/urls.py | 5 +- rowers/views/paymentviews.py | 206 +++++++++++++++++++++ rowers/views/planviews.py | 9 +- rowers/views/statements.py | 1 + 12 files changed, 419 insertions(+), 10 deletions(-) create mode 100644 rowers/templates/buy_trainingplan.html create mode 100644 rowers/templates/confirm_trainingplan.html diff --git a/rowers/braintreestuff.py b/rowers/braintreestuff.py index 043542b9..00fc602f 100644 --- a/rowers/braintreestuff.py +++ b/rowers/braintreestuff.py @@ -183,7 +183,7 @@ def make_payment(rower,data): return False,0 amount = data['amount'] - amount = '{amount:.f2}'.format(amount=amount) + amount = '{amount}'.format(amount=amount) result = gateway.transaction.sale({ "amount": amount, @@ -200,13 +200,18 @@ def make_payment(rower,data): l = rower.user.last_name, ) + fakturoid_contact_id = fakturoid.get_contacts(rower) + if not fakturoid_contact_id: + fakturoid_contact_id = fakturoid.create_contact(rower) + id = fakturoid.create_invoice(rower,amount,transaction.id,dosend=True,contact_id=fakturoid_contact_id, + name='Rowsandall Purchase') job = myqueue(queuehigh,handle_send_email_transaction, name, rower.user.email, amount) - return amount,'' + return amount,True else: - return 0,'' + return 0,False def update_subscription(rower,data,method='up'): planid = data['plan'] diff --git a/rowers/fakturoid.py b/rowers/fakturoid.py index 551d582e..8fb46167 100644 --- a/rowers/fakturoid.py +++ b/rowers/fakturoid.py @@ -83,11 +83,14 @@ def create_contact(rower): # this should be triggered by a Braintree webhook def create_invoice(rower,amount,braintreeid,dosend=True, - contact_id=None): + contact_id=None,name=None): if not contact_id: contact_id = get_contacts(rower) + if not name: + name = 'Rowsandall Subscription' + with open('braintreewebhooks.log','a') as f: f.write('Creating invoice for contact iD '+str(contact_id)+'\n') @@ -104,7 +107,7 @@ def create_invoice(rower,amount,braintreeid,dosend=True, 'paid_amount': str(amount), 'status': 'paid', 'lines': [{ - 'name': 'Rowsandall Subscription', + 'name': name, 'quantity': '1', 'unit_price': str(amount), 'vat_rate': 0, diff --git a/rowers/forms.py b/rowers/forms.py index f2ad4f96..8606abe2 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -101,6 +101,19 @@ class BillingForm(forms.Form): paymenttype = forms.CharField(max_length=255,required=True) tac= forms.BooleanField(required=True,initial=False) +# TrainingPlanBillingForm form +class TrainingPlanBillingForm(forms.Form): + amount = FlexibleDecimalField(required=True,decimal_places=2, + max_digits=8) + plan = forms.IntegerField(widget=forms.HiddenInput()) + payment_method_nonce = forms.CharField(max_length=255,required=True) + paymenttype = forms.CharField(max_length=255,required=True) + enddate = forms.DateField(widget=forms.HiddenInput) + name = forms.CharField(max_length=255,required=False) + notes = forms.CharField(max_length=255,required=True) + status = forms.CharField(max_length=255,required=True) + tac= forms.BooleanField(required=True,initial=False) + # login form class LoginForm(forms.Form): diff --git a/rowers/models.py b/rowers/models.py index e744788a..f11ecefa 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1612,7 +1612,7 @@ class TrainingPlan(models.Model): rowers = models.ManyToManyField(Rower,related_name='planathletes', verbose_name='Athletes') manager = models.ForeignKey(Rower,related_name='planmanager',null=True,on_delete=models.SET_NULL) - name = models.CharField(max_length=150,blank=True) + name = models.CharField(max_length=150,blank=True, verbose_name="Plan Name") status = models.BooleanField(default=True,verbose_name='Active') target = models.ForeignKey(TrainingTarget,blank=True,null=True,on_delete=models.SET_NULL) startdate = models.DateField(default=current_day) diff --git a/rowers/templates/buy_trainingplan.html b/rowers/templates/buy_trainingplan.html new file mode 100644 index 00000000..6be1d6bf --- /dev/null +++ b/rowers/templates/buy_trainingplan.html @@ -0,0 +1,43 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Purchase Training Plan{% endblock title %} +{% load rowerfilters %} +{% block main %} + +

Purchase Training Plan

+ +
+ +
+ +{% endblock %} + +{% block sidebar %} +{% include 'menu_payments.html' %} +{% endblock %} diff --git a/rowers/templates/confirm_trainingplan.html b/rowers/templates/confirm_trainingplan.html new file mode 100644 index 00000000..af325e53 --- /dev/null +++ b/rowers/templates/confirm_trainingplan.html @@ -0,0 +1,114 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Purchase Training Plan{% endblock title %} +{% load rowerfilters %} +{% block main %} + +
+
+ +

Confirm Your Payment

+ +

Order Overview

+ +

+ Please refer to our terms and conditions for our + payments and refunds policy. Accepted payment methods are the payment methods offered + by + Braintree + through us. If you have any questions about our payments and refunds policy, please contact + us by email at support@rowsandall.com. +

+ +

+ Payments will be processed by Braintree (A PayPal service): +

+

+ + + +

+ + + +{% include 'braintreedropin.html' %} + + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_payments.html' %} +{% endblock %} diff --git a/rowers/templates/instantplan.html b/rowers/templates/instantplan.html index 2883a0c9..1f4a07e4 100644 --- a/rowers/templates/instantplan.html +++ b/rowers/templates/instantplan.html @@ -27,6 +27,11 @@

What the plan will achieve: {{ plan.target }}

{% endif %}

Weekly volume: {{ plan.hoursperweek }} hours per week over {{ plan.sessionsperweek }} sessions.

+ {% if plan.price == 0 %} +

Price: Free

+ {% else %} +

Price: {{ plan.price }}€

+ {% endif %}
  • @@ -37,12 +42,20 @@ You can select the end date manually or use the training target (if you have any), and the plan will start at the date it needs to complete in time.

    + {% if plan.price == 0 %}
    + {% else %} + + {% endif %} {{ form.as_table }}
    {% csrf_token %} + {% if plan.price == 0 %}

    + {% else %} +

    + {% endif %}
  • diff --git a/rowers/templates/instantplans.html b/rowers/templates/instantplans.html index 38a8146b..4d31c7ac 100644 --- a/rowers/templates/instantplans.html +++ b/rowers/templates/instantplans.html @@ -22,6 +22,11 @@

    Goal: {{ plan.goal }}

    {% endif %}

    {{ plan.hoursperweek }} hours per week over {{ plan.sessionsperweek }} sessions

    + {% if plan.price == 0 %} +

    Price: Free

    + {% else %} +

    Price: {{ plan.price }}€

    + {% endif %}
  • {% endfor %} diff --git a/rowers/urls.py b/rowers/urls.py index a67cc0ee..45d75a9f 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -719,7 +719,8 @@ urlpatterns = [ re_path(r'^me/cancelsubscription/(?P[\w\ ]+.*)/$',views.plan_tobasic_view,name='plan_tobasic_view'), re_path(r'^checkouts/$',views.checkouts_view,name='checkouts'), re_path(r'^upgradecheckouts/$',views.upgrade_checkouts_view,name='upgrade_checkouts'), - re_path(r'^downgradecheckouts/$',views.downgrade_checkouts_view,name='downgrade_checkouts'), + re_path(r'^upgradecheckouts/$',views.upgrade_checkouts_view,name='upgrade_checkouts'), + re_path(r'^purchasecheckouts/$',views.purchase_checkouts_view,name='purchase_checkouts_view'), re_path(r'^planrequired/',views.planrequired_view,name='planrequired_view'), re_path(r'^starttrial/$',views.start_trial_view,name='start_trial_view'), re_path(r'^startplantrial/$',views.start_plantrial_view,name='start_plantrial_view'), @@ -744,6 +745,8 @@ urlpatterns = [ re_path(r'^plans/$', views.rower_select_instantplan, name='rower_select_instantplan'), re_path(r'^plans/(?P[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})/$', views.rower_view_instantplan, name='rower_view_instantplan'), + re_path(r'^buyplan/(?P\d+)/$',views.buy_trainingplan_view,name='buy_trainingplan_view'), + re_path(r'^confirmpurchaseplan/(?P\d+)/$',views.confirm_trainingplan_purchase_view,name='confirm_trainingplan_purchase_view'), re_path(r'^addinstantplan/$', views.add_instantplan_view, name='add_instantplan_view'), re_path(r'^deleteplan/(?P\d+)/$',login_required( views.TrainingPlanDelete.as_view()),name='trainingplan_delete_view'), diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py index be456413..3d3435d5 100644 --- a/rowers/views/paymentviews.py +++ b/rowers/views/paymentviews.py @@ -84,6 +84,212 @@ def billing_view(request): 'planselectform':planselectform, }) +@login_required() +def buy_trainingplan_view(request,id=0): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = request.user.rower + + plan = get_object_or_404(InstantPlan,pk=id) + + if r.paymentprocessor != 'braintree': + messages.error(request,"This purchase is currently only available through BrainTree (by PayPal)") + + if id == 0 or id is None: + messages.error(request,"There was an error accessing this plan") + url = reverse('rower_view_instantplan',kwargs={ + 'id':plan.uuid, + }) + + return HttpResponseRedirect(url) + + if request.method == 'POST': + billingaddressform = RowerBillingAddressForm(instance=r) + form = TrainingPlanForm(request.POST,user=request.user) + if billingaddressform.is_valid(): + cd = billingaddressform.cleaned_data + for attr, value in cd.items(): + setattr(r, attr, value) + r.save() + + # redirect to payment confirmation view + if form.is_valid(): + cd = form.cleaned_data + + enddate = cd['enddate'] + rowers = cd['rowers'] + notes = cd['notes'] + status = cd['status'] + + # get target and set enddate + try: + target = cd['target'] + except KeyError: + try: + targetid = request.POST['target'] + + if targetid != '': + target = TrainingTarget.objects.get(id=int(targetid)) + else: + target = None + except KeyError: + target = None + + if target: + enddate = target.date + + pars = { + 'name':cd['name'], + 'enddate':enddate, + 'notes':notes, + 'status':status, + 'rower':rowers[0].id, + } + params = urllib.parse.urlencode(pars) + url = reverse('confirm_trainingplan_purchase_view',kwargs={'id':plan.id}) + url = url + "?%s" % params + return HttpResponseRedirect(url) + + else: + form = TrainingPlanForm(user=request.user) + billingaddressform = RowerBillingAddressForm(instance=r) + + return render(request, + 'buy_trainingplan.html', + { + 'rower':r, + 'plan':plan, + 'billingaddressform':billingaddressform, + 'form':form, + }) + +@login_required() +def purchase_checkouts_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = request.user.rower + + if request.method != 'POST': + url = reverse('rower_view_instantplan',kwargs={ + 'id':plan.uuid, + }) + return HttpResponseRedirect(url) + + form = TrainingPlanBillingForm(request.POST) + if form.is_valid(): + + data = form.cleaned_data + + plan = InstantPlan.objects.get(id=data['plan']) + authorizationstring = 'Bearer '+settings.WORKOUTS_FIT_TOKEN + url = settings.WORKOUTS_FIT_URL+"/trainingplan/"+str(plan.uuid) + headers = {'Authorization':authorizationstring} + response = requests.get(url=url,headers=headers) + if response.status_code != 200: + messages.error(request,"Could not connect to the training plan server") + return HttpResponseRedirect(reverse('rower_select_instantplan')) + + amount, success = braintreestuff.make_payment(r,data) + + if success: + messages.info(request,"Your payment was completed and the sessions are copied to your calendar") + plansteps = response.json() + name = data['name'] + enddate = data['enddate'] + notes = data['notes'] + status = data['status'] + startdate = enddate-datetime.timedelta(days=plan.duration) + p = TrainingPlan( + name=name, + #target=target, + manager=r, + startdate=startdate, + enddate=enddate,status=status, + notes=notes, + ) + + p.save() + + p.rowers.add(r) + + create_sessions_from_json(plansteps,[r],startdate,r.user) + + url = reverse('plannedsessions_view') + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + url = url+'?when='+timeperiod + + return HttpResponseRedirect(url) + else: + messages.error(request,"There was a problem with your payment") + url = reverse('rower_view_instantplan',kwargs={ + 'id':plan.uuid, + }) + return HttpResponseRedirect(url) + elif 'tac' not in request.POST: + try: + planid=int(request.POST['plan']) + enddate = request.POST['enddate'] + rower = r.id + # incomplete + except IndexError: + messages.error(request,"There was an error in the payment form") + url = reverse("purchase_checkouts_view") + return HttpResponseRedirect(url) + + url = reverse('rower_view_instantplan',kwargs={ + 'id':plan.uuid, + }) + return HttpResponseRedirect(url) + +@login_required() +def confirm_trainingplan_purchase_view(request,id = 0): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = request.user.rower + + plan = get_object_or_404(InstantPlan,pk=id) + + if r.paymentprocessor != 'braintree': + messages.error(request,"This purchase is currently only available through BrainTree (by PayPal)") + + if id == 0 or id is None: + messages.error(request,"There was an error accessing this plan") + url = reverse('rower_view_instantplan',kwargs={ + 'id':plan.uuid, + }) + + return HttpResponseRedirect(url) + + client_token = braintreestuff.get_client_token(r) + + enddate = request.GET.get('enddate',None) + name = request.GET.get('name','') + status = request.GET.get('status',True) + notes = request.GET.get('notes','') + if enddate is None: + messages.error(request,"There was an error accessing this plan") + url = reverse('rower_view_instantplan',kwargs={ + 'id':plan.uuid, + }) + + return render(request, + 'confirm_trainingplan.html', + { + 'plan':plan, + 'client_token':client_token, + 'rower':r, + 'enddate':enddate, + 'status':status, + 'name':name, + 'notes':notes, + }) + @login_required() def upgrade_view(request): if not PAYMENT_PROCESSING_ON: diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py index 4017c547..f97dc7f6 100644 --- a/rowers/views/planviews.py +++ b/rowers/views/planviews.py @@ -2473,9 +2473,6 @@ class PlannedSessionDelete(DeleteView): return obj -@user_passes_test(can_plan,login_url="/rowers/paidplans", - message="This functionality requires a Coach or Self-Coach plan", - redirect_field_name=None) def rower_view_instantplan(request,id='',userid=0): r = getrequestrower(request,userid=userid) if not id: @@ -2521,6 +2518,12 @@ def rower_view_instantplan(request,id='',userid=0): ).order_by("-date") if request.method == 'POST': + if not can_plan(request.user): + messages.error(request,'You must be on a paid plan to use this functionality') + url = reverse('rower_view_instantplan',kwargs={ + 'id':id, + }) + return HttpResponseRedirect(url) form = TrainingPlanForm(request.POST,user=request.user) if form.is_valid(): diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 5881d15d..28430fae 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -77,6 +77,7 @@ from rowers.forms import ( VideoAnalysisCreateForm,WorkoutSingleSelectForm, VideoAnalysisMetricsForm,SurveyForm,HistorySelectForm, StravaChartForm,FitnessFitForm,PerformanceManagerForm, + TrainingPlanBillingForm, ) from django.urls import reverse, reverse_lazy