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 ' + + d = { + 'name': username, + 'siteurl': siteurl, + 'amount': amount + } + + res = send_template_email(from_email,[useremail], + subject, + 'paymentconfirmationemail.html', + d, **kwargs) + + return 1 + +@app.task +def handle_send_email_failed_cancel( + name, email, username, id, **kwargs): + + if 'debug' in kwargs: + debug = kwargs['debug'] + else: + debug = True + + subject = "Rowsandall Subscription Cancellation Error" + + from_email = 'Rowsandall ' + + d = { + 'name': name, + 'siteurl': siteurl, + 'email': email, + 'username': username, + 'id': id, + } + + res = send_template_email(from_email,["support@rowsandall.com"], + subject, + 'cancel_subscription_fail_email.html', + d, **kwargs) + + return 1 + + +@app.task +def handle_send_email_subscription_update( + username, useremail, planname, recurring, price, amount, + end_of_billing_period, method, **kwargs): + + if 'debug' in kwargs: + debug = kwargs['debug'] + else: + debug = True + + + from_email = 'Rowsandall ' + + d = { + 'name': username, + 'siteurl': siteurl, + 'amount': amount, + 'price':price, + 'planname': planname, + 'recurring': recurring, + 'end_of_billing_period': end_of_billing_period, + } + + if method == 'down': + template_name = 'subscription_downgrade_email.html' + notification_template_name = 'subscription_downgrade_notification.html' + subject = "Rowsandall Change Confirmation" + else: + template_name = 'subscription_update_email.html' + notification_template_name = 'subscription_update_notification.html' + subject = "Rowsandall Payment Confirmation" + + res = send_template_email(from_email,[useremail], + subject, + template_name, + d, **kwargs) + + res = send_template_email(from_email,['info@rowsandall.com'], + 'Subscription Update Notification', + template_name, + d, **kwargs) + + return 1 + +@app.task +def handle_send_email_subscription_create( + username, useremail, planname, recurring, price, amount, + end_of_billing_period, **kwargs): + + if 'debug' in kwargs: + debug = kwargs['debug'] + else: + debug = True + + subject = "Rowsandall Payment Confirmation" + + from_email = 'Rowsandall ' + + d = { + 'name': username, + 'siteurl': siteurl, + 'amount': amount, + 'price':price, + 'planname': planname, + 'end_of_billing_period': end_of_billing_period, + 'recurring': recurring, + } + + res = send_template_email(from_email,[useremail], + subject, + 'subscription_create_email.html', + d, **kwargs) + + res = send_template_email(from_email,['info@rowsandall.com'], + 'Subscription Update Notification', + 'subscription_create_notification.html', + d, **kwargs) + return 1 @app.task def handle_sendemail_raceregistration( diff --git a/rowers/templates/billing.html b/rowers/templates/billing.html new file mode 100644 index 00000000..bf4a358f --- /dev/null +++ b/rowers/templates/billing.html @@ -0,0 +1,36 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Paid Membership{% endblock title %} +{% load rowerfilters %} +{% block main %} + +

Upgrade

+ +
+
    +
  • +

    Fill in Billing Details

    +

    For tax reasons, we need your country of residence

    + + {{ billingaddressform.as_table }} +
    +
  • +
  • +

    Choose your Plan

    + + {{ planselectform.as_table }} +
    +
  • +
  • + {% csrf_token %} + + You will be able to review your order before purchase. +
  • +
+
+ +{% 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 %} +

+ 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 +

+{% endblock %} + diff --git a/rowers/templates/downgrade.html b/rowers/templates/downgrade.html new file mode 100644 index 00000000..091b0dfe --- /dev/null +++ b/rowers/templates/downgrade.html @@ -0,0 +1,52 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Paid Membership{% endblock title %} +{% load rowerfilters %} +{% block main %} + +

Downgrade

