feat(api particulier): add MESRI field

This commit is contained in:
François Vantomme 2021-12-15 15:25:02 +01:00 committed by simon lehericey
parent f08081abfe
commit 14e0c68266
29 changed files with 380 additions and 10 deletions

View file

@ -359,7 +359,8 @@
.cnaf-inputs,
.dgfip-inputs,
.pole-emploi-inputs {
.pole-emploi-inputs,
.mesri-inputs {
display: flex;
flex-wrap: wrap;
justify-content: space-between;

View file

@ -0,0 +1,30 @@
@import "constants";
@import "colors";
table.mesri {
margin: 2 * $default-padding 0 $default-padding $default-padding;
width: 100%;
caption {
font-weight: bold;
margin-left: - $default-padding;
margin-bottom: $default-spacer;
text-align: left;
}
th,
td {
font-weight: normal;
padding: $default-spacer;
}
th.text-right {
text-align: right;
}
&.horizontal {
th {
border-bottom: 1px solid $grey;
}
}
}

View file

@ -348,8 +348,8 @@ module Users
def champs_params
params.permit(dossier: {
champs_attributes: [
:id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :piece_justificative_file, :departement, :code_departement, value: [],
champs_attributes: [:id, :_destroy, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :piece_justificative_file, :departement, :code_departement, value: []]
:id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :departement, :code_departement, value: [],
champs_attributes: [:id, :_destroy, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :departement, :code_departement, value: []]
]
})
end

View file

@ -1983,6 +1983,11 @@ enum TypeDeChamp {
"""
linked_drop_down_list
"""
Données du Ministère de l'Enseignement Supérieur, de la Recherche et de l'Innovation
"""
mesri
"""
Choix multiples
"""

View file

@ -8,6 +8,7 @@ module APIParticulier
def available_sources
@procedure.api_particulier_scopes
.filter_map { |provider_and_scope| raw_scopes[provider_and_scope] }
.uniq # remove provider/scope tuples duplicates (e.g. mesri inscriptions)
.map { |provider, scope| extract_sources(provider, scope) }
.reduce({}) { |acc, el| acc.deep_merge(el) { |_, this_val, other_val| this_val + other_val } }
end

View file

@ -0,0 +1,47 @@
# == Schema Information
#
# Table name: champs
#
# id :integer not null, primary key
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer
# etablissement_id :integer
# external_id :string
# parent_id :bigint
# type_de_champ_id :integer
#
class Champs::MesriChamp < Champs::TextChamp
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/mesri-input-validation.middleware.ts
store_accessor :value_json, :ine
def blank?
external_id.nil?
end
def fetch_external_data?
true
end
def fetch_external_data
return if !valid?
APIParticulier::MesriAdapter.new(
procedure.api_particulier_token,
ine,
procedure.api_particulier_sources
).to_params
end
def external_id
{ ine: ine }.to_json if ine.present?
end
end

View file

@ -734,6 +734,10 @@ class Procedure < ApplicationRecord
api_particulier_sources['pole_emploi'].present?
end
def mesri_enabled?
api_particulier_sources['mesri'].present?
end
private
def validate_for_publication?

View file

@ -52,7 +52,8 @@ class TypeDeChamp < ApplicationRecord
annuaire_education: 'annuaire_education',
cnaf: 'cnaf',
dgfip: 'dgfip',
pole_emploi: 'pole_emploi'
pole_emploi: 'pole_emploi',
mesri: 'mesri'
}
belongs_to :revision, class_name: 'ProcedureRevision', optional: true
@ -330,6 +331,8 @@ class TypeDeChamp < ApplicationRecord
procedure.dgfip_enabled?
when TypeDeChamp.type_champs.fetch(:pole_emploi)
procedure.pole_emploi_enabled?
when TypeDeChamp.type_champs.fetch(:mesri)
procedure.mesri_enabled?
else
true
end

View file

@ -0,0 +1,2 @@
class TypesDeChamp::MesriTypeDeChamp < TypesDeChamp::TextTypeDeChamp
end

View file

@ -0,0 +1,11 @@
%table.mesri
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
- admissions.each do |admission|
- admission.slice('statut', 'regime', 'dateDebutAdmission', 'dateFinAdmission', 'codeCommune').keys.each do |key|
%tr
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
- case key
- when 'dateDebutAdmission', 'dateFinAdmission'
%td= try_format_date(Date.strptime(admission[key], "%Y-%m-%d"))
- else
%td= admission[key]

View file

@ -0,0 +1,7 @@
%table.mesri
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
- etablissements.each do |etablissement|
- etablissement.slice('uai', 'nom').keys.each do |key|
%tr
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
%td= etablissement[key]

View file

@ -0,0 +1,6 @@
%table.mesri
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
- identifiant.slice('ine').keys.each do |key|
%tr
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
%td= identifiant[key]

View file

@ -0,0 +1,10 @@
%table.mesri
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
- identite.slice('nom', 'prenom', 'dateNaissance').keys.each do |key|
%tr
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
- case key
- when 'dateNaissance'
%td= try_format_date(Date.strptime(identite[key], "%Y-%m-%d"))
- else
%td= identite[key]

View file

@ -0,0 +1,11 @@
%table.mesri
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
- inscriptions.each do |inscription|
- inscription.slice('statut', 'regime', 'dateDebutInscription', 'dateFinInscription', 'codeCommune').keys.each do |key|
%tr
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
- case key
- when 'dateDebutInscription', 'dateFinInscription'
%td= try_format_date(Date.strptime(inscription[key], "%Y-%m-%d"))
- else
%td= inscription[key]

View file

@ -0,0 +1,25 @@
- if champ.blank?
%p= t('.not_filled')
- elsif champ.data.blank?
%p= t('.fetching_data', ine: champ.ine)
- else
- if profile == 'usager'
- sources = champ.procedure.api_particulier_sources['mesri'].keys
- i18n_sources = sources.map { |s| I18n.t("#{s}.libelle", scope: 'api_particulier.providers.mesri.scopes') }
%p= t('.data_fetched', sources: i18n_sources.to_sentence, ine: champ.ine)
- if profile == 'instructeur'
%p= t('.data_fetched_title')
- champ.data.slice('identifiant', 'identite', 'inscriptions', 'admissions', 'etablissements').keys.each do |scope|
- case scope
- when 'identifiant'
= render partial: 'shared/champs/mesri/identifiant', locals: { scope: scope, identifiant: champ.data[scope] }
- when 'identite'
= render partial: 'shared/champs/mesri/identite', locals: { scope: scope, identite: champ.data[scope] }
- when 'inscriptions'
= render partial: 'shared/champs/mesri/inscriptions', locals: { scope: scope, inscriptions: champ.data[scope] }
- when 'admissions'
= render partial: 'shared/champs/mesri/admissions', locals: { scope: scope, admissions: champ.data[scope] }
- when 'etablissements'
= render partial: 'shared/champs/mesri/etablissements', locals: { scope: scope, etablissements: champ.data[scope] }

View file

@ -42,6 +42,8 @@
= render partial: "shared/champs/dgfip/show", locals: { champ: c, profile: profile }
- when TypeDeChamp.type_champs.fetch(:pole_emploi)
= render partial: "shared/champs/pole_emploi/show", locals: { champ: c, profile: profile }
- when TypeDeChamp.type_champs.fetch(:mesri)
= render partial: "shared/champs/mesri/show", locals: { champ: c, profile: profile }
- when TypeDeChamp.type_champs.fetch(:address)
= render partial: "shared/champs/address/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:communes)

View file

@ -0,0 +1,7 @@
.mesri-inputs
%div
= form.label :ine, t('.ine_label')
%p.notice= t('.ine_notice')
= form.text_field :ine,
required: champ.mandatory?,
aria: { describedby: describedby_id(champ) }

View file

@ -39,3 +39,4 @@ fr:
cnaf: 'Données de la Caisse nationale des allocations familiales'
dgfip: 'Données de la Direction générale des Finances publiques'
pole_emploi: 'Situation Pôle emploi'
mesri: "Données du Ministère de l'Enseignement Supérieur, de la Recherche et de l'Innovation"

View file

@ -15,6 +15,9 @@ en:
pole_emploi:
identifiant_label: Identifier
identifiant_notice: It is usually composed of alphanumeric characters.
mesri:
ine_label: INE
ine_notice: Student National Number. It is usually composed of alphanumeric characters.
header:
expires_at:
brouillon: "Expires at %{date} (%{duree_conservation_totale} months after the creation of this file)"
@ -42,3 +45,9 @@ en:
fetching_data: "Fetching data for identifier %{identifiant}."
data_fetched: "Data concerning %{sources} linked to the identifier %{identifiant} has been received from Pôle emploi."
data_fetched_title: "Data received from Pôle emploi"
mesri:
show:
not_filled: not filled
fetching_data: "Fetching data for INE %{ine}."
data_fetched: "Data concerning %{sources} linked to the INE %{ine} has been received from the MESRI."
data_fetched_title: "Data received from the MESRI"

View file

@ -15,6 +15,9 @@ fr:
pole_emploi:
identifiant_label: Identifiant
identifiant_notice: Il est généralement composé de caractères alphanumériques.
mesri:
ine_label: INE
ine_notice: Identifiant National Étudiant. Il est généralement composé de caractères alphanumériques.
header:
expires_at:
brouillon: "Expirera le %{date} (%{duree_conservation_totale} mois après la création du dossier)"

View file

@ -79,7 +79,13 @@ describe Administrateurs::JetonParticulierController, type: :controller do
'pole_emploi_identite',
'pole_emploi_adresse',
'pole_emploi_contact',
'pole_emploi_inscription'
'pole_emploi_inscription',
'mesri_identifiant',
'mesri_identite',
'mesri_inscription_etudiant',
'mesri_inscription_autre',
'mesri_admission',
'mesri_etablissements'
)
expect(procedure.api_particulier_sources).to be_empty
end

View file

@ -197,6 +197,10 @@ FactoryBot.define do
type_de_champ { association :type_de_champ_pole_emploi, procedure: dossier.procedure }
end
factory :champ_mesri, class: 'Champs::MesriChamp' do
type_de_champ { association :type_de_champ_mesri, procedure: dossier.procedure }
end
factory :champ_siret, class: 'Champs::SiretChamp' do
type_de_champ { association :type_de_champ_siret, procedure: dossier.procedure }
association :etablissement, factory: [:etablissement]

View file

@ -216,6 +216,12 @@ FactoryBot.define do
end
end
trait :with_mesri do
after(:build) do |procedure, _evaluator|
build(:type_de_champ_mesri, procedure: procedure)
end
end
trait :with_explication do
after(:build) do |procedure, _evaluator|
build(:type_de_champ_explication, procedure: procedure)

View file

@ -163,6 +163,9 @@ FactoryBot.define do
factory :type_de_champ_pole_emploi do
type_champ { TypeDeChamp.type_champs.fetch(:pole_emploi) }
end
factory :type_de_champ_mesri do
type_champ { TypeDeChamp.type_champs.fetch(:mesri) }
end
factory :type_de_champ_carte do
type_champ { TypeDeChamp.type_champs.fetch(:carte) }
end

View file

@ -25,7 +25,7 @@ http_interactions:
Content-Type:
- application/json
Content-Length:
- '257'
- '1068'
Connection:
- keep-alive
Keep-Alive:
@ -38,6 +38,6 @@ http_interactions:
- max-age=15552000
body:
encoding: UTF-8
string: '{"_id":"1d99db5a-a099-4314-ad2f-2707c6b505a6","name":"Application de sandbox","scopes":["cnaf_allocataires","cnaf_enfants","cnaf_adresse","cnaf_quotient_familial","dgfip_declarant1_nom","dgfip_declarant1_nom_naissance","dgfip_declarant1_prenoms","dgfip_declarant1_date_naissance","dgfip_declarant2_nom","dgfip_declarant2_nom_naissance","dgfip_declarant2_prenoms","dgfip_declarant2_date_naissance","dgfip_date_recouvrement","dgfip_date_etablissement","dgfip_adresse_fiscale_taxation","dgfip_adresse_fiscale_annee","dgfip_nombre_parts","dgfip_nombre_personnes_a_charge","dgfip_situation_familiale","dgfip_revenu_brut_global","dgfip_revenu_imposable","dgfip_impot_revenu_net_avant_corrections","dgfip_montant_impot","dgfip_revenu_fiscal_reference","dgfip_annee_impot","dgfip_annee_revenus","dgfip_erreur_correctif","dgfip_situation_partielle", "pole_emploi_identite","pole_emploi_adresse","pole_emploi_contact","pole_emploi_inscription"]}'
string: '{"_id":"1d99db5a-a099-4314-ad2f-2707c6b505a6","name":"Application de sandbox","scopes":["cnaf_allocataires","cnaf_enfants","cnaf_adresse","cnaf_quotient_familial","dgfip_declarant1_nom","dgfip_declarant1_nom_naissance","dgfip_declarant1_prenoms","dgfip_declarant1_date_naissance","dgfip_declarant2_nom","dgfip_declarant2_nom_naissance","dgfip_declarant2_prenoms","dgfip_declarant2_date_naissance","dgfip_date_recouvrement","dgfip_date_etablissement","dgfip_adresse_fiscale_taxation","dgfip_adresse_fiscale_annee","dgfip_nombre_parts","dgfip_nombre_personnes_a_charge","dgfip_situation_familiale","dgfip_revenu_brut_global","dgfip_revenu_imposable","dgfip_impot_revenu_net_avant_corrections","dgfip_montant_impot","dgfip_revenu_fiscal_reference","dgfip_annee_impot","dgfip_annee_revenus","dgfip_erreur_correctif","dgfip_situation_partielle", "pole_emploi_identite","pole_emploi_adresse","pole_emploi_contact","pole_emploi_inscription","mesri_identifiant","mesri_identite","mesri_inscription_etudiant","mesri_inscription_autre","mesri_admission","mesri_etablissements"]}'
recorded_at: Tue, 16 Mar 2021 15:25:24 GMT
recorded_with: VCR 6.0.0

View file

@ -41,7 +41,13 @@ describe APIParticulier::API do
'pole_emploi_identite',
'pole_emploi_adresse',
'pole_emploi_contact',
'pole_emploi_inscription'
'pole_emploi_inscription',
'mesri_identifiant',
'mesri_identite',
'mesri_inscription_etudiant',
'mesri_inscription_autre',
'mesri_admission',
'mesri_etablissements'
)
end
end

View file

@ -0,0 +1,56 @@
describe Champs::MesriChamp, type: :model do
let(:champ) { described_class.new }
describe 'INE' do
before do
champ.ine = '090601811AB'
end
it 'saves INE' do
expect(champ.ine).to eq('090601811AB')
end
end
describe 'external_id' do
context 'when no data is given' do
before do
champ.ine = ''
champ.save
end
it { expect(champ.external_id).to be_nil }
end
context 'when all data required for an external fetch are given' do
before do
champ.ine = '090601811AB'
champ.save
end
it { expect(JSON.parse(champ.external_id)).to eq("ine" => "090601811AB") }
end
end
describe '#validate' do
let(:champ) { described_class.new(dossier: create(:dossier), type_de_champ: create(:type_de_champ_mesri)) }
let(:validation_context) { :create }
subject { champ.valid?(validation_context) }
before do
champ.ine = ine
end
context 'when INE is valid' do
let(:ine) { '090601811AB' }
it { is_expected.to be true }
end
context 'when INE is nil' do
let(:ine) { nil }
it { is_expected.to be true }
end
end
end

View file

@ -80,6 +80,7 @@ describe ProcedureExportService do
"cnaf",
"dgfip",
"pole_emploi",
"mesri",
"text"
]
end
@ -170,6 +171,7 @@ describe ProcedureExportService do
"cnaf",
"dgfip",
"pole_emploi",
"mesri",
"text"
]
end
@ -256,6 +258,7 @@ describe ProcedureExportService do
"cnaf",
"dgfip",
"pole_emploi",
"mesri",
"text"
]
end

View file

@ -26,6 +26,13 @@ describe 'fetch API Particulier Data', js: true do
'adresse' => ['INSEECommune', 'codePostal', 'localite', 'ligneVoie', 'ligneComplementDestinataire', 'ligneComplementAdresse', 'ligneComplementDistribution', 'ligneNom'],
'contact' => ['email', 'telephone', 'telephone2'],
'inscription' => ['dateInscription', 'dateCessationInscription', 'codeCertificationCNAV', 'codeCategorieInscription', 'libelleCategorieInscription']
},
'mesri' => {
'identifiant' => ['ine'],
'identite' => ['nom', 'prenom', 'dateNaissance'],
'inscriptions' => ['statut', 'regime', 'dateDebutInscription', 'dateFinInscription', 'codeCommune'],
'admissions' => ['statut', 'regime', 'dateDebutAdmission', 'dateFinAdmission', 'codeCommune'],
'etablissements' => ['uai', 'nom']
}
}
end
@ -158,6 +165,37 @@ describe 'fetch API Particulier Data', js: true do
check("libellé de catégorie dinscription")
end
within('#mesri-identifiant') do
check('INE')
end
within('#mesri-identite') do
check('nom')
check('prénom')
check('date de naissance')
end
within('#mesri-inscriptions') do
check('statut')
check('régime')
check("date de début d'inscription")
check("date de fin d'inscription")
check("code de la commune")
end
within('#mesri-admissions') do
check('statut')
check('régime')
check("date de début d'admission")
check("date de fin d'admission")
check("code de la commune")
end
within('#mesri-etablissements') do
check('UAI')
check('nom')
end
click_on 'Enregistrer'
within('#cnaf-enfants') do
@ -166,10 +204,11 @@ describe 'fetch API Particulier Data', js: true do
procedure.reload
expect(procedure.api_particulier_sources.keys).to contain_exactly('cnaf', 'dgfip', 'pole_emploi')
expect(procedure.api_particulier_sources.keys).to contain_exactly('cnaf', 'dgfip', 'pole_emploi', 'mesri')
expect(procedure.api_particulier_sources['cnaf'].keys).to contain_exactly('adresse', 'allocataires', 'enfants', 'quotient_familial')
expect(procedure.api_particulier_sources['dgfip'].keys).to contain_exactly('declarant1', 'declarant2', 'echeance_avis', 'foyer_fiscal', 'agregats_fiscaux', 'complements')
expect(procedure.api_particulier_sources['pole_emploi'].keys).to contain_exactly('identite', 'adresse', 'contact', 'inscription')
expect(procedure.api_particulier_sources['mesri'].keys).to contain_exactly('identifiant', 'identite', 'inscriptions', 'admissions', 'etablissements')
procedure.api_particulier_sources.each do |provider, scopes|
scopes.each do |scope, fields|
@ -203,10 +242,11 @@ describe 'fetch API Particulier Data', js: true do
let(:reference_avis) { '2097699999077' }
let(:instructeur) { create(:instructeur) }
let(:identifiant) { 'georges_moustaki_77' }
let(:ine) { '090601811AB' }
let(:api_particulier_token) { '29eb50b65f64e8e00c0847a8bbcbd150e1f847' }
let(:procedure) do
create(:procedure, :for_individual, :with_service, :with_cnaf, :with_dgfip, :with_pole_emploi, :published,
create(:procedure, :for_individual, :with_service, :with_cnaf, :with_dgfip, :with_pole_emploi, :with_mesri, :published,
libelle: 'libellé de la procédure',
path: 'libelle-de-la-procedure',
instructeurs: [instructeur],
@ -336,6 +376,67 @@ describe 'fetch API Particulier Data', js: true do
end
end
context 'MESRI' do
let(:api_particulier_token) { 'c6d23f3900b8fb4b3586c4804c051af79062f54b' }
scenario 'it can fill a MESRI field' do
visit commencer_path(path: procedure.path)
click_on 'Commencer la démarche'
choose 'Madame'
fill_in 'individual_nom', with: 'Dubois'
fill_in 'individual_prenom', with: 'Angela Claire Louise'
click_button('Continuer')
fill_in "INE", with: 'wrong code'
blur
expect(page).to have_css('span', text: 'Brouillon enregistré', visible: true)
dossier = Dossier.last
mesri_champ = dossier.champs.fourth
expect(mesri_champ.ine).to eq('wrong code')
fill_in "INE", with: ine
VCR.use_cassette('api_particulier/success/etudiants') do
perform_enqueued_jobs { click_on 'Déposer le dossier' }
end
visit demande_dossier_path(dossier)
expect(page).to have_content(/Des données.*ont été reçues depuis le MESRI/)
log_out
login_as instructeur.user, scope: :user
visit instructeur_dossier_path(procedure, dossier)
expect(page).to have_content('INE 090601811AB')
expect(page).to have_content('nom DUBOIS')
expect(page).to have_content('prénom Angela Claire Louise')
expect(page).to have_content('date de naissance 24 août 1962')
expect(page).to have_content('statut inscrit')
expect(page).to have_content('régime formation continue')
expect(page).to have_content("date de début d'inscription 1 septembre 2022")
expect(page).to have_content("date de fin d'inscription 31 août 2023")
expect(page).to have_content('code de la commune 75106')
expect(page).to have_content('statut admis')
expect(page).to have_content('régime formation continue')
expect(page).to have_content("date de début d'admission 1 septembre 2021")
expect(page).to have_content("date de fin d'admission 31 août 2022")
expect(page).to have_content('code de la commune 75106')
expect(page).to have_content('UAI 0751722P')
expect(page).to have_content('nom Université Pierre et Marie Curie - UPCM (Paris 6)')
end
end
scenario 'it can fill a DGFiP field' do
visit commencer_path(path: procedure.path)
click_on 'Commencer la démarche'