├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── datacash ├── __init__.py ├── admin.py ├── dashboard │ ├── __init__.py │ ├── app.py │ └── views.py ├── facade.py ├── gateway.py ├── locale │ └── en │ │ └── LC_MESSAGES │ │ └── django.po ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_fraudresponse.py │ ├── 0003_auto__del_unique_fraudresponse_t3m_id.py │ ├── 0004_auto__add_field_ordertransaction_currency.py │ └── __init__.py ├── models.py ├── templates │ └── datacash │ │ └── dashboard │ │ ├── fraudresponse_list.html │ │ ├── transaction_detail.html │ │ └── transaction_list.html ├── the3rdman │ ├── __init__.py │ ├── document.py │ ├── signals.py │ ├── utils.py │ └── views.py ├── urls.py └── xmlutils.py ├── docs ├── DataCash_Developers_Guide_2.38.pdf └── Gatekeeper Retail User Doc Version 1 3.pdf ├── makefile ├── release.sh ├── requirements.txt ├── runtests.py ├── sandbox ├── apps │ ├── __init__.py │ ├── app.py │ └── checkout │ │ ├── __init__.py │ │ ├── app.py │ │ ├── models.py │ │ └── views.py ├── fixtures │ ├── auth.json │ ├── books-catalogue.csv │ └── books.csv ├── manage.py ├── settings.py ├── templates │ └── checkout │ │ ├── payment_details.html │ │ ├── preview.html │ │ └── thank_you.html └── urls.py ├── setup.py ├── tests ├── __init__.py ├── facade_tests.py ├── fixtures.py ├── gateway_tests.py ├── integration_tests.py ├── model_tests.py ├── the3rdman_callback_tests.py ├── the3rdman_model_tests.py ├── the3rdman_tests.py └── urls.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = datacash 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.DS_Store 4 | *.swp 5 | integration.py 6 | dist/ 7 | .coverage 8 | htmlcov/* 9 | sandbox/public 10 | .coveralls.yml 11 | .tox 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - '2.7' 5 | 6 | env: 7 | matrix: 8 | - COMBO="Django==1.5.8 django-oscar==0.6.5" 9 | - COMBO="Django==1.5.8 django-oscar==0.7.2" 10 | - COMBO="Django==1.6.5 django-oscar==0.6.5" 11 | - COMBO="Django==1.6.5 django-oscar==0.7.2" 12 | - COMBO="Django==1.6.5 South==1.0 https://github.com/tangentlabs/django-oscar/archive/master.tar.gz#egg=django-oscar" 13 | - COMBO="https://www.djangoproject.com/download/1.7c2/tarball/#egg=Django https://github.com/tangentlabs/django-oscar/archive/master.tar.gz#egg=django-oscar" 14 | 15 | matrix: 16 | include: 17 | - python: '3.3' 18 | env: COMBO="https://www.djangoproject.com/download/1.7c2/tarball/#egg=Django https://github.com/tangentlabs/django-oscar/archive/master.tar.gz#egg=django-oscar" 19 | - python: '3.4' 20 | env: COMBO="https://www.djangoproject.com/download/1.7c2/tarball/#egg=Django https://github.com/tangentlabs/django-oscar/archive/master.tar.gz#egg=django-oscar" 21 | 22 | install: 23 | - pip install $COMBO -r requirements.txt 24 | - python setup.py develop 25 | 26 | script: 27 | - coverage run ./runtests.py 28 | 29 | after_success: 30 | - coveralls 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Tangent Communications PLC and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Tangent Communications PLC nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include LICENSE 3 | recursive-include datacash/templates *.html 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | Datacash package for django-oscar 3 | ================================= 4 | 5 | This package provides integration with the payment gateway, DataCash_. It is designed to 6 | work seamlessly with the e-commerce framework `django-oscar`_ but can be used without 7 | Oscar. 8 | 9 | It also supports batch fraud screening using The3rdMan. 10 | 11 | .. _DataCash: http://www.datacash.com/ 12 | .. _`django-oscar`: https://github.com/tangentlabs/django-oscar 13 | 14 | * `PyPI homepage`_ 15 | 16 | .. _`continuous integration status`: http://travis-ci.org/#!/tangentlabs/django-oscar-datacash 17 | .. _`PyPI homepage`: http://pypi.python.org/pypi/django-oscar-datacash/ 18 | 19 | .. image:: https://secure.travis-ci.org/tangentlabs/django-oscar-datacash.png 20 | :alt: Continuous integration 21 | :target: http://travis-ci.org/#!/tangentlabs/django-oscar-datacash?branch=master 22 | 23 | .. image:: https://coveralls.io/repos/tangentlabs/django-oscar-datacash/badge.png?branch=master 24 | :alt: Coverage 25 | :target: https://coveralls.io/r/tangentlabs/django-oscar-datacash 26 | 27 | Getting started 28 | =============== 29 | 30 | Sandbox 31 | ------- 32 | 33 | When following the below instructions, it may be helpful to browse the sandbox 34 | folder above as this is an example Oscar install which has been integrated with 35 | Datacash. 36 | 37 | Installation 38 | ------------ 39 | 40 | From PyPi:: 41 | 42 | pip install django-oscar-datacash 43 | 44 | or from Github:: 45 | 46 | pip install git+git://github.com/tangentlabs/django-oscar-datacash.git#egg=django-oscar-datacash 47 | 48 | Add ``'datacash'`` to ``INSTALLED_APPS`` and run:: 49 | 50 | ./manage.py migrate datacash 51 | 52 | to create the appropriate database tables. 53 | 54 | Configuration 55 | ------------- 56 | 57 | Edit your ``settings.py`` to set the following settings:: 58 | 59 | DATACASH_HOST = 'testserver.datacash.com' 60 | DATACASH_CLIENT = '...' 61 | DATACASH_PASSWORD = '...' 62 | DATACASH_CURRENCY = 'GBP' 63 | 64 | There are other settings available (see below). Obviously, you'll need to 65 | specify different settings in your test environment as opposed to your 66 | production environment. 67 | 68 | Integration into checkout 69 | ------------------------- 70 | 71 | You'll need to use a subclass of ``oscar.apps.checkout.views.PaymentDetailsView`` within your own 72 | checkout views. See `Oscar's documentation`_ on how to create a local version of the checkout app. 73 | 74 | .. _`Oscar's documentation`: http://django-oscar.readthedocs.org/en/latest/index.html 75 | 76 | Override the ``handle_payment`` method (which does nothing by default) and add your integration code. See 77 | `the sandbox site`_ for an example of integrating Datacash payments 78 | into an Oscar site. 79 | 80 | .. _`the sandbox site`: https://github.com/tangentlabs/django-oscar-datacash/tree/master/sandbox 81 | 82 | Logging 83 | ------- 84 | 85 | The gateway modules uses the named logger ``datacash``. 86 | 87 | The3rdMan callbacks use the named logger ``datacash.the3rdman``. It is 88 | recommended that you use ``django.utils.log.AdminMailHandler`` with this logger 89 | to ensure error emails are sent out for 500 responses. 90 | 91 | Integration trouble-shooting 92 | ---------------------------- 93 | 94 | Many Datacash features require your merchant account to be configured correctly. 95 | For instance, the default Datacash set-up won't include: 96 | 97 | * Payments using historic transactions 98 | * Split settlements 99 | 100 | When investigating problems, make sure your Datacash account is set-up 101 | correctly. 102 | 103 | Integration with The3rdMan 104 | -------------------------- 105 | 106 | Using realtime fraud services requires submitting a dict of relevant data as part 107 | of the initial transaction. A helper method is provided that will extract all 108 | it needs from Oscar's models: 109 | 110 | .. code:: python 111 | 112 | from datacash.the3rdman import build_data_dict 113 | 114 | fraud_data = build_data_dict( 115 | request=request, 116 | order_number='1234', 117 | basket=request.basket, 118 | email=email 119 | shipping_address=shipping_address, 120 | billing_addres=billing_address) 121 | 122 | then pass this data as a named argument when creating the transaction: 123 | 124 | .. code:: python 125 | 126 | ref = Facade().pre_authorise(..., the3rdman_data=fraud_data) 127 | 128 | To receive the callback, include the following in your ``urls.py``: 129 | 130 | .. code:: python 131 | 132 | urlpatterns = patterns('', 133 | ... 134 | (r'^datacash/', include('datacash.urls')), 135 | ... 136 | ) 137 | 138 | When a fraud response is received, a custom signal is raised which your client 139 | code should listen for. Example: 140 | 141 | .. code:: python 142 | 143 | from django.dispatch import receiver 144 | from datacash.the3rdman import signals 145 | 146 | @receiver(signals.response_received) 147 | def handle_fraud_response(sender, response, **kwargs): 148 | # Do something with response 149 | 150 | Packages structure 151 | ================== 152 | 153 | There are two key components: 154 | 155 | Gateway 156 | ------- 157 | 158 | The class ``datacash.gateway.Gateway`` provides fine-grained access to the 159 | various DataCash APIs, which involve constructing XML requests and decoding XML 160 | responses. All calls return a ``datacash.gateway.Response`` instance which 161 | provides dictionary-like access to the attributes of the response. 162 | 163 | Example calls: 164 | 165 | .. code:: python 166 | 167 | from decimal import Decimal as D 168 | from datacash.gateway import Gateway 169 | 170 | gateway = Gateway() 171 | 172 | # Single stage processing 173 | response = gateway.auth(amount=D('100.00'), currency='GBP', 174 | merchant_reference='AA_1234', 175 | card_number='4500203021916406', 176 | expiry_date='10/14', 177 | ccv='345') 178 | 179 | response = gateway.refund(amount=D('100.00'), currency='GBP', 180 | merchant_reference='AA_1234', 181 | card_number='4500203021916406', 182 | expiry_date='10/14', 183 | ccv='345') 184 | 185 | # Two-stage processing (using pre-registered card) 186 | response = gateway.pre(amount=D('50.00'), currency='GBP', 187 | previous_txn_reference='3000000088888888') 188 | response = gateway.fulfill(amount=D('50.00'), currency='GBP', 189 | txn_reference=response['datacash_reference']) 190 | 191 | The gateway object know nothing of Oscar's classes and can be used in a stand-alone 192 | manner. 193 | 194 | Facade 195 | ------ 196 | 197 | The class ``datacash.facade.Facade`` wraps the above gateway object and provides a 198 | less granular API, as well as saving instances of ``datacash.models.OrderTransaction`` to 199 | provide an audit trail for Datacash activity. 200 | 201 | Settings 202 | ======== 203 | 204 | * ``DATACASH_HOST`` - Host of DataCash server 205 | 206 | * ``DATACASH_CLIENT`` - Username 207 | 208 | * ``DATACASH_PASSWORD`` - Password 209 | 210 | * ``DATACASH_CURRENCY`` - Currency to use for transactions 211 | 212 | * ``DATACASH_USE_CV2AVS`` - Whether to pass CV2AVS data 213 | 214 | * ``DATACASH_CAPTURE_METHOD`` - The 'capture method' to use. Defaults to 'ecomm'. 215 | 216 | Contributing 217 | ============ 218 | 219 | To work on ``django-oscar-datacash``, clone the repo, set up a virtualenv and install 220 | in develop mode:: 221 | 222 | make install 223 | 224 | The test suite can then be run using:: 225 | 226 | ./runtests.py 227 | 228 | There is a sandbox Oscar site that can be used for development. Create it 229 | with:: 230 | 231 | make sandbox 232 | 233 | and browse it with:: 234 | 235 | python sandbox/manage.py runserver 236 | 237 | Magic card numbers are available on the Datacash site: 238 | https://testserver.datacash.com/software/download.cgi?show=magicnumbers 239 | 240 | Here's an example: 241 | 242 | 1000010000000007 243 | 244 | Have fun! 245 | 246 | Changelog 247 | ========= 248 | 249 | 0.8.3 250 | ----- 251 | 252 | * Added support for Python 3 253 | 254 | 0.8.2 255 | ----- 256 | 257 | * Added support for Django 1.7 258 | 259 | 0.8.1 260 | ----- 261 | * Drop use of deprecated bankcard attribute 262 | 263 | 0.8 264 | --- 265 | * Ensure compatibility with Oscar 0.7 266 | * Drop support for Oscar 0.4 and 0.5 267 | 268 | 0.7 269 | --- 270 | * Ensure compatibility with Django 1.6 and Oscar 0.6 271 | 272 | 0.6.2 273 | ----- 274 | * Ensure templates are compatible with Django 1.5 275 | * Ensure tests pass with Oscar 0.6 276 | 277 | 0.6.1 278 | ----- 279 | * Format country codes in fraud submission. They must be 3 digits. 280 | 281 | 0.6 282 | --- 283 | * Allow the transaction currency to be set pre transaction. This is to support 284 | the new multi-currency features of Oscar 0.6. 285 | 286 | 0.5.3 287 | ----- 288 | * Fix logging formatting bug 289 | 290 | 0.5.2 291 | ----- 292 | * Remove uniqueness constraint for 3rdman response 293 | * Add links to Gatekeeper from dashboard 294 | 295 | 0.5.1 296 | ----- 297 | * Adjust how the response type of callback is determined 298 | 299 | 0.5 300 | --- 301 | * Add support for The3rdMan fraud screening 302 | 303 | 0.4.2 304 | ----- 305 | * Fix mis-handling of datetimes introduced in 0.4.1 306 | 307 | 0.4.1 308 | ----- 309 | * Handle bankcard dates passed as ``datetime.datetime`` instances instead of 310 | strings. This is a compatability fix for Oscar 0.6 development. 311 | 312 | 0.4 313 | --- 314 | * Oscar 0.5 support 315 | 316 | 0.3.5 / 2012-07-08 317 | ------------------ 318 | * Merchants passwords now removed from saved raw request XML 319 | * A random int is now appended to the merchant ref to avoid having duplicates 320 | 321 | 0.3.4 / 2012-07-08 322 | ------------------ 323 | * Minor tweak to sort order of transactions in dashboard 324 | 325 | 0.3.2, 0.3.3 / 2012-06-13 326 | ------------------------- 327 | * Updated packaging to include HTML templates 328 | 329 | 0.3.1 / 2012-06-12 330 | ------------------ 331 | * Added handling for split shipment payments 332 | 333 | 0.3 / 2012-05-10 334 | ---------------- 335 | * Added sandbox site 336 | * Added dashboard view of transactions 337 | 338 | 0.2.3 / 2012-05-09 339 | ------------------ 340 | * Added admin.py 341 | * Added travis.ci support 342 | 343 | 0.2.2 / 2012-02-14 344 | ------------------ 345 | * Fixed bug with currency in refund transactions 346 | 347 | 0.2.1 / 2012-02-7 348 | ------------------ 349 | * Fixed issue with submitting currency attribute for historic transactions 350 | -------------------------------------------------------------------------------- /datacash/__init__.py: -------------------------------------------------------------------------------- 1 | DATACASH = 'Datacash' 2 | -------------------------------------------------------------------------------- /datacash/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from datacash.models import OrderTransaction 3 | 4 | 5 | class OrderTransactionAdmin(admin.ModelAdmin): 6 | readonly_fields = ('order_number', 'method', 'amount', 'merchant_reference', 7 | 'datacash_reference', 'auth_code', 'status', 'reason', 8 | 'request_xml', 'response_xml', 'date_created') 9 | 10 | 11 | admin.site.register(OrderTransaction, OrderTransactionAdmin) -------------------------------------------------------------------------------- /datacash/dashboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-oscar/django-oscar-datacash/0e153b45db0f7e40d9f84dc12c40d526ab3e47f1/datacash/dashboard/__init__.py -------------------------------------------------------------------------------- /datacash/dashboard/app.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | from django.contrib.admin.views.decorators import staff_member_required 3 | 4 | from oscar.core.application import Application 5 | 6 | from . import views 7 | 8 | 9 | class DatacashDashboardApplication(Application): 10 | name = None 11 | list_view = views.TransactionListView 12 | detail_view = views.TransactionDetailView 13 | fraud_list_view = views.FraudResponseListView 14 | 15 | def get_urls(self): 16 | urlpatterns = patterns('', 17 | url(r'^transactions/$', self.list_view.as_view(), 18 | name='datacash-transaction-list'), 19 | url(r'^transactions/(?P\d+)/$', self.detail_view.as_view(), 20 | name='datacash-transaction-detail'), 21 | url(r'^fraud-responses/$', self.fraud_list_view.as_view(), 22 | name='datacash-fraud-response-list'), 23 | ) 24 | return self.post_process_urls(urlpatterns) 25 | 26 | def get_url_decorator(self, url_name): 27 | return staff_member_required 28 | 29 | 30 | application = DatacashDashboardApplication() 31 | -------------------------------------------------------------------------------- /datacash/dashboard/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView, DetailView 2 | 3 | from datacash import models 4 | 5 | 6 | class TransactionListView(ListView): 7 | model = models.OrderTransaction 8 | context_object_name = 'transactions' 9 | template_name = 'datacash/dashboard/transaction_list.html' 10 | paginate_by = 20 11 | 12 | 13 | class TransactionDetailView(DetailView): 14 | model = models.OrderTransaction 15 | context_object_name = 'txn' 16 | template_name = 'datacash/dashboard/transaction_detail.html' 17 | 18 | 19 | class FraudResponseListView(ListView): 20 | model = models.FraudResponse 21 | context_object_name = 'responses' 22 | template_name = 'datacash/dashboard/fraudresponse_list.html' 23 | paginate_by = 20 24 | -------------------------------------------------------------------------------- /datacash/facade.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from django.conf import settings 4 | from django.utils.translation import ugettext_lazy as _ 5 | from oscar.apps.payment.exceptions import UnableToTakePayment, InvalidGatewayRequestError 6 | 7 | from datacash import gateway 8 | from datacash.models import OrderTransaction 9 | 10 | 11 | class Facade(object): 12 | """ 13 | A bridge between oscar's objects and the core gateway object 14 | """ 15 | 16 | def __init__(self): 17 | self.gateway = gateway.Gateway( 18 | settings.DATACASH_HOST, 19 | getattr(settings, 'DATACASH_PATH', '/Transaction'), 20 | settings.DATACASH_CLIENT, 21 | settings.DATACASH_PASSWORD, 22 | getattr(settings, 'DATACASH_USE_CV2AVS', False), 23 | getattr(settings, 'DATACASH_CAPTURE_METHOD', 'ecomm')) 24 | 25 | def handle_response(self, method, order_number, amount, currency, response): 26 | 27 | # Maintain audit trail 28 | self.record_txn(method, order_number, amount, currency, response) 29 | 30 | # A response is either successful, declined or an error 31 | if response.is_successful(): 32 | return response['datacash_reference'] 33 | elif response.is_declined(): 34 | msg = self.get_friendly_decline_message(response) 35 | raise UnableToTakePayment(msg) 36 | else: 37 | # If there's a customer friendly version of this error, raise 38 | # UnableToTakePayment so that the error gets shown to the customer, 39 | # otherwise raise InvalidGatewayRequestError (a subclass of 40 | # PaymentError) which will show a generic error message. 41 | friendly_msg = self.get_friendly_error_message(response) 42 | if friendly_msg: 43 | raise UnableToTakePayment(friendly_msg) 44 | raise InvalidGatewayRequestError(response.reason) 45 | 46 | def record_txn(self, method, order_number, amount, currency, response): 47 | OrderTransaction.objects.create( 48 | order_number=order_number, 49 | method=method, 50 | datacash_reference=response['datacash_reference'], 51 | merchant_reference=response['merchant_reference'], 52 | amount=amount, 53 | currency=currency or "", 54 | auth_code=response['auth_code'], 55 | status=response.status, 56 | reason=response['reason'], 57 | request_xml=response.request_xml, 58 | response_xml=response.response_xml) 59 | 60 | def get_friendly_decline_message(self, response): 61 | return _('The transaction was declined by your bank - ' 62 | 'please check your bankcard details and try again') 63 | 64 | def get_friendly_error_message(self, response): 65 | # TODO: expand this dict to handle the most common errors 66 | errors = { 67 | 56: _('This transaction was submitted too soon after the ' 68 | 'previous one. Please wait for a minute then try again'), 69 | 59: _("We currently don't support your bankcard type. If " 70 | "possible, please use a different bankcard to try again"), 71 | 19: _('Unable to fulfill transaction'), 72 | } 73 | return errors.get(response.status) 74 | 75 | def extract_address_data(self, address): 76 | data = {} 77 | if not address: 78 | return data 79 | for i in range(1, 5): 80 | key = 'line%d' % i 81 | if hasattr(address, key): 82 | data['address_line%d' % i] = getattr(address, key) 83 | data['postcode'] = address.postcode 84 | return data 85 | 86 | # ======================== 87 | # API - 2 stage processing 88 | # ======================== 89 | 90 | def pre_authorise(self, order_number, amount, bankcard=None, 91 | txn_reference=None, billing_address=None, 92 | the3rdman_data=None, currency=None): 93 | """ 94 | Ring-fence an amount of money from the given card. This is the first 95 | stage of a two-stage payment process. A further call to fulfill is 96 | required to debit the money. 97 | 98 | As there are SO MANY values that can be submitted to 3rdMan, a separate 99 | dict of data must be submitted as a kwarg. 100 | """ 101 | if amount == 0: 102 | raise UnableToTakePayment("Order amount must be non-zero") 103 | if currency is None: 104 | currency = settings.DATACASH_CURRENCY 105 | merchant_ref = self.merchant_reference(order_number, gateway.PRE) 106 | address_data = self.extract_address_data(billing_address) 107 | if bankcard: 108 | response = self.gateway.pre( 109 | card_number=bankcard.number, 110 | expiry_date=bankcard.expiry_date, 111 | amount=amount, 112 | currency=currency, 113 | merchant_reference=merchant_ref, 114 | ccv=bankcard.ccv, 115 | the3rdman_data=the3rdman_data, 116 | **address_data) 117 | elif txn_reference: 118 | response = self.gateway.pre( 119 | amount=amount, 120 | currency=currency, 121 | merchant_reference=merchant_ref, 122 | previous_txn_reference=txn_reference, 123 | the3rdman_data=the3rdman_data, 124 | **address_data) 125 | else: 126 | raise ValueError( 127 | "You must specify either a bankcard or a previous txn reference") 128 | return self.handle_response( 129 | gateway.PRE, order_number, amount, currency, response) 130 | 131 | def merchant_reference(self, order_number, method): 132 | # Determine the previous number of these transactions. 133 | num_previous = OrderTransaction.objects.filter( 134 | order_number=order_number, method=method).count() 135 | # Get a random number to append to the end. This solves the problem 136 | # where a previous request crashed out and didn't save a model 137 | # instance. Hence we can get a clash of merchant references. 138 | rand = "%04.f" % (random.random() * 10000) 139 | return u'%s_%s_%d_%s' % (order_number, method.upper(), num_previous+1, 140 | rand) 141 | 142 | def fulfill_transaction(self, order_number, amount, txn_reference, 143 | auth_code, currency=None): 144 | """ 145 | Settle a previously ring-fenced transaction 146 | """ 147 | if currency is None: 148 | currency = settings.DATACASH_CURRENCY 149 | # Split shipments require that fulfills after the first one must have a 150 | # different merchant reference to the original 151 | merchant_ref = self.merchant_reference(order_number, gateway.FULFILL) 152 | response = self.gateway.fulfill(amount=amount, 153 | currency=currency, 154 | merchant_reference=merchant_ref, 155 | txn_reference=txn_reference, 156 | auth_code=auth_code) 157 | return self.handle_response(gateway.FULFILL, order_number, amount, 158 | currency, response) 159 | 160 | def refund_transaction(self, order_number, amount, txn_reference, 161 | currency=None): 162 | """ 163 | Refund against a previous ransaction 164 | """ 165 | if currency is None: 166 | currency = settings.DATACASH_CURRENCY 167 | response = self.gateway.txn_refund(amount=amount, 168 | currency=currency, 169 | txn_reference=txn_reference) 170 | return self.handle_response(gateway.TXN_REFUND, order_number, amount, 171 | currency, response) 172 | 173 | def cancel_transaction(self, order_number, txn_reference): 174 | """ 175 | Refund against a previous ransaction 176 | """ 177 | response = self.gateway.cancel(txn_reference) 178 | return self.handle_response( 179 | gateway.CANCEL, order_number, None, None, response) 180 | 181 | # ======================== 182 | # API - 1 stage processing 183 | # ======================== 184 | 185 | def authorise(self, order_number, amount, bankcard=None, txn_reference=None, 186 | billing_address=None, the3rdman_data=None, currency=None): 187 | """ 188 | Debit a bankcard for the given amount 189 | 190 | A bankcard object or a txn_reference can be passed depending on whether 191 | you are using a new or existing bankcard. 192 | """ 193 | if amount == 0: 194 | raise UnableToTakePayment("Order amount must be non-zero") 195 | if currency is None: 196 | currency = settings.DATACASH_CURRENCY 197 | 198 | merchant_ref = self.merchant_reference(order_number, gateway.AUTH) 199 | address_data = self.extract_address_data(billing_address) 200 | if bankcard: 201 | response = self.gateway.auth(card_number=bankcard.number, 202 | expiry_date=bankcard.expiry_date, 203 | amount=amount, 204 | currency=currency, 205 | merchant_reference=merchant_ref, 206 | ccv=bankcard.ccv, 207 | the3rdman_data=the3rdman_data, 208 | **address_data) 209 | elif txn_reference: 210 | response = self.gateway.auth(amount=amount, 211 | currency=currency, 212 | merchant_reference=merchant_ref, 213 | previous_txn_reference=txn_reference, 214 | the3rdman_data=the3rdman_data, 215 | **address_data) 216 | else: 217 | raise ValueError( 218 | "You must specify either a bankcard or a previous txn reference") 219 | 220 | return self.handle_response(gateway.AUTH, order_number, amount, 221 | currency, response) 222 | 223 | def refund(self, order_number, amount, bankcard=None, txn_reference=None, 224 | currency=None): 225 | """ 226 | Return funds to a bankcard 227 | """ 228 | if currency is None: 229 | currency = settings.DATACASH_CURRENCY 230 | merchant_ref = self.merchant_reference(order_number, gateway.REFUND) 231 | if bankcard: 232 | response = self.gateway.refund(card_number=bankcard.number, 233 | expiry_date=bankcard.expiry_date, 234 | amount=amount, 235 | currency=currency, 236 | merchant_reference=merchant_ref, 237 | ccv=bankcard.ccv) 238 | elif txn_reference: 239 | response = self.gateway.refund( 240 | amount=amount, currency=currency, 241 | merchant_reference=merchant_ref, 242 | previous_txn_reference=txn_reference) 243 | else: 244 | raise ValueError("You must specify either a bankcard or a previous txn reference") 245 | return self.handle_response(gateway.REFUND, order_number, amount, 246 | currency, response) 247 | -------------------------------------------------------------------------------- /datacash/gateway.py: -------------------------------------------------------------------------------- 1 | from xml.dom.minidom import Document, parseString 2 | 3 | from six.moves import http_client 4 | import re 5 | import logging 6 | import datetime 7 | from django.utils.encoding import python_2_unicode_compatible 8 | 9 | from oscar.apps.payment.exceptions import GatewayError 10 | 11 | from . import the3rdman, xmlutils 12 | 13 | logger = logging.getLogger('datacash') 14 | 15 | # Methods 16 | AUTH = 'auth' 17 | PRE = 'pre' 18 | REFUND = 'refund' 19 | ERP = 'erp' 20 | CANCEL = 'cancel' 21 | FULFILL = 'fulfill' 22 | TXN_REFUND = 'txn_refund' 23 | 24 | # Status codes 25 | ACCEPTED, DECLINED, INVALID_CREDENTIALS = '1', '7', '10' 26 | 27 | 28 | @python_2_unicode_compatible 29 | class Response(object): 30 | """ 31 | Encapsulate a Datacash response 32 | """ 33 | 34 | def __init__(self, request_xml, response_xml): 35 | self.request_xml = request_xml 36 | self.response_xml = response_xml 37 | self.data = self._extract_data(response_xml) 38 | 39 | def _extract_data(self, response_xml): 40 | doc = parseString(response_xml) 41 | data = {'status': self._get_element_text(doc, 'status'), 42 | 'datacash_reference': self._get_element_text(doc, 'datacash_reference'), 43 | 'merchant_reference': self._get_element_text(doc, 'merchantreference'), 44 | 'reason': self._get_element_text(doc, 'reason'), 45 | 'card_scheme': self._get_element_text(doc, 'card_scheme'), 46 | 'country': self._get_element_text(doc, 'country'), 47 | 'auth_code': self._get_element_text(doc, 'authcode') 48 | } 49 | return data 50 | 51 | def _get_element_text(self, doc, tag): 52 | try: 53 | ele = doc.getElementsByTagName(tag)[0] 54 | except IndexError: 55 | return None 56 | return ele.firstChild.data 57 | 58 | def __getitem__(self, key): 59 | return self.data[key] 60 | 61 | def __contains__(self, key): 62 | return key in self.data 63 | 64 | def __str__(self): 65 | return self.response_xml 66 | 67 | @property 68 | def reason(self): 69 | return self.data['reason'] 70 | 71 | @property 72 | def datacash_reference(self): 73 | return self.data['datacash_reference'] 74 | 75 | @property 76 | def status(self): 77 | if 'status' in self.data and self.data['status'] is not None: 78 | return int(self.data['status']) 79 | return None 80 | 81 | def is_successful(self): 82 | return self.data.get('status', None) == ACCEPTED 83 | 84 | def is_declined(self): 85 | return self.data.get('status', None) == DECLINED 86 | 87 | 88 | class Gateway(object): 89 | 90 | def __init__(self, host, path, client, password, cv2avs=False, capturemethod='ecomm'): 91 | if host.startswith('http'): 92 | raise RuntimeError("DATACASH_HOST should not include http") 93 | self._host = host 94 | self._path = path 95 | self._client = client 96 | self._password = password 97 | self._cv2avs = cv2avs 98 | self._capturemethod = capturemethod 99 | 100 | def _fetch_response_xml(self, request_xml): 101 | # Need to fill in HTTP request here 102 | conn = http_client.HTTPSConnection(self._host, 443, timeout=30) 103 | headers = {"Content-type": "application/xml", 104 | "Accept": ""} 105 | conn.request("POST", self._path, request_xml.encode('utf8'), headers) 106 | response = conn.getresponse() 107 | response_xml = response.read() 108 | if response.status != http_client.OK: 109 | raise GatewayError("Unable to communicate with payment gateway (code: %s, response: %s)" % (response.status, response_xml)) 110 | conn.close() 111 | return response_xml 112 | 113 | def _build_request_xml(self, method_name, **kwargs): 114 | """ 115 | Builds the XML for a transaction 116 | """ 117 | doc = Document() 118 | req = self._create_element(doc, doc, 'Request') 119 | 120 | # Authentication 121 | auth = self._create_element(doc, req, 'Authentication') 122 | self._create_element(doc, auth, 'client', self._client) 123 | self._create_element(doc, auth, 'password', self._password) 124 | 125 | # Transaction 126 | txn = self._create_element(doc, req, 'Transaction') 127 | 128 | # CardTxn 129 | if 'card_number' in kwargs or 'previous_txn_reference' in kwargs: 130 | card_txn = self._create_element(doc, txn, 'CardTxn') 131 | self._create_element(doc, card_txn, 'method', method_name) 132 | 133 | if 'card_number' in kwargs: 134 | card = self._create_element(doc, card_txn, 'Card') 135 | self._create_element(doc, card, 'pan', kwargs['card_number']) 136 | self._create_element(doc, card, 'expirydate', kwargs['expiry_date']) 137 | 138 | if 'start_date' in kwargs: 139 | self._create_element(doc, card, 'startdate', kwargs['start_date']) 140 | if 'issue_number' in kwargs: 141 | self._create_element(doc, card, 'issuenumber', kwargs['issue_number']) 142 | if 'auth_code' in kwargs: 143 | self._create_element(doc, card, 'authcode', kwargs['auth_code']) 144 | if self._cv2avs: 145 | self._add_cv2avs_elements(doc, card, kwargs) 146 | 147 | elif 'previous_txn_reference' in kwargs: 148 | self._create_element(doc, card_txn, 'card_details', kwargs['previous_txn_reference'], 149 | attributes={'type': 'preregistered'}) 150 | 151 | # HistoricTxn 152 | is_historic = False 153 | if 'txn_reference' in kwargs: 154 | is_historic = True 155 | historic_txn = self._create_element(doc, txn, 'HistoricTxn') 156 | self._create_element(doc, historic_txn, 'reference', kwargs['txn_reference']) 157 | self._create_element(doc, historic_txn, 'method', method_name) 158 | if 'auth_code' in kwargs: 159 | self._create_element(doc, historic_txn, 'authcode', kwargs['auth_code']) 160 | 161 | # TxnDetails 162 | txn_details = self._create_element(doc, txn, 'TxnDetails') 163 | if 'merchant_reference' in kwargs: 164 | self._create_element( 165 | doc, txn_details, 'merchantreference', 166 | kwargs['merchant_reference']) 167 | if 'amount' in kwargs: 168 | if is_historic: 169 | self._create_element(doc, txn_details, 'amount', 170 | str(kwargs['amount'])) 171 | else: 172 | self._create_element( 173 | doc, txn_details, 'amount', str(kwargs['amount']), 174 | {'currency': kwargs['currency']}) 175 | self._create_element( 176 | doc, txn_details, 'capturemethod', self._capturemethod) 177 | 178 | # The3rdMan 179 | if 'the3rdman_data' in kwargs and kwargs['the3rdman_data']: 180 | the3rdman.add_fraud_fields( 181 | doc, txn_details, **kwargs['the3rdman_data']) 182 | 183 | return doc.toxml() 184 | 185 | def _do_request(self, method, **kwargs): 186 | amount = kwargs.get('amount', '') 187 | merchant_ref = kwargs.get('merchant_reference', '') 188 | logger.info("Merchant ref %s - performing %s request for amount: %s", 189 | merchant_ref, method, amount) 190 | 191 | request_xml = self._build_request_xml(method, **kwargs) 192 | logger.debug("Merchant ref %s - request:\n %s", 193 | merchant_ref, request_xml) 194 | 195 | response_xml = self._fetch_response_xml(request_xml) 196 | logger.debug("Merchant ref %s - received response:\n %s", 197 | merchant_ref, response_xml) 198 | 199 | response = Response(request_xml, response_xml) 200 | if response.is_successful(): 201 | logger.info("Merchant ref %s - response successful, Datacash ref: %s", 202 | merchant_ref, response.datacash_reference) 203 | else: 204 | logger.warning("Merchant ref %s - response unsuccessful, Datacash ref: %s", 205 | merchant_ref, response.datacash_reference) 206 | return response 207 | 208 | def _add_cv2avs_elements(self, doc, card, kwargs): 209 | """ 210 | Add CV2AVS anti-fraud elements. Extended policy isn't 211 | handled yet. 212 | """ 213 | cv2avs = self._create_element(doc, card, 'Cv2Avs') 214 | for n in range(1, 5): 215 | key = 'address_line%d' % n 216 | if key in kwargs: 217 | self._create_element(doc, cv2avs, 'street_address%d' % n, kwargs[key]) 218 | if 'postcode' in kwargs: 219 | # Restrict size of postcode submitted 220 | self._create_element(doc, cv2avs, 'postcode', 221 | kwargs['postcode'][:9]) 222 | if 'ccv' in kwargs: 223 | self._create_element(doc, cv2avs, 'cv2', kwargs['ccv']) 224 | 225 | def _create_element(self, doc, parent, tag, value=None, attributes=None): 226 | """ 227 | Creates an XML element 228 | """ 229 | return xmlutils.create_element(doc, parent, tag, value, attributes) 230 | 231 | def _check_kwargs(self, kwargs, required_keys): 232 | for key in required_keys: 233 | if key not in kwargs: 234 | raise ValueError('You must provide a "%s" argument' % key) 235 | for key in kwargs: 236 | value = kwargs[key] 237 | if key == 'amount' and value == 0: 238 | raise ValueError('Amount must be non-zero') 239 | if key in ('expiry_date', 'start_date'): 240 | # Convert datetime instances if they have been passed. This is 241 | # really handling an upgrade issue for Oscar 0.6 where the 242 | # bankcard instance returns a datetime instead of a string. 243 | if isinstance(kwargs[key], datetime.date): 244 | kwargs[key] = kwargs[key].strftime("%m/%y") 245 | elif not re.match(r'^\d{2}/\d{2}$', value): 246 | raise ValueError("%s not in format mm/yy" % key) 247 | if key == 'issue_number' and not re.match(r'^\d{1,2}$', kwargs[key]): 248 | raise ValueError("Issue number must be one or two digits (passed value: %s)" % value) 249 | if key == 'currency' and not re.match(r'^[A-Z]{3}$', kwargs[key]): 250 | raise ValueError("Currency code must be a 3 character ISO 4217 code") 251 | if key == 'merchant_reference' and not (6 <= len(value) <= 32): 252 | raise ValueError("Merchant reference must be between 6 and 32 characters") 253 | 254 | # === 255 | # API 256 | # === 257 | 258 | # "Initial" transaction types 259 | 260 | def auth(self, **kwargs): 261 | """ 262 | Performs an 'auth' request, which is to debit the money immediately 263 | as a one-off transaction. 264 | 265 | Note that currency should be ISO 4217 Alphabetic format. 266 | """ 267 | self._check_kwargs(kwargs, ['amount', 'currency', 'merchant_reference']) 268 | return self._do_request(AUTH, **kwargs) 269 | 270 | def pre(self, **kwargs): 271 | """ 272 | Performs an 'pre' request, which is to ring-fence the requested money 273 | so it can be fulfilled at a later time. 274 | """ 275 | self._check_kwargs(kwargs, ['amount', 'currency', 'merchant_reference']) 276 | return self._do_request(PRE, **kwargs) 277 | 278 | def refund(self, **kwargs): 279 | """ 280 | Refund against a card 281 | """ 282 | self._check_kwargs(kwargs, ['amount', 'currency', 'merchant_reference']) 283 | return self._do_request(REFUND, **kwargs) 284 | 285 | def erp(self, **kwargs): 286 | self._check_kwargs(kwargs, ['amount', 'currency', 'merchant_reference']) 287 | return self._do_request(ERP, **kwargs) 288 | 289 | # "Historic" transaction types 290 | 291 | def cancel(self, txn_reference): 292 | """ 293 | Cancel an AUTH or PRE transaction. 294 | 295 | AUTH txns can only be cancelled before the end of the day when they 296 | are settled. 297 | """ 298 | return self._do_request(CANCEL, txn_reference=txn_reference) 299 | 300 | def fulfill(self, **kwargs): 301 | """ 302 | Settle a previous PRE transaction. The actual settlement will take place 303 | the next working day. 304 | """ 305 | self._check_kwargs(kwargs, ['amount', 'currency', 'txn_reference', 'auth_code']) 306 | return self._do_request(FULFILL, **kwargs) 307 | 308 | def txn_refund(self, **kwargs): 309 | """ 310 | Refund against a specific transaction 311 | """ 312 | self._check_kwargs(kwargs, ['amount', 'currency', 'txn_reference']) 313 | return self._do_request(TXN_REFUND, **kwargs) 314 | -------------------------------------------------------------------------------- /datacash/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-04-09 11:41+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: templates/dashboard/datacash/transaction_detail.html:6 21 | msgid "Datacash transaction" 22 | msgstr "" 23 | 24 | #: templates/dashboard/datacash/transaction_detail.html:12 25 | #: templates/dashboard/datacash/transaction_list.html:12 26 | msgid "Dashboard" 27 | msgstr "" 28 | 29 | #: templates/dashboard/datacash/transaction_detail.html:16 30 | msgid "Datacash" 31 | msgstr "" 32 | 33 | #: templates/dashboard/datacash/transaction_detail.html:25 34 | msgid "Transaction" 35 | msgstr "" 36 | 37 | #: templates/dashboard/datacash/transaction_detail.html:32 38 | #: templates/dashboard/datacash/transaction_list.html:30 39 | msgid "Datacash reference" 40 | msgstr "" 41 | 42 | #: templates/dashboard/datacash/transaction_detail.html:33 43 | #: templates/dashboard/datacash/transaction_list.html:31 44 | msgid "Order number" 45 | msgstr "" 46 | 47 | #: templates/dashboard/datacash/transaction_detail.html:34 48 | #: templates/dashboard/datacash/transaction_list.html:32 49 | msgid "Method" 50 | msgstr "" 51 | 52 | #: templates/dashboard/datacash/transaction_detail.html:35 53 | #: templates/dashboard/datacash/transaction_list.html:33 54 | msgid "Amount" 55 | msgstr "" 56 | 57 | #: templates/dashboard/datacash/transaction_detail.html:36 58 | #: templates/dashboard/datacash/transaction_list.html:34 59 | msgid "Merchant reference" 60 | msgstr "" 61 | 62 | #: templates/dashboard/datacash/transaction_detail.html:37 63 | #: templates/dashboard/datacash/transaction_list.html:35 64 | msgid "Auth code" 65 | msgstr "" 66 | 67 | #: templates/dashboard/datacash/transaction_detail.html:38 68 | #: templates/dashboard/datacash/transaction_list.html:36 69 | msgid "Status" 70 | msgstr "" 71 | 72 | #: templates/dashboard/datacash/transaction_detail.html:39 73 | #: templates/dashboard/datacash/transaction_list.html:37 74 | msgid "Reason" 75 | msgstr "" 76 | 77 | #: templates/dashboard/datacash/transaction_detail.html:40 78 | msgid "Request XML" 79 | msgstr "" 80 | 81 | #: templates/dashboard/datacash/transaction_detail.html:41 82 | msgid "Response XML" 83 | msgstr "" 84 | 85 | #: templates/dashboard/datacash/transaction_detail.html:42 86 | #: templates/dashboard/datacash/transaction_list.html:38 87 | msgid "Date" 88 | msgstr "" 89 | 90 | #: templates/dashboard/datacash/transaction_list.html:6 91 | #: templates/dashboard/datacash/transaction_list.html:15 92 | #: templates/dashboard/datacash/transaction_list.html:21 93 | msgid "Datacash transactions" 94 | msgstr "" 95 | 96 | #: templates/dashboard/datacash/transaction_list.html:55 97 | msgid "View" 98 | msgstr "" 99 | -------------------------------------------------------------------------------- /datacash/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'OrderTransaction' 12 | db.create_table('datacash_ordertransaction', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('order_number', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), 15 | ('method', self.gf('django.db.models.fields.CharField')(max_length=12)), 16 | ('amount', self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=12, decimal_places=2, blank=True)), 17 | ('merchant_reference', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)), 18 | ('datacash_reference', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)), 19 | ('auth_code', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)), 20 | ('status', self.gf('django.db.models.fields.PositiveIntegerField')()), 21 | ('reason', self.gf('django.db.models.fields.CharField')(max_length=255)), 22 | ('request_xml', self.gf('django.db.models.fields.TextField')()), 23 | ('response_xml', self.gf('django.db.models.fields.TextField')()), 24 | ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), 25 | )) 26 | db.send_create_signal('datacash', ['OrderTransaction']) 27 | 28 | 29 | def backwards(self, orm): 30 | 31 | # Deleting model 'OrderTransaction' 32 | db.delete_table('datacash_ordertransaction') 33 | 34 | 35 | models = { 36 | 'datacash.ordertransaction': { 37 | 'Meta': {'object_name': 'OrderTransaction'}, 38 | 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}), 39 | 'auth_code': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 40 | 'datacash_reference': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 41 | 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'merchant_reference': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 44 | 'method': ('django.db.models.fields.CharField', [], {'max_length': '12'}), 45 | 'order_number': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), 46 | 'reason': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 47 | 'request_xml': ('django.db.models.fields.TextField', [], {}), 48 | 'response_xml': ('django.db.models.fields.TextField', [], {}), 49 | 'status': ('django.db.models.fields.PositiveIntegerField', [], {}) 50 | } 51 | } 52 | 53 | complete_apps = ['datacash'] 54 | -------------------------------------------------------------------------------- /datacash/migrations/0002_auto__add_fraudresponse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'FraudResponse' 12 | db.create_table('datacash_fraudresponse', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('aggregator_identifier', self.gf('django.db.models.fields.CharField')(max_length=15, blank=True)), 15 | ('merchant_identifier', self.gf('django.db.models.fields.CharField')(max_length=15)), 16 | ('merchant_order_ref', self.gf('django.db.models.fields.CharField')(max_length=250, db_index=True)), 17 | ('t3m_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=128)), 18 | ('score', self.gf('django.db.models.fields.IntegerField')()), 19 | ('recommendation', self.gf('django.db.models.fields.IntegerField')()), 20 | ('message_digest', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), 21 | ('raw_response', self.gf('django.db.models.fields.TextField')()), 22 | ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), 23 | )) 24 | db.send_create_signal('datacash', ['FraudResponse']) 25 | 26 | 27 | def backwards(self, orm): 28 | # Deleting model 'FraudResponse' 29 | db.delete_table('datacash_fraudresponse') 30 | 31 | 32 | models = { 33 | 'datacash.fraudresponse': { 34 | 'Meta': {'object_name': 'FraudResponse'}, 35 | 'aggregator_identifier': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 36 | 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 37 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 38 | 'merchant_identifier': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 39 | 'merchant_order_ref': ('django.db.models.fields.CharField', [], {'max_length': '250', 'db_index': 'True'}), 40 | 'message_digest': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 41 | 'raw_response': ('django.db.models.fields.TextField', [], {}), 42 | 'recommendation': ('django.db.models.fields.IntegerField', [], {}), 43 | 'score': ('django.db.models.fields.IntegerField', [], {}), 44 | 't3m_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}) 45 | }, 46 | 'datacash.ordertransaction': { 47 | 'Meta': {'ordering': "('-date_created',)", 'object_name': 'OrderTransaction'}, 48 | 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}), 49 | 'auth_code': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 50 | 'datacash_reference': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 51 | 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 52 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 53 | 'merchant_reference': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 54 | 'method': ('django.db.models.fields.CharField', [], {'max_length': '12'}), 55 | 'order_number': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), 56 | 'reason': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 57 | 'request_xml': ('django.db.models.fields.TextField', [], {}), 58 | 'response_xml': ('django.db.models.fields.TextField', [], {}), 59 | 'status': ('django.db.models.fields.PositiveIntegerField', [], {}) 60 | } 61 | } 62 | 63 | complete_apps = ['datacash'] -------------------------------------------------------------------------------- /datacash/migrations/0003_auto__del_unique_fraudresponse_t3m_id.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Removing unique constraint on 'FraudResponse', fields ['t3m_id'] 12 | db.delete_unique('datacash_fraudresponse', ['t3m_id']) 13 | 14 | # Adding index on 'FraudResponse', fields ['t3m_id'] 15 | db.create_index('datacash_fraudresponse', ['t3m_id']) 16 | 17 | 18 | def backwards(self, orm): 19 | # Removing index on 'FraudResponse', fields ['t3m_id'] 20 | db.delete_index('datacash_fraudresponse', ['t3m_id']) 21 | 22 | # Adding unique constraint on 'FraudResponse', fields ['t3m_id'] 23 | db.create_unique('datacash_fraudresponse', ['t3m_id']) 24 | 25 | 26 | models = { 27 | 'datacash.fraudresponse': { 28 | 'Meta': {'ordering': "('-date_created',)", 'object_name': 'FraudResponse'}, 29 | 'aggregator_identifier': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 30 | 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 31 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 32 | 'merchant_identifier': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 33 | 'merchant_order_ref': ('django.db.models.fields.CharField', [], {'max_length': '250', 'db_index': 'True'}), 34 | 'message_digest': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 35 | 'raw_response': ('django.db.models.fields.TextField', [], {}), 36 | 'recommendation': ('django.db.models.fields.IntegerField', [], {}), 37 | 'score': ('django.db.models.fields.IntegerField', [], {}), 38 | 't3m_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) 39 | }, 40 | 'datacash.ordertransaction': { 41 | 'Meta': {'ordering': "('-date_created',)", 'object_name': 'OrderTransaction'}, 42 | 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}), 43 | 'auth_code': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 44 | 'datacash_reference': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 45 | 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 46 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 47 | 'merchant_reference': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 48 | 'method': ('django.db.models.fields.CharField', [], {'max_length': '12'}), 49 | 'order_number': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), 50 | 'reason': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 51 | 'request_xml': ('django.db.models.fields.TextField', [], {}), 52 | 'response_xml': ('django.db.models.fields.TextField', [], {}), 53 | 'status': ('django.db.models.fields.PositiveIntegerField', [], {}) 54 | } 55 | } 56 | 57 | complete_apps = ['datacash'] -------------------------------------------------------------------------------- /datacash/migrations/0004_auto__add_field_ordertransaction_currency.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | from django.conf import settings 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding field 'OrderTransaction.currency' 13 | db.add_column('datacash_ordertransaction', 'currency', 14 | self.gf('django.db.models.fields.CharField')(default=settings.DATACASH_CURRENCY, max_length=12), 15 | keep_default=False) 16 | 17 | 18 | def backwards(self, orm): 19 | # Deleting field 'OrderTransaction.currency' 20 | db.delete_column('datacash_ordertransaction', 'currency') 21 | 22 | 23 | models = { 24 | 'datacash.fraudresponse': { 25 | 'Meta': {'ordering': "('-date_created',)", 'object_name': 'FraudResponse'}, 26 | 'aggregator_identifier': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 27 | 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 28 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 29 | 'merchant_identifier': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 30 | 'merchant_order_ref': ('django.db.models.fields.CharField', [], {'max_length': '250', 'db_index': 'True'}), 31 | 'message_digest': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 32 | 'raw_response': ('django.db.models.fields.TextField', [], {}), 33 | 'recommendation': ('django.db.models.fields.IntegerField', [], {}), 34 | 'score': ('django.db.models.fields.IntegerField', [], {}), 35 | 't3m_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) 36 | }, 37 | 'datacash.ordertransaction': { 38 | 'Meta': {'ordering': "('-date_created',)", 'object_name': 'OrderTransaction'}, 39 | 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}), 40 | 'auth_code': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 41 | 'currency': ('django.db.models.fields.CharField', [], {'default': "'GBP'", 'max_length': '12'}), 42 | 'datacash_reference': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 43 | 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 44 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 45 | 'merchant_reference': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 46 | 'method': ('django.db.models.fields.CharField', [], {'max_length': '12'}), 47 | 'order_number': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), 48 | 'reason': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 49 | 'request_xml': ('django.db.models.fields.TextField', [], {}), 50 | 'response_xml': ('django.db.models.fields.TextField', [], {}), 51 | 'status': ('django.db.models.fields.PositiveIntegerField', [], {}) 52 | } 53 | } 54 | 55 | complete_apps = ['datacash'] 56 | -------------------------------------------------------------------------------- /datacash/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-oscar/django-oscar-datacash/0e153b45db0f7e40d9f84dc12c40d526ab3e47f1/datacash/migrations/__init__.py -------------------------------------------------------------------------------- /datacash/models.py: -------------------------------------------------------------------------------- 1 | import re 2 | from xml.dom.minidom import parseString 3 | from six.moves.urllib.parse import parse_qs 4 | 5 | from django.db import models 6 | from django.conf import settings 7 | from django.utils.encoding import python_2_unicode_compatible 8 | 9 | from .the3rdman import signals 10 | 11 | 12 | def prettify_xml(xml_str): 13 | xml_str = re.sub(r'\s*\n\s*', '', xml_str) 14 | ugly = parseString(xml_str.encode('utf8')).toprettyxml(indent=' ') 15 | regex = re.compile(r'>\n\s+([^<>\s].*?)\n\s+\g<1>%(hidden)s%(last4)s" % { 55 | 'element': matchobj.group(1), 56 | 'hidden': "X" * len(matchobj.group(2)), 57 | 'last4': matchobj.group(3), 58 | } 59 | 60 | def save(self, *args, **kwargs): 61 | # Ensure sensitive data isn't saved 62 | if not self.pk: 63 | cc_regex = re.compile(r'<(pan|alt_pan)>(\d+)(\d{4})') 64 | self.request_xml = cc_regex.sub(self._replace_credit_card_number, 65 | self.request_xml) 66 | ccv_regex = re.compile(r'\d+') 67 | self.request_xml = ccv_regex.sub('XXX', self.request_xml) 68 | pw_regex = re.compile(r'.*') 69 | self.request_xml = pw_regex.sub('XXX', self.request_xml) 70 | super(OrderTransaction, self).save(*args, **kwargs) 71 | 72 | def __str__(self): 73 | return u'%s txn for order %s - ref: %s, status: %s' % ( 74 | self.method.upper(), 75 | self.order_number, 76 | self.datacash_reference, 77 | self.status) 78 | 79 | @property 80 | def pretty_request_xml(self): 81 | return prettify_xml(self.request_xml) 82 | 83 | @property 84 | def pretty_response_xml(self): 85 | return prettify_xml(self.response_xml) 86 | 87 | @property 88 | def accepted(self): 89 | return self.status == 1 90 | 91 | @property 92 | def declined(self): 93 | return self.status == 7 94 | 95 | 96 | @python_2_unicode_compatible 97 | class FraudResponse(models.Model): 98 | aggregator_identifier = models.CharField(max_length=15, blank=True) 99 | merchant_identifier = models.CharField(max_length=15) 100 | merchant_order_ref = models.CharField(max_length=250, db_index=True) 101 | t3m_id = models.CharField(max_length=128, db_index=True) 102 | score = models.IntegerField() 103 | 104 | RELEASE, HOLD, REJECT, UNDER_INVESTIGATION = 0, 1, 2, 9 105 | recommendation = models.IntegerField() 106 | message_digest = models.CharField(max_length=128, blank=True) 107 | raw_response = models.TextField() 108 | date_created = models.DateTimeField(auto_now_add=True) 109 | 110 | def __str__(self): 111 | return u"t3m ID %s (score: %s, recommendation: %s)" % ( 112 | self.t3m_id, self.score, self.recommendation) 113 | 114 | class Meta: 115 | ordering = ('-date_created',) 116 | 117 | @classmethod 118 | def create_from_xml(cls, xml_string): 119 | """ 120 | Create a fraud response instance from an XML payload 121 | """ 122 | # Helper function for text extraction 123 | def tag_text(doc, tag_name): 124 | try: 125 | ele = doc.getElementsByTagName(tag_name)[0] 126 | except IndexError: 127 | return '' 128 | if ele.firstChild: 129 | return ele.firstChild.data 130 | return '' 131 | 132 | doc = parseString(xml_string) 133 | return cls.create_from_payload(xml_string, doc, tag_text) 134 | 135 | @classmethod 136 | def create_from_querystring(cls, query): 137 | """ 138 | Create a fraud response instance from a querystring payload 139 | """ 140 | def extract(data, key): 141 | return data.get(key, [""])[0] 142 | 143 | data = parse_qs(query) 144 | return cls.create_from_payload(query, data, extract) 145 | 146 | @classmethod 147 | def create_from_payload(cls, raw, payload, extract_fn): 148 | response = cls.objects.create( 149 | aggregator_identifier=extract_fn(payload, 'aggregator_identifier'), 150 | merchant_identifier=extract_fn(payload, 'merchant_identifier'), 151 | merchant_order_ref=extract_fn(payload, 'merchant_order_ref'), 152 | t3m_id=extract_fn(payload, 't3m_id'), 153 | score=int(extract_fn(payload, 'score')), 154 | recommendation=int(extract_fn(payload, 'recommendation')), 155 | message_digest=extract_fn(payload, 'message_digest'), 156 | raw_response=raw) 157 | 158 | # Raise signal so other processes can update orders based on this fraud 159 | # response. 160 | signals.response_received.send_robust(sender=cls, response=response) 161 | 162 | return response 163 | 164 | @property 165 | def on_hold(self): 166 | return self.recommendation == self.HOLD 167 | 168 | @property 169 | def released(self): 170 | return self.recommendation == self.RELEASE 171 | 172 | @property 173 | def rejected(self): 174 | return self.recommendation == self.REJECT 175 | 176 | @property 177 | def order_number(self): 178 | """ 179 | Return the order number from the original transaction. 180 | 181 | This assumes the merchant ref was generated using the datacash.facade 182 | class 183 | """ 184 | return self.merchant_order_ref.split("_")[0] 185 | 186 | @property 187 | def recommendation_text(self): 188 | mapping = { 189 | self.RELEASE: "Released", 190 | self.HOLD: "On hold", 191 | self.REJECT: "Rejected", 192 | self.UNDER_INVESTIGATION: "Under investigation", 193 | } 194 | return mapping.get(self.recommendation, "Unknown") 195 | 196 | @property 197 | def gatekeeper_url(self): 198 | """ 199 | Return the transaction detail URL on the Gatekeeper site 200 | """ 201 | is_live = 'mars' in settings.DATACASH_HOST 202 | host = 'cnpanalyst.com' if is_live else 'test.cnpanalyst.com' 203 | return 'https://%s/TransactionDetails.aspx?TID=%s' % ( 204 | host, self.t3m_id) 205 | -------------------------------------------------------------------------------- /datacash/templates/datacash/dashboard/fraudresponse_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard/layout.html' %} 2 | {% load currency_filters %} 3 | {% load url from future %} 4 | {% load i18n %} 5 | 6 | {% block title %} 7 | {% trans "Fraud responses" %} | {{ block.super }} 8 | {% endblock %} 9 | 10 | {% block breadcrumbs %} 11 | 18 | {% endblock %} 19 | 20 | {% block headertext %} 21 | {% trans "Fraud responses" %} 22 | {% endblock %} 23 | 24 | {% block dashboard_content %} 25 | {% if responses %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% for r in responses %} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {% endfor %} 54 | 55 |
{% trans "The3rdMan ID" %}{% trans "Merchant ref" %}{% trans "Order" %}{% trans "Aggregator ID" %}{% trans "Merchant ID" %}{% trans "Score" %}{% trans "Recommendation" %}{% trans "Date receieved" %}
{{ r.t3m_id }}{{ r.merchant_order_ref }}{{ r.order_number }}{{ r.aggregator_identifier|default:"-" }}{{ r.merchant_identifier }}{{ r.score }}{{ r.recommendation_text }}{{ r.date_created }}View on Gatekeeper site
56 | {% include "partials/pagination.html" %} 57 | {% else %} 58 |

{% trans "No fraud responses have been made yet." %}

59 | {% endif %} 60 | {% endblock dashboard_content %} 61 | -------------------------------------------------------------------------------- /datacash/templates/datacash/dashboard/transaction_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard/layout.html' %} 2 | {% load url from future %} 3 | {% load currency_filters %} 4 | {% load i18n %} 5 | 6 | {% block title %} 7 | {% trans "Datacash transaction" %} {{ txn.datacash_reference }} | {{ block.super }} 8 | {% endblock %} 9 | 10 | {% block breadcrumbs %} 11 | 22 | {% endblock %} 23 | 24 | {% block headertext %} 25 | {% trans "Transaction" %} {{ txn.datacash_reference }} 26 | {% endblock %} 27 | 28 | {% block dashboard_content %} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
{% trans "Datacash reference" %}{{ txn.datacash_reference }}
{% trans "Order number" %}{{ txn.order_number }}
{% trans "Method" %}{{ txn.method }}
{% trans "Amount" %}{{ txn.amount|currency:txn.currency }}
{% trans "Currency" %}{{ txn.currency }}
{% trans "Merchant reference" %}{{ txn.merchant_reference }}
{% trans "Auth code" %}{{ txn.auth_code|default:"-" }}
{% trans "Status" %}{{ txn.status }}
{% trans "Reason" %}{{ txn.reason }}
{% trans "Request XML" %}
{{ txn.pretty_request_xml }}
{% trans "Response XML" %}
{{ txn.pretty_response_xml }}
{% trans "Date" %}{{ txn.date_created }}
45 | {% endblock dashboard_content %} 46 | -------------------------------------------------------------------------------- /datacash/templates/datacash/dashboard/transaction_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'dashboard/layout.html' %} 2 | {% load url from future %} 3 | {% load i18n %} 4 | {% load currency_filters %} 5 | 6 | {% block title %} 7 | {% trans "Datacash transactions" %} | {{ block.super }} 8 | {% endblock %} 9 | 10 | {% block breadcrumbs %} 11 | 18 | {% endblock %} 19 | 20 | {% block headertext %} 21 | {% trans "Datacash transactions" %} 22 | {% endblock %} 23 | 24 | {% block dashboard_content %} 25 | {% if transactions %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for txn in transactions %} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | {% endfor %} 58 | 59 |
{% trans "Datacash reference" %}{% trans "Order number" %}{% trans "Method" %}{% trans "Amount" %}{% trans "Merchant reference" %}{% trans "Auth code" %}{% trans "Status" %}{% trans "Reason" %}{% trans "Date" %}
{{ txn.datacash_reference }}{{ txn.order_number }}{{ txn.method }}{{ txn.amount|currency:txn.currency }}{{ txn.merchant_reference|default:"-" }}{{ txn.auth_code|default:"-" }}{{ txn.status }}{{ txn.reason }}{{ txn.date_created }} 54 | {% trans "View" %} 55 |
60 | {% include "partials/pagination.html" %} 61 | {% else %} 62 |

{% trans "No transactions have been made yet." %}

63 | {% endif %} 64 | {% endblock dashboard_content %} 65 | -------------------------------------------------------------------------------- /datacash/the3rdman/__init__.py: -------------------------------------------------------------------------------- 1 | from .document import add_fraud_fields 2 | from .utils import build_data_dict 3 | -------------------------------------------------------------------------------- /datacash/the3rdman/document.py: -------------------------------------------------------------------------------- 1 | from datacash.xmlutils import create_element 2 | from xml.dom.minidom import Document, parseString 3 | 4 | 5 | def add_fraud_fields(doc=None, element=None, customer_info=None, delivery_info=None, 6 | billing_info=None, account_info=None, order_info=None, 7 | **kwargs): 8 | """ 9 | Submit a transaction to The3rdMan for Batch Fraud processing 10 | 11 | Doesn't support (yet): 12 | - Previous orders 13 | - Additional delivery addreses 14 | - Alternative payments 15 | 16 | See section 2.4.7 of the Developer Guide 17 | """ 18 | # Build the request XML 19 | if doc is None: 20 | doc = Document() 21 | if element is None: 22 | element = doc 23 | 24 | envelope = create_element( 25 | doc, element, 'The3rdMan', attributes={'type': 'realtime'}) 26 | 27 | if 'callback_url' in kwargs: 28 | callback_format = kwargs.get('callback_format', 'XML') 29 | callback_url = kwargs['callback_url'] 30 | add_realtime_information(doc, envelope, callback_format, callback_url) 31 | 32 | add_customer_information(doc, envelope, customer_info) 33 | add_delivery_address(doc, envelope, delivery_info) 34 | add_billing_address(doc, envelope, billing_info) 35 | add_account_information(doc, envelope, account_info) 36 | add_order_information(doc, envelope, order_info) 37 | return doc 38 | 39 | 40 | def add_realtime_information(doc, ele, format, url): 41 | rt_ele = create_element(doc, ele, 'Realtime') 42 | create_element(doc, rt_ele, 'real_time_callback_format', format) 43 | create_element(doc, rt_ele, 'real_time_callback', url) 44 | 45 | 46 | def add_xml_fields(doc, parent, fields, values): 47 | for field in fields: 48 | if field in values and values[field] is not None: 49 | create_element(doc, parent, field, values[field]) 50 | 51 | 52 | def intersects(fields, values): 53 | """ 54 | Test if there are any fields in the values 55 | """ 56 | overlap = set(fields).intersection(values.keys()) 57 | return len(overlap) > 0 58 | 59 | 60 | def add_customer_information(doc, envelope, customer_info): 61 | if not customer_info: 62 | return 63 | cust_ele = create_element(doc, envelope, 'CustomerInformation') 64 | cust_fields = ( 65 | 'alt_telephone', 'customer_dob', 'customer_reference', 66 | 'delivery_forename', 'delivery_phone_number', 'delivery_surname', 67 | 'delivery_title', 'driving_license_number', 'email', 68 | 'first_purchase_date', 'forename', 'introduced_by', 'ip_address', 69 | 'order_number', 'sales_channel', 'surname', 'telephone', 70 | 'time_zone', 'title') 71 | add_xml_fields(doc, cust_ele, cust_fields, customer_info) 72 | 73 | 74 | def add_delivery_address(doc, envelope, delivery_info): 75 | if not delivery_info: 76 | return 77 | delivery_ele = create_element(doc, envelope, 'DeliveryAddress') 78 | delivery_fields = ( 79 | 'street_address_1', 'street_address_2', 'city', 80 | 'county', 'postcode', 'country') 81 | add_xml_fields(doc, delivery_ele, delivery_fields, delivery_info) 82 | 83 | 84 | def add_billing_address(doc, envelope, billing_info): 85 | if not billing_info: 86 | return 87 | billing_ele = create_element(doc, envelope, 'BillingAddress') 88 | billing_fields = ( 89 | 'street_address_1', 'street_address_2', 'city', 'county', 90 | 'postcode', 'country') 91 | add_xml_fields(doc, billing_ele, billing_fields, billing_info) 92 | 93 | 94 | def add_account_information(doc, envelope, account_info): 95 | if not account_info: 96 | return 97 | account_ele = create_element(doc, envelope, 'AccountInformation') 98 | 99 | # Bank information 100 | bank_fields = ( 101 | 'account_number', 'bank_address', 'bank_country', 'bank_name', 102 | 'customer_name', 'sort_code') 103 | if intersects(bank_fields, account_info): 104 | bank_ele = create_element(doc, account_ele, 'BankInformation') 105 | add_xml_fields(doc, bank_ele, bank_fields, account_info) 106 | 107 | purchase_fields = ('avg', 'max', 'min') 108 | if intersects(purchase_fields, account_info): 109 | purchase_ele = create_element(doc, account_ele, 'PurchaseInformation') 110 | add_xml_fields(doc, purchase_ele, purchase_fields, account_info) 111 | 112 | 113 | def add_order_information(doc, envelope, order_info): 114 | if not order_info or 'products' not in order_info: 115 | return 116 | order_ele = create_element(doc, envelope, 'OrderInformation') 117 | products_ele = create_element( 118 | doc, order_ele, 'Products', 119 | attributes={'count': len(order_info['products'])}) 120 | product_fields = ('code', 'prod_id', 'quantity', 'price', 'prod_category', 121 | 'prod_description') 122 | for product_info in order_info['products']: 123 | product_ele = create_element(doc, products_ele, 'Product') 124 | add_xml_fields(doc, product_ele, product_fields, product_info) 125 | -------------------------------------------------------------------------------- /datacash/the3rdman/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | response_received = django.dispatch.Signal(providing_args=["response"]) 4 | -------------------------------------------------------------------------------- /datacash/the3rdman/utils.py: -------------------------------------------------------------------------------- 1 | def build_data_dict(request=None, user=None, email=None, order_number=None, 2 | basket=None, shipping_address=None, billing_address=None): 3 | """ 4 | Build a dict of the fields that can be passed as 3rdMan data 5 | """ 6 | if request and user is None: 7 | user = request.user 8 | if request and basket is None: 9 | basket = request.basket 10 | data = { 11 | 'customer_info': build_customer_info( 12 | request, user, email, order_number, shipping_address), 13 | 'delivery_info': build_delivery_info( 14 | shipping_address), 15 | 'billing_info': build_billing_info( 16 | billing_address), 17 | 'account_info': {}, # Not implemented for now 18 | 'order_info': build_order_info(basket), 19 | } 20 | return data 21 | 22 | 23 | def build_customer_info(request, user, email, order_number, shipping_address): 24 | # We only use fields denoted 'R' (for Retail) 25 | payload = {} 26 | 27 | if user and user.is_authenticated(): 28 | payload['customer_reference'] = user.id 29 | payload['email'] = user.email 30 | payload['forename'] = user.first_name 31 | payload['surname'] = user.last_name 32 | 33 | if email: 34 | payload['email'] = email 35 | 36 | if shipping_address: 37 | payload['delivery_forename'] = shipping_address.first_name 38 | payload['delivery_phone_number'] = shipping_address.phone_number 39 | payload['delivery_surname'] = shipping_address.last_name 40 | payload['delivery_title'] = shipping_address.title 41 | 42 | if request and 'REMOTE_ADDR' in request.META: 43 | payload['ip_address'] = request.META['REMOTE_ADDR'] 44 | # We let HTTP_X_FORWARDED_FOR take precedence if it exists 45 | if request and 'HTTP_X_FORWARDED_FOR' in request.META: 46 | payload['ip_address'] = request.META['HTTP_X_FORWARDED_FOR'] 47 | 48 | if order_number: 49 | payload['order_number'] = order_number 50 | 51 | payload['sales_channel'] = 3 # Internet 52 | 53 | return payload 54 | 55 | 56 | def build_delivery_info(shipping_address): 57 | payload = {} 58 | if not shipping_address: 59 | return payload 60 | payload['street_address_1'] = shipping_address.line1 61 | payload['street_address_2'] = shipping_address.line2 62 | payload['city'] = shipping_address.line4 63 | payload['county'] = shipping_address.state 64 | payload['postcode'] = shipping_address.postcode 65 | payload['country'] = u"%.03d" % shipping_address.country.iso_3166_1_numeric 66 | return payload 67 | 68 | 69 | def build_billing_info(billing_address): 70 | payload = {} 71 | if not billing_address: 72 | return payload 73 | payload['street_address_1'] = billing_address.line1 74 | payload['street_address_2'] = billing_address.line2 75 | payload['city'] = billing_address.line4 76 | payload['county'] = billing_address.state 77 | payload['postcode'] = billing_address.postcode 78 | return payload 79 | 80 | 81 | def build_order_info(basket): 82 | if not basket: 83 | return {} 84 | payload = {'products': []} 85 | for line in basket.all_lines(): 86 | product = line.product 87 | datum = { 88 | 'code': product.upc, 89 | 'price': line.price_incl_tax, 90 | 'prod_description': product.description, 91 | 'prod_id': product.id, 92 | 'quantity': line.quantity, 93 | } 94 | payload['products'].append(datum) 95 | return payload 96 | -------------------------------------------------------------------------------- /datacash/the3rdman/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.views import generic 4 | from django import http 5 | 6 | from datacash import models 7 | 8 | logger = logging.getLogger('datacash.the3rdman') 9 | 10 | 11 | class CallbackView(generic.View): 12 | """ 13 | Datacash will POST to this view when they have a fraud score 14 | for a transaction. This view must respond with a simple string 15 | response within 1 second for it to be acknowledged. 16 | """ 17 | 18 | def post(self, request, *args, **kwargs): 19 | # Create a fraud response object. Other processes should listen 20 | # to the post create signal in order to hook fraud processing into 21 | # this order pipeline. 22 | try: 23 | # Datacash send both XML and query string with the same content 24 | # type header :( so we have to check for XML syntax. 25 | if b' 0: 91 | sys.exit(num_failures) 92 | 93 | 94 | def generate_migration(): 95 | from south.management.commands.schemamigration import Command 96 | com = Command() 97 | com.handle(app='datacash', initial=True) 98 | 99 | 100 | if __name__ == '__main__': 101 | parser = OptionParser() 102 | (options, args) = parser.parse_args() 103 | run_tests(*args) 104 | -------------------------------------------------------------------------------- /sandbox/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-oscar/django-oscar-datacash/0e153b45db0f7e40d9f84dc12c40d526ab3e47f1/sandbox/apps/__init__.py -------------------------------------------------------------------------------- /sandbox/apps/app.py: -------------------------------------------------------------------------------- 1 | from oscar.app import Shop 2 | from apps.checkout.app import application as checkout_app 3 | 4 | 5 | class DatacashShop(Shop): 6 | # Specify a local checkout app where we override the payment details view 7 | checkout_app = checkout_app 8 | 9 | 10 | shop = DatacashShop() 11 | -------------------------------------------------------------------------------- /sandbox/apps/checkout/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-oscar/django-oscar-datacash/0e153b45db0f7e40d9f84dc12c40d526ab3e47f1/sandbox/apps/checkout/__init__.py -------------------------------------------------------------------------------- /sandbox/apps/checkout/app.py: -------------------------------------------------------------------------------- 1 | from oscar.apps.checkout.app import CheckoutApplication 2 | 3 | from apps.checkout import views 4 | 5 | 6 | class OverriddenCheckoutApplication(CheckoutApplication): 7 | # Specify new view for payment details 8 | payment_details_view = views.PaymentDetailsView 9 | 10 | 11 | application = OverriddenCheckoutApplication() 12 | -------------------------------------------------------------------------------- /sandbox/apps/checkout/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-oscar/django-oscar-datacash/0e153b45db0f7e40d9f84dc12c40d526ab3e47f1/sandbox/apps/checkout/models.py -------------------------------------------------------------------------------- /sandbox/apps/checkout/views.py: -------------------------------------------------------------------------------- 1 | from django import http 2 | from django.core.urlresolvers import reverse 3 | from django.contrib import messages 4 | from django.conf import settings 5 | from django.utils.translation import ugettext as _ 6 | 7 | from oscar.apps.checkout.views import PaymentDetailsView as OscarPaymentDetailsView 8 | from oscar.apps.payment.forms import BankcardForm 9 | from oscar.apps.payment.models import SourceType, Source 10 | from oscar.apps.order.models import ShippingAddress 11 | from oscar.apps.address.models import UserAddress 12 | 13 | from datacash.facade import Facade 14 | from datacash import the3rdman 15 | 16 | 17 | class PaymentDetailsView(OscarPaymentDetailsView): 18 | 19 | def get_context_data(self, **kwargs): 20 | # Add bankcard form to the template context 21 | ctx = super(PaymentDetailsView, self).get_context_data(**kwargs) 22 | ctx['bankcard_form'] = kwargs.get('bankcard_form', BankcardForm()) 23 | return ctx 24 | 25 | def handle_payment_details_submission(self, request): 26 | # Check bankcard form is valid 27 | bankcard_form = BankcardForm(request.POST) 28 | if bankcard_form.is_valid(): 29 | return self.render_preview( 30 | request, bankcard_form=bankcard_form) 31 | 32 | # Form invalid - re-render 33 | return self.render_payment_details( 34 | request, bankcard_form=bankcard_form) 35 | 36 | def handle_place_order_submission(self, request): 37 | bankcard_form = BankcardForm(request.POST) 38 | if bankcard_form.is_valid(): 39 | submission = self.build_submission( 40 | payment_kwargs={ 41 | 'bankcard_form': bankcard_form 42 | }) 43 | return self.submit(**submission) 44 | 45 | messages.error(request, _("Invalid submission")) 46 | return http.HttpResponseRedirect( 47 | reverse('checkout:payment-details')) 48 | 49 | def build_submission(self, **kwargs): 50 | # Ensure the shipping address is part of the payment keyword args 51 | submission = super(PaymentDetailsView, self).build_submission(**kwargs) 52 | submission['payment_kwargs']['shipping_address'] = submission[ 53 | 'shipping_address'] 54 | return submission 55 | 56 | def handle_payment(self, order_number, total, **kwargs): 57 | # Make request to DataCash - if there any problems (eg bankcard 58 | # not valid / request refused by bank) then an exception would be 59 | # raised and handled) 60 | facade = Facade() 61 | 62 | # Use The3rdMan - so build a dict of data to pass 63 | # email = None 64 | # if not self.request.user.is_authenticated(): 65 | # email = self.checkout_session.get_guest_email() 66 | # fraud_data = the3rdman.build_data_dict( 67 | # request=self.request, 68 | # email=email, 69 | # order_number=order_number, 70 | # shipping_address=kwargs['shipping_address']) 71 | 72 | # We're not using 3rd-man by default 73 | bankcard = kwargs['bankcard_form'].bankcard 74 | datacash_ref = facade.pre_authorise( 75 | order_number, total.incl_tax, bankcard) 76 | 77 | # Request was successful - record the "payment source". As this 78 | # request was a 'pre-auth', we set the 'amount_allocated' - if we had 79 | # performed an 'auth' request, then we would set 'amount_debited'. 80 | source_type, _ = SourceType.objects.get_or_create(name='Datacash') 81 | source = Source(source_type=source_type, 82 | currency=settings.DATACASH_CURRENCY, 83 | amount_allocated=total.incl_tax, 84 | reference=datacash_ref) 85 | self.add_payment_source(source) 86 | 87 | # Also record payment event 88 | self.add_payment_event('pre-auth', total.incl_tax) 89 | -------------------------------------------------------------------------------- /sandbox/fixtures/auth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "superuser", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": true, 11 | "is_staff": true, 12 | "last_login": "2012-09-12T17:13:49Z", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "pbkdf2_sha256$10000$ZMgucNrRNjms$6GCMZaItv61aATkdd6+qA1banOUPrxF2P0rCkLqVRSk=", 16 | "email": "superuser@example.com", 17 | "date_joined": "2012-09-12T17:00:02Z" 18 | } 19 | }, 20 | { 21 | "pk": 2, 22 | "model": "auth.user", 23 | "fields": { 24 | "username": "staff", 25 | "first_name": "", 26 | "last_name": "", 27 | "is_active": true, 28 | "is_superuser": false, 29 | "is_staff": true, 30 | "last_login": "2012-09-12T17:11:20Z", 31 | "groups": [], 32 | "user_permissions": [], 33 | "password": "pbkdf2_sha256$10000$EhDNXhiM1P6f$eVSrbJxxdsflcd9Cl9ysN13lQHR/EtRPgp5+ZRRDZgU=", 34 | "email": "staff@example.com", 35 | "date_joined": "2012-09-12T17:08:56Z" 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /sandbox/fixtures/books.csv: -------------------------------------------------------------------------------- 1 | Book,Books > Non-Fiction > Essential programming,9780131103627,The C Programming Language,"As we said in the first preface to the first edition, C ""wears well as one's experience with it grows."" With a decade more experience, we still feel that way. We hope that this book will help you to learn C and use it well.",Book partner,9780131103627,9.99,20 2 | Book,Books > Non-Fiction > Essential programming,9780201616224,Alfresco 3 Web Services,... urn:isbn:978020161622420100114T14:20:24+0100 ThePragmaticProgrammerDavidThomas Andrew Hunt Non-Fiction > Essential programming,9780201633610,Design Patterns,"With Design Patterns as your guide, you will learn how these important patterns fit into the software development process, and how you can leverage them to solve your own design problems most efficiently.",Book partner,9780201633610,23.99,74 4 | Book,Books > Non-Fiction > Essential programming,9780262510875,Structure and Interpretation of Computer Programs,"This long-awaited revision contains changesthroughout the text.There are new implementations of most of the major programming systems in thebook, including the interpreters and compilers, and the authors have incorporated many small ...",Book partner,9780262510875,23.99,54 5 | Book,Books > Non-Fiction > Essential programming,9780201485677,Refactoring,"improving the design of existing code M. Fowler. Object Technology/Software Engineering As the application of object technology— particularly the Java programming language— has become commonplace, a new problem has emerged to ...",Book partner,9780201485677,1.99,9 6 | Book,Books > Non-Fiction > Essential programming,9780201485417,The Art of Computer Programming: Sorting and searching,"Check out the boxed set that brings together Volumes 1 - 4A in one elegant case, and offers the purchaser a $50 discount off the price of buying the four volumes individually.",Book partner,9780201485417,18.99,37 7 | Book,Books > Non-Fiction > Essential programming,9780201835953,The mythical man-month,"These essays draw from his experience as project manager for the IBM System/360 computer family and then for OS/360, its massive software system.",Book partner,9780201835953,8.99,90 8 | Book,Books > Non-Fiction > Essential programming,9780735619678,"Code Complete, Second Edition","Features the best practices in the art and science of constructing software--topics include design, applying good techniques to construction, eliminating errors, planning, managing construction activities, and relating personal character to ...",Book partner,9780735619678,27.99,88 9 | Book,Books > Non-Fiction > Essential programming,9780132350884,Clean Code,"Looks at the principles and clean code, includes case studies showcasing the practices of writing clean code, and contains a list of heuristics and ""smells"" accumulated from the process of writing clean code.",Book partner,9780132350884,1.99,91 10 | Book,Books > Non-Fiction > Essential programming,9780201700732,The C++ programming language,"More than three-quarters of a million programmers have benefited from this book in all of its editions Written by Bjarne Stroustrup, the creator of C++, this is the world's most trusted and widely read book on C++.",Book partner,9780201700732,11.99,88 11 | Book,Books > Non-Fiction > Essential programming,9780596517748,JavaScript: The Good Parts,"If you develop sites or applications for the Web, this book is an absolute must.",Book partner,9780596517748,21.99,49 12 | Book,Books > Non-Fiction > Essential programming,9780201100884,"Compilers, principles, techniques, and tools",This book provides the foundation for understanding the theory and pracitce of compilers.,Book partner,9780201100884,15.99,2 13 | Book,Books > Non-Fiction > Essential programming,9780130313584,Современные операционные системы,"NEW-Over 200 references to books and papers published since the first edition. NEW-The Web site for this book contains PowerPoint slides, simulators, figures in various formats, and other teaching aids.",Book partner,9780130313584,16.99,23 14 | Book,Books > Non-Fiction > Essential programming,9780137903955,Artificial Intelligence,"All of this is available at: ""aima.cs.berkeley.edu""",Book partner,9780137903955,9.99,23 15 | Book,Books > Non-Fiction > Essential programming,9780262011532,Structure and Interpretation of Computer Programs,Structure and Interpretation of Computer Programs has had a dramatic impact on computer science curricula over the past decade. This long-awaited revision contains changes throughout the text.,Book partner,9780262011532,28.99,5 16 | Book,Books > Non-Fiction > Essential programming,9780139376818,The UNIX programming environment,"Most of the book is devoted to discussions of individual tools, but throughout run the themes of combining programs and of using programs to build programs--emphasizing how they fit in the environment.",Book partner,9780139376818,24.99,44 17 | Book,Books > Non-Fiction > Essential programming,9780596514983,Real World Haskell,"With this book, you will: Understand the differences between procedural and functional programming Learn the features of Haskell, and how to use it to develop useful programs Interact with filesystems, databases, and network services Write ...",Book partner,9780596514983,11.99,18 18 | Book,Books > Non-Fiction > Essential programming,9780262560993,Little LISPer,"Daniel Paul Friedman. True: as long as we use the names consistently, we are just fine. And mk-length is a far more equal name than length. If we use a name like mk-length, it is a constant reminder that the first argument to mk-length is ...",Book partner,9780262560993,21.99,31 19 | Book,Books > Non-Fiction > Essential programming,9780596007126,Head First Design Patterns,"Meanwhile,. back. at. the. PizzaStore. The design for the PizzaStore is really shaping up: it's got a flexible framework and it does a good job of adhering to design principles. Now, the key to Objectville Pizza's success has always been fresh, ...",Book partner,9780596007126,23.99,12 20 | Book,Books > Non-Fiction > Essential programming,9780131177055,Working Effectively With Legacy Code,"In this book, Michael Feathers offers start-to-finish strategies for working more effectively with large, untested legacy code bases.",Book partner,9780131177055,24.99,23 21 | Book,Books > Non-Fiction > Essential programming,9780262062183,How to design programs,"an introduction to programming and computing Matthias Felleisen. The abstraction: Next we replace the contents of corresponding pairs of boxes with new names and add these names to the parameter list. For example, if there are three pairs ...",Book partner,9780262062183,15.99,4 22 | Book,Books > Non-Fiction > Essential programming,9780201615869,La práctica de la programación,"Rob Pike. probably need to make changes to the main body of the code, and if you edit a copy, before long you will have divergent versions. As much as possible, there should only be a single source for a program; if you find you need to ...",Book partner,9780201615869,15.99,63 23 | Book,Books > Non-Fiction > Essential programming,9780974514055,Programming Ruby,"A tutorial and reference to the object-oriented programming language for beginning to experienced programmers, updated for version 1.8, describes the language's structure, syntax, and operation, and explains how to build applications.",Book partner,9780974514055,1.99,61 24 | Book,Books > Non-Fiction > Essential programming,9780596002817,Learning Python,"This edition of Learning Python puts you in the hands of two expert teachers, Mark Lutz and David Ascher, whose friendly, well-structured prose has guided many a programmer to proficiency with the language.",Book partner,9780596002817,18.99,7 25 | Book,Books > Non-Fiction > Essential programming,9780134900124,UNIX Network Programming,Interprocess Communications.,Book partner,9780134900124,2.99,21 26 | Book,Books > Non-Fiction > Essential programming,9780321503626,"Growing Object-Oriented Software, Guided by Tests","Along the way, the book systematically addresses challenges that development teams encounter with TDD—from integrating TDD into your processes to testing your most difficult features.",Book partner,9780321503626,17.99,49 27 | Book,Books > Non-Fiction > Essential programming,9780321146533,Test-Driven Development,By Example. The Addison-Wesley Signature Series provides readers with practical and authoritative information on the latest trends in modern technology for computer professionals. The series is based on one simple premise: great books ...,Book partner,9780321146533,19.99,39 28 | Book,Books > Non-Fiction > Essential programming,9780974514048,Ship it!,"Experienced practitioners Richardson and Gwaltney give inside information on the practicalities of managing a development project, whether from the aforesaid garage or from the largest cube farm in th.",Book partner,9780974514048,20.99,94 29 | Book,Books > Non-Fiction > Essential programming,9780133708752,ANSI Common Lisp,"Consisting of three appendices, the summary half of the book gives source code for a selection of widely used Common Lisp operators, with definitions that offer a comprehensive explanation of the language and provide a rich source of real ...",Book partner,9780133708752,24.99,93 30 | Book,Books > Non-Fiction > Essential programming,9780596000271,Programming Perl,"On the other hand, the best managers also understand the job their employees are trying to do. The same is true of pattern matching in Perl. The more thoroughly you understand of how Perl goes about the task of matching any particular ...",Book partner,9780596000271,21.99,48 31 | Book,Books > Non-Fiction > Essential programming,9780321334879,Effective Java,"Each chapter in the book consists of several “items” presented in the form of a short, standalone essay that provides specific advice, insight into Java platform subtleties, and outstanding code examples.",Book partner,9780321334879,1.99,34 32 | Book,Books > Non-Fiction > Essential programming,9780262033848,Algorithms,"A new edition of the essential text and professional reference, with substantial newmaterial on such topics as vEB trees, multithreaded algorithms, dynamic programming, and edge-baseflow.",Book partner,9780262033848,0.99,49 33 | Book,Books > Non-Fiction > Essential programming,9780976694007,Agile Web Development Whit Rails,Provides information on creating Web-based applications.,Book partner,9780976694007,8.99,18 34 | Book,Books > Non-Fiction > Essential programming,9780131495050,Xunit Test Patterns,"The definitive guide to writing tests for todays popular XUnit test automation frameworks, this guide by a renowned expert introduces more than 120 proven patterns for making tests easier to write, understand, and maintain.",Book partner,9780131495050,2.99,30 35 | Book,Books > Non-Fiction > Essential programming,9780135974445,Agile software development,"Taking on a global orientation to software programming, this practical guide offers scores of tested methods for using the C++ programming language with object-oriented design techniques for creating a variety of applications and solving a ...",Book partner,9780135974445,2.99,40 36 | Book,Books > Non-Fiction > Essential programming,9780596101053,Learning Perl,"Shows how to write, debug, and run a Perl program, describes CGI scripting and data manipulation, and describes scalar values, basic operators, and associative arrays.",Book partner,9780596101053,11.99,84 37 | Book,Books > Non-Fiction > Essential programming,9780201433074,Advanced programming in the Unix environment,"This book includes lots of realistic examples, and I find it quite helpful when I have systems programming tasks to do."" -- RS/Magazine ""This is the definitive reference book for any serious or professional UNIX systems programmer.",Book partner,9780201433074,3.99,29 38 | Book,Books > Non-Fiction > Essential programming,9780534950972,Introduction To The Theory Of Computation,This market leading text on computational theory provides a mathematical treatment of computer science theory designed around theorems and proofs.,Book partner,9780534950972,26.99,7 39 | Book,Books > Non-Fiction > Essential programming,9781590593899,Joel on Software,"The Guerilla Guide to Interviewing Incentive Pay Considered Harmful Top Five (Wrong) Reasons You Don’t Have Testers Human Task Switches Considered Harmful Things You Should Never Do, Part One The Iceberg Secret, Revealed The Law of Leaky ...",Book partner,9781590593899,20.99,44 40 | Book,Books > Non-Fiction > Essential programming,9780205313426,Elements of Style,The Elements of Style is a classic work which is intended for use in English courses in which the practice of composition is combined with the study of literature.,Book partner,9780205313426,23.99,80 41 | Book,Books > Non-Fiction > Essential programming,9781934356371,The Rspec Book,"The RSpec Book will introduce you to RSpec, Cucumber, and a number of other tools that make up the Ruby BDD family.",Book partner,9781934356371,19.99,93 42 | Book,Books > Non-Fiction > Essential programming,9780136291558,Object-oriented software construction,"This is, quite simply, the definitive reference on the most important development in software technology for the last 20 years: object-orientation.A whole generation was introduced to object technology through the first edition of this book ...",Book partner,9780136291558,11.99,9 43 | Book,Books > Non-Fiction > Essential programming,9780201889543,The C++ programming language,"Written by the inventor of the language, this book is the defining text on the language that has become central to software development over the past five years.",Book partner,9780201889543,6.99,14 44 | Book,Books > Non-Fiction > Essential programming,9780201342758,Haskell,The second edition of Haskell: The Craft of Functional Programmingis essential reading for beginners to functional programming and newcomers to the Haskell programming language.,Book partner,9780201342758,8.99,57 45 | Book,Books > Non-Fiction > Essential programming,9780596529864,Learning Ruby,"You'll find examples on nearly every page of this book that you can imitate and hack. Briefly, this book: Outlines many of the most important features of Ruby Demonstrates how to use conditionals, and how to manipulate strings in Ruby.",Book partner,9780596529864,9.99,58 46 | Book,Books > Non-Fiction > Essential programming,9780672328794,Sams Teach Yourself JavaScript in 24 Hours,"The book is written in a clear and personable style with an extensive use of practical, complete examples. It also includes material on the latest developments in JavaScript and web scripting.",Book partner,9780672328794,14.99,7 47 | Book,Books > Non-Fiction > Essential programming,9780672326721,Php And Mysql Web Development,"Explains how to access and create MySQL databases through PHP scripting, including authentication, network connectivity, session management, and content customization.",Book partner,9780672326721,6.99,41 48 | Book,Books > Non-Fiction > Essential programming,9780672323492,Sams teach yourself MySQL in 24 hours,"Demonstrates the features of the SQL-based relational database system, covering configuration, integration with third-party tools, and Web page generation as well as column types, operators, functions, and syntax.",Book partner,9780672323492,26.99,54 49 | Book,Books > Non-Fiction > Essential programming,9780201703535,Accelerated C++,"This book describes real problems and solutions, not just language features. It covers the language and standard library together. ""This is a first-rate introductory book that takes a practical approach to solving problems using C++.",Book partner,9780201703535,0.99,2 50 | Book,Books > Non-Fiction > Essential programming,9780130810816,UNIX Network Programming: Interprocess communications,"8108A-2 Don't miss the rest of the series! Vol. 1, Networking APIs: Sockets and XTI Vol. 3, Applications (forthcoming) The only guide to UNIX(r) interprocess communications you'll ever need!",Book partner,9780130810816,4.99,39 51 | Book,Books > Non-Fiction > Essential programming,9780961392147,Library Resources & Technical Services,,Book partner,9780961392147,13.99,85 52 | Book,Books > Non-Fiction > Essential programming,9780767907699,Slack,"Argues that the ""lean and mean"" corporate model of workaholism and downsizing is proving counterproductive, explaining how companies can implement downtime, promote flexibility, and foster creativity as part of realizing increased revenues.",Book partner,9780767907699,7.99,5 53 | Book,Books > Non-Fiction > Essential programming,9780137081073,The Clean Coder,"Readers will come away from this book understanding How to tell the difference between good and bad codeHow to write good code and how to transform bad code into good codeHow to create good names, good functions, good objects, and good ...",Book partner,9780137081073,28.99,66 54 | Book,Books > Non-Fiction > Essential programming,9780978739218,Release It!,Provides information on ways to effectively design and release an application.,Book partner,9780978739218,27.99,90 55 | Book,Books > Non-Fiction > Essential programming,9780471578147,Assembly Language,"Destined to become a classic, this book weaves a careful, patient explanation of assembly language instructions and programming methods with descriptions of the CPU and memory.",Book partner,9780471578147,14.99,81 56 | Book,Books > Non-Fiction > Essential programming,9781934356852,Lean from the Trenches,"From start to finish, readers will see what it takes to develop a successful agile project.",Book partner,9781934356852,0.99,42 57 | Book,Books > Non-Fiction > Essential programming,9780557030798,Reviewing C++,"A simple C++ review book and your best guide to learning C++. This book covers the most seen topics in introductory programming courses such as conditions, loops, arrays, classes and pointers.",Book partner,9780557030798,15.99,84 58 | Book,Books > Non-Fiction > Essential programming,9780764543654,Beginning Java 2 SDK 1.4 edition,What this book will teach you This book will teach you all you need to know to start programming in Java. This latest edition of my series teaches Java with the Java 2 SDK 1.4; a free Software Development Kit for creating Java applications.,Book partner,9780764543654,21.99,19 59 | Book,Books > Non-Fiction > Essential programming,9781848000698,The Algorithm Design Manual,"Expanding on the highly successful formula of the first edition, the book now serves as the primary textbook of choice for any algorithm design course while maintaining its status as the premier practical reference guide to algorithms.NEW: ...",Book partner,9781848000698,5.99,73 60 | Book,Books > Non-Fiction > Essential programming,9781934356586,The Agile Samurai,"Looks at the principles of agile software development, covering such topics as project inception, estimation, iteration management, unit testing, refactoring, test-driven development, and continuous integration.",Book partner,9781934356586,9.99,65 61 | Book,Books > Non-Fiction > Essential programming,9780262062794,Essentials of programming languages,"A new edition of a textbook that provides students with a deep, working understanding of the essential concepts of programming languages, completely revised, with significant new material.",Book partner,9780262062794,14.99,97 62 | Book,Books > Non-Fiction > Essential programming,9780672327933,Sams teach yourself Perl in 24 hours,"Offers a tutorial explaining how to use Perl scripts and modules to create such CGI Web applications as data collection, shopping cart, server push, and e-mail forms.",Book partner,9780672327933,2.99,64 63 | Book,Books > Non-Fiction > Essential programming,9780133262247,C,"This essential manual introduces the notion of ""Clean C"", writing C code that can be compiled as a C++ program, and incorporates the ISO C Amendment 1 (1994) which specifies new facilities for writing portable, international programs in C.",Book partner,9780133262247,4.99,40 64 | Book,Books > Non-Fiction > Essential programming,9780201379235,STL tutorial and reference guide,"The generic algorithms chapter with so many more examples than in the previous edition is delightful! The examples work cumulatively to give a sense of comfortable competence with the algorithms, containers, and iterators used.",Book partner,9780201379235,6.99,51 65 | Book,Books > Non-Fiction > Essential programming,9780955683619,Bridging the Communication Gap,"Bridging the Communication Gap is a book about improving communication between customers, business analysts, developers and testers on software projects, especially by using specification by example and agile acceptance testing.",Book partner,9780955683619,20.99,77 66 | Book,Books > Non-Fiction > Essential programming,9780321437389,Implementing Lean Software Development,"""This remarkable book combines practical advice, ready-to-use techniques, anda deep understanding of why this is the right way to develop software. I haveseen software teams transformed by the ideas in this book.",Book partner,9780321437389,0.99,9 67 | Book,Books > Non-Fiction > Essential programming,9781934356296,Manage Your Project Portfolio,"Introducing readers to different ways of ordering all of the projects they are working on, ""Manage Your Project Portfolio"" helps to define a team's, group's, or department's mission--whether the projects include of software or hardware ...",Book partner,9781934356296,26.99,33 68 | Book,Books > Non-Fiction > Essential programming,9780201741575,Fearless Change,"The co-authors reveal 48 patterns of behavior associated with successful change in knowledge-driven organizations, and show readers exactly how to use them in their own organization.",Book partner,9780201741575,20.99,95 69 | Book,Books > Non-Fiction > Essential programming,9781430322641,Scrum and XP from the Trenches,This book aims to give you a head start by providing a detailed down-to-earth account of how one Swedish company implemented Scrum and XP with a team of approximately 40 people and how they continuously improved their process over a year's ...,Book partner,9781430322641,13.99,23 70 | Book,Books > Non-Fiction > Essential programming,9780321278654,Extreme Programming Explained.,"You may love XP, or you may hate it, but ""Extreme Programming Explained"" will force you to take a fresh look at how you develop software. 0201616416B04062001",Book partner,9780321278654,6.99,62 71 | Book,Books > Non-Fiction > Essential programming,9780977616640,Agile Retrospective,The tools and recipes in this book will help readers uncover and solve hidden and not-so-hidden problems with their technology and methodology. It offers tips to fix the problems faced on a software development project on an ongoing basis.,Book partner,9780977616640,9.99,1 72 | Book,Books > Non-Fiction > Essential programming,9781591840565,The Art Of The Start,"Explains how to transform ideas into action, offering a step-by-step approach to launching great products, services, and companies and demonstrating how managers can unleash a creative approach to business at established companies.",Book partner,9781591840565,10.99,43 73 | Book,Books > Non-Fiction > Essential programming,9780884271789,The Goal,"Mr. Rogo, a plant manager, must improve his factory's efficiency or face its closing in just three months.",Book partner,9780884271789,3.99,5 74 | Book,Books > Non-Fiction > Essential programming,9780262111898,Genetic Programming,The lawnmower problem. The bumblebee problem. The increasing benefits of ADFs as problems are scaled up. Finding an impulse response function. Artificial ant on the San Mateo trail. Obstacle-avoiding robot. The minesweeper problem.,Book partner,9780262111898,24.99,81 75 | Book,Books > Non-Fiction > Essential programming,9780976458708,Thinking Forth,A Language and Philosophy for Solving Problems Leo Brodie. THREE Preliminary Design/ Decomposition Assuming you have some idea of what your program should. Justine Time.,Book partner,9780976458708,11.99,35 76 | Book,Books > Non-Fiction > Essential programming,9780596809485,97 Things Every Programmer Should Know,"With the 97 short and extremely useful tips for programmers in this book, you'll expand your skills by adopting new approaches to old problems, learning appropriate best practices, and honing your craft through sound advice.",Book partner,9780596809485,3.99,84 77 | Book,Books > Non-Fiction > Essential programming,9781617290084,Specification by Example,"Describes a method of effectively specifying, testing, and delivering software, covering such topics as documentation, process patterns, and automation, along with case studies from a variety of firms.",Book partner,9781617290084,0.99,23 78 | Book,Books > Non-Fiction > Essential programming,9780321213358,Refactoring to patterns,"This book introduces the theory and practice of pattern-directed refactorings: sequences of low-level refactorings that allow designers to safely move designs to, towards, or away from pattern implementations.",Book partner,9780321213358,28.99,6 79 | Book,Books > Non-Fiction > Essential programming,9780201733860,Software craftsmanship,"Chapter. 19. Perpetual. Learning. Software developers need to have a good memory, be very good at learning, and be great at forgetting. Forgetting is the most important ability because it is the key to perpetual learning. Learning is important ...",Book partner,9780201733860,11.99,63 80 | Book,Books > Non-Fiction > Essential programming,9780557043552,Reviewing Java,"A simple Java review book and your best guide to learning Java programming. This book covers the most seen topics in introductory programming courses such as conditions, loops, arrays, classes and inheritance.",Book partner,9780557043552,1.99,30 81 | Book,Books > Non-Fiction > Essential programming,9780596009205,深入浅出 Java,"number formatting Number formatting InJava, formatting numbers and dates doesn't have to be coupled with I/O. Think about it. One of the most typical ways to display numbers to a user is through a GUI. You put Strings into a scrolling text ...",Book partner,9780596009205,15.99,86 82 | Book,Books > Non-Fiction > Essential programming,9780131774292,Expert C Programming,"Deep C Secrets Peter van der Linden. #include banana ( ) { printf ( "" in banana ( ) \n"" ) ; longjmp (buf, 1) ; /*NOTREACHED*/ printf ( ""you' 11 never see this, because I longjmp'd"" ) ; main ( ) if (set jmp(buf ) ) printf (""back in main\n""); ...",Book partner,9780131774292,2.99,45 83 | -------------------------------------------------------------------------------- /sandbox/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /sandbox/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Django settings for oscar project. 4 | PROJECT_DIR = os.path.dirname(__file__) 5 | location = lambda x: os.path.join(os.path.dirname(os.path.realpath(__file__)), x) 6 | 7 | DEBUG = True 8 | TEMPLATE_DEBUG = True 9 | SQL_DEBUG = True 10 | 11 | USE_TZ = True 12 | 13 | ADMINS = ( 14 | # ('Your Name', 'your_email@domain.com'), 15 | ) 16 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 17 | 18 | MANAGERS = ADMINS 19 | 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 23 | 'NAME': location('db.sqlite'), # Or path to database file if using sqlite3. 24 | 'USER': '', # Not used with sqlite3. 25 | 'PASSWORD': '', # Not used with sqlite3. 26 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 27 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 28 | } 29 | } 30 | ATOMIC_REQUESTS = True 31 | 32 | # Local time zone for this installation. Choices can be found here: 33 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 34 | # although not all choices may be available on all operating systems. 35 | # On Unix systems, a value of None will cause Django to use the same 36 | # timezone as the operating system. 37 | # If running in a Windows environment this must be set to the same as your 38 | # system time zone. 39 | TIME_ZONE = 'Europe/London' 40 | 41 | # Language code for this installation. All choices can be found here: 42 | # http://www.i18nguy.com/unicode/language-identifiers.html 43 | LANGUAGE_CODE = 'en-us' 44 | 45 | SITE_ID = 1 46 | 47 | # If you set this to False, Django will make some optimizations so as not 48 | # to load the internationalization machinery. 49 | USE_I18N = True 50 | 51 | # If you set this to False, Django will not format dates, numbers and 52 | # calendars according to the current locale 53 | USE_L10N = True 54 | 55 | # Absolute path to the directory that holds media. 56 | # Example: "/home/media/media.lawrence.com/" 57 | MEDIA_ROOT = location("public/media") 58 | 59 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 60 | # trailing slash if there is a path component (optional in other cases). 61 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 62 | MEDIA_URL = '/media/' 63 | 64 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 65 | # trailing slash. 66 | # Examples: "http://foo.com/media/", "/media/". 67 | #ADMIN_MEDIA_PREFIX = '/media/admin/' 68 | 69 | STATIC_URL = '/static/' 70 | STATICFILES_DIRS = (location('static/'),) 71 | STATIC_ROOT = location('public') 72 | STATICFILES_FINDERS = ( 73 | 'django.contrib.staticfiles.finders.FileSystemFinder', 74 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 75 | 'compressor.finders.CompressorFinder', 76 | ) 77 | 78 | # Make this unique, and don't share it with anybody. 79 | SECRET_KEY = '$)a7n&o80u!6y5t-+jrd3)3!%vh&shg$wqpjpxc!ar&p#!)n1a' 80 | 81 | # List of callables that know how to import templates from various sources. 82 | TEMPLATE_LOADERS = ( 83 | 'django.template.loaders.filesystem.Loader', 84 | 'django.template.loaders.app_directories.Loader', 85 | # 'django.template.loaders.eggs.Loader', 86 | ) 87 | 88 | TEMPLATE_CONTEXT_PROCESSORS = ( 89 | "django.contrib.auth.context_processors.auth", 90 | "django.core.context_processors.request", 91 | "django.core.context_processors.debug", 92 | "django.core.context_processors.i18n", 93 | "django.core.context_processors.media", 94 | "django.core.context_processors.static", 95 | "django.contrib.messages.context_processors.messages", 96 | # Oscar specific 97 | 'oscar.apps.search.context_processors.search_form', 98 | 'oscar.apps.promotions.context_processors.promotions', 99 | 'oscar.apps.checkout.context_processors.checkout', 100 | 'oscar.core.context_processors.metadata', 101 | ) 102 | 103 | MIDDLEWARE_CLASSES = ( 104 | 'django.middleware.common.CommonMiddleware', 105 | 'django.contrib.sessions.middleware.SessionMiddleware', 106 | 'django.middleware.csrf.CsrfViewMiddleware', 107 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 108 | 'django.contrib.messages.middleware.MessageMiddleware', 109 | 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 110 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 111 | 'oscar.apps.basket.middleware.BasketMiddleware', 112 | ) 113 | 114 | INTERNAL_IPS = ('127.0.0.1',) 115 | 116 | ROOT_URLCONF = 'urls' 117 | 118 | from oscar import OSCAR_MAIN_TEMPLATE_DIR 119 | TEMPLATE_DIRS = ( 120 | location('templates'), 121 | OSCAR_MAIN_TEMPLATE_DIR, 122 | ) 123 | 124 | # A sample logging configuration. The only tangible logging 125 | # performed by this configuration is to send an email to 126 | # the site admins on every HTTP 500 error. 127 | # See http://docs.djangoproject.com/en/dev/topics/logging for 128 | # more details on how to customize your logging configuration. 129 | LOGGING = { 130 | 'version': 1, 131 | 'disable_existing_loggers': False, 132 | 'formatters': { 133 | 'verbose': { 134 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 135 | }, 136 | 'simple': { 137 | 'format': '%(levelname)s %(message)s' 138 | }, 139 | }, 140 | 'handlers': { 141 | 'null': { 142 | 'level': 'DEBUG', 143 | 'class':'django.utils.log.NullHandler', 144 | }, 145 | 'console':{ 146 | 'level':'DEBUG', 147 | 'class':'logging.StreamHandler', 148 | 'formatter': 'verbose' 149 | }, 150 | 'datacash_file': { 151 | 'level': 'DEBUG', 152 | 'class': 'logging.FileHandler', 153 | 'filename': '/tmp/datacash.log', 154 | 'formatter': 'verbose' 155 | }, 156 | 'mail_admins': { 157 | 'level': 'ERROR', 158 | 'class': 'django.utils.log.AdminEmailHandler', 159 | }, 160 | }, 161 | 'loggers': { 162 | 'django': { 163 | 'handlers':['null'], 164 | 'propagate': True, 165 | 'level':'INFO', 166 | }, 167 | 'django.request': { 168 | 'handlers': ['mail_admins'], 169 | 'level': 'ERROR', 170 | 'propagate': False, 171 | }, 172 | 'oscar.checkout': { 173 | 'handlers': ['console'], 174 | 'propagate': True, 175 | 'level':'INFO', 176 | }, 177 | 'django.db.backends': { 178 | 'handlers':['null'], 179 | 'propagate': False, 180 | 'level':'DEBUG', 181 | }, 182 | 'datacash': { 183 | 'handlers': ['console', 'datacash_file'], 184 | 'propagate': True, 185 | 'level':'DEBUG', 186 | }, 187 | } 188 | } 189 | 190 | 191 | INSTALLED_APPS = [ 192 | 'django.contrib.auth', 193 | 'django.contrib.contenttypes', 194 | 'django.contrib.sessions', 195 | 'django.contrib.sites', 196 | 'django.contrib.messages', 197 | 'django.contrib.admin', 198 | 'django.contrib.flatpages', 199 | 'django.contrib.staticfiles', 200 | # External apps 201 | 'django_extensions', 202 | 'debug_toolbar', 203 | 'haystack', 204 | 'sorl.thumbnail', 205 | 'datacash', 206 | 'compressor', 207 | 'south', 208 | ] 209 | from oscar import get_core_apps 210 | INSTALLED_APPS += get_core_apps() 211 | 212 | AUTHENTICATION_BACKENDS = ( 213 | 'oscar.apps.customer.auth_backends.Emailbackend', 214 | 'django.contrib.auth.backends.ModelBackend', 215 | ) 216 | 217 | LOGIN_REDIRECT_URL = '/accounts/' 218 | APPEND_SLASH = True 219 | 220 | 221 | # ============== 222 | # Oscar settings 223 | # ============== 224 | 225 | from oscar.defaults import * 226 | OSCAR_ALLOW_ANON_CHECKOUT = True 227 | 228 | # Haystack settings 229 | HAYSTACK_CONNECTIONS = { 230 | 'default': { 231 | 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', 232 | }, 233 | } 234 | 235 | OSCAR_SHOP_TAGLINE = 'Datacash sandbox' 236 | 237 | COMPRESS_ENABLED = False 238 | COMPRESS_PRECOMPILERS = ( 239 | ('text/less', 'lessc {infile} {outfile}'), 240 | ) 241 | 242 | # ================= 243 | # Datacash settings 244 | # ================= 245 | 246 | # Add the Datacash dashboard to the nav 247 | OSCAR_DASHBOARD_NAVIGATION.append( 248 | { 249 | 'label': 'Datacash', 250 | 'icon': 'icon-globe', 251 | 'children': [ 252 | { 253 | 'label': 'Transactions', 254 | 'url_name': 'datacash-transaction-list', 255 | }, 256 | { 257 | 'label': 'Fraud responses', 258 | 'url_name': 'datacash-fraud-response-list', 259 | }, 260 | ] 261 | }) 262 | 263 | 264 | try: 265 | from integration import * 266 | except ImportError: 267 | pass 268 | -------------------------------------------------------------------------------- /sandbox/templates/checkout/payment_details.html: -------------------------------------------------------------------------------- 1 | {% extends 'oscar/checkout/payment_details.html' %} 2 | {% load i18n %} 3 | 4 | {% block payment_details_content %} 5 |
6 | {% csrf_token %} 7 | {% include "partials/form_fields.html" with form=bankcard_form %} 8 | 9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /sandbox/templates/checkout/preview.html: -------------------------------------------------------------------------------- 1 | {% extends 'oscar/checkout/preview.html' %} 2 | {% load currency_filters %} 3 | {% load i18n %} 4 | 5 | {% block payment_method %} 6 |
7 |

