First commit

This commit is contained in:
Benjamin Dauvergne 2011-04-22 17:21:16 +02:00
commit 7b899eea74
8 changed files with 905 additions and 0 deletions

142
eopayment/__init__.py Normal file
View File

@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
import logging
from common import URL, HTML
__all__ = [ 'Payment', 'URL', 'HTML' ]
SIPS = 'sips'
SYSTEMPAY = 'systempayv2'
SPPLUS = 'spplus'
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.
>>> import eopayment
>>> 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',
}
>>> p = Payment(kind=SPPLUS, options=spplus_options)
>>> print p.request('10.00', email='bob@example.com',
next_url='https://my-site.com')
('ZYX0NIFcbZIDuiZfazQp', 1, 'https://www.spplus.net/paiement/init.do?devise=978&validite=23%2F04%2F2011&version=1&reference=ZYX0NIFcbZIDuiZfazQp&montant=10.00&siret=00000000000001-01&langue=FR&taxe=0.00&email=bob%40example.com&hmac=b43dce98f97e5d249ef96f7f31d962f8fa5636ff')
Supported backend of French banks are:
- sips, 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
- systempay for Banque Populaire (after 2010)
For SIPs you also need the bank provided middleware especially the two
executables, request and response, as the protocol from ATOS/SIPS is not
documented. For the other backends the modules are autonomous.
'''
def __init__(self, kind, options):
self.kind = kind
module = __import__(kind)
self.backend = module.Payment(options)
def request(self, amount, email=None, next_url=None):
'''Request a payment to the payment backend.
Arguments:
amount -- the amount of money to ask
email -- the email of the customer (optional)
next_url -- the URL where the customer will be returned (optional),
usually redundant with the hardwired settings in the bank
configuration panel. At this url you must use the Payment.response
method to analyze the bank returned values.
It returns a triple of values, (transaction_id, kind, data):
- the first gives a string value to later match the payment with
the invoice,
- kind gives the type of the third value, payment.URL or
payment.HTML,
- the third is the URL or the HTML form to contact the payment
server, which must be sent to the customer browser.
kind of the third argument, it can be URL or HTML, the third is the
corresponding value as string containing HTML or an URL
>>> transaction_id, kind, data = processor.request('100.00')
>>> # asociate transaction_id to invoice
>>> invoice.add_transaction_id(transaction_id)
>>> if kind == eopayment.URL:
# redirect the user to the URL in data
elif kind == eopayment.HTML:
# present the form in HTML to the user
'''
return self.backend.request(amount, email=email)
def response(self, query_string):
'''
Process a response from the Bank API. It must be used on the URL
where the user browser of the payment server is going to post the
result of the payment. Beware it can happen multiple times for the
same payment, so you MUST support multiple notification of the same
event, i.e. it should be idempotent. For example if you already
validated some invoice, receiving a new payment notification for the
same invoice should alter this state change.
Beware that when notified directly by the bank (and not through the
customer browser) no applicative session will exist, so you should
not depend on it in your handler.
Arguments:
query_string -- the URL encoded form-data from a GET or a POST
It returns a quadruplet of values:
(result, transaction_id, bank_data, return_content)
- result is a boolean stating whether the transaction worked, use it
to decide whether to act on a valid payment,
- the transaction_id return the same id than returned by request
when requesting for the payment, use it to find the invoice or
transaction which is linked to the payment,
- bank_data is a dictionnary of the data sent by the bank, it should
be logged for security reasons,
- return_content, if not None you must return this content as the
result of the HTTP request, it's used when the bank is calling
your site as a web service.
'''
return self.backend.response(query_string)
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
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',
}
p = Payment(kind=SPPLUS, options=spplus_options)
print p.request('10.00', email='bob@example.com',
next_url='https://my-site.com')
systempay_options = {
'secrets': {
'TEST': '1234567890123456',
'PRODUCTION': 'yyy'
},
'site_id': '00001234',
'ctx_mode': 'PRODUCTION'
}
p = Payment(SYSTEMPAY, systempay_options)
print p.request('10.00', email='bob@example.com',
next_url='https://my-site.com')
sips_options = { 'filepath': '/', 'binpath': './' }
p = Payment(kind=SIPS, options=sips_options)
print p.request('10.00', email='bob@example.com',
next_url='https://my-site.com')

28
eopayment/common.py Normal file
View File

@ -0,0 +1,28 @@
import os.path
import os
import random
from datetime import date
__all__ = [ 'PaymentCommon', 'URL', 'HTML', 'RANDOM' ]
RANDOM = random.SystemRandom()
URL = 1
HTML = 2
class PaymentCommon(object):
PATH = '/tmp'
def transaction_id(self, length, choices, *prefixes):
while True:
parts = [RANDOM.choice(choices) for x in range(length)]
id = ''.join(parts)
name = '%s_%s_%s' % (str(date.today()), '-'.join(prefixes), str(id))
try:
fd=os.open(os.path.join(self.PATH, name), os.O_CREAT|os.O_EXCL)
except:
raise
else:
os.close(fd)
return id

2
eopayment/request Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
echo -ne 0!!coin

1
eopayment/response Executable file
View File

@ -0,0 +1 @@
echo -ne xx=1!yy=2

95
eopayment/sips.py Normal file
View File

@ -0,0 +1,95 @@
import urlparse
import string
import subprocess
from decimal import Decimal
import logging
import os
import os.path
from common import PaymentCommon, HTML
'''
Payment backend module for the ATOS/SIPS system used by many Frenck banks.
It use the middleware given by the bank.
The necessary options are:
- pathfile, to indicate the absolute path of the pathfile file given by the
bank,
- binpath, the path of the directory containing the request and response
executables,
All the other needed parameters SHOULD already be set in the parmcom files
contained in the middleware distribution file.
'''
__all__ = [ 'Payment' ]
BINPATH = 'binpath'
PATHFILE = 'pathfile'
REQUEST_VALID_PARAMS = ['merchant_id', 'merchant_country', 'amount',
'currency_code', 'pathfile', 'normal_return_url', 'cancel_return_url',
'automatic_response_url', 'language', 'payment_means', 'header_flag',
'capture_day', 'capture_mode', 'bgcolor', 'block_align', 'block_order',
'textcolor', 'receipt_complement', 'caddie', 'customer_id', 'customer_email',
'customer_ip_address', 'data', 'return_context', 'target', 'order_id']
DATA = 'DATA'
PARAMS = 'params'
TRANSACTION_ID = 'transaction_id'
MERCHANT_ID = 'merchant_id'
RESPONSE_CODE = 'response_code'
DEFAULT_PARAMS = { 'merchant_id': '014213245611111',
'merchant_country': 'fr',
'currency_code': '978' }
LOGGER = logging.getLogger(__name__)
class Payment(PaymentCommon):
def __init__(self, options):
self.options = options
LOGGER.debug('initializing sips payment class with %s' % options)
def execute(self, executable, params):
if PATHFILE in self.options:
params[PATHFILE] = self.options[PATHFILE]
executable = os.path.join(self.options[BINPATH], executable)
args = [executable] + [ "%s=%s" % p for p in params.iteritems() ]
LOGGER.debug('executing %s' % args)
result, _ = subprocess.Popen(args, executable=executable,
stdout=subprocess.PIPE, shell=True).communicate()
result = result.split('!')
LOGGER.debug('got response %s' % result)
return result
def get_request_params(self):
params = DEFAULT_PARAMS.copy()
params.update(self.options.get(PARAMS, {}))
return params
def request(self, amount, email=None, next_url=None):
params = self.get_request_params()
transaction_id = self.transaction_id(6, string.digits, 'sips',
params[MERCHANT_ID])
params[TRANSACTION_ID] = transaction_id
params['amount'] = str(Decimal(amount)*100)
if email:
params['customer_email'] = email
if next_url:
params['normal_return_url'] = next_url
code, error, form = self.execute('request', params)
if int(code) == 0:
return transaction_id, HTML, form
else:
raise RuntimeError('sips/request returned -1: %s' % error)
def response(self, query_string):
form = urlparse.parse_qs(query_string)
params = {'message': form[DATA]}
result = self.execute('response', params)
d = dict([p.split('=',1) for p in result])
LOGGER.debug('response contains fields %s' % d)
return result.get(RESPONSE_CODE) == '00', form.get(TRANSACTION_ID), d

115
eopayment/spplus.py Normal file
View File

@ -0,0 +1,115 @@
from decimal import Decimal
import binascii
import hmac
import hashlib
import urlparse
import urllib
import string
import datetime as dt
import logging
import Crypto.Cipher.DES
from common import PaymentCommon, URL
KEY_DES_KEY = '\x45\x1f\xba\x4f\x4c\x3f\xd4\x97'
IV = '\x30\x78\x30\x62\x2c\x30\x78\x30'
REFERENCE = 'reference'
ETAT = 'etat'
ETAT_PAIEMENT_ACCEPTE = '1'
SPCHECKOK = 'spcheckok'
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))
return hmac.new(key[:20], data_to_sign, hashlib.sha1).hexdigest()
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()
ALPHANUM = string.letters + string.digits
SERVICE_URL = "https://www.spplus.net/paiement/init.do"
LOGGER = logging.getLogger(__name__)
class Payment(PaymentCommon):
def __init__(self, options):
self.cle = options['cle']
self.siret = options['siret']
self.devise = '978'
self.langue = options.get('langue', 'FR')
self.taxe = options.get('taxe', '0.00')
def request(self, montant, email=None, next_url=None):
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,
'version': '1'}
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
query = urllib.urlencode(fields)
return reference, URL, '%s?%s&hmac=%s' % (SERVICE_URL, query,
sign_ntkey_query(self.cle, query))
def response(self, query_string):
form = urlparse.parse_qs(query_string)
LOGGER.debug('received query_string %s' % query_string)
LOGGER.debug('parsed as %s' % form)
reference = form.get(REFERENCE)
if not 'hmac' in form:
return form.get('etat') == 1, reference, form, None
else:
try:
signed_data, signature = query_string.rsplit('&', 1)
_, hmac = signature.split('=', 1)
LOGGER.debug('got signature %s' % hmac)
computed_hmac = sign_ntkey_query(self.clem, signed_data)
LOGGER.debug('computed signature %s' % hmac)
result = hmac==computed_hmac \
and reference.get(ETAT) == ETAT_PAIEMENT_ACCEPTE
return result, reference, form, SPCHECKOK
except ValueError:
return False, reference, form, SPCHECKOK
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'
payment = Payment({'cle': ntkey, 'siret': '00000000000001-01'})
print payment.request(10)
if len(sys.argv) > 1:
print sign_url_paiement(ntkey, sys.argv[1])
print sign_ntkey_query(ntkey, sys.argv[1])
else:
tests = [('x=coin', 'c04f8266d6ae3ce37551cce996c751be4a95d10a'),
('x=coin&y=toto', 'ef008e02f8dbf5e70e83da416b0b3a345db203de')]
for query, result in tests:
assert sign_ntkey_query(ntkey, query) == result

240
eopayment/systempayv1.py Normal file
View File

@ -0,0 +1,240 @@
# -*- coding: utf-8 -*-
import datetime as dt
import hashlib
import logging
__all__ = ['Payment']
PAYMENT_URL = "https://systempay.cyberpluspaiement.com/vads-payment/"
def isonow():
return dt.datetime.now() \
.isoformat('T') \
.replace('-','') \
.replace('T','') \
.replace(':','')[:14]
logger = logging.getLogger(__name__)
class Parameter:
def __init__(self, name, ptype, code, max_length=None, length=None, needed=False,
sign=False, default=None, choices=None):
self.name = name
self.ptype = ptype
self.code = code
self.max_length = max_length
self.length = length
self.needed = needed
self.sign = sign
self.default = default
self.choices = choices
def check_value(self, value):
if self.length and len(str(value)) != self.length:
return False
if self.max_length and len(str(value)) > self.max_length:
return False
if value == '':
return True
if self.ptype == 'n':
return str(value).isdigit()
elif self.ptype == 'an':
return str(value).isalnum()
elif self.ptype == 'an-':
return str(value).replace('-','').isalnum()
elif self.ptype == 'an;':
return str(value).replace(';','').isalnum()
# elif self.ptype == 'ans':
return True
PARAMETERS = [
# amount as euro cents
Parameter('amount', 'n', 9, max_length=12, needed=True, sign=True),
Parameter('capture_delay', 'n', 6, max_length=3, needed=True,
sign=True, default=''),
Parameter('contrib', 'ans', 31, max_length=255, default='eopayment'),
# defaut currency = EURO, norme ISO4217
Parameter('currency', 'n', 10, length=3, default=978, needed=True,
sign=True),
Parameter('cust_address', 'an', 19, max_length=255),
Parameter('cust_country', 'a', 22, length=2, default='FR'),
Parameter('cust_email', 'an', 15, max_length=127),
Parameter('cust_id', 'an', 16, max_length=63),
Parameter('cust_name', 'an', 18, max_length=127),
Parameter('cust_phone', 'an', 23, max_length=63),
Parameter('cust_title', 'an', 17, max_length=63),
Parameter('cust_city', 'an', 21, max_length=63),
Parameter('cust_zip', 'an', 20, max_length=63),
# must be TEST or PRODUCTION
Parameter('ctx_mode', 'a', 11, needed=True, sign=True, default='TEST'),
# ISO 639 code
Parameter('language', 'a', 12, length=2, default='fr'),
Parameter('order_id', 'an-', 13, max_length=32),
Parameter('order_info', 'an', 14, max_length=255),
Parameter('order_info2', 'an', 14, max_length=255),
Parameter('order_info3', 'an', 14, max_length=255),
Parameter('payment_cards', 'an;', 8, max_length=127, default='',
needed=True, sign=True),
# must be SINGLE or MULTI with parameters
Parameter('payment_config', '', 07, default='SINGLE',
choices=('SINGLE','MULTI'), needed=True, sign=True),
Parameter('payment_src', 'a', 60, max_length=5, default='',
choices=('', 'BO', 'MOTO', 'CC', 'OTHER')),
Parameter('signature', 'an', None, length=40),
Parameter('site_id', 'n', 02, length=8, needed=True, sign=True),
Parameter('theme_config', 'ans', 32, max_length=255),
Parameter('trans_date', 'n', 04, length=14, needed=True, sign=True,
default=isonow),
Parameter('trans_id', 'n', 03, length=6, needed=True, sign=True),
Parameter('validation_mode', 'n', 5, max_length=1, choices=('', 0, 1),
needed=True, sign=True, default=''),
Parameter('version', 'an', 01, default='V1', needed=True, sign=True),
Parameter('url_success', 'ans', 24, max_length=127),
Parameter('url_referral', 'ans', 26, max_length=127),
Parameter('url_refused', 'ans', 25, max_length=127),
Parameter('url_cancel', 'ans', 27, max_length=127),
Parameter('url_error', 'ans', 29, max_length=127),
Parameter('url_return', 'ans', 28, max_length=127),
Parameter('user_info', 'ans', 61, max_length=255),
Parameter('contracts', 'ans', 62, max_length=255),
]
AUTH_RESULT_MAP = {
'00': "transaction approuvée ou traitée avec succés",
'02': "contacter l'émetteur de la carte",
'03': "accepteur invalid",
'04': "conserver la carte",
'05': "ne pas honorer",
'07': "conserver la carte, conditions spéciales",
'08': "approuver aprés identification",
'12': "transaction invalide",
'13': "montant invalide",
'14': "numéro de porteur invalide",
'30': "erreur de format",
'31': "identifiant de l'organisme acquéreur inconnu",
'33': "date de validité de la carte dépassée",
'34': "suspicion de fraude",
'41': "carte perdue",
'43': "carte volée",
'51': "provision insuffisante",
'54': "date de validité de la carte dépassée",
'56': "carte absente du fichier",
'57': "transaction non permise à ce porteur",
'58': "transaction interdite au terminal",
'59': "suspicion de fraude",
'60': "l'accepteur de carte doit contacter l'acquéreur",
'61': "montant de retrait hors limite",
'63': "règles de sécurité non respectée",
'68': "réponse non parvenu ou réçu trop tard",
'90': "arrêt momentané du système",
'91': "émetteur de carte inacessible",
'96': "mauvais fonctionnement du système",
'94': "transaction dupliquée",
'97': "échéance de la temporisation de surveillance globale",
'98': "serveur indisponible routage réseau demandé à nouveau",
'99': "incident domain initiateur",
}
RESULT_MAP = {
'00': 'paiement réalisé avec succés',
'02': 'le commerçant doit contacter la banque du porteur',
'05': 'paiement refusé',
'17': 'annulation client',
'30': 'erreur de format',
'96': 'erreur technique lors du paiement'
}
EXTRA_RESULT_MAP = {
'': "Pas de contrôle effectué",
'00': "Tous les contrôles se sont déroulés avec succés",
'02': "La carte a dépassé l'encours autorisé",
'03': "La carte appartient à la liste grise du commerçant",
'04': "Le pays d'émission de la carte appartient à la liste grise du \
commerçant ou le pays d'émission de la carte n'appartient pas à la \
liste blanche du commerçant",
'05': "L'addresse IP appartient à la liste grise du commerçant",
'99': "Problème technique recontré par le serveur lors du traitement \
d'un des contrôles locaux",
}
REQUEST_SIGNATURE_FIELDS = ['version', 'site_id', 'ctx_mode', 'trans_id', 'trans_date',
'validation_mode', 'capture_delay', 'payment_config', 'payment_cards',
'amount', 'currency']
RESPONSE_SIGNATURE_FIELDS = ['version', 'site_id', 'ctx_mode', 'trans_id',
'trans_date', 'validation_mode', 'capture_delay', 'payment_config',
'card_brand', 'card_number', 'amount', 'currency', 'auth_mode', 'auth_result',
'auth_number', 'warranty_result', 'payment_certificate', 'result' ]
S2S_RESPONSE_SIGNATURE_FIELDS = RESPONSE_SIGNATURE_FIELDS + [ 'hash' ]
class Payment:
'''
ex.: Payment(secrets={'TEST': 'xxx', 'PRODUCTION': 'yyyy'}, site_id=123,
ctx_mode='PRODUCTION')
'''
def __init__(self, **kwargs):
self.options = kwargs
def request(self, amount, **kwargs):
'''
Create a dictionnary to send a payment request to systempay the
Credit Card payment server of the NATIXIS group
'''
if not isinstance(amount, int) or amount < 0:
raise TypeError('amount must be an integerer >= 0')
fields = { 'amount': amount }
fields.update(kwargs)
for parameter in PARAMETERS:
name = parameter.name
# import default parameters from configuration
if name not in fields \
and name in self.options:
fields[name] = self.options[name]
# import default parameters from module
if name not in fields and parameter.default is not None:
if callable(parameter.default):
fields[name] = parameter.default()
else:
fields[name] = parameter.default
# raise error if needed parameters are absent
if name not in fields and parameter.needed:
raise ValueError('payment request is missing the %s parameter,\
parameters received: %s' % (name, kwargs))
if name in fields \
and not parameter.check_value(fields[name]):
raise TypeError('%s value %s is not of the type %s' % (
name, fields[name],
parameter.ptype))
fields['signature'] = self.signature(fields, REQUEST_SIGNATURE_FIELDS)
return fields
def check_response(self, fields):
signature = self.signature(fields, RESPONSE_SIGNATURE_FIELDS)
return signature == fields['signature']
def check_s2s_response(self, fields):
signature = self.signature(fields, S2S_RESPONSE_SIGNATURE_FIELDS)
return signature == fields['signature']
def signature(self, fields, fields_to_sign):
logging.debug('got fields %s to sign' % fields)
secret = self.options['secrets'][fields['ctx_mode']]
ordered_fields = [ str(fields[field]) for field in fields_to_sign]
signed_data = '+'.join(ordered_fields)
logger.debug('generating signature on «%s»' % signed_data)
sign = hashlib.sha1('%s+%s' % (signed_data, secret)).hexdigest()
logger.debug('signature «%s»' % sign)
return sign
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
p = Payment(secrets={'TEST': '1234567890123456', 'PRODUCTION': 'yyy'}, site_id='00001234', ctx_mode='PRODUCTION')
print p.request(100, ctx_mode='TEST', site_id='12345678',
trans_date='20090324122302', trans_id='122302',
url_return='http://url.de.retour/retour.php')

282
eopayment/systempayv2.py Normal file
View File

@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
import datetime as dt
import hashlib
import logging
import string
import urlparse
import urllib
from decimal import Decimal
from common import PaymentCommon, URL
__all__ = ['Payment']
PAYMENT_URL = "https://systempay.cyberpluspaiement.com/vads-payment/"
LOGGER = logging.getLogger(__name__)
SERVICE_URL = '???'
def isonow():
return dt.datetime.now() \
.isoformat('T') \
.replace('-','') \
.replace('T','') \
.replace(':','')[:14]
class Parameter:
def __init__(self, name, ptype, code, max_length=None, length=None, needed=False,
default=None, choices=None):
self.name = name
self.ptype = ptype
self.code = code
self.max_length = max_length
self.length = length
self.needed = needed
self.default = default
self.choices = choices
def check_value(self, value):
if self.length and len(str(value)) != self.length:
return False
if self.max_length and len(str(value)) > self.max_length:
return False
if self.choices and str(value) not in self.choices:
return False
if value == '':
return True
value = str(value).replace('.','')
if self.ptype == 'n':
return value.isdigit()
elif self.ptype == 'an':
return value.isalnum()
elif self.ptype == 'an-':
return value.replace('-','').isalnum()
elif self.ptype == 'an;':
return value.replace(';','').isalnum()
elif self.ptype == 'an@':
return value.replace('@','').isalnum()
# elif self.ptype == 'ans':
return True
PARAMETERS = [
# amount as euro cents
Parameter('vads_action_mode', None, 47, needed=True,
default='INTERACTIVE', choices=('SILENT','INTERACTIVE')),
Parameter('vads_amount', 'n', 9, max_length=12, needed=True),
Parameter('vads_capture_delay', 'n', 6, max_length=3, default=''),
Parameter('vads_contrib', 'ans', 31, max_length=255, default='eopayment'),
# defaut currency = EURO, norme ISO4217
Parameter('vads_currency', 'n', 10, length=3, default=978, needed=True),
Parameter('vads_cust_address', 'an', 19, max_length=255),
# code ISO 3166
Parameter('vads_cust_country', 'a', 22, length=2, default='FR'),
Parameter('vads_cust_email', 'an@', 15, max_length=127),
Parameter('vads_cust_id', 'an', 16, max_length=63),
Parameter('vads_cust_name', 'an', 18, max_length=127),
Parameter('vads_cust_phone', 'an', 23, max_length=63),
Parameter('vads_cust_title', 'an', 17, max_length=63),
Parameter('vads_cust_city', 'an', 21, max_length=63),
Parameter('vads_cust_zip', 'an', 20, max_length=63),
# must be TEST or PRODUCTION
Parameter('vads_ctx_mode', 'a', 11, needed=True),
# ISO 639 code
Parameter('vads_language', 'a', 12, length=2, default='fr'),
Parameter('vads_order_id', 'an-', 13, max_length=32),
Parameter('vads_order_info', 'an', 14, max_length=255),
Parameter('vads_order_info2', 'an', 14, max_length=255),
Parameter('vads_order_info3', 'an', 14, max_length=255),
Parameter('vads_page_action', None, 46, needed=True, default='PAYMENT',
choices=('PAYMENT',)),
Parameter('vads_payment_cards', 'an;', 8, max_length=127, default=''),
# must be SINGLE or MULTI with parameters
Parameter('vads_payment_config', '', 07, default='SINGLE',
choices=('SINGLE','MULTI'), needed=True),
Parameter('vads_return_mode', None, 48, default='NONE',
choices=('','NONE','POST','GET')),
Parameter('signature', 'an', None, length=40),
Parameter('vads_site_id', 'n', 02, length=8, needed=True),
Parameter('vads_theme_config', 'ans', 32, max_length=255),
Parameter('vads_trans_date', 'n', 04, length=14, needed=True,
default=isonow),
Parameter('vads_trans_id', 'n', 03, length=6, needed=True),
Parameter('vads_validation_mode', 'n', 5, max_length=1, choices=('', 0, 1),
default=''),
Parameter('vads_version', 'an', 01, default='V2', needed=True,
choices=('V2',)),
Parameter('vads_url_success', 'ans', 24, max_length=127),
Parameter('vads_url_referral', 'ans', 26, max_length=127),
Parameter('vads_url_refused', 'ans', 25, max_length=127),
Parameter('vads_url_cancel', 'ans', 27, max_length=127),
Parameter('vads_url_error', 'ans', 29, max_length=127),
Parameter('vads_url_return', 'ans', 28, max_length=127),
Parameter('vads_user_info', 'ans', 61, max_length=255),
Parameter('vads_contracts', 'ans', 62, max_length=255),
]
AUTH_RESULT_MAP = {
'00': "transaction approuvée ou traitée avec succés",
'02': "contacter l'émetteur de la carte",
'03': "accepteur invalid",
'04': "conserver la carte",
'05': "ne pas honorer",
'07': "conserver la carte, conditions spéciales",
'08': "approuver aprés identification",
'12': "transaction invalide",
'13': "montant invalide",
'14': "numéro de porteur invalide",
'30': "erreur de format",
'31': "identifiant de l'organisme acquéreur inconnu",
'33': "date de validité de la carte dépassée",
'34': "suspicion de fraude",
'41': "carte perdue",
'43': "carte volée",
'51': "provision insuffisante",
'54': "date de validité de la carte dépassée",
'56': "carte absente du fichier",
'57': "transaction non permise à ce porteur",
'58': "transaction interdite au terminal",
'59': "suspicion de fraude",
'60': "l'accepteur de carte doit contacter l'acquéreur",
'61': "montant de retrait hors limite",
'63': "règles de sécurité non respectée",
'68': "réponse non parvenu ou réçu trop tard",
'90': "arrêt momentané du système",
'91': "émetteur de carte inacessible",
'96': "mauvais fonctionnement du système",
'94': "transaction dupliquée",
'97': "échéance de la temporisation de surveillance globale",
'98': "serveur indisponible routage réseau demandé à nouveau",
'99': "incident domain initiateur",
}
RESULT_MAP = {
'00': 'paiement réalisé avec succés',
'02': 'le commerçant doit contacter la banque du porteur',
'05': 'paiement refusé',
'17': 'annulation client',
'30': 'erreur de format',
'96': 'erreur technique lors du paiement'
}
EXTRA_RESULT_MAP = {
'': "Pas de contrôle effectué",
'00': "Tous les contrôles se sont déroulés avec succés",
'02': "La carte a dépassé l'encours autorisé",
'03': "La carte appartient à la liste grise du commerçant",
'04': "Le pays d'émission de la carte appartient à la liste grise du \
commerçant ou le pays d'émission de la carte n'appartient pas à la \
liste blanche du commerçant",
'05': "L'addresse IP appartient à la liste grise du commerçant",
'99': "Problème technique recontré par le serveur lors du traitement \
d'un des contrôles locaux",
}
def add_vads(kwargs):
new_vargs={}
for k, v in kwargs.iteritems():
if k.startswith('vads_'):
new_vargs[k] = v
else:
new_vargs['vads_'+k] = v
return new_vargs
class Payment(PaymentCommon):
'''
ex.: Payment(secrets={'TEST': 'xxx', 'PRODUCTION': 'yyyy'}, site_id=123,
ctx_mode='PRODUCTION')
'''
def __init__(self, options):
self.secrets = options.pop('secrets')
options = add_vads(options)
self.options = options
def request(self, amount, email=None, next_url=None):
'''
Create a dictionary to send a payment request to systempay the
Credit Card payment server of the NATIXIS group
'''
kwargs = add_vads({'amount': amount})
if Decimal(kwargs['vads_amount']) < 0:
raise TypeError('amount must be an integer >= 0')
if email:
kwargs['vads_cust_email'] = email
if next_url:
kwargs['vads_url_return'] = next_url
transaction_id = self.transaction_id(6, string.digits,
'systempay', self.options['vads_site_id'])
kwargs['vads_trans_id'] = transaction_id
fields = kwargs
for parameter in PARAMETERS:
name = parameter.name
# import default parameters from configuration
if name not in fields \
and name in self.options:
fields[name] = self.options[name]
# import default parameters from module
if name not in fields and parameter.default is not None:
if callable(parameter.default):
fields[name] = parameter.default()
else:
fields[name] = parameter.default
# raise error if needed parameters are absent
if name not in fields and parameter.needed:
raise ValueError('payment request is missing the %s parameter,\
parameters received: %s' % (name, kwargs))
if name in fields \
and not parameter.check_value(fields[name]):
raise TypeError('%s value %s is not of the type %s' % (
name, fields[name],
parameter.ptype))
fields['signature'] = self.signature(fields)
url = '%s?%s' % (SERVICE_URL, urllib.urlencode(fields))
return transaction_id, URL, fields
def response(self, query_string):
fields = urlparse.parse_qs(query_string)
copy = fields.copy()
if 'vads_auth_result' in fields:
v = copy['vads_auth_result']
copy['vads_auth_result'] = '%s: %s' % (v, AUTH_RESULT_MAP.get(v, 'Code inconnu'))
if 'vads_result' in copy:
v = copy['vads_result']
copy['vads_result'] = '%s: %s' % (v, RESULT_MAP.get(v, 'Code inconnu'))
if v == '30':
if 'vads_extra_result' in fields:
v = fields['vads_extra_result']
if v.isdigit():
for parameter in PARAMETERS:
if int(v) == parameter.code:
fields['vads_extra_result'] = 'erreur dans le champ %s' % parameter.name
elif v in ('05', '00'):
v = fields['vads_extra_result']
fields['vads_extra_result'] = '%s: %s' % (v, EXTRA_RESULT_MAP.get(v, 'Code inconnu'))
LOGGER.debug('checking systempay response on:')
for key in sorted(fields.keys):
LOGGER.debug(' %s: %s' % (key, copy[key]))
signature = self.signature(fields)
result = signature == fields['signature']
LOGGER.debug('signature check result: %s' % result)
return result
def signature(self, fields):
LOGGER.debug('got fields %s to sign' % fields )
ordered_keys = sorted([ key for key in fields.keys() if key.startswith('vads_') ])
LOGGER.debug('ordered keys %s' % ordered_keys)
ordered_fields = [ str(fields[key]) for key in ordered_keys ]
secret = self.secrets[fields['vads_ctx_mode']]
signed_data = '+'.join(ordered_fields)
LOGGER.debug('generating signature on «%s»' % signed_data)
sign = hashlib.sha1('%s+%s' % (signed_data, secret)).hexdigest()
LOGGER.debug('signature «%s»' % sign)
return sign
if __name__ == '__main__':
p = Payment(secrets={'TEST': '1234567890123456', 'PRODUCTION': 'yyy'}, site_id='00001234', ctx_mode='PRODUCTION')
print p.request(amount=100, ctx_mode='TEST', site_id='12345678',
trans_date='20090324122302', trans_id='122302',
url_return='http://url.de.retour/retour.php')