+ +
+
    +
  • +

    Billing Details

    +

    For tax reasons, we need your country of residence. You should + update this when it is incorrect.

    + + {{ billingaddressform.as_table }} +
    +
  • +
  • +

    Choose your Plan

    + + {{ planselectform.as_table }} +
    +
  • +
  • +

    + Your downgrade will be effective immediately. + The price difference for the current billing cycle will + be credited to your next charge (prorated). For example, + when you downgrade from a 65€ plan to a 15€ plan + (50€ difference), in the 6th month of the 12 month + billing cycle, you will have a credit of 25€ which + will be used for the next billing cycles. +

    + +

    + Looking for the downgrade option? +

    +
  • +
  • + {% csrf_token %} + + You will be able to review your order before purchase. +
  • +
+
+ +{% 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

+ +

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

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

Confirm Your Changes

+ +

Order Overview

+ +
    +
  • +

    + + + + + + + + + + + + + + + +
    Plan{{ plan.name }}
    Payment Type{{ plan.paymenttype }}
    Billing Cycle1 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 +

  • +
  • +
    +
    + +
    +
    +
    +
    + + + + {% csrf_token %} + +
    +
  • +
  • +

    + Your downgrade will be effective immediately. You will not be charged. +

    +
  • +
+ +{% include 'braintreedropin.html' %} + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_profile.html' %} +{% endblock %} + diff --git a/rowers/templates/menu_profile.html b/rowers/templates/menu_profile.html index d3dd90b0..90bd73b9 100644 --- a/rowers/templates/menu_profile.html +++ b/rowers/templates/menu_profile.html @@ -27,8 +27,16 @@  Manage Workflow + {% if user.is_authenticated and user.is_staff %} +
  • + +  Transactions + +
  • + {% endif %} + {% if user.is_authenticated and user|is_manager %}

     

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

    Paid Membership Plans

    + +
      +
    • +

      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. +

      + +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if rower %} + + + + + + + + {% endif %} + + + + + + + + + + + {% 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 %} + + +
       BASICPROSELF-COACHCOACH
      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   
      PricingFREEFrom 15€/yearFrom 65€/yearFrom 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 + +   + + + + +   + +    + +    + + +   +
      +

      + +

      Coach and Self-Coach Membership

      + +

      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.

      + +
    • +
    + +{% endblock %} + +{% block sidebar %} +{% include 'menu_help.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..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 %} + +

    Confirm Your Payment

    + +

    Order Overview

    + +
      +
    • +

      + + + + + + + + + + + + + + + +
      Plan{{ plan.name }}
      Payment Type{{ plan.paymenttype }}
      Plan Duration1 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 +

    • +
    • +
      +
      + +
      +
      +
      +
      + + + + {% csrf_token %} + +
      +
    • +
    + +{% include 'braintreedropin.html' %} + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_profile.html' %} +{% endblock %} + diff --git a/rowers/templates/paymentconfirmationemail.html b/rowers/templates/paymentconfirmationemail.html new file mode 100644 index 00000000..0bc62f45 --- /dev/null +++ b/rowers/templates/paymentconfirmationemail.html @@ -0,0 +1,19 @@ +{% extends "emailbase.html" %} + +{% block body %} +

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

    Payments

    + +
      +
    • +

      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

      +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
       BASICPROSELF-COACHCOACH
      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   
      +

      +

      Coach and Self-Coach Membership

      + +

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

      Free Trial

      +

      + 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. +

      +

      Recurring Payment

      +

      You need a Paypal account for this. This is plan will automatically renew each year.

      +

      +

      + + + + + + + + + + + + + + + +
      + Plans +
      + +
      + Your User Name +
      + +
      + + + +
      +

      +

      One Year Subscription

      +

      Only a credit card needed. Will not automatically renew

      + +

      +

      + + + + + +
      Plans
      Your User Name
      + + + +
      +

      +

      Payment Processing

      +

      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.

      + +

      BrainTree Experimental Corner

      + +
      +
      + + +
      +
      +
      +
      + + + {% csrf_token %} + +
      + + + + +
    • +
    + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_help.html' %} +{% endblock %} + +{% block scripts %} +{% endblock %} + diff --git a/rowers/templates/promembership.html b/rowers/templates/promembership.html index 201d3044..391d9f51 100644 --- a/rowers/templates/promembership.html +++ b/rowers/templates/promembership.html @@ -232,6 +232,7 @@ 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.

    + 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 @@

    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.clubsize < 100 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/subscription_create_email.html b/rowers/templates/subscription_create_email.html new file mode 100644 index 00000000..c312a6b1 --- /dev/null +++ b/rowers/templates/subscription_create_email.html @@ -0,0 +1,60 @@ +{% extends "emailbase.html" %} + +{% block body %} +

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

    Cancel Subscriptions

    + +
      +
    • + {% if subscriptions %} +

      + + + + + + + + {% for subscription in subscriptions %} + + + + + + + {% endfor %} + +
      SubscriptionNext Billing DatePrice 
      + {{ 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 %} +
    • +
    + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_profile.html' %} +{% endblock %} + diff --git a/rowers/templates/transactions.html b/rowers/templates/transactions.html new file mode 100644 index 00000000..08cd6a0a --- /dev/null +++ b/rowers/templates/transactions.html @@ -0,0 +1,24 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block main %} +

    Download Transactions

    + +
    + + {{ dateform.as_table }} +
    + {% csrf_token %} + +
    + +{% endblock %} + +{% block sideheader %} +

    Profile

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

    Upgrade

    + +
    +
      +
    • +

      Billing Details

      +

      For tax reasons, we need your country of residence. You should + update this when it is incorrect.

      + + {{ billingaddressform.as_table }} +
      +
    • +
    • +

      Choose your Plan

      + + {{ planselectform.as_table }} +
      +
    • +
    • +

      + 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€. +

      +

      + Looking for the downgrade option? +

      +
    • +
    • + {% csrf_token %} + + You will be able to review your order before purchase. +
    • +
    +
    + +{% endblock %} + +{% block sidebar %} +{% include 'menu_profile.html' %} +{% endblock %} + diff --git a/rowers/templates/upgradeconfirm.html b/rowers/templates/upgradeconfirm.html new file mode 100644 index 00000000..6d88470b --- /dev/null +++ b/rowers/templates/upgradeconfirm.html @@ -0,0 +1,99 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Paid Membership{% endblock title %} +{% load rowerfilters %} +{% block main %} + +

    Confirm Your Payment

    + +

    Order Overview

    + +
      +
    • +

      + + + + + + + + + + + + + + + +
      Plan{{ plan.name }}
      Payment Type{{ plan.paymenttype }}
      Billing Cycle1 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 +

    • +
    • +
      +
      + +
      +
      +
      +
      + + + + {% csrf_token %} + +
      +
    • +
    • +

      + 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€. +

      +
    • +
    + +{% include 'braintreedropin.html' %} + + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_profile.html' %} +{% endblock %} + diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index c9b81bd3..253ced99 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -24,6 +24,8 @@ from rowers.models import checkaccessuser from rowers.mytypes import otwtypes from rowers.utils import NoTokenError +import rowers.payments as payments + def strfdelta(tdelta): minutes,seconds = divmod(tdelta.seconds,60) tenths = int(tdelta.microseconds/1e5) @@ -59,6 +61,13 @@ def secondstotimestring(tdelta): return res +@register.filter +def existing_customer(user): + if user.is_anonymous(): + return False + else: + return payments.is_existing_customer(user.rower) + @register.filter def aantalcomments(workout): try: @@ -116,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 c64090b0..f37951da 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -404,6 +404,7 @@ urlpatterns = [ url(r'^me/edit/$',views.rower_edit_view), url(r'^me/edit/user/(?P\d+)$',views.rower_edit_view), url(r'^me/preferences/$',views.rower_prefs_view), + url(r'^me/transactions/$',views.transactions_view), url(r'^me/preferences/user/(?P\d+)$',views.rower_prefs_view), url(r'^me/edit/(.+.*)/$',views.rower_edit_view), url(r'^me/c2authorize/$',views.rower_c2_authorize), @@ -438,6 +439,20 @@ 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'^upgradecheckout/(?P\d+)$',views.upgrade_confirm_view), + url(r'^downgradecheckout/(?P\d+)$',views.downgrade_confirm_view), + url(r'^billing$',views.billing_view,name='billing'), + url(r'^upgrade$',views.upgrade_view,name='upgrade'), + url(r'^downgrade$',views.downgrade_view,name='downgrade'), + url(r'^paymentcompleted$',views.payment_completed_view), + url(r'^downgradecompleted$',views.downgrade_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'^upgradecheckouts$',views.upgrade_checkouts_view,name='upgrade_checkouts'), + url(r'^downgradecheckouts$',views.downgrade_checkouts_view,name='downgrade_checkouts'), url(r'^planrequired',views.planrequired_view), url(r'^starttrial$',views.start_trial_view), url(r'^startplantrial$',views.start_plantrial_view), diff --git a/rowers/utils.py b/rowers/utils.py index cb9fbaae..eb5627b3 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -426,6 +426,13 @@ class NoTokenError(Exception): def __str__(self): return repr(self.value) +class ProcessorCustomerError(Exception): + def __init__(self, value): + self.value=value + + def __str__(self): + return repr(self.value) + # Custom exception handler, returns a 401 HTTP message # with exception details in the json data def custom_exception_handler(exc,message): diff --git a/rowers/views.py b/rowers/views.py index 38c1b289..73e3dad9 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -28,6 +28,8 @@ import isodate import re import cgi from icalendar import Calendar, Event +import rowers.braintreestuff as braintreestuff +import rowers.payments as payments from django.shortcuts import render from django.template.loader import render_to_string @@ -50,7 +52,7 @@ from rowers.forms import ( RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm, FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm, MetricsForm,DisqualificationForm,disqualificationreasons, - disqualifiers,SearchForm, + disqualifiers,SearchForm,BillingForm,PlanSelectForm ) from django.core.urlresolvers import reverse, reverse_lazy @@ -83,7 +85,7 @@ from rowers.models import ( createmicrofillers, createmesofillers, microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates, TrainingMesoCycleForm, TrainingMicroCycleForm, - RaceLogo, + RaceLogo,RowerBillingAddressForm,PaidPlan, ) from rowers.models import ( RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm, @@ -139,6 +141,8 @@ from rowsandall_app.settings import ( UNDERARMOUR_CLIENT_SECRET,UNDERARMOUR_CLIENT_KEY, RUNKEEPER_CLIENT_ID,RUNKEEPER_REDIRECT_URI,RUNKEEPER_CLIENT_SECRET, TP_CLIENT_ID,TP_REDIRECT_URI,TP_CLIENT_KEY,TP_CLIENT_SECRET, + BRAINTREE_MERCHANT_ID,BRAINTREE_PUBLIC_KEY,BRAINTREE_PRIVATE_KEY, + PAYMENT_PROCESSING_ON ) from rowers.tasks_standalone import addcomment2 @@ -736,8 +740,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' @@ -788,7 +811,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, @@ -996,7 +1029,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): @@ -1027,6 +1060,413 @@ def add_defaultfavorites(r): f.save() return 1 +def paidplans_view(request): + if not request.user.is_anonymous(): + r = getrequestrower(request) + else: + r = None + + + + return render(request, + 'paidplans.html', + {'rower':r}) + +@login_required() +def billing_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + if payments.is_existing_customer(r): + url = reverse(upgrade_view) + return HttpResponseRedirect(url) + + if request.method == 'POST': + billingaddressform = RowerBillingAddressForm(request.POST) + planselectform = PlanSelectForm(request.POST,paymentprocessor='braintree') + if billingaddressform.is_valid(): + cd = billingaddressform.cleaned_data + for attr, value in cd.items(): + setattr(r, attr, value) + r.save() + + 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') + + return render(request, + 'billing.html', + {'rower':r, + 'billingaddressform':billingaddressform, + 'planselectform':planselectform, + }) + +@login_required() +def upgrade_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + if r.subscription_id is None or r.subscription_id == '': + url = reverse(billing_view) + return HttpResponseRedirect(url) + + if request.method == 'POST': + billingaddressform = RowerBillingAddressForm(request.POST) + planselectform = PlanSelectForm(request.POST,paymentprocessor='braintree') + if billingaddressform.is_valid(): + cd = billingaddressform.cleaned_data + for attr, value in cd.items(): + setattr(r, attr, value) + r.save() + + if planselectform.is_valid(): + plan = planselectform.cleaned_data['plan'] + if billingaddressform.is_valid(): + url = reverse(upgrade_confirm_view, + kwargs={ + 'planid':plan.id + }) + return HttpResponseRedirect(url) + + else: + billingaddressform = RowerBillingAddressForm(instance=r) + planselectform = PlanSelectForm(paymentprocessor='braintree', + rower=r) + + return render(request, + 'upgrade.html', + {'rower':r, + 'billingaddressform':billingaddressform, + 'planselectform':planselectform, + }) + +@login_required() +def downgrade_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + if r.subscription_id is None or r.subscription_id == '': + url = reverse(billing_view) + return HttpResponseRedirect(url) + + if request.method == 'POST': + billingaddressform = RowerBillingAddressForm(request.POST) + planselectform = PlanSelectForm(request.POST,paymentprocessor='braintree') + if billingaddressform.is_valid(): + cd = billingaddressform.cleaned_data + for attr, value in cd.items(): + setattr(r, attr, value) + r.save() + + if planselectform.is_valid(): + plan = planselectform.cleaned_data['plan'] + + if plan.price > r.paidplan.price: + nextview = upgrade_confirm_view + elif plan.price == r.paidplan.price: + messages.info(request,'You did not select a new plan') + url = reverse(downgrade_view) + return HttpResponseRedirect(url) + else: + nextview = downgrade_confirm_view + + if billingaddressform.is_valid(): + url = reverse(nextview, + kwargs={ + 'planid':plan.id + }) + return HttpResponseRedirect(url) + + else: + billingaddressform = RowerBillingAddressForm(instance=r) + planselectform = PlanSelectForm(paymentprocessor='braintree', + rower=r,includeall=True, initial={'plan':r.paidplan}) + + return render(request, + 'downgrade.html', + {'rower':r, + 'billingaddressform':billingaddressform, + 'planselectform':planselectform, + }) + +@login_required() +def plan_stop_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + 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): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + 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 upgrade_confirm_view(request,planid = 0): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + 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) + + client_token = braintreestuff.get_client_token(r) + + return render(request, + "upgradeconfirm.html", + { + 'plan':plan, + 'client_token':client_token, + 'rower':r, + }) + +@login_required() +def downgrade_confirm_view(request,planid = 0): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + 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) + + client_token = braintreestuff.get_client_token(r) + + return render(request, + "downgradeconfirm.html", + { + 'plan':plan, + 'client_token':client_token, + 'rower':r, + }) + + +@login_required() +def payment_confirm_view(request,planid = 0): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + 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) + + client_token = braintreestuff.get_client_token(r) + + return render(request, + "paymentconfirm.html", + { + 'plan':plan, + 'client_token':client_token, + 'rower':r, + }) + + +@login_required() +def checkouts_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + + r = getrequestrower(request) + + if request.method != 'POST': + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + + form = BillingForm(request.POST) + if form.is_valid(): + 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(billing_view) + return HttpResponseRedirect(url) + + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + +@login_required() +def upgrade_checkouts_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + + r = getrequestrower(request) + + if request.method != 'POST': + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + + form = BillingForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + success = braintreestuff.update_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(upgrade_view) + return HttpResponseRedirect(url) + + else: + messages.error(request,"There was an error in the payment form") + url = reverse(upgrade_view) + return HttpResponseRedirect(url) + + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + +@login_required() +def downgrade_checkouts_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + + r = getrequestrower(request) + + if request.method != 'POST': + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + + form = BillingForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + success = braintreestuff.update_subscription(r,data,method='down') + if success: + messages.info(request,"Your plan has been updated") + url = reverse(downgrade_completed_view) + return HttpResponseRedirect(url) + else: + messages.error(request,"There was a problem with your transaction") + url = reverse(upgrade_view) + return HttpResponseRedirect(url) + + else: + messages.error(request,"There was an error in the payment form") + url = reverse(upgrade_view) + return HttpResponseRedirect(url) + + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + + +@login_required() +def payment_completed_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + return render(request, + "payment_completed.html", + { + 'rower':r + }) + +@login_required() +def downgrade_completed_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + return render(request, + "downgrade_completed.html", + { + 'rower':r + }) # User registration def rower_register_view(request): @@ -1477,6 +1917,33 @@ def plannedsessions_icsemail_view(request,userid=0): return response +@login_required() +def transactions_view(request): + if not request.user.is_staff: + raise PermissionDenied("Not Allowed") + + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + + df = braintreestuff.get_transactions(startdate,enddate) + filename="transactions_{s}_{e}.csv".format(s = startdate, e = enddate) + response = HttpResponse(df.to_csv()) + response['Content-Disposition'] = 'attachment; filename="%s"' % filename + response['Content-Type'] = 'application/octet-stream' + + return response + + else: + dateform = DateRangeForm() + + return render(request, + 'transactions.html', + { + 'dateform':dateform + }) @login_required() def course_kmlemail_view(request,id=0): diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 93bfb4a4..b96c3f66 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -66,6 +66,7 @@ INSTALLED_APPS = [ 'django_extensions', 'tz_detect', 'django_social_share', + 'django_countries', ] AUTHENTICATION_BACKENDS = ( @@ -421,3 +422,25 @@ try: except KeyError: workoutemailbox = 'workouts@rowsandall.com' + +# payments + +try: + BRAINTREE_MERCHANT_ID = CFG['braintree_merchant_id'] +except KeyError: + BRAINTREE_MERCHANT_ID = '' + +try: + BRAINTREE_PUBLIC_KEY = CFG['braintree_public_key'] +except KeyError: + BRAINTREE_PUBLIC_KEY = '' + +try: + BRAINTREE_PRIVATE_KEY = CFG['braintree_private_key'] +except KeyError: + BRAINTREE_PRIVATE_KEY = '' + +try: + PAYMENT_PROCESSING_ON = CFG['payment_processing_on'] +except KeyError: + PAYMENT_PROCESSING_ON = False diff --git a/static/css/rowsandall.css b/static/css/rowsandall.css index 95fd1d8b..86682c66 100644 --- a/static/css/rowsandall.css +++ b/static/css/rowsandall.css @@ -170,6 +170,15 @@ cox { text-align: center; } +.plantable { + border-collapse: collapse; +} + +.plantable > td { + text-align: center; +} + + th.rotate { /* Something you can count on */ height: 78px; @@ -220,6 +229,7 @@ th.rotate > div > span { background-color: #fee; } + .successmessage { border: 1px solid #000; background-color: #8f8; @@ -907,3 +917,21 @@ a.wh:hover { hyphens: auto; } + +.upgradebutton { + background-color: #4CAF50; + border: none; + color: white; + padding: 20px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; +} + +.buttonr2 {border-radius: 2px;} +.buttonr4 {border-radius: 4px;} +.buttonr8 {border-radius: 8px;} +.buttonr12 {border-radius: 12px;} +.buttonround {border-radius: 50%;}