Projet

Général

Profil

0001-avis_imposition-mimic-avis-imposition-API-particulie.patch

Benjamin Dauvergne, 01 juin 2020 09:48

Télécharger (31,1 ko)

Voir les différences:

Subject: [PATCH] avis_imposition: mimic avis-imposition API particulier
 endpoint (#43479)

Return value is the same as API particulier for the avis-imposition
endpoint, except for some data massaging put into alternative keys:
* dates are converted to ISO format and put in keys with @_iso@ suffix,
* family situation is simplified to ASCII and put in key with @_simple@
suffix,
* addresse is returned as oneline joined with newlines as in API
particulier, but also with individual lines separated into 3 keys :
adresse1, adresse2, adresse3.
 passerelle/apps/avis_imposition/__init__.py   |   0
 .../migrations/0001_initial.py                |  30 +++
 .../avis_imposition/migrations/__init__.py    |   0
 passerelle/apps/avis_imposition/models.py     | 233 ++++++++++++++++++
 passerelle/settings.py                        |   1 +
 tests/data/avis-imposition.html               |  85 +++++++
 tests/data/avis-imposition.json               |  86 +++++++
 tests/test_avis_imposition.py                 | 116 +++++++++
 8 files changed, 551 insertions(+)
 create mode 100644 passerelle/apps/avis_imposition/__init__.py
 create mode 100644 passerelle/apps/avis_imposition/migrations/0001_initial.py
 create mode 100644 passerelle/apps/avis_imposition/migrations/__init__.py
 create mode 100644 passerelle/apps/avis_imposition/models.py
 create mode 100644 tests/data/avis-imposition.html
 create mode 100644 tests/data/avis-imposition.json
 create mode 100644 tests/test_avis_imposition.py
passerelle/apps/avis_imposition/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.20 on 2020-05-28 19:49
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    initial = True
11

  
12
    dependencies = [
13
        ('base', '0020_auto_20200515_1923'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='AvisImposition',
19
            fields=[
20
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
                ('title', models.CharField(max_length=50, verbose_name='Title')),
22
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
23
                ('description', models.TextField(verbose_name='Description')),
24
                ('users', models.ManyToManyField(blank=True, related_name='_avisimposition_users_+', related_query_name='+', to='base.ApiUser')),
25
            ],
26
            options={
27
                'verbose_name': 'API Particulier',
28
            },
29
        ),
30
    ]
passerelle/apps/avis_imposition/models.py
1
# coding: utf-8
2
# passerelle - uniform access to multiple data sources and services
3
# Copyright (C) 2017  Entr'ouvert
4
#
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU Affero General Public License as published
7
# by the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Affero General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

  
18
'''Interface with "Service de vérification des avis" of impots.gouv.fr
19
'''
20

  
21
from __future__ import unicode_literals
22

  
23
import datetime
24
import logging
25
import re
26

  
27
import lxml.html
28
import requests
29

  
30
from django.utils.translation import ugettext_lazy as _
31
from django.utils.six.moves.urllib_parse import urljoin
32

  
33
from passerelle.base.models import BaseResource
34
from passerelle.utils.api import endpoint
35
from passerelle.utils.jsonresponse import APIError
36
from passerelle.utils.xml import text_content
37
from passerelle.utils.json import unflatten
38

  
39
AVIS_IMPOSITION_GOUV_FR_URL = 'https://cfsmsp.impots.gouv.fr/secavis/'
40

  
41

  
42
def remove_spaces(value):
43
    return re.sub(r'\s', '', value, flags=re.U)
44

  
45

  
46
def simplify_spaces(value):
47
    return re.sub(r'\s+', ' ', value, flags=re.U)
48

  
49

  
50
FIELD_INDEXES = {
51
    'declarant1/nom': 4,
52
    'declarant2/nom': 5,
53
    'declarant1/nomNaissance': 7,
54
    'declarant2/nomNaissance': 8,
55
    'declarant1/prenom': 10,
56
    'declarant2/prenom': 11,
57
    'declarant1/dateNaissance': 13,
58
    'declarant2/dateNaissance': 14,
59
    'declarant1/dateNaissance_iso': 13,
60
    'declarant2/dateNaissance_iso': 14,
61
    'dateRecouvrement': 24,
62
    'dateEtablissement': 26,
63
    'dateRecouvrement_iso': 24,
64
    'dateEtablissement_iso': 26,
65
    'nombreParts': 28,
66
    'situationFamille': 30,
67
    'situationFamille_simple': 30,
68
    'nombrePersonnesCharge': 32,
69
    'revenuBrutGlobal': 34,
70
    'revenuImposable': 36,
71
    'impotRevenuNetAvantCorrections': 38,
72
    'montantImpot': 40,
73
    'revenuFiscalReference': 42,
74
    'foyerFiscal/adresse1': 16,
75
    'foyerFiscal/adresse2': 19,
76
    'foyerFiscal/adresse3': 21,
77
}
78

  
79

  
80
def nombre_de_parts(value):
81
    try:
82
        return float(value)
83
    except (ValueError, TypeError):
84
        return None
85

  
86

  
87
def euros(value):
88
    if not value:
89
        return value
90
    value = remove_spaces(value)
91
    value = value.rstrip('€')
92
    try:
93
        return int(value)
94
    except ValueError:
95
        return value
96

  
97

  
98
def situation_familiale(value):
99
    if value == 'Pacs\xe9(e)s':
100
        return 'pacs/mariage'
101
    if value == 'Mari\xe9(e)s':
102
        return 'pacs/mariage'
103
    if value == 'Divorc\xe9(e)':
104
        return 'divorce'
105
    if value == 'C\xe9libataire':
106
        return 'celibataire'
107
    raise NotImplementedError(value)
108

  
109

  
110
def date(value):
111
    if not value:
112
        return ''
113
    try:
114
        return datetime.datetime.strptime(value, '%d/%m/%Y').date()
115
    except (ValueError, TypeError):
116
        return ''
117

  
118
ADAPTERS = {
119
    'nombreParts': nombre_de_parts,
120
    'revenuBrutGlobal': euros,
121
    'revenuImposable': euros,
122
    'impotRevenuNetAvantCorrections': euros,
123
    'revenuFiscalReference': euros,
124
    'montantImpot': euros,
125
    'situationFamille_simple': situation_familiale,
126
    'dateEtablissement_iso': date,
127
    'dateRecouvrement_iso': date,
128
    'declarant1/dateNaissance_iso': date,
129
    'declarant2/dateNaissance_iso': date,
130
    'nombrePersonnesCharge': nombre_de_parts,
131
}
132

  
133

  
134
def get_form(logger=None):
135
    logger = logger or logging
136
    try:
137
        response = requests.get(AVIS_IMPOSITION_GOUV_FR_URL)
138
        response.raise_for_status()
139
    except requests.RequestException as e:
140
        raise APIError('service-is-down', data=str(e))
141

  
142
    if 'Saisissez les identifiants' not in response.text:
143
        raise RuntimeError('service has changed')
144

  
145
    html = lxml.html.fromstring(response.content)
146
    data = {}
147
    for form_elt in html.xpath('//form'):
148
        if 'action' not in form_elt.attrib:
149
            continue
150
        action_url = form_elt.attrib['action']
151
        break
152
    else:
153
        raise RuntimeError('service has changed')
154
    logger.debug('using found action_url %s', action_url)
155

  
156
    for input_elt in html.xpath('//input'):
157
        if 'value' in input_elt.attrib:
158
            data[input_elt.attrib['name']] = input_elt.attrib['value']
159

  
160
    return action_url, data
161

  
162

  
163
def get_avis_imposition(numero_fiscal, reference_avis, logger=None):
164
    logger = logger or logging
165

  
166
    action_url, data = get_form()
167
    data['j_id_7:spi'] = numero_fiscal
168
    data['j_id_7:num_facture'] = reference_avis
169

  
170
    logger.debug('sending data %s', data)
171

  
172
    try:
173
        response = requests.post(urljoin(AVIS_IMPOSITION_GOUV_FR_URL, action_url), params=data)
174
        response.raise_for_status()
175
    except requests.RequestException as e:
176
        raise APIError('service-is-down', data=str(e))
177

  
178
    if 'Saisissez les identifiants' in response.text:
179
        raise APIError('not-found')
180

  
181
    response_html = lxml.html.fromstring(response.content)
182
    td_contents = [simplify_spaces(text_content(td).strip()) for td in response_html.xpath('//td')]
183

  
184
    logger.debug('got td_contents %s', td_contents)
185

  
186
    data = {
187
        # https://github.com/betagouv/svair-api/blob/master/utils/year.js
188
        'foyerFiscal/year': int('20' + reference_avis[:2]),
189
    }
190
    for field, index in FIELD_INDEXES.items():
191
        try:
192
            data[field] = td_contents[index] or ''
193
        except IndexError:
194
            raise RuntimeError('service has changed')
195
        if field in ADAPTERS:
196
            data[field] = ADAPTERS[field](data[field])
197
    for situation_partielle_elt in response_html.xpath('//*[@id="situationPartielle"]'):
198
        data['situationPartielle'] = text_content(situation_partielle_elt).strip()
199
        break
200
    return data
201

  
202

  
203
class AvisImposition(BaseResource):
204
    @endpoint(perm='can_access',
205
              description=_('Get citizen\'s fiscal informations'),
206
              parameters={
207
                  'numero_fiscal': {
208
                      'description': _('fiscal identifier'),
209
                      'example_value': '1562456789521',
210
                  },
211
                  'reference_avis': {
212
                      'description': _('tax notice number'),
213
                      'example_value': '1512456789521',
214
                  },
215
              })
216
    def verify(self, request, numero_fiscal, reference_avis):
217
        numero_fiscal = remove_spaces(numero_fiscal)
218
        reference_avis = remove_spaces(reference_avis)
219
        data = get_avis_imposition(numero_fiscal, reference_avis)
220
        unflattened = unflatten(data)
221
        foyer_fiscal = unflattened['foyerFiscal']
222
        foyer_fiscal['adresse'] = ' '.join(
223
            value for key, value in sorted(foyer_fiscal.items()) if key.startswith('adresse') and value
224
        )
225
        return {'data': unflattened}
226

  
227
    def check_status(self):
228
        get_form()
229

  
230
    category = _('Business Process Connectors')
231

  
232
    class Meta:
233
        verbose_name = _('API Particulier')
passerelle/settings.py
129 129
    'passerelle.apps.astregs',
130 130
    'passerelle.apps.atal',
131 131
    'passerelle.apps.atos_genesys',
132
    'passerelle.apps.avis_imposition',
132 133
    'passerelle.apps.base_adresse',
133 134
    'passerelle.apps.bdp',
134 135
    'passerelle.apps.cartads_cs',
tests/data/avis-imposition.html
1
<!DOCTYPE html>
2
<html xmlns="http://www.w3.org/1999/xhtml"><head>
3
<meta http-equiv="Content-type" content="text/html; charset=UTF-8">
4

  
5
<title>Impots.gouv.fr - Service de vérification en ligne des avis</title>
6

  
7
<link href="coin_fichiers/style.css" rel="styleSheet" type="text/css">
8

  
9
<script type="text/javascript" src="coin_fichiers/fonctions.js"></script>
10

  
11
</head>
12
<body>
13
<div id="conteneur">
14
<div id="barre_haut">
15
			<div style="float: left;"><img src="coin_fichiers/bo_seule-2.gif" alt=""></div>
16
			<div style="float: right;"><img src="coin_fichiers/bo_seule-3.gif" alt=""></div>
17
</div>
18
<div id="principal">
19
<div id="nav_pro">
20
<b>Bienvenue sur le service de vérification des avis</b>
21
</div>
22

  
23
<div id="infoService"><p>Le service permet de vérifier l'authenticité des avis (Impôt sur le revenu) présentés par un usager</p></div>
24
<br>
25
<div class="titre"><span>Accès au service de vérification</span></div>
26
<br>
27
<div class="titre2">Saisissez les identifiants</div><form id="j_id_7" name="j_id_7" method="post" action="/secavis/faces/commun/index.jsf" enctype="application/x-www-form-urlencoded">
28
	<table>
29
		<tbody>
30
			<tr>
31
				<td width="290"> 
32
					<br><br>
33
				</td>
34
			</tr>		
35
			<tr>
36
				<td> 
37
					<label>Numéro fiscal *</label><a href="#" onclick="win = ouvrePopup('/secavis/faces/commun/aideSpi.jsf', 523, 375); win.focus();" tabindex="4"><img src="coin_fichiers/pic_aide_pro.gif" alt="aideSPI" style="vertical-align:middle"></a>					
38
				</td>
39
			</tr>
40
			<tr>
41
				<td><input id="j_id_7:spi" name="j_id_7:spi" type="text" maxlength="13" size="15" tabindex="1" autocomplete="off">
42
				</td>
43
				
44
			</tr>
45
			<tr>
46
				<td></td>
47
			</tr>
48
			<tr>
49
				<td>
50
				 <label>Référence de l'avis *</label><a href="#" onclick="win = ouvrePopup('/secavis/faces/commun/aideNumFacture.jsf', 520, 375); win.focus();" tabindex="5"><img src="coin_fichiers/pic_aide_pro.gif" alt="aideReferenceAvis" style="vertical-align:middle"></a>	
51
				</td>
52
			</tr>
53
			<tr>
54
				<td><input id="j_id_7:num_facture" name="j_id_7:num_facture" type="text" maxlength="13" size="15" tabindex="2" autocomplete="off">
55
				</td>
56
				
57
			</tr>
58
			
59

  
60
			<tr>
61
				<td></td>
62
			</tr>
63
			<tr>
64
				<td></td>
65
				<td></td>
66

  
67
				<td>
68
				<div align="right">
69
					
70
					<div class="bloc_boutons"><input id="j_id_7:j_id_l" name="j_id_7:j_id_l" type="submit" value="Valider" title="Vérifier les informations d'avis" class="valider" tabindex="3">
71
					</div>
72

  
73
			 	</div>
74
				</td>
75
			</tr>
76

  
77
		</tbody>
78
	</table><input type="hidden" name="j_id_7_SUBMIT" value="1"><input type="hidden" name="javax.faces.ViewState" id="j_id__v_0:javax.faces.ViewState:1" value="RxJe/1JKTJSr3aiM3H9DqZq0DrwqEXsY7Rw4eLRgEBsCF1IALJGqVgWTaQkiKbbdcGDWW774BWUCa/+j2CDznhw1/3bxJteY6ZCui66yNevhkej4xuyrFMte5KQnKORt9JZrOQ=="></form>
79
<br>
80
<div id="donneesObligatoires">* données obligatoires</div>
81
</div>
82
<div id="bas_page">© Ministère de l'Économie et des Finances</div><img src="coin_fichiers/hit.gif" alt="" width="1" height="1">
83
</div>
84

  
85
<div id="grammalecte_menu_main_button_shadow_host" style="width: 0px; height: 0px;"></div></body><script src="coin_fichiers/api.js"></script></html>
tests/data/avis-imposition.json
1
[
2
  {
3
    "numero_fiscal": "1234",
4
    "reference_avis": "18abcd",
5
    "response": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\t\n<head>\n<meta http-equiv=\"Content-type\" content=\"text/html; charset=UTF-8\" />\n\n<title>Impots.gouv.fr - Service de v\u00e9rification en ligne des avis</title>\n<link href=\"/secavis/css/style.css\" rel=\"styleSheet\" type=\"text/css\" />\n</head>\n\n<body>\n\n<div id=\"conteneur\">\n<div id=\"barre_haut\">\n\t\t\t<div style=\"float: left;\"><img src=\"/secavis/img/bo_seule-2.gif\" alt=\"\" /></div>\n\t\t\t<div style=\"float: right;\"><img src=\"/secavis/img/bo_seule-3.gif\" alt=\"\" /></div>\n</div>\n<div id=\"principal\">\n<div id=\"nav_pro\">\n<b>L'administration fiscale certifie l'authenticit\u00e9 du document pr\u00e9sent\u00e9 pour les donn\u00e9es suivantes :</b>\n</div>\n\n<div class=\"titre_affiche_avis\">\n<span>Imp\u00f4t   2019 sur les revenus de l'ann\u00e9e  2018  \n</span>\n</div>\t\t\n\t\t\n\n\t\t\t<table>\n\t\t\t\t<tbody>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"label\">\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"label\">D\u00e9clarant 1\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"label\">D\u00e9clarant 2\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">Nom\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">DOE\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">DOE\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\">Nom de naissance\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelImpair\">DOE\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelImpair\">DOE\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">Pr\u00e9nom(s)\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">JOHN\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">JANE\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\">Date de naissance\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelImpair\">01/01/1970\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelImpair\">\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t\tAdresse d\u00e9clar\u00e9e au 1<sup>er</sup> janvier  2019\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">R\u00c9SIDENCE DU CALVAIRE\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t</td>\n\t\t\t\t\t<td colspan=\"2\" class=\"labelPair\">RUE VICTOR HUGO\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t</td>\n\t\t\t\t\t<td colspan=\"2\" class=\"labelPair\">75014 PARIS\n\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"espace\"></td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Date de mise en recouvrement de l'avis d'imp\u00f4t\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">31/12/2019\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Date d'\u00e9tablissement\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">09/12/2019\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Nombre de part(s)\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">4.00\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Situation de famille\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">Pacs\u00e9(e)s\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Nombre de personne(s) \u00e0 charge\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">4\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Revenu brut global\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">48\u00a0473 \u20ac\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Revenu imposable\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">48\u00a0473 \u20ac\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Imp\u00f4t sur le revenu net avant corrections\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">112 \u20ac\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Montant de l'imp\u00f4t\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">Non imposable\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\t\t\t\t\t\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Revenu fiscal de r\u00e9f\u00e9rence\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">48\u00a0473 \u20ac\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\t\t\t\t\t\t\t\n\n\t\t\t</tbody></table>\n\t\t\t\n\t\t\t<div id=\"situationPartielle\">\n\t\t\t</div>\n\t\t\t\t\n\t\t\t<div id=\"boutonsAvis\">\t\t\n\t\t\t\t<a class=\"nouvelle_recherche\" href=\"/secavis/\">\t\t\t\n\t\t\t\t\tNouvelle recherche\t\t\t\t\n\t\t\t\t</a>\n\t\t\t </div>\n\n\t\t</div>\n\t\t<div id=\"bas_page\">\u00a9 Minist\u00e8re de l'\u00c9conomie et des Finances</div><img src=\"https://logs1279.xiti.com/hit.xiti?s=532158&amp;s2=3&amp;p=AFFICHAGE::Avis_Sans_Correctif_erreur_pas_javascript&amp;\" alt=\"\" height=\"1\" width=\"1\" />\n\t</div>\n</body>\n</html>",
6
    "result": {
7
      "dateEtablissement": "09/12/2019",
8
      "dateRecouvrement": "31/12/2019",
9
      "dateEtablissement_iso": "2019-12-09",
10
      "dateRecouvrement_iso": "2019-12-31",
11
      "declarant1": {
12
        "dateNaissance": "01/01/1970",
13
        "dateNaissance_iso": "1970-01-01",
14
        "nom": "DOE",
15
        "nomNaissance": "DOE",
16
        "prenom": "JOHN"
17
      },
18
      "declarant2": {
19
        "dateNaissance": "",
20
        "dateNaissance_iso": "",
21
        "nom": "DOE",
22
        "nomNaissance": "DOE",
23
        "prenom": "JANE"
24
      },
25
      "foyerFiscal": {
26
        "adresse": "R\u00c9SIDENCE DU CALVAIRE RUE VICTOR HUGO 75014 PARIS",
27
        "adresse1": "R\u00c9SIDENCE DU CALVAIRE",
28
        "adresse2": "RUE VICTOR HUGO",
29
        "adresse3": "75014 PARIS",
30
        "year": 2018
31
      },
32
      "impotRevenuNetAvantCorrections": 112,
33
      "montantImpot": "Nonimposable",
34
      "nombreParts": 4.00,
35
      "nombrePersonnesCharge": 4.00,
36
      "revenuBrutGlobal": 48473,
37
      "revenuFiscalReference": 48473,
38
      "revenuImposable": 48473,
39
      "situationFamille": "Pacsé(e)s",
40
      "situationFamille_simple": "pacs/mariage",
41
      "situationPartielle": ""
42
    }
43
  },
44
  {
45
    "numero_fiscal": "1234",
46
    "reference_avis": "18abcd",
47
    "response": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\t\n<head>\n<meta http-equiv=\"Content-type\" content=\"text/html; charset=UTF-8\" />\n\n<title>Impots.gouv.fr - Service de v\u00e9rification en ligne des avis</title>\n<link href=\"/secavis/css/style.css\" rel=\"styleSheet\" type=\"text/css\" />\n</head>\n\n<body>\n\n<div id=\"conteneur\">\n<div id=\"barre_haut\">\n\t\t\t<div style=\"float: left;\"><img src=\"/secavis/img/bo_seule-2.gif\" alt=\"\" /></div>\n\t\t\t<div style=\"float: right;\"><img src=\"/secavis/img/bo_seule-3.gif\" alt=\"\" /></div>\n</div>\n<div id=\"principal\">\n<div id=\"nav_pro\">\n<b>L'administration fiscale certifie l'authenticit\u00e9 du document pr\u00e9sent\u00e9 pour les donn\u00e9es suivantes :</b>\n</div>\n\n<div class=\"titre_affiche_avis\">\n<span>Imp\u00f4t   2019 sur les revenus de l'ann\u00e9e  2018  \n</span>\n</div>\t\t\n\t\t\n\n\t\t\t<table>\n\t\t\t\t<tbody>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"label\">\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"label\">D\u00e9clarant 1\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"label\">D\u00e9clarant 2\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">Nom\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">DOE\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\">Nom de naissance\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelImpair\">DOE\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelImpair\">\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">Pr\u00e9nom(s)\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">JOHN\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\">Date de naissance\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelImpair\">01/01/1970\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelImpair\">\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t\tAdresse d\u00e9clar\u00e9e au 1<sup>er</sup> janvier  2019\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">R\u00c9SIDENCE DU CALVAIRE\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t</td>\n\t\t\t\t\t<td colspan=\"2\" class=\"labelPair\">RUE VICTOR HUGO\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\">\n\t\t\t\t\t</td>\n\t\t\t\t\t<td colspan=\"2\" class=\"labelPair\">75014 PARIS\n\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"espace\"></td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Date de mise en recouvrement de l'avis d'imp\u00f4t\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">31/07/2019\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Date d'\u00e9tablissement\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">09/07/2019\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Nombre de part(s)\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">2.00\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Situation de famille\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">Divorc\u00e9(e)\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Nombre de personne(s) \u00e0 charge\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">2\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Revenu brut global\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">48\u00a0473 \u20ac\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Revenu imposable\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">48\u00a0473 \u20ac\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Imp\u00f4t sur le revenu net avant corrections\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">5\u00a0084 \u20ac\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelPair\" colspan=\"2\">Montant de l'imp\u00f4t\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textPair\">Non imposable\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\t\t\t\t\t\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"labelImpair\" colspan=\"2\">Revenu fiscal de r\u00e9f\u00e9rence\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"textImpair\">48\u00a0473 \u20ac\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\t\t\t\t\t\t\t\n\n\t\t\t</tbody></table>\n\t\t\t\n\t\t\t<div id=\"situationPartielle\">\n\t\t\t</div>\n\t\t\t\t\n\t\t\t<div id=\"boutonsAvis\">\t\t\n\t\t\t\t<a class=\"nouvelle_recherche\" href=\"/secavis/\">\t\t\t\n\t\t\t\t\tNouvelle recherche\t\t\t\t\n\t\t\t\t</a>\n\t\t\t </div>\n\n\t\t</div>\n\t\t<div id=\"bas_page\">\u00a9 Minist\u00e8re de l'\u00c9conomie et des Finances</div><img src=\"https://logs1279.xiti.com/hit.xiti?s=532158&amp;s2=3&amp;p=AFFICHAGE::Avis_Sans_Correctif_erreur_pas_javascript&amp;\" alt=\"\" height=\"1\" width=\"1\" />\n\t</div>\n</body>\n</html>",
48
    "result": {
49
      "dateEtablissement": "09/07/2019",
50
      "dateRecouvrement": "31/07/2019",
51
      "dateEtablissement_iso": "2019-07-09",
52
      "dateRecouvrement_iso": "2019-07-31",
53
      "declarant1": {
54
        "dateNaissance": "01/01/1970",
55
        "dateNaissance_iso": "1970-01-01",
56
        "nom": "DOE",
57
        "nomNaissance": "DOE",
58
        "prenom": "JOHN"
59
      },
60
      "declarant2": {
61
        "dateNaissance": "",
62
        "dateNaissance_iso": "",
63
        "nom": "",
64
        "nomNaissance": "",
65
        "prenom": ""
66
      },
67
      "foyerFiscal": {
68
        "adresse": "R\u00c9SIDENCE DU CALVAIRE RUE VICTOR HUGO 75014 PARIS",
69
        "adresse1": "R\u00c9SIDENCE DU CALVAIRE",
70
        "adresse2": "RUE VICTOR HUGO",
71
        "adresse3": "75014 PARIS",
72
        "year": 2018
73
      },
74
      "impotRevenuNetAvantCorrections": 5084,
75
      "montantImpot": "Nonimposable",
76
      "nombreParts": 2.00,
77
      "nombrePersonnesCharge": 2.00,
78
      "revenuBrutGlobal": 48473,
79
      "revenuFiscalReference": 48473,
80
      "revenuImposable": 48473,
81
      "situationFamille": "Divorcé(e)",
82
      "situationFamille_simple": "divorce",
83
      "situationPartielle": ""
84
    }
85
  }
86
]
tests/test_avis_imposition.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2016 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from __future__ import unicode_literals
18

  
19
import json
20

  
21
import requests
22

  
23
import httmock
24

  
25
import pytest
26

  
27
from passerelle.apps.avis_imposition.models import AvisImposition
28

  
29
import utils
30

  
31

  
32
# Content from the form page
33
with open('tests/data/avis-imposition.html') as fd:
34
    html = fd.read()
35

  
36
# Contents from the submit result page
37
with open('tests/data/avis-imposition.json') as fd:
38
    data = json.load(fd)
39

  
40

  
41
@pytest.fixture
42
def avis_imposition(db):
43
    return utils.make_resource(AvisImposition, slug='test')
44

  
45

  
46
@pytest.mark.parametrize('data', data)
47
def test_ok(avis_imposition, data, app):
48
    @httmock.urlmatch()
49
    def http_response(url, request):
50
        if request.method == 'GET':
51
            return html
52
        else:
53
            return data['response']
54

  
55
    with httmock.HTTMock(http_response):
56
        response = utils.endpoint_get(
57
            expected_url='/avis-imposition/test/verify',
58
            app=app,
59
            resource=avis_imposition,
60
            endpoint='verify',
61
            params={
62
                'numero_fiscal': data['numero_fiscal'],
63
                'reference_avis': data['reference_avis'],
64
            })
65
    assert {key: value for key, value in response.json['data'].items()} == data['result']
66

  
67

  
68
def test_not_found(avis_imposition, app):
69
    @httmock.urlmatch()
70
    def http_response(url, request):
71
        return html
72

  
73
    with httmock.HTTMock(http_response):
74
        response = utils.endpoint_get(
75
            expected_url='/avis-imposition/test/verify',
76
            app=app,
77
            resource=avis_imposition,
78
            endpoint='verify',
79
            params={
80
                'numero_fiscal': '1234',
81
                'reference_avis': 'abcd',
82
            })
83
    assert response.json['err_desc'] == 'not-found'
84

  
85

  
86
@pytest.mark.parametrize('error', [
87
    'exception-on-get',
88
    '500-on-get',
89
    'exception-on-post',
90
    '500-on-post',
91
])
92
def test_request_error(avis_imposition, app, error):
93
    @httmock.urlmatch()
94
    def http_response(url, request):
95
        if error == 'exception-on-get':
96
            raise requests.RequestException('boom')
97
        if error == '500-on-get':
98
            return {'status_code': 500}
99
        if request.method == 'GET':
100
            return html
101
        if error == 'exception-on-post':
102
            raise requests.RequestException('boom')
103
        if error == '500-on-post':
104
            return {'status_code': 500}
105

  
106
    with httmock.HTTMock(http_response):
107
        response = utils.endpoint_get(
108
            expected_url='/avis-imposition/test/verify',
109
            app=app,
110
            resource=avis_imposition,
111
            endpoint='verify',
112
            params={
113
                'numero_fiscal': '1234',
114
                'reference_avis': 'abcd',
115
            })
116
    assert response.json['err_desc'] == 'service-is-down'
0
-