avis_imposition: mimic avis-imposition API particulier endpoint (#43479)
gitea/passerelle/pipeline/head This commit looks good
Details
gitea/passerelle/pipeline/head This commit looks good
Details
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. * fake datas for testing can be configured trough AVIS_IMPOSITION_FAKE_DATA.
This commit is contained in:
parent
0ed60d380d
commit
1de7845982
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 1.11.20 on 2020-05-28 19:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('base', '0020_auto_20200515_1923'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AvisImposition',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('title', models.CharField(max_length=50, verbose_name='Title')),
|
||||
('slug', models.SlugField(unique=True, verbose_name='Identifier')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
(
|
||||
'users',
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name='_avisimposition_users_+',
|
||||
related_query_name='+',
|
||||
to='base.ApiUser',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'API Particulier',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,478 @@
|
|||
# passerelle - uniform access to multiple data sources and services
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
'''Interface with "Service de vérification des avis" of impots.gouv.fr
|
||||
|
||||
See also:
|
||||
|
||||
https://cfsmsp.impots.gouv.fr/secavis/
|
||||
https://www.economie.gouv.fr/particuliers/authenticite-avis-impot-svair
|
||||
|
||||
Original code:
|
||||
https://github.com/betagouv/svair-api
|
||||
https://github.com/bdauvergne/svair-api (fork if upstream disapear)
|
||||
'''
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
import lxml.html
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passerelle.base.models import BaseResource
|
||||
from passerelle.utils.api import endpoint
|
||||
from passerelle.utils.json import unflatten
|
||||
from passerelle.utils.jsonresponse import APIError
|
||||
from passerelle.utils.xml import text_content
|
||||
|
||||
AVIS_IMPOSITION_GOUV_FR_URL = 'https://cfsmsp.impots.gouv.fr/secavis/'
|
||||
|
||||
|
||||
def remove_spaces(value):
|
||||
return re.sub(r'\s', '', value, flags=re.U)
|
||||
|
||||
|
||||
def simplify_spaces(value):
|
||||
return re.sub(r'\s+', ' ', value, flags=re.U)
|
||||
|
||||
|
||||
class NotFound(APIError):
|
||||
def __init__(self):
|
||||
super().__init__(_('These fiscal number and tax notice number are unknown.'))
|
||||
|
||||
|
||||
VERIFY_SCHEMA_RESPONSE_NUMBERS = [
|
||||
"dateEtablissement_year",
|
||||
"dateRecouvrement_year",
|
||||
"foyerFiscal__year",
|
||||
"impotRevenuNetAvantCorrections",
|
||||
"nombreParts",
|
||||
"nombrePersonnesCharge",
|
||||
"revenuBrutGlobal",
|
||||
"revenuFiscalReference",
|
||||
"revenuImposable",
|
||||
]
|
||||
|
||||
ISO_DATE_PATTERN = {'type': 'string', 'pattern': r'\d\d\d\d-\d\d-\d\d'}
|
||||
FR_DATE_PATTERN = {'type': 'string', 'pattern': r'\d\d/\d\d/\d\d\d\d'}
|
||||
|
||||
VERIFY_SCHEMA_OTHERS = {
|
||||
"montantImpot": {
|
||||
'oneOf': [
|
||||
{'type': 'string'},
|
||||
{'enum': ['Nonimposable']},
|
||||
]
|
||||
},
|
||||
"situationFamille_simple": {
|
||||
'enum': [
|
||||
'pacs/mariage',
|
||||
'divorce',
|
||||
'celibataire',
|
||||
]
|
||||
},
|
||||
"dateEtablissement_iso": ISO_DATE_PATTERN,
|
||||
"dateRecouvrement_iso": ISO_DATE_PATTERN,
|
||||
"declarant1__dateNaissance_iso": ISO_DATE_PATTERN,
|
||||
"declarant2__dateNaissance_iso": ISO_DATE_PATTERN,
|
||||
"dateEtablissement": FR_DATE_PATTERN,
|
||||
"dateRecouvrement": FR_DATE_PATTERN,
|
||||
"declarant1__dateNaissance": FR_DATE_PATTERN,
|
||||
"declarant2__dateNaissance": FR_DATE_PATTERN,
|
||||
}
|
||||
|
||||
VERIFY_SCHEMA_RESPONSE_STRINGS = [
|
||||
'numero_fiscal',
|
||||
'reference_avis',
|
||||
"declarant1",
|
||||
"declarant1__nom",
|
||||
"declarant1__nomNaissance",
|
||||
"declarant1__prenom",
|
||||
"declarant2__nom",
|
||||
"declarant2__nomNaissance",
|
||||
"declarant2__prenom",
|
||||
"foyerFiscal__adresse",
|
||||
"foyerFiscal__adresse1",
|
||||
"foyerFiscal__adresse2",
|
||||
"foyerFiscal__adresse3",
|
||||
"situationFamille",
|
||||
"situationPartielle",
|
||||
]
|
||||
|
||||
|
||||
def generate_verify_schema_response():
|
||||
schema = {
|
||||
'type': 'object',
|
||||
}
|
||||
|
||||
def set_key(name, value):
|
||||
parts = name.split('__')
|
||||
d = schema
|
||||
for part in parts[:-1]:
|
||||
d = d.setdefault('properties', {}).setdefault(part, {})
|
||||
d['type'] = 'object'
|
||||
d.setdefault('properties', {})[parts[-1]] = value
|
||||
|
||||
for key in VERIFY_SCHEMA_RESPONSE_NUMBERS:
|
||||
set_key(key, {'type': 'number'})
|
||||
for key in VERIFY_SCHEMA_RESPONSE_STRINGS:
|
||||
set_key(key, {'type': 'string'})
|
||||
for key, value in VERIFY_SCHEMA_OTHERS.items():
|
||||
set_key(key, value)
|
||||
return schema
|
||||
|
||||
|
||||
VERIFY_RESPONSE_SCHEMA = generate_verify_schema_response()
|
||||
|
||||
# Map fields by tr/td indexes
|
||||
FIELD_INDEXES = {
|
||||
'declarant1/nom': (1, 1),
|
||||
'declarant2/nom': (1, 2),
|
||||
'declarant1/nomNaissance': (2, 1),
|
||||
'declarant2/nomNaissance': (2, 2),
|
||||
'declarant1/prenom': (3, 1),
|
||||
'declarant2/prenom': (3, 2),
|
||||
'declarant1/dateNaissance': (4, 1),
|
||||
'declarant2/dateNaissance': (4, 2),
|
||||
'declarant1/dateNaissance_iso': (4, 1),
|
||||
'declarant2/dateNaissance_iso': (4, 2),
|
||||
'foyerFiscal/adresse1': (5, 1),
|
||||
'foyerFiscal/adresse2': (6, 1),
|
||||
'foyerFiscal/adresse3': (7, 1),
|
||||
'dateRecouvrement': (9, 1),
|
||||
'dateEtablissement': (10, 1),
|
||||
'dateRecouvrement_iso': (9, 1),
|
||||
'dateEtablissement_iso': (10, 1),
|
||||
'dateRecouvrement_year': (9, 1),
|
||||
'dateEtablissement_year': (10, 1),
|
||||
'nombreParts': (11, 1),
|
||||
'situationFamille': (12, 1),
|
||||
'situationFamille_simple': (12, 1),
|
||||
'nombrePersonnesCharge': (13, 1),
|
||||
'revenuBrutGlobal': (14, 1),
|
||||
'revenuImposable': (15, 1),
|
||||
'impotRevenuNetAvantCorrections': (16, 1),
|
||||
'montantImpot': (17, 1),
|
||||
'revenuFiscalReference': (18, 1),
|
||||
}
|
||||
|
||||
|
||||
def get_field(name, td_contents, tr_length):
|
||||
# adjust tr/td indexes based on the number of address lines deduced from
|
||||
# the number of tr elements
|
||||
i, j = FIELD_INDEXES[name]
|
||||
# address has 3 lines
|
||||
if tr_length == 19:
|
||||
return td_contents[(i, j)]
|
||||
# address has 2 lines
|
||||
if tr_length == 18:
|
||||
if i < 5:
|
||||
return td_contents[(i, j)]
|
||||
if i == 5:
|
||||
return ''
|
||||
if i > 5:
|
||||
return td_contents[(i - 1, j)]
|
||||
# address has 1 line
|
||||
if tr_length == 17:
|
||||
if i < 5:
|
||||
return td_contents[(i, j)]
|
||||
if i in (5, 6):
|
||||
return ''
|
||||
if i > 6:
|
||||
return td_contents[(i - 2, j)]
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def nombre_de_parts(value):
|
||||
try:
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def euros(value):
|
||||
if not value:
|
||||
return value
|
||||
value = remove_spaces(value)
|
||||
value = value.rstrip('€')
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
def situation_familiale(value):
|
||||
if value == 'Pacs\xe9(e)s':
|
||||
return 'pacs/mariage'
|
||||
if value == 'Mari\xe9(e)s':
|
||||
return 'pacs/mariage'
|
||||
if value == 'Divorc\xe9(e)':
|
||||
return 'divorce'
|
||||
if value == 'C\xe9libataire':
|
||||
return 'celibataire'
|
||||
raise NotImplementedError(value)
|
||||
|
||||
|
||||
def date(value):
|
||||
if not value:
|
||||
return ''
|
||||
try:
|
||||
return datetime.datetime.strptime(value, '%d/%m/%Y').date()
|
||||
except (ValueError, TypeError):
|
||||
return ''
|
||||
|
||||
|
||||
def year(value):
|
||||
if not value:
|
||||
return ''
|
||||
try:
|
||||
return datetime.datetime.strptime(value, '%d/%m/%Y').date().year
|
||||
except (ValueError, TypeError):
|
||||
return ''
|
||||
|
||||
|
||||
ADAPTERS = {
|
||||
'nombreParts': nombre_de_parts,
|
||||
'revenuBrutGlobal': euros,
|
||||
'revenuImposable': euros,
|
||||
'impotRevenuNetAvantCorrections': euros,
|
||||
'revenuFiscalReference': euros,
|
||||
'montantImpot': euros,
|
||||
'situationFamille_simple': situation_familiale,
|
||||
'dateEtablissement_iso': date,
|
||||
'dateRecouvrement_iso': date,
|
||||
'dateEtablissement_year': year,
|
||||
'dateRecouvrement_year': year,
|
||||
'declarant1/dateNaissance_iso': date,
|
||||
'declarant2/dateNaissance_iso': date,
|
||||
'nombrePersonnesCharge': nombre_de_parts,
|
||||
}
|
||||
|
||||
|
||||
def get_form(session, logger=None):
|
||||
cache_key = 'avis-imposition-form'
|
||||
try:
|
||||
action_url, data = cache.get(cache_key)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
return action_url, data
|
||||
|
||||
logger = logger or logging
|
||||
try:
|
||||
response = session.get(AVIS_IMPOSITION_GOUV_FR_URL, timeout=15)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
raise APIError('service-is-down', data=str(e))
|
||||
|
||||
if 'Saisissez les identifiants' not in response.text:
|
||||
logger.error(_('https://cfsmsp.impots.gouv.fr/secavis/: service has changed, %s'), response.text)
|
||||
raise RuntimeError('service has changed')
|
||||
|
||||
html = lxml.html.fromstring(response.content)
|
||||
data = {}
|
||||
for form_elt in html.xpath('//form'):
|
||||
if 'action' not in form_elt.attrib:
|
||||
continue
|
||||
action_url = form_elt.attrib['action']
|
||||
break
|
||||
else:
|
||||
logger.error(
|
||||
_('https://cfsmsp.impots.gouv.fr/secavis/: service has changed, form[action] not found, %s'),
|
||||
response.text,
|
||||
)
|
||||
raise RuntimeError('service has changed')
|
||||
logger.debug('using found action_url %s', action_url)
|
||||
|
||||
fields = ['j_id_7:spi', 'j_id_7:num_facture']
|
||||
for input_elt in html.xpath('//input'):
|
||||
if 'value' in input_elt.attrib or input_elt.attrib['name'] in fields:
|
||||
data[input_elt.attrib['name']] = input_elt.attrib.get('value', '')
|
||||
for field in fields:
|
||||
if field not in data:
|
||||
logger.error(
|
||||
_('https://cfsmsp.impots.gouv.fr/secavis/: service has changed, field %s not found, %s'),
|
||||
field,
|
||||
response.text,
|
||||
)
|
||||
raise RuntimeError('service has changed')
|
||||
cache.set(cache_key, (action_url, data), 180)
|
||||
return action_url, data
|
||||
|
||||
|
||||
def get_avis_imposition(session, numero_fiscal, reference_avis, logger=None):
|
||||
logger = logger or logging
|
||||
|
||||
action_url, data = get_form(session, logger=logger)
|
||||
|
||||
data['j_id_7:spi'] = numero_fiscal
|
||||
data['j_id_7:num_facture'] = reference_avis
|
||||
|
||||
url = urllib.parse.urljoin(AVIS_IMPOSITION_GOUV_FR_URL, action_url)
|
||||
try:
|
||||
response = session.post(url, params=data)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
raise APIError('service-is-down', data=str(e))
|
||||
|
||||
if 'Saisissez les identifiants' in response.text:
|
||||
raise NotFound
|
||||
|
||||
response_html = lxml.html.fromstring(response.content)
|
||||
td_contents = {}
|
||||
tr_elements = list(response_html.xpath('//tr'))
|
||||
for i, tr_element in enumerate(tr_elements):
|
||||
for j, td_element in enumerate(tr_element.xpath('td')):
|
||||
td_contents[(i, j)] = simplify_spaces(text_content(td_element).strip())
|
||||
|
||||
logger.debug('got td_contents %s', td_contents)
|
||||
|
||||
data = {
|
||||
# https://github.com/betagouv/svair-api/blob/master/utils/year.js
|
||||
'foyerFiscal/year': int('20' + reference_avis[:2]),
|
||||
}
|
||||
for field, coordinates in FIELD_INDEXES.items():
|
||||
try:
|
||||
data[field] = get_field(field, td_contents, len(tr_elements))
|
||||
except IndexError:
|
||||
logger.error(
|
||||
_(
|
||||
'https://cfsmsp.impots.gouv.fr/secavis/: service has changed, data field %s(%s) not found, %s'
|
||||
),
|
||||
field,
|
||||
coordinates,
|
||||
response.text,
|
||||
)
|
||||
raise RuntimeError('service has changed')
|
||||
if field in ADAPTERS:
|
||||
data[field] = ADAPTERS[field](data[field])
|
||||
for situation_partielle_elt in response_html.xpath('//*[@id="situationPartielle"]'):
|
||||
data['situationPartielle'] = text_content(situation_partielle_elt).strip()
|
||||
break
|
||||
|
||||
# add reference number to result
|
||||
data['numero_fiscal'] = numero_fiscal
|
||||
data['reference_avis'] = reference_avis
|
||||
|
||||
# restore structure to the data
|
||||
unflattened = unflatten(data)
|
||||
|
||||
# flatten address for simple use case
|
||||
foyer_fiscal = unflattened['foyerFiscal']
|
||||
foyer_fiscal['adresse'] = ' '.join(
|
||||
value for key, value in sorted(foyer_fiscal.items()) if key.startswith('adresse') and value
|
||||
)
|
||||
return unflattened
|
||||
|
||||
|
||||
def get_fake_data(numero_fiscal, reference_avis):
|
||||
fake_data = getattr(settings, 'AVIS_IMPOSITION_FAKE_DATA', [])
|
||||
for data in fake_data or []:
|
||||
if data['numero_fiscal'] == numero_fiscal and data['reference_avis'] == reference_avis:
|
||||
return data
|
||||
|
||||
|
||||
class AvisImposition(BaseResource):
|
||||
log_requests_errors = True
|
||||
requests_timeout = 10
|
||||
requests_max_retries = {
|
||||
'total': 5,
|
||||
'backoff_factor': 0.5,
|
||||
'allowed_methods': ['GET', 'POST'],
|
||||
# retry after: 0.5, 1.5 and 3.5 seconds
|
||||
'status_forcelist': [413, 429, 503, 504],
|
||||
}
|
||||
|
||||
category = _('Business Process Connectors')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('French tax notice validator on impots.gouv.fr')
|
||||
|
||||
@endpoint(
|
||||
perm='can_access',
|
||||
description=_('Get citizen\'s fiscal informations'),
|
||||
parameters={
|
||||
'numero_fiscal': {
|
||||
'description': _('fiscal identifier'),
|
||||
'example_value': '1562456789521',
|
||||
},
|
||||
'reference_avis': {
|
||||
'description': _('tax notice number'),
|
||||
'example_value': '1512456789521',
|
||||
},
|
||||
},
|
||||
json_schema_response=VERIFY_RESPONSE_SCHEMA,
|
||||
)
|
||||
def verify(self, request, numero_fiscal, reference_avis):
|
||||
numero_fiscal = remove_spaces(numero_fiscal)
|
||||
reference_avis = remove_spaces(reference_avis)
|
||||
|
||||
if not (
|
||||
# See.
|
||||
# https://fr.wikipedia.org/wiki/Num%C3%A9ro_d'immatriculation_fiscale#France
|
||||
# a 13 digits decimal number
|
||||
len(numero_fiscal) == 13
|
||||
and numero_fiscal.isascii()
|
||||
and numero_fiscal.isdigit()
|
||||
# No clear information on the format, it seems to be a 13 digits
|
||||
# decimal number too from examples. As we aren´t sure we will ask
|
||||
# for a more than 10 digits long alphanumeric string.
|
||||
and len(reference_avis) >= 10
|
||||
and reference_avis.isascii()
|
||||
and reference_avis.isalnum()
|
||||
):
|
||||
raise APIError(_('Fiscal identifier or tax notice number are invalid.'))
|
||||
|
||||
cache_key = f'avis-imposition-{numero_fiscal}-{reference_avis}'
|
||||
data = cache.get(cache_key)
|
||||
|
||||
if data == 'not-found':
|
||||
raise NotFound
|
||||
|
||||
if not data:
|
||||
# use test vectors...
|
||||
data = get_fake_data(numero_fiscal=numero_fiscal, reference_avis=reference_avis)
|
||||
# or the real thing.
|
||||
try:
|
||||
data = data or get_avis_imposition(
|
||||
self.requests, numero_fiscal=numero_fiscal, reference_avis=reference_avis
|
||||
)
|
||||
except NotFound:
|
||||
cache.set(cache_key, 'not-found', 60)
|
||||
raise
|
||||
# service is slow, cache successful response for 12 hours
|
||||
cache.set(cache_key, data, 3600 * 12)
|
||||
return {'data': data}
|
||||
|
||||
def check_status(self):
|
||||
get_form(self.requests)
|
||||
|
||||
def get_test_data(self):
|
||||
fake_data = getattr(settings, 'AVIS_IMPOSITION_FAKE_DATA', [])
|
||||
result = []
|
||||
for data in fake_data or []:
|
||||
try:
|
||||
numero_fiscal = data['numero_fiscal']
|
||||
reference_avis = data['reference_avis']
|
||||
revenu_fiscal_de_reference = data['revenuFiscalReference']
|
||||
except Exception:
|
||||
pass
|
||||
result.append((numero_fiscal, reference_avis, revenu_fiscal_de_reference))
|
||||
return result
|
|
@ -0,0 +1,40 @@
|
|||
{% extends "passerelle/manage/service_view.html" %}
|
||||
{% load i18n passerelle %}
|
||||
|
||||
{% block description %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This connector verify fiscal number and tax notice number on
|
||||
<a href="https://cfsmsp.impots.gouv.fr/secavis">https://cfsmsp.impots.gouv.fr/secavis/</a>
|
||||
and returns the scrapped data.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>{% blocktrans trimmed %}
|
||||
<a href="https://www.economie.gouv.fr/particuliers/authenticite-avis-impot-svair">Documentation on the secavis service.</a>
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% with test_data=object.get_test_data %}
|
||||
{% if test_data %}
|
||||
<h4>{% trans "Test datas" %}</h4>
|
||||
<table class="main" style="width: 80%; margin: auto auto;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Fiscal number" %}</th>
|
||||
<th>{% trans "Tax notice number" %}</th>
|
||||
<th>{% trans "Reference income" %}</th>
|
||||
<th>{% trans "Test URL" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for numero_fiscal, reference_avis, revenu_fiscal_de_reference in test_data %}
|
||||
<tr>
|
||||
<td>{{ numero_fiscal }}</td>
|
||||
<td>{{ reference_avis }}</td>
|
||||
<td>{{ revenu_fiscal_de_reference}} €</td>
|
||||
<td><a href="verify?numero_fiscal={{ numero_fiscal }}&reference_avis={{ reference_avis }}" class="button">Test</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
|
@ -136,6 +136,7 @@ INSTALLED_APPS = (
|
|||
'passerelle.apps.astre_rest',
|
||||
'passerelle.apps.atal',
|
||||
'passerelle.apps.atos_genesys',
|
||||
'passerelle.apps.avis_imposition',
|
||||
'passerelle.apps.base_adresse',
|
||||
'passerelle.apps.bbb',
|
||||
'passerelle.apps.bdp',
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"><head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=UTF-8">
|
||||
|
||||
<title>Impots.gouv.fr - Service de vérification en ligne des avis</title>
|
||||
|
||||
<link href="coin_fichiers/style.css" rel="styleSheet" type="text/css">
|
||||
|
||||
<script type="text/javascript" src="coin_fichiers/fonctions.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="conteneur">
|
||||
<div id="barre_haut">
|
||||
<div style="float: left;"><img src="coin_fichiers/bo_seule-2.gif" alt=""></div>
|
||||
<div style="float: right;"><img src="coin_fichiers/bo_seule-3.gif" alt=""></div>
|
||||
</div>
|
||||
<div id="principal">
|
||||
<div id="nav_pro">
|
||||
<b>Bienvenue sur le service de vérification des avis</b>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<br>
|
||||
<div class="titre"><span>Accès au service de vérification</span></div>
|
||||
<br>
|
||||
<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">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="290">
|
||||
<br><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input id="j_id_7:spi" name="j_id_7:spi" type="text" maxlength="13" size="15" tabindex="1" autocomplete="off">
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input id="j_id_7:num_facture" name="j_id_7:num_facture" type="text" maxlength="13" size="15" tabindex="2" autocomplete="off">
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
||||
<td>
|
||||
<div align="right">
|
||||
|
||||
<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">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</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>
|
||||
<br>
|
||||
<div id="donneesObligatoires">* données obligatoires</div>
|
||||
</div>
|
||||
<div id="bas_page">© Ministère de l'Économie et des Finances</div><img src="coin_fichiers/hit.gif" alt="" width="1" height="1">
|
||||
</div>
|
||||
|
||||
<div id="grammalecte_menu_main_button_shadow_host" style="width: 0px; height: 0px;"></div></body><script src="coin_fichiers/api.js"></script></html>
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,190 @@
|
|||
# passerelle - uniform access to multiple data sources and services
|
||||
# Copyright (C) 2016 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import responses
|
||||
|
||||
from passerelle.apps.avis_imposition import models
|
||||
|
||||
from . import utils
|
||||
from .test_manager import login
|
||||
|
||||
# Content from the form page
|
||||
with open('tests/data/avis-imposition.html') as fd:
|
||||
html = fd.read()
|
||||
|
||||
# Contents from the submit result page
|
||||
with open('tests/data/avis-imposition.json') as fd:
|
||||
cases = json.load(fd)
|
||||
|
||||
|
||||
def responses_add(*args, url=re.compile('https://cfsmsp.impots.gouv.fr/secavis/.*'), **kwargs):
|
||||
responses.upsert(*args, url=url, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def avis_imposition(db, settings):
|
||||
from django.core.cache import close_caches
|
||||
|
||||
settings.CACHES['default'] = settings.CACHES['dummy']
|
||||
close_caches()
|
||||
with responses.mock:
|
||||
responses_add(responses.GET, body=html)
|
||||
yield utils.make_resource(models.AvisImposition, slug='test')
|
||||
|
||||
|
||||
@responses.activate
|
||||
@pytest.mark.parametrize(
|
||||
'data',
|
||||
cases,
|
||||
ids=[
|
||||
''.join([data['result']['declarant1']['prenom'], data['result']['declarant1']['nom']])
|
||||
for data in cases
|
||||
],
|
||||
)
|
||||
def test_ok(avis_imposition, data, app):
|
||||
responses_add(responses.POST, body=data['response'])
|
||||
response = utils.endpoint_get(
|
||||
expected_url='/avis-imposition/test/verify',
|
||||
app=app,
|
||||
resource=avis_imposition,
|
||||
endpoint='verify',
|
||||
params={
|
||||
'numero_fiscal': data['numero_fiscal'],
|
||||
'reference_avis': data['reference_avis'],
|
||||
},
|
||||
)
|
||||
assert response.json['err'] == 0
|
||||
assert response.json['data'] == data['result']
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_not_found(avis_imposition, app):
|
||||
responses_add(responses.GET, body=html)
|
||||
responses_add(responses.POST, body=html)
|
||||
|
||||
response = utils.endpoint_get(
|
||||
expected_url='/avis-imposition/test/verify',
|
||||
app=app,
|
||||
resource=avis_imposition,
|
||||
endpoint='verify',
|
||||
params={
|
||||
'numero_fiscal': '1' * 13,
|
||||
'reference_avis': '2' * 13,
|
||||
},
|
||||
)
|
||||
assert response.json['err'] == 1
|
||||
assert response.json['err_desc'] == 'These fiscal number and tax notice number are unknown.'
|
||||
|
||||
|
||||
@responses.activate
|
||||
@pytest.mark.parametrize(
|
||||
'response_kwargs',
|
||||
[
|
||||
{'method_or_response': responses.GET, 'body': requests.RequestException('boom!')},
|
||||
{'method_or_response': responses.GET, 'status': 500},
|
||||
{'method_or_response': responses.POST, 'body': requests.RequestException('boom!')},
|
||||
{'method_or_response': responses.POST, 'status': 500},
|
||||
],
|
||||
)
|
||||
def test_request_error(avis_imposition, app, response_kwargs):
|
||||
responses_add(**response_kwargs)
|
||||
|
||||
response = utils.endpoint_get(
|
||||
expected_url='/avis-imposition/test/verify',
|
||||
app=app,
|
||||
resource=avis_imposition,
|
||||
endpoint='verify',
|
||||
params={
|
||||
'numero_fiscal': '1234567890123',
|
||||
'reference_avis': '0987654321',
|
||||
},
|
||||
)
|
||||
assert response.json['err'] == 1
|
||||
assert response.json['err_desc'] == 'service-is-down'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_fake_data(avis_imposition, app, settings, admin_user):
|
||||
settings.AVIS_IMPOSITION_FAKE_DATA = [
|
||||
{
|
||||
'numero_fiscal': '1' * 13,
|
||||
'reference_avis': '2' * 13,
|
||||
"dateEtablissement": "09/12/2019",
|
||||
"dateEtablissement_iso": "2019-12-09",
|
||||
"dateRecouvrement": "31/12/2019",
|
||||
"dateRecouvrement_iso": "2019-12-31",
|
||||
"dateEtablissement_year": 2019,
|
||||
"dateRecouvrement_year": 2019,
|
||||
"declarant1": {
|
||||
"dateNaissance": "01/01/1970",
|
||||
"dateNaissance_iso": "1970-01-01",
|
||||
"nom": "DOE",
|
||||
"nomNaissance": "DOE",
|
||||
"prenom": "JOHN",
|
||||
},
|
||||
"declarant2": {
|
||||
"dateNaissance": "",
|
||||
"dateNaissance_iso": "",
|
||||
"nom": "DOE",
|
||||
"nomNaissance": "DOE",
|
||||
"prenom": "JANE",
|
||||
},
|
||||
"foyerFiscal": {
|
||||
"adresse": "R\u00c9SIDENCE DU CALVAIRE RUE VICTOR HUGO 75014 PARIS",
|
||||
"adresse1": "R\u00c9SIDENCE DU CALVAIRE",
|
||||
"adresse2": "RUE VICTOR HUGO",
|
||||
"adresse3": "75014 PARIS",
|
||||
"year": 2022,
|
||||
},
|
||||
"impotRevenuNetAvantCorrections": 112,
|
||||
"montantImpot": "Nonimposable",
|
||||
"nombreParts": 4.0,
|
||||
"nombrePersonnesCharge": 4.0,
|
||||
"revenuBrutGlobal": 48473,
|
||||
"revenuFiscalReference": 48473,
|
||||
"revenuImposable": 48473,
|
||||
"situationFamille": "Pacs\u00e9(e)s",
|
||||
"situationFamille_simple": "pacs/mariage",
|
||||
"situationPartielle": "",
|
||||
}
|
||||
]
|
||||
|
||||
response = utils.endpoint_get(
|
||||
expected_url='/avis-imposition/test/verify',
|
||||
app=app,
|
||||
resource=avis_imposition,
|
||||
endpoint='verify',
|
||||
params={
|
||||
'numero_fiscal': '1' * 13,
|
||||
'reference_avis': '2' * 13,
|
||||
},
|
||||
)
|
||||
assert response.json['err'] == 0
|
||||
assert response.json['data'] == settings.AVIS_IMPOSITION_FAKE_DATA[0]
|
||||
|
||||
# Check that test data are displayed on the backoffice page
|
||||
app = login(app)
|
||||
response = app.get('/avis-imposition/test/')
|
||||
assert 'Test datas' in response
|
||||
assert [
|
||||
[td.text() for td in tr.find('td').items()]
|
||||
for tr in response.pyquery('table').eq(0).find('tbody tr').items()
|
||||
] == [['1111111111111', '2222222222222', '48473\xa0€', 'Test']]
|
Loading…
Reference in New Issue