Compare commits
52 Commits
Author | SHA1 | Date |
---|---|---|
Benjamin Dauvergne | e5d5cb8881 | |
Benjamin Dauvergne | 4ae04f39c4 | |
Benjamin Dauvergne | e46ac82192 | |
raphigaziano | ef65a604f7 | |
Benjamin Dauvergne | 1645205966 | |
Benjamin Dauvergne | edf7064335 | |
Benjamin Dauvergne | 4d3e995e09 | |
Benjamin Dauvergne | 5a9eb7fa97 | |
Benjamin Dauvergne | 935c5d9367 | |
Benjamin Dauvergne | 1bdb3a8c88 | |
Benjamin Dauvergne | 0020cb511a | |
Benjamin Dauvergne | 236753e8b3 | |
Benjamin Dauvergne | db99c7bd66 | |
Benjamin Dauvergne | 412e924c60 | |
Benjamin Dauvergne | ed976c1deb | |
Benjamin Dauvergne | ef6a3a63b2 | |
Benjamin Dauvergne | 07b7c553d2 | |
Benjamin Dauvergne | 5b211a0a93 | |
Benjamin Dauvergne | ef6fc6c9c1 | |
Benjamin Dauvergne | a244c4fec1 | |
Benjamin Dauvergne | 65b9a89c1f | |
Benjamin Dauvergne | 1da33a4d00 | |
Benjamin Dauvergne | b011c0c18d | |
Benjamin Dauvergne | 6041c7593d | |
Benjamin Dauvergne | 10251b2b4c | |
Benjamin Dauvergne | 079181a3e8 | |
Benjamin Dauvergne | da3b05a6fb | |
Benjamin Dauvergne | 29c8cf03c7 | |
Benjamin Dauvergne | 6352e7afff | |
Mathieu Comandon | 6865acfbcb | |
Mathieu Comandon | 3e0a86c918 | |
Mathieu Comandon | 2bf9560737 | |
Mathieu Comandon | 7b231b5ed3 | |
Mathieu Comandon | e5f644101d | |
Mathieu Comandon | fb9e19d11d | |
Mathieu Comandon | 7c76f2b121 | |
Mathieu Comandon | 9b9112d5d7 | |
Mathieu Comandon | 90f03878f2 | |
Mathieu Comandon | f551aca200 | |
Mathieu Comandon | 2ce287cf97 | |
Benjamin Dauvergne | 277e53d7c4 | |
Benjamin Dauvergne | 1c3d1e3a62 | |
Benjamin Dauvergne | c072f3a4ab | |
Benjamin Dauvergne | 3b6da13deb | |
Benjamin Dauvergne | c39ef64c72 | |
Benjamin Dauvergne | 7d9cd8a708 | |
Benjamin Dauvergne | ae609803ee | |
Benjamin Dauvergne | 72ee3ea663 | |
Benjamin Dauvergne | 311b71e4e6 | |
Benjamin Dauvergne | 8da8b3a341 | |
Benjamin Dauvergne | cdba3db82a | |
Benjamin Dauvergne | b8c6cff7bb |
|
@ -0,0 +1,2 @@
|
|||
recursive-include debian *
|
||||
recursive-include tests *.py
|
|
@ -1,40 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from common import URL, HTML
|
||||
|
||||
__all__ = [ 'Payment', 'URL', 'HTML', '__version__', 'SIPS', 'SYSTEMPAY',
|
||||
'SPPLUS', 'DUMMY', 'get_backend' ]
|
||||
__all__ = ['Payment', 'URL', 'HTML', '__version__', 'SIPS', 'SYSTEMPAY',
|
||||
'SPPLUS', 'DUMMY', 'get_backend', 'get_backends']
|
||||
|
||||
__version__ = "0.0.10"
|
||||
__version__ = "0.0.22"
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SIPS = 'sips'
|
||||
SYSTEMPAY = 'systempayv2'
|
||||
SPPLUS = 'spplus'
|
||||
DUMMY = 'dummy'
|
||||
|
||||
|
||||
def get_backend(kind):
|
||||
'''Resolve a backend name into a module object'''
|
||||
module = __import__(kind, globals(), locals(), [])
|
||||
return module.Payment
|
||||
|
||||
__BACKENDS = [ DUMMY, SIPS, SYSTEMPAY, SPPLUS ]
|
||||
|
||||
def get_backends():
|
||||
'''Return a dictionnary mapping existing eopayment backends name to their
|
||||
description.
|
||||
|
||||
>>> get_backends()['dummy'].description['caption']
|
||||
'Dummy payment backend'
|
||||
|
||||
'''
|
||||
return {backend: get_backend(backend) for backend in __BACKENDS}
|
||||
|
||||
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.
|
||||
|
||||
>>> from eopayment import Payment, SPPLUS
|
||||
>>> 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',
|
||||
>>> 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')
|
||||
>>> transaction_id, kind, data = p.request('10.00', email='bob@example.com', \
|
||||
next_url='https://my-site.com')
|
||||
>>> print (transaction_id, kind, data) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
||||
('...', 1, 'https://www.spplus.net/paiement/init.do?...')
|
||||
|
||||
Supported backend of French banks are:
|
||||
|
||||
|
@ -52,28 +66,22 @@ class Payment(object):
|
|||
description of the backend list those parameters. The description
|
||||
dictionary can be used to generate configuration forms.
|
||||
|
||||
>>> d = eopayment.get_backend(SPPLUS).description
|
||||
>>> d = get_backend(SPPLUS).description
|
||||
>>> print d['caption']
|
||||
SSPPlus payment service of French bank Caisse d'epargne
|
||||
>>> print d['parameters'].keys()
|
||||
('cle','siret')
|
||||
>>> print d['parameters']['cle']['caption']
|
||||
Secret Key
|
||||
SPPlus payment service of French bank Caisse d'epargne
|
||||
>>> print [p['name'] for p in d['parameters']] # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
||||
['cle', ..., 'moyen']
|
||||
>>> print d['parameters'][0]['caption']
|
||||
Secret key, a 40 digits hexadecimal number
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, kind, options, logger=None):
|
||||
def __init__(self, kind, options, logger=LOGGER):
|
||||
self.logger = logger
|
||||
self.kind = kind
|
||||
self.backend = get_backend(kind)(options, **self.__get_extra_args())
|
||||
self.backend = get_backend(kind)(options, logger=logger)
|
||||
|
||||
def __get_extra_args(self):
|
||||
if self.logger:
|
||||
return { 'logger': self.logger }
|
||||
else:
|
||||
return {}
|
||||
|
||||
def request(self, amount, email=None, next_url=None):
|
||||
def request(self, amount, **kwargs):
|
||||
'''Request a payment to the payment backend.
|
||||
|
||||
Arguments:
|
||||
|
@ -88,24 +96,14 @@ class Payment(object):
|
|||
- 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,
|
||||
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, next_url=next_url,
|
||||
**self.__get_extra_args())
|
||||
return self.backend.request(amount, **kwargs)
|
||||
|
||||
def response(self, query_string):
|
||||
'''
|
||||
|
@ -125,7 +123,7 @@ class Payment(object):
|
|||
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
|
||||
|
@ -140,32 +138,4 @@ class Payment(object):
|
|||
your site as a web service.
|
||||
|
||||
'''
|
||||
return self.backend.response(query_string, **self.__get_extra_args())
|
||||
|
||||
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': os.path.dirname(__file__) }
|
||||
p = Payment(kind=SIPS, options=sips_options)
|
||||
print p.request('10.00', email='bob@example.com',
|
||||
next_url='https://my-site.com')
|
||||
return self.backend.response(query_string)
|
||||
|
|
|
@ -4,8 +4,8 @@ import random
|
|||
import logging
|
||||
from datetime import date
|
||||
|
||||
__all__ = [ 'PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED',
|
||||
'PAID', 'ERROR' ]
|
||||
__all__ = ['PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED',
|
||||
'PAID', 'ERROR']
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
@ -17,17 +17,18 @@ HTML = 2
|
|||
RECEIVED = 1
|
||||
ACCEPTED = 2
|
||||
PAID = 3
|
||||
DENIED = 4
|
||||
CANCELED = 5
|
||||
ERROR = 99
|
||||
|
||||
|
||||
class PaymentResponse(object):
|
||||
'''Holds a generic view on the result of payment transaction response.
|
||||
|
||||
result -- holds the declarative result of the transaction, does not use
|
||||
it to validate the payment in your backoffice, it's just for informing
|
||||
the user that all is well.
|
||||
signed_result -- holds the signed result of the transaction, when it is
|
||||
not None, it contains the result of the transaction as asserted by the
|
||||
bank with an electronic signature.
|
||||
signed -- holds whether the message was signed
|
||||
bank_data -- a dictionnary containing some data depending on the bank,
|
||||
you have to log it for audit purpose.
|
||||
return_content -- when handling a response in a callback endpoint, i.e.
|
||||
|
@ -73,8 +74,10 @@ class PaymentCommon(object):
|
|||
BANK_ID = '__bank_id'
|
||||
|
||||
def __init__(self, options, logger=LOGGER):
|
||||
self.logger = logger
|
||||
logger.debug('initializing with options %s' % options)
|
||||
for key, value in self.description['parameters'].iteritems():
|
||||
for value in self.description['parameters']:
|
||||
key = value['name']
|
||||
if 'default' in value:
|
||||
setattr(self, key, options.get(key, None) or value['default'])
|
||||
else:
|
||||
|
@ -84,9 +87,11 @@ class PaymentCommon(object):
|
|||
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))
|
||||
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)
|
||||
fd = os.open(os.path.join(self.PATH, name),
|
||||
os.O_CREAT | os.O_EXCL)
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
|
|
|
@ -71,19 +71,28 @@ class Payment(PaymentCommon):
|
|||
],
|
||||
}
|
||||
|
||||
def request(self, montant, email=None, next_url=None, logger=LOGGER):
|
||||
def request(self, amount, name=None, address=None, email=None, phone=None,
|
||||
info1=None, info2=None, info3=None, next_url=None, **kwargs):
|
||||
self.logger.debug('%s amount %s name %s address %s email %s phone %s'
|
||||
' next_url %s info1 %s info2 %s info3 %s kwargs: %s',
|
||||
__name__, amount, name, address, email, phone, info1, info2, info3, next_url, kwargs)
|
||||
transaction_id = self.transaction_id(30, ALPHANUM, 'dummy', self.siret)
|
||||
if self.next_url:
|
||||
next_url = self.next_url
|
||||
query = {
|
||||
'transaction_id': transaction_id,
|
||||
'siret': self.siret,
|
||||
'amount': montant,
|
||||
'amount': amount,
|
||||
'email': email,
|
||||
'return_url': next_url or '',
|
||||
'direct_notification_url': self.direct_notification_url,
|
||||
'origin': self.origin
|
||||
}
|
||||
query.update(dict(name=name, address=address, email=email, phone=phone,
|
||||
info1=info1, info2=info2, info3=info3))
|
||||
for key in query.keys():
|
||||
if query[key] is None:
|
||||
del query[key]
|
||||
url = '%s?%s' % (SERVICE_URL, urllib.urlencode(query))
|
||||
return transaction_id, URL, url
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import logging
|
|||
import os
|
||||
import os.path
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from common import PaymentCommon, HTML, PaymentResponse
|
||||
from cb import CB_RESPONSE_CODES
|
||||
|
@ -29,26 +28,28 @@ contained in the middleware distribution file.
|
|||
|
||||
'''
|
||||
|
||||
__all__ = [ 'Payment' ]
|
||||
__all__ = ['Payment']
|
||||
|
||||
BINPATH = 'binpath'
|
||||
BINPATH = 'binpath'
|
||||
PATHFILE = 'pathfile'
|
||||
AUTHORISATION_ID = 'authorisation_id'
|
||||
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']
|
||||
'textcolor', 'receipt_complement', 'caddie', 'customer_id',
|
||||
'customer_email', 'customer_ip_address', 'data', 'return_context',
|
||||
'target', 'order_id']
|
||||
|
||||
RESPONSE_PARAMS = [ 'code', 'error', 'merchant_id', 'merchant_country',
|
||||
RESPONSE_PARAMS = ['code', 'error', 'merchant_id', 'merchant_country',
|
||||
'amount', 'transaction_id', 'payment_means', 'transmission_date',
|
||||
'payment_time', 'payment_date', 'response_code', 'payment_certificate',
|
||||
AUTHORISATION_ID, 'currency_code', 'card_number', 'cvv_flag',
|
||||
'cvv_response_code', 'bank_response_code', 'complementary_code',
|
||||
'complementary_info', 'return_context', 'caddie', 'receipt_complement',
|
||||
'merchant_language', 'language', 'customer_id', 'order_id', 'customer_email',
|
||||
'customer_ip_address', 'capture_day', 'capture_mode', 'data', ]
|
||||
'merchant_language', 'language', 'customer_id', 'order_id',
|
||||
'customer_email', 'customer_ip_address', 'capture_day', 'capture_mode',
|
||||
'data', ]
|
||||
|
||||
DATA = 'DATA'
|
||||
PARAMS = 'params'
|
||||
|
@ -58,9 +59,9 @@ ORDER_ID = 'order_id'
|
|||
MERCHANT_ID = 'merchant_id'
|
||||
RESPONSE_CODE = 'response_code'
|
||||
|
||||
DEFAULT_PARAMS = { 'merchant_id': '014213245611111',
|
||||
'merchant_country': 'fr',
|
||||
'currency_code': '978' }
|
||||
DEFAULT_PARAMS = {'merchant_id': '014213245611111',
|
||||
'merchant_country': 'fr',
|
||||
'currency_code': '978'}
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -80,43 +81,65 @@ FINAREF_BANK_RESPONSE_CODE = {
|
|||
'05': 'Compte / Porteur avec statut bloqué ou invalide',
|
||||
'11': 'Compte / porteur inconnu',
|
||||
'16': 'Provision insuffisante',
|
||||
'20': 'Commerçant invalide - Code monnaie incorrect - Opération commerciale inconnue - Opération commerciale invalide',
|
||||
'20': 'Commerçant invalide - Code monnaie incorrect - ' + \
|
||||
'Opération commerciale inconnue - Opération commerciale invalide',
|
||||
'80': 'Transaction approuvée avec dépassement',
|
||||
'81': 'Transaction approuvée avec augmentation capital',
|
||||
'82': 'Transaction approuvée NPAI',
|
||||
'83': 'Compte / porteur invalide',
|
||||
}
|
||||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
description = {
|
||||
'caption': 'SIPS',
|
||||
'parameters': [{
|
||||
'name': 'merchand_id',
|
||||
},
|
||||
{'name': 'merchant_country', },
|
||||
{'name': 'currency_code', }
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, options, logger=LOGGER):
|
||||
self.options = options
|
||||
self.binpath = self.options.pop(BINPATH)
|
||||
self.logger = logger
|
||||
logger.debug('initializing sips payment class with %s' % options)
|
||||
self.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() ]
|
||||
executable = os.path.join(self.binpath, executable)
|
||||
args = [executable] + ["%s=%s" % p for p in params.iteritems()]
|
||||
self.logger.debug('executing %s' % args)
|
||||
result, _ = subprocess.Popen(args, executable=executable,
|
||||
result,_ = subprocess.Popen(' '.join(args),
|
||||
stdout=subprocess.PIPE, shell=True).communicate()
|
||||
try:
|
||||
if result[0] == '!':
|
||||
result = result[1:]
|
||||
if result[-1] == '!':
|
||||
result = result[:-1]
|
||||
except IndexError:
|
||||
raise ValueError("Invalid response", result)
|
||||
return False
|
||||
result = result.split('!')
|
||||
self.logger.debug('got response %s' % result)
|
||||
return result
|
||||
|
||||
def get_request_params(self):
|
||||
params = DEFAULT_PARAMS.copy()
|
||||
params.update(self.options.get(PARAMS, {}))
|
||||
params.update(self.options)
|
||||
return params
|
||||
|
||||
def request(self, amount, email=None, next_url=None, logger=LOGGER):
|
||||
def request(self, amount, name=None, address=None, email=None, phone=None, info1=None,
|
||||
info2=None, info3=None, next_url=None, **kwargs):
|
||||
params = self.get_request_params()
|
||||
transaction_id = self.transaction_id(6, string.digits, 'sips',
|
||||
params[MERCHANT_ID])
|
||||
params[TRANSACTION_ID] = transaction_id
|
||||
params[ORDER_ID] = str(uuid.uuid4()).replace('-','')
|
||||
params['amount'] = str(Decimal(amount)*100)
|
||||
params[ORDER_ID] = str(uuid.uuid4()).replace('-', '')
|
||||
params['amount'] = str(int(Decimal(amount) * 100))
|
||||
if email:
|
||||
params['customer_email'] = email
|
||||
if next_url:
|
||||
|
@ -129,7 +152,7 @@ class Payment(PaymentCommon):
|
|||
|
||||
def response(self, query_string):
|
||||
form = urlparse.parse_qs(query_string)
|
||||
params = {'message': form[DATA]}
|
||||
params = {'message': form[DATA][0]}
|
||||
result = self.execute('response', params)
|
||||
d = dict(zip(RESPONSE_PARAMS, result))
|
||||
# The reference identifier for the payment is the authorisation_id
|
||||
|
@ -139,7 +162,7 @@ class Payment(PaymentCommon):
|
|||
response_code_msg = CB_BANK_RESPONSE_CODES.get(d.get(RESPONSE_CODE))
|
||||
response = PaymentResponse(
|
||||
result=response_result,
|
||||
signed_result=response_result,
|
||||
signed=response_result,
|
||||
bank_data=d,
|
||||
order_id=d.get(ORDER_ID),
|
||||
transaction_id=d.get(AUTHORISATION_ID),
|
||||
|
|
|
@ -121,9 +121,10 @@ class Payment(PaymentCommon):
|
|||
}
|
||||
devise = '978'
|
||||
|
||||
def request(self, montant, email=None, next_url=None, logger=LOGGER):
|
||||
def request(self, amount, name=None, address=None, email=None, phone=None, info1=None,
|
||||
info2=None, info3=None, next_url=None, logger=LOGGER, **kwargs):
|
||||
logger.debug('requesting spplus payment with montant %s email=%s and \
|
||||
next_url=%s' % (montant, email, next_url))
|
||||
next_url=%s' % (amount, email, next_url))
|
||||
reference = self.transaction_id(20, ALPHANUM, 'spplus', self.siret)
|
||||
validite = dt.date.today()+dt.timedelta(days=1)
|
||||
validite = validite.strftime('%d/%m/%Y')
|
||||
|
@ -131,7 +132,7 @@ next_url=%s' % (montant, email, next_url))
|
|||
'devise': self.devise,
|
||||
'langue': self.langue,
|
||||
'taxe': self.taxe,
|
||||
'montant': str(Decimal(montant)),
|
||||
'montant': str(Decimal(amount)),
|
||||
REFERENCE: reference,
|
||||
'validite': validite,
|
||||
'version': '1',
|
||||
|
|
|
@ -6,34 +6,46 @@ import logging
|
|||
import string
|
||||
import urlparse
|
||||
import urllib
|
||||
from decimal import Decimal
|
||||
from gettext import gettext as _
|
||||
|
||||
from common import PaymentCommon, URL, PaymentResponse
|
||||
from common import PaymentCommon, PaymentResponse, URL, PAID, ERROR
|
||||
from cb import CB_RESPONSE_CODES
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
PAYMENT_URL = "https://systempay.cyberpluspaiement.com/vads-payment/"
|
||||
SERVICE_URL = "https://paiement.systempay.fr/vads-payment/"
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
SERVICE_URL = '???'
|
||||
VADS_TRANS_DATE = 'vads_trans_date'
|
||||
VADS_AUTH_NUMBER = 'vads_auth_number'
|
||||
VADS_AUTH_RESULT = 'vads_auth_result'
|
||||
VADS_RESULT = 'vads_result'
|
||||
VADS_EXTRA_RESULT = 'vads_extra_result'
|
||||
VADS_CUST_EMAIL = 'vads_cust_email'
|
||||
VADS_CUST_NAME = 'vads_cust_name'
|
||||
VADS_CUST_PHONE = 'vads_cust_phone'
|
||||
VADS_CUST_INFO1 = 'vads_order_info'
|
||||
VADS_CUST_INFO2 = 'vads_order_info2'
|
||||
VADS_CUST_INFO3 = 'vads_order_info3'
|
||||
VADS_URL_RETURN = 'vads_url_return'
|
||||
VADS_AMOUNT = 'vads_amount'
|
||||
VADS_SITE_ID = 'vads_site_id'
|
||||
VADS_TRANS_ID = 'vads_trans_id'
|
||||
SIGNATURE = 'signature'
|
||||
VADS_TRANS_ID = 'vads_trans_id'
|
||||
|
||||
|
||||
def isonow():
|
||||
return dt.datetime.now() \
|
||||
return dt.datetime.utcnow() \
|
||||
.isoformat('T') \
|
||||
.replace('-','') \
|
||||
.replace('T','') \
|
||||
.replace(':','')[:14]
|
||||
.replace('-', '') \
|
||||
.replace('T', '') \
|
||||
.replace(':', '')[:14]
|
||||
|
||||
|
||||
class Parameter:
|
||||
def __init__(self, name, ptype, code, max_length=None, length=None,
|
||||
needed=False, default=None, choices=None):
|
||||
needed=False, default=None, choices=None, description=None,
|
||||
help_text=None):
|
||||
self.name = name
|
||||
self.ptype = ptype
|
||||
self.code = code
|
||||
|
@ -42,6 +54,8 @@ class Parameter:
|
|||
self.needed = needed
|
||||
self.default = default
|
||||
self.choices = choices
|
||||
self.description = description
|
||||
self.help_text = help_text
|
||||
|
||||
def check_value(self, value):
|
||||
if self.length and len(str(value)) != self.length:
|
||||
|
@ -52,64 +66,72 @@ class Parameter:
|
|||
return False
|
||||
if value == '':
|
||||
return True
|
||||
value = str(value).replace('.','')
|
||||
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()
|
||||
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 value.replace(';', '').isalnum()
|
||||
return True
|
||||
|
||||
|
||||
PARAMETERS = [
|
||||
# amount as euro cents
|
||||
Parameter('vads_action_mode', None, 47, needed=True,
|
||||
default='INTERACTIVE', choices=('SILENT','INTERACTIVE')),
|
||||
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'),
|
||||
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_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_name', 'ans', 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),
|
||||
Parameter('vads_ctx_mode', 'a', 11, needed=True, choices=('TEST',
|
||||
'PRODUCTION'), default='TEST'),
|
||||
# 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_order_info', 'an', 14, max_length=255,
|
||||
description=_(u"Complément d'information 1")),
|
||||
Parameter('vads_order_info2', 'an', 14, max_length=255,
|
||||
description=_(u"Complément d'information 2")),
|
||||
Parameter('vads_order_info3', 'an', 14, max_length=255,
|
||||
description=_(u"Complément d'information 3")),
|
||||
Parameter('vads_page_action', None, 46, needed=True, default='PAYMENT',
|
||||
choices=('PAYMENT',)),
|
||||
Parameter('vads_payment_cards', 'an;', 8, max_length=127, default=''),
|
||||
Parameter('vads_payment_cards', 'an;', 8, max_length=127, default='',
|
||||
description=_(u'Liste des cartes de paiement acceptées'),
|
||||
help_text=_(u'vide ou des valeurs sépareés par un point-virgule parmi '
|
||||
'AMEX, AURORE-MULTI, BUYSTER, CB, COFINOGA, E-CARTEBLEUE, '
|
||||
'MASTERCARD, JCB, MAESTRO, ONEY, ONEY_SANDBOX, PAYPAL, '
|
||||
'PAYPAL_SB, PAYSAFECARD, VISA')),
|
||||
# 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')),
|
||||
choices=('SINGLE', 'MULTI'), needed=True),
|
||||
Parameter('vads_return_mode', None, 48, default='GET',
|
||||
choices=('', 'NONE', 'POST', 'GET')),
|
||||
Parameter('signature', 'an', None, length=40),
|
||||
Parameter('vads_site_id', 'n', 02, length=8, needed=True),
|
||||
Parameter('vads_site_id', 'n', 02, length=8, needed=True,
|
||||
description=_(u'Identifiant de la boutique')),
|
||||
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_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),
|
||||
|
@ -121,6 +143,8 @@ PARAMETERS = [
|
|||
Parameter('vads_user_info', 'ans', 61, max_length=255),
|
||||
Parameter('vads_contracts', 'ans', 62, max_length=255),
|
||||
]
|
||||
PARAMETER_MAP = dict(((parameter.name,
|
||||
parameter) for parameter in PARAMETERS))
|
||||
|
||||
AUTH_RESULT_MAP = CB_RESPONSE_CODES
|
||||
|
||||
|
@ -146,42 +170,122 @@ liste blanche du commerçant",
|
|||
d'un des contrôles locaux",
|
||||
}
|
||||
|
||||
|
||||
def add_vads(kwargs):
|
||||
new_vargs={}
|
||||
new_vargs = {}
|
||||
for k, v in kwargs.iteritems():
|
||||
if k.startswith('vads_'):
|
||||
new_vargs[k] = v
|
||||
else:
|
||||
new_vargs['vads_'+k] = v
|
||||
new_vargs['vads_' + k] = v
|
||||
return new_vargs
|
||||
|
||||
|
||||
def check_vads(kwargs, exclude=[]):
|
||||
for parameter in PARAMETERS:
|
||||
name = parameter.name
|
||||
if name not in kwargs and name not in exclude and parameter.needed:
|
||||
raise ValueError('parameter %s must be defined' % name)
|
||||
if name in kwargs and not parameter.check_value(kwargs[name]):
|
||||
raise ValueError('parameter %s value %s is not of the type %s' % (
|
||||
name, kwargs[name],
|
||||
parameter.ptype))
|
||||
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''
|
||||
ex.: Payment(secrets={'TEST': 'xxx', 'PRODUCTION': 'yyyy'}, site_id=123,
|
||||
ctx_mode='PRODUCTION')
|
||||
'''
|
||||
Produce request for and verify response from the SystemPay payment
|
||||
gateway.
|
||||
|
||||
>>> gw =Payment(dict(secret_test='xxx', secret_production='yyyy',
|
||||
site_id=123, ctx_mode='PRODUCTION'))
|
||||
>>> print gw.request(100)
|
||||
('20120525093304_188620',
|
||||
'https://paiement.systempay.fr/vads-payment/?vads_url_return=http%3A%2F%2Furl.de.retour%2Fretour.php&vads_cust_country=FR&vads_site_id=93413345&vads_payment_config=SINGLE&vads_trans_id=188620&vads_action_mode=INTERACTIVE&vads_contrib=eopayment&vads_page_action=PAYMENT&vads_trans_date=20120525093304&vads_ctx_mode=TEST&vads_validation_mode=&vads_version=V2&vads_payment_cards=&signature=5d412498ab523627ec5730a09118f75afa602af5&vads_language=fr&vads_capture_delay=&vads_currency=978&vads_amount=100&vads_return_mode=NONE',
|
||||
{'vads_url_return': 'http://url.de.retour/retour.php',
|
||||
'vads_cust_country': 'FR', 'vads_site_id': '93413345',
|
||||
'vads_payment_config': 'SINGLE', 'vads_trans_id': '188620',
|
||||
'vads_action_mode': 'INTERACTIVE', 'vads_contrib': 'eopayment',
|
||||
'vads_page_action': 'PAYMENT', 'vads_trans_date': '20120525093304',
|
||||
'vads_ctx_mode': 'TEST', 'vads_validation_mode': '',
|
||||
'vads_version': 'V2', 'vads_payment_cards': '', 'signature':
|
||||
'5d412498ab523627ec5730a09118f75afa602af5', 'vads_language': 'fr',
|
||||
'vads_capture_delay': '', 'vads_currency': 978, 'vads_amount': 100,
|
||||
'vads_return_mode': 'NONE'})
|
||||
|
||||
'''
|
||||
description = {
|
||||
'caption': 'SystemPay, système de paiment du groupe BPCE',
|
||||
'parameters': [
|
||||
{'name': 'service_url',
|
||||
'default': SERVICE_URL,
|
||||
'caption': _(u'URL du service de paiment'),
|
||||
'help_text': _(u'ne pas modifier si vous ne savez pas'),
|
||||
'validation': lambda x: x.startswith('http'),
|
||||
'required': True, },
|
||||
{'name': 'secret_test',
|
||||
'caption': _(u'Secret pour la configuration de TEST'),
|
||||
'validation': str.isdigit,
|
||||
'required': True, },
|
||||
{'name': 'secret_production',
|
||||
'caption': _(u'Secret pour la configuration de PRODUCTION'),
|
||||
'validation': str.isdigit, },
|
||||
]
|
||||
}
|
||||
|
||||
for name in ('vads_ctx_mode', VADS_SITE_ID, 'vads_order_info',
|
||||
'vads_order_info2', 'vads_order_info3',
|
||||
'vads_payment_cards', 'vads_payment_config'):
|
||||
parameter = PARAMETER_MAP[name]
|
||||
x = {'name': name,
|
||||
'caption': parameter.description or name,
|
||||
'validation': parameter.check_value,
|
||||
'default': parameter.default,
|
||||
'required': parameter.needed,
|
||||
'help_text': parameter.help_text,
|
||||
'max_length': parameter.max_length}
|
||||
description['parameters'].append(x)
|
||||
|
||||
def __init__(self, options, logger=LOGGER):
|
||||
self.secrets = options.pop('secrets')
|
||||
self.service_url = options.pop('service_url', SERVICE_URL)
|
||||
self.secret_test = options.pop('secret_test')
|
||||
self.secret_production = options.pop('secret_production', None)
|
||||
options = add_vads(options)
|
||||
self.options = options
|
||||
self.logger = logger
|
||||
|
||||
def request(self, amount, email=None, next_url=None, logger=LOGGER):
|
||||
def request(self, amount, name=None, address=None, email=None, phone=None, info1=None,
|
||||
info2=None, info3=None, next_url=None, **kwargs):
|
||||
'''
|
||||
Create a dictionary to send a payment request to systempay the
|
||||
Credit Card payment server of the NATIXIS group
|
||||
Create the URL string to send a request to SystemPay
|
||||
'''
|
||||
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
|
||||
self.logger.debug('%s amount %s name %s address %s email %s phone %s next_url %s info1 %s info2 %s info3 %s kwargs: %s',
|
||||
__name__, amount, name, address, email, phone, info1, info2, info3, next_url, kwargs)
|
||||
# amount unit is cents
|
||||
amount = '%.0f' % (100 * amount)
|
||||
kwargs.update(add_vads({'amount': amount}))
|
||||
if amount < 0:
|
||||
raise ValueError('amount must be an integer >= 0')
|
||||
if next_url:
|
||||
kwargs['vads_url_return'] = next_url
|
||||
kwargs[VADS_URL_RETURN] = next_url
|
||||
if name is not None:
|
||||
kwargs['vads_cust_name'] = name
|
||||
if address is not None:
|
||||
kwargs['vads_cust_address'] = address
|
||||
if email is not None:
|
||||
kwargs['vads_cust_email'] = email
|
||||
if phone is not None:
|
||||
kwargs['vads_cust_phone'] = phone
|
||||
if info1 is not None:
|
||||
kwargs['vads_order_info'] = info1
|
||||
if info2 is not None:
|
||||
kwargs['vads_order_info2'] = info2
|
||||
if info3 is not None:
|
||||
kwargs['vads_order_info3'] = info3
|
||||
|
||||
transaction_id = self.transaction_id(6,
|
||||
string.digits, 'systempay', self.options['vads_site_id'])
|
||||
kwargs['vads_trans_id'] = transaction_id
|
||||
string.digits, 'systempay', self.options[VADS_SITE_ID])
|
||||
kwargs[VADS_TRANS_ID] = transaction_id
|
||||
fields = kwargs
|
||||
for parameter in PARAMETERS:
|
||||
name = parameter.name
|
||||
|
@ -195,22 +299,19 @@ class Payment(PaymentCommon):
|
|||
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))
|
||||
check_vads(fields)
|
||||
fields[SIGNATURE] = self.signature(fields)
|
||||
self.logger.debug('%s request contains fields: %s', __name__, fields)
|
||||
url = '%s?%s' % (SERVICE_URL, urllib.urlencode(fields))
|
||||
self.logger.debug('%s return url %s', __name__, url)
|
||||
transaction_id = '%s_%s' % (fields[VADS_TRANS_DATE], transaction_id)
|
||||
return transaction_id, URL, fields
|
||||
self.logger.debug('%s transaction id: %s', __name__, transaction_id)
|
||||
return transaction_id, URL, url
|
||||
|
||||
def response(self, query_string, logger=LOGGER):
|
||||
fields = urlparse.parse_qs(query_string)
|
||||
def response(self, query_string):
|
||||
fields = urlparse.parse_qs(query_string, True)
|
||||
for key, value in fields.iteritems():
|
||||
fields[key] = value[0]
|
||||
copy = fields.copy()
|
||||
bank_status = []
|
||||
if VADS_AUTH_RESULT in fields:
|
||||
|
@ -229,52 +330,60 @@ parameters received: %s' % (name, kwargs))
|
|||
if v.isdigit():
|
||||
for parameter in PARAMETERS:
|
||||
if int(v) == parameter.code:
|
||||
s ='erreur dans le champ %s' % parameter.name
|
||||
s = 'erreur dans le champ %s' % parameter.name
|
||||
copy[VADS_EXTRA_RESULT] = s
|
||||
bank_status.append(copy[VADS_EXTRA_RESULT])
|
||||
elif v in ('05', '00'):
|
||||
v = fields[VADS_EXTRA_RESULT]
|
||||
copy[VADS_EXTRA_RESULT] = '%s: %s' % (v,
|
||||
EXTRA_RESULT_MAP.get(v, 'Code inconnu'))
|
||||
bank_status.append(copy[VADS_EXTRA_RESULT])
|
||||
logger.debug('checking systempay response on:')
|
||||
for key in sorted(fields.keys):
|
||||
logger.debug(' %s: %s' % (key, copy[key]))
|
||||
signature = self.signature(fields, logger)
|
||||
if VADS_EXTRA_RESULT in fields:
|
||||
v = fields[VADS_EXTRA_RESULT]
|
||||
copy[VADS_EXTRA_RESULT] = '%s: %s' % (v,
|
||||
EXTRA_RESULT_MAP.get(v, 'Code inconnu'))
|
||||
bank_status.append(copy[VADS_EXTRA_RESULT])
|
||||
self.logger.debug('checking systempay response on:')
|
||||
for key in sorted(fields.keys()):
|
||||
self.logger.debug(' %s: %s' % (key, copy[key]))
|
||||
signature = self.signature(fields)
|
||||
signature_result = signature == fields[SIGNATURE]
|
||||
self.logger.debug('signature check: %s <!> %s', signature,
|
||||
fields[SIGNATURE])
|
||||
if not signature_result:
|
||||
bank_status.append('invalid signature')
|
||||
result = fields[VADS_AUTH_RESULT] == '00'
|
||||
signed_result = signature_result and result
|
||||
logger.debug('signature check result: %s' % result)
|
||||
|
||||
if fields[VADS_AUTH_RESULT] == '00':
|
||||
result = PAID
|
||||
else:
|
||||
result = ERROR
|
||||
transaction_id = '%s_%s' % (copy[VADS_TRANS_DATE], copy[VADS_TRANS_ID])
|
||||
# the VADS_AUTH_NUMBER is the number to match payment in bank logs
|
||||
copy[self.BANK_ID] = copy.get(VADS_AUTH_NUMBER, '')
|
||||
response = PaymentResponse(
|
||||
result=result,
|
||||
signed_result=signed_result,
|
||||
bankd_data=copy,
|
||||
signed=signature_result,
|
||||
bank_data=copy,
|
||||
order_id=transaction_id,
|
||||
transaction_id=copy.get(VADS_AUTH_NUMBER),
|
||||
bank_status=' - '.join(bank_status))
|
||||
return response
|
||||
|
||||
def signature(self, fields, logger):
|
||||
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']]
|
||||
def signature(self, fields):
|
||||
self.logger.debug('got fields %s to sign' % fields)
|
||||
ordered_keys = sorted([key for key in fields.keys() if key.startswith('vads_')])
|
||||
self.logger.debug('ordered keys %s' % ordered_keys)
|
||||
ordered_fields = [str(fields[key]) for key in ordered_keys]
|
||||
secret = getattr(self, 'secret_%s' % fields['vads_ctx_mode'].lower())
|
||||
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)
|
||||
signed_data = '%s+%s' % (signed_data, secret)
|
||||
self.logger.debug('generating signature on «%s»' % signed_data)
|
||||
sign = hashlib.sha1(signed_data).hexdigest()
|
||||
self.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')
|
||||
|
||||
|
||||
p = Payment(dict(
|
||||
secret_test='2662931409789978',
|
||||
site_id='93413345',
|
||||
ctx_mode='TEST'))
|
||||
print p.request(100, vads_url_return='http://url.de.retour/retour.php')
|
||||
qs = 'vads_amount=100&vads_auth_mode=FULL&vads_auth_number=767712&vads_auth_result=00&vads_capture_delay=0&vads_card_brand=CB&vads_card_number=497010XXXXXX0000&vads_payment_certificate=9da32cc109882089e1b3fb80888ebbef072f70b7&vads_ctx_mode=TEST&vads_currency=978&vads_effective_amount=100&vads_site_id=93413345&vads_trans_date=20120529132547&vads_trans_id=620594&vads_validation_mode=0&vads_version=V2&vads_warranty_result=NO&vads_payment_src=&vads_order_id=---&vads_cust_country=FR&vads_contrib=eopayment&vads_contract_used=2334233&vads_expiry_month=6&vads_expiry_year=2013&vads_pays_ip=FR&vads_identifier=&vads_subscription=&vads_threeds_enrolled=&vads_threeds_cavv=&vads_threeds_eci=&vads_threeds_xid=&vads_threeds_cavvAlgorithm=&vads_threeds_status=&vads_threeds_sign_valid=&vads_threeds_error_code=&vads_threeds_exit_status=&vads_result=00&vads_extra_result=&vads_card_country=FR&vads_language=fr&vads_action_mode=INTERACTIVE&vads_page_action=PAYMENT&vads_payment_config=SINGLE&signature=9c4f2bf905bb06b008b07090905adf36638d8ece&'
|
||||
response = p.response(qs)
|
||||
assert response.signed and response.result
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED,
|
||||
CANCELED, ERROR)
|
||||
from urllib import urlencode
|
||||
from urlparse import parse_qs
|
||||
from gettext import gettext as _
|
||||
import logging
|
||||
|
||||
from systempayv2 import isonow
|
||||
|
||||
__all__ = ['Payment']
|
||||
|
||||
TIPI_URL = 'http://www.jepaiemesserviceslocaux.dgfip.finances.gouv.fr' \
|
||||
'/tpa/paiement.web'
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
SEPARATOR = '#'
|
||||
|
||||
class Payment(PaymentCommon):
|
||||
'''Produce requests for and verify response from the TIPI online payment
|
||||
processor from the French Finance Ministry.
|
||||
|
||||
'''
|
||||
|
||||
description = {
|
||||
'caption': 'TIPI, Titres Payables par Internet',
|
||||
'parameters': [
|
||||
{
|
||||
'name': 'numcli',
|
||||
'caption': _(u'Numéro client'),
|
||||
'help_text': _(u'un numéro à 6 chiffres communiqué par l’administrateur TIPI'),
|
||||
'validation': lambda s: str.isdigit(s) and (0 < int(s) < 1000000),
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'name': 'service_url',
|
||||
'default': TIPI_URL,
|
||||
'caption': _(u'URL du service TIPI'),
|
||||
'help_text': _(u'ne pas modifier si vous ne savez pas'),
|
||||
'validation': lambda x: x.startswith('http'),
|
||||
'required': True,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, options, logger=LOGGER):
|
||||
self.service_url = options.pop('service_url', TIPI_URL)
|
||||
self.numcli = options.pop('numcli', '')
|
||||
self.logger = logger
|
||||
|
||||
def request(self, amount, next_url=None, exer=None, refdet=None,
|
||||
objet=None, mel=None, saisie=None, **kwargs):
|
||||
try:
|
||||
montant = Decimal(amount)
|
||||
if Decimal('0') > montant > Decimal('9999.99'):
|
||||
raise ValueError('MONTANT > 9999.99 euros')
|
||||
montant = montant*Decimal('100')
|
||||
montant = montant.to_integral_value(ROUND_DOWN)
|
||||
except ValueError:
|
||||
raise ValueError('MONTANT invalid format, must be '
|
||||
'a decimal integer with less than 4 digits '
|
||||
'before and 2 digits after the decimal point '
|
||||
', here it is %s' % repr(amount))
|
||||
if next_url is not None:
|
||||
if not isinstance(next_url, str) or \
|
||||
not next_url.startswith('http'):
|
||||
raise ValueError('URLCL invalid URL format')
|
||||
try:
|
||||
if exer is not None:
|
||||
exer = int(exer)
|
||||
if exer > 9999:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
raise ValueError('EXER format invalide')
|
||||
try:
|
||||
refdet = str(refdet)
|
||||
if len(refdet) != 18:
|
||||
raise ValueError('len(REFDET) != 18')
|
||||
except Exception, e:
|
||||
raise ValueError('REFDET format invalide, %r' % refdet, e)
|
||||
if objet is not None:
|
||||
try:
|
||||
objet = str(objet)
|
||||
except Exception, e:
|
||||
raise ValueError('OBJET must be a string', e)
|
||||
if not objet.replace(' ','').isalnum():
|
||||
raise ValueError('OBJECT must only contains '
|
||||
'alphanumeric characters, %r' % objet)
|
||||
if len(objet) > 99:
|
||||
raise ValueError('OBJET length must be less than 100')
|
||||
try:
|
||||
mel = str(mel)
|
||||
if '@' not in mel:
|
||||
raise ValueError('no @ in MEL')
|
||||
if not (6 <= len(mel) <= 80):
|
||||
raise ValueError('len(MEL) is invalid, must be between 6 and 80')
|
||||
except Exception, e:
|
||||
raise ValueError('MEL is not a valid email, %r' % mel, e)
|
||||
|
||||
if saisie not in ('M', 'T', 'X', 'A'):
|
||||
raise ValueError('SAISIE invalid format, %r, must be M, T, X or A' % saisie)
|
||||
|
||||
iso_now = isonow()
|
||||
transaction_id = '%s_%s' % (iso_now, refdet)
|
||||
if objet:
|
||||
objet = objet[:100-len(iso_now)-2] + ' ' + SEPARATOR \
|
||||
+ iso_now
|
||||
else:
|
||||
objet = SEPARATOR + iso_now
|
||||
params = {
|
||||
'NUMCLI': self.numcli,
|
||||
'REFDET': refdet,
|
||||
'MONTANT': montant,
|
||||
'MEL': mel,
|
||||
'SAISIE': saisie,
|
||||
'OBJET': objet,
|
||||
}
|
||||
if exer:
|
||||
params['EXER'] = exer
|
||||
if next_url:
|
||||
params['URLCL'] = next_url
|
||||
url = '%s?%s' % (self.service_url, urlencode(params))
|
||||
return transaction_id, URL, url
|
||||
|
||||
def response(self, query_string):
|
||||
fields = parse_qs(query_string, True)
|
||||
for key, value in fields.iteritems():
|
||||
fields[key] = value[0]
|
||||
refdet = fields.get('REFDET')
|
||||
if refdet is None:
|
||||
raise ValueError('REFDET is missing')
|
||||
if 'OBJET' in fields and SEPARATOR in fields['OBJET']:
|
||||
iso_now = fields['OBJET'].rsplit(SEPARATOR, 1)[1]
|
||||
else:
|
||||
iso_now = isonow()
|
||||
transaction_id = '%s_%s' % (iso_now, refdet)
|
||||
|
||||
result = fields.get('RESULTRANS')
|
||||
if result == 'P':
|
||||
result = PAID
|
||||
bank_status = ''
|
||||
elif result == 'R':
|
||||
result = DENIED
|
||||
bank_status = 'refused'
|
||||
elif result == 'A':
|
||||
result = CANCELED
|
||||
bank_status = 'canceled'
|
||||
else:
|
||||
bank_status = 'wrong return: %r' % result
|
||||
result = ERROR
|
||||
return PaymentResponse(
|
||||
result=result,
|
||||
bank_status=bank_status,
|
||||
signed=True,
|
||||
bank_data=fields,
|
||||
transaction_id=transaction_id)
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = Payment({'numcli': '12345'})
|
||||
print p.request(amount=Decimal('123.12'),
|
||||
exer=9999,
|
||||
refdet=999900000000999999,
|
||||
objet='tout a fait',
|
||||
mel='bdauvergne@entrouvert.com',
|
||||
urlcl='http://example.com/tipi/test',
|
||||
saisie='T')
|
||||
print p.response('OBJET=tout+a+fait+%2320121010131958&MONTANT=12312&SAISIE=T&MEL=bdauvergne%40entrouvert.com&NUMCLI=12345&EXER=9999&REFDET=999900000000999999&RESULTRANS=P')
|
|
@ -9,8 +9,8 @@ import distutils.core
|
|||
from glob import glob
|
||||
from os.path import splitext, basename, join as pjoin
|
||||
import os
|
||||
import re
|
||||
from unittest import TextTestRunner, TestLoader
|
||||
import doctest
|
||||
|
||||
class TestCommand(distutils.core.Command):
|
||||
user_options = [ ]
|
||||
|
@ -33,13 +33,41 @@ class TestCommand(distutils.core.Command):
|
|||
)
|
||||
|
||||
tests = TestLoader().loadTestsFromNames(testfiles)
|
||||
import eopayment
|
||||
tests.addTests(doctest.DocTestSuite(eopayment))
|
||||
t = TextTestRunner(verbosity = 4)
|
||||
t.run(tests)
|
||||
|
||||
def get_version():
|
||||
text = file('eopayment/__init__.py').read()
|
||||
m = re.search("__version__ = ['\"](.*)['\"]", text)
|
||||
return m.group(1)
|
||||
import glob
|
||||
import re
|
||||
import os
|
||||
|
||||
version = None
|
||||
for d in glob.glob('*'):
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
module_file = os.path.join(d, '__init__.py')
|
||||
if not os.path.exists(module_file):
|
||||
continue
|
||||
for v in re.findall("""__version__ *= *['"](.*)['"]""",
|
||||
open(module_file).read()):
|
||||
assert version is None
|
||||
version = v
|
||||
if version:
|
||||
break
|
||||
assert version is not None
|
||||
if os.path.exists('.git'):
|
||||
import subprocess
|
||||
p = subprocess.Popen(['git','describe','--dirty','--match=v*'],
|
||||
stdout=subprocess.PIPE)
|
||||
result = p.communicate()[0]
|
||||
assert p.returncode == 0, 'git returned non-zero'
|
||||
new_version = result.split()[0][1:]
|
||||
assert not new_version.endswith('-dirty'), 'git workdir is not clean'
|
||||
assert new_version.split('-')[0] == version, '__version__ must match the last git annotated tag'
|
||||
version = new_version.replace('-', '.')
|
||||
return version
|
||||
|
||||
distutils.core.setup(name='eopayment',
|
||||
version=get_version(),
|
||||
|
|
Loading…
Reference in New Issue