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
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
=========

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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