misc: style, PEP8

This commit is contained in:
Benjamin Dauvergne 2020-04-10 11:10:15 +02:00
parent d0bdc988ac
commit 768f65f72f
22 changed files with 817 additions and 343 deletions

View File

@ -1,18 +1,35 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import importlib
import logging
import pytz
from .common import ( # noqa: F401
URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED,
CANCELED, CANCELLED, ERROR, WAITING, force_text,
ResponseError,
)
from .common import (URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED,
CANCELED, CANCELLED, ERROR, WAITING, ResponseError, force_text,
PaymentException)
__all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS',
'SYSTEMPAY', 'SPPLUS', 'TIPI', 'DUMMY', 'get_backend', 'RECEIVED', 'ACCEPTED',
'PAID', 'DENIED', 'CANCELED', 'CANCELLED', 'ERROR', 'WAITING', 'get_backends', 'PAYFIP_WS']
__all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', 'SYSTEMPAY', 'SPPLUS',
'TIPI', 'DUMMY', 'get_backend', 'RECEIVED', 'ACCEPTED', 'PAID',
'DENIED', 'CANCELED', 'CANCELLED', 'ERROR', 'WAITING',
'get_backends', 'PAYFIP_WS']
SIPS = 'sips'
SIPS2 = 'sips2'
@ -27,12 +44,15 @@ PAYFIP_WS = 'payfip_ws'
logger = logging.getLogger(__name__)
def get_backend(kind):
'''Resolve a backend name into a module object'''
module = importlib.import_module('.' + kind, package='eopayment')
return module.Payment
__BACKENDS = [ DUMMY, SIPS, SIPS2, SYSTEMPAY, SPPLUS, OGONE, PAYBOX, PAYZEN, TIPI, PAYFIP_WS ]
__BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, SPPLUS, OGONE, PAYBOX, PAYZEN,
TIPI, PAYFIP_WS]
def get_backends():
'''Return a dictionnary mapping existing eopayment backends name to their
@ -44,6 +64,7 @@ def get_backends():
'''
return dict((backend, get_backend(backend)) for backend in __BACKENDS)
class Payment(object):
'''
Interface to credit card online payment servers of French banks. The

View File

@ -1,4 +1,19 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
'''Responses codes emitted by EMV Card or 'Carte Bleu' in France'''

View File

@ -1,3 +1,19 @@
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os.path
import os
import random
@ -54,6 +70,7 @@ def force_text(s, encoding='utf-8'):
return six.text_type(s, encoding, 'ignore')
return s
def force_byte(s, encoding='utf-8'):
if isinstance(s, bytes):
return s
@ -144,7 +161,7 @@ class PaymentCommon(object):
try:
fd = os.open(os.path.join(self.PATH, name),
os.O_CREAT | os.O_EXCL)
except:
except Exception:
raise
else:
os.close(fd)

View File

@ -1,20 +1,46 @@
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import string
import logging
import warnings
def N_(message): return message
from six.moves.urllib.parse import parse_qs, urlencode
from .common import (PaymentCommon, URL, PaymentResponse, PAID, ERROR, WAITING,
ResponseError, force_text)
from .common import (
PaymentCommon,
PaymentResponse,
ResponseError,
URL,
PAID, ERROR, WAITING,
force_text
)
__all__ = ['Payment']
def N_(message):
return message
__all__ = [ 'Payment' ]
SERVICE_URL = 'http://dummy-payment.demo.entrouvert.com/'
ALPHANUM = string.ascii_letters + string.digits
LOGGER = logging.getLogger(__name__)
class Payment(PaymentCommon):
'''
Dummy implementation of the payment interface.
@ -36,59 +62,68 @@ class Payment(PaymentCommon):
per request basis).
'''
description = {
'caption': 'Dummy payment backend',
'parameters': [
{
'name': 'normal_return_url',
'caption': N_('Normal return URL'),
'default': '',
'required': True,
},
{
'name': 'automatic_return_url',
'caption': N_('Automatic return URL'),
'required': False,
},
{ 'name': 'dummy_service_url',
'caption': 'URL of the dummy payment service',
'default': SERVICE_URL,
'type': str,
},
{ 'name': 'origin',
'caption': 'name of the requesting service, '
'to present in the user interface',
'type': str,
},
{ 'name': 'siret',
'caption': 'dummy siret parameter',
'type': str,
},
{ 'name': 'consider_all_response_signed',
'caption': 'All response will be considered as signed '
'(to test payment locally for example, as you '
'cannot received the signed callback)',
'type': bool,
'default': False,
},
{ 'name': 'direct_notification_url',
'caption': 'direct notification url (replaced by automatic_return_url)',
'type': str,
'deprecated': True,
},
{ 'name': 'next_url (replaced by normal_return_url)',
'caption': 'Return URL for the user',
'type': str,
'deprecated': True,
},
],
'caption': 'Dummy payment backend',
'parameters': [
{
'name': 'normal_return_url',
'caption': N_('Normal return URL'),
'default': '',
'required': True,
},
{
'name': 'automatic_return_url',
'caption': N_('Automatic return URL'),
'required': False,
},
{
'name': 'dummy_service_url',
'caption': 'URL of the dummy payment service',
'default': SERVICE_URL,
'type': str,
},
{
'name': 'origin',
'caption': 'name of the requesting service, '
'to present in the user interface',
'type': str,
},
{
'name': 'siret',
'caption': 'dummy siret parameter',
'type': str,
},
{
'name': 'consider_all_response_signed',
'caption': (
'All response will be considered as signed '
'(to test payment locally for example, as you '
'cannot received the signed callback)'
),
'type': bool,
'default': False,
},
{
'name': 'direct_notification_url',
'caption': 'direct notification url (replaced by automatic_return_url)',
'type': str,
'deprecated': True,
},
{
'name': 'next_url (replaced by normal_return_url)',
'caption': 'Return URL for the user',
'type': str,
'deprecated': True,
},
],
}
def request(self, amount, name=None, address=None, email=None, phone=None,
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)
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)
transaction_id = self.transaction_id(30, ALPHANUM, 'dummy', self.siret)
normal_return_url = self.normal_return_url
if next_url and not normal_return_url:
@ -101,16 +136,17 @@ class Payment(PaymentCommon):
"use automatic_return_url", DeprecationWarning)
automatic_return_url = self.direct_notification_url
query = {
'transaction_id': transaction_id,
'siret': self.siret,
'amount': amount,
'email': email,
'return_url': normal_return_url or '',
'direct_notification_url': automatic_return_url or '',
'origin': self.origin
'transaction_id': transaction_id,
'siret': self.siret,
'amount': amount,
'email': email,
'return_url': normal_return_url or '',
'direct_notification_url': automatic_return_url or '',
'origin': self.origin
}
query.update(dict(name=name, address=address, email=email, phone=phone,
orderid=orderid, info1=info1, info2=info2, info3=info3))
query.update(
dict(name=name, address=address, email=email, phone=phone,
orderid=orderid, info1=info1, info2=info2, info3=info3))
for key in list(query.keys()):
if query[key] is None:
del query[key]
@ -119,9 +155,9 @@ class Payment(PaymentCommon):
def response(self, query_string, logger=LOGGER, **kwargs):
form = parse_qs(force_text(query_string))
if not 'transaction_id' in form:
if 'transaction_id' not in form:
raise ResponseError('missing transaction_id')
transaction_id = form.get('transaction_id',[''])[0]
transaction_id = form.get('transaction_id', [''])[0]
form[self.BANK_ID] = transaction_id
signed = 'signed' in form
@ -134,14 +170,15 @@ class Payment(PaymentCommon):
if 'waiting' in form:
result = WAITING
response = PaymentResponse(result=result,
signed=signed,
bank_data=form,
return_content=content,
order_id=transaction_id,
transaction_id=transaction_id,
bank_status=form.get('reason'),
test=True)
response = PaymentResponse(
result=result,
signed=signed,
bank_data=form,
return_content=content,
order_id=transaction_id,
transaction_id=transaction_id,
bank_status=form.get('reason'),
test=True)
return response
def validate(self, amount, bank_data, **kwargs):

View File

@ -1,14 +1,35 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import hashlib
import string
import six
from six.moves.urllib import parse as urlparse
from decimal import Decimal, ROUND_HALF_UP
from .common import (PaymentCommon, PaymentResponse, FORM, CANCELLED, PAID,
ERROR, Form, DENIED, ACCEPTED, ORDERID_TRANSACTION_SEPARATOR,
WAITING, ResponseError, force_byte, force_text)
def N_(message): return message
from .common import (
PaymentCommon, PaymentResponse, FORM, CANCELLED, PAID,
ERROR, Form, DENIED, ACCEPTED, ORDERID_TRANSACTION_SEPARATOR,
WAITING, ResponseError, force_byte, force_text
)
def N_(message):
return message
ENVIRONMENT_TEST = 'TEST'
ENVIRONMENT_TEST_URL = 'https://secure.ogone.com/ncol/test/orderstandard.asp'
@ -409,6 +430,7 @@ TRXDATE
VC
""".split()
class Payment(PaymentCommon):
# See http://payment-services.ingenico.com/fr/fr/ogone/support/guides/integration%20guides/e-commerce
description = {
@ -425,33 +447,40 @@ class Payment(PaymentCommon):
'caption': N_('Automatic return URL (ignored, must be set in Ogone backoffice)'),
'required': False,
},
{'name': 'environment',
{
'name': 'environment',
'default': ENVIRONMENT_TEST,
'caption': N_(u'Environnement'),
'choices': ENVIRONMENT,
},
{'name': 'pspid',
{
'name': 'pspid',
'caption': N_(u"Nom d'affiliation dans le système"),
'required': True,
},
{'name': 'language',
{
'name': 'language',
'caption': N_(u'Langage'),
'default': 'fr_FR',
'choices': (('fr_FR', N_('français')),),
},
{'name': 'hash_algorithm',
{
'name': 'hash_algorithm',
'caption': N_(u'Algorithme de hachage'),
'default': 'sha1',
},
{'name': 'sha_in',
{
'name': 'sha_in',
'caption': N_(u'Clé SHA-IN'),
'required': True,
},
{'name': 'sha_out',
{
'name': 'sha_out',
'caption': N_(u'Clé SHA-OUT'),
'required': True,
},
{'name': 'currency',
{
'name': 'currency',
'caption': N_(u'Monnaie'),
'default': 'EUR',
'choices': ('EUR',),
@ -485,7 +514,7 @@ class Payment(PaymentCommon):
raise NotImplementedError('unknown environment %s' % self.environment)
def request(self, amount, orderid=None, name=None, email=None,
language=None, description=None, **kwargs):
language=None, description=None, **kwargs):
reference = self.transaction_id(20, string.digits + string.ascii_letters)
@ -493,18 +522,22 @@ class Payment(PaymentCommon):
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)
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
# arrondi comptable francais
amount = amount.quantize(Decimal('1.'), rounding=ROUND_HALF_UP)
params = {
'AMOUNT': force_text(amount),
'ORDERID': reference,
'PSPID': self.pspid,
'LANGUAGE': language,
'CURRENCY': self.currency,
'AMOUNT': force_text(amount),
'ORDERID': reference,
'PSPID': self.pspid,
'LANGUAGE': language,
'CURRENCY': self.currency,
}
if self.normal_return_url:
params['ACCEPTURL'] = self.normal_return_url
@ -526,11 +559,11 @@ class Payment(PaymentCommon):
params[key] = force_text(params[key])
url = self.get_request_url()
form = Form(
url=url,
method='POST',
fields=[{'type': 'hidden',
'name': key,
'value': params[key]} for key in params])
url=url,
method='POST',
fields=[{'type': 'hidden',
'name': key,
'value': params[key]} for key in params])
return reference, FORM, form
def response(self, query_string, **kwargs):
@ -570,14 +603,14 @@ class Payment(PaymentCommon):
result = WAITING
else:
self.logger.error('response STATUS=%s NCERROR=%s NCERRORPLUS=%s',
status, error, params.get('NCERRORPLUS', ''))
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,
bank_data=params,
order_id=reference,
transaction_id=transaction_id)
result=result,
signed=signed,
bank_data=params,
order_id=reference,
transaction_id=transaction_id)

View File

@ -1,4 +1,19 @@
# -*- coding: utf-8
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import codecs
from collections import OrderedDict
@ -289,7 +304,9 @@ class Payment(PaymentCommon):
'signature:K'
)
d['PBX_HASH'] = 'SHA512'
d['PBX_TIME'] = kwargs.get('time') or (force_text(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
if self.normal_return_url:
d['PBX_EFFECTUE'] = self.normal_return_url

View File

@ -1,5 +1,5 @@
# eopayment - online payment library
# Copyright (C) 2011-2019 Entr'ouvert
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published

View File

@ -1,9 +1,26 @@
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from copy import deepcopy
from . import systempayv2
__all__ = ['Payment']
class Payment(systempayv2.Payment):
service_url = 'https://secure.payzen.eu/vads-payment/'

View File

@ -1,4 +1,20 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from six.moves.urllib import parse as urlparse
import string
import subprocess
@ -34,23 +50,25 @@ __all__ = ['Payment']
BINPATH = 'binpath'
PATHFILE = 'pathfile'
AUTHORISATION_ID = 'authorisation_id'
REQUEST_VALID_PARAMS = ['merchant_id', 'merchant_country', 'amount',
'currency_code', 'pathfile', 'normal_return_url', 'cancel_return_url',
'automatic_response_url', 'language', 'payment_means', 'header_flag',
'capture_day', 'capture_mode', 'bgcolor', 'block_align', 'block_order',
'textcolor', 'receipt_complement', 'caddie', 'customer_id',
'customer_email', 'customer_ip_address', 'data', 'return_context',
'target', 'order_id']
REQUEST_VALID_PARAMS = [
'merchant_id', 'merchant_country', 'amount', 'currency_code', 'pathfile',
'normal_return_url', 'cancel_return_url', 'automatic_response_url',
'language', 'payment_means', 'header_flag', 'capture_day', 'capture_mode',
'bgcolor', 'block_align', 'block_order', 'textcolor', 'receipt_complement',
'caddie', 'customer_id', 'customer_email', 'customer_ip_address', 'data',
'return_context', 'target', 'order_id',
]
RESPONSE_PARAMS = ['code', 'error', 'merchant_id', 'merchant_country',
'amount', 'transaction_id', 'payment_means', 'transmission_date',
'payment_time', 'payment_date', 'response_code', 'payment_certificate',
AUTHORISATION_ID, 'currency_code', 'card_number', 'cvv_flag',
'cvv_response_code', 'bank_response_code', 'complementary_code',
'complementary_info', 'return_context', 'caddie', 'receipt_complement',
'merchant_language', 'language', 'customer_id', 'order_id',
'customer_email', 'customer_ip_address', 'capture_day', 'capture_mode',
'data', ]
RESPONSE_PARAMS = [
'code', 'error', 'merchant_id', 'merchant_country', 'amount',
'transaction_id', 'payment_means', 'transmission_date', 'payment_time',
'payment_date', 'response_code', 'payment_certificate', AUTHORISATION_ID,
'currency_code', 'card_number', 'cvv_flag', 'cvv_response_code',
'bank_response_code', 'complementary_code', 'complementary_info',
'return_context', 'caddie', 'receipt_complement', 'merchant_language',
'language', 'customer_id', 'order_id', 'customer_email',
'customer_ip_address', 'capture_day', 'capture_mode', 'data',
]
DATA = 'DATA'
PARAMS = 'params'
@ -69,37 +87,38 @@ LOGGER = logging.getLogger(__name__)
CB_BANK_RESPONSE_CODES = CB_RESPONSE_CODES
AMEX_BANK_RESPONSE_CODE = {
'00': 'Transaction approuvée ou traitée avec succès',
'02': 'Dépassement de plafond',
'04': 'Conserver la carte',
'05': 'Ne pas honorer',
'97': 'Échéance de la temporisation de surveillance globale',
'00': 'Transaction approuvée ou traitée avec succès',
'02': 'Dépassement de plafond',
'04': 'Conserver la carte',
'05': 'Ne pas honorer',
'97': 'Échéance de la temporisation de surveillance globale',
}
FINAREF_BANK_RESPONSE_CODE = {
'00': 'Transaction approuvée',
'03': 'Commerçant inconnu - Identifiant de commerçant incorrect',
'05': 'Compte / Porteur avec statut bloqué ou invalide',
'11': 'Compte / porteur inconnu',
'16': 'Provision insuffisante',
'20': 'Commerçant invalide - Code monnaie incorrect - ' + \
'Opération commerciale inconnue - Opération commerciale invalide',
'80': 'Transaction approuvée avec dépassement',
'81': 'Transaction approuvée avec augmentation capital',
'82': 'Transaction approuvée NPAI',
'83': 'Compte / porteur invalide',
'00': 'Transaction approuvée',
'03': 'Commerçant inconnu - Identifiant de commerçant incorrect',
'05': 'Compte / Porteur avec statut bloqué ou invalide',
'11': 'Compte / porteur inconnu',
'16': 'Provision insuffisante',
'20': (
'Commerçant invalide - Code monnaie incorrect - '
'Opération commerciale inconnue - Opération commerciale invalide'
),
'80': 'Transaction approuvée avec dépassement',
'81': 'Transaction approuvée avec augmentation capital',
'82': 'Transaction approuvée NPAI',
'83': 'Compte / porteur invalide',
}
class Payment(PaymentCommon):
description = {
'caption': 'SIPS',
'parameters': [{
'name': 'merchand_id',
},
{'name': 'merchant_country', },
{'name': 'currency_code', }
],
'caption': 'SIPS',
'parameters': [
{'name': 'merchand_id'},
{'name': 'merchant_country', },
{'name': 'currency_code', }
],
}
def __init__(self, options, logger=None):
@ -114,8 +133,11 @@ class Payment(PaymentCommon):
executable = os.path.join(self.binpath, executable)
args = [executable] + ["%s=%s" % p for p in params.items()]
self.logger.debug('executing %s' % args)
result,_ = subprocess.Popen(' '.join(args),
stdout=subprocess.PIPE, shell=True).communicate()
result, _ = subprocess.Popen(
' '.join(args),
stdout=subprocess.PIPE,
shell=True
).communicate()
try:
if result[0] == '!':
result = result[1:]
@ -133,11 +155,11 @@ class Payment(PaymentCommon):
params.update(self.options)
return params
def request(self, amount, name=None, address=None, email=None, phone=None, orderid=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])
transaction_id = self.transaction_id(6, string.digits, 'sips', params[MERCHANT_ID])
params[TRANSACTION_ID] = transaction_id
params[ORDER_ID] = orderid or str(uuid.uuid4())
params[ORDER_ID] = params[ORDER_ID].replace('-', '')
@ -159,7 +181,7 @@ class Payment(PaymentCommon):
def response(self, query_string, **kwargs):
form = urlparse.parse_qs(query_string)
if not DATA in form:
if DATA not in form:
raise ResponseError('missing %s' % DATA)
params = {'message': form[DATA][0]}
result = self.execute('response', params)
@ -170,10 +192,10 @@ class Payment(PaymentCommon):
response_result = d.get(RESPONSE_CODE) == '00'
response_code_msg = CB_BANK_RESPONSE_CODES.get(d.get(RESPONSE_CODE))
response = PaymentResponse(
result=response_result,
signed=response_result,
bank_data=d,
order_id=d.get(ORDER_ID),
transaction_id=d.get(AUTHORISATION_ID),
bank_status=response_code_msg)
result=response_result,
signed=response_result,
bank_data=d,
order_id=d.get(ORDER_ID),
transaction_id=d.get(AUTHORISATION_ID),
bank_status=response_code_msg)
return response

View File

@ -1,4 +1,19 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
from decimal import Decimal

View File

@ -1,7 +1,22 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from decimal import Decimal
import binascii
from gettext import gettext as _
import hmac
import hashlib
from six.moves.urllib import parse as urlparse
@ -13,10 +28,14 @@ import re
import warnings
import Crypto.Cipher.DES
from .common import (PaymentCommon, URL, PaymentResponse, RECEIVED, ACCEPTED,
PAID, ERROR, ResponseError, force_byte)
from .common import (
PaymentCommon, URL, PaymentResponse, RECEIVED, ACCEPTED,
PAID, ERROR, ResponseError, force_byte
)
def N_(message): return message
def N_(message):
return message
__all__ = ['Payment']
@ -39,7 +58,7 @@ SPPLUS_RESPONSE_CODES = {
'8': 'Chèque encaissé',
'10': 'Paiement terminé',
'11': 'Echéance du paiement annulée par le commerçant',
'12': 'Abandon de l\internaute',
'12': 'Abandon de linternaute',
'15': 'Remboursement enregistré',
'16': 'Remboursement annulé',
'17': 'Remboursement accepté',
@ -59,10 +78,12 @@ def decrypt_ntkey(ntkey):
key = binascii.unhexlify(ntkey.replace(b' ', b''))
return decrypt_key(key)
def decrypt_key(key):
CIPHER = Crypto.Cipher.DES.new(KEY_DES_KEY, Crypto.Cipher.DES.MODE_CBC, IV)
return CIPHER.decrypt(key)
def extract_values(query_string):
kvs = query_string.split('&')
result = []
@ -72,88 +93,104 @@ def extract_values(query_string):
result.append(v)
return force_byte(''.join(result))
def sign_ntkey_query(ntkey, query):
key = decrypt_ntkey(ntkey)
data_to_sign = extract_values(query)
return hmac.new(key[:20], data_to_sign, hashlib.sha1).hexdigest().upper()
PAIEMENT_FIELDS = [ 'siret', REFERENCE, 'langue', 'devise', 'montant',
'taxe', 'validite' ]
PAIEMENT_FIELDS = [
'siret', REFERENCE, 'langue', 'devise', 'montant', 'taxe', 'validite'
]
def sign_url_paiement(ntkey, query):
if '?' in query:
query = query[query.index('?')+1:]
query = query[query.index('?') + 1:]
key = decrypt_ntkey(ntkey)
data = urlparse.parse_qs(query, True)
fields = [data.get(field,[''])[0] for field in PAIEMENT_FIELDS]
fields = [data.get(field, [''])[0] for field in PAIEMENT_FIELDS]
data_to_sign = ''.join(fields)
return hmac.new(key[:20], data_to_sign, hashlib.sha1).hexdigest().upper()
ALPHANUM = string.ascii_letters + string.digits
SERVICE_URL = "https://www.spplus.net/paiement/init.do"
class Payment(PaymentCommon):
description = {
'caption': "SPPlus payment service of French bank Caisse d'epargne",
'parameters': [
{
'name': 'normal_return_url',
'caption': N_('Normal return URL'),
'default': '',
'required': True,
},
{
'name': 'automatic_return_url',
'caption': N_('Automatic return URL'),
'required': False,
},
{ 'name': 'cle',
'caption': 'Secret key, a 40 digits hexadecimal number',
'regexp': re.compile('^ *((?:[a-fA-F0-9] *){40}) *$')
},
{ 'name': 'siret',
'caption': 'Siret of the entreprise augmented with the '
'site number, example: 00000000000001-01',
'regexp': re.compile('^ *(\d{14}-\d{2}) *$')
},
{ 'name': 'langue',
'caption': 'Language of the customers',
'default': 'FR',
},
{ 'name': 'taxe',
'caption': 'Taxes',
'default': '0.00'
},
{ 'name': 'modalite',
'caption': '1x, 2x, 3x, xx, nx (if multiple separated by "/")',
'default': '1x',
},
{ 'name': 'moyen',
'caption': 'AUR, AMX, CBS, CGA, '
'CHK, DIN, PRE (if multiple separate by "/")',
'default': 'CBS',
},
]
'caption': "SPPlus payment service of French bank Caisse d'epargne",
'parameters': [
{
'name': 'normal_return_url',
'caption': N_('Normal return URL'),
'default': '',
'required': True,
},
{
'name': 'automatic_return_url',
'caption': N_('Automatic return URL'),
'required': False,
},
{
'name': 'cle',
'caption': 'Secret key, a 40 digits hexadecimal number',
'regexp': re.compile('^ *((?:[a-fA-F0-9] *){40}) *$')
},
{
'name': 'siret',
'caption': (
'Siret of the entreprise augmented with the '
'site number, example: 00000000000001-01'
),
'regexp': re.compile(r'^ *(\d{14}-\d{2}) *$')
},
{
'name': 'langue',
'caption': 'Language of the customers',
'default': 'FR',
},
{
'name': 'taxe',
'caption': 'Taxes',
'default': '0.00'
},
{
'name': 'modalite',
'caption': '1x, 2x, 3x, xx, nx (if multiple separated by "/")',
'default': '1x',
},
{
'name': 'moyen',
'caption': (
'AUR, AMX, CBS, CGA, '
'CHK, DIN, PRE (if multiple separate by "/")'
),
'default': 'CBS',
},
]
}
devise = '978'
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):
orderid=None, info1=None, info2=None, info3=None,
next_url=None, logger=LOGGER, **kwargs):
logger.debug('requesting spplus payment with montant %s email=%s' % (amount, email))
reference = self.transaction_id(20, ALPHANUM, 'spplus', self.siret)
validite = dt.date.today()+dt.timedelta(days=1)
validite = validite.strftime('%d/%m/%Y')
fields = { 'siret': self.siret,
'devise': self.devise,
'langue': self.langue,
'taxe': self.taxe,
'montant': str(Decimal(amount)),
REFERENCE: orderid or reference,
'validite': validite,
'version': '1',
'modalite': self.modalite,
'moyen': self.moyen }
fields = {
'siret': self.siret,
'devise': self.devise,
'langue': self.langue,
'taxe': self.taxe,
'montant': str(Decimal(amount)),
REFERENCE: orderid or reference,
'validite': validite,
'version': '1',
'modalite': self.modalite,
'moyen': self.moyen,
}
if email:
fields['email'] = email
normal_return_url = self.normal_return_url
@ -162,15 +199,14 @@ class Payment(PaymentCommon):
"set normal_return_url in options", DeprecationWarning)
normal_return_url = next_url
if normal_return_url:
if (not normal_return_url.startswith('http://') \
and not normal_return_url.startswith('https://')) \
or '?' in normal_return_url:
raise ValueError('normal_return_url must be an absolute URL without parameters')
if ((not normal_return_url.startswith('http://')
and not normal_return_url.startswith('https://'))
or '?' in normal_return_url):
raise ValueError('normal_return_url must be an absolute URL without parameters')
fields['urlretour'] = normal_return_url
logger.debug('sending fields %s' % fields)
query = urllib.urlencode(fields)
url = '%s?%s&hmac=%s' % (SERVICE_URL, query, sign_url_paiement(self.cle,
query))
url = '%s?%s&hmac=%s' % (SERVICE_URL, query, sign_url_paiement(self.cle, query))
logger.debug('full url %s' % url)
return reference, URL, url
@ -211,20 +247,20 @@ class Payment(PaymentCommon):
elif etat in VALID_STATE:
result = RECEIVED
elif etat in TEST_STATE:
result = RECEIVED # what else ?
result = RECEIVED # what else ?
test = True
else:
result = ERROR
response = PaymentResponse(
result=result,
signed=signed,
bank_data=form,
order_id=reference,
transaction_id=form[self.BANK_ID],
bank_status=' - '.join(bank_status),
return_content=SPCHECKOK,
test=test)
result=result,
signed=signed,
bank_data=form,
order_id=reference,
transaction_id=form[self.BANK_ID],
bank_status=' - '.join(bank_status),
return_content=SPCHECKOK,
test=test)
return response

View File

@ -1,4 +1,19 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytz
import datetime as dt
@ -286,7 +301,8 @@ class Payment(PaymentCommon):
def request(self, amount, name=None, first_name=None, last_name=None,
address=None, email=None, phone=None, orderid=None, info1=None,
info2=None, info3=None, next_url=None, manual_validation=None, **kwargs):
info2=None, info3=None, next_url=None, manual_validation=None,
**kwargs):
'''
Create the URL string to send a request to SystemPay
'''
@ -332,8 +348,8 @@ class Payment(PaymentCommon):
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))
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',

View File

@ -1,3 +1,19 @@
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import random
from decimal import Decimal, ROUND_DOWN

View File

@ -1,13 +1,29 @@
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import date, datetime, timedelta
import mock
import pytest
import eopayment
def do_mock_backend(monkeypatch):
class MockBackend(object):
request = mock.Mock()
description = {
@ -38,7 +54,6 @@ def do_mock_backend(monkeypatch):
return MockBackend
return backend
import eopayment
monkeypatch.setattr(eopayment, 'get_backend', get_backend)
return MockBackend, eopayment.Payment('kind', None)

View File

@ -1,24 +1,56 @@
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import eopayment
import pytest
def test_dummy():
options = {
'direct_notification_url': 'http://example.com/direct_notification_url',
'siret': '1234',
'origin': 'Mairie de Perpette-les-oies'
'direct_notification_url': 'http://example.com/direct_notification_url',
'siret': '1234',
'origin': 'Mairie de Perpette-les-oies'
}
p = eopayment.Payment('dummy', 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])
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.signed
assert r.transaction_id == '6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T'
assert r.return_content 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])
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.signed
assert r.transaction_id == '6Tfw2e1bPyYnz7CedZqvdHt7T9XX6T'
assert r.return_content == 'signature ok'
data = {'foo': 'bar'}
with pytest.raises(eopayment.ResponseError, match='missing transaction_id'):
p.response('foo=bar')

View File

@ -1,6 +1,23 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from unittest import TestCase
from xml.etree import ElementTree as ET
from six.moves.urllib import parse as urllib
import eopayment
@ -17,17 +34,18 @@ BACKEND_PARAMS = {
'automatic_return_url': u'http://example.com/autömatic_réturn_url'
}
class OgoneTests(TestCase):
class OgoneTests(TestCase):
def test_request(self):
ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS)
amount = '42.42'
order_id = u'my ordér'
reference, kind, what = ogone_backend.request(amount=amount,
orderid=order_id, email='foo@example.com')
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')
@ -67,27 +85,39 @@ class OgoneTests(TestCase):
def test_iso_8859_1_response(self):
ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS)
order_id = 'lRXK4Rl1N2yIR3R5z7Kc'
backend_response = 'orderID=lRXK4Rl1N2yIR3R5z7Kc&currency=EUR&amount=7%2E5&PM=CreditCard&ACCEPTANCE=test123&STATUS=9&CARDNO=XXXXXXXXXXXX9999&ED=0118&CN=Miha%EF+Serghe%EF&TRXDATE=10%2F24%2F16&PAYID=3011228911&NCERROR=0&BRAND=MasterCard&IP=80%2E12%2E92%2E47&SHASIGN=435D5E36E1F4B17739C1054FFD204218E65C15AB'
backend_response = (
'orderID=lRXK4Rl1N2yIR3R5z7Kc&currency=EUR&amount=7%2E5'
'&PM=CreditCard&ACCEPTANCE=test123&STATUS=9'
'&CARDNO=XXXXXXXXXXXX9999&ED=0118'
'&CN=Miha%EF+Serghe%EF&TRXDATE=10%2F24%2F16'
'&PAYID=3011228911&NCERROR=0&BRAND=MasterCard'
'&IP=80%2E12%2E92%2E47&SHASIGN=435D5E36E1F4B17739C1054FFD204218E65C15AB'
)
response = ogone_backend.response(backend_response)
assert response.signed
self.assertEqual(response.order_id, order_id)
def test_bad_response(self):
ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS)
order_id = 'myorder'
data = {'payid': '32100123', 'status': 9, 'ncerror': 0}
with self.assertRaisesRegexp(ResponseError, 'missing ORDERID, PAYID, STATUS or NCERROR'):
response = ogone_backend.response(urllib.urlencode(data))
ogone_backend.response(urllib.urlencode(data))
def test_bank_transfer_response(self):
ogone_backend = eopayment.Payment('ogone', BACKEND_PARAMS)
order_id = 'myorder'
data = {'orderid': u'myorder', 'status': u'41', 'payid': u'3011229363',
'cn': u'User', 'ncerror': u'0',
'trxdate': u'10/24/16',
'brand': 'Bank transfer', 'pm': 'bank transfer',
'currency': u'eur', 'amount': u'7.5',
'shasign': u'0E35F687ACBEAA6CA769E0ADDBD0863EB6C1678A'}
data = {
'orderid': u'myorder',
'status': u'41',
'payid': u'3011229363',
'cn': u'User',
'ncerror': u'0',
'trxdate': u'10/24/16',
'brand': 'Bank transfer',
'pm': 'bank transfer',
'currency': u'eur',
'amount': u'7.5',
'shasign': u'0E35F687ACBEAA6CA769E0ADDBD0863EB6C1678A'
}
# uniformize to utf-8 first
for k in data:
data[k] = eopayment.common.force_byte(data[k])

View File

@ -1,4 +1,20 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import codecs
from unittest import TestCase
@ -12,32 +28,45 @@ import eopayment.paybox as paybox
import eopayment
BACKEND_PARAMS = {
'platform': u'test',
'site': u'12345678',
'rang': u'001',
'identifiant': u'12345678',
'shared_secret': u'0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF',
'callback': u'http://example.com/callback',
'platform': u'test',
'site': u'12345678',
'rang': u'001',
'identifiant': u'12345678',
'shared_secret': (
u'0123456789ABCDEF0123456789ABCDEF01234'
u'56789ABCDEF0123456789ABCDEF0123456789'
u'ABCDEF0123456789ABCDEF0123456789ABCDE'
u'F0123456789ABCDEF'
),
'callback': u'http://example.com/callback',
}
class PayboxTests(TestCase):
def test_sign(self):
key = b'0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF'
key = (b'0123456789ABCDEF0123456789ABCDEF0123456789'
b'ABCDEF0123456789ABCDEF0123456789ABCDEF0123'
b'456789ABCDEF0123456789ABCDEF0123456789ABCD'
b'EF')
key = codecs.decode(key, 'hex')
d = dict(paybox.sign([
['PBX_SITE', u'12345678'],
['PBX_RANG', u'32'],
['PBX_IDENTIFIANT', u'12345678'],
['PBX_TOTAL', u'999'],
['PBX_DEVISE', u'978'],
['PBX_CMD', u'appel à Paybox'],
['PBX_PORTEUR', u'test@paybox.com'],
['PBX_RETOUR', u'Mt:M;Ref:R;Auto:A;Erreur:E'],
['PBX_HASH', u'SHA512'],
['PBX_TIME', u'2015-06-08T16:21:16+02:00'],
],
key))
result = '7E74D8E9A0DBB65AAE51C5C50C2668FD98FC99AEDF18244BB1935F602B6C2E953B61FD84364F34FDB88B049901C0A07F6040AF446BBF5589113F48A733D551D4'
['PBX_SITE', u'12345678'],
['PBX_RANG', u'32'],
['PBX_IDENTIFIANT', u'12345678'],
['PBX_TOTAL', u'999'],
['PBX_DEVISE', u'978'],
['PBX_CMD', u'appel à Paybox'],
['PBX_PORTEUR', u'test@paybox.com'],
['PBX_RETOUR', u'Mt:M;Ref:R;Auto:A;Erreur:E'],
['PBX_HASH', u'SHA512'],
['PBX_TIME', u'2015-06-08T16:21:16+02:00'],
], key))
result = (
'7E74D8E9A0DBB65AAE51C5C50C2668FD98FC99AED'
'F18244BB1935F602B6C2E953B61FD84364F34FDB8'
'8B049901C0A07F6040AF446BBF5589113F48A733D'
'551D4'
)
self.assertIn('PBX_HMAC', d)
self.assertEqual(d['PBX_HMAC'], result)
@ -61,14 +90,23 @@ class PayboxTests(TestCase):
'PBX_RANG': '01',
'PBX_SITE': '12345678',
'PBX_IDENTIFIANT': '12345678',
'PBX_RETOUR': 'montant:M;reference:R;code_autorisation:A;erreur:E;numero_appel:T;numero_transaction:S;date_transaction:W;heure_transaction:Q;signature:K',
'PBX_RETOUR': (
'montant:M;reference:R;code_autorisation:A;'
'erreur:E;numero_appel:T;numero_transaction:S;'
'date_transaction:W;heure_transaction:Q;signature:K'
),
'PBX_TIME': time,
'PBX_PORTEUR': email,
'PBX_CMD': order_id + eopayment.common.ORDERID_TRANSACTION_SEPARATOR + transaction,
'PBX_TOTAL': amount.replace('.', ''),
'PBX_DEVISE': '978',
'PBX_HASH': 'SHA512',
'PBX_HMAC': 'CE29AB421D9FF5E22B52A0F0D31BB881E6D3040B7A0B390AC3F335292A75D2389253A3ED6B3E430A90D30088F6AC29F792B484A2ECFC36A1B73771796A5FD15C',
'PBX_HMAC': (
'CE29AB421D9FF5E22B52A0F0D31BB881E6D'
'3040B7A0B390AC3F335292A75D2389253A3'
'ED6B3E430A90D30088F6AC29F792B484A2E'
'CFC36A1B73771796A5FD15C'
),
'PBX_ARCHIVAGE': '1234',
'PBX_REPONDRE_A': 'http://example.com/callback',
'PBX_AUTOSEULE': 'N'
@ -101,7 +139,8 @@ class PayboxTests(TestCase):
transaction_id=transaction, time=time)
root = ET.fromstring(str(what))
form_params = dict(((node.attrib['name'], node.attrib['value']) for node in root if node.attrib['type'] == 'hidden'))
form_params = dict(
((node.attrib['name'], node.attrib['value']) for node in root if node.attrib['type'] == 'hidden'))
self.assertIn('PBX_DIFF', form_params)
self.assertEqual(form_params['PBX_DIFF'], '07')
@ -109,13 +148,13 @@ class PayboxTests(TestCase):
params = BACKEND_PARAMS.copy()
backend = eopayment.Payment('paybox', params)
transaction_id, kind, what = backend.request(
Decimal(amount), email=email, orderid=order_id,
transaction_id=transaction, time=time, capture_day=2)
Decimal(amount), email=email, orderid=order_id,
transaction_id=transaction, time=time, capture_day=2)
root = ET.fromstring(str(what))
form_params = dict(((
node.attrib['name'], node.attrib['value']) for node in root
if node.attrib['type'] == 'hidden'))
if node.attrib['type'] == 'hidden'))
self.assertIn('PBX_DIFF', form_params)
self.assertEqual(form_params['PBX_DIFF'], '02')
@ -125,17 +164,16 @@ class PayboxTests(TestCase):
params['capture_day'] = '7'
backend = eopayment.Payment('paybox', params)
transaction_id, kind, what = backend.request(
Decimal(amount), email=email, orderid=order_id,
transaction_id=transaction, time=time, capture_day=2)
Decimal(amount), email=email, orderid=order_id,
transaction_id=transaction, time=time, capture_day=2)
root = ET.fromstring(str(what))
form_params = dict(((
node.attrib['name'], node.attrib['value']) for node in root
if node.attrib['type'] == 'hidden'))
if node.attrib['type'] == 'hidden'))
self.assertIn('PBX_DIFF', form_params)
self.assertEqual(form_params['PBX_DIFF'], '02')
def test_request_with_authorization_only(self):
params = BACKEND_PARAMS.copy()
time = '2018-08-21T10:26:32+02:00'
@ -151,7 +189,8 @@ class PayboxTests(TestCase):
transaction_id=transaction, time=time)
root = ET.fromstring(str(what))
form_params = dict(((node.attrib['name'], node.attrib['value']) for node in root if node.attrib['type'] == 'hidden'))
form_params = dict(
((node.attrib['name'], node.attrib['value']) for node in root if node.attrib['type'] == 'hidden'))
self.assertEqual(form_params['PBX_AUTOSEULE'], 'O')
def test_response(self):
@ -180,11 +219,16 @@ class PayboxTests(TestCase):
params = BACKEND_PARAMS.copy()
params['cle'] = 'cancelling_key'
backend = eopayment.Payment('paybox', params)
bank_data = {'numero_transaction': ['13957441'],
'numero_appel': ['30310733'],
'reference': ['830657461681']
bank_data = {
'numero_transaction': ['13957441'],
'numero_appel': ['30310733'],
'reference': ['830657461681'],
}
backend_raw_response = u"""NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR="""
backend_raw_response = (
u'NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862'
u'&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000'
u'&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR='
)
backend_expected_response = {"CODEREPONSE": "00000",
"RANG": "32",
"AUTORISATION": "XXXXXX",
@ -206,17 +250,18 @@ class PayboxTests(TestCase):
assert 'DATEQ' in params_sent
# don't care about its value
params_sent.pop('DATEQ')
expected_params = {'CLE': 'cancelling_key',
'VERSION': '00103',
'TYPE': operation_code,
'MONTANT': Decimal('1000'),
'NUMAPPEL': '30310733',
'NUMTRANS': '13957441',
'NUMQUESTION': '0013957441',
'REFERENCE': '830657461681',
'RANG': backend.backend.rang,
'SITE': backend.backend.site,
'DEVISE': backend.backend.devise
expected_params = {
'CLE': 'cancelling_key',
'VERSION': '00103',
'TYPE': operation_code,
'MONTANT': Decimal('1000'),
'NUMAPPEL': '30310733',
'NUMTRANS': '13957441',
'NUMQUESTION': '0013957441',
'REFERENCE': '830657461681',
'RANG': backend.backend.rang,
'SITE': backend.backend.site,
'DEVISE': backend.backend.devise
}
self.assertEqual(params_sent, expected_params)
self.assertEqual(backend_response, backend_expected_response)
@ -233,26 +278,33 @@ class PayboxTests(TestCase):
error_response = u"""CODEREPONSE=00015&COMMENTAIRE=PAYBOX : Transaction non trouvée"""
response = mock.Mock(status_code=200, text=error_response)
requests_post.return_value = response
self.assertRaisesRegexp(eopayment.ResponseError, 'Transaction non trouvée', getattr(backend, operation_name),
Decimal('10'), bank_data)
self.assertRaisesRegexp(
eopayment.ResponseError,
'Transaction non trouvée',
getattr(backend, operation_name),
Decimal('10'),
bank_data)
def test_validate_payment(self):
params = BACKEND_PARAMS.copy()
params['cle'] = 'cancelling_key'
backend = eopayment.Payment('paybox', params)
bank_data = {'numero_transaction': ['13957441'],
'numero_appel': ['30310733'],
'reference': ['830657461681']
bank_data = {
'numero_transaction': ['13957441'],
'numero_appel': ['30310733'],
'reference': ['830657461681']
}
backend_raw_response = u"""NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR="""
backend_raw_response = (
u'NUMTRANS=0013989865&NUMAPPEL=0030378572&NUMQUESTION=0013989862'
u'&SITE=1999888&RANG=32&AUTORISATION=XXXXXX&CODEREPONSE=00000'
u'&COMMENTAIRE=Demande traitée avec succès&REFABONNE=&PORTEUR='
)
with mock.patch('eopayment.paybox.requests.post') as requests_post:
response = mock.Mock(status_code=200, text=backend_raw_response)
requests_post.return_value = response
backend.validate(Decimal(12), bank_data)
def test_rsa_signature_validation(self):
pkey = '''-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUgYufHuheMztK1LhQSG6xsOzb
@ -277,8 +329,11 @@ FBFKOZhgBJnkC+l6+XhT4aYWKaQ4ocmOMV92yjeXTE4='''
backend = eopayment.Payment('paybox', params)
transaction_id, kind, what = backend.request(
Decimal(amount), email=email, orderid=order_id,
transaction_id=transaction, time=time)
Decimal(amount),
email=email,
orderid=order_id,
transaction_id=transaction,
time=time)
root = ET.fromstring(str(what))
form_params = dict((
(node.attrib['name'], node.attrib['value']) for node in root
@ -287,8 +342,12 @@ FBFKOZhgBJnkC+l6+XhT4aYWKaQ4ocmOMV92yjeXTE4='''
self.assertEqual(form_params['PBX_AUTOSEULE'], 'N')
transaction_id, kind, what = backend.request(
Decimal(amount), email=email, orderid=order_id,
transaction_id=transaction, time=time, manual_validation=True)
Decimal(amount),
email=email,
orderid=order_id,
transaction_id=transaction,
time=time,
manual_validation=True)
root = ET.fromstring(str(what))
form_params = dict((
(node.attrib['name'], node.attrib['value']) for node in root

View File

@ -1,7 +1,7 @@
# coding: utf-8
#
# eopayment - online payment library
# Copyright (C) 2011-2019 Entr'ouvert
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published

View File

@ -1,4 +1,20 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import eopayment
import pytest
@ -9,7 +25,7 @@ def test_build_request():
transaction, f, form = backend.request(amount=u'12', last_name=u'Foo',
first_name=u'Félix000000')
data = [f for f in form.fields if f['name'] == 'Data']
assert not u'lix000000' in data[0]['value']
assert u'lix000000' not in data[0]['value']
transaction, f, form = backend.request(amount=u'12')
data = [f for f in form.fields if f['name'] == 'Data']
@ -29,19 +45,20 @@ def test_options():
assert payment.backend.get_data()['captureMode'] == 'VALIDATION'
payment = eopayment.Payment('sips2', {})
assert not 'captureDay' in payment.backend.get_data()
assert 'captureDay' not in payment.backend.get_data()
payment = eopayment.Payment('sips2', {'capture_day': '10'})
assert 'captureDay' in payment.backend.get_data()
def test_parse_response():
qs = '''Data=captureDay%3D0%7CcaptureMode%3DAUTHOR_CAPTURE%7CcurrencyCode%3D978%7CmerchantId%3D002001000000001%7CorderChannel%3DINTERNET%7CresponseCode%3D00%7CtransactionDateTime%3D2016-02-01T17%3A44%3A20%2B01%3A00%7CtransactionReference%3D668930%7CkeyVersion%3D1%7CacquirerResponseCode%3D00%7Camount%3D1200%7CauthorisationId%3D12345%7CcardCSCResultCode%3D4E%7CpanExpiryDate%3D201605%7CpaymentMeanBrand%3DMASTERCARD%7CpaymentMeanType%3DCARD%7CcustomerIpAddress%3D82.244.203.243%7CmaskedPan%3D5100%23%23%23%23%23%23%23%23%23%23%23%2300%7CorderId%3Dd4903de7027f4d56ac01634fd7ab9526%7CholderAuthentRelegation%3DN%7CholderAuthentStatus%3D3D_ERROR%7CtransactionOrigin%3DINTERNET%7CpaymentPattern%3DONE_SHOT&Seal=6ca3247765a19b45d25ad54ef4076483e7d55583166bd5ac9c64357aac097602&InterfaceVersion=HP_2.0&Encode='''
qs = '''Data=captureDay%3D0%7CcaptureMode%3DAUTHOR_CAPTURE%7CcurrencyCode%3D978%7CmerchantId%3D002001000000001%7CorderChannel%3DINTERNET%7CresponseCode%3D00%7CtransactionDateTime%3D2016-02-01T17%3A44%3A20%2B01%3A00%7CtransactionReference%3D668930%7CkeyVersion%3D1%7CacquirerResponseCode%3D00%7Camount%3D1200%7CauthorisationId%3D12345%7CcardCSCResultCode%3D4E%7CpanExpiryDate%3D201605%7CpaymentMeanBrand%3DMASTERCARD%7CpaymentMeanType%3DCARD%7CcustomerIpAddress%3D82.244.203.243%7CmaskedPan%3D5100%23%23%23%23%23%23%23%23%23%23%23%2300%7CorderId%3Dd4903de7027f4d56ac01634fd7ab9526%7CholderAuthentRelegation%3DN%7CholderAuthentStatus%3D3D_ERROR%7CtransactionOrigin%3DINTERNET%7CpaymentPattern%3DONE_SHOT&Seal=6ca3247765a19b45d25ad54ef4076483e7d55583166bd5ac9c64357aac097602&InterfaceVersion=HP_2.0&Encode=''' # noqa: E501
backend = eopayment.Payment('sips2', {})
response = backend.response(qs)
assert response.signed
assert response.transaction_date is None
qs = '''Data=captureDay%3D0%7CcaptureMode%3DAUTHOR_CAPTURE%7CcurrencyCode%3D978%7CmerchantId%3D002001000000001%7CorderChannel%3DINTERNET%7CresponseCode%3D00%7CtransactionDateTime%3D2016-02-01T17%3A44%3A20%2B01%3A00%7CtransactionReference%3D668930%7CkeyVersion%3D1%7CacquirerResponseCode%3D00%7Camount%3D1200%7CauthorisationId%3D12345%7CcardCSCResultCode%3D4E%7CpanExpiryDate%3D201605%7CpaymentMeanBrand%3DMASTERCARD%7CpaymentMeanType%3DCARD%7CcustomerIpAddress%3D82.244.203.243%7CmaskedPan%3D5100%23%23%23%23%23%23%23%23%23%23%23%2300%7CorderId%3Dd4903de7027f4d56ac01634fd7ab9526%7CholderAuthentRelegation%3DN%7CholderAuthentStatus%3D3D_ERROR%7CtransactionOrigin%3DINTERNET%7CpaymentPattern%3DONE_SHOT%7CtransactionDateTime%3D2020-01-01%2001:01:01&Seal=6ca3247765a19b45d25ad54ef4076483e7d55583166bd5ac9c64357aac097602&InterfaceVersion=HP_2.0&Encode='''
qs = '''Data=captureDay%3D0%7CcaptureMode%3DAUTHOR_CAPTURE%7CcurrencyCode%3D978%7CmerchantId%3D002001000000001%7CorderChannel%3DINTERNET%7CresponseCode%3D00%7CtransactionDateTime%3D2016-02-01T17%3A44%3A20%2B01%3A00%7CtransactionReference%3D668930%7CkeyVersion%3D1%7CacquirerResponseCode%3D00%7Camount%3D1200%7CauthorisationId%3D12345%7CcardCSCResultCode%3D4E%7CpanExpiryDate%3D201605%7CpaymentMeanBrand%3DMASTERCARD%7CpaymentMeanType%3DCARD%7CcustomerIpAddress%3D82.244.203.243%7CmaskedPan%3D5100%23%23%23%23%23%23%23%23%23%23%23%2300%7CorderId%3Dd4903de7027f4d56ac01634fd7ab9526%7CholderAuthentRelegation%3DN%7CholderAuthentStatus%3D3D_ERROR%7CtransactionOrigin%3DINTERNET%7CpaymentPattern%3DONE_SHOT%7CtransactionDateTime%3D2020-01-01%2001:01:01&Seal=6ca3247765a19b45d25ad54ef4076483e7d55583166bd5ac9c64357aac097602&InterfaceVersion=HP_2.0&Encode=''' # noqa: E501
response = backend.response(qs)
assert not response.signed
assert response.transaction_date.isoformat() == '2020-01-01T01:01:01+01:00'

View File

@ -1,14 +1,36 @@
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from unittest import TestCase
import eopayment.spplus as spplus
from eopayment import ResponseError
class SPPlustTest(TestCase):
ntkey = b'58 6d fc 9c 34 91 9b 86 3f ' \
b'fd 64 63 c9 13 4a 26 ba 29 74 1e c7 e9 80 79'
tests = [('x=coin', 'c04f8266d6ae3ce37551cce996c751be4a95d10a'),
('x=coin&y=toto', 'ef008e02f8dbf5e70e83da416b0b3a345db203de'),
('x=wdwd%20%3Fdfgfdgd&z=343&hmac=04233b78bb5aff332d920d4e89394f505ec58a2a', '04233b78bb5aff332d920d4e89394f505ec58a2a')]
tests = [
('x=coin',
'c04f8266d6ae3ce37551cce996c751be4a95d10a'),
('x=coin&y=toto',
'ef008e02f8dbf5e70e83da416b0b3a345db203de'),
('x=wdwd%20%3Fdfgfdgd&z=343&hmac=04233b78bb5aff332d920d4e89394f505ec58a2a',
'04233b78bb5aff332d920d4e89394f505ec58a2a')
]
def test_spplus(self):
payment = spplus.Payment({'cle': self.ntkey, 'siret': '00000000000001-01'})

View File

@ -1,4 +1,21 @@
# -*- coding: utf-8 -*-
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, timedelta
import pytest
@ -25,14 +42,18 @@ def get_field(form, field_name):
def test_systempayv2():
p = Payment(PARAMS)
data = {'amount': 15.24, 'orderid': '654321',
'first_name': u'Jean Michél',
'last_name': u'Mihaï'
data = {
'amount': 15.24,
'orderid': '654321',
'first_name': u'Jean Michél',
'last_name': u'Mihaï'
}
qs = 'vads_version=V2&vads_page_action=PAYMENT&vads_action_mode=INTERACTIV' \
'E&vads_payment_config=SINGLE&vads_site_id=12345678&vads_ctx_mode=TES' \
'T&vads_trans_id=654321&vads_trans_date=20090501193530&vads_amount=15' \
'24&vads_currency=978&vads_cust_first_name=Jean+Mich%C3%A9l&vads_cust_last_name=Mihaï'
qs = (
'vads_version=V2&vads_page_action=PAYMENT&vads_action_mode=INTERACTIV'
'E&vads_payment_config=SINGLE&vads_site_id=12345678&vads_ctx_mode=TES'
'T&vads_trans_id=654321&vads_trans_date=20090501193530&vads_amount=15'
'24&vads_currency=978&vads_cust_first_name=Jean+Mich%C3%A9l&vads_cust_last_name=Mihaï'
)
qs = urlparse.parse_qs(qs)
for key in qs.keys():
qs[key] = qs[key][0]

View File

@ -1,3 +1,19 @@
# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
from decimal import Decimal
from six.moves.urllib.parse import urlparse, parse_qs