Compare commits

...

24 Commits

Author SHA1 Message Date
Frédéric Péters 3814073d30 add basic unit test for dummy 2018-07-25 21:10:16 +02:00
Frédéric Péters fd56510014 add basic unit test for tipi 2018-07-25 21:10:16 +02:00
Frédéric Péters bbf699c827 dummy: always parse query string as text 2018-07-25 21:10:16 +02:00
Frédéric Péters b88a759e10 dummy: use urlencode from six 2018-07-25 18:23:26 +02:00
Frédéric Péters d313a9cac4 dummy: iterate over a copy of dictionary keys as they are changed in the loop 2018-07-25 18:23:26 +02:00
François Poulain 36f2e38a84 use relative import 2018-07-25 10:53:32 +02:00
François Poulain 714bf8023b python3 exceptions 2018-07-25 10:53:29 +02:00
Frédéric Péters 7b2f106348 systempayv2: update for python3 2018-07-25 10:22:22 +02:00
Frédéric Péters 9e1ead2e7c spplus: adapt for python3 2018-07-25 10:22:22 +02:00
Frédéric Péters f99d6eeb91 sips2: adapt for python3 2018-07-25 10:22:22 +02:00
Frédéric Péters f3611bcc38 paybox: adapt for python3 2018-07-25 10:22:22 +02:00
Frédéric Péters fd6566a4ca ogone: use force_text 2018-07-25 10:22:22 +02:00
Frédéric Péters c028af586b common: add escape() method for python3 2018-07-25 10:17:59 +02:00
Frédéric Péters d009b35ab2 python3: force_text/force_byte 2018-07-25 10:14:25 +02:00
Frédéric Péters fe718e1159 python3: use importlib.import_module to import backends 2018-07-25 10:14:25 +02:00
Frédéric Péters 79735f6418 python3: use items instead of iteritems 2018-07-25 10:14:25 +02:00
Frédéric Péters f21d662912 python3: use string.ascii_letters 2018-07-25 10:14:25 +02:00
Frédéric Péters 8e74d949b5 python3: use six to get urlparse/urllib modules 2018-07-25 10:14:25 +02:00
Frédéric Péters 2469de19e2 tox: also run tests with python 3 2018-07-25 10:13:27 +02:00
Frédéric Péters b4cc0e71d4 python3: don't use leading 0 in integers 2018-07-25 10:13:27 +02:00
Frédéric Péters fbf3ebf34d python3: use print() for compatibility 2018-07-25 10:13:27 +02:00
Frédéric Péters 019188f70e python3: use relative import 2018-07-25 10:13:27 +02:00
Frédéric Péters 2ee8b2e5fd adapt setup.py for python3 compatibility 2018-07-25 10:13:27 +02:00
Frédéric Péters c2f059e7aa setup: add six as a dependency for python 2/3 compatibility 2018-07-25 10:13:27 +02:00
17 changed files with 187 additions and 151 deletions

View File

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import importlib
import logging import logging
from common import (URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED, from .common import (URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED,
CANCELED, CANCELLED, ERROR, WAITING, ResponseError, force_text) CANCELED, CANCELLED, ERROR, WAITING, ResponseError, force_text)
__all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', __all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS',
'SYSTEMPAY', 'SPPLUS', 'TIPI', 'DUMMY', 'get_backend', 'RECEIVED', 'ACCEPTED', 'SYSTEMPAY', 'SPPLUS', 'TIPI', 'DUMMY', 'get_backend', 'RECEIVED', 'ACCEPTED',
@ -23,7 +24,7 @@ logger = logging.getLogger(__name__)
def get_backend(kind): def get_backend(kind):
'''Resolve a backend name into a module object''' '''Resolve a backend name into a module object'''
module = __import__(kind, globals(), locals(), []) module = importlib.import_module('.' + kind, package='eopayment')
return module.Payment return module.Payment
__BACKENDS = [ DUMMY, SIPS, SIPS2, SYSTEMPAY, SPPLUS, OGONE, PAYBOX, PAYZEN, TIPI ] __BACKENDS = [ DUMMY, SIPS, SIPS2, SYSTEMPAY, SPPLUS, OGONE, PAYBOX, PAYZEN, TIPI ]

View File

@ -2,9 +2,15 @@ import os.path
import os import os
import random import random
import logging import logging
import cgi
from datetime import date from datetime import date
import six
if six.PY3:
import html
else:
import cgi
__all__ = ['PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED', __all__ = ['PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED',
'PAID', 'ERROR', 'WAITING'] 'PAID', 'ERROR', 'WAITING']
@ -29,15 +35,27 @@ ORDERID_TRANSACTION_SEPARATOR = '!'
def force_text(s, encoding='utf-8'): def force_text(s, encoding='utf-8'):
if isinstance(s, unicode): if issubclass(type(s), six.text_type):
return s return s
try: try:
return unicode(s, encoding) if not issubclass(type(s), six.string_types):
if six.PY3:
if isinstance(s, bytes):
s = six.text_type(s, encoding)
else:
s = six.text_type(s)
elif hasattr(s, '__unicode__'):
s = six.text_type(s)
else:
s = six.text_type(bytes(s), encoding)
else:
s = s.decode(encoding)
except UnicodeDecodeError: except UnicodeDecodeError:
return unicode(s) return six.text_type(s, encoding, 'ignore')
return s
def force_byte(s, encoding='utf-8'): def force_byte(s, encoding='utf-8'):
if isinstance(s, str): if isinstance(s, bytes):
return s return s
try: try:
return s.encode(encoding) return s.encode(encoding)
@ -148,7 +166,10 @@ class Form(object):
return s return s
def escape(self, s): def escape(self, s):
return cgi.escape(force_text(s, self.encoding).encode(self.encoding)) if six.PY3:
return html.escape(force_text(s, self.encoding))
else:
return cgi.escape(force_text(s, self.encoding)).encode(self.encoding)
def __str__(self): def __str__(self):
s = '<form method="%s" action="%s">' % (self.method, self.url) s = '<form method="%s" action="%s">' % (self.method, self.url)

