Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 46009f6264d996ec176cab3e373e2ac730669abc @codeinthehole codeinthehole committed May 2, 2012
27 LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2011, Tangent Communications PLC and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of Tangent Communications PLC nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
+include *.rst
@@ -0,0 +1,60 @@
+===================================
+GoCardLess package for django-oscar
+===================================
+
+This is a work in progress - not ready for production yet. It also depends on
+the forthcoming version of oscar (0.2) which hasn't been released yet.
+
+Overview
+========
+
+GoCardless_ provide payments straight from customer's bank accounts using the
+Direct Debit system. A typical integration involves redirecting the user to
+GoCardless' site where they enter their bank account details and confirm the
+transaction.
+
+This library provides integration between GoCardless and `django-oscar`_.
+
+.. _GoCardless: https://gocardless.com/
+.. _`django-oscar`: https://github.com/tangentlabs/django-oscar
+
+Installation
+============
+
+You need to make sure your redirect URI and cancel URIs are set up within your
+GoCardless dashboard. Only the scheme and domain need to match so you can just
+put your site URL in there.
+
+Limitations
+===========
+
+* Only the 'one-off bill' transaction has been implemented so far.
+* Webhook handling isn't implemented yet.
+* Refunds aren't possible through the GoCardless API at the moment. These
+ should be handled either through BACS transfers or a cheque payment.
+
+Contribute
+==========
+
+Create a virtualenv, clone repo and install dependencies::
+
+ mkvirtualenv oscar-gocardless
+ git clone git://github.com/tangentlabs/django-oscar-gocardless.git
+ cd django-oscar-gocardless
+ python setup.py develop
+ pip install -r requirements.txt
+
+Set up a sandbox site to play with::
+
+ cd sandbox
+ ./manage.py syncdb --noinput
+ ./manage.py migrate
+ ./manage.py oscar_import_catalogue data/books-catalogue.csv
+
+You can test the end-to-end process by adding an item to your basket and then
+proceeding through checkout. After the preview page, you'll be redirected to
+the GoCardless 'sandbox' site where you can use the following account details to
+complete an order:
+
+ Account number - 55779911
+ Sort code - 200000
2 TODO
@@ -0,0 +1,2 @@
+Create model for txn
+Passing anon email address
No changes.
@@ -0,0 +1,64 @@
+from django.core.urlresolvers import reverse
+import gocardless
+from gocardless.exceptions import SignatureError, ClientError
+from oscar.apps.payment.exceptions import PaymentError
+
+
+# Implemented as a class so that project can subclass and override methods
+# if the parameters passed to GoCardless need to be configured
+class BillingFacade(object):
+
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def get_redirect_url(self, order_number, total_incl_tax, user):
+ """
+ Return a URL for a new one-off bill transaction
+ """
+ user_params = None
+ if user and user.is_authenticated():
+ user_params = {
+ 'first_name': user.first_name,
+ 'last_name': user.last_name,
+ 'email': user.email
+ }
+ return gocardless.client.new_bill_url(
+ total_incl_tax,
+ self.get_transaction_name(order_number),
+ self.get_transaction_description(order_number),
+ redirect_uri="%s%s" % (self.base_url, reverse('gocardless-response')),
+ cancel_uri="%s%s" % (self.base_url, reverse('gocardless-cancel')),
+ state=self.get_transaction_state(order_number),
+ user=user_params)
+
+ def get_transaction_name(self, order_number):
+ return 'One-off bill for order #%s' % order_number
+
+ def get_transaction_description(self, order_number):
+ return ''
+
+ def get_transaction_state(self, order_number):
+ return ''
+
+
+def confirm(request):
+ """
+ Confirm a transaction with GoCardles
+ """
+ # Pluck params straight off the request path - if any are missing or have been
+ # manipulated, then we'll get a SignatureError
+ params = {
+ 'resource_id': request.GET.get('resource_id', None),
+ 'resource_type': request.GET.get('resource_type', None),
+ 'resource_uri': request.GET.get('resource_uri', None),
+ 'signature': request.GET.get('signature', None),
+ }
+ state = request.GET.get('state', None)
+ if state:
+ params['state'] = state
+ try:
+ gocardless.client.confirm_resource(params)
+ except SignatureError:
+ raise PaymentError("Invalid signature")
+ except ClientError:
+ raise PaymentError("An error occurred communicating with payment gateway")
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import *
+
+from oscar_gocardless import views
+
+
+urlpatterns = patterns('',
+ url(r'^redirect/', views.RedirectView.as_view(), name='gocardless-redirect'),
+ url(r'^confirm/', views.ConfirmView.as_view(), name='gocardless-response'),
+ url(r'^cancel/', views.CancelView.as_view(), name='gocardless-cancel'),
+)
@@ -0,0 +1,50 @@
+from django.views.generic import RedirectView, View
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.contrib import messages
+
+from oscar_gocardless import facade
+from oscar.apps.payment.exceptions import PaymentError
+from oscar.apps.checkout.views import OrderPlacementMixin
+from oscar.apps.payment.models import SourceType, Source
+
+
+class ConfirmView(OrderPlacementMixin, View):
+ """
+ Handle the response from GoCardless
+ """
+ def get(self, request, *args, **kwargs):
+ try:
+ facade.confirm(request)
+ except PaymentError, e:
+ messages.error(self.request, str(e))
+ self.restore_frozen_basket()
+ return HttpResponseRedirect(reverse('checkout:payment-details'))
+
+ # Fetch submission data out of session
+ order_number = self.checkout_session.get_order_number()
+ basket = self.get_submitted_basket()
+ total_incl_tax, total_excl_tax = self.get_order_totals(basket)
+
+ # Record payment source
+ source_type, is_created = SourceType.objects.get_or_create(name='GoCardless')
+ source = Source(source_type=source_type,
+ currency='GBP',
+ amount_allocated=total_incl_tax,
+ amount_debited=total_incl_tax)
+ self.add_payment_source(source)
+
+ # Place order
+ return self.handle_order_placement(order_number,
+ basket,
+ total_incl_tax,
+ total_excl_tax)
+
+
+class CancelView(RedirectView):
+
+ def get_redirect_url(self, **kwargs):
+ messages.error(self.request, "Transaction cancelled")
+ return reverse('checkout:payment-details')
+
+
@@ -0,0 +1,9 @@
+South==0.7.3
+Werkzeug==0.8.3
+coverage==3.5.1
+django-debug-toolbar==0.9.4
+django-extensions==0.8
+django-nose==1.0
+-e git+git@github.com:tangentlabs/django-oscar.git#egg=django_oscar
+mock==0.7.2
+nose==1.1.2
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+import sys
+from coverage import coverage
+from optparse import OptionParser
+
+from django.conf import settings
+from oscar import get_core_apps
+
+if not settings.configured:
+ from oscar.defaults import *
+ extra_settings = {}
+ for key, value in locals().items():
+ if key.startswith('OSCAR'):
+ extra_settings[key] = value
+
+ settings.configure(
+ DATABASES={
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ }
+ },
+ INSTALLED_APPS=[
+ 'django.contrib.auth',
+ 'django.contrib.admin',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.flatpages',
+ 'south',
+ ] + get_core_apps(),
+ MIDDLEWARE_CLASSES=(
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.transaction.TransactionMiddleware',
+ 'oscar.apps.basket.middleware.BasketMiddleware',
+ ),
+ DEBUG=False,
+ SOUTH_TESTS_MIGRATE=False,
+ HAYSTACK_SITECONF='oscar.search_sites',
+ HAYSTACK_SEARCH_ENGINE='dummy',
+ SITE_ID=1,
+ ROOT_URLCONF='tests.urls',
+ NOSE_ARGS=['-s'],
+ **extra_settings
+ )
+
+from django_nose import NoseTestSuiteRunner
+
+
+def run_tests(*test_args):
+ if 'south' in settings.INSTALLED_APPS:
+ from south.management.commands import patch_for_test_db_setup
+ patch_for_test_db_setup()
+
+ if not test_args:
+ test_args = ['tests']
+
+ # Run tests
+ test_runner = NoseTestSuiteRunner(verbosity=1)
+
+ c = coverage(source=['oscar_cardless'], omit=['*migrations*', '*tests*'])
+ c.start()
+ num_failures = test_runner.run_tests(test_args)
+ c.stop()
+
+ if num_failures > 0:
+ sys.exit(num_failures)
+ print "Generating HTML coverage report"
+ c.html_report()
+
+def generate_migration():
+ from south.management.commands.schemamigration import Command
+ com = Command()
+ com.handle(app='paypal', initial=True)
+
+
+if __name__ == '__main__':
+ parser = OptionParser()
+ (options, args) = parser.parse_args()
+ run_tests(*args)
No changes.
@@ -0,0 +1,9 @@
+from oscar.app import Shop
+from apps.checkout.app import application as checkout_app
+
+
+class GoCardlessShop(Shop):
+ checkout_app = checkout_app
+
+
+shop = GoCardlessShop()
No changes.
@@ -0,0 +1,11 @@
+from oscar.apps.checkout.app import CheckoutApplication
+
+from apps.checkout import views
+
+
+class OverriddenCheckoutApplication(CheckoutApplication):
+ # Specify new view for payment details
+ payment_details_view = views.PaymentDetailsView
+
+
+application = OverriddenCheckoutApplication()
No changes.
@@ -0,0 +1,27 @@
+from django.conf import settings
+from django.contrib.sites.models import Site
+from oscar.apps.checkout.views import PaymentDetailsView as OscarPaymentDetailsView
+from oscar.apps.payment.exceptions import RedirectRequired
+
+from oscar_gocardless import facade
+
+
+class PaymentDetailsView(OscarPaymentDetailsView):
+
+ def handle_payment(self, order_number, total_incl_tax, **kwargs):
+ # Determine base URL of current site - you could just set this in a
+ # setting
+ if settings.DEBUG:
+ # Determine the localserver's hostname to use when
+ # in testing mode
+ base_url = 'http://%s' % self.request.META['HTTP_HOST']
+ else:
+ base_url = 'https://%s' % Site.objects.get_current().domain
+
+ # Payment requires a redirect so we raise a RedirectRequired exception
+ # and oscar's checkout flow will handle the rest.
+ url = facade.BilingFacade(base_url).get_redirect_url(order_number,
+ total_incl_tax,
+ self.request.user)
+ raise RedirectRequired(url)
+
Oops, something went wrong.

0 comments on commit 46009f6

Please sign in to comment.