diff --git a/eopayment/__init__.py b/eopayment/__init__.py
index 6e27094..43eefb8 100644
--- a/eopayment/__init__.py
+++ b/eopayment/__init__.py
@@ -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
diff --git a/eopayment/spplus.py b/eopayment/spplus.py
deleted file mode 100644
index 1f527c7..0000000
--- a/eopayment/spplus.py
+++ /dev/null
@@ -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 .
-
-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]))
diff --git a/tests/test_spplus.py b/tests/test_spplus.py
deleted file mode 100644
index 3be0db5..0000000
--- a/tests/test_spplus.py
+++ /dev/null
@@ -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 .
-
-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)