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):
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+ | Plan | {{ plan.name }} |
+
+
+ | Total | € {{ plan.price|currency }}
+ |
+
+
+
+
+
+
+
+
+ | Street Address | {{ user.rower.street_address }} |
+
+
+ | City | {{ user.rower.city }} |
+
+
+ | Postal Code | {{ user.rower.postal_code }} |
+
+
+ | Country | {{ user.rower.country }}
+ |
+
+
+
+
+
+
+
+
+
+
+ After you hit the Purchase button, the transaction will be launched.
+ Please wait until the transaction completes. Do not click the
+ button twice. Do not close your browser window.
+
+
+
+
+{% 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 %}
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