View File

@ -1,21 +1,18 @@
import urllib
import string import string
import logging import logging
import warnings import warnings
def N_(message): return message def N_(message): return message
try: from six.moves.urllib.parse import parse_qs, urlencode
from cgi import parse_qs
except ImportError:
from urlparse import parse_qs
from common import PaymentCommon, URL, PaymentResponse, PAID, ERROR, WAITING, ResponseError from .common import (PaymentCommon, URL, PaymentResponse, PAID, ERROR, WAITING,
ResponseError, force_text)
__all__ = [ 'Payment' ] __all__ = [ 'Payment' ]
SERVICE_URL = 'http://dummy-payment.demo.entrouvert.com/' SERVICE_URL = 'http://dummy-payment.demo.entrouvert.com/'
ALPHANUM = string.letters + string.digits ALPHANUM = string.ascii_letters + string.digits
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
class Payment(PaymentCommon): class Payment(PaymentCommon):
@ -114,14 +111,14 @@ class Payment(PaymentCommon):
} }
query.update(dict(name=name, address=address, email=email, phone=phone, query.update(dict(name=name, address=address, email=email, phone=phone,
orderid=orderid, info1=info1, info2=info2, info3=info3)) orderid=orderid, info1=info1, info2=info2, info3=info3))
for key in query.keys(): for key in list(query.keys()):
if query[key] is None: if query[key] is None:
del query[key] del query[key]
url = '%s?%s' % (SERVICE_URL, urllib.urlencode(query)) url = '%s?%s' % (SERVICE_URL, urlencode(query))
return transaction_id, URL, url return transaction_id, URL, url
def response(self, query_string, logger=LOGGER, **kwargs): def response(self, query_string, logger=LOGGER, **kwargs):
form = parse_qs(query_string) form = parse_qs(force_text(query_string))
if not 'transaction_id' in form: if not 'transaction_id' in form:
raise ResponseError() raise ResponseError()
transaction_id = form.get('transaction_id',[''])[0] transaction_id = form.get('transaction_id',[''])[0]
@ -152,21 +149,3 @@ class Payment(PaymentCommon):
def cancel(self, amount, bank_data, **kwargs): def cancel(self, amount, bank_data, **kwargs):
return {} return {}
if __name__ == '__main__':
options = {
'direct_notification_url': 'http://example.com/direct_notification_url',
'siret': '1234',
'origin': 'Mairie de Perpette-les-oies'
}
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 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[1] == '6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T'
assert r[3] == 'signature ok'

