Permalink
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...
1 parent 7bb60c3 commit ea70439374b0ab2b5c0db25ac5a03b2d65af443e @spookylukey spookylukey committed with tuxcanfly Feb 12, 2014
View
@@ -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
@@ -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
@@ -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)]
+
@@ -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)
Oops, something went wrong.

0 comments on commit ea70439

Please sign in to comment.