Skip to content

Commit

Permalink
Upgrade GoCardless API and use gocardless_pro
Browse files Browse the repository at this point in the history
GoCardless updated their API and deprecated the older one. These changes
keep the same donation flow with the new API.

Note that there's a slightly ugly relationship between the
GoCardlessHelper and the session. This is because we need to hand the
user off to GC to create a "customer" object (that contains the bank
details), and that object is accessible after being redirected.

It would be nice to find a neater way to do this, as right now
the logic is split between the helper class, the middleware and the
views.
  • Loading branch information
symroe committed Dec 1, 2017
1 parent 18cd131 commit 2fd8918
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 78 deletions.
96 changes: 56 additions & 40 deletions democracy_club/apps/donations/helpers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from decimal import Decimal

from django.conf import settings

import gocardless
import gocardless_pro

PAYMENT_TYPES = (
('subscription', 'Monthly donation'),
Expand All @@ -18,50 +16,68 @@


class GoCardlessHelper(object):
def __init__(self):
self.gocardless = gocardless
def __init__(self, request):
self.request = request
# We need the session to be saved before it gets a session_key
# This is because the code is called from a middleware
self.request.session.save()

if getattr(settings, 'GOCARDLESS_USE_SANDBOX', False):
self.gocardless.environment = "sandbox"
self.gocardless.set_details(
app_id=settings.GOCARDLESS_APP_ID,
app_secret=settings.GOCARDLESS_APP_SECRET,
access_token=settings.GOCARDLESS_ACCESS_TOKEN,
merchant_id=settings.GOCARDLESS_MERCHANT_ID,
)
gc_environment = "sandbox"
gc_token = settings.GOCARDLESS_ACCESS_TOKEN
self.client = gocardless_pro.Client(
access_token=gc_token, environment=gc_environment)

def get_payment_url(self, amount, other_amount, payment_type="bill",
name=None, description=None):
def get_redirect_url(self):
"""
A bill is one off, a subscription is repeating
Get a URL for creating a customer object.
"""
redirect_flow = self.client.redirect_flows.create(
params={
"description": settings.GO_CARDLESS_PAYMENT_DESCRIPTION,
"session_token": self.request.session.session_key,
"success_redirect_url": settings.GOCARDLESS_REDIRECT_URL
}
)

assert payment_type in [i[0] for i in PAYMENT_TYPES]
if other_amount:
amount = other_amount
amount = Decimal(amount)

if not name:
name = settings.GO_CARDLESS_PAYMENT_NAME
if not description:
description = settings.GO_CARDLESS_PAYMENT_DESCRIPTION
# Save the flow ID on the session
self.request.session['GC_REDIRECT_FLOW_ID'] = redirect_flow.id
self.request.session.save()
return redirect_flow.redirect_url

if payment_type == "bill":
return gocardless.client.new_bill_url(amount, name=name)
def confirm_redirect_flow(self):
redirect_flow = self.client.redirect_flows.complete(
self.request.GET.get('redirect_flow_id'),
params={
"session_token": self.request.session.session_key
}
)
self.request.session['GC_CUSTOMER_ID'] = redirect_flow.links.customer
self.request.session['GC_MANDATE_ID'] = redirect_flow.links.mandate
self.request.session.save()

return gocardless.client.new_subscription_url(
amount=amount,
interval_length=1,
interval_unit="month",
name=name,
description=description,
redirect_uri=settings.GOCARDLESS_REDIRECT_URL)
def create_payment(self):
form = self.request.session['donation_form']
payment_type = form['payment_type']
assert payment_type in [i[0] for i in PAYMENT_TYPES]
amount = form['amount']
if form['other_amount']:
amount = form['other_amount']
amount = int(float(amount) * 100)

def confirm_payment(self, resource_uri, resource_id, resource_type):
params = {
'resource_uri': resource_uri,
'resource_id': resource_id,
'resource_type': resource_type,
"amount": amount,
"currency": "GBP",
"links": {
"mandate": self.request.session['GC_MANDATE_ID']
},
}
params['signature'] = gocardless.utils.generate_signature(params,
settings.GOCARDLESS_APP_SECRET)
self.gocardless.client.confirm_resource(params)
if payment_type == "bill":
# One off donation
return self.client.payments.create(params)

if payment_type == "subscription":
# Monthly donation
params['interval_unit'] = "monthly"
params["day_of_month"] = "1"
return self.client.subscriptions.create(params)
18 changes: 13 additions & 5 deletions democracy_club/apps/donations/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ def get_initial(self):
'amount': 10,
}