View File

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import hashlib import hashlib
import string import string
import urlparse import six
from six.moves.urllib import parse as 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, ORDERID_TRANSACTION_SEPARATOR, ERROR, Form, DENIED, ACCEPTED, ORDERID_TRANSACTION_SEPARATOR,
WAITING, ResponseError, force_byte, force_text) WAITING, ResponseError, force_byte, force_text)
def N_(message): return message def N_(message): return message
@ -499,7 +500,7 @@ class Payment(PaymentCommon):
# arrondi comptable francais # arrondi comptable francais
amount = amount.quantize(Decimal('1.'), rounding=ROUND_HALF_UP) amount = amount.quantize(Decimal('1.'), rounding=ROUND_HALF_UP)
params = { params = {
'AMOUNT': unicode(amount), 'AMOUNT': force_text(amount),
'ORDERID': reference, 'ORDERID': reference,
'PSPID': self.pspid, 'PSPID': self.pspid,
'LANGUAGE': language, 'LANGUAGE': language,
@ -517,7 +518,7 @@ class Payment(PaymentCommon):
params['EMAIL'] = email params['EMAIL'] = email
if description: if description:
params['COM'] = description params['COM'] = description
for key, value in kwargs.iteritems(): for key, value in kwargs.items():
params[key.upper()] = value params[key.upper()] = value
params['SHASIGN'] = self.sha_sign_in(params) params['SHASIGN'] = self.sha_sign_in(params)
# uniformize all values to UTF-8 string # uniformize all values to UTF-8 string
@ -533,7 +534,10 @@ class Payment(PaymentCommon):
return reference, FORM, form return reference, FORM, form
def response(self, query_string, **kwargs): def response(self, query_string, **kwargs):
params = urlparse.parse_qs(query_string, True) if six.PY3:
params = urlparse.parse_qs(query_string, True, encoding='iso-8859-1')
else:
params = urlparse.parse_qs(query_string, True)
params = dict((key.upper(), params[key][0]) for key in params) params = dict((key.upper(), params[key][0]) for key in params)
if not set(params) >= set(['ORDERID', 'PAYID', 'STATUS', 'NCERROR']): if not set(params) >= set(['ORDERID', 'PAYID', 'STATUS', 'NCERROR']):
raise ResponseError() raise ResponseError()

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 # -*- coding: utf-8
import codecs
from collections import OrderedDict from collections import OrderedDict
import datetime import datetime
import logging import logging
@ -9,15 +10,18 @@ from decimal import Decimal, ROUND_DOWN
from Crypto.Signature import PKCS1_v1_5 from Crypto.Signature import PKCS1_v1_5
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Hash import SHA from Crypto.Hash import SHA
import urlparse
import urllib import six
from six.moves.urllib import parse as urlparse
from six.moves.urllib import parse as urllib
import base64 import base64
from gettext import gettext as _ from gettext import gettext as _
import string import string
import warnings import warnings
from common import (PaymentCommon, PaymentResponse, FORM, PAID, ERROR, Form, from .common import (PaymentCommon, PaymentResponse, FORM, PAID, ERROR, Form,
ORDERID_TRANSACTION_SEPARATOR, ResponseError) ORDERID_TRANSACTION_SEPARATOR, ResponseError, force_text)
__all__ = ['sign', 'Payment'] __all__ = ['sign', 'Payment']
@ -109,9 +113,10 @@ def sign(data, key):
algo = ALGOS[v] algo = ALGOS[v]
break break
assert algo, 'Missing or invalid PBX_HASH' assert algo, 'Missing or invalid PBX_HASH'
tosign = ['%s=%s' % (k, unicode(v).encode('utf-8')) for k, v in data] tosign = ['%s=%s' % (k, force_text(v)) for k, v in data]
tosign = '&'.join(tosign) tosign = '&'.join(tosign)
logger.debug('signed string %r', tosign) logger.debug('signed string %r', tosign)
tosign = tosign.encode('utf-8')
signature = hmac.new(key, tosign, algo) signature = hmac.new(key, tosign, algo)
return tuple(data) + (('PBX_HMAC', signature.hexdigest().upper()),) return tuple(data) + (('PBX_HMAC', signature.hexdigest().upper()),)
@ -208,22 +213,22 @@ class Payment(PaymentCommon):
def request(self, amount, email, name=None, orderid=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'] = force_text(self.site)
d['PBX_RANG'] = unicode(self.rang).strip()[-2:] d['PBX_RANG'] = force_text(self.rang).strip()[-2:]
d['PBX_IDENTIFIANT'] = unicode(self.identifiant) d['PBX_IDENTIFIANT'] = force_text(self.identifiant)
d['PBX_TOTAL'] = (amount * Decimal(100)).to_integral_value(ROUND_DOWN) d['PBX_TOTAL'] = (amount * Decimal(100)).to_integral_value(ROUND_DOWN)
d['PBX_DEVISE'] = unicode(self.devise) d['PBX_DEVISE'] = force_text(self.devise)
transaction_id = kwargs.get('transaction_id') or \ transaction_id = kwargs.get('transaction_id') or \
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'] = force_text(transaction_id)
# prepend order id command reference # prepend order id command reference
if orderid: if orderid:
d['PBX_CMD'] = orderid + ORDERID_TRANSACTION_SEPARATOR + d['PBX_CMD'] d['PBX_CMD'] = orderid + ORDERID_TRANSACTION_SEPARATOR + d['PBX_CMD']
d['PBX_PORTEUR'] = unicode(email) d['PBX_PORTEUR'] = force_text(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'
d['PBX_TIME'] = kwargs.get('time') or (unicode(datetime.datetime.utcnow().isoformat('T')).split('.')[0]+'+00:00') d['PBX_TIME'] = kwargs.get('time') or (force_text(datetime.datetime.utcnow().isoformat('T')).split('.')[0]+'+00:00')
d['PBX_ARCHIVAGE'] = transaction_id d['PBX_ARCHIVAGE'] = transaction_id
if self.normal_return_url: if self.normal_return_url:
d['PBX_EFFECTUE'] = self.normal_return_url d['PBX_EFFECTUE'] = self.normal_return_url
@ -236,16 +241,21 @@ class Payment(PaymentCommon):
"use automatic_return_url", DeprecationWarning) "use automatic_return_url", DeprecationWarning)
automatic_return_url = self.callback automatic_return_url = self.callback
if automatic_return_url: if automatic_return_url:
d['PBX_REPONDRE_A'] = unicode(automatic_return_url) d['PBX_REPONDRE_A'] = force_text(automatic_return_url)
d = d.items() d = d.items()
d = sign(d, self.shared_secret.decode('hex'))
if six.PY3:
shared_secret = codecs.decode(bytes(self.shared_secret, 'ascii'), 'hex')
else:
shared_secret = codecs.decode(bytes(self.shared_secret), 'hex')
d = sign(d, shared_secret)
url = URLS[self.platform] url = URLS[self.platform]
fields = [] fields = []
for k, v in d: for k, v in d:
fields.append({ fields.append({
'type': u'hidden', 'type': u'hidden',
'name': unicode(k), 'name': force_text(k),
'value': unicode(v), 'value': force_text(v),
}) })
form = Form(url, 'POST', fields, submit_name=None, form = Form(url, 'POST', fields, submit_name=None,
submit_value=u'Envoyer', encoding='utf-8') submit_value=u'Envoyer', encoding='utf-8')

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import urlparse from six.moves.urllib import parse as urlparse
import string import string
import subprocess import subprocess
from decimal import Decimal from decimal import Decimal
@ -9,8 +9,8 @@ import os.path
import uuid import uuid
import warnings import warnings
from common import PaymentCommon, HTML, PaymentResponse, ResponseError from .common import PaymentCommon, HTML, PaymentResponse, ResponseError
from cb import CB_RESPONSE_CODES from .cb import CB_RESPONSE_CODES
''' '''
Payment backend module for the ATOS/SIPS system used by many Frenck banks. Payment backend module for the ATOS/SIPS system used by many Frenck banks.
@ -112,7 +112,7 @@ class Payment(PaymentCommon):
if PATHFILE in self.options: if PATHFILE in self.options:
params[PATHFILE] = self.options[PATHFILE] params[PATHFILE] = self.options[PATHFILE]
executable = os.path.join(self.binpath, executable) executable = os.path.join(self.binpath, executable)
args = [executable] + ["%s=%s" % p for p in params.iteritems()] args = [executable] + ["%s=%s" % p for p in params.items()]
self.logger.debug('executing %s' % args) self.logger.debug('executing %s' % args)
result,_ = subprocess.Popen(' '.join(args), result,_ = subprocess.Popen(' '.join(args),
stdout=subprocess.PIPE, shell=True).communicate() stdout=subprocess.PIPE, shell=True).communicate()

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import collections import collections
import json import json
import urlparse from six.moves.urllib import parse as urlparse
import string import string
from decimal import Decimal from decimal import Decimal
import uuid import uuid
@ -11,8 +11,8 @@ from gettext import gettext as _
import requests import requests
import warnings import warnings
from common import (PaymentCommon, FORM, Form, PaymentResponse, PAID, ERROR, from .common import (PaymentCommon, FORM, Form, PaymentResponse, PAID, ERROR,
CANCELED, ResponseError) CANCELED, ResponseError, force_text)
__all__ = ['Payment'] __all__ = ['Payment']
@ -139,12 +139,12 @@ class Payment(PaymentCommon):
} }
def encode_data(self, data): def encode_data(self, data):
return u'|'.join(u'%s=%s' % (unicode(key), unicode(value)) return u'|'.join(u'%s=%s' % (force_text(key), force_text(value))
for key, value in data.iteritems()) for key, value in data.items())
def seal_data(self, data): def seal_data(self, data):
s = self.encode_data(data) s = self.encode_data(data)
s += unicode(self.secret_key) s += force_text(self.secret_key)
s = s.encode('utf-8') s = s.encode('utf-8')
s = hashlib.sha256(s).hexdigest() s = hashlib.sha256(s).hexdigest()
return s return s
@ -172,13 +172,13 @@ class Payment(PaymentCommon):
info1=None, info2=None, info3=None, next_url=None, **kwargs): info1=None, info2=None, info3=None, next_url=None, **kwargs):
data = self.get_data() data = self.get_data()
transaction_id = self.transaction_id(10, string.digits, 'sips2', data['merchantId']) transaction_id = self.transaction_id(10, string.digits, 'sips2', data['merchantId'])
data['transactionReference'] = unicode(transaction_id) data['transactionReference'] = force_text(transaction_id)
data['orderId'] = orderid or unicode(uuid.uuid4()).replace('-', '') data['orderId'] = orderid or force_text(uuid.uuid4()).replace('-', '')
if info1: if info1:
data['statementReference'] = unicode(info1) data['statementReference'] = force_text(info1)
else: else:
data['statementReference'] = data['transactionReference'] data['statementReference'] = data['transactionReference']
data['amount'] = unicode(int(Decimal(amount) * 100)) data['amount'] = force_text(int(Decimal(amount) * 100))
if email: if email:
data['billingContact.email'] = email data['billingContact.email'] = email
if 'captureDay' in kwargs: if 'captureDay' in kwargs:
@ -215,7 +215,7 @@ class Payment(PaymentCommon):
def decode_data(self, data): def decode_data(self, data):
data = data.split('|') data = data.split('|')
data = [map(unicode, p.split('=', 1)) for p in data] data = [map(force_text, p.split('=', 1)) for p in data]
return collections.OrderedDict(data) return collections.OrderedDict(data)
def check_seal(self, data, seal): def check_seal(self, data, seal):
@ -254,9 +254,9 @@ class Payment(PaymentCommon):
for key in sorted(data.keys()): for key in sorted(data.keys()):
if key in ('keyVersion', 'sealAlgorithm', 'seal'): if key in ('keyVersion', 'sealAlgorithm', 'seal'):
continue continue
data_to_send.append(unicode(data[key])) data_to_send.append(force_text(data[key]))
data_to_send_str = u''.join(data_to_send).encode('utf-8') data_to_send_str = u''.join(data_to_send).encode('utf-8')
return hmac.new(unicode(self.secret_key).encode('utf-8'), data_to_send_str, hashlib.sha256).hexdigest() return hmac.new(force_text(self.secret_key).encode('utf-8'), data_to_send_str, hashlib.sha256).hexdigest()
def perform_cash_management_operation(self, endpoint, data): def perform_cash_management_operation(self, endpoint, data):
data['merchantId'] = self.merchant_id data['merchantId'] = self.merchant_id
@ -282,13 +282,13 @@ class Payment(PaymentCommon):
def cancel(self, amount, bank_data, **kwargs): def cancel(self, amount, bank_data, **kwargs):
data = {} data = {}
data['operationAmount'] = unicode(int(Decimal(amount) * 100)) data['operationAmount'] = force_text(int(Decimal(amount) * 100))
data['transactionReference'] = bank_data.get('transactionReference') data['transactionReference'] = bank_data.get('transactionReference')
return self.perform_cash_management_operation('cancel', data) return self.perform_cash_management_operation('cancel', data)
def validate(self, amount, bank_data, **kwargs): def validate(self, amount, bank_data, **kwargs):
data = {} data = {}
data['operationAmount'] = unicode(int(Decimal(amount) * 100)) data['operationAmount'] = force_text(int(Decimal(amount) * 100))
data['transactionReference'] = bank_data.get('transactionReference') data['transactionReference'] = bank_data.get('transactionReference')
return self.perform_cash_management_operation('validate', data) return self.perform_cash_management_operation('validate', data)

