Skip to content
This repository has been archived by the owner on May 10, 2019. It is now read-only.

Commit

Permalink
Merge pull request #3 from fmr/master
Browse files Browse the repository at this point in the history
PyPi preparation
  • Loading branch information
Jonathan Moss committed Aug 15, 2012
2 parents 48f4dcc + 34030da commit ab25c52
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 13 deletions.
27 changes: 27 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Copyright (c) 2012, 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.
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include *.rst
recursive-include paymentexpress/templates *.html
186 changes: 183 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,186 @@
PaymentExpress package for django-oscar
=======================================

This package provides integration with the payment gateway, PaymentExpress. It is designed to
work seamlessly with the e-commerce framework `django-oscar` but can be used without
oscar.
This package provides integration with the payment gateway, PaymentExpress using their `PX POST API`_. It is designed to work seamlessly with the e-commerce framework `django-oscar`_ but can be used without it.

.. _`PX Post API`: http://sec.paymentexpress.com/technical_resources/ecommerce_nonhosted/pxpost.html
.. _`django-oscar`: https://github.com/tangentlabs/django-oscar

Installation
------------

From PyPi::

pip install django-oscar-paymentexpress

or from Github::

pip install git+git://github.com/tangentlabs/django-oscar-paymentexpress.git#egg=django-oscar-paymentexpress

Add ``'paymentexpress'`` to ``INSTALLED_APPS`` and run::

./manage.py migrate paymentexpress

to create the appropriate database tables.

Configuration
-------------

Edit your ``settings.py`` to set the following settings::

PAYMENTEXPRESS_POST_URL = 'https://sec.paymentexpress.com/pxpost.aspx'
PAYMENTEXPRESS_USERNAME = '…'
PAYMENTEXPRESS_PASSWORD = '…'
PAYMENTEXPRESS_CURRENCY = 'AUD'

Integration into checkout
-------------------------

You'll need to use a subclass of ``oscar.apps.checkout.views.PaymentDetailsView`` within your own
checkout views. See `oscar's documentation`_ on how to create a local version of the checkout app.

.. _`oscar's documentation`: http://django-oscar.readthedocs.org/en/latest/index.html

Override the ``handle_payment`` method (which is blank by default) and add your integration code. An example
integration might look like::

# myshop.checkout.views
from django.conf import settings

from oscar.apps.checkout.views import PaymentDetailsView as OscarPaymentDetailsView
from oscar.apps.payment.forms import BankcardForm
from paymentexpress.facade import Facade
from paymentexpress import PAYMENTEXPRESS

...

class PaymentDetailsView(OscarPaymentDetailsView):

def get_context_data(self, **kwargs):
...
ctx['bankcard_form'] = BankcardForm()
...
return ctx

def post(self, request, *args, **kwargs):
"""
This method is designed to be overridden by subclasses which will
validate the forms from the payment details page. If the forms are valid
then the method can call submit()
"""
# Check bankcard form is valid
bankcard_form = BankcardForm(request.POST)
if not bankcard_form.is_valid():
ctx = self.get_context_data(**kwargs)
ctx['bankcard_form'] = bankcard_form
return self.render_to_response(ctx)

bankcard = bankcard_form.get_bankcard_obj()

# Call oscar's submit method, passing through the bankcard object so it gets
# passed to the 'handle_payment' method
return self.submit(request.basket, payment_kwargs={'bankcard': bankcard})

def handle_payment(self, order_number, total, **kwargs):
# Make request to PaymentExpress - if there any problems (eg bankcard
# not valid / request refused by bank) then an exception would be
# raised and handled) within oscar's PaymentDetails view.
bankcard = kwargs['bankcard']
response_dict = Facade().purchase(order_number, total, None, bankcard)

source_type, _ = SourceType.objects.get_or_create(name=PAYMENTEXPRESS)
source = Source(source_type=source_type,
currency=settings.PAYMENTEXPRESS_CURRENCY,
amount_allocated=total,
amount_debited=total,
reference=response_dict['partner_reference'])

self.add_payment_source(source)

# Also record payment event
self.add_payment_event(PURCHASE, total)

Oscar's view will handle the various exceptions that can get raised.

Package structure
=================

There are two key components:

Gateway
-------

The class ``paymentexpress.gateway.Gateway`` provides fine-grained access to the PaymentExpress API, which involve constructing XML requests and decoding XML responses. All calls return a ``paymentexpress.gateway.Response`` instance which provides dictionary-like access to the attributes of the response.

Example calls::

# Authorise a transaction.
# The funds are not transferred from the cardholder account.
response = gateway.authorise(card_holder='John Smith',
card_number='4500230021616301',
cvc2='123',
amount=50.23)

# Completes (settles) a pre-approved Auth Transaction.
response = gateway.complete(amount=50.23,
dps_txn_ref='0000000809b61753')


# Purchase on a new card - funds are transferred immediately
response = gateway.purchase(card_holder='Frankie',
card_number=CARD_VISA,
card_expiry='1015',
cvc2='123',
merchant_ref='100001_PURCHASE_1_2008',
enable_add_bill_card=1,
amount=29.95)

# Purchase on a previously used card
response = gateway.purchase(amount=29.95,
billing_id='0000080023748351')


