Compare commits

...

52 Commits

Author SHA1 Message Date
Benjamin Dauvergne e5d5cb8881 Add get_backends() function 2015-03-04 14:50:03 +01:00
Benjamin Dauvergne 4ae04f39c4 Fix SPPlus request method 2015-03-04 14:50:03 +01:00
Benjamin Dauvergne e46ac82192 Use doctests as parts of tests, fix some of them 2015-03-04 14:50:03 +01:00
raphigaziano ef65a604f7 store binpath on init and use this copy later on 2013-12-12 12:05:56 +01:00
Benjamin Dauvergne 1645205966 bump release to 0.0.22 2013-12-11 11:01:55 +01:00
Benjamin Dauvergne edf7064335 setup.py: remove tests from installed packages 2013-10-07 15:09:11 +02:00
Benjamin Dauvergne 4d3e995e09 setup.py: update get_version 2013-05-15 15:34:40 +02:00
Benjamin Dauvergne 5a9eb7fa97 bump version to 0.0.20 2013-05-15 15:20:44 +02:00
Benjamin Dauvergne 935c5d9367 dummy: only keep useful variable in the generated URL not all the locals 2013-05-15 15:19:46 +02:00
Benjamin Dauvergne 1bdb3a8c88 common: store unused logger parameter 2013-05-15 15:19:18 +02:00
Benjamin Dauvergne 0020cb511a setup.py: import get_version function from python-entrouvert setup.py 2013-05-15 15:18:35 +02:00
Benjamin Dauvergne 236753e8b3 chmod +x setup.py 2013-04-12 18:03:05 +02:00
Benjamin Dauvergne db99c7bd66 Merge branch 'master' of git://github.com/strycore/eopayment
Conflicts:
	eopayment/systempayv2.py
2013-03-29 18:45:36 +01:00
Benjamin Dauvergne 412e924c60 bump version to 0.0.19 2012-11-23 09:48:11 +01:00
Benjamin Dauvergne ed976c1deb systempayv2: do not try to validate email fields, we suppose the caller knows better 2012-11-23 09:46:50 +01:00
Benjamin Dauvergne ef6a3a63b2 add a TIPI backend
fixes #1773
2012-10-10 15:34:20 +02:00
Benjamin Dauvergne 07b7c553d2 remove unused import 2012-10-10 14:56:46 +02:00
Benjamin Dauvergne 5b211a0a93 bump version to 0.0.18 2012-10-05 16:10:54 +02:00
Benjamin Dauvergne ef6fc6c9c1 add more variables to the dummy backend request 2012-10-05 16:10:34 +02:00
Benjamin Dauvergne a244c4fec1 vads_cust_name can contain spaces, its type is "ans" no "an" 2012-10-05 16:08:27 +02:00
Benjamin Dauvergne 65b9a89c1f bump version to 0.0.17 2012-10-05 10:48:24 +02:00
Benjamin Dauvergne 1da33a4d00 systempayv2: the date must be in the UTC zone 2012-10-05 10:47:59 +02:00
Benjamin Dauvergne b011c0c18d bump version to 0.0.16 2012-10-03 17:54:44 +02:00
Benjamin Dauvergne 6041c7593d bump version to 0.0.15 2012-10-03 17:34:02 +02:00
Benjamin Dauvergne 10251b2b4c fix generic handling of backend parameters 2012-10-03 17:32:52 +02:00
Benjamin Dauvergne 079181a3e8 bump version to 0.0.14 2012-10-03 17:31:25 +02:00
Benjamin Dauvergne da3b05a6fb bump version to 0.0.13 2012-10-03 17:30:52 +02:00
Benjamin Dauvergne 29c8cf03c7 add more parameters to request() 2012-10-03 17:30:13 +02:00
Benjamin Dauvergne 6352e7afff augment the number of arguments accepted by the request method 2012-09-24 15:43:55 +02:00
Mathieu Comandon 6865acfbcb pep8 on systempay 2012-07-10 18:04:22 +02:00
Mathieu Comandon 3e0a86c918 catch invalid response and show detailled error message 2012-07-04 18:10:54 +02:00
Mathieu Comandon 2bf9560737 fixed calls to request script 2012-06-13 15:13:47 +02:00
Mathieu Comandon 7b231b5ed3 Amount not passed as integer in params 2012-06-13 14:38:52 +02:00
Mathieu Comandon e5f644101d Be less greedy while removing leading and trailing '!' 2012-06-13 14:33:02 +02:00
Mathieu Comandon fb9e19d11d fix: request returns 5 elements instead of 3 2012-06-13 14:14:52 +02:00
Mathieu Comandon 7c76f2b121 __init__.py pep8 compliance 2012-06-13 13:45:46 +02:00
Mathieu Comandon 9b9112d5d7 common.py pep8 compliance 2012-06-13 13:43:35 +02:00
Mathieu Comandon 90f03878f2 sips.py pep8 compliance 2012-06-13 13:40:48 +02:00
Mathieu Comandon f551aca200 fix: PaymentResponse arguments does not match signature 2012-06-13 13:36:03 +02:00
Mathieu Comandon 2ce287cf97 fix: DATA query passed as array in the response 2012-06-13 13:31:10 +02:00
Benjamin Dauvergne 277e53d7c4 systempayv2: does not use syntax from python 2.7 2012-06-01 16:45:16 +02:00
Benjamin Dauvergne 1c3d1e3a62 systempayv2: amount unit is cents 2012-05-29 17:43:51 +02:00
Benjamin Dauvergne c072f3a4ab bumper version to 0.0.12 2012-05-29 16:56:17 +02:00
Benjamin Dauvergne 3b6da13deb properly initialize PaymentResponse.result attribute 2012-05-29 16:55:55 +02:00
Benjamin Dauvergne c39ef64c72 add backend parameters description to the sips backend 2012-05-29 16:55:55 +02:00
Benjamin Dauvergne 7d9cd8a708 bump version to 0.0.11 2012-05-29 14:52:32 +02:00
Benjamin Dauvergne ae609803ee sips: remove logger parameter for .request() 2012-05-29 14:52:32 +02:00
Benjamin Dauvergne 72ee3ea663 systempayv2: finish response handling, fix signature checking for responses 2012-05-29 14:50:23 +02:00
Benjamin Dauvergne 311b71e4e6 systempayv2: improve parameter descriptions 2012-05-29 14:49:26 +02:00
Benjamin Dauvergne 8da8b3a341 fix documentation of the PaymentResponse documentation 2012-05-29 14:47:27 +02:00
Benjamin Dauvergne cdba3db82a does not pass the logger parameter anymore to .response() and .request() 2012-05-29 14:46:26 +02:00
Benjamin Dauvergne b8c6cff7bb systempayv2: raise it to the level of the spplus backend
- create description,
- add descriptions to certain parameters,
- add more logs,
- add vads_order_info* parameters,
- add vads_payment_cards parameter
- set GET as the default return mode,
- fix return value of Payment.request.
2012-05-25 15:42:04 +02:00
9 changed files with 512 additions and 197 deletions

View File

@ -0,0 +1,2 @@
recursive-include debian *
recursive-include tests *.py

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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),

View File

@ -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',

View File

@ -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

168
eopayment/tipi.py Normal file
View File

@ -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 ladministrateur 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')

36
setup.py Normal file → Executable file
View File

@ -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(),