View File

@ -4,8 +4,8 @@ import binascii
from gettext import gettext as _ from gettext import gettext as _
import hmac import hmac
import hashlib import hashlib
import urlparse from six.moves.urllib import parse as urlparse
import urllib from six.moves.urllib import parse as urllib
import string import string
import datetime as dt import datetime as dt
import logging import logging
@ -13,15 +13,15 @@ import re
import warnings import warnings
import Crypto.Cipher.DES import Crypto.Cipher.DES
from common import (PaymentCommon, URL, PaymentResponse, RECEIVED, ACCEPTED, from .common import (PaymentCommon, URL, PaymentResponse, RECEIVED, ACCEPTED,
PAID, ERROR, ResponseError) PAID, ERROR, ResponseError, force_byte)
def N_(message): return message def N_(message): return message
__all__ = ['Payment'] __all__ = ['Payment']
KEY_DES_KEY = '\x45\x1f\xba\x4f\x4c\x3f\xd4\x97' KEY_DES_KEY = b'\x45\x1f\xba\x4f\x4c\x3f\xd4\x97'
IV = '\x30\x78\x30\x62\x2c\x30\x78\x30' IV = b'\x30\x78\x30\x62\x2c\x30\x78\x30'
REFERENCE = 'reference' REFERENCE = 'reference'
ETAT = 'etat' ETAT = 'etat'
SPCHECKOK = 'spcheckok' SPCHECKOK = 'spcheckok'
@ -56,7 +56,7 @@ TEST_STATE = ('99',)
def decrypt_ntkey(ntkey): def decrypt_ntkey(ntkey):
key = binascii.unhexlify(ntkey.replace(' ','')) key = binascii.unhexlify(ntkey.replace(b' ', b''))
return decrypt_key(key) return decrypt_key(key)
def decrypt_key(key): def decrypt_key(key):
@ -70,7 +70,7 @@ def extract_values(query_string):
k, v = kv.split('=', 1) k, v = kv.split('=', 1)
if k != 'hmac': if k != 'hmac':
result.append(v) result.append(v)
return ''.join(result) return force_byte(''.join(result))
def sign_ntkey_query(ntkey, query): def sign_ntkey_query(ntkey, query):
key = decrypt_ntkey(ntkey) key = decrypt_ntkey(ntkey)
@ -89,7 +89,7 @@ def sign_url_paiement(ntkey, query):
data_to_sign = ''.join(fields) data_to_sign = ''.join(fields)
return hmac.new(key[:20], data_to_sign, hashlib.sha1).hexdigest().upper() return hmac.new(key[:20], data_to_sign, hashlib.sha1).hexdigest().upper()
ALPHANUM = string.letters + string.digits ALPHANUM = string.ascii_letters + string.digits
SERVICE_URL = "https://www.spplus.net/paiement/init.do" SERVICE_URL = "https://www.spplus.net/paiement/init.do"
class Payment(PaymentCommon): class Payment(PaymentCommon):
@ -178,7 +178,7 @@ class Payment(PaymentCommon):
form = urlparse.parse_qs(query_string) form = urlparse.parse_qs(query_string)
if not set(form) >= set([REFERENCE, ETAT, REFSFP]): if not set(form) >= set([REFERENCE, ETAT, REFSFP]):
raise ResponseError() raise ResponseError()
for key, value in form.iteritems(): for key, value in form.items():
form[key] = value[0] form[key] = value[0]
logger.debug('received query_string %s' % query_string) logger.debug('received query_string %s' % query_string)
logger.debug('parsed as %s' % form) logger.debug('parsed as %s' % form)
@ -233,8 +233,8 @@ if __name__ == '__main__':
ntkey = '58 6d fc 9c 34 91 9b 86 3f fd 64 63 c9 13 4a 26 ba 29 74 1e c7 e9 80 79' ntkey = '58 6d fc 9c 34 91 9b 86 3f fd 64 63 c9 13 4a 26 ba 29 74 1e c7 e9 80 79'
if len(sys.argv) == 2: if len(sys.argv) == 2:
print sign_url_paiement(ntkey, sys.argv[1]) print(sign_url_paiement(ntkey, sys.argv[1]))
print sign_ntkey_query(ntkey, sys.argv[1]) print(sign_ntkey_query(ntkey, sys.argv[1]))
elif len(sys.argv) > 2: elif len(sys.argv) > 2:
print sign_url_paiement(sys.argv[1], sys.argv[2]) print(sign_url_paiement(sys.argv[1], sys.argv[2]))
print sign_ntkey_query(sys.argv[1], sys.argv[2]) print(sign_ntkey_query(sys.argv[1], sys.argv[2]))

