2012-02-17 18:14:07 +01:00
|
|
|
|
# -*- coding: utf-8 -*-
|
2011-04-22 17:21:16 +02:00
|
|
|
|
from decimal import Decimal
|
|
|
|
|
import binascii
|
2016-02-16 10:51:41 +01:00
|
|
|
|
from gettext import gettext as _
|
2011-04-22 17:21:16 +02:00
|
|
|
|
import hmac
|
|
|
|
|
import hashlib
|
2018-03-26 09:35:45 +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
|
2011-04-30 19:04:50 +02:00
|
|
|
|
import re
|
2016-02-16 10:51:41 +01:00
|
|
|
|
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,
|
2016-03-09 22:31:40 +01:00
|
|
|
|
PAID, ERROR, ResponseError)
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2016-02-16 19:11:47 +01:00
|
|
|
|
def N_(message): return message
|
|
|
|
|
|
2011-04-26 12:41:19 +02:00
|
|
|
|
__all__ = ['Payment']
|
|
|
|
|
|
2011-04-22 17:21:16 +02:00
|
|
|
|
KEY_DES_KEY = '\x45\x1f\xba\x4f\x4c\x3f\xd4\x97'
|
|
|
|
|
IV = '\x30\x78\x30\x62\x2c\x30\x78\x30'
|
|
|
|
|
REFERENCE = 'reference'
|
|
|
|
|
ETAT = 'etat'
|
|
|
|
|
SPCHECKOK = 'spcheckok'
|
2011-04-26 12:41:19 +02:00
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
2012-01-30 17:47:47 +01:00
|
|
|
|
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
|
2012-02-17 18:14:07 +01:00
|
|
|
|
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',)
|
2014-03-30 19:27:48 +02:00
|
|
|
|
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(ntkey.replace(' ',''))
|
|
|
|
|
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)
|
|
|
|
|
|
2012-02-20 21:01:44 +01: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)
|
|
|
|
|
return ''.join(result)
|
|
|
|
|
|
2011-04-22 17:21:16 +02:00
|
|
|
|
def sign_ntkey_query(ntkey, query):
|
|
|
|
|
key = decrypt_ntkey(ntkey)
|
2012-02-20 21:01:44 +01:00
|
|
|
|
data_to_sign = extract_values(query)
|
2011-12-12 17:24:12 +01:00
|
|
|
|
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)
|
2011-12-12 17:24:12 +01:00
|
|
|
|
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):
|
2011-04-30 19:04:50 +02:00
|
|
|
|
description = {
|
|
|
|
|
'caption': "SPPlus payment service of French bank Caisse d'epargne",
|
2012-05-25 15:40:41 +02:00
|
|
|
|
'parameters': [
|
2016-02-16 10:51:41 +01:00
|
|
|
|
{
|
|
|
|
|
'name': 'normal_return_url',
|
2016-02-16 19:11:47 +01:00
|
|
|
|
'caption': N_('Normal return URL'),
|
2016-02-16 10:51:41 +01:00
|
|
|
|
'default': '',
|
|
|
|
|
'required': True,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
'name': 'automatic_return_url',
|
2016-02-16 19:11:47 +01:00
|
|
|
|
'caption': N_('Automatic return URL'),
|
2016-02-16 10:51:41 +01:00
|
|
|
|
'required': False,
|
|
|
|
|
},
|
2012-05-25 15:40:41 +02:00
|
|
|
|
{ 'name': 'cle',
|
2011-04-30 19:04:50 +02:00
|
|
|
|
'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',
|
2011-04-30 19:04:50 +02:00
|
|
|
|
'caption': 'Siret of the entreprise augmented with the '
|
|
|
|
|
'site number, example: 00000000000001-01',
|
|
|
|
|
'regexp': re.compile('^ *(\d{14}-\d{2}) *$')
|
2011-05-27 15:55:26 +02:00
|
|
|
|
},
|
2012-05-25 15:40:41 +02:00
|
|
|
|
{ 'name': 'langue',
|
2011-05-27 15:55:26 +02:00
|
|
|
|
'caption': 'Language of the customers',
|
|
|
|
|
'default': 'FR',
|
|
|
|
|
},
|
2012-05-25 15:40:41 +02:00
|
|
|
|
{ 'name': 'taxe',
|
2011-05-27 15:55:26 +02:00
|
|
|
|
'caption': 'Taxes',
|
|
|
|
|
'default': '0.00'
|
|
|
|
|
},
|
2012-05-25 15:40:41 +02:00
|
|
|
|
{ 'name': 'modalite',
|
2011-12-12 17:51:09 +01:00
|
|
|
|
'caption': '1x, 2x, 3x, xx, nx (if multiple separated by "/")',
|
|
|
|
|
'default': '1x',
|
|
|
|
|
},
|
2012-05-25 15:40:41 +02:00
|
|
|
|
{ 'name': 'moyen',
|
2011-12-12 17:51:09 +01:00
|
|
|
|
'caption': 'AUR, AMX, CBS, CGA, '
|
|
|
|
|
'CHK, DIN, PRE (if multiple separate by "/")',
|
|
|
|
|
'default': 'CBS',
|
|
|
|
|
},
|
2012-05-25 15:40:41 +02:00
|
|
|
|
]
|
2011-04-30 19:04:50 +02:00
|
|
|
|
}
|
2011-05-27 15:55:26 +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):
|
2016-02-16 10:51:41 +01:00
|
|
|
|
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,
|
2011-12-12 17:51:09 +01:00
|
|
|
|
'version': '1',
|
|
|
|
|
'modalite': self.modalite,
|
|
|
|
|
'moyen': self.moyen }
|
2011-04-22 17:21:16 +02:00
|
|
|
|
if email:
|
|
|
|
|
fields['email'] = email
|
2016-02-16 10:51:41 +01:00
|
|
|
|
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)
|
2011-12-12 17:18:52 +01:00
|
|
|
|
url = '%s?%s&hmac=%s' % (SERVICE_URL, query, sign_url_paiement(self.cle,
|
2011-04-26 12:41:19 +02:00
|
|
|
|
query))
|
2012-02-20 17:14:30 +01:00
|
|
|
|
logger.debug('full url %s' % url)
|
2011-04-26 12:41:19 +02:00
|
|
|
|
return reference, URL, url
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
2015-07-16 14:12:15 +02:00
|
|
|
|
def response(self, query_string, logger=LOGGER, **kwargs):
|
2011-04-22 17:21:16 +02:00
|
|
|
|
form = urlparse.parse_qs(query_string)
|
2016-03-09 22:31:40 +01:00
|
|
|
|
if not set(form) >= set([REFERENCE, ETAT, REFSFP]):
|
|
|
|
|
raise ResponseError()
|
2012-02-20 16:48:44 +01:00
|
|
|
|
for key, value in form.iteritems():
|
|
|
|
|
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)
|
2012-02-17 18:17:56 +01:00
|
|
|
|
bank_status = []
|
2012-02-20 19:07:55 +01:00
|
|
|
|
signed = False
|
2012-02-17 17:49:51 +01:00
|
|
|
|
form[self.BANK_ID] = form.get(REFSFP)
|
2012-02-17 18:17:56 +01:00
|
|
|
|
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)
|
2012-02-17 18:17:56 +01:00
|
|
|
|
bank_status.append(status)
|
2012-02-17 17:49:51 +01:00
|
|
|
|
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)
|
2011-04-30 19:04:50 +02:00
|
|
|
|
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:
|
2012-02-17 18:17:56 +01:00
|
|
|
|
bank_status.append('invalid signature')
|
2011-04-22 17:21:16 +02:00
|
|
|
|
except ValueError:
|
2012-02-17 18:17:56 +01:00
|
|
|
|
bank_status.append('invalid signature')
|
2014-03-30 19:27:48 +02:00
|
|
|
|
|
|
|
|
|
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
|
2014-03-30 19:27:48 +02:00
|
|
|
|
elif etat in TEST_STATE:
|
|
|
|
|
result = RECEIVED # what else ?
|
|
|
|
|
test = True
|
2012-02-20 19:07:55 +01:00
|
|
|
|
else:
|
|
|
|
|
result = ERROR
|
2012-02-17 17:49:51 +01:00
|
|
|
|
|
|
|
|
|
response = PaymentResponse(
|
|
|
|
|
result=result,
|
2012-02-20 19:07:55 +01:00
|
|
|
|
signed=signed,
|
2012-02-17 17:49:51 +01:00
|
|
|
|
bank_data=form,
|
|
|
|
|
order_id=reference,
|
|
|
|
|
transaction_id=form[self.BANK_ID],
|
2012-02-17 18:17:56 +01:00
|
|
|
|
bank_status=' - '.join(bank_status),
|
2014-03-30 19:27:48 +02:00
|
|
|
|
return_content=SPCHECKOK,
|
|
|
|
|
test=test)
|
2012-02-17 17:49:51 +01:00
|
|
|
|
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'
|
2011-12-12 17:05:02 +01:00
|
|
|
|
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]))
|
2011-12-12 17:05:02 +01:00
|
|
|
|
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]))
|