diff --git a/app/lib/api_particulier/api.rb b/app/lib/api_particulier/api.rb index ba2207c22..1805d71db 100644 --- a/app/lib/api_particulier/api.rb +++ b/app/lib/api_particulier/api.rb @@ -5,6 +5,7 @@ class APIParticulier::API COMPOSITION_FAMILIALE_RESOURCE_NAME = "v2/composition-familiale" AVIS_IMPOSITION_RESOURCE_NAME = "v2/avis-imposition" SITUATION_POLE_EMPLOI = "v2/situations-pole-emploi" + ETUDIANTS_RESOURCE_NAME = "v2/etudiants" TIMEOUT = 20 @@ -34,6 +35,16 @@ class APIParticulier::API get(SITUATION_POLE_EMPLOI, identifiant: identifiant) end + def etudiants(ine) + # NOTE: Paramètres d'appel mutuellement exclusifs, + # l'appel par INE est réservé aux acteurs de la sphère de l'enseignement + # - INE, l'Identifiant National Étudiant + # - état civil, constitué du nom, prénom, date de naissance, sexe et lieu de naissance + + # TODO: ajouter le support de l'état civil + get(ETUDIANTS_RESOURCE_NAME, ine: ine) + end + private def get(resource_name, params = {}) diff --git a/app/lib/api_particulier/mesri_adapter.rb b/app/lib/api_particulier/mesri_adapter.rb new file mode 100644 index 000000000..ea27f4168 --- /dev/null +++ b/app/lib/api_particulier/mesri_adapter.rb @@ -0,0 +1,48 @@ +class APIParticulier::MesriAdapter + class InvalidSchemaError < ::StandardError + def initialize(errors) + super(errors.map(&:to_json).join("\n")) + end + end + + def initialize(api_particulier_token, ine, requested_sources) + @api = APIParticulier::API.new(api_particulier_token) + @ine = ine + @requested_sources = requested_sources + end + + def to_params + @api.etudiants(@ine) + .tap { |d| ensure_valid_schema!(d) } + .then { |d| extract_requested_sources(d) } + end + + private + + def ensure_valid_schema!(data) + if !schemer.valid?(data) + errors = schemer.validate(data).to_a + raise InvalidSchemaError.new(errors) + end + end + + def schemer + @schemer ||= JSONSchemer.schema(Rails.root.join('app/schemas/etudiants.json')) + end + + def extract_requested_sources(data) + @requested_sources['mesri']&.map do |(scope, sources)| + case scope + when 'inscriptions' + { scope => data[scope].filter_map { |d| d.slice(*sources) if d.key?('dateDebutInscription') } } + when 'admissions' + { scope => data['inscriptions'].filter_map { |d| d.slice(*sources) if d.key?('dateDebutAdmission') } } + when 'etablissements' + { scope => data['inscriptions'].map { |d| d['etablissement'].slice(*sources) }.uniq } + else + { scope => data.slice(*sources) } + end + end + &.flatten&.reduce(&:deep_merge) || {} + end +end diff --git a/app/schemas/etudiants.json b/app/schemas/etudiants.json new file mode 100644 index 000000000..2b302ed20 --- /dev/null +++ b/app/schemas/etudiants.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://demarches-simplifiees.fr/etudiants.schema.json", + "title": "statut étudiant", + "type": "object", + "properties": { + "ine": { + "type": "string" + }, + "nom": { + "type": "string" + }, + "prenom": { + "type": "string" + }, + "dateNaissance": { + "format": "date", + "type": "string" + }, + "inscriptions": { + "type": "array", + "items" : { + "type": "object", + "properties": { + "dateDebutInscription": { + "format": "date", + "type": "string" + }, + "dateFinInscription": { + "format": "date", + "type": "string" + }, + "statut": { + "enum": ["admis", "inscrit"] + }, + "regime": { + "enum": ["formation initiale", "formation continue"] + }, + "codeCommune": { + "type": "string" + }, + "etablissement": { + "type": "object", + "properties": { + "uai": { + "type": "string" + }, + "nom": { + "type": "string" + } + } + } + } + } + } + } +} diff --git a/spec/fixtures/cassettes/api_particulier/success/etudiants.yml b/spec/fixtures/cassettes/api_particulier/success/etudiants.yml new file mode 100644 index 000000000..2b80f807b --- /dev/null +++ b/spec/fixtures/cassettes/api_particulier/success/etudiants.yml @@ -0,0 +1,83 @@ +--- +http_interactions: +- request: + method: get + uri: https://particulier.api.gouv.fr/api/v2/etudiants?ine=090601811AB + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - demarches-simplifiees.fr + Accept: + - application/json + X-Api-Key: + - c6d23f3900b8fb4b3586c4804c051af79062f54b + Expect: + - '' + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 16 Mar 2021 17:01:19 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '805' + Connection: + - keep-alive + Keep-Alive: + - timeout=5 + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Expose-Headers: + - Range,Content-Range,X-Content-Range,X-API-Key + Etag: + - W/"2eb-A0NiRd+gbJKIAT0y4tR4j9tjXb0" + Server: + - nginx + Strict-Transport-Security: + - max-age=15552000 + - max-age=15552000 + Vary: + - Origin, Accept + X-Gravitee-Request-Id: + - 7bfb7f99-ac2d-4443-bb7f-99ac2d0443c5 + X-Gravitee-Transaction-Id: + - f5dca8b3-2ab7-4c9a-9ca8-b32ab70c9a2b + body: + encoding: ASCII-8BIT + string: '{ + "ine": "090601811AB", + "nom": "DUBOIS", + "prenom": "Angela Claire Louise", + "dateNaissance": "1962-08-24", + "inscriptions": [ + { + "statut": "admis", + "regime": "formation continue", + "dateDebutAdmission": "2021-09-01T00:00:00.000Z", + "dateFinAdmission": "2022-08-31T00:00:00.000Z", + "etablissement": { + "uai": "0751722P", + "nom": "Université Pierre et Marie Curie - UPCM (Paris 6)" + }, + "codeCommune": "75106" + }, + { + "statut": "inscrit", + "regime": "formation continue", + "dateDebutInscription": "2022-09-01", + "dateFinInscription": "2023-08-31", + "etablissement": { + "uai": "0751722P", + "nom": "Université Pierre et Marie Curie - UPCM (Paris 6)" + }, + "codeCommune": "75106" + } + ] + }' + recorded_at: Tue, 16 Mar 2021 17:01:18 GMT +recorded_with: VCR 6.0.0 diff --git a/spec/fixtures/cassettes/api_particulier/success/etudiants_invalid.yml b/spec/fixtures/cassettes/api_particulier/success/etudiants_invalid.yml new file mode 100644 index 000000000..8b1c31f69 --- /dev/null +++ b/spec/fixtures/cassettes/api_particulier/success/etudiants_invalid.yml @@ -0,0 +1,83 @@ +--- +http_interactions: +- request: + method: get + uri: https://particulier.api.gouv.fr/api/v2/etudiants?ine=090601811AB + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - demarches-simplifiees.fr + Accept: + - application/json + X-Api-Key: + - c6d23f3900b8fb4b3586c4804c051af79062f54b + Expect: + - '' + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 16 Mar 2021 17:01:19 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '806' + Connection: + - keep-alive + Keep-Alive: + - timeout=5 + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Expose-Headers: + - Range,Content-Range,X-Content-Range,X-API-Key + Etag: + - W/"2eb-A0NiRd+gbJKIAT0y4tR4j9tjXb0" + Server: + - nginx + Strict-Transport-Security: + - max-age=15552000 + - max-age=15552000 + Vary: + - Origin, Accept + X-Gravitee-Request-Id: + - 7bfb7f99-ac2d-4443-bb7f-99ac2d0443c5 + X-Gravitee-Transaction-Id: + - f5dca8b3-2ab7-4c9a-9ca8-b32ab70c9a2b + body: + encoding: ASCII-8BIT + string: '{ + "ine": "090601811AB", + "nom": "DUBOIS", + "prenom": "Angela Claire Louise", + "dateNaissance": "1962-08-24", + "inscriptions": [ + { + "statut": "absent", + "regime": "formation continue", + "dateDebutAdmission": "2021-09-01T00:00:00.000Z", + "dateFinAdmission": "2022-08-31T00:00:00.000Z", + "etablissement": { + "uai": "0751722P", + "nom": "Université Pierre et Marie Curie - UPCM (Paris 6)" + }, + "codeCommune": "75106" + }, + { + "statut": "inscrit", + "regime": "formation continue", + "dateDebutInscription": "2022-09-01", + "dateFinInscription": "2023-08-31", + "etablissement": { + "uai": "0751722P", + "nom": "Université Pierre et Marie Curie - UPCM (Paris 6)" + }, + "codeCommune": "75106" + } + ] + }' + recorded_at: Tue, 16 Mar 2021 17:01:18 GMT +recorded_with: VCR 6.0.0 diff --git a/spec/fixtures/files/api_particulier/etudiants.json b/spec/fixtures/files/api_particulier/etudiants.json new file mode 100644 index 000000000..25ee0bd67 --- /dev/null +++ b/spec/fixtures/files/api_particulier/etudiants.json @@ -0,0 +1,34 @@ +{ + "identifiant": { + "ine": "090601811AB" + }, + "identite": { + "nom": "DUBOIS", + "prenom": "Angela Claire Louise", + "dateNaissance": "1962-08-24" + }, + "admissions": [ + { + "statut": "admis", + "regime": "formation continue", + "dateDebutAdmission": "2021-09-01T00:00:00.000Z", + "dateFinAdmission": "2022-08-31T00:00:00.000Z", + "codeCommune": "75106" + } + ], + "inscriptions": [ + { + "statut": "inscrit", + "regime": "formation continue", + "dateDebutInscription": "2022-09-01", + "dateFinInscription": "2023-08-31", + "codeCommune": "75106" + } + ], + "etablissements": [ + { + "uai": "0751722P", + "nom": "Université Pierre et Marie Curie - UPCM (Paris 6)" + } + ] +} diff --git a/spec/lib/api_particulier/mesri_adapter_spec.rb b/spec/lib/api_particulier/mesri_adapter_spec.rb new file mode 100644 index 000000000..5bb94d57c --- /dev/null +++ b/spec/lib/api_particulier/mesri_adapter_spec.rb @@ -0,0 +1,70 @@ +describe APIParticulier::MesriAdapter do + let(:adapter) { described_class.new(api_particulier_token, ine, requested_sources) } + + before { stub_const('API_PARTICULIER_URL', 'https://particulier.api.gouv.fr/api') } + + describe '#to_params' do + let(:api_particulier_token) { 'c6d23f3900b8fb4b3586c4804c051af79062f54b' } + let(:ine) { '090601811AB' } + + subject { VCR.use_cassette(cassette) { adapter.to_params } } + + context 'when the api answer is valid' do + let(:cassette) { 'api_particulier/success/etudiants' } + + context 'when the token has all the MESRI scopes' do + context 'and all the sources are requested' do + let(:requested_sources) do + { + 'mesri' => { + 'identifiant' => ['ine'], + 'identite' => ['nom', 'prenom', 'dateNaissance'], + 'inscriptions' => ['statut', 'regime', 'dateDebutInscription', 'dateFinInscription', 'codeCommune'], + 'admissions' => ['statut', 'regime', 'dateDebutAdmission', 'dateFinAdmission', 'codeCommune'], + 'etablissements' => ['uai', 'nom'] + } + } + end + + let(:result) { JSON.parse(File.read('spec/fixtures/files/api_particulier/etudiants.json')) } + + it { is_expected.to eq(result) } + end + + context 'when no sources is requested' do + let(:requested_sources) { {} } + + it { is_expected.to eq({}) } + end + + context 'when an admission statut is requested' do + let(:requested_sources) { { 'mesri' => { 'admissions' => ['statut'] } } } + + it { is_expected.to eq('admissions' => [{ 'statut' => 'admis' }]) } + end + + context 'when an inscription statut is requested' do + let(:requested_sources) { { 'mesri' => { 'inscriptions' => ['statut'] } } } + + it { is_expected.to eq('inscriptions' => [{ 'statut' => 'inscrit' }]) } + end + + context 'when a first name is requested' do + let(:requested_sources) { { 'mesri' => { 'identite' => ['prenom'] } } } + + it { is_expected.to eq('identite' => { 'prenom' => 'Angela Claire Louise' }) } + end + end + end + + context 'when the api answer is invalid' do + let(:cassette) { 'api_particulier/success/etudiants_invalid' } + + context 'when no sources is requested' do + let(:requested_sources) { {} } + + it { expect { subject }.to raise_error(APIParticulier::MesriAdapter::InvalidSchemaError) } + end + end + end +end