View File

@ -4,13 +4,13 @@ import datetime as dt
import hashlib import hashlib
import logging import logging
import string import string
import urlparse from six.moves.urllib import parse as urlparse
import warnings import warnings
from gettext import gettext as _ from gettext import gettext as _
from common import (PaymentCommon, PaymentResponse, PAID, ERROR, FORM, Form, from .common import (PaymentCommon, PaymentResponse, PAID, ERROR, FORM, Form,
ResponseError, force_text, force_byte) ResponseError, force_text, force_byte)
from cb import CB_RESPONSE_CODES from .cb import CB_RESPONSE_CODES
__all__ = ['Payment'] __all__ = ['Payment']
@ -117,20 +117,20 @@ PARAMETERS = [
'ONEY_SANDBOX, PAYPAL, PAYPAL_SB, PAYSAFECARD, ' 'ONEY_SANDBOX, PAYPAL, PAYPAL_SB, PAYSAFECARD, '
'VISA')), 'VISA')),
# must be SINGLE or MULTI with parameters # must be SINGLE or MULTI with parameters
Parameter('vads_payment_config', '', 07, default='SINGLE', Parameter('vads_payment_config', '', 7, default='SINGLE',
choices=('SINGLE', 'MULTI'), needed=True), choices=('SINGLE', 'MULTI'), needed=True),
Parameter('vads_return_mode', None, 48, default='GET', Parameter('vads_return_mode', None, 48, default='GET',
choices=('', 'NONE', 'POST', 'GET')), choices=('', 'NONE', 'POST', 'GET')),
Parameter('signature', 'an', None, length=40), Parameter('signature', 'an', None, length=40),
Parameter('vads_site_id', 'n', 02, length=8, needed=True, Parameter('vads_site_id', 'n', 2, length=8, needed=True,
description=_(u'Identifiant de la boutique')), description=_(u'Identifiant de la boutique')),
Parameter('vads_theme_config', 'ans', 32, max_length=255), Parameter('vads_theme_config', 'ans', 32, max_length=255),
Parameter(VADS_TRANS_DATE, 'n', 04, length=14, needed=True, Parameter(VADS_TRANS_DATE, 'n', 4, length=14, needed=True,
default=isonow), default=isonow),
Parameter('vads_trans_id', 'n', 03, length=6, needed=True), Parameter('vads_trans_id', 'n', 3, length=6, needed=True),
Parameter('vads_validation_mode', 'n', 5, max_length=1, choices=('', 0, 1), Parameter('vads_validation_mode', 'n', 5, max_length=1, choices=('', 0, 1),
default=''), default=''),
Parameter('vads_version', 'an', 01, default='V2', needed=True, Parameter('vads_version', 'an', 1, default='V2', needed=True,
choices=('V2',)), choices=('V2',)),
Parameter('vads_url_success', 'ans', 24, max_length=127), Parameter('vads_url_success', 'ans', 24, max_length=127),
Parameter('vads_url_referral', 'ans', 26, max_length=127), Parameter('vads_url_referral', 'ans', 26, max_length=127),
@ -173,7 +173,7 @@ EXTRA_RESULT_MAP = {
def add_vads(kwargs): def add_vads(kwargs):
new_vargs = {} new_vargs = {}
for k, v in kwargs.iteritems(): for k, v in kwargs.items():
if k.startswith('vads_'): if k.startswith('vads_'):
new_vargs[k] = v new_vargs[k] = v
else: else:
@ -276,8 +276,8 @@ class Payment(PaymentCommon):
info2, info3, next_url, kwargs) info2, info3, next_url, kwargs)
# amount unit is cents # amount unit is cents
amount = '%.0f' % (100 * amount) amount = '%.0f' % (100 * amount)
kwargs.update(add_vads({'amount': unicode(amount)})) kwargs.update(add_vads({'amount': force_text(amount)}))
if amount < 0: if int(amount) < 0:
raise ValueError('amount must be an integer >= 0') raise ValueError('amount must be an integer >= 0')
normal_return_url = self.normal_return_url normal_return_url = self.normal_return_url
if next_url: if next_url:
@ -285,30 +285,30 @@ class Payment(PaymentCommon):
"set normal_return_url in options", DeprecationWarning) "set normal_return_url in options", DeprecationWarning)
normal_return_url = next_url normal_return_url = next_url
if normal_return_url: if normal_return_url:
kwargs[VADS_URL_RETURN] = unicode(normal_return_url) kwargs[VADS_URL_RETURN] = force_text(normal_return_url)
if name is not None: if name is not None:
kwargs['vads_cust_name'] = unicode(name) kwargs['vads_cust_name'] = force_text(name)
if first_name is not None: if first_name is not None:
kwargs[VADS_CUST_FIRST_NAME] = unicode(first_name) kwargs[VADS_CUST_FIRST_NAME] = force_text(first_name)
if last_name is not None: if last_name is not None:
kwargs[VADS_CUST_LAST_NAME] = unicode(last_name) kwargs[VADS_CUST_LAST_NAME] = force_text(last_name)
if address is not None: if address is not None:
kwargs['vads_cust_address'] = unicode(address) kwargs['vads_cust_address'] = force_text(address)
if email is not None: if email is not None:
kwargs['vads_cust_email'] = unicode(email) kwargs['vads_cust_email'] = force_text(email)
if phone is not None: if phone is not None:
kwargs['vads_cust_phone'] = unicode(phone) kwargs['vads_cust_phone'] = force_text(phone)
if info1 is not None: if info1 is not None:
kwargs['vads_order_info'] = unicode(info1) kwargs['vads_order_info'] = force_text(info1)
if info2 is not None: if info2 is not None:
kwargs['vads_order_info2'] = unicode(info2) kwargs['vads_order_info2'] = force_text(info2)
if info3 is not None: if info3 is not None:
kwargs['vads_order_info3'] = unicode(info3) kwargs['vads_order_info3'] = force_text(info3)
if orderid is not None: if orderid is not None:
# check orderid format first # check orderid format first
name = 'vads_order_id' name = 'vads_order_id'
orderid = unicode(orderid) orderid = force_text(orderid)
ptype = 'an-' ptype = 'an-'
p = Parameter(name, ptype, 13, max_length=32) p = Parameter(name, ptype, 13, max_length=32)
if not p.check_value(orderid): if not p.check_value(orderid):
@ -318,14 +318,14 @@ class Payment(PaymentCommon):
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])
kwargs[VADS_TRANS_ID] = unicode(transaction_id) kwargs[VADS_TRANS_ID] = force_text(transaction_id)
fields = kwargs fields = kwargs
for parameter in PARAMETERS: for parameter in PARAMETERS:
name = parameter.name name = parameter.name
# import default parameters from configuration # import default parameters from configuration
if name not in fields \ if name not in fields \
and name in self.options: and name in self.options:
fields[name] = unicode(self.options[name]) fields[name] = force_text(self.options[name])
# import default parameters from module # import default parameters from module
if name not in fields and parameter.default is not None: if name not in fields and parameter.default is not None:
if callable(parameter.default): if callable(parameter.default):
@ -333,7 +333,7 @@ class Payment(PaymentCommon):
else: else:
fields[name] = parameter.default fields[name] = parameter.default
check_vads(fields) check_vads(fields)
fields[SIGNATURE] = unicode(self.signature(fields)) fields[SIGNATURE] = force_text(self.signature(fields))
self.logger.debug('%s request contains fields: %s', __name__, fields) self.logger.debug('%s request contains fields: %s', __name__, fields)
transaction_id = '%s_%s' % (fields[VADS_TRANS_DATE], transaction_id) transaction_id = '%s_%s' % (fields[VADS_TRANS_DATE], transaction_id)
self.logger.debug('%s transaction id: %s', __name__, transaction_id) self.logger.debug('%s transaction id: %s', __name__, transaction_id)
@ -346,14 +346,14 @@ class Payment(PaymentCommon):
'name': force_text(field_name), 'name': force_text(field_name),
'value': force_text(field_value), 'value': force_text(field_value),
} }
for field_name, field_value in fields.iteritems()]) for field_name, field_value in fields.items()])
return transaction_id, FORM, form return transaction_id, FORM, form
def response(self, query_string, **kwargs): def response(self, query_string, **kwargs):
fields = urlparse.parse_qs(query_string, True) fields = urlparse.parse_qs(query_string, True)
if not set(fields) >= set([SIGNATURE, VADS_CTX_MODE, VADS_AUTH_RESULT]): if not set(fields) >= set([SIGNATURE, VADS_CTX_MODE, VADS_AUTH_RESULT]):
raise ResponseError() raise ResponseError()
for key, value in fields.iteritems(): for key, value in fields.items():
fields[key] = value[0] fields[key] = value[0]
copy = fields.copy() copy = fields.copy()
bank_status = [] bank_status = []
@ -417,8 +417,8 @@ class Payment(PaymentCommon):
self.logger.debug('ordered keys %s' % ordered_keys) self.logger.debug('ordered keys %s' % ordered_keys)
ordered_fields = [force_byte(fields[key]) for key in ordered_keys] ordered_fields = [force_byte(fields[key]) for key in ordered_keys]
secret = getattr(self, 'secret_%s' % fields['vads_ctx_mode'].lower()) secret = getattr(self, 'secret_%s' % fields['vads_ctx_mode'].lower())
signed_data = '+'.join(ordered_fields) signed_data = b'+'.join(ordered_fields)
signed_data = '%s+%s' % (signed_data, force_byte(secret)) signed_data = b'%s+%s' % (signed_data, force_byte(secret))
self.logger.debug(u'generating signature on «%s»', signed_data) self.logger.debug(u'generating signature on «%s»', signed_data)
sign = hashlib.sha1(signed_data).hexdigest() sign = hashlib.sha1(signed_data).hexdigest()
self.logger.debug(u'signature «%s»', sign) self.logger.debug(u'signature «%s»', sign)

