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
|
|
|
|
|
import hmac
|
|
|
|
|
import hashlib
|
|
|
|
|
import urlparse
|
|
|
|
|
import urllib
|
|
|
|
|
import string
|
|
|
|
|
import datetime as dt
|
|
|
|
|
import logging
|
2011-04-30 19:04:50 +02:00
|
|
|
|
import re
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
|
|
|
|
import Crypto.Cipher.DES
|
2012-02-20 19:07:55 +01:00
|
|
|
|
from common import (PaymentCommon, URL, PaymentResponse, RECEIVED, ACCEPTED,
|
|
|
|
|
PAID, ERROR)
|
2011-04-22 17:21:16 +02:00
|
|
|
|
|
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',)
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
def sign_ntkey_query(ntkey, query):
|
|
|
|
|
key = decrypt_ntkey(ntkey)
|
|
|
|
|
data_to_sign = ''.join(y for x,y in urlparse.parse_qsl(query, True))
|
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
|
|
|
|
|
|
|
|
|
ALPHANUM = string.letters + string.digits
|
|
|
|
|
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",
|
|
|
|
|
'parameters': {
|
|
|
|
|
'cle': {
|
|
|
|
|
'caption': 'Secret key, a 40 digits hexadecimal number',
|
|
|
|
|
'regexp': re.compile('^ *((?:[a-fA-F0-9] *){40}) *$')
|
|
|
|
|
},
|
|
|
|
|
'siret': {
|
|
|
|
|
'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
|
|
|
|
},
|
|
|
|
|
'langue': {
|
|
|
|
|
'caption': 'Language of the customers',
|
|
|
|
|
'default': 'FR',
|
|
|
|
|
},
|
|
|
|
|
'taxe': {
|
|
|
|
|
'caption': 'Taxes',
|
|
|
|
|
'default': '0.00'
|
|
|
|
|
},
|
2011-12-12 17:51:09 +01:00
|
|
|
|
'modalite': {
|
|
|
|
|
'caption': '1x, 2x, 3x, xx, nx (if multiple separated by "/")',
|
|
|
|
|
'default': '1x',
|
|
|
|
|
},
|
|
|
|
|
'moyen': {
|
|
|
|
|
'caption': 'AUR, AMX, CBS, CGA, '
|
|
|
|
|
'CHK, DIN, PRE (if multiple separate by "/")',
|
|
|
|
|
'default': 'CBS',
|
|
|
|
|
},
|
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
|
|
|
|
|
2012-02-20 17:14:30 +01:00
|
|
|
|
def request(self, montant, email=None, next_url=None, logger=LOGGER):
|
|
|
|
|
logger.debug('requesting spplus payment with montant %s email=%s and \
|
2011-04-26 12:41:19 +02:00
|
|
|
|
next_url=%s' % (montant, email, next_url))
|
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,
|
|
|
|
|
'montant': str(Decimal(montant)),
|
|
|
|
|
REFERENCE: reference,
|
|
|
|
|
'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
|
|
|
|
|
if next_url:
|
|
|
|
|
if (not next_url.startswith('http://') \
|
|
|
|
|
and not next_url.startswith('https://')) \
|
|
|
|
|
or '?' in next_url:
|
|
|
|
|
raise ValueError('next_url must be an absolute URL without parameters')
|
|
|
|
|
fields['urlretour'] = next_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
|
|
|
|
|
2012-02-20 17:14:30 +01:00
|
|
|
|
def response(self, query_string, logger=LOGGER):
|
2011-04-22 17:21:16 +02:00
|
|
|
|
form = urlparse.parse_qs(query_string)
|
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 17:14:30 +01:00
|
|
|
|
logger.debug('computed signature %s' % 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')
|
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
|
|
|
|
|
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),
|
2012-02-20 19:07:55 +01:00
|
|
|
|
return_content=SPCHECKOK)
|
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:
|
2011-04-22 17:21:16 +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:
|
|
|
|
|
print sign_url_paiement(sys.argv[1], sys.argv[2])
|
|
|
|
|
print sign_ntkey_query(sys.argv[1], sys.argv[2])
|