Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial implementation of Global Iris gateway

Global Iris: Various fixes for consistency

Global Iris RealMPI implementation, with docs and tests

Global Iris - increased test coverage
  • Loading branch information...
commit ea70439374b0ab2b5c0db25ac5a03b2d65af443e 1 parent 7bb60c3
@spookylukey spookylukey authored tuxcanfly committed
View
1  CONTRIBUTORS.txt
@@ -23,6 +23,7 @@ Thanks go out to all the contributors listed in alphabetical order:
* Ken Cochrane <https://github.com/kencochrane>
* Konrad Rotkiewicz <https://github.com/krotkiewicz>
* Lalit Chandnani <https://github.com/lalitchandnani>
+* Luke Plant <http://lukeplant.me.uk/>
* Markus Gattol <markus.gattol@sunoano.org>
* Matt McClanahan <https://github.com/mattmcc>
* Mikey Ryan <https://github.com/mikery>
View
47 billing/forms/common.py
@@ -0,0 +1,47 @@
+from django import forms
+
+from billing.utils.credit_card import CreditCard, CardNotSupported
+
+
+class CreditCardFormBase(forms.Form):
+ """
+ Base class for a simple credit card form which provides some utilities like
+ 'get_credit_card' to return a CreditCard instance.
+
+ If you pass the gateway as a keyword argument to the constructor,
+ the gateway.validate_card method will be used in form validation.
+
+ This class must be subclassed to provide the actual fields to be used.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.gateway = kwargs.pop('gateway', None)
+ super(CreditCardFormBase, self).__init__(*args, **kwargs)
+
+ def get_credit_card(self):
+ """
+ Returns a CreditCard from the submitted (cleaned) data.
+
+ If gateway was passed to the form constructor, the gateway.validate_card
+ method will be called - which can throw CardNotSupported, and will also
+ add the attribute 'card_type' which is the CreditCard subclass if it is
+ successful.
+ """
+ card = CreditCard(**self.cleaned_data)
+ if self.gateway is not None:
+ self.gateway.validate_card(card)
+ return card
+
+ def clean(self):
+ cleaned_data = super(CreditCardFormBase, self).clean()
+ if self.errors:
+ # Don't bother with further validation, it only confuses things
+ # for the user to be presented with multiple error messages.
+ return cleaned_data
+ try:
+ credit_card = self.get_credit_card()
+ if not credit_card.is_valid():
+ raise forms.ValidationError("Credit card details are invalid")
+ except CardNotSupported:
+ raise forms.ValidationError("This type of credit card is not supported. Please check the number.")
+ return cleaned_data
View
27 billing/forms/global_iris_forms.py
@@ -0,0 +1,27 @@
+import datetime
+
+from django import forms
+
+from billing.forms.common import CreditCardFormBase
+
+class CreditCardForm(CreditCardFormBase):
+
+ cardholders_name = forms.CharField(label="Card holder's name", required=True)
+ number = forms.CharField(required=True)
+ month = forms.ChoiceField(label="Expiry month", choices=[])
+ year = forms.ChoiceField(label="Expiry year", choices=[])
+ verification_value = forms.CharField(label='CVV', required=True)
+
+ def __init__(self, *args, **kwargs):
+ super(CreditCardForm, self).__init__(*args, **kwargs)
+ self.fields['year'].choices = self.get_year_choices()
+ self.fields['month'].choices = self.get_month_choices()
+
+ def get_year_choices(self):
+ today = datetime.date.today()
+ return [(y, y) for y in range(today.year, today.year + 21)]
+
+ def get_month_choices(self):
+ # Override if you want month names, for instance.
+ return [(m, m) for m in range(1, 13)]
+
View
162 billing/gateways/global_iris_gateway.py
@@ -0,0 +1,162 @@
+from datetime import datetime
+from decimal import Decimal
+import sha
+
+from django.conf import settings
+from django.template.loader import render_to_string
+import lxml
+import requests
+
+from billing import Gateway
+from billing.signals import transaction_was_successful, transaction_was_unsuccessful
+from billing.utils.credit_card import Visa, MasterCard, AmericanExpress, InvalidCard
+
+
+# See https://resourcecentre.globaliris.com/documents/pdf.html?id=43 for details
+
+CARD_NAMES = {
+ Visa: 'VISA',
+ MasterCard: 'MC',
+ AmericanExpress: 'AMEX',
+ # Maestro and Switch are probably broken due to need for issue number to be passed.
+ }
+
+
+class Config(object):
+ def __init__(self, config_dict):
+ self.shared_secret = config_dict['SHARED_SECRET']
+ self.merchant_id = config_dict['MERCHANT_ID']
+ self.account = config_dict['ACCOUNT']
+
+
+class GlobalIrisBase(object):
+
+ default_currency = "GBP"
+ supported_countries = ["GB"]
+ supported_cardtypes = [Visa, MasterCard, AmericanExpress]
+ homepage_url = "https://resourcecentre.globaliris.com/"
+
+ def __init__(self, config=None, test_mode=None):
+ if config is None:
+ config = settings.MERCHANT_SETTINGS['global_iris']
+ self.config = config
+
+ if test_mode is None:
+ test_mode = getattr(settings, 'MERCHANT_TEST_MODE', True)
+ self.test_mode = test_mode
+
+ def get_config(self, credit_card):
+ setting_name_base = 'LIVE' if not self.test_mode else 'TEST'
+ setting_names = ['%s_%s' % (setting_name_base, CARD_NAMES[credit_card.card_type]),
+ setting_name_base]
+
+ for name in setting_names:
+ try:
+ config_dict = self.config[name]
+ except KeyError:
+ continue
+ return Config(config_dict)
+
+ raise KeyError("Couldn't find key %s in config %s" % (' or '.join(setting_names), self.config))
+
+ def make_timestamp(self, dt):
+ return dt.strftime('%Y%m%d%H%M%S')
+
+ def standardize_data(self, data):
+ config = self.get_config(data['card'])
+ all_data = {
+ 'currency': self.default_currency,
+ 'merchant_id': config.merchant_id,
+ 'account': config.account,
+ }
+
+ all_data.update(data)
+ if not 'timestamp' in all_data:
+ all_data['timestamp'] = datetime.now()
+ all_data['timestamp'] = self.make_timestamp(all_data['timestamp'])
+ currency = all_data['currency']
+ if currency in ['GBP', 'USD', 'EUR']:
+ all_data['amount_normalized'] = int(all_data['amount'] * Decimal('100.00'))
+ else:
+ raise ValueError("Don't know how to normalise amounts in currency %s" % currency)
+ card = all_data['card']
+ card.month_normalized = "%02d" % int(card.month)
+ year = int(card.year)
+ card.year_normalized = "%02d" % (year if year < 100 else int(str(year)[-2:]))
+ card.name_normalized = CARD_NAMES[card.card_type]
+
+ all_data['sha1_hash'] = self.get_standard_signature(all_data, config)
+ return all_data
+
+ def get_signature(self, data, config, signing_string):
+ d = data.copy()
+ d['merchant_id'] = config.merchant_id
+ val1 = signing_string.format(**d)
+ hash1 = sha.sha(val1).hexdigest()
+ val2 = "{0}.{1}".format(hash1, config.shared_secret)
+ hash2 = sha.sha(val2).hexdigest()
+ return hash2
+
+ def get_standard_signature(self, data, config):
+ return self.get_signature(data, config, "{timestamp}.{merchant_id}.{order_id}.{amount_normalized}.{currency}.{card.number}")
+
+ def do_request(self, xml):
+ return requests.post(self.base_url, xml)
+
+
+class GlobalIrisGateway(GlobalIrisBase, Gateway):
+
+ display_name = "Global Iris"
+
+ base_url = "https://remote.globaliris.com/RealAuth"
+
+ def build_xml(self, data):
+ all_data = self.standardize_data(data)
+ return render_to_string("billing/global_iris_realauth_request.xml", all_data).encode('utf-8')
+
+ def purchase(self, money, credit_card, options=None):
+ if options is None or 'order_id' not in options:
+ raise ValueError("Required parameter 'order_id' not found in options")
+
+ if not self.validate_card(credit_card):
+ raise InvalidCard("Invalid Card")
+
+ data = {
+ 'amount': money,
+ 'card': credit_card,
+ }
+ data.update(options)
+ xml = self.build_xml(data)
+ return self.handle_response(self.do_request(xml), "purchase")
+
+ def _failure(self, type, message, response, response_code=None):
+ transaction_was_unsuccessful.send(self, type=type, response=response, response_code=response_code)
+ retval = {"status": "FAILURE",
+ "message": message,
+ "response": response,
+ }
+ if response_code is not None:
+ retval['response_code'] = response_code
+ return retval
+
+ def _success(self, type, message, response, response_code=None):
+ transaction_was_successful.send(self, type=type, response=response, response_code=response_code)
+ return {"status": "SUCCESS",
+ "message": message,
+ "response": response,
+ "response_code": response_code,
+ }
+
+ def handle_response(self, response, type):
+ if response.status_code != 200:
+ return self._failure(type, response.reason, response)
+
+ # Parse XML
+ xml = lxml.etree.fromstring(response.content)
+ response_code = xml.find('result').text
+ message = xml.find('message').text
+ if response_code == '00':
+ return self._success(type, message, response, response_code=response_code)
+
+ else:
+ return self._failure(type, message, response, response_code=response_code)
View
232 billing/integrations/global_iris_real_mpi_integration.py
@@ -0,0 +1,232 @@
+import base64
+import decimal
+import json
+import logging
+
+from django.conf import settings
+from django.core.signing import TimestampSigner
+from django.shortcuts import render_to_response
+from django.template.loader import render_to_string
+import lxml
+import requests
+
+from billing import Integration, get_gateway, IntegrationNotConfigured
+from billing.gateways.global_iris_gateway import GlobalIrisBase
+from billing.utils.credit_card import Visa, MasterCard, Maestro, CreditCard
+from billing.utils.json import chain_custom_encoders, chain_custom_decoders
+import billing.utils.credit_card
+
+log = logging.getLogger(__name__)
+
+
+def get_signer():
+ return TimestampSigner(salt="billing.global_iris_real_mpi_integration")
+
+
+class GlobalIrisRealMpiIntegration(GlobalIrisBase, Integration):
+ display_name = "Global Iris RealMPI"
+
+ base_url = "https://remote.globaliris.com/realmpi"
+
+ def get_gateway(self):
+ return get_gateway("global_iris")
+
+ def __init__(self, config=None, test_mode=None):
+ super(GlobalIrisRealMpiIntegration, self).__init__(config=config, test_mode=test_mode)
+ self.gateway = self.get_gateway()
+
+ def card_supported(self, card):
+ return card.card_type in [Visa, MasterCard, Maestro]
+
+ def send_3ds_verifyenrolled(self, data):
+ return self.handle_3ds_verifyenrolled_response(self.do_request(self.build_3ds_verifyenrolled_xml(data)))
+
+ def handle_3ds_verifyenrolled_response(self, response):
+ if response.status_code != 200:
+ return GlobalIris3dsError(response.reason, response)
+ return GlobalIris3dsVerifyEnrolled(response.content)
+
+ def build_3ds_verifyenrolled_xml(self, data):
+ all_data = self.standardize_data(data)
+ return render_to_string("billing/global_iris_real_mpi_3ds_verifyenrolled_request.xml", all_data).encode('utf-8')
+
+ def encode_merchant_data(self, data_dict):
+ # resourcecentre.globaliris.com talks about encrypting this data.
+ # Encryption is not necessary here, since the data has been either
+ # entered by the user, or relating to the users stuff, and we are sending
+ # it only to services we trust (RealMPI and their bank). However, we do
+ # need to ensure that there is no tampering (which encryption does not
+ # guarantee), so we sign it.
+ return base64.encodestring(get_signer().sign(json.dumps(data_dict,
+ default=json_encoder_func,
+ )))
+
+ def decode_merchant_data(self, s):
+ return json.loads(get_signer().unsign(base64.decodestring(s),
+ max_age=10*60*60), # Shouldn't take more than 1 hour to fill in auth details!
+ object_hook=json_decoder_func)
+
+ def redirect_to_acs_url(self, enrolled_response, term_url, merchant_data):
+ return render_to_response("billing/global_iris_real_mpi_redirect_to_acs.html",
+ {'enrolled_response': enrolled_response,
+ 'term_url': term_url,
+ 'merchant_data': self.encode_merchant_data(merchant_data),
+ })
+
+ def parse_3d_secure_request(self, request):
+ """
+ Extracts the PaRes and merchant data from the HTTP request that is sent
+ to the website when the user returns from the 3D secure website.
+ """
+ return request.POST['PaRes'], self.decode_merchant_data(request.POST['MD'])
+
+ def send_3ds_verifysig(self, pares, data):
+ return self.handle_3ds_verifysig_response(self.do_request(self.build_3ds_verifysig_xml(pares, data)))
+
+ def handle_3ds_verifysig_response(self, response):
+ if response.status_code != 200:
+ return GlobalIris3dsError(response.reason, response)
+ return GlobalIris3dsVerifySig(response.content)
+
+ def build_3ds_verifysig_xml(self, pares, data):
+ all_data = self.standardize_data(data)
+ all_data['pares'] = pares
+ return render_to_string("billing/global_iris_real_mpi_3ds_verifysig_request.xml", all_data).encode('utf-8')
+
+
+def encode_credit_card_as_json(obj):
+ if isinstance(obj, CreditCard):
+ card_type = getattr(obj, 'card_type', None)
+ if card_type is not None:
+ card_type = card_type.__name__
+
+ return {'__credit_card__': True,
+ 'first_name': obj.first_name,
+ 'last_name': obj.last_name,
+ 'month': obj.month,
+ 'year': obj.year,
+ 'number': obj.number,
+ 'verification_value': obj.verification_value,
+ 'card_type': card_type,
+ }
+ raise TypeError("Unknown type %s" % obj.__class__)
+
+
+def decode_credit_card_from_dict(dct):
+ if '__credit_card__' in dct:
+ d = dct.copy()
+ d.pop('__credit_card__')
+ d.pop('card_type')
+ retval = CreditCard(**d)
+ card_type = dct.get('card_type', None) # put there by Gateway.validate_card
+ if card_type is not None:
+ # Get the credit card class with this name
+ retval.card_type = getattr(billing.utils.credit_card, card_type)
+ return retval
+ return dct
+
+
+def encode_decimal_as_json(obj):
+ if isinstance(obj, decimal.Decimal):
+ return {'__decimal__': True,
+ 'value': str(obj),
+ }
+ return TypeError("Unknown type %s" % obj.__class__)
+
+
+def decode_decimal_from_dict(dct):
+ if '__decimal__' in dct:
+ return decimal.Decimal(dct['value'])
+ return dct
+
+
+json_encoder_func = chain_custom_encoders([encode_credit_card_as_json, encode_decimal_as_json])
+json_decoder_func = chain_custom_decoders([decode_credit_card_from_dict, decode_decimal_from_dict])
+
+
+class GlobalIris3dsAttempt(object):
+ pass
+
+
+class GlobalIris3dsError(GlobalIris3dsAttempt):
+ error = True
+
+ def __init__(self, message, response):
+ self.message = message
+ self.response = response
+
+ def __repr__(self):
+ return "GlobalIris3dsError(%r, %r)" % (self.message, self.response)
+
+
+class GlobalIris3dsResponse(GlobalIris3dsAttempt):
+ error = False
+
+
+class GlobalIris3dsVerifyEnrolled(GlobalIris3dsResponse):
+ def __init__(self, xml_content):
+ tree = lxml.etree.fromstring(xml_content)
+ self.response_code = tree.find('result').text
+ enrolled_node = tree.find('enrolled')
+ self.enrolled = enrolled_node is not None and enrolled_node.text == "Y"
+ self.message = tree.find('message').text
+ if self.response_code in ["00", "110"]:
+ self.url = tree.find('url').text
+ self.pareq = tree.find('pareq').text
+ else:
+ self.error = True
+ log.warning("3Ds verifyenrolled error", extra={'response_xml': xml_content})
+
+
+ def proceed_with_auth(self, card):
+ """
+ Returns a tuple (bool, dict) indicating if you can
+ proceed directly with authorisation.
+
+ If the bool == True, you must pass the data in the dict as additional
+ data to the gateway.purchase() method.
+ """
+ if self.error:
+ return False, {}
+ if not self.enrolled and (self.url is None or self.url == ""):
+ eci = 6 if card.card_type is Visa else 1
+ return True, {'mpi': {'eci': eci}}
+ return False, {}
+
+
+class GlobalIris3dsVerifySig(GlobalIris3dsResponse):
+ def __init__(self, xml_content):
+ tree = lxml.etree.fromstring(xml_content)
+ self.response_code = tree.find('result').text
+ self.message = tree.find('message').text
+ if self.response_code == "00":
+ threed = tree.find('threedsecure')
+ self.status = threed.find('status').text
+ if self.status in ["Y", "A"]:
+ self.eci = threed.find('eci').text
+ self.xid = threed.find('xid').text
+ self.cavv = threed.find('cavv').text
+ else:
+ self.error = True
+ log.warning("3Ds verifysig error", extra={'response_xml': xml_content})
+
+ def proceed_with_auth(self, card):
+ """
+ Returns a tuple (bool, dict) indicating if you can
+ proceed with authorisation.
+
+ If the bool == True, you must pass the data in the dict as additional
+ data to the gateway.purchase() method.
+ """
+ if self.error or self.status in ["N", "U"]:
+ # Proceeding with status "U" is allowed, but risky
+ return False, {}
+
+ if self.status in ["Y", "A"]:
+ mpi_data = {'eci': self.eci,
+ 'xid': self.xid,
+ 'cavv': self.cavv,
+ }
+ return True, {'mpi': mpi_data}
+
+ return False, {}
View
15 billing/templates/billing/global_iris_real_mpi_3ds_base_request.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<request timestamp="{{ timestamp }}" type="{% block requesttype %}{% endblock %}">
+ <merchantid>{{ merchant_id }}</merchantid>
+ <account>{{ account }}</account>
+ <orderid>{{ order_id }}</orderid>
+ <amount currency="{{ currency }}">{{ amount_normalized }}</amount>
+ <card>
+ <number>{{ card.number }}</number>
+ <expdate>{{ card.month_normalized }}{{ card.year_normalized }}</expdate>
+ <chname>{{ card.first_name }} {{ card.last_name }}</chname>
+ <type>{{ card.name_normalized }}</type>
+ </card>
+ {% block body %}{% endblock %}
+ <sha1hash>{{ sha1_hash }}</sha1hash>
+</request>
View
2  billing/templates/billing/global_iris_real_mpi_3ds_verifyenrolled_request.xml
@@ -0,0 +1,2 @@
+{% extends "billing/global_iris_real_mpi_3ds_base_request.xml" %}
+{% block requesttype %}3ds-verifyenrolled{% endblock %}
View
7 billing/templates/billing/global_iris_real_mpi_3ds_verifysig_request.xml
@@ -0,0 +1,7 @@
+{% extends "billing/global_iris_real_mpi_3ds_base_request.xml" %}
+
+{% block requesttype %}3ds-verifysig{% endblock %}
+
+{% block body %}
+<pares>{{ pares }}</pares>
+{% endblock %}
View
20 billing/templates/billing/global_iris_real_mpi_redirect_to_acs.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<title>3D secure</title>
+<script language="javascript" >
+<!--
+function OnLoadEvent() {
+document.form.submit();
+}
+//-->
+</script>
+</head>
+<body onLoad="OnLoadEvent()">
+<form name="form" action="{{ enrolled_response.url }}" method="post">
+<input type="hidden" NAME="PaReq" value="{{ enrolled_response.pareq }}">
+<input type="hidden" NAME="TermUrl" value="{{ term_url }}">
+<input type="hidden" NAME="MD" value="{{ merchant_data }}">
+<noscript><input type="submit" value="Press to continue"></noscript>
+</form>
+</body>
+</html>
View
48 billing/templates/billing/global_iris_realauth_request.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<request timestamp="{{ timestamp }}" type="auth">
+ <merchantid>{{ merchant_id }}</merchantid>
+ <account>{{ account }}</account>
+ <channel>ECOM</channel>
+ <orderid>{{ order_id }}</orderid>
+ <amount currency="{{ currency }}">{{ amount_normalized }}</amount>
+ <card>
+ <number>{{ card.number }}</number>
+ <expdate>{{ card.month_normalized }}{{ card.year_normalized }}</expdate>
+ <chname>{{ card.first_name }} {{ card.last_name }}</chname>
+ <type>{{ card.name_normalized }}</type>
+ <cvn>
+ <number>{{ card.verification_value }}</number>
+ <presind>{% if card.verification_value %}1{% else %}3{% endif %}</presind>
+ </cvn>
+ </card>
+ <autosettle flag="1" />
+ {% if mpi %}
+ <mpi>
+ <eci>{{ mpi.eci }}</eci>
+ {% if mpi.cavv %}
+ <cavv>{{ mpi.cavv }}</cavv>
+ {% endif %}
+ {% if mpi.xid %}
+ <xid>{{ mpi.xid }}</xid>
+ {% endif %}
+ </mpi>
+ {% endif %}
+ <tssinfo>
+ {% if customer %}
+ <custnum>{{ customer }}</custnum>
+ {% endif %}
+ {% if billing_address %}
+ <address type="billing">
+ <code>{{ billing_address.zip }}</code>
+ <country>{{ billing_address.country }}</country>
+ </address>
+ {% endif %}
+ {% if shipping_address %}
+ <address type="shipping">
+ <code>{{ shipping_address.zip }}</code>
+ <country>{{ shipping_address.country }}</country>
+ </address>
+ {% endif %}
+ </tssinfo>
+ <sha1hash>{{ sha1_hash }}</sha1hash>
+</request>
View
1  billing/tests/__init__.py
@@ -37,6 +37,7 @@
from bitcoin_tests import *
from ogone_payments_tests import *
from pin_tests import *
+from global_iris_tests import *
if __name__ == "__main__":
unittest.main()
View
12 billing/tests/base_tests.py
@@ -60,3 +60,15 @@ def testTemplateTagLoad(self):
self.assertTrue(len(tmpl.render(Context({"obj": gc}))) > 0)
settings.MERCHANT_SETTINGS = original_settings
+
+
+class CreditCardTestCase(TestCase):
+ def test_constructor(self):
+ opts = dict(number='x', year=2000, month=1, verification_value='123')
+ self.assertRaises(TypeError, lambda: CreditCard(**opts))
+ self.assertRaises(TypeError, lambda: CreditCard(first_name='x', **opts))
+ self.assertRaises(TypeError, lambda: CreditCard(last_name='y', **opts))
+ c = CreditCard(first_name='x', last_name='y', **opts)
+ self.assertEqual(c.cardholders_name, None)
+ c2 = CreditCard(cardholders_name='z', **opts)
+ self.assertEqual(c2.cardholders_name, 'z')
View
491 billing/tests/global_iris_tests.py
@@ -0,0 +1,491 @@
+# -*- coding: utf-8 -*-
+import random
+import sys
+from datetime import datetime
+from decimal import Decimal
+
+from django.conf import settings
+from django.test import TestCase
+from django.utils.unittest.case import skipIf
+
+from billing.tests.utils import BetterXMLCompareMixin
+from billing.gateway import get_gateway
+from billing.gateways.global_iris_gateway import GlobalIrisGateway, CARD_NAMES
+from billing.integrations.global_iris_real_mpi_integration import GlobalIrisRealMpiIntegration
+from billing.signals import transaction_was_unsuccessful, transaction_was_successful
+from billing.utils.credit_card import CreditCard, CardNotSupported, Visa
+
+
+class Dummy200Response(object):
+ def __init__(self, content):
+ self.status_code = 200
+ self.content = content
+
+
+class GlobalIrisTestBase(object):
+
+ def mk_gateway(self):
+ return GlobalIrisGateway(
+ config={'TEST': dict(SHARED_SECRET="mysecret",
+ MERCHANT_ID="thestore",
+ ACCOUNT="theaccount")
+ },
+ test_mode=True)
+
+ def mk_integration(self):
+ return GlobalIrisRealMpiIntegration(
+ config={'TEST': dict(SHARED_SECRET="mysecret",
+ MERCHANT_ID="thestore",
+ ACCOUNT="theaccount")
+ },
+ test_mode=True)
+
+ def get_visa_card(self):
+ return CreditCard(first_name='Mickey',
+ last_name='Mouse',
+ month=7,
+ year=2035,
+ number='4903034000057389',
+ verification_value='123',
+ )
+
+ def get_amex_card(self):
+ return CreditCard(first_name='Donald',
+ last_name='Duck',
+ month=8,
+ year=2035,
+ number='374101012180018',
+ verification_value='4567',
+ )
+
+ def get_order_id(self):
+ # Need unique IDs for orders
+ return str(datetime.now()).replace(':', '_').replace(' ', '_').replace('.', '_') + str(random.randint(0, sys.maxint))
+
+
+@skipIf(not settings.MERCHANT_SETTINGS.get("global_iris", None), "gateway not configured")
+class GlobalIrisGatewayTestCase(BetterXMLCompareMixin, GlobalIrisTestBase, TestCase):
+
+ def test_request_xml(self):
+ gateway = self.mk_gateway()
+ card = CreditCard(first_name='Mickey',
+ last_name='Mouse',
+ month=7,
+ year=2014,
+ number='4903034000057389',
+ verification_value='123',
+ )
+ gateway.validate_card(card)
+ xml = gateway.build_xml({
+ 'timestamp': datetime(2001, 4, 27, 12, 45, 23),
+ 'order_id': '345',
+ 'amount': Decimal('20.00'),
+ 'card': card,
+ 'customer': '567',
+ 'billing_address': {
+ 'zip': 'ABC 123',
+ 'country': 'GB',
+ }
+ })
+
+ self.assertXMLEqual(u"""<?xml version="1.0" encoding="UTF-8" ?>
+<request timestamp="20010427124523" type="auth">
+ <merchantid>thestore</merchantid>
+ <account>theaccount</account>
+ <channel>ECOM</channel>
+ <orderid>345</orderid>
+ <amount currency="GBP">2000</amount>
+ <card>
+ <number>4903034000057389</number>
+ <expdate>0714</expdate>
+ <chname>Mickey Mouse</chname>
+ <type>VISA</type>
+ <cvn>
+ <number>123</number>
+ <presind>1</presind>
+ </cvn>
+ </card>
+ <autosettle flag="1" />
+ <tssinfo>
+ <custnum>567</custnum>
+ <address type="billing">
+ <code>ABC 123</code>
+ <country>GB</country>
+ </address>
+ </tssinfo>
+ <sha1hash>eeaeaf2751a86edcf0d77e906b2daa08929e7cbe</sha1hash>
+</request>""".encode('utf-8'), xml)
+
+ def test_signature(self):
+ gateway = self.mk_gateway()
+ card = CreditCard(number='5105105105105100',
+ first_name='x',
+ last_name='x',
+ year='1', month='1',
+ verification_value='123')
+ gateway.validate_card(card)
+ config = gateway.get_config(card)
+ sig = gateway.get_standard_signature(
+ {
+ 'timestamp':'20010403123245',
+ 'amount_normalized':'29900',
+ 'order_id': 'ORD453-11',
+ 'currency': 'GBP',
+ 'card': card,
+ }, config)
+ self.assertEqual(sig, "9e5b49f4df33b52efa646cce1629bcf8e488f7bb")
+
+ def test_parse_xml(self):
+ gateway = GlobalIrisGateway(config={'TEST': dict(SHARED_SECRET="mysecret",
+ MERCHANT_ID="thestore",
+ ACCOUNT="theaccount")
+ },
+ test_mode=True)
+ fail_resp = Dummy200Response('<?xml version="1.0" ?>\r\n<response timestamp="20140212143606">\r\n<result>504</result>\r\n<message>There is no such merchant id.</message>\r\n<orderid>1</orderid>\r\n</response>\r\n')
+ retval = gateway.handle_response(fail_resp, "purchase")
+ self.assertEqual(retval['status'], 'FAILURE')
+ self.assertEqual(retval['message'], 'There is no such merchant id.')
+ self.assertEqual(retval['response_code'], "504")
+ self.assertEqual(retval['response'], fail_resp)
+
+ def test_config_for_card_type(self):
+ """
+ Test that the GateWay object can pick the correct config depending on the card type.
+ """
+ gateway = GlobalIrisGateway(config={
+ 'LIVE': dict(SHARED_SECRET="mysecret",
+ MERCHANT_ID="thestore",
+ ACCOUNT="theaccount"),
+ 'LIVE_AMEX': dict(SHARED_SECRET="mysecret2",
+ MERCHANT_ID="thestore",
+ ACCOUNT="theaccountamex"),
+ }, test_mode=False)
+
+ vc = self.get_visa_card()
+ self.assertTrue(gateway.validate_card(vc)) # needed for side effects
+
+ ac = self.get_amex_card()
+ self.assertTrue(gateway.validate_card(ac))
+
+ self.assertEqual("theaccount", gateway.get_config(vc).account)
+ self.assertEqual("theaccountamex", gateway.get_config(ac).account)
+
+ def test_purchase_fail(self):
+ received_signals = []
+ def receive(sender, **kwargs):
+ received_signals.append(kwargs.get("signal"))
+ transaction_was_unsuccessful.connect(receive)
+
+ gateway = get_gateway('global_iris')
+ card = self.get_visa_card()
+ gateway.validate_card(card)
+ response = gateway.purchase(Decimal("45.00"), card, options={'order_id': 1})
+ # Difficult to test success, because we need dummy card numbers etc.
+ # But we can at least test we aren't getting exceptions.
+ self.assertEqual(response['status'], 'FAILURE')
+
+ self.assertEqual(received_signals, [transaction_was_unsuccessful])
+
+ def _get_test_cards(self):
+ cards = []
+ card_dicts = settings.MERCHANT_SETTINGS['global_iris']['TEST_CARDS']
+ for card_dict in card_dicts:
+ card_type = card_dict['TYPE']
+ d = dict(first_name= 'Test',
+ last_name= 'Test',
+ month=1,
+ year=datetime.now().year + 2,
+ number=card_dict['NUMBER'],
+ verification_value="1234" if card_type == "AMEX" else "123")
+ card = CreditCard(**d)
+ card.expected_response_code = card_dict['RESPONSE_CODE']
+ cards.append(card)
+ return cards
+
+ @skipIf('TEST' not in settings.MERCHANT_SETTINGS.get('global_iris', {})
+ or 'TEST_CARDS' not in settings.MERCHANT_SETTINGS.get('global_iris', {}),
+ "gateway not configured")
+
+ def test_purchase_with_test_cards(self):
+ # This test requires valid test numbers
+ gateway = GlobalIrisGateway()
+ if not gateway.test_mode:
+ self.fail("MERCHANT_TEST_MODE must be true for running tests")
+
+ for card in self._get_test_cards():
+ received_signals = []
+ def success(sender, **kwargs):
+ received_signals.append(kwargs.get("signal"))
+ transaction_was_successful.connect(success)
+
+ def fail(sender, **kwargs):
+ received_signals.append(kwargs.get("signal"))
+ transaction_was_unsuccessful.connect(fail)
+
+ # Cards with invalid numbers will get caught by billing code, not by
+ # the gateway itself.
+ try:
+ gateway.validate_card(card)
+ except CardNotSupported:
+ self.assertNotEqual(card.expected_response_code, "00")
+ continue # skip the rest
+
+
+ response = gateway.purchase(Decimal("45.00"), card,
+ options={'order_id': self.get_order_id(),
+ })
+
+ actual_rc = response['response_code']
+ expected_rc = card.expected_response_code
+
+ self.assertEqual(actual_rc, expected_rc,
+ "%s != %s - card %s, message: %s" % (actual_rc, expected_rc, card.number, response['message']))
+ if card.expected_response_code == "00":
+ self.assertEqual(response['status'], 'SUCCESS')
+ self.assertEqual(received_signals, [transaction_was_successful])
+ else:
+ self.assertEqual(response['status'], 'FAILURE')
+ self.assertEqual(received_signals, [transaction_was_unsuccessful])
+
+
+@skipIf(not settings.MERCHANT_SETTINGS.get("global_iris", None), "integration not configured")
+class GlobalIrisRealMpiIntegrationTestCase(BetterXMLCompareMixin, GlobalIrisTestBase, TestCase):
+
+ def test_3ds_verifyenrolled_xml(self):
+
+ expected = """<?xml version="1.0" encoding="UTF-8" ?>
+<request timestamp="20100625172305" type="3ds-verifyenrolled">
+ <merchantid>thestore</merchantid>
+ <account>theaccount</account>
+ <orderid>1</orderid>
+ <amount currency="GBP">2499</amount>
+ <card>
+ <number>4903034000057389</number>
+ <expdate>0714</expdate>
+ <chname>Mickey Mouse</chname>
+ <type>VISA</type>
+ </card>
+ <sha1hash>272d8dde0bf34a0e744f696f2860a7894b687cf7</sha1hash>
+</request>
+"""
+ integration = self.mk_integration()
+ gateway = self.mk_gateway()
+ card = CreditCard(first_name='Mickey',
+ last_name='Mouse',
+ month=7,
+ year=2014,
+ number='4903034000057389',
+ verification_value='123',
+ )
+ gateway.validate_card(card)
+ actual_xml = integration.build_3ds_verifyenrolled_xml(
+ {'order_id': 1,
+ 'amount': Decimal('24.99'),
+ 'card': card,
+ 'timestamp': datetime(2010,6, 25, 17, 23, 05),
+ })
+ self.assertXMLEqual(actual_xml, expected)
+
+ def test_parse_3ds_verifyenrolled_response(self):
+ example_xml = """<?xml version="1.0" encoding="UTF-8" ?>
+<response timestamp="20030625171810">
+<merchantid>merchantid</merchantid>
+<account>internet</account>
+<orderid>orderid</orderid>
+<authcode></authcode>
+<result>00</result>
+<message>Enrolled</message>
+<pasref></pasref>
+<timetaken>3</timetaken>
+<authtimetaken>0</authtimetaken>
+<pareq>eJxVUttygkAM/ZUdnitZFlBw4na02tE6bR0vD+0bLlHpFFDASv++u6i1zVNycju54H2dfrIvKsokz3qWY3OLUabyOMm2PWu1fGwF1r3E5a4gGi5IHQuS+ExlGW2JJXHPCjcuVyLYbIRQnrf2o3VMEY+57q05oIsibP+nA4SL02k7mELhKupqxVqF2WVxEgdBpMX6dwE4YJhSsVkKB3RH9ypGFyvNXpkrLW982HcancQzn7MopSkO2RnqmxJZYXQgKjyY1YV39Lt6O5XA4/Fp9xV1b4LcDqdbDcum8xKJ9oqTxFMAMKN5OxotFIXrJNY1otpMH0qYQwP43w08Pn0/W1Ql6+nj+cegonAOKpICs5d3hY+czpdJ+g6HKHBUoNEyk8OwzZaDXXE58R3JtG/as7DBH+IqhZFvpS3zLsBHqeq4VU7/OMTA7Cr45wo/0wNptWlV4Xb8Thftv3A30xs+7GYaokej3c415TxhgIJhUu54TLF2jt33f8ADVyvnA=</pareq>
+<url>http://www.acs.com</url>
+<enrolled>Y</enrolled>
+<xid>7ba3b1e6e6b542489b73243aac050777</xid>
+<sha1hash>9eda1f99191d4e994627ddf38550b9f47981f614</sha1hash>
+</response>"""
+ integration = self.mk_integration()
+ retval = integration.handle_3ds_verifyenrolled_response(Dummy200Response(example_xml))
+ self.assertEqual(retval.enrolled, True)
+ self.assertEqual(retval.response_code, "00")
+ self.assertEqual(retval.message, "Enrolled")
+ self.assertEqual(retval.url, "http://www.acs.com")
+ self.assertEqual(retval.pareq, "eJxVUttygkAM/ZUdnitZFlBw4na02tE6bR0vD+0bLlHpFFDASv++u6i1zVNycju54H2dfrIvKsokz3qWY3OLUabyOMm2PWu1fGwF1r3E5a4gGi5IHQuS+ExlGW2JJXHPCjcuVyLYbIRQnrf2o3VMEY+57q05oIsibP+nA4SL02k7mELhKupqxVqF2WVxEgdBpMX6dwE4YJhSsVkKB3RH9ypGFyvNXpkrLW982HcancQzn7MopSkO2RnqmxJZYXQgKjyY1YV39Lt6O5XA4/Fp9xV1b4LcDqdbDcum8xKJ9oqTxFMAMKN5OxotFIXrJNY1otpMH0qYQwP43w08Pn0/W1Ql6+nj+cegonAOKpICs5d3hY+czpdJ+g6HKHBUoNEyk8OwzZaDXXE58R3JtG/as7DBH+IqhZFvpS3zLsBHqeq4VU7/OMTA7Cr45wo/0wNptWlV4Xb8Thftv3A30xs+7GYaokej3c415TxhgIJhUu54TLF2jt33f8ADVyvnA=")
+
+ def test_parse_3ds_verifyenrolled_response_not_enrolled(self):
+ example_xml = """<?xml version="1.0" encoding="UTF-8" ?>
+<response timestamp="20030625171810">
+<merchantid>merchantid</merchantid>
+<account>internet</account>
+<orderid>orderid</orderid>
+<authcode></authcode>
+<result>110</result>
+<message>Not Enrolled</message>
+<pasref></pasref>
+<timetaken>3</timetaken>
+<authtimetaken>0</authtimetaken>
+<pareq>eJxVUttygkAM/ZUdnitZFlBw4na02tE6bR0vD+0bLlHpFFDASv++u6i1
+ zVNycju54H2dfrIvKsokz3qWY3OLUabyOMm2PWu1fGwF1r3E5a4gGi5IH
+ QuS+ExlGW2JJXHPCjcuVyLYbIRQnrf2o3VMEY+57q05oIsibP+nA4SL02k
+ 7mELhKupqxVqF2WVxEgdBpMX6dwE4YJhSsVkKB3RH9ypGFyvNXpkrLW
+ 982HcancQzn7MopSkO2RnqmxJZYXQgKjyY1YV39Lt6O5XA4/Fp9xV1b4L
+ cDqdbDcum8xKJ9oqTxFMAMKN5OxotFIXrJNY1otpMH0qYQwP43w08Pn0
+ /W1Ql6+nj+cegonAOKpICs5d3hY+czpdJ+g6HKHBUoNEyk8OwzZaDXXE
+ 58R3JtG/as7DBH+IqhZFvpS3zLsBHqeq4VU7/OMTA7Cr45wo/0wNptWlV
+
+4Xb8Thftv3A30xs+7GYaokej3c415TxhgIJhUu54TLF2jt33f8ADVyvnA=</pareq>
+<url></url>
+<enrolled>N</enrolled>
+<xid>e9dafe706f7142469c45d4877aaf5984</xid>
+<sha1hash>9eda1f99191d4e994627ddf38550b9f47981f614</sha1hash>
+</response>
+"""
+ integration = self.mk_integration()
+ retval = integration.handle_3ds_verifyenrolled_response(Dummy200Response(example_xml))
+ self.assertEqual(retval.enrolled, False)
+ self.assertEqual(retval.response_code, "110")
+ self.assertEqual(retval.message, "Not Enrolled")
+ self.assertEqual(retval.url, None)
+ gateway = self.mk_gateway()
+ card = self.get_visa_card()
+ gateway.validate_card(card)
+ proceed, extra = retval.proceed_with_auth(card)
+ self.assertEqual(proceed, True),
+ self.assertEqual(extra, {'mpi': {'eci': 6}})
+
+ def test_send_3ds_verifyenrolled(self):
+ integration = self.mk_integration()
+ gateway = self.mk_gateway()
+ card = self.get_visa_card()
+ gateway.validate_card(card)
+ response = integration.send_3ds_verifyenrolled({
+ 'order_id': 1,
+ 'amount': Decimal('24.99'),
+ 'card': card,
+ })
+
+ self.assertEqual(response.error, True)
+
+ def test_encode(self):
+ card = self.get_visa_card()
+ integration = self.mk_integration()
+ gateway = self.mk_gateway()
+ gateway.validate_card(card) # Adds 'card_type'
+ d = {'some_data': 1,
+ 'card': card,
+ 'amount': Decimal('12.34'),
+ }
+ encoded = integration.encode_merchant_data(d)
+ decoded = integration.decode_merchant_data(encoded)
+ self.assertEqual(decoded['some_data'], 1)
+ self.assertEqual(decoded['card'].number, card.number)
+ self.assertEqual(decoded['card'].card_type, Visa)
+ self.assertEqual(decoded['amount'], d['amount'])
+
+ def test_3ds_verifysig_xml(self):
+
+ expected = """<?xml version="1.0" encoding="UTF-8" ?>
+<request timestamp="20100625172305" type="3ds-verifysig">
+ <merchantid>thestore</merchantid>
+ <account>theaccount</account>
+ <orderid>1</orderid>
+ <amount currency="GBP">2499</amount>
+ <card>
+ <number>4903034000057389</number>
+ <expdate>0714</expdate>
+ <chname>Mickey Mouse</chname>
+ <type>VISA</type>
+ </card>
+ <pares>xyz</pares>
+ <sha1hash>272d8dde0bf34a0e744f696f2860a7894b687cf7</sha1hash>
+</request>"""
+
+ integration = self.mk_integration()
+ gateway = self.mk_gateway()
+ card = CreditCard(first_name='Mickey',
+ last_name='Mouse',
+ month=7,
+ year=2014,
+ number='4903034000057389',
+ verification_value='123',
+ )
+ gateway.validate_card(card)
+ actual_xml = integration.build_3ds_verifysig_xml('xyz',
+ {'order_id': 1,
+ 'amount': Decimal('24.99'),
+ 'card': card,
+ 'timestamp': datetime(2010,6, 25, 17, 23, 05),
+ })
+ self.assertXMLEqual(actual_xml, expected)
+
+ def test_parse_3ds_verifysig_response_no_auth(self):
+ example_xml = """<response timestamp="20100625171823">
+<merchantid>merchantid</merchantid>
+<account />
+<orderid>orderid</orderid>
+<result>00</result>
+<message>Authentication Successful</message>
+<threedsecure>
+<status>N</status>
+<eci />
+<xid />
+<cavv />
+<algorithm />
+</threedsecure>
+<sha1hash>e5a7745da5dc32d234c3f52860132c482107e9ac</sha1hash>
+</response>
+"""
+ integration = self.mk_integration()
+ gateway = self.mk_gateway()
+ card = self.get_visa_card()
+ gateway.validate_card(card)
+ retval = integration.handle_3ds_verifysig_response(Dummy200Response(example_xml))
+ self.assertEqual(retval.response_code, "00")
+ self.assertEqual(retval.message, "Authentication Successful")
+ self.assertEqual(retval.status, "N")
+ self.assertEqual(retval.proceed_with_auth(card)[0], False) # status is "N"
+
+ def test_parse_3ds_verifysig_response_yes_auth(self):
+ example_xml = """<response timestamp="20100625171823">
+<merchantid>merchantid</merchantid>
+<account />
+<orderid>orderid</orderid>
+<result>00</result>
+<message>Authentication Successful</message>
+<threedsecure>
+<status>Y</status>
+<eci>5</eci>
+<xid>crqAeMwkEL9r4POdxpByWJ1/wYg=</xid>
+<cavv>AAABASY3QHgwUVdEBTdAAAAAAAA=</cavv>
+<algorithm />
+</threedsecure>
+<sha1hash>e5a7745da5dc32d234c3f52860132c482107e9ac</sha1hash>
+</response>
+"""
+ integration = self.mk_integration()
+ gateway = self.mk_gateway()
+ card = self.get_visa_card()
+ gateway.validate_card(card)
+ retval = integration.handle_3ds_verifysig_response(Dummy200Response(example_xml))
+ self.assertEqual(retval.response_code, "00")
+ self.assertEqual(retval.message, "Authentication Successful")
+ self.assertEqual(retval.status, "Y")
+ proceed, data = retval.proceed_with_auth(card)
+ self.assertTrue(proceed)
+ self.assertEqual(data, {'mpi':{'eci': '5',
+ 'xid': 'crqAeMwkEL9r4POdxpByWJ1/wYg=',
+ 'cavv': 'AAABASY3QHgwUVdEBTdAAAAAAAA=',
+ }})
+
+ def test_send_3ds_verifysig(self):
+ integration = self.mk_integration()
+ gateway = self.mk_gateway()
+ card = self.get_visa_card()
+ gateway.validate_card(card)
+ response = integration.send_3ds_verifysig('xyz', {
+ 'order_id': 1,
+ 'amount': Decimal('24.99'),
+ 'card': card,
+ })
+
+ self.assertEqual(response.error, True)
View
21 billing/tests/utils.py
@@ -0,0 +1,21 @@
+import re
+
+from django.test.utils import compare_xml
+from lxml import etree
+
+
+class BetterXMLCompareMixin(object):
+
+ maxDiff = None
+
+ def assertXMLEqual(self, expected, actual):
+ if not compare_xml(actual, expected):
+ self.assertMultiLineEqual(self.norm_whitespace(expected),
+ self.norm_whitespace(actual))
+
+ def norm_whitespace(self, v):
+ v = re.sub(b"^ *", b"", v, flags=re.MULTILINE)
+ v = re.sub(b"\n", b"", v)
+ v = re.sub(b"\t", b"", v)
+ return etree.tostring(etree.fromstring(v), pretty_print=True)
+
View
21 billing/utils/credit_card.py
@@ -23,8 +23,12 @@ class CreditCard(object):
card_name = None
def __init__(self, **kwargs):
- self.first_name = kwargs["first_name"]
- self.last_name = kwargs["last_name"]
+ if ("first_name" not in kwargs
+ or "last_name" not in kwargs) and "cardholders_name" not in kwargs:
+ raise TypeError("You must provide cardholders_name or first_name and last_name")
+ self.first_name = kwargs.get("first_name", None)
+ self.last_name = kwargs.get("last_name", None)
+ self.cardholders_name = kwargs.get("cardholders_name", None)
self.month = int(kwargs["month"])
self.year = int(kwargs["year"])
self.number = kwargs["number"]
@@ -45,12 +49,13 @@ def is_expired(self):
def valid_essential_attributes(self):
"""Validate that all the required attributes of card are given"""
- return self.first_name and \
- self.last_name and \
- self.month and \
- self.year and \
- self.number and \
- self.verification_value and True
+ return (((self.first_name and
+ self.last_name) or
+ self.cardholders_name)
+ and self.month
+ and self.year
+ and self.number
+ and self.verification_value)
def is_valid(self):
"""Check the validity of the card"""
View
25 billing/utils/json.py
@@ -0,0 +1,25 @@
+
+
+# Utilties for building functions to pass to json.loads and json.dumps
+# for custom encoding/decoding.
+
+def chain_custom_encoders(encoders):
+ def combined_encoder(obj):
+ for encoder in encoders:
+ try:
+ return encoder(obj)
+ except TypeError:
+ continue
+ raise TypeError("Unknown type %s" % obj.__class__)
+ return combined_encoder
+
+
+def chain_custom_decoders(decoders):
+ def combined_decoder(dct):
+ for decoder in decoders:
+ dct = decoder(dct)
+ if not hasattr(dct, '__getitem__'):
+ # Already changed
+ return dct
+ return dct
+ return combined_decoder
View
16 docs/credit_card.rst
@@ -20,14 +20,16 @@ Attribute Reference
Method Reference
-----------------
-* `__init__`: This method expects 6 **compulsory** keyword arguments. They are
+* `__init__`: This method expects 6 keyword arguments. They are
* `first_name`: The first name of the credit card holder.
* `last_name`: The last name of the credit card holder.
- * `month`: The expiration month of the credit card as an integer.
- * `year`: The expiration year of the credit card as an integer.
- * `number`: The credit card number (generally 16 digits).
- * `verification_value`: The card security code (CVV2).
+ * `cardholders_name`: The full name of the credit card holder, as an
+ alternative to supplying `first_name` and `last_name`.
+ * `month`: The expiration month of the credit card as an integer. **Required**
+ * `year`: The expiration year of the credit card as an integer. **Required**
+ * `number`: The credit card number (generally 16 digits). **Required**
+ * `verification_value`: The card security code (CVV2). **Required**
* `is_luhn_valid`: Checks the validity of the credit card number by using the
`Luhn's algorithm` and returns a boolean. This method takes no arguments.
* `is_expired`: Checks if the expiration date of the card is beyond today and
@@ -48,6 +50,10 @@ Method Reference
Subclasses
----------
+Normally you do not use the subclasses directly. Instead, you use `CreditCard`,
+and call gateway.validate_card() which will add a `card_type` attribute which is
+the subclass.
+
The various credit cards and debit cards supported by Merchant_ are:
Credit Cards
View
105 docs/gateways/global_iris_gateway.rst
@@ -0,0 +1,105 @@
+===========
+Global Iris
+===========
+
+This gateway is an implementation of `Global Iris
+<https://resourcecentre.globaliris.com/>`_ RealAuth, previously known as HSBC
+Merchant Services or Global Payments.
+
+Normally you will use this in conjunction with the :doc:`Global Iris RealMPI </offsite/global_iris_real_mpi_integration>`.
+
+Usage
+-----
+
+You will need an account with Global Iris - email
+globaliris@realexpayments.com. You will also need to let them know your
+server(s) IP address(es).
+
+* Add the following attributes to your `settings.py`::
+
+ MERCHANT_SETTINGS = {
+ "global_iris": {
+ "TEST": {
+ "SHARED_SECRET": "???",
+ "MERCHANT_ID": "???",
+ "ACCOUNT": "???",
+ },
+ "LIVE": {
+ "SHARED_SECRET": "???",
+ "MERCHANT_ID": "???",
+ "ACCOUNT": "???",
+ },
+ }
+ }
+
+ The details in 'TEST' are used for some of the automated tests, and if
+ MERCHANT_TEST_MODE is True.
+
+ In addition, you may have been provided with separate accounts for different
+ credit card types. These can be configured using additional dictionaries,
+ with a key composed of ``'LIVE_'`` or ``'TEST_'`` followed by the following strings:
+
+ * 'VISA' for Visa
+ * 'MC' for MasterCard
+ * 'AMEX' for AmericanExpress
+
+
+* Use the gateway instance::
+
+ >>> g1 = get_gateway("global_iris")
+ >>> cc = Visa(first_name="Joe",
+ last_name="Bloggs",
+ number="4012345678901234",
+ month=12,
+ year=2014,
+ verification_value="456")
+
+ >>> response = g1.purchase(Decimal("15.00"), cc, options={'order_id': 123})
+ >>> response
+ {"status":"SUCCESS", "message": "Authcode: ...", "response":<Response [200]> }
+
+ >>> response
+ {"status:"FAILURE", "message": "...", response_code: 205}
+
+
+ ``options`` must include the following:
+
+ * ``order_id``: string that uniquely identifies the transaction.
+
+ It may include:
+
+ * ``billing_address`` and ``shipping_address``: dictionaries with the items:
+
+ * ``zip``: ZIP or post code.
+ * ``country``: 2 letter ISO country code.
+
+ * ``currency``: which can be ``'GBP'``, ``'USD'`` or ``'EUR'`` (defaults to ``'GBP'``).
+
+ * ``customer``: string that uniquely identifies the customer
+
+* You may want to run the tests::
+
+ ./manage.py test billing
+
+
+ To run all the test suite, you will need a 'TEST' dictionary in your
+ MERCHANT_SETTINGS['global_iris'] dictionary, as above, and you will also need
+ to add a list of cards to test in a 'TEST_CARDS' entry as below::
+
+ MERCHANT_SETTINGS = {
+ "global_iris": {
+ "TEST": {
+ "SHARED_SECRET": 'x',
+ "MERCHANT_ID": 1234,
+ "ACCOUNT": "foo",
+ },
+ "TEST_CARDS": [
+ {
+ 'TYPE': 'VISA',
+ 'NUMBER': '4263791920101037',
+ 'RESPONSE_CODE': '00',
+ },
+ ...
+ ],
+ }
+ }
View
2  docs/index.rst
@@ -68,6 +68,7 @@ Contents:
* :doc:`Beanstream <gateways/beanstream_gateway>`
* :doc:`Chargebee <gateways/chargebee_gateway>`
* :doc:`Bitcoin <gateways/bitcoin_gateway>`
+ * :doc:`Global Iris <gateways/global_iris_gateway>`
* :doc:`Off-site Processing <offsite_processing>`
@@ -79,6 +80,7 @@ Contents:
* :doc:`Stripe <offsite/stripe_integration>`
* :doc:`eWAY <offsite/eway_au>`
* :doc:`Authorize.Net Direct Post Method <offsite/authorize_net_dpm>`
+ * :doc:`Global Iris RealMPI <offsite/global_iris_real_mpi_integration>`
* :doc:`Signals <signals>`
* :doc:`Writing your own gateway <custom_gateway>`
View
237 docs/offsite/global_iris_real_mpi_integration.rst
@@ -0,0 +1,237 @@
+=====================
+ Global Iris RealMPI
+=====================
+
+Global Iris RealMPI is an implementation of “3D Secure” that goes hand-in-hand
+with the :doc:`Global Iris RealAuth gateway </gateways/global_iris_gateway>`
+implementation. It is possible to use the gateway without the integration,
+and in fact that has to be done in for some card types.
+
+“3D secure” (better known by the branded services such as “Verified by Visa” and
+“SecureCard”) is a service which helps to reduce the problem of online card
+fraud (especially fraudulent chargebacks) by getting the customer to authorize
+the use of a card via the card provider's own website.
+
+Please see the `Global Iris docs <https://resourcecentre.globaliris.com/>`_ for
+more information.
+
+A summary of the full workflow looks like this:
+
+1. At checkout, the user fills in their credit card details and presses "Pay" or
+ "Buy" etc.
+
+2. Your website uses this integration implementation to send a
+ ‘3DS-Verifyenrolled’ request to the Global Iris RealMPI service. The service
+ will return a response indicating:
+
+ * whether the card issuer has a 3D Secure implementation. If they do, the URL
+ of that service will be provided.
+
+ * whether the card user is enrolled in the 3D Secure service.
+
+3. Assuming there is a 3D Secure service and the card user is enrolled, your
+ website returns a response that contains a form. This form is targetted at
+ the URL provided in step 2, and auto-submits itself, taking the user to the
+ card issuer's 3D Secure site (e.g. Verified by Visa). The form includes data
+ relevant to the transaction (including card details and the amount etc.), and
+ also a return URL that you must provide (the ‘TermURL’). You will also need
+ to provide some custom 'merchant data' that will be passed back, so that you
+ can continue the process in step 5.
+
+4. The user enters authentication details at the card issuer's site (e.g. parts
+ of a password) and hits submit.
+
+5. The card issuer's site returns the user to your website, to the URL you
+ specified in step 3, along with some POST data indicating the status of the
+ authentication attempt. (The 'PaRes' data plus the 'merchant data' you supplied
+ in step 3).
+
+6. Using the integration implementation, your website sends another request to the
+ RealMPI service to verify the information POSTed in step 5 (since it is possible for
+ the user to tamper with this data).
+
+7. If everything is OK, your website proceeds to use the Global Iris gateway to make
+ the purchase. If there was an error, you will need to display an error message
+ to the user and give them the opportunity to try again.
+
+Many of these steps require custom code and views, but the integration does
+the heavy lifting of implementing the Real MPI spec.
+
+Also, at various points there might be errors and shortcuts, so the full process
+is more complex. A full example is below. It assumes you are able to store
+order details etc. in some way. You will also need to be able to generate unique
+transaction IDs for each attempt at authentication. The best way is to create
+some kind of 'pending payment' model that is related to your order model.
+
+Example:
+--------
+
+This example assumes you are using the messages framework, and will display
+messages on the payment page. This simplifies some of the flows. Bits of code
+that you need to implement are marked TODO.
+
+In some_project/views.py::
+
+
+ from django.contrib import messages
+ from django.core.urlresolvers import reverse
+ from django.http import HttpResponseRedirect
+ from django.shortcuts import render
+ from django.views.decorators.csrf import csrf_exempt
+
+ from billing import get_gateway, get_integration
+ from billing.forms.global_iris_forms import CreditCardForm
+
+
+ # 'Pay' page - user fills in credit card.
+
+ def pay(request):
+
+ # Order should have been checked, with shipping details etc.
+ order = order_from_request(request) # TODO
+
+ gateway = get_gateway('global_iris')
+ integration = get_integration('global_iris_real_mpi')
+
+ if request.method == "POST":
+ form = CreditCardForm(request.POST, gateway=gateway)
+
+ if form.is_valid(): # This has already included the gateway.validate_card check
+ card = form.get_credit_card() # Returns a standard billing.CreditCard instance, validated.
+ pending_payment = create_pending_payment(order)
+ transaction_id = pending_payment.transaction_id
+ global_iris_data = build_global_iris_data(order, transaction_id, card)
+
+ # Some cards aren't supported, so we have to skip the integration
+ do_purchase_directly = not integration.card_supported(card)
+
+ if not do_purchase_directly:
+ enrolled_resp = integration.send_3ds_verifyenrolled(global_iris_data)
+ if enrolled_resp.error:
+ messages.error(request, "There was an error processing the credit card information: %s" % enrolled_resp.message)
+ else:
+ # In some cases we can proceed to gateway.purchase at this point without doing
+ # the full 3Dsecure external checks (e.g. the user is not enrolled)
+
+ can_proceed, extra_data = enrolled_resp.proceed_with_auth(card)
+ if can_proceed:
+ global_iris_data.update(extra_data) # Includes some extra MPI data
+ do_purchase_directly = True
+ else:
+ return integration.redirect_to_acs_url(enrolled_resp,
+ term_url(request),
+ global_iris_data)
+
+ if do_purchase_directly:
+ return attempt_purchase(request, gateway, order, card, global_iris_data)
+
+ else:
+ form = CreditCardForm() # Could supply some initial data for 'cardholders_name' here
+
+ return render(request, 'pay.html', {'form': form})
+
+
+ # View that handles POST from 3D secure site
+
+ @csrf_exempt
+ def handle_3ds(request):
+
+ gateway = get_gateway('global_iris')
+ integration = get_integration('global_iris_real_mpi')
+
+ pares, merchant_data = integration.parse_3d_secure_request(request)
+ verifysig_response = integration.send_3ds_verifysig(pares, merchant_data)
+ card = merchant_data['card']
+ proceed, extra_data = verifysig_response.proceed_with_auth(card)
+ if proceed:
+ merchant_data.update(extra_data)
+ order = order_from_merchant_data(merchant_data)
+ return attempt_purchase(request, cc_gateway, order, card, merchant_data)
+ else:
+ messages.error(request, "The use of your credit card could not be verified. Please try again.")
+ return redirect_to_payment_page(request)
+
+ # Final view - when payment is complete:
+
+ def success(request):
+ # TODO
+ return render(request, 'success.html', {})
+
+ # Utilities:
+
+ def build_global_iris_data(order, transaction_id, card):
+ return {
+ 'order_id': transaction_id,
+ 'card': card,
+ 'amount': order.balance, # TODO
+ # Other 'merchant data' can be put here.
+ # It will be serialised to JSON, so must be supported by the JSON encoder.
+ # The JSON encoder has special support for:
+ # * CreditCard objects
+ # * Decimal objects
+ }
+
+ def create_pending_payment(order)
+ # A pending payment should have a unique transaction_id. It's not
+ # enough to use the order ID, because it needs to be different for
+ # each *attempt* to pay for the order.
+ # You will also probably need routines to clean up these things.
+
+ # TODO
+
+ def term_url(request):
+ # TODO - change
+ return reverse('some_project.views.handle_3ds_request')
+
+ def attempt_purchase(request, gateway, order, card, options):
+ transaction_id = options['order_id']
+ purchase_resp = gateway.purchase(options['amount'],
+ card,
+ options=options)
+ if purchase_resp['status'] == 'SUCCESS':
+ record_payment(order, transaction_id)
+ return redirect_to_success_page(request)
+ else:
+ record_failure(order, transaction_id, purchase_resp.get('response_code', ''), purchase_resp['message'])
+ messages.error(request, "There was a problem with your credit card information: %s" % purchase_resp['message'])
+ return redirect_to_payment_page(request)
+
+
+ def record_payment(order, transaction_id):
+ # TODO:
+ # record the payment and start processing the order
+ # (change status etc). if it is fully paid.
+
+
+ def record_failure(order, transaction_id, response_code, message):
+ # TODO
+ # record a payment failure somehow.
+
+
+ def redirect_to_success_page(request):
+ # TODO - change
+ return HttpResponseRedirect(reverse('some_project.views.success'))
+
+
+ def redirect_to_payment_page(request):
+ # TODO - change
+ return HttpResponseRedirect(reverse('some_project.views.pay'))
+
+
+ def order_from_merchant_data(merchant_data):
+ transaction_id = merchant_data['order_id']
+ # TODO - change
+ pending_payment = PendingPayment.objects.get(transaction_id=transaction_id)
+ return pending_payment.order
+
+
+In the urls.py::
+
+
+ urlpatterns += patterns('',
+ (r'^pay/', 'some_project.views.pay'),
+ (r'^success/', 'some_project.views.success'),
+ (r'^3ds/', 'some_project.views.handle_3ds'),
+ )
+
+
View
1  requirements.txt
@@ -1 +1,2 @@
-e git://github.com/arjunc77/django-ogone.git#egg=django-ogone
+lxml

0 comments on commit ea70439

Please sign in to comment.
Something went wrong with that request. Please try again.