remove spplus module (#47539)
This commit is contained in:
parent
8a099ecfa9
commit
4f77d05222
|
@ -28,7 +28,7 @@ from .common import ( # noqa: F401
|
|||
ResponseError, PaymentException,
|
||||
)
|
||||
|
||||
__all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', 'SYSTEMPAY', 'SPPLUS',
|
||||
__all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', 'SYSTEMPAY',
|
||||
'TIPI', 'DUMMY', 'get_backend', 'RECEIVED', 'ACCEPTED', 'PAID',
|
||||
'DENIED', 'CANCELED', 'CANCELLED', 'ERROR', 'WAITING',
|
||||
'get_backends', 'PAYFIP_WS']
|
||||
|
@ -39,7 +39,6 @@ if six.PY3:
|
|||
SIPS = 'sips'
|
||||
SIPS2 = 'sips2'
|
||||
SYSTEMPAY = 'systempayv2'
|
||||
SPPLUS = 'spplus'
|
||||
TIPI = 'tipi'
|
||||
DUMMY = 'dummy'
|
||||
OGONE = 'ogone'
|
||||
|
@ -57,7 +56,7 @@ def get_backend(kind):
|
|||
module = importlib.import_module('.' + kind, package='eopayment')
|
||||
return module.Payment
|
||||
|
||||
__BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, SPPLUS, OGONE, PAYBOX, PAYZEN,
|
||||
__BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, OGONE, PAYBOX, PAYZEN,
|
||||
TIPI, PAYFIP_WS, KEYWARE, MOLLIE]
|
||||
|
||||
|
||||
|
@ -77,19 +76,17 @@ class Payment(object):
|
|||
Interface to credit card online payment servers of French banks. The
|
||||
only use case supported for now is a unique automatic payment.
|
||||
|
||||
>>> spplus_options = { \
|
||||
'cle': '58 6d fc 9c 34 91 9b 86 3f fd 64 ' \
|
||||
'63 c9 13 4a 26 ba 29 74 1e c7 e9 80 79', \
|
||||
'siret': '00000000000001-01', \
|
||||
>>> options = {
|
||||
'numcli': '12345',
|
||||
}
|
||||
>>> p = Payment(kind=SPPLUS, options=spplus_options)
|
||||
>>> p = Payment(kind=TIPI, options=options)
|
||||
>>> transaction_id, kind, data = p.request('10.00', email='bob@example.com')
|
||||
>>> print (transaction_id, kind, data) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
||||
('...', 1, 'https://www.spplus.net/paiement/init.do?...')
|
||||
('...', 1, 'https://www.payfip.gov.fr/tpa/paiement.web?...')
|
||||
|
||||
Supported backend of French banks are:
|
||||
|
||||
- sips, for BNP, Banque Populaire (before 2010), CCF, HSBC, Crédit
|
||||
- SIPS 2.0, for BNP, Banque Populaire (before 2010), CCF, HSBC, Crédit
|
||||
Agricole, La Banque Postale, LCL, Société Générale and Crédit du
|
||||
Nord.
|
||||
- spplus for Caisse d'épargne
|
||||
|
|
|
@ -1,276 +0,0 @@
|
|||
# -*- 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
|
||||
import hmac
|
||||
import hashlib
|
||||
from six.moves.urllib import parse as urlparse
|
||||
from six.moves.urllib import parse as urllib
|
||||
import string
|
||||
import datetime as dt
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
|
||||
import Crypto.Cipher.DES
|
||||
from .common import (
|
||||
PaymentCommon, URL, PaymentResponse, RECEIVED, ACCEPTED,
|
||||
PAID, ERROR, ResponseError, force_byte
|
||||
)
|
||||
|
||||
|
||||
def N_(message):
|
||||
return message
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
KEY_DES_KEY = b'\x45\x1f\xba\x4f\x4c\x3f\xd4\x97'
|
||||
IV = b'\x30\x78\x30\x62\x2c\x30\x78\x30'
|
||||
REFERENCE = 'reference'
|
||||
ETAT = 'etat'
|
||||
SPCHECKOK = 'spcheckok'
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
REFSFP = 'refsfp'
|
||||
|
||||
# Pour un paiement comptant la chaine des états est: 1 -> 4 -> 10, seul l'état
|
||||
# 10 garanti le paiement
|
||||
SPPLUS_RESPONSE_CODES = {
|
||||
'1': 'Autorisation de paiement acceptée',
|
||||
'2': 'Autorisation de paiement refusée',
|
||||
'4': 'Echéance du paiement acceptée et en attente de remise',
|
||||
'5': 'Echéance du paiement refusée',
|
||||
'6': 'Paiement par chèque accepté',
|
||||
'8': 'Chèque encaissé',
|
||||
'10': 'Paiement terminé',
|
||||
'11': 'Echéance du paiement annulée par le commerçant',
|
||||
'12': 'Abandon de l’internaute',
|
||||
'15': 'Remboursement enregistré',
|
||||
'16': 'Remboursement annulé',
|
||||
'17': 'Remboursement accepté',
|
||||
'20': 'Echéance du paiement avec un impayé',
|
||||
'21': 'Echéance du paiement avec un impayé et en attente de validation des services SP PLUS',
|
||||
'30': 'Echéance du paiement remisée',
|
||||
'99': 'Paiement de test en production',
|
||||
}
|
||||
|
||||
VALID_STATE = ('1', '4', '10')
|
||||
ACCEPTED_STATE = ('1', '4')
|
||||
PAID_STATE = ('10',)
|
||||
TEST_STATE = ('99',)
|
||||
|
||||
|
||||
def decrypt_ntkey(ntkey):
|
||||
key = binascii.unhexlify(force_byte(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 = []
|
||||
for kv in kvs:
|
||||
k, v = kv.split('=', 1)
|
||||
if k != 'hmac':
|
||||
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'
|
||||
]
|
||||
|
||||
|
||||
def sign_url_paiement(ntkey, query):
|
||||
if '?' in query:
|
||||
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]
|
||||
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(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):
|
||||
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,
|
||||
}
|
||||
if email:
|
||||
fields['email'] = email
|
||||
normal_return_url = self.normal_return_url
|
||||
if next_url and not normal_return_url:
|
||||
warnings.warn("passing next_url to request() is deprecated, "
|
||||
"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')
|
||||
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))
|
||||
logger.debug('full url %s' % url)
|
||||
return reference, URL, url
|
||||
|
||||
def response(self, query_string, logger=LOGGER, **kwargs):
|
||||
form = urlparse.parse_qs(query_string)
|
||||
if not set(form) >= set([REFERENCE, ETAT, REFSFP]):
|
||||
raise ResponseError('missing %s, %s or %s' % (REFERENCE, ETAT, REFSFP))
|
||||
for key, value in form.items():
|
||||
form[key] = value[0]
|
||||
logger.debug('received query_string %s' % query_string)
|
||||
logger.debug('parsed as %s' % form)
|
||||
reference = form.get(REFERENCE)
|
||||
bank_status = []
|
||||
signed = False
|
||||
form[self.BANK_ID] = form.get(REFSFP)
|
||||
etat = form.get('etat')
|
||||
status = '%s: %s' % (etat, SPPLUS_RESPONSE_CODES.get(etat, 'Unknown code'))
|
||||
logger.debug('status is %s', status)
|
||||
bank_status.append(status)
|
||||
if 'hmac' in form:
|
||||
try:
|
||||
signed_data, signature = query_string.rsplit('&', 1)
|
||||
_, hmac = signature.split('=', 1)
|
||||
logger.debug('got signature %s' % hmac)
|
||||
computed_hmac = sign_ntkey_query(self.cle, signed_data)
|
||||
logger.debug('computed signature %s' % computed_hmac)
|
||||
signed = hmac == computed_hmac
|
||||
if not signed:
|
||||
bank_status.append('invalid signature')
|
||||
except ValueError:
|
||||
bank_status.append('invalid signature')
|
||||
|
||||
test = False
|
||||
if etat in PAID_STATE:
|
||||
result = PAID
|
||||
elif etat in ACCEPTED_STATE:
|
||||
result = ACCEPTED
|
||||
elif etat in VALID_STATE:
|
||||
result = RECEIVED
|
||||
elif etat in TEST_STATE:
|
||||
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)
|
||||
return response
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
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:
|
||||
print(sign_url_paiement(ntkey, sys.argv[1]))
|
||||
print(sign_ntkey_query(ntkey, sys.argv[1]))
|
||||
elif len(sys.argv) > 2:
|
||||
print(sign_url_paiement(sys.argv[1], sys.argv[2]))
|
||||
print(sign_ntkey_query(sys.argv[1], sys.argv[2]))
|
|
@ -1,47 +0,0 @@
|
|||
# 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.spplus as spplus
|
||||
from eopayment import ResponseError, force_text
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
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')
|
||||
]
|
||||
|
||||
|
||||
def test_spplus():
|
||||
payment = spplus.Payment({'cle': ntkey, 'siret': '00000000000001-01'})
|
||||
|
||||
for query, result in tests:
|
||||
assert spplus.sign_ntkey_query(ntkey, query).lower() == result
|
||||
|
||||
with pytest.raises(ResponseError, match=r'missing reference, etat or refsfp'):
|
||||
payment.response('foo=bar')
|
||||
|
||||
# make sure key string and bytes representations are understood
|
||||
spplus.decrypt_ntkey(force_text(ntkey))
|
||||
spplus.decrypt_ntkey(ntkey)
|
Loading…
Reference in New Issue