diff --git a/eopayment/__init__.py b/eopayment/__init__.py index 4b964d9..9b82803 100644 --- a/eopayment/__init__.py +++ b/eopayment/__init__.py @@ -20,15 +20,48 @@ import logging import pytz from .common import ( # noqa: F401 - URL, HTML, FORM, RECEIVED, ACCEPTED, PAID, DENIED, - CANCELED, CANCELLED, ERROR, WAITING, EXPIRED, force_text, - ResponseError, PaymentException, + URL, + HTML, + FORM, + RECEIVED, + ACCEPTED, + PAID, + DENIED, + CANCELED, + CANCELLED, + ERROR, + WAITING, + EXPIRED, + force_text, + ResponseError, + PaymentException, ) -__all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', 'SYSTEMPAY', - 'TIPI', 'DUMMY', 'get_backend', 'RECEIVED', 'ACCEPTED', 'PAID', - 'DENIED', 'CANCELED', 'CANCELLED', 'ERROR', 'WAITING', - 'EXPIRED', 'get_backends', 'PAYFIP_WS', 'SAGA', 'KEYWARE', 'MOLLIE'] +__all__ = [ + 'Payment', + 'URL', + 'HTML', + 'FORM', + 'SIPS', + 'SYSTEMPAY', + 'TIPI', + 'DUMMY', + 'get_backend', + 'RECEIVED', + 'ACCEPTED', + 'PAID', + 'DENIED', + 'CANCELED', + 'CANCELLED', + 'ERROR', + 'WAITING', + 'EXPIRED', + 'get_backends', + 'PAYFIP_WS', + 'SAGA', + 'KEYWARE', + 'MOLLIE', +] SIPS = 'sips' SIPS2 = 'sips2' @@ -51,86 +84,86 @@ def get_backend(kind): module = importlib.import_module('.' + kind, package='eopayment') return module.Payment -__BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, OGONE, PAYBOX, PAYZEN, - TIPI, PAYFIP_WS, KEYWARE, MOLLIE, SAGA] + +__BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, OGONE, PAYBOX, PAYZEN, TIPI, PAYFIP_WS, KEYWARE, MOLLIE, SAGA] def get_backends(): - '''Return a dictionnary mapping existing eopayment backends name to their - description. + """Return a dictionnary mapping existing eopayment backends name to their + description. - >>> get_backends()['dummy'].description['caption'] - 'Dummy payment backend' + >>> get_backends()['dummy'].description['caption'] + 'Dummy payment backend' - ''' + """ return {backend: get_backend(backend) for backend in __BACKENDS} class Payment: - ''' - Interface to credit card online payment servers of French banks. The - only use case supported for now is a unique automatic payment. + """ + Interface to credit card online payment servers of French banks. The + only use case supported for now is a unique automatic payment. - >>> options = { - 'numcli': '12345', - } - >>> p = Payment(kind=TIPI, options=options) - >>> transaction_id, kind, data = p.request('10.00', email='bob@example.com') - >>> print (transaction_id, kind, data) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE - ('...', 1, 'https://www.payfip.gov.fr/tpa/paiement.web?...') + >>> options = { + 'numcli': '12345', + } + >>> p = Payment(kind=TIPI, options=options) + >>> transaction_id, kind, data = p.request('10.00', email='bob@example.com') + >>> print (transaction_id, kind, data) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ('...', 1, 'https://www.payfip.gov.fr/tpa/paiement.web?...') - Supported backend of French banks are: + Supported backend of French banks are: - - TIPI/PayFiP - - SIPS 2.0, 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. - - SystemPay v2/Payzen for Banque Populaire and Caise d'Epargne (Natixis, after 2010) - - Ogone - - Paybox - - Mollie (Belgium) - - Keyware (Belgium) + - TIPI/PayFiP + - SIPS 2.0, 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. + - SystemPay v2/Payzen for Banque Populaire and Caise d'Epargne (Natixis, after 2010) + - Ogone + - Paybox + - Mollie (Belgium) + - Keyware (Belgium) - 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. + 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. - Each backend need some configuration parameters to be used, the - description of the backend list those parameters. The description - dictionary can be used to generate configuration forms. + Each backend need some configuration parameters to be used, the + description of the backend list those parameters. The description + dictionary can be used to generate configuration forms. - >>> d = get_backend(SPPLUS).description - >>> print d['caption'] - 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 + >>> d = get_backend(SPPLUS).description + >>> print d['caption'] + 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): self.kind = kind self.backend = get_backend(kind)(options, logger=logger) def request(self, amount, **kwargs): - '''Request a payment to the payment backend. + """Request a payment to the payment backend. - Arguments: - amount -- the amount of money to ask - email -- the email of the customer (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. + Arguments: + amount -- the amount of money to ask + email -- the email of the customer (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 or payment.FORM, - - the third is the URL or the HTML form to contact the payment - server, which must be sent to the customer browser. - ''' + 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 or payment.FORM, + - the third is the URL or the HTML form to contact the payment + server, which must be sent to the customer browser. + """ logger.debug('%r' % kwargs) if 'capture_date' in kwargs: @@ -151,8 +184,7 @@ class Payment: # backend timezone should come from some backend configuration backend_tz = pytz.timezone('Europe/Paris') utc_tz = pytz.timezone('Etc/UTC') - backend_trans_date = utc_tz.localize( - datetime.datetime.utcnow()).astimezone(backend_tz) + backend_trans_date = utc_tz.localize(datetime.datetime.utcnow()).astimezone(backend_tz) capture_day = (capture_date - backend_trans_date.date()).days if capture_day <= 0: raise ValueError("capture_date needs to be superior to the transaction date.") @@ -162,58 +194,58 @@ class Payment: return self.backend.request(amount, **kwargs) def response(self, query_string, **kwargs): - ''' - 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. + """ + 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. + 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 + Arguments: + query_string -- the URL encoded form-data from a GET or a POST - It returns a quadruplet of values: + It returns a quadruplet of values: - (result, transaction_id, bank_data, return_content) + (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. + - 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, **kwargs) def cancel(self, amount, bank_data, **kwargs): - ''' - Cancel or edit the amount of a transaction sent to the bank. + """ + Cancel or edit the amount of a transaction sent to the bank. - Arguments: - - amount -- the amount of money to cancel - - bank_data -- the transaction dictionary received from the bank - ''' + Arguments: + - amount -- the amount of money to cancel + - bank_data -- the transaction dictionary received from the bank + """ return self.backend.cancel(amount, bank_data, **kwargs) def validate(self, amount, bank_data, **kwargs): - ''' - Validate and trigger the transmission of a transaction to the bank. + """ + Validate and trigger the transmission of a transaction to the bank. - Arguments: - - amount -- the amount of money - - bank_data -- the transaction dictionary received from the bank - ''' + Arguments: + - amount -- the amount of money + - bank_data -- the transaction dictionary received from the bank + """ return self.backend.validate(amount, bank_data, **kwargs) def get_parameters(self, scope='global'): diff --git a/eopayment/__main__.py b/eopayment/__main__.py index 077fb61..ebbf158 100644 --- a/eopayment/__main__.py +++ b/eopayment/__main__.py @@ -67,7 +67,7 @@ def main(ctx, backend, debug, option, name): backend = config_backend load = True elif name and backend: - load = (config_backend == backend and config_name == name) + load = config_backend == backend and config_name == name elif name: load = config_name == name elif backend: @@ -123,4 +123,5 @@ def response(backend, query_string, param): for line in formatted_value.splitlines(False): print(' ', line) + main() diff --git a/eopayment/common.py b/eopayment/common.py index 400b66e..750c5f9 100644 --- a/eopayment/common.py +++ b/eopayment/common.py @@ -36,8 +36,7 @@ except ImportError: pass -__all__ = ['PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED', - 'PAID', 'ERROR', 'WAITING'] +__all__ = ['PaymentCommon', 'URL', 'HTML', 'RANDOM', 'RECEIVED', 'ACCEPTED', 'PAID', 'ERROR', 'WAITING'] RANDOM = random.SystemRandom() @@ -94,29 +93,38 @@ class ResponseError(PaymentException): class PaymentResponse: - '''Holds a generic view on the result of payment transaction response. + """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. - test -- indicates if the transaction was a test - 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. - a response transmitted directly from the bank to the merchant website, - you usually have to confirm good reception of the message by returning a - properly formatted response, this is it. - bank_status -- if result is False, it contains the reason - order_id -- the id given by the merchant in the payment request - transaction_id -- the id assigned by the bank to this transaction, it - could be the one sent by the merchant in the request, but it is usually - an identifier internal to the bank. - ''' + 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. + test -- indicates if the transaction was a test + 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. + a response transmitted directly from the bank to the merchant website, + you usually have to confirm good reception of the message by returning a + properly formatted response, this is it. + bank_status -- if result is False, it contains the reason + order_id -- the id given by the merchant in the payment request + transaction_id -- the id assigned by the bank to this transaction, it + could be the one sent by the merchant in the request, but it is usually + an identifier internal to the bank. + """ - def __init__(self, result=None, signed=None, bank_data=dict(), - return_content=None, bank_status='', transaction_id='', - order_id='', test=False, transaction_date=None): + def __init__( + self, + result=None, + signed=None, + bank_data=dict(), + return_content=None, + bank_status='', + transaction_id='', + order_id='', + test=False, + transaction_date=None, + ): self.result = result self.signed = signed self.bank_data = bank_data @@ -163,11 +171,9 @@ class PaymentCommon: 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 Exception: raise else: @@ -179,9 +185,12 @@ class PaymentCommon: try: amount = Decimal(amount) except ValueError: - raise ValueError('invalid amount %s: it must be a decimal integer with two digits ' - 'at most after the decimal point', amount) - if int(amount) < min_amount or (max_amount and int(amount) > max_amount): + raise ValueError( + 'invalid amount %s: it must be a decimal integer with two digits ' + 'at most after the decimal point', + amount, + ) + if int(amount) < min_amount or (max_amount and int(amount) > max_amount): raise ValueError('amount %s is not in range [%s, %s]' % (amount, min_amount, max_amount)) if cents: amount *= Decimal('100') # convert to cents @@ -190,8 +199,7 @@ class PaymentCommon: class Form: - def __init__(self, url, method, fields, encoding='utf-8', - submit_name='Submit', submit_value='Submit'): + def __init__(self, url, method, fields, encoding='utf-8', submit_name='Submit', submit_value='Submit'): self.url = url self.method = method self.fields = fields @@ -211,13 +219,14 @@ class Form: def escape(self, s): return html.escape(force_text(s, self.encoding)) - def __str__(self): s = '