diff --git a/README.txt b/README.txt index 8591a1f..301797a 100644 --- a/README.txt +++ b/README.txt @@ -29,6 +29,15 @@ reported for logging purpose. 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. +Some backends allow to specify the order and transaction ids in different +fields, in order to allow to match them in payment system backoffice. They are: +- Payzen +- SIPS +- SystemPay + +For other backends, the order and transaction ids, separated by '!' are sent in +order id field, so they can be matched in backoffice. + Changelog ========= diff --git a/eopayment/common.py b/eopayment/common.py index 1aa1ac2..555cda1 100644 --- a/eopayment/common.py +++ b/eopayment/common.py @@ -23,6 +23,9 @@ CANCELLED = 5 CANCELED = 5 # typo for backward compatibility ERROR = 99 +# separator between order and transaction ids +ORDERID_TRANSACTION_SEPARATOR = '!' + class PaymentResponse(object): '''Holds a generic view on the result of payment transaction response. diff --git a/eopayment/dummy.py b/eopayment/dummy.py index 8c0f367..11d1d63 100644 --- a/eopayment/dummy.py +++ b/eopayment/dummy.py @@ -72,7 +72,7 @@ class Payment(PaymentCommon): } def request(self, amount, name=None, address=None, email=None, phone=None, - info1=None, info2=None, info3=None, next_url=None, **kwargs): + orderid=None, info1=None, info2=None, info3=None, next_url=None, **kwargs): self.logger.debug('%s amount %s name %s address %s email %s phone %s' ' next_url %s info1 %s info2 %s info3 %s kwargs: %s', __name__, amount, name, address, email, phone, info1, info2, info3, next_url, kwargs) @@ -89,7 +89,7 @@ class Payment(PaymentCommon): 'origin': self.origin } query.update(dict(name=name, address=address, email=email, phone=phone, - info1=info1, info2=info2, info3=info3)) + orderid=orderid, info1=info1, info2=info2, info3=info3)) for key in query.keys(): if query[key] is None: del query[key] @@ -128,13 +128,11 @@ if __name__ == '__main__': p = Payment(options) retour = 'http://example.com/retour?amount=10.0&direct_notification_url=http%3A%2F%2Fexample.com%2Fdirect_notification_url&email=toto%40example.com&transaction_id=6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T&return_url=http%3A%2F%2Fexample.com%2Fretour&nok=1' r = p.response(retour.split('?',1)[1]) - assert not r[0] + assert not r[0] assert r[1] == '6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T' assert r[3] is None retour = 'http://example.com/retour?amount=10.0&direct_notification_url=http%3A%2F%2Fexample.com%2Fdirect_notification_url&email=toto%40example.com&transaction_id=6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T&return_url=http%3A%2F%2Fexample.com%2Fretour&ok=1&signed=1' r = p.response(retour.split('?',1)[1]) - assert r[0] + assert r[0] assert r[1] == '6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T' assert r[3] == 'signature ok' - - diff --git a/eopayment/ogone.py b/eopayment/ogone.py index 281b138..2a72d7c 100644 --- a/eopayment/ogone.py +++ b/eopayment/ogone.py @@ -5,7 +5,7 @@ import urlparse from decimal import Decimal, ROUND_HALF_UP from common import (PaymentCommon, PaymentResponse, FORM, CANCELLED, PAID, - ERROR, Form, DENIED, ACCEPTED) + ERROR, Form, DENIED, ACCEPTED, ORDERID_TRANSACTION_SEPARATOR) def N_(message): return message ENVIRONMENT_TEST = 'TEST' @@ -467,7 +467,14 @@ class Payment(PaymentCommon): def request(self, amount, orderid=None, name=None, email=None, language=None, description=None, **kwargs): - reference = orderid or self.transaction_id(20, string.digits + string.ascii_letters) + + reference = self.transaction_id(20, string.digits + string.ascii_letters) + + # prepend order id in payment reference + if orderid: + if len(orderid) > 24: + raise ValueError('orderid length exceeds 25 characters') + reference = orderid + ORDERID_TRANSACTION_SEPARATOR + self.transaction_id(29-len(orderid), string.digits + string.ascii_letters) language = language or self.language # convertir en centimes amount = Decimal(amount) * 100 @@ -527,6 +534,9 @@ class Payment(PaymentCommon): self.logger.error('response STATUS=%s NCERROR=%s NCERRORPLUS=%s', status, error, params.get('NCERRORPLUS', '')) result = ERROR + # extract reference from received order id + if ORDERID_TRANSACTION_SEPARATOR in reference: + reference, transaction_id = reference.split(ORDERID_TRANSACTION_SEPARATOR, 1) return PaymentResponse( result=result, signed=signed, diff --git a/eopayment/paybox.py b/eopayment/paybox.py index 20e3b43..31a90f1 100644 --- a/eopayment/paybox.py +++ b/eopayment/paybox.py @@ -15,7 +15,8 @@ import base64 from gettext import gettext as _ import string -from common import PaymentCommon, PaymentResponse, FORM, PAID, ERROR, Form +from common import (PaymentCommon, PaymentResponse, FORM, PAID, ERROR, Form, + ORDERID_TRANSACTION_SEPARATOR) __all__ = ['sign', 'Payment'] @@ -192,7 +193,7 @@ class Payment(PaymentCommon): ] } - def request(self, amount, email, name=None, **kwargs): + def request(self, amount, email, name=None, orderid=None, **kwargs): d = OrderedDict() d['PBX_SITE'] = unicode(self.site) d['PBX_RANG'] = unicode(self.rang).strip()[-2:] @@ -203,6 +204,9 @@ class Payment(PaymentCommon): self.transaction_id(12, string.digits, 'paybox', self.site, self.rang, self.identifiant) d['PBX_CMD'] = unicode(transaction_id) + # prepend order id command reference + if orderid: + d['PBX_CMD'] = orderid + ORDERID_TRANSACTION_SEPARATOR + d['PBX_CMD'] d['PBX_PORTEUR'] = unicode(email) d['PBX_RETOUR'] = 'montant:M;reference:R;code_autorisation:A;erreur:E;signature:K' d['PBX_HASH'] = 'SHA512' @@ -252,9 +256,12 @@ class Payment(PaymentCommon): bank_status = PAYBOX_ERROR_CODES.get(prefix + suffix) if bank_status is not None: break - + orderid = d['reference'][0] + # decode order id from returned reference + if ORDERID_TRANSACTION_SEPARATOR in orderid: + orderid, transaction_id = orderid.split(ORDERID_TRANSACTION_SEPARATOR, 1) return PaymentResponse( - order_id=d['reference'][0], + order_id=orderid, signed=signed, bank_data=d, result=result, diff --git a/eopayment/sips.py b/eopayment/sips.py index 028a8f6..73c79d5 100644 --- a/eopayment/sips.py +++ b/eopayment/sips.py @@ -132,13 +132,14 @@ class Payment(PaymentCommon): params.update(self.options) return params - def request(self, amount, name=None, address=None, email=None, phone=None, info1=None, - info2=None, info3=None, next_url=None, **kwargs): + def request(self, amount, name=None, address=None, email=None, phone=None, orderid=None, + info1=None, info2=None, info3=None, next_url=None, **kwargs): params = self.get_request_params() transaction_id = self.transaction_id(6, string.digits, 'sips', params[MERCHANT_ID]) params[TRANSACTION_ID] = transaction_id - params[ORDER_ID] = str(uuid.uuid4()).replace('-', '') + params[ORDER_ID] = orderid or str(uuid.uuid4()) + params[ORDER_ID] = params[ORDER_ID].replace('-', '') params['amount'] = str(int(Decimal(amount) * 100)) if email: params['customer_email'] = email diff --git a/eopayment/sips2.py b/eopayment/sips2.py index 156b4d7..2522028 100644 --- a/eopayment/sips2.py +++ b/eopayment/sips2.py @@ -135,12 +135,12 @@ class Payment(PaymentCommon): 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): + def request(self, amount, name=None, address=None, email=None, phone=None, + orderid=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['orderId'] = orderid or unicode(uuid.uuid4()).replace('-', '') data['amount'] = unicode(int(Decimal(amount) * 100)) if email: data['billingContact.email'] = email diff --git a/eopayment/spplus.py b/eopayment/spplus.py index 57e1e5c..1e9b11c 100644 --- a/eopayment/spplus.py +++ b/eopayment/spplus.py @@ -122,8 +122,9 @@ class Payment(PaymentCommon): } devise = '978' - def request(self, amount, name=None, address=None, email=None, phone=None, info1=None, - info2=None, info3=None, next_url=None, logger=LOGGER, **kwargs): + def request(self, amount, name=None, address=None, email=None, phone=None, + orderid=None, info1=None, info2=None, info3=None, next_url=None, + logger=LOGGER, **kwargs): logger.debug('requesting spplus payment with montant %s email=%s and \ next_url=%s' % (amount, email, next_url)) reference = self.transaction_id(20, ALPHANUM, 'spplus', self.siret) @@ -134,7 +135,7 @@ next_url=%s' % (amount, email, next_url)) 'langue': self.langue, 'taxe': self.taxe, 'montant': str(Decimal(amount)), - REFERENCE: reference, + REFERENCE: orderid or reference, 'validite': validite, 'version': '1', 'modalite': self.modalite, diff --git a/eopayment/systempayv2.py b/eopayment/systempayv2.py index f975b16..90bb822 100644 --- a/eopayment/systempayv2.py +++ b/eopayment/systempayv2.py @@ -252,7 +252,8 @@ class Payment(PaymentCommon): self.logger = logger or logging.getLogger(__name__) def request(self, amount, name=None, address=None, email=None, phone=None, - info1=None, info2=None, info3=None, next_url=None, **kwargs): + orderid=None, info1=None, info2=None, info3=None, + next_url=None, **kwargs): ''' Create the URL string to send a request to SystemPay ''' @@ -281,6 +282,16 @@ class Payment(PaymentCommon): kwargs['vads_order_info2'] = unicode(info2) if info3 is not None: kwargs['vads_order_info3'] = unicode(info3) + if orderid is not None: + # check orderid format first + name = 'vads_order_id' + orderid = unicode(orderid) + ptype = 'an-' + p = Parameter(name, ptype, 13, max_length=32) + if not p.check_value(orderid): + raise ValueError('%s value %s is not of the type %s' % (name, + orderid, ptype)) + kwargs[name] = orderid transaction_id = self.transaction_id(6, string.digits, 'systempay', self.options[VADS_SITE_ID]) diff --git a/eopayment/tipi.py b/eopayment/tipi.py index 7bb5aaf..763cfa8 100644 --- a/eopayment/tipi.py +++ b/eopayment/tipi.py @@ -48,8 +48,8 @@ class Payment(PaymentCommon): self.numcli = options.pop('numcli', '') self.logger = logger - def request(self, amount, next_url=None, exer=None, refdet=None, - objet=None, email=None, saisie=None, **kwargs): + def request(self, amount, next_url=None, exer=None, orderid=None, + refdet=None, objet=None, email=None, saisie=None, **kwargs): try: montant = Decimal(amount) if Decimal('0') > montant > Decimal('9999.99'): @@ -73,6 +73,7 @@ class Payment(PaymentCommon): except ValueError: raise ValueError('EXER format invalide') try: + refdet = orderid or refdet refdet = str(refdet) if 6 > len(refdet) > 30: raise ValueError('len(REFDET) < 6 or > 30') diff --git a/tests/test_ogone.py b/tests/test_ogone.py new file mode 100644 index 0000000..ee210dd --- /dev/null +++ b/tests/test_ogone.py @@ -0,0 +1,54 @@ +from unittest import TestCase +import urllib + +import eopayment +import eopayment.ogone as ogone + +PSPID = '2352566' + +BACKEND_PARAMS = { + 'environment': ogone.ENVIRONMENT_TEST, + 'pspid': PSPID, + 'sha_in': 'secret', + 'sha_out': 'secret' +} + +class OgoneTests(TestCase): + + def test_request(self): + ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS) + amount = '42.42' + order_id = 'myorder' + reference, kind, what = ogone_backend.request(amount=amount, + orderid=order_id, email='foo@example.com') + self.assertEqual(len(reference), 30) + assert reference.startswith(order_id) + from xml.etree import ElementTree as ET + root = ET.fromstring(str(what)) + self.assertEqual(root.tag, 'form') + self.assertEqual(root.attrib['method'], 'POST') + self.assertEqual(root.attrib['action'], ogone.ENVIRONMENT_TEST_URL) + values = { + 'CURRENCY': 'EUR', + 'ORDERID': reference, + 'PSPID': PSPID, + 'EMAIL': 'foo@example.com', + 'AMOUNT': amount.replace('.', ''), + 'LANGUAGE': 'fr_FR', + } + values.update({'SHASIGN': ogone_backend.backend.sha_sign_in(values)}) + for node in root: + self.assertIn(node.attrib['type'], ('hidden', 'submit')) + self.assertEqual(set(node.attrib.keys()), set(['type', 'name', 'value'])) + name = node.attrib['name'] + if node.attrib['type'] == 'hidden': + self.assertIn(name, values) + self.assertEqual(node.attrib['value'], values[name]) + + def test_response(self): + ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS) + order_id = 'myorder' + data = {'orderid': order_id + eopayment.common.ORDERID_TRANSACTION_SEPARATOR + 'RtEpMXZn4dX8k1rYbwLlby', + 'payid': '32100123', 'status': 9, 'ncerror': 0} + response = ogone_backend.response(urllib.urlencode(data)) + self.assertEqual(response.order_id, order_id) diff --git a/tests/test_paybox.py b/tests/test_paybox.py index 41b8da2..7e1e99f 100644 --- a/tests/test_paybox.py +++ b/tests/test_paybox.py @@ -1,18 +1,28 @@ from unittest import TestCase from decimal import Decimal import base64 +import urllib import eopayment.paybox as paybox import eopayment +BACKEND_PARAMS = { + 'platform': 'test', + 'site': '12345678', + 'rang': '001', + 'identifiant': '12345678', + 'shared_secret': '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF', + 'callback': 'http://example.com/callback', +} + class PayboxTests(TestCase): def test_sign(self): key = '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF'.decode('hex') d = dict(paybox.sign([ - ['PBX_SITE', '1999888'], + ['PBX_SITE', '12345678'], ['PBX_RANG', '32'], - ['PBX_IDENTIFIANT', '110647233'], - ['PBX_TOTAL', '999'], + ['PBX_IDENTIFIANT', '12345678'], + ['PBX_TOTAL', '999'], ['PBX_DEVISE', '978'], ['PBX_CMD', 'TEST Paybox'], ['PBX_PORTEUR', 'test@paybox.com'], @@ -21,25 +31,20 @@ class PayboxTests(TestCase): ['PBX_TIME', '2015-06-08T16:21:16+02:00'], ], key)) - result = '7ABB5F7A31DF4C8976A44374D3BA2F9831E7927CFD62F774ED378F4E27471708F4EFE6D0BEFA44EBABCBD978B661C74E22EEB16DEF73A510E86D0A5C0E7B6D88' + result = '475FE1C45A0D09D26D2CAC9A1AC39C024773D790F50B6DD15B260F55FCD527FD3AD4AA3998F4162EFE9BDC494B9850A673355A32ACC4F85B67F8566037836F8E' self.assertIn('PBX_HMAC', d) self.assertEqual(d['PBX_HMAC'], result) def test_request(self): - key = '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF' - backend = eopayment.Payment('paybox', { - 'platform': 'test', - 'site': '12345678', - 'rang': '001', - 'identifiant': '12345678', - 'shared_secret': key, - 'callback': 'http://example.com/callback', - }) + backend = eopayment.Payment('paybox', BACKEND_PARAMS) time = '2015-07-15T18:26:32+02:00' email = 'bdauvergne@entrouvert.com' + order_id = '20160216' + transaction = '1234' + amount = '19.99' transaction_id, kind, what = backend.request( - Decimal('19.99'), email=email, - transaction_id='1234', time=time) + Decimal(amount), email=email, orderid=order_id, + transaction_id=transaction, time=time) self.assertEqual(kind, eopayment.FORM) self.assertEqual(transaction_id, '1234') from xml.etree import ElementTree as ET @@ -54,6 +59,7 @@ class PayboxTests(TestCase): if node.attrib['type'] == 'hidden': self.assertEqual(set(node.attrib.keys()), set(['type', 'name', 'value'])) name = node.attrib['name'] + reference = order_id + eopayment.common.ORDERID_TRANSACTION_SEPARATOR + transaction values = { 'PBX_RANG': '01', 'PBX_SITE': '12345678', @@ -61,17 +67,27 @@ class PayboxTests(TestCase): 'PBX_RETOUR': 'montant:M;reference:R;code_autorisation:A;erreur:E;signature:K', 'PBX_TIME': time, 'PBX_PORTEUR': email, - 'PBX_CMD': '1234', - 'PBX_TOTAL': '1999', + 'PBX_CMD': reference, + 'PBX_TOTAL': amount.replace('.', ''), 'PBX_DEVISE': '978', 'PBX_HASH': 'SHA512', - 'PBX_HMAC': 'A0AA37FC3DD46F3233C0AD3BF95242CD71003D98F33DF85124E4423D53759A82A132EC2CC42B7234B22A75F00CF5DA124DF3A34331F3F6B9D7308B2EF09DCA3C', + 'PBX_HMAC': '173483CFF84A7ECF21039F99E9A95C5FB53D98A1562184F5B2C4543E4F87BFA227CC2CA10DE989D6C8B4DC03BC2ED44B7D7BDF5B4FABA8274D5D37C2F6445F36', 'PBX_ARCHIVAGE': '1234', 'PBX_REPONDRE_A': 'http://example.com/callback', } self.assertIn(name, values) self.assertEqual(node.attrib['value'], values[name]) + def test_response(self): + backend = eopayment.Payment('paybox', BACKEND_PARAMS) + order_id = '20160216' + transaction = '1234' + reference = order_id + eopayment.common.ORDERID_TRANSACTION_SEPARATOR + transaction + data = {'montant': '4242', 'reference': reference, + 'code_autorisation': 'A', 'erreur': '00000'} + response = backend.response(urllib.urlencode(data)) + self.assertEqual(response.order_id, order_id) + def test_rsa_signature_validation(self): pkey = '''-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUgYufHuheMztK1LhQSG6xsOzb