# -*- coding: utf-8 -*- from decimal import Decimal, ROUND_DOWN from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED, CANCELLED, ERROR, ResponseError) from six.moves.urllib.parse import urlencode, parse_qs from gettext import gettext as _ import logging import warnings from .systempayv2 import isonow __all__ = ['Payment'] TIPI_URL = 'http://www.jepaiemesserviceslocaux.dgfip.finances.gouv.fr' \ '/tpa/paiement.web' LOGGER = logging.getLogger(__name__) 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, }, { 'name': 'normal_return_url', 'caption': _('Normal return URL (unused by TIPI)'), 'required': False, }, { 'name': 'automatic_return_url', 'caption': _('Automatic return URL'), 'required': True, }, { 'name': 'saisie', 'caption': _('Payment type'), 'required': True, 'default': 'T', }, ], } def request(self, amount, next_url=None, exer=None, orderid=None, refdet=None, objet=None, email=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)) automatic_return_url = self.automatic_return_url if next_url and not automatic_return_url: warnings.warn("passing next_url to request() is deprecated, " "set automatic_return_url in options", DeprecationWarning) automatic_return_url = next_url if automatic_return_url is not None: if not isinstance(automatic_return_url, str) or \ not automatic_return_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 = orderid or refdet refdet = str(refdet) if 6 > len(refdet) > 30: raise ValueError('len(REFDET) < 6 or > 30') except Exception as e: raise ValueError('REFDET format invalide, %r' % refdet, e) if objet is not None: try: objet = str(objet) except Exception as 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(email) 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 as e: raise ValueError('MEL is not a valid email, %r' % mel, e) saisie = saisie or self.saisie 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] + ' ' + iso_now else: objet = iso_now params = { 'numcli': self.numcli, 'refdet': refdet, 'montant': montant, 'mel': mel, 'saisie': saisie, 'objet': objet, } if exer: params['exer'] = exer if automatic_return_url: params['urlcl'] = automatic_return_url url = '%s?%s' % (self.service_url, urlencode(params)) return transaction_id, URL, url def response(self, query_string, **kwargs): fields = parse_qs(query_string, True) if not set(fields) >= set(['refdet', 'resultrans']): raise ResponseError() for key, value in fields.items(): fields[key] = value[0] refdet = fields.get('refdet') if refdet is None: raise ValueError('refdet is missing') if 'objet' in fields: iso_now = fields['objet'] 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 = CANCELLED bank_status = 'canceled' else: bank_status = 'wrong return: %r' % result result = ERROR test = fields.get('saisie') == 'T' return PaymentResponse( result=result, bank_status=bank_status, signed=True, bank_data=fields, transaction_id=transaction_id, test=test)