View File

@ -1,15 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from decimal import Decimal, ROUND_DOWN from decimal import Decimal, ROUND_DOWN
from common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED, from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED,
CANCELLED, ERROR, ResponseError) CANCELLED, ERROR, ResponseError)
from urllib import urlencode from six.moves.urllib.parse import urlencode, parse_qs
from urlparse import parse_qs
from gettext import gettext as _ from gettext import gettext as _
import logging import logging
import warnings import warnings
from systempayv2 import isonow from .systempayv2 import isonow
__all__ = ['Payment'] __all__ = ['Payment']
@ -95,12 +95,12 @@ class Payment(PaymentCommon):
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')
except Exception, e: except Exception as e:
raise ValueError('REFDET format invalide, %r' % refdet, e) raise ValueError('REFDET format invalide, %r' % refdet, e)
if objet is not None: if objet is not None:
try: try:
objet = str(objet) objet = str(objet)
except Exception, e: except Exception as e:
raise ValueError('OBJET must be a string', e) raise ValueError('OBJET must be a string', e)
if not objet.replace(' ','').isalnum(): if not objet.replace(' ','').isalnum():
raise ValueError('OBJECT must only contains ' raise ValueError('OBJECT must only contains '
@ -113,7 +113,7 @@ class Payment(PaymentCommon):
raise ValueError('no @ in MEL') raise ValueError('no @ in MEL')
if not (6 <= len(mel) <= 80): if not (6 <= len(mel) <= 80):
raise ValueError('len(MEL) is invalid, must be between 6 and 80') raise ValueError('len(MEL) is invalid, must be between 6 and 80')
except Exception, e: except Exception as e:
raise ValueError('MEL is not a valid email, %r' % mel, e) raise ValueError('MEL is not a valid email, %r' % mel, e)
saisie = saisie or self.saisie saisie = saisie or self.saisie
@ -146,7 +146,7 @@ class Payment(PaymentCommon):
fields = parse_qs(query_string, True) fields = parse_qs(query_string, True)
if not set(fields) >= set(['refdet', 'resultrans']): if not set(fields) >= set(['refdet', 'resultrans']):
raise ResponseError() raise ResponseError()
for key, value in fields.iteritems(): for key, value in fields.items():
fields[key] = value[0] fields[key] = value[0]
refdet = fields.get('refdet') refdet = fields.get('refdet')
if refdet is None: if refdet is None:
@ -180,14 +180,3 @@ class Payment(PaymentCommon):
bank_data=fields, bank_data=fields,
transaction_id=transaction_id, transaction_id=transaction_id,
test=test) test=test)
if __name__ == '__main__':
p = Payment({'numcli': '12345'})
print p.request(amount=Decimal('123.12'),
exer=9999,
refdet=999900000000999999,
objet='tout a fait',
email='info@entrouvert.com',
urlcl='http://example.com/tipi/test',
saisie='T')
print p.response('objet=tout+a+fait&montant=12312&saisie=T&mel=info%40entrouvert.com&numcli=12345&exer=9999&refdet=999900000000999999&resultrans=P')

