eopayment/eopayment/spplus.py

241 lines
8.8 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
2011-04-22 17:21:16 +02:00
from decimal import Decimal
import binascii
from gettext import gettext as _
2011-04-22 17:21:16 +02:00
import hmac
import hashlib
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
2018-03-26 09:30:17 +02:00
from .common import (PaymentCommon, URL, PaymentResponse, RECEIVED, ACCEPTED,
2018-03-26 20:28:14 +02:00
PAID, ERROR, ResponseError, force_byte)
2011-04-22 17:21:16 +02:00
def N_(message): return message
__all__ = ['Payment']
2018-03-26 20:28:14 +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',
'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',
}
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):
2018-03-26 20:28:14 +02:00
key = binascii.unhexlify(ntkey.replace(b' ', b''))
2011-04-22 17:21:16 +02:00
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)
2018-03-26 20:28:14 +02:00
return force_byte(''.join(result))
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
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()
2011-04-22 17:21:16 +02:00
2018-03-26 09:48:09 +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"
class Payment(PaymentCommon):
description = {
'caption': "SPPlus payment service of French bank Caisse d'epargne",
2012-05-25 15:40:41 +02:00
'parameters': [
{
'name': 'normal_return_url',
'caption': N_('Normal return URL'),
'default': '',
'required': True,
},
{
'name': 'automatic_return_url',
'caption': N_('Automatic return URL'),
'required': False,
},
2012-05-25 15:40:41 +02:00
{ 'name': 'cle',
'caption': 'Secret key, a 40 digits hexadecimal number',
'regexp': re.compile('^ *((?:[a-fA-F0-9] *){40}) *$')
},
2012-05-25 15:40:41 +02:00
{ 'name': 'siret',
'caption': 'Siret of the entreprise augmented with the '
'site number, example: 00000000000001-01',
'regexp': re.compile('^ *(\d{14}-\d{2}) *$')
},
2012-05-25 15:40:41 +02:00
{ 'name': 'langue',
'caption': 'Language of the customers',
'default': 'FR',
},
2012-05-25 15:40:41 +02:00
{ 'name': 'taxe',
'caption': 'Taxes',
'default': '0.00'
},
2012-05-25 15:40:41 +02:00
{ 'name': 'modalite',
'caption': '1x, 2x, 3x, xx, nx (if multiple separated by "/")',
'default': '1x',
},
2012-05-25 15:40:41 +02:00
{ 'name': 'moyen',
'caption': 'AUR, AMX, CBS, CGA, '
'CHK, DIN, PRE (if multiple separate by "/")',
'default': 'CBS',
},
2012-05-25 15:40:41 +02:00
]
}
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,
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')
fields = { 'siret': self.siret,
'devise': self.devise,
'langue': self.langue,
'taxe': self.taxe,
2015-03-04 14:49:26 +01:00
'montant': str(Decimal(amount)),
2016-02-08 18:40:45 +01:00
REFERENCE: orderid or reference,
2011-04-22 17:21:16 +02:00
'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:
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)
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()
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:
result = RECEIVED # what else ?
test = True
2012-02-20 19:07:55 +01:00
else:
result = ERROR
response = PaymentResponse(
result=result,
2012-02-20 19:07:55 +01:00
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:38:02 +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:38:02 +02:00
print(sign_url_paiement(sys.argv[1], sys.argv[2]))
print(sign_ntkey_query(sys.argv[1], sys.argv[2]))