pass order id to all backends (#9941)

This commit is contained in:
Serghei Mihai 2016-02-08 18:40:45 +01:00
parent 33ba732c91
commit 8cd625e465
12 changed files with 153 additions and 42 deletions

View File

@ -29,6 +29,15 @@ reported for logging purpose.
The spplus and paybox module also depend upon the python Crypto library for DES 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. 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 Changelog
========= =========

View File

@ -23,6 +23,9 @@ CANCELLED = 5
CANCELED = 5 # typo for backward compatibility CANCELED = 5 # typo for backward compatibility
ERROR = 99 ERROR = 99
# separator between order and transaction ids
ORDERID_TRANSACTION_SEPARATOR = '!'
class PaymentResponse(object): class PaymentResponse(object):
'''Holds a generic view on the result of payment transaction response. '''Holds a generic view on the result of payment transaction response.

View File

@ -72,7 +72,7 @@ class Payment(PaymentCommon):
} }
def request(self, amount, name=None, address=None, email=None, phone=None, 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' 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', ' next_url %s info1 %s info2 %s info3 %s kwargs: %s',
__name__, amount, name, address, email, phone, info1, info2, info3, next_url, kwargs) __name__, amount, name, address, email, phone, info1, info2, info3, next_url, kwargs)
@ -89,7 +89,7 @@ class Payment(PaymentCommon):
'origin': self.origin 'origin': self.origin
} }
query.update(dict(name=name, address=address, email=email, phone=phone, 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(): for key in query.keys():
if query[key] is None: if query[key] is None:
del query[key] del query[key]
@ -128,13 +128,11 @@ if __name__ == '__main__':
p = Payment(options) 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' 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]) r = p.response(retour.split('?',1)[1])
assert not r[0] assert not r[0]
assert r[1] == '6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T' assert r[1] == '6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T'
assert r[3] is None 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' 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]) r = p.response(retour.split('?',1)[1])
assert r[0] assert r[0]
assert r[1] == '6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T' assert r[1] == '6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T'
assert r[3] == 'signature ok' assert r[3] == 'signature ok'

View File

@ -5,7 +5,7 @@ import urlparse
from decimal import Decimal, ROUND_HALF_UP from decimal import Decimal, ROUND_HALF_UP
from common import (PaymentCommon, PaymentResponse, FORM, CANCELLED, PAID, from common import (PaymentCommon, PaymentResponse, FORM, CANCELLED, PAID,
ERROR, Form, DENIED, ACCEPTED) ERROR, Form, DENIED, ACCEPTED, ORDERID_TRANSACTION_SEPARATOR)
def N_(message): return message def N_(message): return message
ENVIRONMENT_TEST = 'TEST' ENVIRONMENT_TEST = 'TEST'
@ -467,7 +467,14 @@ class Payment(PaymentCommon):
def request(self, amount, orderid=None, name=None, email=None, def request(self, amount, orderid=None, name=None, email=None,
language=None, description=None, **kwargs): 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 language = language or self.language
# convertir en centimes # convertir en centimes
amount = Decimal(amount) * 100 amount = Decimal(amount) * 100
@ -527,6 +534,9 @@ class Payment(PaymentCommon):
self.logger.error('response STATUS=%s NCERROR=%s NCERRORPLUS=%s', self.logger.error('response STATUS=%s NCERROR=%s NCERRORPLUS=%s',
status, error, params.get('NCERRORPLUS', '')) status, error, params.get('NCERRORPLUS', ''))
result = ERROR 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( return PaymentResponse(
result=result, result=result,
signed=signed, signed=signed,

View File

@ -15,7 +15,8 @@ import base64
from gettext import gettext as _ from gettext import gettext as _
import string 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'] __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 = OrderedDict()
d['PBX_SITE'] = unicode(self.site) d['PBX_SITE'] = unicode(self.site)
d['PBX_RANG'] = unicode(self.rang).strip()[-2:] 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.transaction_id(12, string.digits, 'paybox', self.site,
self.rang, self.identifiant) self.rang, self.identifiant)
d['PBX_CMD'] = unicode(transaction_id) 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_PORTEUR'] = unicode(email)
d['PBX_RETOUR'] = 'montant:M;reference:R;code_autorisation:A;erreur:E;signature:K' d['PBX_RETOUR'] = 'montant:M;reference:R;code_autorisation:A;erreur:E;signature:K'
d['PBX_HASH'] = 'SHA512' d['PBX_HASH'] = 'SHA512'
@ -252,9 +256,12 @@ class Payment(PaymentCommon):
bank_status = PAYBOX_ERROR_CODES.get(prefix + suffix) bank_status = PAYBOX_ERROR_CODES.get(prefix + suffix)
if bank_status is not None: if bank_status is not None:
break 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( return PaymentResponse(
order_id=d['reference'][0], order_id=orderid,
signed=signed, signed=signed,
bank_data=d, bank_data=d,
result=result, result=result,

View File

@ -132,13 +132,14 @@ class Payment(PaymentCommon):
params.update(self.options) params.update(self.options)
return params return params
def request(self, amount, name=None, address=None, email=None, phone=None, info1=None, def request(self, amount, name=None, address=None, email=None, phone=None, orderid=None,
info2=None, info3=None, next_url=None, **kwargs): info1=None, info2=None, info3=None, next_url=None, **kwargs):
params = self.get_request_params() params = self.get_request_params()
transaction_id = self.transaction_id(6, string.digits, 'sips', transaction_id = self.transaction_id(6, string.digits, 'sips',
params[MERCHANT_ID]) params[MERCHANT_ID])
params[TRANSACTION_ID] = transaction_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)) params['amount'] = str(int(Decimal(amount) * 100))
if email: if email:
params['customer_email'] = email params['customer_email'] = email

View File

@ -135,12 +135,12 @@ class Payment(PaymentCommon):
def get_url(self): def get_url(self):
return self.URL[self.platform] return self.URL[self.platform]
def request(self, amount, name=None, address=None, email=None, phone=None, info1=None, def request(self, amount, name=None, address=None, email=None, phone=None,
info2=None, info3=None, next_url=None, **kwargs): orderid=None, info1=None, info2=None, info3=None, next_url=None, **kwargs):
data = self.get_data() data = self.get_data()
transaction_id = self.transaction_id(6, string.digits, 'sips2', data['merchantId']) transaction_id = self.transaction_id(6, string.digits, 'sips2', data['merchantId'])
data['transactionReference'] = unicode(transaction_id) 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)) data['amount'] = unicode(int(Decimal(amount) * 100))
if email: if email:
data['billingContact.email'] = email data['billingContact.email'] = email

View File

@ -122,8 +122,9 @@ class Payment(PaymentCommon):
} }
devise = '978' devise = '978'
def request(self, amount, name=None, address=None, email=None, phone=None, info1=None, def request(self, amount, name=None, address=None, email=None, phone=None,
info2=None, info3=None, next_url=None, logger=LOGGER, **kwargs): 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 \ logger.debug('requesting spplus payment with montant %s email=%s and \
next_url=%s' % (amount, email, next_url)) next_url=%s' % (amount, email, next_url))
reference = self.transaction_id(20, ALPHANUM, 'spplus', self.siret) reference = self.transaction_id(20, ALPHANUM, 'spplus', self.siret)
@ -134,7 +135,7 @@ next_url=%s' % (amount, email, next_url))
'langue': self.langue, 'langue': self.langue,
'taxe': self.taxe, 'taxe': self.taxe,
'montant': str(Decimal(amount)), 'montant': str(Decimal(amount)),
REFERENCE: reference, REFERENCE: orderid or reference,
'validite': validite, 'validite': validite,
'version': '1', 'version': '1',
'modalite': self.modalite, 'modalite': self.modalite,

View File

@ -252,7 +252,8 @@ class Payment(PaymentCommon):
self.logger = logger or logging.getLogger(__name__) self.logger = logger or logging.getLogger(__name__)
def request(self, amount, name=None, address=None, email=None, phone=None, 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 Create the URL string to send a request to SystemPay
''' '''
@ -281,6 +282,16 @@ class Payment(PaymentCommon):
kwargs['vads_order_info2'] = unicode(info2) kwargs['vads_order_info2'] = unicode(info2)
if info3 is not None: if info3 is not None:
kwargs['vads_order_info3'] = unicode(info3) 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', transaction_id = self.transaction_id(6, string.digits, 'systempay',
self.options[VADS_SITE_ID]) self.options[VADS_SITE_ID])

