summaryrefslogtreecommitdiffstats
path: root/eopayment/tipi.py
blob: 015e93903bc0ca7b32e5aea04d5a3a81fe629483 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import re
import random
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 = 'https://www.tipi.budget.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'Client number'),
                'help_text': _(u'6 digits number provided by DGFIP'),
                'validation': lambda s: str.isdigit(s) and (0 < int(s) < 1000000),
                'required': True,
            },
            {
                'name': 'service_url',
                'default': TIPI_URL,
                'caption': _(u'TIPI service URL'),
                'help_text': _(u'do not modify if you do not know'),
                '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',
                'choices': [
                    ('T', _('test')),
                    ('X', _('production')),
                    ('A', _('with user account')),
                    ('M', _('manual entry')),
                ],
            },
        ],
    }

    REFDET_RE = re.compile('^[a-zA-Z0-9]{6,30}$')

    def _generate_refdet(self):
        return '%s%10d' % (isonow(), random.randint(1, 1000000000))

    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')
        assert not (orderid and refdet), 'orderid and refdet cannot be used together'
        # check or generate refdet
        if refdet:
            try:
                if not self.REFDET_RE.match(refdet):
                    raise ValueError
            except (TypeError, ValueError):
                raise ValueError('refdet must be 6 to 30 alphanumeric characters string')
        if orderid:
            if self.REFDET_RE.match(orderid):
                refdet = orderid
            else:
                objet = orderid + (' ' + objet) if objet else ''
        if not refdet:
            refdet = self._generate_refdet()
            transaction_id = refdet
        else:
            transaction_id = '%s_%s' % (refdet, random.randint(1, 1000000000))
        # check objet or fix objet
        if objet is not None:
            try:
                objet = objet.encode('ascii')
            except Exception as e:
                raise ValueError('OBJET must be an alphanumeric string', e)
        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)

        # check saisie
        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)

        params = {
            'numcli': self.numcli,
            'refdet': refdet,
            'montant': montant,
            'mel': mel,
            'saisie': saisie,
        }
        if exer:
            params['exer'] = exer
        if objet:
            params['objet'] = objet
        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('missing refdet or resultrans')
        for key, value in fields.items():
            fields[key] = value[0]
        refdet = fields.get('refdet')
        if refdet is None:
            raise ResponseError('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)