{% trans "Payment" %}

8 |
9 | 10 |

Bankcard

11 |

{% blocktrans with amount=order_total.incl_tax|currency %} 12 | {{ amount }} will be debited from your bankcard: 13 | {% endblocktrans %}

14 | {% with bankcard=bankcard_form.bankcard %} 15 |

16 | {% trans "Card type" %}: {{ bankcard.card_type }}
17 | {% trans "Card number" %}: {{ bankcard.obfuscated_number }}
18 | {% trans "Expiry month" %}: {{ bankcard.expiry_month }}

19 | {% endwith %} 20 | 21 | 24 |
25 |
26 | {% endblock %} 27 | 28 | {% block hiddenforms %} 29 | {{ bankcard_form.as_p }} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /sandbox/templates/checkout/thank_you.html: -------------------------------------------------------------------------------- 1 | {% extends 'oscar/checkout/thank_you.html' %} 2 | {% load currency_filters %} 3 | {% load i18n %} 4 | 5 | {% block checkout-nav %} 6 | {% include 'checkout/nav.html' with step=4 %} 7 | {% endblock %} 8 | 9 | {% block payment_info %} 10 |
11 |

{% trans "Payment" %}

12 |
13 |
14 | {% for source in order.sources.all %} 15 | {% if source.source_type.name == 'Datacash' %} 16 | {% blocktrans with amount=source.amount_allocated|currency %}{{ amount }} authorised on your bankcard{% endblocktrans %} 17 | {% else %} 18 | {{ source }} 19 | {% endif %} 20 | {% endfor %} 21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /sandbox/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import * 2 | from django.conf import settings 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.conf.urls.static import static 6 | 7 | from apps.app import shop 8 | 9 | from datacash.dashboard.app import application 10 | 11 | admin.autodiscover() 12 | 13 | urlpatterns = patterns('', 14 | (r'^admin/', include(admin.site.urls)), 15 | # Include dashboard URLs 16 | (r'^dashboard/datacash/', include(application.urls)), 17 | (r'^datacash/', include('datacash.urls')), 18 | url(r'^i18n/', include('django.conf.urls.i18n')), 19 | (r'', include(shop.urls)), 20 | ) 21 | if settings.DEBUG: 22 | urlpatterns += staticfiles_urlpatterns() 23 | urlpatterns += static( 24 | settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | 5 | setup(name='django-oscar-datacash', 6 | version='0.8.3', 7 | url='https://github.com/tangentlabs/django-oscar-datacash', 8 | author="David Winterbottom", 9 | author_email="david.winterbottom@tangentlabs.co.uk", 10 | description="Datacash payment module for django-oscar", 11 | long_description=open('README.rst').read(), 12 | keywords="Payment, Datacash", 13 | license='BSD', 14 | packages=find_packages(exclude=['sandbox*', 'tests*']), 15 | include_package_data=True, 16 | install_requires=[ 17 | 'django-oscar>=0.6', 18 | # Python 2 & 3 compatibility helper 19 | 'six>=1.5.2', 20 | ], 21 | # See http://pypi.python.org/pypi?%3Aaction=list_classifiers 22 | classifiers=[ 23 | 'Environment :: Web Environment', 24 | 'Framework :: Django', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: Unix', 28 | 'Programming Language :: Python', 29 | 'Programming Language :: Python :: 2', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.3', 33 | 'Programming Language :: Python :: 3.4', 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from xml.dom.minidom import parseString 2 | 3 | from django.test import TestCase 4 | 5 | 6 | class XmlTestingMixin(object): 7 | 8 | def assertXmlElementEquals(self, xml_str, value, element_path): 9 | doc = parseString(xml_str) 10 | elements = element_path.split('.') 11 | parent = doc 12 | for element_name in elements: 13 | sub_elements = parent.getElementsByTagName(element_name) 14 | if len(sub_elements) == 0: 15 | self.fail("No element matching '%s' found using XML string '%s'" % (element_name, element_path)) 16 | return 17 | parent = sub_elements[0] 18 | self.assertEqual(value, parent.firstChild.data) 19 | 20 | 21 | class MiscTests(TestCase): 22 | """ 23 | Miscellaneous stuff: 24 | """ 25 | 26 | def test_datacash_constant_exist(self): 27 | from datacash import DATACASH 28 | self.assertEqual('Datacash', DATACASH) 29 | -------------------------------------------------------------------------------- /tests/facade_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decimal import Decimal as D 3 | from mock import Mock 4 | 5 | from django.test import TestCase 6 | from oscar.apps.payment.utils import Bankcard 7 | from oscar.apps.payment.exceptions import UnableToTakePayment, InvalidGatewayRequestError 8 | 9 | from datacash.models import OrderTransaction 10 | from datacash.facade import Facade 11 | 12 | from . import XmlTestingMixin, fixtures 13 | 14 | 15 | class MockBillingAddress(object): 16 | def __init__(self, **kwargs): 17 | for k, v in kwargs.items(): 18 | setattr(self, k, v) 19 | 20 | 21 | class FacadeTests(TestCase, XmlTestingMixin): 22 | 23 | def setUp(self): 24 | self.facade = Facade() 25 | 26 | def test_unicode_handling(self): 27 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 28 | card = Bankcard(card_number='1000350000000007', expiry_date='10/13', cvv='345') 29 | self.facade.pre_authorise( 30 | '1234', D('10.00'), card, 31 | the3rdman_data={ 32 | 'customer_info': { 33 | 'surname': u'Smörgåsbord' 34 | } 35 | }) 36 | 37 | def test_second_fulfill_has_merchant_ref(self): 38 | # Initial pre-auth 39 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 40 | card = Bankcard('1000350000000007', '10/13', cvv='345') 41 | ref = self.facade.pre_authorise('1234', D('100.00'), card) 42 | txn = OrderTransaction.objects.get(datacash_reference=ref) 43 | 44 | # First fulfill 45 | self.facade.gateway._fetch_response_xml = Mock( 46 | return_value=fixtures.SAMPLE_SUCCESSFUL_FULFILL_RESPONSE) 47 | self.facade.fulfill_transaction('1234', D('50.00'), 48 | txn.datacash_reference, txn.auth_code) 49 | 50 | self.facade.fulfill_transaction('1234', D('40.00'), 51 | txn.datacash_reference, txn.auth_code) 52 | fulfill_txn = OrderTransaction.objects.get( 53 | order_number='1234', 54 | amount=D('40.00') 55 | ) 56 | self.assertTrue('merchantreference' in fulfill_txn.request_xml) 57 | 58 | def test_zero_amount_for_pre_raises_exception(self): 59 | card = Bankcard('1000350000000007', '10/13', cvv='345') 60 | with self.assertRaises(UnableToTakePayment): 61 | self.facade.pre_authorise('1234', D('0.00'), card) 62 | 63 | def test_zero_amount_for_auth_raises_exception(self): 64 | card = Bankcard('1000350000000007', '10/13', cvv='345') 65 | with self.assertRaises(UnableToTakePayment): 66 | self.facade.authorise('1234', D('0.00'), card) 67 | 68 | def test_auth_request_creates_txn_model(self): 69 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 70 | card = Bankcard('1000350000000007', '10/13', cvv='345') 71 | self.facade.authorise('100001', D('123.22'), card) 72 | txn = OrderTransaction.objects.filter(order_number='100001')[0] 73 | self.assertEquals('auth', txn.method) 74 | self.assertEquals(D('123.22'), txn.amount) 75 | self.assertTrue(len(txn.request_xml) > 0) 76 | self.assertTrue(len(txn.response_xml) > 0) 77 | 78 | def test_auth_request_with_integer_cvv(self): 79 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 80 | card = Bankcard('1000350000000007', '10/13', cvv=345) 81 | self.facade.authorise('100001', D('123.22'), card) 82 | 83 | def test_pre_request_creates_txn_model(self): 84 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 85 | card = Bankcard('1000350000000007', '10/13', cvv='345') 86 | self.facade.pre_authorise('100001', D('123.22'), card) 87 | txn = OrderTransaction.objects.filter(order_number='100001')[0] 88 | self.assertEquals('pre', txn.method) 89 | self.assertEquals(D('123.22'), txn.amount) 90 | self.assertTrue(len(txn.request_xml) > 0) 91 | self.assertTrue(len(txn.response_xml) > 0) 92 | 93 | def test_pre_request_uses_billing_address_fields(self): 94 | mock = Mock(return_value=fixtures.SAMPLE_RESPONSE) 95 | self.facade.gateway._fetch_response_xml = mock 96 | card = Bankcard('1000350000000007', '10/13', cvv='345') 97 | address = MockBillingAddress(line1='1 Egg Street', 98 | line2='Farmville', 99 | line4='Greater London', 100 | postcode='N1 8RT') 101 | self.facade.pre_authorise('100001', D('123.22'), card, 102 | billing_address=address) 103 | request_xml = mock.call_args[0][0] 104 | self.assertXmlElementEquals(request_xml, '1 Egg Street', 105 | 'Request.Transaction.CardTxn.Card.Cv2Avs.street_address1') 106 | self.assertXmlElementEquals(request_xml, 'Farmville', 107 | 'Request.Transaction.CardTxn.Card.Cv2Avs.street_address2') 108 | self.assertXmlElementEquals(request_xml, 'N1 8RT', 109 | 'Request.Transaction.CardTxn.Card.Cv2Avs.postcode') 110 | 111 | def test_auth_request_uses_billing_address_fields(self): 112 | mock = Mock(return_value=fixtures.SAMPLE_RESPONSE) 113 | self.facade.gateway._fetch_response_xml = mock 114 | card = Bankcard('1000350000000007', '10/13', cvv='345') 115 | address = MockBillingAddress(line1='1 Egg Street', 116 | line2='Farmville', 117 | line4='Greater London', 118 | postcode='N1 8RT') 119 | self.facade.authorise('100001', D('123.22'), card, billing_address=address) 120 | request_xml = mock.call_args[0][0] 121 | self.assertXmlElementEquals(request_xml, '1 Egg Street', 122 | 'Request.Transaction.CardTxn.Card.Cv2Avs.street_address1') 123 | self.assertXmlElementEquals(request_xml, 'Farmville', 124 | 'Request.Transaction.CardTxn.Card.Cv2Avs.street_address2') 125 | self.assertXmlElementEquals(request_xml, 'N1 8RT', 126 | 'Request.Transaction.CardTxn.Card.Cv2Avs.postcode') 127 | 128 | def test_refund_request_doesnt_include_currency_attribute(self): 129 | mock = Mock(return_value=fixtures.SAMPLE_RESPONSE) 130 | self.facade.gateway._fetch_response_xml = mock 131 | self.facade.refund_transaction('100001', D('123.22'), 132 | txn_reference='12345') 133 | request_xml = mock.call_args[0][0] 134 | self.assertTrue('currency' not in request_xml) 135 | 136 | def test_auth_request_returns_datacash_ref(self): 137 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 138 | card = Bankcard('1000350000000007', '10/13', cvv='345') 139 | ref = self.facade.authorise('100001', D('123.22'), card) 140 | self.assertEquals('3000000088888888', ref) 141 | 142 | def test_auth_request_using_previous_txn_ref(self): 143 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 144 | ref = self.facade.authorise('100001', D('123.22'), txn_reference='3000000088888888') 145 | self.assertEquals('3000000088888888', ref) 146 | 147 | def test_refund_request(self): 148 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 149 | card = Bankcard('1000350000000007', '10/13', cvv='345') 150 | ref = self.facade.refund('100005', D('123.22'), card) 151 | txn = OrderTransaction.objects.filter(order_number='100005')[0] 152 | self.assertEquals('refund', txn.method) 153 | 154 | def test_pre_auth_using_history_txn(self): 155 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 156 | ref = self.facade.pre_authorise('100001', D('123.22'), txn_reference='3000000088888888') 157 | self.assertEquals('3000000088888888', ref) 158 | 159 | def test_refund_using_historic_txn(self): 160 | self.facade.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 161 | ref = self.facade.refund('100001', D('123.22'), txn_reference='3000000088888888') 162 | self.assertEquals('3000000088888888', ref) 163 | 164 | def test_refund_without_source_raises_exception(self): 165 | with self.assertRaises(ValueError): 166 | ref = self.facade.refund('100001', D('123.22')) 167 | 168 | def test_pre_auth_without_source_raises_exception(self): 169 | with self.assertRaises(ValueError): 170 | ref = self.facade.pre_authorise('100001', D('123.22')) 171 | 172 | def test_auth_without_source_raises_exception(self): 173 | with self.assertRaises(ValueError): 174 | ref = self.facade.authorise('100001', D('123.22')) 175 | 176 | def test_successful_cancel_request(self): 177 | response_xml = """ 178 | 179 | 4900200000000001 180 | 4900200000000001 181 | TEST 182 | CANCELLED OK 183 | 1 184 | 185 | """ 186 | self.facade.gateway._fetch_response_xml = Mock(return_value=response_xml) 187 | ref = self.facade.cancel_transaction('100001', '3000000088888888') 188 | self.assertEquals('4900200000000001', ref) 189 | 190 | def test_successful_fulfill_request(self): 191 | response_xml = """ 192 | 193 | 3900200000000001 194 | 3900200000000001 195 | LIVE 196 | FULFILLED OK 197 | 1 198 | 199 | """ 200 | self.facade.gateway._fetch_response_xml = Mock(return_value=response_xml) 201 | self.facade.fulfill_transaction('100002', D('45.00'), '3000000088888888', '1234') 202 | txn = OrderTransaction.objects.filter(order_number='100002')[0] 203 | self.assertEquals('fulfill', txn.method) 204 | 205 | def test_successful_refund_request(self): 206 | response_xml = """ 207 | 208 | 4000000088889999 209 | 210 | 896876 211 | 212 | 4100000088888888 213 | LIVE 214 | ACCEPTED 215 | 1 216 | 217 | """ 218 | self.facade.gateway._fetch_response_xml = Mock(return_value=response_xml) 219 | self.facade.refund_transaction('100003', D('45.00'), '3000000088888888') 220 | txn = OrderTransaction.objects.filter(order_number='100003')[0] 221 | self.assertEquals('txn_refund', txn.method) 222 | 223 | def test_transaction_declined_exception_raised_for_decline(self): 224 | response_xml = """ 225 | 226 | 227 | DECLINED 228 | Mastercard 229 | United Kingdom 230 | 231 | 4400200045583767 232 | AA004630 233 | TEST 234 | DECLINED 235 | 7 236 | 237 | """ 238 | self.facade.gateway._fetch_response_xml = Mock(return_value=response_xml) 239 | card = Bankcard('1000350000000007', '10/13', cvv='345') 240 | with self.assertRaises(UnableToTakePayment): 241 | self.facade.pre_authorise('100001', D('123.22'), card) 242 | 243 | def test_invalid_request_exception_raised_for_error(self): 244 | response_xml = """ 245 | 246 | 21859999000005679 247 | This vTID is not configured to process pre-registered card transactions. 248 | 123403 249 | Prereg: Merchant Not Subscribed 250 | 251 251 | 252 | """ 253 | self.facade.gateway._fetch_response_xml = Mock(return_value=response_xml) 254 | card = Bankcard('1000350000000007', '10/13', cvv='345') 255 | with self.assertRaises(InvalidGatewayRequestError): 256 | self.facade.pre_authorise('100001', D('123.22'), card) 257 | 258 | 259 | 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | SAMPLE_REQUEST = """ 2 | 3 | 4 | 99000001 5 | boomboom 6 | 7 | 8 | 9 | 10 | 1000011100000004 11 | 04/06 12 | 01/04 13 | 14 | auth 15 | 16 | 17 | 1000001 18 | 95.99 19 | 20 | 21 | """ 22 | 23 | SAMPLE_CV2AVS_REQUEST = """ 24 | 25 | 26 | 99001381 27 | hbANDMzErH 28 | 29 | 30 | 31 | pre 32 | 33 | XXXXXXXXXXXX0007 34 | 02/12 35 | 36 | 1 37 | house 38 | 39 | 40 | 41 | n12 42 | 9et 43 | 123 44 | 45 | 46 | 47 | 48 | 100024_182223 49 | 35.21 50 | ecomm 51 | 52 | 53 | """ 54 | 55 | SAMPLE_RESPONSE = """ 56 | 57 | 58 | 060642 59 | Switch 60 | United Kingdom 61 | HSBC 62 | 63 | 3000000088888888 64 | 1000001 65 | LIVE 66 | ACCEPTED 67 | 1 68 | 69 | """ 70 | 71 | SAMPLE_SUCCESSFUL_FULFILL_RESPONSE = """ 72 | 73 | 3000000088888888 74 | 3000000088888888 75 | TEST 76 | FULFILLED OK 77 | 1 78 | 79 | """ 80 | 81 | SAMPLE_DATACASH_REFERENCE_REQUEST = """ 82 | 83 | 84 | 99001381 85 | samplepassword 86 | 87 | 88 | 89 | 1234567890124209 90 | fulfill 91 | 747595 92 | 93 | 94 | 100001_FULFILL_1_6664 95 | 767.00 96 | ecomm 97 | 98 | 99 | """ 100 | -------------------------------------------------------------------------------- /tests/gateway_tests.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as D 2 | from mock import Mock 3 | 4 | from django.test import TestCase 5 | 6 | from datacash.gateway import Gateway, Response 7 | 8 | from . import XmlTestingMixin, fixtures 9 | 10 | 11 | class TestGatewayWithCV2AVSMock(TestCase, XmlTestingMixin): 12 | """ 13 | Gateway using CV2AVS 14 | """ 15 | 16 | def setUp(self): 17 | self.gateway = Gateway('example.com', '/Transaction', 'dummyclient', 'dummypassword', True) 18 | 19 | def gateway_auth(self, amount=D('1000.00'), currency='GBP', card_number='1000350000000007', 20 | expiry_date='10/12', merchant_reference='TEST_132473839018', response_xml=fixtures.SAMPLE_RESPONSE, **kwargs): 21 | self.gateway._fetch_response_xml = Mock(return_value=response_xml) 22 | response = self.gateway.auth(amount=amount, 23 | currency=currency, 24 | card_number=card_number, 25 | expiry_date=expiry_date, 26 | merchant_reference=merchant_reference, 27 | **kwargs) 28 | return response 29 | 30 | def test_zero_amount_raises_exception(self): 31 | with self.assertRaises(ValueError): 32 | self.gateway_auth(amount=D('0.00')) 33 | 34 | def test_cvv_is_included_in_request(self): 35 | response = self.gateway_auth(ccv='456') 36 | self.assertXmlElementEquals(response.request_xml, '456', 'Request.Transaction.CardTxn.Card.Cv2Avs.cv2') 37 | 38 | def test_capture_method_defaults_to_ecomm(self): 39 | response = self.gateway_auth() 40 | self.assertXmlElementEquals(response.request_xml, 'ecomm', 'Request.Transaction.TxnDetails.capturemethod') 41 | 42 | 43 | class TestGatewayWithoutCV2AVSMock(TestCase, XmlTestingMixin): 44 | """ 45 | Gateway without CV2AVS 46 | """ 47 | 48 | def setUp(self): 49 | self.gateway = Gateway('example.com', '/Transaction', 'dummyclient', 'dummypassword') 50 | 51 | def gateway_auth(self, amount=D('1000.00'), currency='GBP', card_number='1000350000000007', 52 | expiry_date='10/12', merchant_reference='TEST_132473839018', response_xml=fixtures.SAMPLE_RESPONSE, **kwargs): 53 | self.gateway._fetch_response_xml = Mock(return_value=response_xml) 54 | response = self.gateway.auth(amount=amount, 55 | currency=currency, 56 | card_number=card_number, 57 | expiry_date=expiry_date, 58 | merchant_reference=merchant_reference, 59 | **kwargs) 60 | return response 61 | 62 | def gateway_cancel(self, datacash_reference='132473839018', response_xml=fixtures.SAMPLE_RESPONSE, **kwargs): 63 | self.gateway._fetch_response_xml = Mock(return_value=response_xml) 64 | return self.gateway.cancel(datacash_reference, **kwargs) 65 | 66 | def test_successful_auth(self): 67 | response_xml = """ 68 | 69 | 70 | 100000 71 | Mastercard 72 | United Kingdom 73 | 74 | 4000203021904745 75 | TEST_132473839018 76 | TEST 77 | ACCEPTED 78 | 1 79 | 80 | """ 81 | response = self.gateway_auth(response_xml=response_xml) 82 | self.assertEquals('1', response['status']) 83 | self.assertEquals('TEST_132473839018', response['merchant_reference']) 84 | self.assertEquals('ACCEPTED', response['reason']) 85 | self.assertEquals('100000', response['auth_code']) 86 | self.assertEquals('Mastercard', response['card_scheme']) 87 | self.assertEquals('United Kingdom', response['country']) 88 | self.assertTrue(response.is_successful()) 89 | 90 | def test_unsuccessful_auth(self): 91 | response_xml = """ 92 | 93 | 94 | DECLINED 95 | Mastercard 96 | United Kingdom 97 | 98 | 4400200045583767 99 | AA004630 100 | TEST 101 | DECLINED 102 | 7 103 | 104 | """ 105 | response = self.gateway_auth(response_xml=response_xml) 106 | self.assertEquals('7', response['status']) 107 | self.assertEquals('AA004630', response['merchant_reference']) 108 | self.assertEquals('DECLINED', response['reason']) 109 | self.assertEquals('Mastercard', response['card_scheme']) 110 | self.assertEquals('United Kingdom', response['country']) 111 | self.assertFalse(response.is_successful()) 112 | 113 | def test_startdate_is_included_in_request_xml(self): 114 | response = self.gateway_auth(start_date='10/10') 115 | self.assertXmlElementEquals(response.request_xml, '10/10', 'Request.Transaction.CardTxn.Card.startdate') 116 | 117 | def test_auth_code_is_included_in_request_xml(self): 118 | response = self.gateway_auth(auth_code='11122') 119 | self.assertXmlElementEquals(response.request_xml, '11122', 120 | 'Request.Transaction.CardTxn.Card.authcode') 121 | 122 | def test_issue_number_is_included_in_request_xml(self): 123 | response = self.gateway_auth(issue_number='01') 124 | self.assertXmlElementEquals(response.request_xml, '01', 'Request.Transaction.CardTxn.Card.issuenumber') 125 | 126 | def test_issue_number_is_validated_for_format(self): 127 | with self.assertRaises(ValueError): 128 | self.gateway_auth(issue_number='A') 129 | 130 | def test_dates_are_validated_for_format(self): 131 | with self.assertRaises(ValueError): 132 | self.gateway_auth(expiry_date='10/2012') 133 | 134 | def test_issuenumber_is_validated_for_format(self): 135 | with self.assertRaises(ValueError): 136 | self.gateway.auth(issue_number='123') 137 | 138 | def test_currency_is_validated_for_format(self): 139 | with self.assertRaises(ValueError): 140 | self.gateway_auth(currency='BGRR') 141 | 142 | def test_merchant_ref_is_validated_for_min_length(self): 143 | with self.assertRaises(ValueError): 144 | self.gateway_auth(merchant_reference='12345') 145 | 146 | def test_merchant_ref_is_validated_for_max_length(self): 147 | with self.assertRaises(ValueError): 148 | self.gateway_auth(merchant_reference='123456789012345678901234567890123') 149 | 150 | def test_successful_cancel_response(self): 151 | response_xml = """ 152 | 153 | 4500203021916406 154 | 4500203021916406 155 | TEST 156 | CANCELLED OK 157 | 1 158 | 159 | """ 160 | response = self.gateway_cancel(response_xml=response_xml) 161 | self.assertEquals('CANCELLED OK', response['reason']) 162 | self.assertTrue(response.is_successful()) 163 | 164 | def test_request_xml_for_auth_using_previous_transaction_ref(self): 165 | self.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 166 | response = self.gateway.auth(amount=D('1000.00'), 167 | currency='GBP', 168 | merchant_reference='TEST_132473839018', 169 | previous_txn_reference='4500203021916406') 170 | self.assertXmlElementEquals(response.request_xml, 171 | '4500203021916406', 'Request.Transaction.CardTxn.card_details') 172 | 173 | def test_request_xml_for_pre_using_previous_transaction_ref(self): 174 | self.gateway._fetch_response_xml = Mock(return_value=fixtures.SAMPLE_RESPONSE) 175 | response = self.gateway.pre(amount=D('1000.00'), 176 | currency='GBP', 177 | merchant_reference='TEST_132473839018', 178 | previous_txn_reference='4500203021916406') 179 | self.assertXmlElementEquals(response.request_xml, 180 | '4500203021916406', 'Request.Transaction.CardTxn.card_details') 181 | 182 | 183 | class GatewayErrorTests(TestCase): 184 | 185 | def test_exception_raised_with_bad_host(self): 186 | with self.assertRaises(RuntimeError): 187 | Gateway('http://test.datacash.com', '/Transaction', client='', password='') 188 | 189 | 190 | class ResponseTests(TestCase): 191 | 192 | def test_str_version_is_response_xml(self): 193 | response_xml = '' 194 | r = Response('', response_xml) 195 | self.assertEqual(response_xml, str(r)) 196 | 197 | def test_none_is_returned_for_missing_status(self): 198 | r = Response('', '') 199 | self.assertIsNone(r.status) 200 | 201 | 202 | class SuccessfulResponseTests(TestCase): 203 | 204 | def setUp(self): 205 | request_xml = "" 206 | response_xml = """ 207 | 208 | 4500203021916406 209 | 4500203021916406 210 | TEST 211 | CANCELLED OK 212 | 1 213 | 214 | """ 215 | self.response = Response(request_xml, response_xml) 216 | 217 | def test_dict_access(self): 218 | self.assertEquals('1', self.response['status']) 219 | 220 | def test_in_access(self): 221 | self.assertTrue('status' in self.response) 222 | 223 | def test_is_successful(self): 224 | self.assertTrue(self.response.is_successful()) 225 | 226 | def test_status_is_returned_correctly(self): 227 | self.assertEquals(1, self.response.status) 228 | 229 | 230 | class DeclinedResponseTests(TestCase): 231 | 232 | def setUp(self): 233 | request_xml = "" 234 | response_xml = """ 235 | 236 | 237 | DECLINED 238 | Mastercard 239 | United Kingdom 240 | 241 | 4400200045583767 242 | AA004630 243 | TEST 244 | DECLINED 245 | 7 246 | 247 | """ 248 | self.response = Response(request_xml, response_xml) 249 | 250 | def test_is_successful(self): 251 | self.assertFalse(self.response.is_successful()) 252 | 253 | def test_is_declined(self): 254 | self.assertTrue(self.response.is_declined()) 255 | -------------------------------------------------------------------------------- /tests/integration_tests.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as D 2 | import math 3 | import time 4 | from mock import Mock 5 | from unittest import skipUnless 6 | 7 | from django.test import TestCase 8 | from django.conf import settings 9 | from oscar.apps.payment.exceptions import UnableToTakePayment, InvalidGatewayRequestError 10 | 11 | from datacash.models import OrderTransaction 12 | from datacash.gateway import Gateway, Response 13 | from datacash.facade import Facade 14 | 15 | 16 | @skipUnless(getattr(settings, 'DATACASH_ENABLE_INTEGRATION_TESTS', False), "Currently disabled") 17 | class GatewayIntegrationTests(TestCase): 18 | """ 19 | There can be problems with DataCash speed limits when running these tests as 20 | you aren't supposed to perform a transaction with the same card more 21 | than once every two minutes. 22 | """ 23 | 24 | def setUp(self): 25 | self.gateway = Gateway(settings.DATACASH_HOST, 26 | '/Transaction', 27 | settings.DATACASH_CLIENT, 28 | settings.DATACASH_PASSWORD) 29 | 30 | def generate_merchant_reference(self): 31 | return 'TEST_%s' % int(math.floor(time.time() * 100)) 32 | 33 | def test_successful_auth(self): 34 | # Using test card from Datacash's docs 35 | ref = self.generate_merchant_reference() 36 | response = self.gateway.auth(amount=D('1000.00'), 37 | currency='GBP', 38 | card_number='1000350000000007', 39 | expiry_date='10/12', 40 | merchant_reference=ref) 41 | self.assertEquals('1', response['status']) 42 | self.assertEquals(ref, response['merchant_reference']) 43 | self.assertEquals('ACCEPTED', response['reason']) 44 | self.assertEquals('100000', response['auth_code']) 45 | self.assertEquals('Mastercard', response['card_scheme']) 46 | self.assertEquals('United Kingdom', response['country']) 47 | 48 | def test_declined_auth(self): 49 | ref = self.generate_merchant_reference() 50 | response = self.gateway.auth(amount=D('1000.02'), 51 | currency='GBP', 52 | card_number='4444333322221111', 53 | expiry_date='10/12', 54 | merchant_reference=ref) 55 | self.assertEquals('7', response['status']) 56 | self.assertEquals(ref, response['merchant_reference']) 57 | self.assertEquals('DECLINED', response['reason']) 58 | self.assertEquals('VISA', response['card_scheme']) 59 | 60 | def test_cancel_auth(self): 61 | ref = self.generate_merchant_reference() 62 | response = self.gateway.auth(amount=D('1000.00'), 63 | currency='GBP', 64 | card_number='1000011000000005', 65 | expiry_date='10/12', 66 | merchant_reference=ref) 67 | self.assertTrue(response.is_successful()) 68 | cancel_response = self.gateway.cancel(response['datacash_reference']) 69 | self.assertEquals('1', response['status']) 70 | 71 | def test_refund_auth(self): 72 | ref = self.generate_merchant_reference() 73 | refund_response = self.gateway.refund(amount=D('200.00'), 74 | currency='GBP', 75 | card_number='1000010000000007', 76 | expiry_date='10/12', 77 | merchant_reference=ref) 78 | self.assertTrue(refund_response.is_successful()) 79 | 80 | def test_txn_refund_of_auth(self): 81 | ref = self.generate_merchant_reference() 82 | response = self.gateway.auth(amount=D('1000.00'), 83 | currency='GBP', 84 | card_number='1000011100000004', 85 | expiry_date='10/12', 86 | merchant_reference=ref) 87 | self.assertTrue(response.is_successful()) 88 | cancel_response = self.gateway.txn_refund(txn_reference=response['datacash_reference'], 89 | amount=D('1000.00'), 90 | currency='GBP') 91 | self.assertTrue(response.is_successful()) 92 | 93 | def test_pre(self): 94 | ref = self.generate_merchant_reference() 95 | response = self.gateway.pre(amount=D('1000.00'), 96 | currency='GBP', 97 | card_number='1000020000000014', 98 | expiry_date='10/12', 99 | merchant_reference=ref) 100 | self.assertTrue(response.is_successful()) 101 | self.assertTrue(response['auth_code']) 102 | 103 | def test_fulfill(self): 104 | ref = self.generate_merchant_reference() 105 | pre_response = self.gateway.pre(amount=D('1000.00'), 106 | currency='GBP', 107 | card_number='1000070000000001', 108 | expiry_date='10/12', 109 | merchant_reference=ref) 110 | self.assertTrue(pre_response.is_successful()) 111 | 112 | response = self.gateway.fulfill(amount=D('800.00'), 113 | currency='GBP', 114 | auth_code=pre_response['auth_code'], 115 | txn_reference=pre_response['datacash_reference']) 116 | self.assertEquals('FULFILLED OK', response['reason']) 117 | self.assertTrue(response.is_successful()) 118 | -------------------------------------------------------------------------------- /tests/model_tests.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as D 2 | 3 | from django.test import TestCase 4 | 5 | from datacash.models import OrderTransaction 6 | from . import XmlTestingMixin, fixtures 7 | 8 | 9 | class TransactionModelTests(TestCase, XmlTestingMixin): 10 | 11 | def test_unicode_method(self): 12 | txn = OrderTransaction.objects.create(order_number='1000', 13 | method='auth', 14 | datacash_reference='3000000088888888', 15 | merchant_reference='1000001', 16 | amount=D('95.99'), 17 | status=1, 18 | reason='ACCEPTED', 19 | request_xml=fixtures.SAMPLE_CV2AVS_REQUEST, 20 | response_xml=fixtures.SAMPLE_RESPONSE) 21 | self.assertTrue('AUTH txn ' in str(txn)) 22 | 23 | def test_password_is_not_saved_in_xml(self): 24 | txn = OrderTransaction.objects.create(order_number='1000', 25 | method='auth', 26 | datacash_reference='3000000088888888', 27 | merchant_reference='1000001', 28 | amount=D('95.99'), 29 | status=1, 30 | reason='ACCEPTED', 31 | request_xml=fixtures.SAMPLE_CV2AVS_REQUEST, 32 | response_xml=fixtures.SAMPLE_RESPONSE) 33 | self.assertXmlElementEquals(txn.request_xml, 'XXX', 34 | 'Request.Transaction.CardTxn.Card.Cv2Avs.cv2') 35 | 36 | def test_cvv_numbers_are_not_saved_in_xml(self): 37 | txn = OrderTransaction.objects.create(order_number='1000', 38 | method='auth', 39 | datacash_reference='3000000088888888', 40 | merchant_reference='1000001', 41 | amount=D('95.99'), 42 | status=1, 43 | reason='ACCEPTED', 44 | request_xml=fixtures.SAMPLE_CV2AVS_REQUEST, 45 | response_xml=fixtures.SAMPLE_RESPONSE) 46 | self.assertXmlElementEquals(txn.request_xml, 'XXX', 47 | 'Request.Authentication.password') 48 | 49 | def test_cc_numbers_are_not_saved_in_xml(self): 50 | txn = OrderTransaction.objects.create(order_number='1000', 51 | method='auth', 52 | datacash_reference='3000000088888888', 53 | merchant_reference='1000001', 54 | amount=D('95.99'), 55 | status=1, 56 | reason='ACCEPTED', 57 | request_xml=fixtures.SAMPLE_REQUEST, 58 | response_xml=fixtures.SAMPLE_RESPONSE) 59 | self.assertXmlElementEquals(txn.request_xml, 'XXXXXXXXXXXX0004', 'Request.Transaction.CardTxn.Card.pan') 60 | 61 | def test_datacash_refrences_are_not_obfuscated(self): 62 | txn = OrderTransaction.objects.create(order_number='1000', 63 | method='auth', 64 | datacash_reference='3000000088888888', 65 | merchant_reference='100001_FULFILL_1_6664', 66 | amount=D('767.00'), 67 | status=1, 68 | reason='ACCEPTED', 69 | request_xml=fixtures.SAMPLE_DATACASH_REFERENCE_REQUEST, 70 | response_xml=fixtures.SAMPLE_RESPONSE) 71 | self.assertXmlElementEquals(txn.request_xml, '1234567890124209', 'Request.Transaction.HistoricTxn.reference') 72 | -------------------------------------------------------------------------------- /tests/the3rdman_callback_tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.core.urlresolvers import reverse 3 | 4 | SUCCESS_RESPONSE = b""" 5 | 6 | 7 | 5567 8 | 12345 9 | 333333333 10 | 0 11 | 1 12 | 13 | """ 14 | 15 | HOLD_RESPONSE = b""" 16 | 17 | 18 | 32217 19 | 100117 20 | 1815120370 21 | 46 22 | 1 23 | baa7421d73c962ce92220e64526af1a559f26f46 24 | """ 25 | 26 | RELEASE_RESPONSE = b""" 27 | 28 | 29 | 32217 30 | 100117 31 | 1815120370 32 | 46 33 | 0 34 | baa7421d73c962ce92220e64526af1a559f26f46 35 | """ 36 | 37 | 38 | class TestCallbackView(TestCase): 39 | 40 | def test_success_response(self): 41 | url = reverse('datacash-3rdman-callback') 42 | response = self.client.post(url, SUCCESS_RESPONSE, content_type="text/xml") 43 | self.assertEquals(response.content, b"ok") 44 | 45 | def test_hold_then_release(self): 46 | url = reverse('datacash-3rdman-callback') 47 | response = self.client.post(url, HOLD_RESPONSE, content_type="text/xml") 48 | self.assertEquals(response.content, b"ok") 49 | response = self.client.post(url, RELEASE_RESPONSE, content_type="text/xml") 50 | self.assertEquals(response.content, b"ok") 51 | 52 | def test_error_response(self): 53 | url = reverse('datacash-3rdman-callback') 54 | response = self.client.post(url, b'', content_type="text/xml") 55 | self.assertEquals(response.content, b"error") 56 | -------------------------------------------------------------------------------- /tests/the3rdman_model_tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from datacash import models 4 | 5 | XML_RESPONSE = """ 6 | 7 | 8 | 5567 9 | 12345 10 | 333333333 11 | %(score)s 12 | %(recommendation)s 13 | 14 | """ 15 | 16 | QUERY_RESPONSE = "aggregator_identifier=&merchant_identifier=32195&merchant_order_ref=100032&t3m_id=1701673332&score=114&recommendation=2&message_digest=87d81ea49035fe2f8d59ceea3f16b1f43744701c" 17 | 18 | 19 | def stub_response(score=0, recommendation=0): 20 | return XML_RESPONSE % { 21 | 'score': score, 22 | 'recommendation': recommendation} 23 | 24 | 25 | class TestFraudResponseModel(TestCase): 26 | 27 | def test_recognises_release_response(self): 28 | xml = stub_response(recommendation=0) 29 | response = models.FraudResponse.create_from_xml(xml) 30 | self.assertTrue(response.released) 31 | 32 | def test_recognises_on_hold_response(self): 33 | xml = stub_response(recommendation=1) 34 | response = models.FraudResponse.create_from_xml(xml) 35 | self.assertTrue(response.on_hold) 36 | 37 | def test_recognises_reject_response(self): 38 | xml = stub_response(recommendation=2) 39 | response = models.FraudResponse.create_from_xml(xml) 40 | self.assertTrue(response.rejected) 41 | 42 | 43 | class TestFormURLEncodedResponse(TestCase): 44 | 45 | def test_for_smoke(self): 46 | response = models.FraudResponse.create_from_querystring(QUERY_RESPONSE) 47 | self.assertTrue(response.rejected) 48 | -------------------------------------------------------------------------------- /tests/the3rdman_tests.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as D 2 | 3 | from django.test import TestCase 4 | from oscar.test import factories 5 | from oscar.apps.basket.models import Basket 6 | 7 | from datacash import the3rdman, models 8 | from . import XmlTestingMixin 9 | 10 | 11 | class TestRequestGeneration(TestCase, XmlTestingMixin): 12 | 13 | def test_for_smoke(self): 14 | doc = the3rdman.add_fraud_fields(customer_info={'title': 'mr'}) 15 | xml = doc.toxml() 16 | self.assertXmlElementEquals(xml, 'mr', 17 | 'The3rdMan.CustomerInformation.title') 18 | 19 | 20 | class TestBuildingDataDict(TestCase): 21 | 22 | def test_includes_sales_channel(self): 23 | data = the3rdman.build_data_dict( 24 | request=None, user=None, 25 | order_number="1234", basket=None) 26 | self.assertEquals(3, data['customer_info']['sales_channel']) 27 | 28 | 29 | class TestIntegration(TestCase, XmlTestingMixin): 30 | 31 | def test_basket_lines_are_converted_to_xml(self): 32 | product = factories.create_product(price=D('12.99')) 33 | basket = Basket() 34 | 35 | # Nasty hack to make test suite work with both Oscar 0.5 and 0.6 36 | try: 37 | from oscar.apps.partner import strategy 38 | except ImportError: 39 | pass 40 | else: 41 | basket.strategy = strategy.Default() 42 | 43 | basket.add_product(product) 44 | data = the3rdman.build_data_dict( 45 | basket=basket) 46 | doc = the3rdman.add_fraud_fields(**data) 47 | xml = doc.toxml() 48 | self.assertXmlElementEquals(xml, '3', 49 | 'The3rdMan.CustomerInformation.sales_channel') 50 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import * 2 | 3 | from datacash.dashboard.app import application 4 | 5 | 6 | urlpatterns = patterns('', 7 | # Include dashboard URLs 8 | (r'^dashboard/datacash/', include(application.urls)), 9 | (r'^datacash/', include('datacash.urls')), 10 | ) 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = D15-O06, D15-O07, D16-O06, D16-O07, D16-O08, D17-O08-P27, D17-O08-P33, D17-O08-P34 3 | 4 | [testenv] 5 | commands = python runtests.py [] 6 | deps = -r{toxinidir}/requirements.txt 7 | 8 | # Django 1.5 9 | 10 | [testenv:D15-O06] 11 | basepython = python2.7 12 | deps = {[testenv]deps} 13 | Django==1.5.8 14 | django-oscar==0.6.5 15 | 16 | [testenv:D15-O07] 17 | basepython = python2.7 18 | deps = {[testenv]deps} 19 | Django==1.5.8 20 | django-oscar==0.7.2 21 | 22 | # Django 1.6 23 | 24 | [testenv:D16-O06] 25 | basepython = python2.7 26 | deps = {[testenv]deps} 27 | Django==1.6.5 28 | django-oscar==0.6.5 29 | 30 | [testenv:D16-O07] 31 | basepython = python2.7 32 | deps = {[testenv]deps} 33 | Django==1.6.5 34 | django-oscar==0.7.2 35 | 36 | [testenv:D16-O08] 37 | basepython = python2.7 38 | deps = {[testenv]deps} 39 | Django==1.6.5 40 | South==1.0 41 | https://github.com/tangentlabs/django-oscar/archive/master.tar.gz#egg=django-oscar 42 | 43 | # Django 1.7 44 | 45 | [testenv:D17-O08-P27] 46 | basepython = python2.7 47 | deps = {[testenv]deps} 48 | https://www.djangoproject.com/download/1.7c2/tarball/#egg=Django 49 | https://github.com/tangentlabs/django-oscar/archive/master.tar.gz#egg=django-oscar 50 | 51 | [testenv:D17-O08-P33] 52 | basepython = python3.3 53 | deps = {[testenv]deps} 54 | https://www.djangoproject.com/download/1.7c2/tarball/#egg=Django 55 | https://github.com/tangentlabs/django-oscar/archive/master.tar.gz#egg=django-oscar 56 | 57 | [testenv:D17-O08-P34] 58 | basepython = python3.4 59 | deps = {[testenv]deps} 60 | https://www.djangoproject.com/download/1.7c2/tarball/#egg=Django 61 | https://github.com/tangentlabs/django-oscar/archive/master.tar.gz#egg=django-oscar 62 | --------------------------------------------------------------------------------