eopayment/eopayment/tipi.py

183 lines
6.6 KiB
Python
Raw Normal View History

2012-10-10 15:25:13 +02:00
# -*- coding: utf-8 -*-
from decimal import Decimal, ROUND_DOWN
2018-03-26 09:30:17 +02:00
from .common import (PaymentCommon, PaymentResponse, URL, PAID, DENIED,
CANCELLED, ERROR, ResponseError)
from six.moves.urllib.parse import urlencode, parse_qs
2012-10-10 15:25:13 +02:00
from gettext import gettext as _
import logging
import warnings
2012-10-10 15:25:13 +02:00
2018-06-29 15:10:30 +02:00
from .systempayv2 import isonow
2012-10-10 15:25:13 +02:00
__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 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,
},
{
'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',
},
2012-10-10 15:25:13 +02:00
],
}
2016-02-08 18:40:45 +01:00
def request(self, amount, next_url=None, exer=None, orderid=None,
refdet=None, objet=None, email=None, saisie=None, **kwargs):
2012-10-10 15:25:13 +02:00
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'):
2012-10-10 15:25:13 +02:00
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:
2016-02-08 18:40:45 +01:00
refdet = orderid or refdet
2012-10-10 15:25:13 +02:00
refdet = str(refdet)
2014-03-19 12:35:06 +01:00
if 6 > len(refdet) > 30:
raise ValueError('len(REFDET) < 6 or > 30')
2018-06-29 14:53:40 +02:00
except Exception as e:
2012-10-10 15:25:13 +02:00
raise ValueError('REFDET format invalide, %r' % refdet, e)
if objet is not None:
try:
objet = str(objet)
2018-06-29 14:53:40 +02:00
except Exception as e:
2012-10-10 15:25:13 +02:00
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:
2014-03-19 12:35:06 +01:00
mel = str(email)
if '@' not in mel:
2012-10-10 15:25:13 +02:00
raise ValueError('no @ in MEL')
if not (6 <= len(mel) <= 80):
2012-10-10 15:25:13 +02:00
raise ValueError('len(MEL) is invalid, must be between 6 and 80')
2018-06-29 14:53:40 +02:00
except Exception as e:
raise ValueError('MEL is not a valid email, %r' % mel, e)
2012-10-10 15:25:13 +02:00
saisie = saisie or self.saisie
2012-10-10 15:25:13 +02:00
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:
2014-03-06 16:58:53 +01:00
objet = objet[:100-len(iso_now)-2] + ' ' + iso_now
2012-10-10 15:25:13 +02:00
else:
2014-03-06 16:58:53 +01:00
objet = iso_now
2012-10-10 15:25:13 +02:00
params = {
2014-03-06 16:58:53 +01:00
'numcli': self.numcli,
'refdet': refdet,
'montant': montant,
'mel': mel,
'saisie': saisie,
'objet': objet,
2012-10-10 15:25:13 +02:00
}
if exer:
2014-03-06 16:58:53 +01:00
params['exer'] = exer
if automatic_return_url:
params['urlcl'] = automatic_return_url
2012-10-10 15:25:13 +02:00
url = '%s?%s' % (self.service_url, urlencode(params))
return transaction_id, URL, url
def response(self, query_string, **kwargs):
2012-10-10 15:25:13 +02:00
fields = parse_qs(query_string, True)
if not set(fields) >= set(['refdet', 'resultrans']):
raise ResponseError()
for key, value in fields.items():
2012-10-10 15:25:13 +02:00
fields[key] = value[0]
2014-03-11 17:20:45 +01:00
refdet = fields.get('refdet')
2012-10-10 15:25:13 +02:00
if refdet is None:
2014-03-11 17:20:45 +01:00
raise ValueError('refdet is missing')
if 'objet' in fields:
iso_now = fields['objet']
2012-10-10 15:25:13 +02:00
else:
iso_now = isonow()
transaction_id = '%s_%s' % (iso_now, refdet)
2014-03-11 17:20:45 +01:00
result = fields.get('resultrans')
2012-10-10 15:25:13 +02:00
if result == 'P':
result = PAID
bank_status = ''
elif result == 'R':
result = DENIED
bank_status = 'refused'
elif result == 'A':
result = CANCELLED
2012-10-10 15:25:13 +02:00
bank_status = 'canceled'
else:
bank_status = 'wrong return: %r' % result
result = ERROR
test = fields.get('saisie') == 'T'
2012-10-10 15:25:13 +02:00
return PaymentResponse(
result=result,
bank_status=bank_status,
signed=True,
bank_data=fields,
transaction_id=transaction_id,
test=test)