diff --git a/README.txt b/README.txt index c2d6a62..cb84646 100644 --- a/README.txt +++ b/README.txt @@ -2,7 +2,7 @@ Python module to interface with French's bank online credit card processing services. Services supported are: -- ATOS/SIP used by: +- ATOS/SIPS used by: - BNP under the name Mercanet, - Banque Populaire (before 2010/2011) under the name Cyberplus, - CCF under the name Elysnet, @@ -25,10 +25,16 @@ from those services, reporting whether the transaction was successful and which one it was. The full content (which is specific to the service) is also reported for logging purpose. -For SystemPay and SPPlus the module is totally independent from the respective -implementation distributed by the Bank, but for ATOS/SIPS the kit distributed -by the bank is also needed as the protocol created by ATOS is proprietary and -not documented. - The spplus and paybox module also depend upon the python Crypto library for DES decoding of the merchant key and RSA signature validation on the responses. + +Changelog +========= + +1.4 +--- +- add sips2 backend to conform with version 2.3 of their interface + +1.3 +--- +- add payzen backend diff --git a/eopayment/__init__.py b/eopayment/__init__.py index 3e16d78..1d5dade 100644 --- a/eopayment/__init__.py +++ b/eopayment/__init__.py @@ -7,6 +7,7 @@ __all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', 'PAID', 'DENIED', 'CANCELED', 'CANCELLED', 'ERROR', 'get_backends'] SIPS = 'sips' +SIPS2 = 'sips2' SYSTEMPAY = 'systempayv2' SPPLUS = 'spplus' TIPI = 'tipi' @@ -21,7 +22,7 @@ def get_backend(kind): module = __import__(kind, globals(), locals(), []) return module.Payment -__BACKENDS = [ DUMMY, SIPS, SYSTEMPAY, SPPLUS, OGONE, PAYBOX, PAYZEN ] +__BACKENDS = [ DUMMY, SIPS, SIPS2, SYSTEMPAY, SPPLUS, OGONE, PAYBOX, PAYZEN ] def get_backends(): '''Return a dictionnary mapping existing eopayment backends name to their diff --git a/eopayment/sips2.py b/eopayment/sips2.py new file mode 100644 index 0000000..0323c5e --- /dev/null +++ b/eopayment/sips2.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +import urlparse +import string +from decimal import Decimal +import uuid +import hashlib +from gettext import gettext as _ + +from common import PaymentCommon, FORM, Form, PaymentResponse, PAID, ERROR, CANCELED + +''' +Payment backend module for the ATOS/SIPS system used by many French banks. + +It use the middleware given by the bank. + +The necessary options are: + + - pathfile, to indicate the absolute path of the pathfile file given by the + bank, + - binpath, the path of the directory containing the request and response + executables, + +All the other needed parameters SHOULD already be set in the parmcom files +contained in the middleware distribution file. + +''' + +__all__ = ['Payment'] + + +class Payment(PaymentCommon): + URL = { + 'test': 'https://payment-webinit.simu.sips-atos.com/paymentInit', + 'prod': 'https://payment-webinit.sips-atos.com/paymentInit', + } + INTERFACE_VERSION = 'HP_2.3' + RESPONSE_CODES = { + '00': 'Authorisation accepted', + '02': 'Authorisation request to be performed via telephone with the issuer, as the ' + 'card authorisation threshold has been exceeded, if the forcing is authorised for ' + 'the merchant', + '03': 'Invalid distance selling contract', + '05': 'Authorisation refused', + '12': 'Invalid transaction, verify the parameters transferred in the request.', + '14': 'Invalid bank details or card security code', + '17': 'Buyer cancellation', + '24': 'Operation impossible. The operation the merchant wishes to perform is not ' + 'compatible with the status of the transaction.', + '25': 'Transaction not found in the Sips database', + '30': 'Format error', + '34': 'Suspicion of fraud', + '40': 'Function not supported: the operation that the merchant would like to perform ' + 'is not part of the list of operations for which the merchant is authorised', + '51': 'Amount too high', + '54': 'Card is past expiry date', + '60': 'Transaction pending', + '63': 'Security rules not observed, transaction stopped', + '75': 'Number of attempts at entering the card number exceeded', + '90': 'Service temporarily unavailable', + '94': 'Duplicated transaction: for a given day, the TransactionReference has already been ' + 'used', + '97': 'Timeframe exceeded, transaction refused', + '99': 'Temporary problem at the Sips Office Server level', + } + TEST_MERCHANT_ID = '002001000000001' + + description = { + 'caption': 'SIPS 2', + 'parameters': [ + { + 'name': 'platform', + 'caption': _('Platform'), + 'default': 'test', + 'choices': ['test', 'prod'], + 'required': True, + }, + { + 'name': 'merchand_id', + 'caption': _('Merchant ID'), + 'default': TEST_MERCHANT_ID, + 'required': True, + }, + { + 'name': 'secret_key', + 'caption': _('Secret Key'), + 'default': '002001000000001_KEY1', + 'required': True, + }, + { + 'name': 'key_version', + 'caption': _('Key Version'), + 'default': '1', + 'required': True, + }, + { + 'name': 'normal_return_url', + 'caption': _('Normal return URL'), + 'default': 'http://www.example.com/', + 'required': True, + }, + { + 'name': 'automatic_return_url', + 'caption': _('Automatic return URL'), + 'required': False, + }, + { + 'name': 'currency_code', + 'caption': _('Currency code'), + 'default': '978', + 'choices': ['978'], + 'required': True, + } + ], + } + + def __init__(self, options, logger=None): + super(Payment, self).__init__(options) + + def encode_data(self, data): + return u'|'.join(u'%s=%s' % (unicode(key), unicode(value)) + for key, value in data.iteritems()) + + def seal_data(self, data): + s = self.encode_data(data) + s += unicode(self.secret_key) + s = s.encode('utf-8') + s = hashlib.sha256(s).hexdigest() + return s + + def get_data(self): + data = {} + data['merchantId'] = self.merchand_id + data['keyVersion'] = self.key_version + data['normalReturnUrl'] = self.normal_return_url + data['currencyCode'] = self.currency_code + return data + + def get_url(self): + return self.URL[self.platform] + + def request(self, amount, name=None, address=None, email=None, phone=None, info1=None, + info2=None, info3=None, next_url=None, **kwargs): + data = self.get_data() + transaction_id = self.transaction_id(6, string.digits, 'sips2', data['merchantId']) + data['transactionReference'] = unicode(transaction_id) + data['orderId'] = unicode(uuid.uuid4()).replace('-', '') + data['Amount'] = unicode(int(Decimal(amount) * 100)) + if email: + data['customerEmail'] = email + if next_url: + data['normalReturnUrl'] = next_url + form = Form( + url=self.get_url(), + method='POST', + fields=[ + { + 'type': 'hidden', + 'name': 'Data', + 'value': self.encode_data(data) + }, + { + 'type': 'hidden', + 'name': 'Seal', + 'value': self.seal_data(data), + }, + { + 'type': 'hidden', + 'name': 'InterfaceVersion', + 'value': self.INTERFACE_VERSION, + }, + ]) + self.logger.debug('emitting request %r', data) + return transaction_id, FORM, form + + def decode_data(self, data): + data = data.split('|') + data = [map(unicode, p.split('=')) for p in data] + return dict(data) + + def check_seal(self, data, seal): + return seal == self.seal_data(data) + + response_code_to_result = { + '00': PAID, + '17': CANCELED, + } + + def response(self, query_string, **kwargs): + form = urlparse.parse_qs(query_string) + self.logger.debug('received query string %r', form) + assert 'Data' in form + assert 'Seal' in form + assert 'InterfaceVersion' in form + data = self.decode_data(form['Data'][0]) + seal = form['Seal'] + self.logger.debug('parsed response %r seal %r', data, seal) + signed = self.check_seal(data, seal) + response_code = data['responseCode'] + transaction_id = data.get('transactionReference') + result = self.response_code_to_result.get(response_code, ERROR) + merchant_id = data.get('merchantId') + test = merchant_id == self.TEST_MERCHANT_ID + return PaymentResponse( + result=result, + signed=signed, + bank_data=data, + order_id=transaction_id, + transaction_id=data.get('authorisationId'), + bank_status=self.RESPONSE_CODES.get(response_code, u'unknown code - ' + response_code), + test=test) diff --git a/tests/sips2.py b/tests/sips2.py new file mode 100644 index 0000000..b641c11 --- /dev/null +++ b/tests/sips2.py @@ -0,0 +1,13 @@ +import pytest +import pprint + +import eopayment + +def test_build_request(): + backend = eopayment.Payment('sips2', {}) + assert backend.request(amount='12') + +def test_parse_response(): + qs = '''Data=captureDay%3D0%7CcaptureMode%3DAUTHOR_CAPTURE%7CcurrencyCode%3D978%7CmerchantId%3D002001000000001%7CorderChannel%3DINTERNET%7CresponseCode%3D00%7CtransactionDateTime%3D2016-02-01T17%3A44%3A20%2B01%3A00%7CtransactionReference%3D668930%7CkeyVersion%3D1%7CacquirerResponseCode%3D00%7Camount%3D1200%7CauthorisationId%3D12345%7CcardCSCResultCode%3D4E%7CpanExpiryDate%3D201605%7CpaymentMeanBrand%3DMASTERCARD%7CpaymentMeanType%3DCARD%7CcustomerIpAddress%3D82.244.203.243%7CmaskedPan%3D5100%23%23%23%23%23%23%23%23%23%23%23%2300%7CorderId%3Dd4903de7027f4d56ac01634fd7ab9526%7CholderAuthentRelegation%3DN%7CholderAuthentStatus%3D3D_ERROR%7CtransactionOrigin%3DINTERNET%7CpaymentPattern%3DONE_SHOT&Seal=6ca3247765a19b45d25ad54ef4076483e7d55583166bd5ac9c64357aac097602&InterfaceVersion=HP_2.0&Encode=''' + backend = eopayment.Payment('sips2', {}) + assert backend.response(qs)