View File

@ -48,8 +48,8 @@ class Payment(PaymentCommon):
self.numcli = options.pop('numcli', '') self.numcli = options.pop('numcli', '')
self.logger = logger self.logger = logger
def request(self, amount, next_url=None, exer=None, refdet=None, def request(self, amount, next_url=None, exer=None, orderid=None,
objet=None, email=None, saisie=None, **kwargs): refdet=None, objet=None, email=None, saisie=None, **kwargs):
try: try:
montant = Decimal(amount) montant = Decimal(amount)
if Decimal('0') > montant > Decimal('9999.99'): if Decimal('0') > montant > Decimal('9999.99'):
@ -73,6 +73,7 @@ class Payment(PaymentCommon):
except ValueError: except ValueError:
raise ValueError('EXER format invalide') raise ValueError('EXER format invalide')
try: try:
refdet = orderid or refdet
refdet = str(refdet) refdet = str(refdet)
if 6 > len(refdet) > 30: if 6 > len(refdet) > 30:
raise ValueError('len(REFDET) < 6 or > 30') raise ValueError('len(REFDET) < 6 or > 30')

54
tests/test_ogone.py Normal file
View File

@ -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)

View File

@ -1,18 +1,28 @@
from unittest import TestCase from unittest import TestCase
from decimal import Decimal from decimal import Decimal
import base64 import base64
import urllib
import eopayment.paybox as paybox import eopayment.paybox as paybox
import eopayment import eopayment
BACKEND_PARAMS = {
'platform': 'test',
'site': '12345678',
'rang': '001',
'identifiant': '12345678',
'shared_secret': '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF',
'callback': 'http://example.com/callback',
}
class PayboxTests(TestCase): class PayboxTests(TestCase):
def test_sign(self): def test_sign(self):
key = '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF'.decode('hex') key = '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF'.decode('hex')
d = dict(paybox.sign([ d = dict(paybox.sign([
['PBX_SITE', '1999888'], ['PBX_SITE', '12345678'],
['PBX_RANG', '32'], ['PBX_RANG', '32'],
['PBX_IDENTIFIANT', '110647233'], ['PBX_IDENTIFIANT', '12345678'],
['PBX_TOTAL', '999'], ['PBX_TOTAL', '999'],
['PBX_DEVISE', '978'], ['PBX_DEVISE', '978'],
['PBX_CMD', 'TEST Paybox'], ['PBX_CMD', 'TEST Paybox'],
['PBX_PORTEUR', 'test@paybox.com'], ['PBX_PORTEUR', 'test@paybox.com'],
@ -21,25 +31,20 @@ class PayboxTests(TestCase):
['PBX_TIME', '2015-06-08T16:21:16+02:00'], ['PBX_TIME', '2015-06-08T16:21:16+02:00'],
], ],
key)) key))
result = '7ABB5F7A31DF4C8976A44374D3BA2F9831E7927CFD62F774ED378F4E27471708F4EFE6D0BEFA44EBABCBD978B661C74E22EEB16DEF73A510E86D0A5C0E7B6D88' result = '475FE1C45A0D09D26D2CAC9A1AC39C024773D790F50B6DD15B260F55FCD527FD3AD4AA3998F4162EFE9BDC494B9850A673355A32ACC4F85B67F8566037836F8E'
self.assertIn('PBX_HMAC', d) self.assertIn('PBX_HMAC', d)
self.assertEqual(d['PBX_HMAC'], result) self.assertEqual(d['PBX_HMAC'], result)
def test_request(self): def test_request(self):
key = '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF' backend = eopayment.Payment('paybox', BACKEND_PARAMS)
backend = eopayment.Payment('paybox', {
'platform': 'test',
'site': '12345678',
'rang': '001',
'identifiant': '12345678',
'shared_secret': key,
'callback': 'http://example.com/callback',
})
time = '2015-07-15T18:26:32+02:00' time = '2015-07-15T18:26:32+02:00'
email = 'bdauvergne@entrouvert.com' email = 'bdauvergne@entrouvert.com'
order_id = '20160216'
transaction = '1234'
amount = '19.99'
transaction_id, kind, what = backend.request( transaction_id, kind, what = backend.request(
Decimal('19.99'), email=email, Decimal(amount), email=email, orderid=order_id,
transaction_id='1234', time=time) transaction_id=transaction, time=time)
self.assertEqual(kind, eopayment.FORM) self.assertEqual(kind, eopayment.FORM)
self.assertEqual(transaction_id, '1234') self.assertEqual(transaction_id, '1234')
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
@ -54,6 +59,7 @@ class PayboxTests(TestCase):
if node.attrib['type'] == 'hidden': if node.attrib['type'] == 'hidden':
self.assertEqual(set(node.attrib.keys()), set(['type', 'name', 'value'])) self.assertEqual(set(node.attrib.keys()), set(['type', 'name', 'value']))
name = node.attrib['name'] name = node.attrib['name']
reference = order_id + eopayment.common.ORDERID_TRANSACTION_SEPARATOR + transaction
values = { values = {
'PBX_RANG': '01', 'PBX_RANG': '01',
'PBX_SITE': '12345678', 'PBX_SITE': '12345678',
@ -61,17 +67,27 @@ class PayboxTests(TestCase):
'PBX_RETOUR': 'montant:M;reference:R;code_autorisation:A;erreur:E;signature:K', 'PBX_RETOUR': 'montant:M;reference:R;code_autorisation:A;erreur:E;signature:K',
'PBX_TIME': time, 'PBX_TIME': time,
'PBX_PORTEUR': email, 'PBX_PORTEUR': email,
'PBX_CMD': '1234', 'PBX_CMD': reference,
'PBX_TOTAL': '1999', 'PBX_TOTAL': amount.replace('.', ''),
'PBX_DEVISE': '978', 'PBX_DEVISE': '978',
'PBX_HASH': 'SHA512', 'PBX_HASH': 'SHA512',
'PBX_HMAC': 'A0AA37FC3DD46F3233C0AD3BF95242CD71003D98F33DF85124E4423D53759A82A132EC2CC42B7234B22A75F00CF5DA124DF3A34331F3F6B9D7308B2EF09DCA3C', 'PBX_HMAC': '173483CFF84A7ECF21039F99E9A95C5FB53D98A1562184F5B2C4543E4F87BFA227CC2CA10DE989D6C8B4DC03BC2ED44B7D7BDF5B4FABA8274D5D37C2F6445F36',
'PBX_ARCHIVAGE': '1234', 'PBX_ARCHIVAGE': '1234',
'PBX_REPONDRE_A': 'http://example.com/callback', 'PBX_REPONDRE_A': 'http://example.com/callback',
} }
self.assertIn(name, values) self.assertIn(name, values)
self.assertEqual(node.attrib['value'], values[name]) 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): def test_rsa_signature_validation(self):
pkey = '''-----BEGIN PUBLIC KEY----- pkey = '''-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUgYufHuheMztK1LhQSG6xsOzb MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUgYufHuheMztK1LhQSG6xsOzb