# Refund a transaction - funds are transferred immediately
response = gateway.refund(dps_txn_ref='0000000809b61753',
merchant_ref='abc123',
amount=50.23)

Facade
------

The class ``paymentexpress.facade.Facade`` wraps the above gateway object and provides a less granular API, as well as saving instances of ``paymentexpress.models.OrderTransaction`` to provide an audit trail for PaymentExpress activity.


Settings
========

* ``PAYMENTEXPRESS_POST_URL`` - PX POST URL

* ``PAYMENTEXPRESS_USERNAME`` - Username

* ``PAYMENTEXPRESS_PASSWORD`` - Password

* ``PAYMENTEXPRESS_CURRENCY`` - Currency to use for transactions


Contributing
============

To work on ``django-oscar-paymentexpress``, clone the repo, set up a virtualenv and install in develop mode::

python setup.py develop

then install the testing dependencies::

pip install -r requirements.txt

The test suite can then be run using::

./run_tests.py

Magic card numbers are available on the PaymentExpress site:
http://www.paymentexpress.com/knowledge_base/faq/developer_faq.html#Testing%20Details

Sample VISA vard:

4111111111111111
2 changes: 1 addition & 1 deletion paymentexpress/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
PAYMENTEXPRESS = 'PaymentExpress'
PAYMENTEXPRESS = 'PaymentExpress'
1 change: 0 additions & 1 deletion paymentexpress/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
)
from paymentexpress.models import OrderTransaction

from oscar.apps.payment.utils import Bankcard
from oscar.apps.payment.exceptions import (UnableToTakePayment,
InvalidGatewayRequestError)
import random
Expand Down
1 change: 0 additions & 1 deletion paymentexpress/gateway.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from xml.dom.minidom import parseString, Document
from xml.parsers.expat import ExpatError
import requests
import re

Expand Down
49 changes: 49 additions & 0 deletions paymentexpress/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models

class Migration(SchemaMigration):

def forwards(self, orm):

# Adding model 'OrderTransaction'
db.create_table('paymentexpress_ordertransaction', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('order_number', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, db_index=True)),
('txn_type', self.gf('django.db.models.fields.CharField')(max_length=12)),
('txn_ref', self.gf('django.db.models.fields.CharField')(max_length=16)),
('amount', self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=12, decimal_places=2, blank=True)),
('response_code', self.gf('django.db.models.fields.CharField')(max_length=2)),
('response_message', self.gf('django.db.models.fields.CharField')(max_length=255)),
('request_xml', self.gf('django.db.models.fields.TextField')()),
('response_xml', self.gf('django.db.models.fields.TextField')()),
('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
))
db.send_create_signal('paymentexpress', ['OrderTransaction'])


def backwards(self, orm):

# Deleting model 'OrderTransaction'
db.delete_table('paymentexpress_ordertransaction')


models = {
'paymentexpress.ordertransaction': {
'Meta': {'ordering': "('-date_created',)", 'object_name': 'OrderTransaction'},
'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order_number': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}),
'request_xml': ('django.db.models.fields.TextField', [], {}),
'response_code': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
'response_message': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'response_xml': ('django.db.models.fields.TextField', [], {}),
'txn_ref': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
'txn_type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
}
}

complete_apps = ['paymentexpress']
Empty file.
3 changes: 1 addition & 2 deletions run_tests.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
#!/usr/bin/env python
import sys
import os
from coverage import coverage
from optparse import OptionParser

from django.conf import settings, global_settings
from django.conf import settings

if not settings.configured:
paymentexpress_settings = {}
Expand Down
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
version='0.1',
url='https://github.com/tangentlabs/django-oscar-paymentexpress',
author="Francis Reyes",
author_email="francis.reyes@tangentone.com.au",
author_email="francis.reyes@tangentsnowball.com.au",
description="PaymentExpress payment module for django-oscar",
long_description=open('README.rst').read(),
keywords="Payment, PaymentExpress",
license='BSD',
packages=find_packages(exclude=['sandbox*', 'tests*']),
install_requires=['django-oscar>=0.1', ],
install_requires=['django-oscar>=0.3', 'requests>=0.13.5'],
include_package_data=True,
# See http://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=['Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: Unix',
'Operating System :: OS Independent',
'Programming Language :: Python']
)
4 changes: 2 additions & 2 deletions tests/facade_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from mock import Mock, patch

from paymentexpress.facade import Facade
from paymentexpress.gateway import Response, AUTH, PURCHASE
from paymentexpress.gateway import AUTH, PURCHASE
from paymentexpress.models import OrderTransaction
from tests import (XmlTestingMixin, CARD_VISA, SAMPLE_SUCCESSFUL_RESPONSE,
SAMPLE_DECLINED_RESPONSE, SAMPLE_ERROR_RESPONSE)
Expand Down Expand Up @@ -159,7 +159,7 @@ def test_declined_call_is_recorded(self):
SAMPLE_DECLINED_RESPONSE)
try:
self.facade.purchase('1001', 10.24, None, self.card)
except Exception, e:
except Exception:
pass
txn = OrderTransaction.objects.filter(order_number='1001')[0]
self.assertIsNotNone(txn)
Expand Down

0 comments on commit ab25c52

Please sign in to comment.