From ef6a3a63b2f3bd4945de815fcf68db4e3d88929f Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 10 Oct 2012 15:25:13 +0200 Subject: [PATCH] add a TIPI backend fixes #1773 --- eopayment/common.py | 2 + eopayment/tipi.py | 168 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 eopayment/tipi.py diff --git a/eopayment/common.py b/eopayment/common.py index 21fb498..ff8db48 100644 --- a/eopayment/common.py +++ b/eopayment/common.py @@ -17,6 +17,8 @@ HTML = 2 RECEIVED = 1 ACCEPTED = 2 PAID = 3 +DENIED = 4 +CANCELED = 5 ERROR = 99 class PaymentResponse(object): diff --git a/eopayment/tipi.py b/eopayment/tipi.py new file mode 100644 index 0000000..f73d235 --- /dev/null +++ b/eopayment/tipi.py @@ -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')