diff --git a/rowers/admin.py b/rowers/admin.py
index 1614ae88..5be9aa7f 100644
--- a/rowers/admin.py
+++ b/rowers/admin.py
@@ -7,10 +7,12 @@ from .models import (
Team,TeamInvite,TeamRequest,
WorkoutComment,C2WorldClassAgePerformance,PlannedSession,
GeoCourse,GeoPolygon,GeoPoint,VirtualRace,VirtualRaceResult,
+ PaidPlan
)
# Register your models here so you can use them in the Admin module
+
# Rower details directly under the User
class RowerInline(admin.StackedInline):
model = Rower
@@ -19,8 +21,10 @@ class RowerInline(admin.StackedInline):
filter_horizontal = ('team','friends')
fieldsets = (
+ ('Billing Details',
+ {'fields':('street_address','city','postal_code','country','paymentprocessor','customer_id')}),
('Rower Plan',
- {'fields':('rowerplan','paymenttype','planexpires','teamplanexpires','clubsize','protrialexpires','plantrialexpires',)}),
+ {'fields':('paidplan','rowerplan','paymenttype','planexpires','teamplanexpires','clubsize','protrialexpires','plantrialexpires',)}),
('Rower Settings',
{'fields':
('gdproptin','gdproptindate','weightcategory','sex','adaptiveclass','birthdate','getemailnotifications',
@@ -118,6 +122,9 @@ class VirtualRaceAdmin(admin.ModelAdmin):
class VirtualRaceResultAdmin(admin.ModelAdmin):
list_display = ('race','userid','username','boattype','age','weightcategory')
search_fields = ['race__name','username']
+
+class PaidPlanAdmin(admin.ModelAdmin):
+ list_display = ('name','shortname','price','paymenttype','paymentprocessor','clubsize','external_id')
admin.site.unregister(User)
admin.site.register(User,UserAdmin)
@@ -135,3 +142,4 @@ admin.site.register(PlannedSession,PlannedSessionAdmin)
admin.site.register(GeoCourse, GeoCourseAdmin)
admin.site.register(VirtualRace, VirtualRaceAdmin)
admin.site.register(VirtualRaceResult, VirtualRaceResultAdmin)
+admin.site.register(PaidPlan,PaidPlanAdmin)
diff --git a/rowers/braintreestuff.py b/rowers/braintreestuff.py
new file mode 100644
index 00000000..7477ddbe
--- /dev/null
+++ b/rowers/braintreestuff.py
@@ -0,0 +1,390 @@
+import braintree
+from django.utils import timezone
+import datetime
+
+import django_rq
+queue = django_rq.get_queue('default')
+queuelow = django_rq.get_queue('low')
+queuehigh = django_rq.get_queue('low')
+
+from rowers.utils import myqueue
+from rowers.tasks import (
+ handle_send_email_transaction,
+ handle_send_email_subscription_update,
+ handle_send_email_subscription_create,
+ handle_send_email_failed_cancel,
+ )
+
+import pandas as pd
+
+from rowsandall_app.settings import (
+ BRAINTREE_MERCHANT_ID,BRAINTREE_PUBLIC_KEY,BRAINTREE_PRIVATE_KEY
+ )
+
+gateway = braintree.BraintreeGateway(
+ braintree.Configuration(
+ braintree.Environment.Sandbox,
+ merchant_id=BRAINTREE_MERCHANT_ID,
+ public_key=BRAINTREE_PUBLIC_KEY,
+ private_key=BRAINTREE_PRIVATE_KEY,
+ )
+)
+
+from rowers.models import Rower,PaidPlan
+from rowers.utils import ProcessorCustomerError
+
+def create_customer(rower,force=False):
+ if not rower.customer_id or force:
+ result = gateway.customer.create(
+ {
+ 'first_name':rower.user.first_name,
+ 'last_name':rower.user.last_name,
+ 'email':rower.user.email,
+ })
+ if not result.is_success:
+ 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
+
+def get_plans_costs():
+ plans = gateway.plan.all()
+
+ localplans = PaidPlan.object.filter(paymentprocessor='braintree')
+
+ for plan in localplans:
+ for btplan in btplans:
+ if int(btplan.id) == plan.external_id:
+ plan.price = float(x)
+ plan.save()
+
+ return plans
+
+def make_payment(rower,data):
+ nonce_from_the_client = data['payment_method_nonce']
+ amount = data['amount']
+ amount = '{amount:.f2}'.format(amount=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
+ name = '{f} {l}'.format(
+ f = rower.user.first_name,
+ l = rower.user.last_name,
+ )
+
+
+ job = myqueue(queuehigh,handle_send_email_transaction,
+ name, rower.user.email, amount)
+
+ return amount
+ else:
+ return 0,''
+
+def update_subscription(rower,data,method='up'):
+ planid = data['plan']
+ plan = PaidPlan.objects.get(id=planid)
+ nonce_from_the_client = data['payment_method_nonce']
+ amount = data['amount']
+ amount = '{amount:.2f}'.format(amount=amount)
+
+ gatewaydata = {
+ "price": amount,
+ "plan_id": plan.external_id,
+ "payment_method_nonce": nonce_from_the_client,
+ "options": {
+ "prorate_charges":True,
+ },
+ }
+
+ if plan.paymenttype == 'single':
+ gatewaydata['number_of_billing_cycles'] = 1
+ else:
+ gatewaydata['never_expires'] = True
+
+ result = gateway.subscription.update(
+ rower.subscription_id,
+ gatewaydata
+ )
+
+
+ if result.is_success:
+ rower.paidplan = plan
+ rower.planexpires = result.subscription.billing_period_end_date
+ rower.teamplanexpires = result.subscription.billing_period_end_date
+ rower.clubsize = plan.clubsize
+ rower.paymenttype = plan.paymenttype
+ rower.rowerplan = plan.shortname
+ rower.subscription_id = result.subscription.id
+ rower.save()
+ name = '{f} {l}'.format(
+ f = rower.user.first_name,
+ l = rower.user.last_name,
+ )
+
+ if method == 'up':
+ transactions = result.subscription.transactions
+
+ if transactions:
+ amount = transactions[0].amount
+ else:
+ amount = 0
+ else:
+ amount = 0
+
+
+ job = myqueue(queuehigh,
+ handle_send_email_subscription_update,
+ name, rower.user.email,
+ plan.name,
+ plan.paymenttype == 'recurring',
+ plan.price,
+ amount,
+ result.subscription.billing_period_end_date.strftime('%Y-%m-%d'),
+ method)
+
+ return True
+ else:
+ errors = result.errors.for_object("subscription")
+ codes = [str(e.code) for e in errors]
+ create_new = False
+ proceed_codes = ['81901','81910']
+ for c in codes:
+ if c in proceed_codes:
+ create_new = True
+
+ if create_new:
+ return create_subscription(rower,data)
+
+ return False
+
+ return False
+
+
+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 = result.subscription.billing_period_end_date
+ rower.teamplanexpires = result.subscription.billing_period_end_date
+ rower.clubsize = plan.clubsize
+ rower.paymenttype = plan.paymenttype
+ rower.rowerplan = plan.shortname
+ rower.subscription_id = result.subscription.id
+ rower.save()
+ name = '{f} {l}'.format(
+ f = rower.user.first_name,
+ l = rower.user.last_name,
+ )
+
+
+ recurring = plan.paymenttype == 'recurring',
+
+ job = myqueue(
+ queuehigh,
+ handle_send_email_subscription_create,
+ name, rower.user.email,
+ plan.name,
+ recurring,
+ plan.price,
+ plan.price,
+ result.subscription.billing_period_end_date.strftime('%Y-%m-%d')
+ )
+ return True
+ else:
+ return False
+
+
+ return False
+
+def cancel_subscription(rower,id):
+ themessages = []
+ errormessages = []
+ try:
+ result = gateway.subscription.cancel(id)
+ themessages.append("Subscription canceled")
+ except:
+ errormessages.append("We could not find the subscription record in our customer database. We have notified the site owner, who will contact you.")
+
+ name = '{f} {l}'.format(f = rower.user.first_name, l = rower.user.last_name)
+
+
+ job = myqueue(queuehigh,
+ handle_send_email_failed_cancel,
+ name, rower.user.email,rower.user.username,id)
+
+ return False, themessages, errormessages
+
+ basicplans = PaidPlan.objects.filter(price=0,paymentprocessor='braintree')
+ rower.paidplan = basicplans[0]
+ 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
+
+def get_transactions(start_date,end_date):
+ results = gateway.transaction.search(
+ braintree.TransactionSearch.created_at.between(
+ start_date,
+ end_date,
+ )
+ )
+
+ amounts = []
+ countries = []
+ card_countries = []
+ names = []
+ emails = []
+ dates = []
+ currencies = []
+ statuses = []
+ ids = []
+ usernames = []
+
+ for transaction in results:
+ try:
+ r = Rower.objects.filter(
+ customer_id=transaction.customer['id'],
+ paymentprocessor='braintree')[0]
+ countries.append(r.country)
+ names.append('{f} {l}'.format(
+ f = r.user.first_name,
+ l = r.user.last_name,
+ )
+ )
+ emails.append(r.user.email)
+ ids.append(r.id)
+ usernames.append(r.user.username)
+
+ except KeyError:
+ countries.append(
+ transaction.credit_card_details.country_of_issuance)
+ names.append('{f} {l}'.format(
+ f = transaction.customer['first_name'],
+ l = transaction.customer['last_name']
+ )
+ )
+ emails.append(transaction.customer.email)
+ ids.append(transaction.customer['id'])
+ usernames.append('unknown')
+
+
+ amounts.append(transaction.amount)
+ dates.append(transaction.created_at)
+ currencies.append(transaction.currency_iso_code)
+ card_countries.append(
+ transaction.credit_card_details.country_of_issuance)
+ statuses.append(transaction.status)
+
+
+ df = pd.DataFrame({
+ 'name':names,
+ 'email':emails,
+ 'date':dates,
+ 'amount':amounts,
+ 'currency':currencies,
+ 'country':countries,
+ 'card_country':card_countries,
+ 'status':statuses,
+ 'username':usernames,
+ 'user_id':ids,
+ }
+ )
+
+ return df
+
+
diff --git a/rowers/forms.py b/rowers/forms.py
index fc4de42a..f0145b7d 100644
--- a/rowers/forms.py
+++ b/rowers/forms.py
@@ -2,7 +2,8 @@ from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from rowers.models import (
Workout,Rower,Team,PlannedSession,GeoCourse,
- VirtualRace,VirtualRaceResult,IndoorVirtualRaceResult
+ VirtualRace,VirtualRaceResult,IndoorVirtualRaceResult,
+ PaidPlan
)
from rowers.rows import validate_file_extension,must_be_csv,validate_image_extension,validate_kml
from django.contrib.auth.forms import UserCreationForm
@@ -18,7 +19,12 @@ from django.forms import formset_factory
from utils import landingpages
from metrics import axes
-
+# BillingForm form
+class BillingForm(forms.Form):
+ amount = forms.FloatField(required=True)
+ plan = forms.IntegerField(widget=forms.HiddenInput())
+ payment_method_nonce = forms.CharField(max_length=255,required=True)
+
# login form
class LoginForm(forms.Form):
@@ -708,7 +714,38 @@ class StatsOptionsForm(forms.Form):
for type in mytypes.checktypes:
self.fields[type] = forms.BooleanField(initial=True,required=False)
+class PlanSelectForm(forms.Form):
+ plan = forms.ModelChoiceField(queryset=PaidPlan.objects.all(),
+ widget=forms.RadioSelect,required=True)
+ def __init__(self, *args, **kwargs):
+ paymentprocessor = kwargs.pop('paymentprocessor',None)
+ rower = kwargs.pop('rower',None)
+ includeall = kwargs.pop('includeall',False)
+ super(PlanSelectForm, self).__init__(*args, **kwargs)
+ self.fields['plan'].empty_label = None
+ if paymentprocessor:
+ self.fields['plan'].queryset = PaidPlan.objects.filter(
+ paymentprocessor=paymentprocessor
+ ).exclude(
+ shortname="basic"
+ ).order_by(
+ "price","clubsize","shortname"
+ )
+ if rower and not includeall:
+ try:
+ amount = rower.paidplan.price
+ except AttributeError:
+ amount = 0
+ self.fields['plan'].queryset = PaidPlan.objects.filter(
+ paymentprocessor=rower.paymentprocessor
+ ).exclude(
+ price__lte=amount
+ ).order_by(
+ "price","clubsize","shortname"
+ )
+
+
class CourseSelectForm(forms.Form):
course = forms.ModelChoiceField(queryset=GeoCourse.objects.all())
diff --git a/rowers/models.py b/rowers/models.py
index e48f252a..ab8f822f 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -17,6 +17,7 @@ import os
import twitter
import re
import pytz
+from django_countries.fields import CountryField
from scipy.interpolate import splprep, splev, CubicSpline
import numpy as np
@@ -536,6 +537,49 @@ weightcategories = (
)
+# Plan
+plans = (
+ ('basic','basic'),
+ ('pro','pro'),
+ ('plan','plan'),
+ ('coach','coach')
+)
+
+paymenttypes = (
+ ('single','single'),
+ ('recurring','recurring')
+)
+
+paymentprocessors = (
+ ('paypal','PayPal'),
+ ('braintree','BrainTree')
+ )
+
+class PaidPlan(models.Model):
+ shortname = models.CharField(max_length=50,choices=plans)
+ name = models.CharField(max_length=200)
+ 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')
+ paymenttype = models.CharField(
+ default='single',max_length=30,
+ verbose_name='Payment Type',
+ choices=paymenttypes,
+ )
+
+ clubsize = models.IntegerField(default=0)
+
+ def __unicode__(self):
+ return '{name} - {shortname} at {price:.2f} EURO ({paymenttype} payment)'.format(
+ name = self.name,
+ shortname = self.shortname,
+ price = self.price,
+ paymenttype = self.paymenttype,
+ paymentprocessor = self.paymentprocessor,
+ )
+
+
# Extension of User with rowing specific data
class Rower(models.Model):
adaptivetypes = mytypes.adaptivetypes
@@ -571,9 +615,43 @@ class Rower(models.Model):
('Yoga','Yoga'),
)
user = models.OneToOneField(User)
+
+ #billing details
+ country = CountryField(default=None, null=True, blank=True)
+ street_address = models.CharField(default='',blank=True,null=True,max_length=200)
+ city = models.CharField(default='',blank=True,null=True,max_length=200)
+ postal_code = models.CharField(default='',blank=True,null=True,max_length=200)
+
+ customer_id = models.CharField(default=None,null=True,blank=True,max_length=200)
+ subscription_id = models.CharField(default=None,null=True,
+ blank=True,max_length=200)
+
+ rowerplan = models.CharField(default='basic',max_length=30,
+ choices=plans)
+ paymenttype = models.CharField(
+ default='single',max_length=30,
+ verbose_name='Payment Type',
+ choices=paymenttypes,
+ )
+ paymentprocessor = models.CharField(max_length=50,
+ choices=paymentprocessors,
+ null=True,blank=True,
+ default='braintree')
+
+ paidplan = models.ForeignKey(PaidPlan,null=True,default=None)
+
+ planexpires = models.DateField(default=timezone.now)
+ teamplanexpires = models.DateField(default=timezone.now)
+ clubsize = models.IntegerField(default=0)
+ protrialexpires = models.DateField(blank=True,null=True)
+ plantrialexpires = models.DateField(blank=True,null=True)
+
+
+ # Privacy Data
gdproptin = models.BooleanField(default=False)
gdproptindate = models.DateTimeField(blank=True,null=True)
+
# Heart Rate Zone data
max = models.IntegerField(default=192,verbose_name="Max Heart Rate")
rest = models.IntegerField(default=48,verbose_name="Resting Heart Rate")
@@ -683,13 +761,6 @@ class Rower(models.Model):
blank=True,null=True)
runkeeper_auto_export = models.BooleanField(default=False)
- # Plan
- plans = (
- ('basic','basic'),
- ('pro','pro'),
- ('plan','plan'),
- ('coach','coach')
- )
privacychoices = (
('visible','Visible'),
@@ -704,23 +775,6 @@ class Rower(models.Model):
getimportantemails = models.BooleanField(default=True,
verbose_name='Get Important Emails')
- rowerplan = models.CharField(default='basic',max_length=30,
- choices=plans)
- paymenttype = models.CharField(
- default='single',max_length=30,
- verbose_name='Payment Type',
- choices=(
- ('single','single'),
- ('recurring','recurring')
- )
- )
-
- planexpires = models.DateField(default=timezone.now)
- teamplanexpires = models.DateField(default=timezone.now)
- clubsize = models.IntegerField(default=0)
- protrialexpires = models.DateField(blank=True,null=True)
- plantrialexpires = models.DateField(blank=True,null=True)
-
# Friends/Team
friends = models.ManyToManyField("self",blank=True)
@@ -2951,6 +3005,20 @@ class RowerImportExportForm(ModelForm):
'trainingpeaks_auto_export',
]
+# Form to collect rower's Billing Info
+class RowerBillingAddressForm(ModelForm):
+ class Meta:
+ model = Rower
+ fields = [
+ 'street_address',
+ 'city',
+ 'postal_code',
+ 'country'
+ ]
+
+ def __init__(self, *args, **kwargs):
+ super(RowerBillingAddressForm, self).__init__(*args, **kwargs)
+ self.fields['country'].required = True
# Form to set rower's Email and Weight category
diff --git a/rowers/payments.py b/rowers/payments.py
new file mode 100644
index 00000000..7e71aad5
--- /dev/null
+++ b/rowers/payments.py
@@ -0,0 +1,40 @@
+from rowers.models import Rower,PaidPlan
+
+# run once - copies plans to paypal
+def planstopaypal():
+ plans = PaidPlan.objects.all()
+
+ for plan in plans:
+ plan.pk = None
+ plan.paymentprocessor = 'paypal'
+ plan.external_id = None
+ plan.save()
+
+def initiaterowerplans():
+ rowers = Rower.objects.filter(paymenttype = 'recurring',paidplan = None)
+ for r in rowers:
+ r.paymentprocessor = 'paypal'
+ r.save()
+
+def setrowerplans():
+ rowers = Rower.objects.all()
+
+ for r in rowers:
+ paidplans = PaidPlan.objects.filter(
+ shortname = r.rowerplan,
+ paymenttype = r.paymenttype,
+ clubsize = r.clubsize,
+ paymentprocessor=r.paymentprocessor)
+
+ if paidplans:
+ r.paidplan = paidplans[0]
+ r.save()
+ else:
+ 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 rower.country != '':
+ return True
+
+ return False
+
diff --git a/rowers/tasks.py b/rowers/tasks.py
index 8601baa0..a848cd37 100644
--- a/rowers/tasks.py
+++ b/rowers/tasks.py
@@ -739,7 +739,139 @@ def handle_updatedps(useremail, workoutids, debug=False,**kwargs):
return 1
-# send email when a breakthrough workout is uploaded
+@app.task
+def handle_send_email_transaction(
+ username, useremail, amount, **kwargs):
+
+ if 'debug' in kwargs:
+ debug = kwargs['debug']
+ else:
+ debug = True
+
+ subject = "Rowsandall Payment Confirmation"
+
+ from_email = 'Rowsandall
+ User {{ name }} tried to cancel his subscription with id "{{ id }}" on {{ siteurl }} but failed.
+
+ User name: {{ username }}
+
+ User email: {{ email }}
+
+ Best Regards, the Rowsandall Team
+
+ Thank you for changing to {{ user.rower.paidplan.name }}. You're all settled.
+ 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 %}
+
+ Upgrade
+
+
+
+{% endblock %}
+
+{% block sidebar %}
+{% include 'menu_profile.html' %}
+{% endblock %}
+
diff --git a/rowers/templates/braintreedropin.html b/rowers/templates/braintreedropin.html
new file mode 100644
index 00000000..9ffe456b
--- /dev/null
+++ b/rowers/templates/braintreedropin.html
@@ -0,0 +1,25 @@
+
+
diff --git a/rowers/templates/cancel_subscription_fail_email.html b/rowers/templates/cancel_subscription_fail_email.html
new file mode 100644
index 00000000..cd4cac32
--- /dev/null
+++ b/rowers/templates/cancel_subscription_fail_email.html
@@ -0,0 +1,21 @@
+{% extends "emailbase.html" %}
+
+{% block body %}
+Downgrade
+
+
+
+{% endblock %}
+
+{% block sidebar %}
+{% include 'menu_profile.html' %}
+{% endblock %}
+
diff --git a/rowers/templates/downgrade_completed.html b/rowers/templates/downgrade_completed.html
new file mode 100644
index 00000000..1fdde09b
--- /dev/null
+++ b/rowers/templates/downgrade_completed.html
@@ -0,0 +1,26 @@
+{% extends "newbase.html" %}
+{% block title %}Rowsandall Paid Membership{% endblock title %}
+{% load rowerfilters %}
+{% block main %}
+
+Your Change was completed
+
+Confirm Your Changes
+
+Order Overview
+
+
+
+
+
+
+
+ Plan {{ plan.name }}
+
+
+ Payment Type {{ plan.paymenttype }}
+
+
+ Billing Cycle 1 year
+
+
+
+ Total € {{ plan.price|currency }}
+ {% if plan.paymenttype == 'recurring' %}
+ /year
+ {% endif %}
+
+
+
| Street Address | {{ user.rower.street_address }} | +
|---|---|
| City | {{ user.rower.city }} | +
| Postal Code | {{ user.rower.postal_code }} | +
| Country | {{ user.rower.country }} + | +
+ Change Downgrade +
+ Your downgrade will be effective immediately. You will not be charged. +
+{% if user|team_members %} diff --git a/rowers/templates/paidplans.html b/rowers/templates/paidplans.html new file mode 100644 index 00000000..bbd4789a --- /dev/null +++ b/rowers/templates/paidplans.html @@ -0,0 +1,293 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Paid Membership{% endblock title %} +{% load rowerfilters %} +{% block main %} + +
Rowsandall.com offers free data and analysis for rowers, by rowers. + Of course, offering this service is not free. To help cover the + hosting costs, we have created paid plans offering extended + functionality. +
+ ++
| + | BASIC | +PRO | +SELF-COACH | +COACH | +|||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Basic rowing metrics (spm, time, distance, heart rate, power) | +✔ | +✔ | +✔ | +✔ | +|||||||||||||||
| Manual Import, Export, Synchronization and download of all your data | +✔ | +✔ | +✔ | +✔ | +|||||||||||||||
| Automatic Synchronization with other fitness sites | ++ | ✔ | +✔ | +✔ | +|||||||||||||||
| Heart rate and power zones | +✔ | +✔ | +✔ | +✔ | +|||||||||||||||
| Ranking Pieces, Stroke Analysis | +✔ | +✔ | +✔ | +✔ | +|||||||||||||||
| Advanced Analysis (Critical Power, Stats, Box Chart, Trend Flex) | ++ | ✔ | +✔ | +✔ | +|||||||||||||||
| Compare Workouts | ++ | ✔ | +✔ | +✔ | +|||||||||||||||
| Empower Stroke Profile | ++ | ✔ | +✔ | +✔ | +|||||||||||||||
| Sensor Fusion, Split Workout, In-stroke metrics | ++ | ✔ | +✔ | +✔ | +|||||||||||||||
| Create Training plans, tests and challenges for yourself. Track your performance + against plan. | ++ | + | ✔ | +✔ | +|||||||||||||||
| Create Training plans, tests and challenges for your athletes. Track their performance + against plan. | ++ | + | + | ✔ | +|||||||||||||||
| Create and manage teams. | ++ | + | + | ✔ | +|||||||||||||||
| Manage your athlete's workouts | ++ | + | + | ✔ | +|||||||||||||||
| Pricing | +FREE | +From 15€/year | +From 65€/year | +From 90€/year | +|||||||||||||||
| Your current plan | +
+ {% if rower.rowerplan == 'basic' %}
+ BASIC+ {% else %} + + {% endif %} + |
+ + {% if rower.rowerplan == 'pro' %} + PRO + {% else %} + + {% endif %} + | ++ {% if rower.rowerplan == 'plan' %} + SELF-COACH + {% else %} + + {% endif %} + | ++ {% if rower.rowerplan == 'coach' %} + COACH + {% else %} + + {% endif %} + | +|||||||||||||||
| + Available trials + | ++ + | ++ {% if user.is_anonymous %} + + {% elif rower and rower.rowerplan == 'basic' and rower.protrialexpires|date_dif == 1 %} + + {% else %} + + {% endif %} + | ++ {% if user.is_anonymous %} + + {% elif rower and rower.rowerplan == 'basic' and rower.plantrialexpires|date_dif == 1 %} + + {% else %} + + {% endif %} + | ++ + | +|||||||||||||||
| + Available upgrades + | ++ + | + {% if user.is_anonymous %} ++ + | + {% elif rower and rower.rowerplan == 'basic' %} ++ + | + {% elif rower and rower.rowerplan == 'pro' %} ++ | + + | + {% elif rower and rower.rowerplan == 'plan' %} ++ | + | + + | + {% elif rower and rower.rowerplan == 'coach' and rower.clubsize < 100 %} ++ | + | + + | + {% else %} ++ + | + {% endif %} +|||||||
The Coach plan functionality listed is available to the coach only. Individual athletes + can purchase upgrades to "Pro" and "Self-Coach" plans. +
+ +Rowsandall.com's Training Planning functionality + is part of the paid "Self-Coach" and "Coach" plans.
+ +On the "Self-Coach" plan, you can plan your own sessions.
+ +On the "Coach" plan, you can establish teams, see workouts done by + athletes on your team, and plan individual and group sessions for your + athletes. +
+ +If you would like to find a coach who helps you plan your training + through rowsandall.com, contact me throught the contact form.
+ ++ 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..f9748ccf --- /dev/null +++ b/rowers/templates/paymentconfirm.html @@ -0,0 +1,89 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Paid Membership{% endblock title %} +{% load rowerfilters %} +{% block main %} + ++
| Plan | {{ plan.name }} | +
|---|---|
| Payment Type | {{ plan.paymenttype }} | +
| Plan Duration | 1 year starting today | +
| Total | € {{ plan.price|currency }} + {% if plan.paymenttype == 'recurring' %} + /year + {% endif %} + | +
+
| Street Address | {{ user.rower.street_address }} | +
|---|---|
| City | {{ user.rower.city }} | +
| Postal Code | {{ user.rower.postal_code }} | +
| Country | {{ user.rower.country }} + | +
+ Change Order +
Dear {{ name }},
+ ++ Thank you. We have received the payment of € {{ amount }} for Rowsandall related services. +
+ ++ Please contact our customer service by replying to this email if you have any further + questions regarding the payment. +
+ ++ Best Regards, the Rowsandall Team +
+{% endblock %} + diff --git a/rowers/templates/payments.html b/rowers/templates/payments.html new file mode 100644 index 00000000..9c248bcb --- /dev/null +++ b/rowers/templates/payments.html @@ -0,0 +1,296 @@ + +{% extends "newbase.html" %} +{% block title %}Rowsandall Pro Membership{% endblock title %} +{% block main %} +{% load rowerfilters %} + +Donations are welcome to keep this web site going. To help cover the hosting
+ costs, I have created several paid plans offering advanced functionality.
+ Once I process your
+ donation, I will give you access to some special
features on this
+ website.
The following table gives an overview of the different plans. As we are + constantly developing new functionality, the table might be slightly outdated. Don't + hesitate to contact us.
+ +The Pro membership is open for a free 14 day trial
++
| + | BASIC | +PRO | +SELF-COACH | +COACH | +
|---|---|---|---|---|
| Basic rowing metrics (spm, time, distance, heart rate, power) | +✔ | +✔ | +✔ | +✔ | +
| Manual Import, Export, Synchronization and download of all your data | +✔ | +✔ | +✔ | +✔ | +
| Automatic Synchronization with other fitness sites | ++ | ✔ | +✔ | +✔ | +
| Heart rate and power zones | +✔ | +✔ | +✔ | +✔ | +
| Ranking Pieces, Stroke Analysis | +✔ | +✔ | +✔ | +✔ | +
| Advanced Analysis (Critical Power, Stats, Box Chart, Trend Flex) | ++ | ✔ | +✔ | +✔ | +
| Compare Workouts | ++ | ✔ | +✔ | +✔ | +
| Empower Stroke Profile | ++ | ✔ | +✔ | +✔ | +
| Sensor Fusion, Split Workout, In-stroke metrics | ++ | ✔ | +✔ | +✔ | +
| Create Training plans, tests and challenges for yourself. Track your performance + against plan. | ++ | + | ✔ | +✔ | +
| Create Training plans, tests and challenges for your athletes. Track their performance + against plan. | ++ | + | + | ✔ | +
| Create and manage teams. | ++ | + | + | ✔ | +
| Manage your athlete's workouts | ++ | + | + | ✔ | +
The Coach plan functionality listed is available to the coach only. Individual athletes + can purchase upgrades to "Pro" and "Self-Coach" plans. +
+ +Rowsandall.com's Training Planning functionality + is part of the paid "Self-Coach" and "Coach" plans.
+ +On the "Self-Coach" plan, you can plan your own sessions.
+ +On the "Coach" plan, you can establish teams, see workouts done by + athletes on your team, and plan individual and group sessions for your + athletes. +
+ +If you would like to find a coach who helps you plan your training + through rowsandall.com, contact me throught the contact form.
+ + {% if user.rower.rowerplan == 'basic' and user.rower.protrialexpires|date_dif == 1 %} ++ You qualify for a 14 day free trial. No credit card needed. + Try out Pro or Self-Coach membership for two weeks. Click the button below to + sign up for the trial. After your trial period expires, you will be + automatically reset to the Basic plan, unless you upgrade to Pro. +
+Yes, I want to try Pro membership for 14 days for free. No strings attached.
+Yes, I want to try Self-Coach membership for 14 days for free. No strings attached.
+ {% endif %} +Click on the PayPal button to pay for your Pro membership. Before you pay, please register for the free Basic membership and add your user name to the form. + Your payment will be valid for one year. + You will be taken to the secure PayPal payment site. +
+You need a Paypal account for this. This is plan will automatically renew each year.
++
+ +Only a credit card needed. Will not automatically renew
+ ++
+ +After you do the payment, we will manually change your membership to + "Pro". Depending on our availability, this may take some time + (typically one working day). Don't hesitate to contact us + if you have any questions at this stage.
+ +If, for any reason, you are not happy with your Pro membership, please let me know through the contact form. I will contact you as soon as possible to discuss how we can make things better.
+ +If, for any reason, you are not happy with your Pro membership, please let me know through the contact form. I will contact you as soon as possible to discuss how we can make things better.
+ diff --git a/rowers/templates/rower_form.html b/rowers/templates/rower_form.html index c5b82b61..b58f18fb 100644 --- a/rowers/templates/rower_form.html +++ b/rowers/templates/rower_form.html @@ -14,7 +14,7 @@{% if rower.user == user %} - Password Change + Password Change {% else %} {% endif %} @@ -34,29 +34,49 @@ {{ userform.as_table }} {{ accountform.as_table }}
+ Upgrade +
{% else %} - ++ +
{% endif %} - + {% if rower.rowerplan != 'basic' and rower.user == user %} + + {% endif %} + {% if rower.user == user %}- Download your data + Download your data
- Deactivate Account + Deactivate Account
Delete Account @@ -79,7 +99,7 @@
Dear {{ name }},
+ ++ Thank you. We have received the payment of € {{ amount }} for your new + subscription to the Rowsandall paid plan "{{ planname }}". +
+ +{% if recurring %} ++ Your next charge is due on {{ end_of_billing_period }}. We will charge your {{ paymentmethod }} + on that date. +
+ ++ The subscription will keep running until you change or stop it. At any point in time you + can change the automatically renewing subscription to a "one year only" subscription through + the upgrade page. On this page, you can also + upgrade your subscription. +
+ +{% else %} ++ This one year subscription will automatically end on {{ end_of_billing_period }}. You can + renew your subscription after that. +
+ ++ At any point in time, you can change your subscription to an automatically renewing subscription. + You can do this on the upgrade page. + Here, you can also upgrade your subscription. +
+ +{% endif %} + ++ Upgrades in the middle of a billing cycle will be charged pro-rated. For the current billing + cycle, you will only be charged for the price difference for the remaining fraction of the + billing cycle. If you downgrade to a lower cost subscription, the pro-rated difference will be + used as a credit, lowering the amount charged on the next billing cycle. +
+ ++ You can stop the subscription through + the subscription management page. The + subscription will be stopped immediately without a refund. +
+ ++ Please contact our customer service by replying to this email if you have any further + questions regarding your subscription. +
+ ++ Best Regards, the Rowsandall Team +
+{% endblock %} + diff --git a/rowers/templates/subscription_create_notification.html b/rowers/templates/subscription_create_notification.html new file mode 100644 index 00000000..37a8c8eb --- /dev/null +++ b/rowers/templates/subscription_create_notification.html @@ -0,0 +1,30 @@ +{% extends "emailbase.html" %} + +{% block body %} +User {{ name }} has created a subscription.
+ ++ New plan: "{{ planname }}". +
+ +{% if recurring %} ++ The subscription cost is €{{ price }} per year. + The next charge is due on {{ end_of_billing_period }}. on that date. +
+{% else %} ++ The subscription cost is €{{ price }}. The subscription ends on {{ end_of_billing_period }} +
+{% endif %} + ++ Amount charged: €{{ amount }} +
+ + ++ Best Regards, the Rowsandall Team +
+{% endblock %} + diff --git a/rowers/templates/subscription_downgrade_email.html b/rowers/templates/subscription_downgrade_email.html new file mode 100644 index 00000000..789b48ae --- /dev/null +++ b/rowers/templates/subscription_downgrade_email.html @@ -0,0 +1,62 @@ +{% extends "emailbase.html" %} + +{% block body %} +Dear {{ name }},
+ ++ Thank you. You have successfully changed your plan to "{{ planname }}". +
+ +{% if recurring %} ++ The subscription cost is €{{ price }} per year. + Your next charge is due on {{ end_of_billing_period }}. We will charge you automatically + on that date. Because you downgraded, you have a credit on your account which will be + used before charging your payment method. +
+ ++ The subscription will keep running until you change or stop it. At any point in time you + can change the automatically renewing subscription to a "one year only" subscription through + the upgrade page. On this page, you can also + upgrade your subscription. +
+ +{% else %} ++ The price of the subscription is €{{ price }}. + This one year subscription will automatically end on {{ end_of_billing_period }}. You can + renew your subscription after that. Because you downgraded, you have a credit on your + account which will be used for future subscriptions or upgrades. +
+ ++ At any point in time, you can change your subscription to an automatically renewing subscription. + You can do this on the upgrade page. + Here, you can also upgrade your subscription. +
+{% endif %} + ++ Upgrades in the middle of a billing cycle are charged pro-rated. For the current billing + cycle, you have only been charged for the price difference for the remaining fraction of the + billing cycle. If you downgraded to a lower cost subscription, the pro-rated difference will be + used as a credit, lowering the amount charged on the next billing cycle. +
+ ++ You can stop the subscription through + the subscription management page. The + subscription will be stopped immediately without a refund. +
+ ++ Please contact our customer service by replying to this email if you have any further + questions regarding your subscription. +
+ ++ Best Regards, the Rowsandall Team +
+{% endblock %} + diff --git a/rowers/templates/subscription_downgrade_notification.html b/rowers/templates/subscription_downgrade_notification.html new file mode 100644 index 00000000..dce13caf --- /dev/null +++ b/rowers/templates/subscription_downgrade_notification.html @@ -0,0 +1,26 @@ +{% extends "emailbase.html" %} + +{% block body %} +User {{ name }} has downgraded his subscription.
+ ++ New plan: "{{ planname }}". +
+ +{% if recurring %} ++ The subscription cost is €{{ price }} per year. + The next charge is due on {{ end_of_billing_period }}. on that date. +
+{% else %} ++ The subscription cost is €{{ price }}. The subscription ends on {{ end_of_billing_period }} +
+{% endif %} + + ++ Best Regards, the Rowsandall Team +
+{% endblock %} + diff --git a/rowers/templates/subscription_update_email.html b/rowers/templates/subscription_update_email.html new file mode 100644 index 00000000..c50e1b08 --- /dev/null +++ b/rowers/templates/subscription_update_email.html @@ -0,0 +1,63 @@ +{% extends "emailbase.html" %} + +{% block body %} +Dear {{ name }},
+ ++ Thank you. We have received the payment of € {{ amount }} for + your updated Rowsandall subscription. + You are now on the Rowsandall paid plan "{{ planname }}". +
+ +{% if recurring %} ++ The subscription cost is €{{ price }} per year. + Your next charge is due on {{ end_of_billing_period }}. We will charge you automatically + on that date. +
+ ++ The subscription will keep running until you change or stop it. At any point in time you + can change the automatically renewing subscription to a "one year only" subscription through + the upgrade page. On this page, you can also + upgrade your subscription. +
+ +{% else %} ++ The price of the subscription is €{{ price }}. You have paid €{{ amount }} as a + prorated cost of your upgrade. + This one year subscription will automatically end on {{ end_of_billing_period }}. You can + renew your subscription after that. +
+ ++ At any point in time, you can change your subscription to an automatically renewing subscription. + You can do this on the upgrade page. + Here, you can also upgrade your subscription. +
+{% endif %} + ++ Upgrades in the middle of a billing cycle are charged pro-rated. For the current billing + cycle, you have only been charged for the price difference for the remaining fraction of the + billing cycle. If you downgraded to a lower cost subscription, the pro-rated difference will be + used as a credit, lowering the amount charged on the next billing cycle. +
+ ++ You can stop the subscription through + the subscription management page. The + subscription will be stopped immediately without a refund. +
+ ++ Please contact our customer service by replying to this email if you have any further + questions regarding your subscription. +
+ ++ Best Regards, the Rowsandall Team +
+{% endblock %} + diff --git a/rowers/templates/subscription_update_notification.html b/rowers/templates/subscription_update_notification.html new file mode 100644 index 00000000..1df49f73 --- /dev/null +++ b/rowers/templates/subscription_update_notification.html @@ -0,0 +1,30 @@ +{% extends "emailbase.html" %} + +{% block body %} +User {{ name }} has updated his subscription.
+ ++ New plan: "{{ planname }}". +
+ +{% if recurring %} ++ The subscription cost is €{{ price }} per year. + The next charge is due on {{ end_of_billing_period }}. on that date. +
+{% else %} ++ The subscription cost is €{{ price }}. The subscription ends on {{ end_of_billing_period }} +
+{% endif %} + ++ Amount charged: €{{ amount }} +
+ + ++ Best Regards, the Rowsandall Team +
+{% endblock %} + 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 %} + ++
| Subscription | Next Billing Date | Price | + |
|---|---|---|---|
| + {{ subscription|lookup:"plan" }} + | ++ {{ subscription|lookup:"end_date" }} + | ++ {{ subscription|lookup:"price" }} € + | ++ Stop this plan + | +
+
+ By clicking on the link to stop the plan, you will downgrade to the Basic plan. + Future payments will be stopped. +
+ {% else %} ++ You don't have any subscriptions or your subscriptions cannot be automatically stopped + from the site. +
++ If you have paid through PayPal, log in to your PayPal account and cancel the recurring payment + there. We will manually downgrade your subscription. +
++ If you have questions, don't hesitate to contact us. +
+ {% endif %} ++
| Plan | {{ plan.name }} | +
|---|---|
| Payment Type | {{ plan.paymenttype }} | +
| Billing Cycle | 1 year | +
| Total | € {{ plan.price|currency }} + {% if plan.paymenttype == 'recurring' %} + /year + {% endif %} + | +
+
| Street Address | {{ user.rower.street_address }} | +
|---|---|
| City | {{ user.rower.city }} | +
| Postal Code | {{ user.rower.postal_code }} | +
| Country | {{ user.rower.country }} + | +
+ Change Upgrade +
+ Your upgrade will be effective immediately. For the current billing + cycle, you will be charged for a prorated amount. For example, when + you upgrade from a 15€ plan to a 65€ plan (a difference of + 50€) in the 6th month of the 12 month billing cycle, you + will be charged 35€. +
+