View File

@ -46,7 +46,7 @@ class TestCommand(distutils.core.Command):
class eo_sdist(sdist): class eo_sdist(sdist):
def run(self): def run(self):
print "creating VERSION file" print("creating VERSION file")
if os.path.exists('VERSION'): if os.path.exists('VERSION'):
os.remove('VERSION') os.remove('VERSION')
version = get_version() version = get_version()
@ -54,7 +54,7 @@ class eo_sdist(sdist):
version_file.write(version) version_file.write(version)
version_file.close() version_file.close()
sdist.run(self) sdist.run(self)
print "removing VERSION file" print("removing VERSION file")
if os.path.exists('VERSION'): if os.path.exists('VERSION'):
os.remove('VERSION') os.remove('VERSION')
@ -72,7 +72,7 @@ def get_version():
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
result = p.communicate()[0] result = p.communicate()[0]
if p.returncode == 0: if p.returncode == 0:
version = result.split()[0][1:] version = str(result.split()[0][1:])
version = version.replace('-', '.') version = version.replace('-', '.')
return version return version
return '0.0.0' return '0.0.0'
@ -83,7 +83,7 @@ setuptools.setup(
license='GPLv3 or later', license='GPLv3 or later',
description='Common API to use all French online payment credit card ' description='Common API to use all French online payment credit card '
'processing services', 'processing services',
long_description=file( long_description=open(
os.path.join( os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
'README.txt')).read(), 'README.txt')).read(),
@ -96,6 +96,7 @@ setuptools.setup(
install_requires=[ install_requires=[
'pycrypto >= 2.5', 'pycrypto >= 2.5',
'requests', 'requests',
'six',
], ],
cmdclass={ cmdclass={
'sdist': eo_sdist, 'sdist': eo_sdist,

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from unittest import TestCase from unittest import TestCase
import urllib from six.moves.urllib import parse as urllib
import eopayment import eopayment
import eopayment.ogone as ogone import eopayment.ogone as ogone

View File

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import codecs
from unittest import TestCase from unittest import TestCase
from decimal import Decimal from decimal import Decimal
import base64 import base64
import urllib from six.moves.urllib import parse as urllib
import eopayment.paybox as paybox import eopayment.paybox as paybox
import eopayment import eopayment
@ -19,7 +20,8 @@ BACKEND_PARAMS = {
class PayboxTests(TestCase): class PayboxTests(TestCase):
def test_sign(self): def test_sign(self):
key = '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF'.decode('hex') key = b'0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF'
key = codecs.decode(key, 'hex')
d = dict(paybox.sign([ d = dict(paybox.sign([
['PBX_SITE', u'12345678'], ['PBX_SITE', u'12345678'],
['PBX_RANG', u'32'], ['PBX_RANG', u'32'],
@ -97,7 +99,7 @@ UX4D2A/QcMvkEcRVXFx5tQqcE9/JnMqE41TF/ebn7jC/MBxxtPFkUN7+EZoeMN7x
OWzAMDm/xsCWRvvel4GGixgm3aQRUPyTrlm4Ksy32Ya0rNnEDMAvB3dxOn7cp8GR OWzAMDm/xsCWRvvel4GGixgm3aQRUPyTrlm4Ksy32Ya0rNnEDMAvB3dxOn7cp8GR
ZdzrudBlevZXpr6iYwIDAQAB ZdzrudBlevZXpr6iYwIDAQAB
-----END PUBLIC KEY-----''' -----END PUBLIC KEY-----'''
data = 'coin\n' data = b'coin\n'
sig64 = '''VCt3sgT0ecacmDEWWNVXJ+jGmIPBMApK42tBJV0FlDjpllOGPy8MsAmLW4/QjTtx sig64 = '''VCt3sgT0ecacmDEWWNVXJ+jGmIPBMApK42tBJV0FlDjpllOGPy8MsAmLW4/QjTtx
z0Dkz0NjxvU+5WzQZh9Uuxr/egRCwV4NMRWqu0zaVVioeBvl4/5CWm4f4/1L9+0m z0Dkz0NjxvU+5WzQZh9Uuxr/egRCwV4NMRWqu0zaVVioeBvl4/5CWm4f4/1L9+0m
FBFKOZhgBJnkC+l6+XhT4aYWKaQ4ocmOMV92yjeXTE4=''' FBFKOZhgBJnkC+l6+XhT4aYWKaQ4ocmOMV92yjeXTE4='''

View File

@ -2,8 +2,8 @@ from unittest import TestCase
import eopayment.spplus as spplus import eopayment.spplus as spplus
class SPPlustTest(TestCase): class SPPlustTest(TestCase):
ntkey = '58 6d fc 9c 34 91 9b 86 3f ' \ ntkey = b'58 6d fc 9c 34 91 9b 86 3f ' \
'fd 64 63 c9 13 4a 26 ba 29 74 1e c7 e9 80 79' b'fd 64 63 c9 13 4a 26 ba 29 74 1e c7 e9 80 79'
tests = [('x=coin', 'c04f8266d6ae3ce37551cce996c751be4a95d10a'), tests = [('x=coin', 'c04f8266d6ae3ce37551cce996c751be4a95d10a'),
('x=coin&y=toto', 'ef008e02f8dbf5e70e83da416b0b3a345db203de'), ('x=coin&y=toto', 'ef008e02f8dbf5e70e83da416b0b3a345db203de'),

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import urlparse from six.moves.urllib import parse as urlparse
from eopayment.systempayv2 import Payment, VADS_CUST_FIRST_NAME, \ from eopayment.systempayv2 import Payment, VADS_CUST_FIRST_NAME, \
VADS_CUST_LAST_NAME, PAID VADS_CUST_LAST_NAME, PAID

25
tests/test_tipi.py Normal file
View File

@ -0,0 +1,25 @@
from decimal import Decimal
from six.moves.urllib.parse import urlparse, parse_qs
import eopayment
def test_tipi():
p = eopayment.Payment('tipi', {'numcli': '12345'})
payment_id, kind, url = p.request(amount=Decimal('123.12'),
exer=9999,
refdet=999900000000999999,
objet='tout a fait',
email='info@entrouvert.com',
urlcl='http://example.com/tipi/test',
saisie='T')
parsed_qs = parse_qs(urlparse(url).query)
assert parsed_qs['objet'][0].startswith('tout a fait ')
assert parsed_qs['montant'] == ['12312']
assert parsed_qs['saisie'] == ['T']
assert parsed_qs['mel'] == ['info@entrouvert.com']
assert parsed_qs['numcli'] == ['12345']
assert parsed_qs['exer'] == ['9999']
assert parsed_qs['refdet'] == ['999900000000999999']
response = p.response('objet=tout+a+fait&montant=12312&saisie=T&mel=info%40entrouvert.com&numcli=12345&exer=9999&refdet=999900000000999999&resultrans=P')
assert response.signed # ...
assert response.result == eopayment.PAID

View File

@ -3,6 +3,10 @@
# test suite on all supported python versions. To use it, "pip install tox" # test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory. # and then run "tox" from this directory.
[tox]
envlist = py2,py3
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/eopayment/
[testenv] [testenv]
# django.contrib.auth is not tested it does not work with our templates # django.contrib.auth is not tested it does not work with our templates
commands = commands =