def form_valid(self, form):
gc = GoCardlessHelper()
url = gc.get_payment_url(**form.cleaned_data)
return HttpResponseRedirect(url)
def form_valid(self, request, form):
# Add the form info to the session
request.session['donation_form'] = form.cleaned_data

# Start the GoCardless process
gc = GoCardlessHelper(request)

# Make a customer object at GC's site first.
redirect_url = gc.get_redirect_url()

# Redirect to GoCardless
return HttpResponseRedirect(redirect_url)

def process_request(self, request):
form_prefix = "donation_form"
Expand All @@ -23,7 +31,7 @@ def process_request(self, request):
if request.method == 'POST' and key_to_check in request.POST:
form = DonationForm(data=request.POST, prefix=form_prefix)
if form.is_valid():
return self.form_valid(form)
return self.form_valid(request, form)
else:
form = DonationForm(
initial=self.get_initial(), prefix=form_prefix)
Expand Down
15 changes: 0 additions & 15 deletions democracy_club/apps/donations/tests.py

This file was deleted.

14 changes: 14 additions & 0 deletions democracy_club/apps/donations/tests/test_donations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import vcr


@vcr.use_cassette(
'fixtures/vcr_cassettes/test_donation_redirect_url.yaml',
filter_headers=['Authorization', 'Idempotency-Key'])
def test_donation_middleware_form_valid(db, client):
form_data = {
'donation_form-amount': 10,
'donation_form-payment_type': 'subscription',
}
req = client.post('/donate/', form_data)
assert req.url == "https://pay-sandbox.gocardless.com/flow/RE0000PV77M4SN1QWF1F30SP8PXQHAWC" # noqa
assert req.status_code == 302
3 changes: 2 additions & 1 deletion democracy_club/apps/donations/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from django.conf.urls import url

from .views import DonateFormView, DonateThanksView
from .views import DonateFormView, DonateThanksView, ProcessDonationView

urlpatterns = [
url(r'thanks', DonateThanksView.as_view(), name="donate_thanks"),
url(r'process', ProcessDonationView.as_view(), name="donate_process"),
url(r'', DonateFormView.as_view(), name="donate"),
]
25 changes: 11 additions & 14 deletions democracy_club/apps/donations/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.views.generic import TemplateView
from django.views.generic import TemplateView, RedirectView
from django.core.urlresolvers import reverse

from .helpers import GoCardlessHelper

Expand All @@ -7,19 +8,15 @@ class DonateFormView(TemplateView):
template_name = "donate.html"


class DonateThanksView(TemplateView):

def get(self, request, *args, **kwargs):
resource_uri = request.GET.get('resource_uri')
resource_id = request.GET.get('resource_id')
resource_type = request.GET.get('resource_type')
if resource_id and resource_type and resource_uri:
GoCardlessHelper().confirm_payment(
resource_uri,
resource_id,
resource_type
)
return super(DonateThanksView, self).get(request, *args, **kwargs)
class ProcessDonationView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
redirect_flow_id = self.request.GET.get('redirect_flow_id')
if redirect_flow_id:
gc = GoCardlessHelper(self.request)
gc.confirm_redirect_flow()
gc.create_payment()
return reverse('donations:donate_thanks')


class DonateThanksView(TemplateView):
template_name = "donate_thanks.html"
6 changes: 4 additions & 2 deletions democracy_club/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@
}

