First commit
This commit is contained in:
commit
7b899eea74
|
@ -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')
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
echo -ne 0!!coin
|
|
@ -0,0 +1 @@
|
|||
echo -ne xx=1!yy=2
|
|
@ -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
|
|
@ -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
|
|
@ -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')
|
||||
|
||||
|
|
@ -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')
|
||||
|
||||
|
Loading…
Reference in New Issue