summaryrefslogtreecommitdiffstats
path: root/eopayment/tipi.py
blob: c53101e52aaec8496504d2e2cfdda2a245a2c016 (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
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')),
                ],
            },
        ],
    }

    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('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)