eopayment/eopayment/spplus.py

277 lines
9.5 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2020-04-10 11:10:15 +02:00
# 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/>.
2011-04-22 17:21:16 +02:00
from decimal import Decimal
import binascii
import hmac
import hashlib
2018-03-26 09:56:16 +02:00
from six.moves.urllib import parse as urlparse
from six.moves.urllib import parse as urllib
2011-04-22 17:21:16 +02:00
import string
import datetime as dt
import logging
import re
import warnings
2011-04-22 17:21:16 +02:00
import Crypto.Cipher.DES
2020-04-10 11:10:15 +02:00
from .common import (
PaymentCommon, URL, PaymentResponse, RECEIVED, ACCEPTED,
PAID, ERROR, ResponseError, force_byte
)
2011-04-22 17:21:16 +02:00
2020-04-10 11:10:15 +02:00
def N_(message):
return message
__all__ = ['Payment']
2018-03-26 09:56:16 +02:00
KEY_DES_KEY = b'\x45\x1f\xba\x4f\x4c\x3f\xd4\x97'
IV = b'\x30\x78\x30\x62\x2c\x30\x78\x30'
2011-04-22 17:21:16 +02:00
REFERENCE = 'reference'
ETAT = 'etat'
SPCHECKOK = 'spcheckok'
LOGGER = logging.getLogger(__name__)
REFSFP = 'refsfp'
2011-04-22 17:21:16 +02:00
2012-02-20 19:07:55 +01:00
# 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',
2020-04-10 11:10:15 +02:00
'12': 'Abandon de linternaute',
'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',
}
2012-02-20 19:07:55 +01:00
VALID_STATE = ('1', '4', '10')
ACCEPTED_STATE = ('1', '4')
PAID_STATE = ('10',)
TEST_STATE = ('99',)
2012-02-20 19:07:55 +01:00
2011-04-22 17:21:16 +02:00
def decrypt_ntkey(ntkey):
key = binascii.unhexlify(force_byte(ntkey).replace(b' ', b''))
2011-04-22 17:21:16 +02:00
return decrypt_key(key)
2020-04-10 11:10:15 +02:00
2011-04-22 17:21:16 +02:00
def decrypt_key(key):
CIPHER = Crypto.Cipher.DES.new(KEY_DES_KEY, Crypto.Cipher.DES.MODE_CBC, IV)
return CIPHER.decrypt(key)
2020-04-10 11:10:15 +02:00
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)
2018-03-26 09:56:16 +02:00
return force_byte(''.join(result))
2020-04-10 11:10:15 +02:00
2011-04-22 17:21:16 +02:00
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()
2011-04-22 17:21:16 +02:00
2020-04-10 11:10:15 +02:00
PAIEMENT_FIELDS = [
'siret', REFERENCE, 'langue', 'devise', 'montant', 'taxe', 'validite'
]
2011-04-22 17:21:16 +02:00
def sign_url_paiement(ntkey, query):
if '?' in query:
2020-04-10 11:10:15 +02:00
query = query[query.index('?') + 1:]
2011-04-22 17:21:16 +02:00
key = decrypt_ntkey(ntkey)
data = urlparse.parse_qs(query, True)
2020-04-10 11:10:15 +02:00
fields = [data.get(field, [''])[0] for field in PAIEMENT_FIELDS]
2011-04-22 17:21:16 +02:00
data_to_sign = ''.join(fields)
return hmac.new(key[:20], data_to_sign, hashlib.sha1).hexdigest().upper()
2011-04-22 17:21:16 +02:00
2018-03-26 09:56:16 +02:00
ALPHANUM = string.ascii_letters + string.digits
2011-04-22 17:21:16 +02:00
SERVICE_URL = "https://www.spplus.net/paiement/init.do"
2020-04-10 11:10:15 +02:00
2011-04-22 17:21:16 +02:00
class Payment(PaymentCommon):
description = {
2020-04-10 11:10:15 +02:00
'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'
2011-04-22 17:21:16 +02:00
2016-02-08 18:40:45 +01:00
def request(self, amount, name=None, address=None, email=None, phone=None,
2020-04-10 11:10:15 +02:00
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))
2011-04-22 17:21:16 +02:00
reference = self.transaction_id(20, ALPHANUM, 'spplus', self.siret)
validite = dt.date.today()+dt.timedelta(days=1)
validite = validite.strftime('%d/%m/%Y')
2020-04-10 11:10:15 +02:00
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,
}
2011-04-22 17:21:16 +02:00
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:
2020-04-10 11:10:15 +02:00
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
2012-02-20 17:14:30 +01:00
logger.debug('sending fields %s' % fields)
2011-04-22 17:21:16 +02:00
query = urllib.urlencode(fields)
2020-04-10 11:10:15 +02:00
url = '%s?%s&hmac=%s' % (SERVICE_URL, query, sign_url_paiement(self.cle, query))
2012-02-20 17:14:30 +01:00
logger.debug('full url %s' % url)
return reference, URL, url
2011-04-22 17:21:16 +02:00
def response(self, query_string, logger=LOGGER, **kwargs):
2011-04-22 17:21:16 +02:00
form = urlparse.parse_qs(query_string)
if not set(form) >= set([REFERENCE, ETAT, REFSFP]):
raise ResponseError('missing %s, %s or %s' % (REFERENCE, ETAT, REFSFP))
2018-03-26 09:56:16 +02:00
for key, value in form.items():
form[key] = value[0]
2012-02-20 17:14:30 +01:00
logger.debug('received query_string %s' % query_string)
logger.debug('parsed as %s' % form)
2011-04-22 17:21:16 +02:00
reference = form.get(REFERENCE)
bank_status = []
2012-02-20 19:07:55 +01:00
signed = False
form[self.BANK_ID] = form.get(REFSFP)
etat = form.get('etat')
status = '%s: %s' % (etat, SPPLUS_RESPONSE_CODES.get(etat, 'Unknown code'))
2012-02-20 19:07:55 +01:00
logger.debug('status is %s', status)
bank_status.append(status)
if 'hmac' in form:
2011-04-22 17:21:16 +02:00
try:
signed_data, signature = query_string.rsplit('&', 1)
_, hmac = signature.split('=', 1)
2012-02-20 17:14:30 +01:00
logger.debug('got signature %s' % hmac)
computed_hmac = sign_ntkey_query(self.cle, signed_data)
2012-02-20 21:02:49 +01:00
logger.debug('computed signature %s' % computed_hmac)
2012-02-20 19:07:55 +01:00
signed = hmac == computed_hmac
if not signed:
bank_status.append('invalid signature')
2011-04-22 17:21:16 +02:00
except ValueError:
bank_status.append('invalid signature')
test = False
2012-02-20 19:07:55 +01:00
if etat in PAID_STATE:
result = PAID
2012-02-20 19:44:30 +01:00
elif etat in ACCEPTED_STATE:
2012-02-20 19:07:55 +01:00
result = ACCEPTED
elif etat in VALID_STATE:
result = RECEIVED
elif etat in TEST_STATE:
2020-04-10 11:10:15 +02:00
result = RECEIVED # what else ?
test = True
2012-02-20 19:07:55 +01:00
else:
result = ERROR
response = PaymentResponse(
2020-04-10 11:10:15 +02:00
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
2011-04-22 17:21:16 +02:00
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:
2018-03-26 09:56:16 +02:00
print(sign_url_paiement(ntkey, sys.argv[1]))
print(sign_ntkey_query(ntkey, sys.argv[1]))
elif len(sys.argv) > 2:
2018-03-26 09:56:16 +02:00
print(sign_url_paiement(sys.argv[1], sys.argv[2]))
print(sign_ntkey_query(sys.argv[1], sys.argv[2]))