GO_CARDLESS_PAYMENT_NAME = "Democracy Club Donation"
GO_CARDLESS_PAYMENT_DESCRIPTION = "Helping Democracy Club increase the quantity,"\
" quality and accessibility of information on election candidates, politicians and democratic processes"
GO_CARDLESS_PAYMENT_DESCRIPTION = "Helping Democracy Club "\
"increase the quality of information on elections & the democratic processes"
GOCARDLESS_REDIRECT_URL = "https://democracyclub.org.uk/donate/thanks/"

CORS_URLS_REGEX = r'^/research/answers/*|/members/api/members/*$'
Expand All @@ -263,3 +263,5 @@

BACKLOG_TRELLO_BOARD_ID = "O00ATMzS"
BACKLOG_TRELLO_DEFAULT_LIST_ID = "58bd618abc9a825bd64b5d8f"


3 changes: 3 additions & 0 deletions democracy_club/settings/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
BACKLOG_TRELLO_TOKEN = "empty"

STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'

GOCARDLESS_USE_SANDBOX = True
GOCARDLESS_ACCESS_TOKEN = "Made Up"
45 changes: 45 additions & 0 deletions fixtures/vcr_cassettes/test_donation_redirect_url.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
interactions:
- request:
body: '{"redirect_flows": {"success_redirect_url": "http://localhost:8000/donate/process/",
"session_token": "l73u2qofrcu2xnfwks8ha5tv6pohqkpu", "description": "Helping
Democracy Club increase the quality of information on elections & the democratic
processes"}}'
headers:
Accept: [application/json]
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Content-Length: ['255']
Content-Type: [application/json]
GoCardless-Client-Library: [gocardless-pro-python]
GoCardless-Client-Version: [1.1.0]
GoCardless-Version: ['2015-07-06']
User-Agent: [gocardless-pro-python/1.1.0 python/3.4 CPython/3.4.3-final0 Darwin/15.2.0
requests/2.17.3]
method: POST
uri: https://api-sandbox.gocardless.com/redirect_flows
response:
body: {string: '{"redirect_flows":{"id":"RE0000PV77M4SN1QWF1F30SP8PXQHAWC","description":"Helping
Democracy Club increase the quality of information on elections & the democratic
processes","session_token":"l73u2qofrcu2xnfwks8ha5tv6pohqkpu","scheme":null,"success_redirect_url":"http://localhost:8000/donate/process/","created_at":"2017-12-01T11:47:43.447Z","links":{"creditor":"CR00005378BCBE"},"redirect_url":"https://pay-sandbox.gocardless.com/flow/RE0000PV77M4SN1QWF1F30SP8PXQHAWC"}}'}
headers:
CF-RAY: [3c65d0342bc806e2-LHR]
Cache-Control: [no-store]
Connection: [keep-alive]
Content-Type: [application/json]
Date: ['Fri, 01 Dec 2017 11:47:43 GMT']
ETag: [W/"1a147b8d7b39ae13c43a721886ca3a35"]
Location: ['https://api-sandbox.gocardless.com/redirect_flows/RE0000PV77M4SN1QWF1F30SP8PXQHAWC']
Pragma: [no-cache]
RateLimit-Limit: ['1000']
RateLimit-Remaining: ['999']
RateLimit-Reset: ['Fri, 01 Dec 2017 11:48:00 GMT']
Server: [cloudflare-nginx]
Set-Cookie: ['__cfduid=d357016786b54d22d85e275fccf3800ff1512128863; expires=Sat,
01-Dec-18 11:47:43 GMT; path=/; domain=.gocardless.com; HttpOnly']
Strict-Transport-Security: [max-age=31556926; includeSubDomains; preload]
Vary: [Origin]
X-Content-Type-Options: [nosniff]
X-Request-Id: [02274725-9552-4874-9023-af56b99a5651]
X-XSS-Protection: [1; mode=block]
status: {code: 201, message: Created}
version: 1
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ django-model-utils==2.3.1
django-ratelimit==0.6.0
Django>=1.10,<1.11
djangorestframework==3.5.4
gocardless==0.5.3
gocardless_pro==1.1.0
jsonfield==1.0.3
markdown==2.6.2
Pillow
Expand Down

0 comments on commit 2fd8918

Please sign in to comment.