From d3d208869a2dc4a056f1d4ad88d489b04b37bbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Tue, 2 Apr 2024 11:11:55 +0200 Subject: [PATCH 01/14] establishments: fix the department code logic ... by directly storing it from the API which already provides a three-digit, INSEE-compliant, 0-padded department code. > %w[9740082W 7200011Z 0340045P].map { |uai| EstablishmentApi.fetch!(uai)["records"][0]["fields"]["code_departement"] } > ["974", "02B", "034"] --- app/models/establishment.rb | 11 ++--------- app/services/asp/mappers/prestadoss_mapper.rb | 2 +- ...24_add_department_code_to_establishments.rb | 7 +++++++ db/schema.rb | 3 ++- spec/factories/establishments.rb | 1 + spec/models/establishment_spec.rb | 18 ------------------ 6 files changed, 13 insertions(+), 29 deletions(-) create mode 100644 db/migrate/20240402090724_add_department_code_to_establishments.rb diff --git a/app/models/establishment.rb b/app/models/establishment.rb index 80c563341..c1ffa9888 100644 --- a/app/models/establishment.rb +++ b/app/models/establishment.rb @@ -45,7 +45,8 @@ def directors "telephone" => :telephone, "mail" => :email, "code_type_contrat_prive" => :private_contract_type_code, - "ministere_tutelle" => :ministry + "ministere_tutelle" => :ministry, + "code_departement" => :department_code }.freeze # Find all codes here : https://infocentre.pleiade.education.fr/bcn/workspace/viewTable/n/N_CONTRAT_ETABLISSEMENT @@ -66,14 +67,6 @@ def select_label [uai, name].compact.join(" - ") end - def department_code - if postal_code.start_with?("97") - postal_code.first(3) - else - postal_code.first(2) - end - end - def address [address_line1, address_line2, postal_code, city].join(", ") end diff --git a/app/services/asp/mappers/prestadoss_mapper.rb b/app/services/asp/mappers/prestadoss_mapper.rb index 386f4c61d..f908cde79 100644 --- a/app/services/asp/mappers/prestadoss_mapper.rb +++ b/app/services/asp/mappers/prestadoss_mapper.rb @@ -27,7 +27,7 @@ def montanttotalengage end def valeur - schooling.establishment.department_code.rjust(3, "0") + schooling.establishment.department_code end def id_prestation_dossier diff --git a/db/migrate/20240402090724_add_department_code_to_establishments.rb b/db/migrate/20240402090724_add_department_code_to_establishments.rb new file mode 100644 index 000000000..e76deed32 --- /dev/null +++ b/db/migrate/20240402090724_add_department_code_to_establishments.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddDepartmentCodeToEstablishments < ActiveRecord::Migration[7.1] + def change + add_column :establishments, :department_code, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index c12279ccc..ede245258 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_21_161647) do +ActiveRecord::Schema[7.1].define(version: 2024_04_02_090724) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -128,6 +128,7 @@ t.string "students_provider" t.string "ministry" t.bigint "confirmed_director_id" + t.string "department_code" t.index ["confirmed_director_id"], name: "index_establishments_on_confirmed_director_id" t.index ["uai"], name: "index_establishments_on_uai", unique: true end diff --git a/spec/factories/establishments.rb b/spec/factories/establishments.rb index 949c8534e..fbfe6e36b 100644 --- a/spec/factories/establishments.rb +++ b/spec/factories/establishments.rb @@ -16,6 +16,7 @@ students_provider { nil } ministry { "MINISTERE DE L'EDUCATION NATIONALE" } confirmed_director { nil } + department_code { "034" } trait :private do private_contract_type_code { "31" } diff --git a/spec/models/establishment_spec.rb b/spec/models/establishment_spec.rb index 121ac5766..c08765f61 100644 --- a/spec/models/establishment_spec.rb +++ b/spec/models/establishment_spec.rb @@ -48,22 +48,4 @@ it { is_expected.not_to be_valid } end end - - describe "department_code" do - context "when it's from metropolitan France" do - before { establishment.update!(postal_code: "34070") } - - it "is the first 2 characters" do - expect(establishment.department_code).to eq "34" - end - end - - context "when it is from overseas France" do - before { establishment.update!(postal_code: "97492") } - - it "is the first 3 characters" do - expect(establishment.department_code).to eq "974" - end - end - end end From c76084197fbfa1aa5c331e9374364198bdfa51eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Tue, 2 Apr 2024 11:48:54 +0200 Subject: [PATCH 02/14] establishments: fix the spec - etab -> establishments ; - don't go through WebMock (that's the EstablishmentApi spec's job) ; - use a "dehydrated" establishment to avoid flaky specs. --- spec/factories/establishments.rb | 6 ++++ spec/jobs/fetch_establishment_job_spec.rb | 37 +++++++++-------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/spec/factories/establishments.rb b/spec/factories/establishments.rb index fbfe6e36b..d700736c0 100644 --- a/spec/factories/establishments.rb +++ b/spec/factories/establishments.rb @@ -18,6 +18,12 @@ confirmed_director { nil } department_code { "034" } + trait :dehydrated do + Establishment::API_MAPPING.each_value do |field| + send(field) { nil } + end + end + trait :private do private_contract_type_code { "31" } end diff --git a/spec/jobs/fetch_establishment_job_spec.rb b/spec/jobs/fetch_establishment_job_spec.rb index 6e4810e18..c49bcc13b 100644 --- a/spec/jobs/fetch_establishment_job_spec.rb +++ b/spec/jobs/fetch_establishment_job_spec.rb @@ -3,44 +3,37 @@ require "rails_helper" RSpec.describe FetchEstablishmentJob do - let(:etab) { create(:establishment, :sygne_provider, private_contract_type_code: nil) } - let!(:fixture) { Rails.root.join("mock/data/etab.json").read } + # we want a "dehydrated" (i.e not API-refreshed) establishment to + # avoid flaky specs where the data returned from the fixture matches + # the factory's attribute which will crash the + # + # expect(api call).to change (establishment, attribute) + # + # matcher further below. + let(:establishment) { create(:establishment, :dehydrated, :sygne_provider) } + let(:json) { Rails.root.join("mock/data/etab.json").read } before do - allow(EstablishmentApi).to receive(:fetch!).and_call_original - - stub_request(:get, /#{ENV.fetch('APLYPRO_ESTABLISHMENTS_DATA_URL')}/) - .with( - headers: { - "Accept" => "*/*", - "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", - "Content-Type" => "application/json" - } - ) - .to_return(status: 200, body: fixture, headers: { "Content-Type" => "application/json" }) + allow(EstablishmentApi).to receive(:fetch!).and_return(JSON.parse(json)) end it "calls the EstablishmentApi proxy" do - described_class.perform_now(etab) + described_class.perform_now(establishment) - expect(EstablishmentApi).to have_received(:fetch!).with(etab.uai) - end - - it "updates the establishement's name" do - expect { described_class.perform_now(etab) }.to change { etab.reload.name }.to "Lycée de la Mer Paul Bousquet" + expect(EstablishmentApi).to have_received(:fetch!).with(establishment.uai) end Establishment::API_MAPPING.each_value do |attr| it "updates the `#{attr}' attribute" do - expect { described_class.perform_now(etab) }.to change(etab, attr) + expect { described_class.perform_now(establishment) }.to change(establishment, attr) end end context "when the EstablishmentApi returns no data" do - let(:fixture) { JSON.parse(Rails.root.join("mock/data/etab.json").read).merge({ "records" => [] }).to_json } + let(:json) { { "records" => [] }.to_json } it "doesn't raise an error" do - expect { described_class.perform_now(etab) }.not_to raise_error + expect { described_class.perform_now(establishment) }.not_to raise_error end end end From 6cad12a4a36981bd272c80a835a069f596a74eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Tue, 2 Apr 2024 15:54:44 +0200 Subject: [PATCH 03/14] pfmp: ensure the day count fits within the start/end dates --- app/models/pfmp.rb | 8 ++++- config/locales/fr.yml | 3 ++ features/anciens_eleves.feature | 2 +- features/calcul_des_montants.feature | 4 +-- features/completion_de_pfmps.feature | 14 ++++----- features/gestion_de_pfmp.feature | 2 +- features/paiements.feature | 26 ++++++++-------- features/step_definitions/pfmp_steps.rb | 11 ++++--- features/validation_en_masse_de_pfmps.feature | 2 +- .../concerns/pfmp_amount_calculator_spec.rb | 12 +++++-- spec/models/pfmp_spec.rb | 31 ++++++++++++++++++- 11 files changed, 81 insertions(+), 34 deletions(-) diff --git a/app/models/pfmp.rb b/app/models/pfmp.rb index 9e68c066e..571133ce8 100644 --- a/app/models/pfmp.rb +++ b/app/models/pfmp.rb @@ -22,7 +22,13 @@ class Pfmp < ApplicationRecord validates :end_date, :start_date, inclusion: Aplypro::SCHOOL_YEAR_RANGE - validates :day_count, numericality: { only_integer: true, allow_nil: true, greater_than: 0 } + validates :day_count, + numericality: { + only_integer: true, + allow_nil: true, + greater_than: 0, + less_than_or_equal_to: ->(pfmp) { (pfmp.end_date - pfmp.start_date).to_i } + } scope :finished, -> { where("pfmps.end_date <= (?)", Time.zone.today) } diff --git a/config/locales/fr.yml b/config/locales/fr.yml index ddf7e59b5..1499232df 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -55,6 +55,9 @@ fr: format: "La date de fin %{message}" inclusion: ne peut pas excéder l'année scolaire en cours greater_than_or_equal_to: "doit être ultérieure à la date de début" + day_count: + format: "Le %{attribute} %{message}" + less_than_or_equal_to: "n'est pas cohérent avec les dates de début et de fin" hints: rib: name: "Noms et prénoms du titulaire du compte" diff --git a/features/anciens_eleves.feature b/features/anciens_eleves.feature index b00975de0..3be6bdc86 100644 --- a/features/anciens_eleves.feature +++ b/features/anciens_eleves.feature @@ -85,7 +85,7 @@ Fonctionnalité: Les anciens élèves sont inclus à part dans l'interface Et que je peux voir dans le tableau "Liste des pfmps à compléter de la classe 1MELEC" | Élève | | Dupuis Jean Sorti(e) de la classe | - Et que je remplis le champ "Nombre de jours" dans la rangée "Dupuis Jean" avec "12" + Et que je remplis le champ "Nombre de jours" dans la rangée "Dupuis Jean" avec "3" Lorsque je clique sur "Enregistrer 1 PFMP" Alors la page contient "Les PFMPs ont bien été modifiées" Et je peux voir dans le tableau "Élèves sortis de la classe" diff --git a/features/calcul_des_montants.feature b/features/calcul_des_montants.feature index 86dbad86b..8e0528a6b 100644 --- a/features/calcul_des_montants.feature +++ b/features/calcul_des_montants.feature @@ -19,10 +19,10 @@ Fonctionnalité: Le personnel de direction peut constater les montants des PFMPs | Saisie à valider | 3 | 30 € | Scénario: Le personnel de direction peut voir le montant plafonné - Quand je renseigne une PFMP de 300 jours + Quand je renseigne une PFMP de 11 jours Et je peux voir dans le tableau "Liste des PFMPs de l'élève" | État | Nombre de jours | Montant | - | Saisie à valider | 3 | 100 € | + | Saisie à valider | 11 | 100 € | Scénario: Le personnel de direction peut voir un montant plafonné par une autre PFMP Quand je renseigne une PFMP de 9 jours diff --git a/features/completion_de_pfmps.feature b/features/completion_de_pfmps.feature index d0a732ad7..4537196f0 100644 --- a/features/completion_de_pfmps.feature +++ b/features/completion_de_pfmps.feature @@ -7,14 +7,14 @@ Fonctionnalité: Complétion des PFMPs d'une classe Et que je passe l'écran d'accueil Et qu'il y a une élève "Marie Curie" au sein de la classe "2NDEB" pour une formation "Développement" Et qu'il y a un élève "Paul Langevin" au sein de la classe "2NDEB" pour une formation "Développement" - Et que je saisis une PFMP pour toute la classe "2NDEB" avec les dates "17/03/2024" et "20/03/2024" + Et que je saisis une PFMP pour toute la classe "2NDEB" avec les dates "17/03/2024" et "17/04/2024" Et que je clique sur "Compléter 2 PFMPs" Scénario: Le personnel peut accéder à la page de complétion des PFMPs à compléter Alors je peux voir dans le tableau "Liste des pfmps à compléter de la classe 2NDEB" - | Élève | PFMP | Nombre de jours | - | Curie Marie | mars 2024 | | - | Langevin Paul | mars 2024 | | + | Élève | PFMP | Nombre de jours | + | Curie Marie | mars 2024 - avr. 2024 | | + | Langevin Paul | mars 2024 - avr. 2024 | | Scénario: Le personnel peut saisir et enregistrer des nombre de jours pour toutes les PFMPs à compléter Et que je remplis le champ "Nombre de jours" dans la rangée "Curie Marie" avec "12" @@ -22,9 +22,9 @@ Fonctionnalité: Complétion des PFMPs d'une classe Quand je clique sur "Enregistrer 2 PFMPs" Alors la page contient "Les PFMPs ont bien été modifiées" Et je peux voir dans le tableau "Liste des élèves" - | Élèves (2) | Décisions d'attribution (0/2) | Coordonnées Bancaires (0/2) | PFMPs (2) | - | Curie Marie | | | Saisie à valider mars 2024 | - | Langevin Paul | | | Saisie à valider mars 2024 | + | Élèves (2) | Décisions d'attribution (0/2) | Coordonnées Bancaires (0/2) | PFMPs (2) | + | Curie Marie | | | Saisie à valider mars 2024 - avr. 2024 | + | Langevin Paul | | | Saisie à valider mars 2024 - avr. 2024 | Scénario: Le personnel est informé d'une erreur de saisie quand il complète les PFMPs d'une classe Et que je remplis le champ "Nombre de jours" dans la rangée "Curie Marie" avec "-12" diff --git a/features/gestion_de_pfmp.feature b/features/gestion_de_pfmp.feature index 4f56f1392..8ba477ed2 100644 --- a/features/gestion_de_pfmp.feature +++ b/features/gestion_de_pfmp.feature @@ -28,7 +28,7 @@ Fonctionnalité: Le personnel de direction édite les PFMPs Et la page ne contient pas "Compléter 10 PFMP" Scénario: Le personnel de direction peut modifier une PFMP - Sachant que je renseigne une PFMP de 3 jours + Sachant que je renseigne une PFMP de 13 jours Et que je clique sur "Voir la PFMP" Et que je clique sur "Modifier la PFMP" Et que je remplis "Nombre de jours" avec "10" diff --git a/features/paiements.feature b/features/paiements.feature index f4416d282..04f3bf4d7 100644 --- a/features/paiements.feature +++ b/features/paiements.feature @@ -8,52 +8,52 @@ Fonctionnalité: Gestion des paiements Et que je passe l'écran d'accueil Et que toutes les tâches de fond sont terminées Et que je consulte le profil de "Marie Curie" dans la classe de "A1" - Et que je renseigne et valide une PFMP de 3 jours + Et que je renseigne et valide une PFMP de 9 jours Et que je consulte le profil de "Marie Curie" dans la classe de "A1" Scénario: une PFMP avec une requête de paiement en attente peut être modifiée Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "En attente" de 30 euros - Et je peux modifier le nombre de jours de la PFMP + Alors je peux voir une demande de paiement "En attente" de 90 euros + Et je peux changer le nombre de jours de la PFMP à 8 Scénario: une PFMP avec une requête de paiement incomplete peut être modifiée Sachant que la dernière PFMP de "Marie Curie" en classe de "A1" a une requête de paiement incomplète Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "Bloquée" de 30 euros - Et je peux modifier le nombre de jours de la PFMP + Alors je peux voir une demande de paiement "Bloquée" de 90 euros + Et je peux changer le nombre de jours de la PFMP à 8 Scénario: une PFMP avec une requête de paiement prête à l'envoi ne peut pas être modifiée Sachant que la dernière PFMP de "Marie Curie" en classe de "A1" a une requête de paiement prête à l'envoi Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "En attente" de 30 euros + Alors je peux voir une demande de paiement "En attente" de 90 euros Et je ne peux pas éditer ni supprimer la PFMP Scénario: une PFMP avec une requête de paiement envoyée ne peut pas être modifiée Sachant que la dernière PFMP de "Marie Curie" en classe de "A1" a une requête de paiement envoyée Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "En traitement" de 30 euros + Alors je peux voir une demande de paiement "En traitement" de 90 euros Et je ne peux pas éditer ni supprimer la PFMP Scénario: une PFMP avec une requête de paiement intégrée ne peut être modifiée Sachant que la dernière PFMP de "Marie Curie" en classe de "A1" a une requête de paiement intégrée Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "En traitement" de 30 euros + Alors je peux voir une demande de paiement "En traitement" de 90 euros Et je ne peux pas éditer ni supprimer la PFMP Scénario: une PFMP avec une requête de paiement rejetée peut être modifiée Sachant que la dernière PFMP de "Marie Curie" en classe de "A1" a une requête de paiement rejetée Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "rejetée" de 30 euros - Et je peux modifier le nombre de jours de la PFMP + Alors je peux voir une demande de paiement "rejetée" de 90 euros + Et je peux changer le nombre de jours de la PFMP à 8 Scénario: une PFMP avec une requête de paiement liquidé ne peut être modifiée Sachant que la dernière PFMP de "Marie Curie" en classe de "A1" a une requête de paiement liquidée Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "envoyé" de 30 euros + Alors je peux voir une demande de paiement "envoyé" de 90 euros Et je ne peux pas éditer ni supprimer la PFMP Scénario: une PFMP avec une requête de paiement échouée peut être modifiée Sachant que la dernière PFMP de "Marie Curie" en classe de "A1" a une requête de paiement échouée Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "échoué" de 30 euros - Et je peux modifier le nombre de jours de la PFMP + Alors je peux voir une demande de paiement "échoué" de 90 euros + Et je peux changer le nombre de jours de la PFMP à 8 diff --git a/features/step_definitions/pfmp_steps.rb b/features/step_definitions/pfmp_steps.rb index db79c3eac..0967b7d86 100644 --- a/features/step_definitions/pfmp_steps.rb +++ b/features/step_definitions/pfmp_steps.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true Quand("je renseigne une PFMP de {int} jours") do |days| + start_date = Date.parse("17/03/2024") + end_date = start_date + days.days + steps %( Quand je clique sur "Ajouter une PFMP" - Et que je remplis "Date de début" avec "17/03/2024" - Et que je remplis "Date de fin" avec "20/03/2024" + Et que je remplis "Date de début" avec "#{start_date}" + Et que je remplis "Date de fin" avec "#{end_date}" Et que je remplis "Nombre de jours effectués" avec "#{days}" Et que je clique sur "Enregistrer" ) @@ -82,9 +85,7 @@ student.pfmps.last.transition_to!(:validated) end -Alors("je peux modifier le nombre de jours de la PFMP") do - days = rand(1..6) - +Alors("je peux changer le nombre de jours de la PFMP à {int}") do |days| steps %( Quand je clique sur "Modifier la PFMP" Et que je remplis "Nombre de jours" avec "#{days}" diff --git a/features/validation_en_masse_de_pfmps.feature b/features/validation_en_masse_de_pfmps.feature index b9ed15a44..fdc444640 100644 --- a/features/validation_en_masse_de_pfmps.feature +++ b/features/validation_en_masse_de_pfmps.feature @@ -7,7 +7,7 @@ Fonctionnalité: Complétion des PFMPs d'une classe Et que je passe l'écran d'accueil Et qu'il y a une élève "Marie Curie" au sein de la classe "2NDEB" pour une formation "2NDEPRO Développement" Et qu'il y a un élève "Paul Langevin" au sein de la classe "2NDEB" pour une formation "2NDEPRO Développement" - Et que je saisis une PFMP pour toute la classe "2NDEB" avec les dates "17/03/2024" et "20/03/2024" + Et que je saisis une PFMP pour toute la classe "2NDEB" avec les dates "01/03/2024" et "30/03/2024" Et que je clique sur "Compléter 2 PFMPs" Et que je remplis le champ "Nombre de jours" dans la rangée "Curie Marie" avec "12" Et que je remplis le champ "Nombre de jours" dans la rangée "Langevin Paul" avec "4" diff --git a/spec/models/concerns/pfmp_amount_calculator_spec.rb b/spec/models/concerns/pfmp_amount_calculator_spec.rb index 765b980d7..46e3d76cb 100644 --- a/spec/models/concerns/pfmp_amount_calculator_spec.rb +++ b/spec/models/concerns/pfmp_amount_calculator_spec.rb @@ -7,7 +7,15 @@ describe PfmpAmountCalculator do subject(:amount) { pfmp.reload.calculate_amount } - let(:pfmp) { create(:pfmp, day_count: 3) } + let(:pfmp) do + create( + :pfmp, + start_date: Aplypro::SCHOOL_YEAR_START, + end_date: Aplypro::SCHOOL_YEAR_START >> 10, + day_count: 3 + ) + end + let(:mef) { create(:mef, daily_rate: 1, yearly_cap: 10) } RSpec.configure do |config| @@ -40,7 +48,7 @@ context "when the PFMP goes over the yearly cap" do before do - pfmp.update!(day_count: 1000) + pfmp.update!(day_count: 200) end it_calculates "the yearly-capped amount" diff --git a/spec/models/pfmp_spec.rb b/spec/models/pfmp_spec.rb index 07b03c9f8..58ee570d3 100644 --- a/spec/models/pfmp_spec.rb +++ b/spec/models/pfmp_spec.rb @@ -45,6 +45,28 @@ it { is_expected.not_to be_valid } end + + describe "day count" do + subject(:pfmp) { build(:pfmp, start_date: Time.zone.now, end_date: 7.days.from_now) } + + context "when the number of days doesn't fit in the date range" do + before { pfmp.day_count = 8 } + + it { is_expected.not_to be_valid } + + it "has the correct error message" do + pfmp.validate + + expect(pfmp.errors[:day_count]).to include(/n'est pas cohérent/) + end + end + + context "when the number fits exactly in the day range" do + before { pfmp.day_count = 7 } + + it { is_expected.to be_valid } + end + end end describe "states" do @@ -110,7 +132,14 @@ context "when there is no allowance left" do before do - create(:pfmp, :validated, schooling: schooling, day_count: 100) + create( + :pfmp, + :validated, + start_date: Aplypro::SCHOOL_YEAR_START, + end_date: Aplypro::SCHOOL_YEAR_START >> 4, + schooling: schooling, + day_count: 100 + ) end it "does not create a payment" do From 4adc080dd2aae1f411c96df16f4c324f920df071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Tue, 2 Apr 2024 16:05:40 +0200 Subject: [PATCH 04/14] asp: fix the integration spec --- spec/lib/asp/entities/entities_integration_spec.rb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/spec/lib/asp/entities/entities_integration_spec.rb b/spec/lib/asp/entities/entities_integration_spec.rb index 009c2ad5c..b6eb891d1 100644 --- a/spec/lib/asp/entities/entities_integration_spec.rb +++ b/spec/lib/asp/entities/entities_integration_spec.rb @@ -15,14 +15,7 @@ describe "ASP Entities" do # rubocop:disable RSpec/DescribeClass subject(:file) { ASP::Entities::Fichier.new(payment_requests) } - let(:students) { create_list(:student, 3, :with_all_asp_info) } - let(:payment_requests) { ASP::PaymentRequest.all } - - before do - students.each do |student| - create(:pfmp, :validated, student: student) - end - end + let(:payment_requests) { create_list(:asp_payment_request, 3, :ready) } it "produce valid documents" do log_on_failure = -> { file.errors.each { |e| puts "ASP validation error: #{e.message}\n" } } From 83821f95ce64699fc5af90c937c761655743bd37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Tue, 2 Apr 2024 20:06:44 +0200 Subject: [PATCH 05/14] students: try and parse apprentice status The SYGNE API returns the result within a "scolarite" hash which contains the latest schooling for that student by date of creation (says their documentation). Instead of assuming it's the latest open schooling, try and re-match the schooling and only then update whatever can be updated, at the moment only the status code which is "AP" for apprentices. --- app/jobs/fetch_student_information_job.rb | 37 ++++++++++++++--- app/models/schooling.rb | 2 + app/models/student/info_mappers/base.rb | 9 +++- app/models/student/info_mappers/fregata.rb | 37 +++++++++++++++++ app/models/student/info_mappers/sygne.rb | 41 +++++++++++++++++++ app/models/student/mappers/fregata.rb | 5 ++- .../20240402160524_add_status_to_schooling.rb | 7 ++++ db/schema.rb | 3 +- features/anciens_eleves.feature | 4 +- features/retour_des_paiments.feature | 2 +- features/step_definitions/api_steps.rb | 7 +++- mock | 2 +- .../fetch_student_information_job_spec.rb | 29 +++++++++++++ .../generate_attributive_decision_job_spec.rb | 5 ++- .../student/info_mappers/fregata_spec.rb | 10 ++++- .../models/student/info_mappers/sygne_spec.rb | 2 +- spec/models/student/mappers/fregata_spec.rb | 10 +++++ 17 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 db/migrate/20240402160524_add_status_to_schooling.rb diff --git a/app/jobs/fetch_student_information_job.rb b/app/jobs/fetch_student_information_job.rb index dfba7ebc6..9399eea0d 100644 --- a/app/jobs/fetch_student_information_job.rb +++ b/app/jobs/fetch_student_information_job.rb @@ -12,12 +12,39 @@ def perform(schooling) api .fetch_student_data!(student.ine) .then do |data| - mapper = api.info_mapper.new(data) + mapper = api.info_mapper.new(data, establishment.uai) - if mapper.attributes.present? - student.assign_attributes(mapper.attributes) - student.save! - end + update_student!(schooling, mapper) + update_schooling!(mapper) end end + + def update_student!(schooling, mapper) + return if mapper.attributes.blank? + + schooling.student.update!(mapper.attributes) + end + + def update_schooling!(mapper) + schooling = find_schooling(mapper) + + return if schooling.nil? + + attributes = mapper + .schooling_attributes + .slice(*Schooling.attribute_names.map(&:to_sym)) + + schooling.update!(attributes) + end + + def find_schooling(mapper) + mapper.schooling_finder_attributes => { uai:, label:, mef_code:, ine: } + + Schooling + .joins(:establishment, :student, [classe: :mef]) + .where("establishments.uai" => uai) + .where("classe.label" => label) + .where("mef.code" => mef_code) + .find_by("student.ine" => ine) + end end diff --git a/app/models/schooling.rb b/app/models/schooling.rb index a1168092c..e917a26a8 100644 --- a/app/models/schooling.rb +++ b/app/models/schooling.rb @@ -3,6 +3,8 @@ class Schooling < ApplicationRecord has_one_attached :attributive_decision + enum :status, { student: 0, apprentice: 1, other: 2 }, validate: { allow_nil: true } + belongs_to :student belongs_to :classe diff --git a/app/models/student/info_mappers/base.rb b/app/models/student/info_mappers/base.rb index b6f63b4b3..c73ef181f 100644 --- a/app/models/student/info_mappers/base.rb +++ b/app/models/student/info_mappers/base.rb @@ -3,15 +3,20 @@ class Student module InfoMappers class Base - attr_reader :payload + attr_reader :payload, :uai - def initialize(payload) + def initialize(payload, uai) @payload = payload + @uai = uai end def attributes self.class::Mapper.new.call(payload) end + + def schooling_attributes + self.class::SchoolingMapper.new.call(payload) + end end end end diff --git a/app/models/student/info_mappers/fregata.rb b/app/models/student/info_mappers/fregata.rb index 3f0d6197e..b045e2f30 100644 --- a/app/models/student/info_mappers/fregata.rb +++ b/app/models/student/info_mappers/fregata.rb @@ -78,6 +78,43 @@ class AddressMapper < Dry::Transformer::Pipe ] end end + + class SchoolingMapper < Dry::Transformer::Pipe + import Dry::Transformer::HashTransformations + import Dry::Transformer::ArrayTransformations + + define! do + deep_symbolize_keys + + unwrap :statutApprenant + unwrap :apprenant + + rename_keys( + code: :status + ) + + map_value :status, lambda { |value| + case value + when "2503" + :apprentice + when "2501" + :student + else + raise Student::Mappers::Errors::SchoolingParsingError + end + } + + accept_keys %i[status ine] + end + end + + # FREGATA uses the same endpoint for listing and extra info so + # we can reuse the other mappers + def schooling_finder_attributes + schooling_attributes + .merge(Student::Mappers::Fregata::ClasseMapper.new.call(payload)) + .merge(uai: uai) + end end end end diff --git a/app/models/student/info_mappers/sygne.rb b/app/models/student/info_mappers/sygne.rb index a7133b703..a406399f4 100644 --- a/app/models/student/info_mappers/sygne.rb +++ b/app/models/student/info_mappers/sygne.rb @@ -42,6 +42,47 @@ class Mapper < Dry::Transformer::Pipe ] end end + + class SchoolingMapper < Dry::Transformer::Pipe + import Dry::Transformer::HashTransformations + import Dry::Transformer::Coercions + + define! do + deep_symbolize_keys + + unwrap :scolarite + + rename_keys( + classe: :label, + codeStatut: :status, + codeMefRatt: :mef_code, + codeUai: :uai + ) + + map_value(:mef_code, Dry::Transformer::Coercions[:to_string]) + + map_value :mef_code, ->(value) { value.chop } + + map_value :status, lambda { |value| + case value + when "ST" + :student + when "AP" + :apprentice + when "FQ" + :other + else + raise Student::Mappers::Errors::SchoolingParsingError + end + } + + accept_keys %i[ine mef_code label status uai] + end + end + + def schooling_finder_attributes + schooling_attributes + end end end end diff --git a/app/models/student/mappers/fregata.rb b/app/models/student/mappers/fregata.rb index 3d259da93..5b4d91e17 100644 --- a/app/models/student/mappers/fregata.rb +++ b/app/models/student/mappers/fregata.rb @@ -41,7 +41,7 @@ class ClasseMapper < Dry::Transformer::Pipe def map_student_attributes(attrs) student_attrs = super(attrs) - extra_attrs = Student::InfoMappers::Fregata.new(attrs).attributes + extra_attrs = Student::InfoMappers::Fregata.new(attrs, uai).attributes student_attrs.merge!(extra_attrs) if extra_attrs.present? @@ -51,7 +51,10 @@ def map_student_attributes(attrs) def map_schooling!(classe, student, entry) schooling = Schooling.find_or_initialize_by(classe: classe, student: student) + schooling_attributes = Student::InfoMappers::Fregata.new(entry, uai).schooling_attributes + schooling.end_date = left_classe_at(entry) + schooling.status = schooling_attributes[:status] student.close_current_schooling! if schooling.open? && student.current_schooling != schooling diff --git a/db/migrate/20240402160524_add_status_to_schooling.rb b/db/migrate/20240402160524_add_status_to_schooling.rb new file mode 100644 index 000000000..14ac202cc --- /dev/null +++ b/db/migrate/20240402160524_add_status_to_schooling.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddStatusToSchooling < ActiveRecord::Migration[7.1] + def change + add_column :schoolings, :status, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index ede245258..8bd76517e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_04_02_090724) do +ActiveRecord::Schema[7.1].define(version: 2024_04_02_160524) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -204,6 +204,7 @@ t.boolean "generating_attributive_decision", default: false, null: false t.string "asp_dossier_id" t.string "administrative_number" + t.integer "status" t.index ["administrative_number"], name: "index_schoolings_on_administrative_number", unique: true t.index ["classe_id"], name: "index_schoolings_on_classe_id" t.index ["student_id", "classe_id"], name: "one_schooling_per_class_student", unique: true diff --git a/features/anciens_eleves.feature b/features/anciens_eleves.feature index 3be6bdc86..a36779009 100644 --- a/features/anciens_eleves.feature +++ b/features/anciens_eleves.feature @@ -75,7 +75,7 @@ Fonctionnalité: Les anciens élèves sont inclus à part dans l'interface Alors la page contient "La PFMP a bien été enregistrée" Et je peux voir dans le tableau "Liste des PFMPs de l'élève" | État | Nombre de jours | Montant | - | Saisie à valider | 3 | 30 € | + | Saisie à valider | 3 | | Scénario: Le personnel peut compléter les PFMPs pour les anciens élèves Quand je consulte la classe de "1MELEC" @@ -100,7 +100,7 @@ Fonctionnalité: Les anciens élèves sont inclus à part dans l'interface Quand je clique sur "1MELEC" Alors je peux voir dans le tableau "Liste des pfmps à valider" | Élève | PFMP | Nombre de jours | Montant | - | Dupuis Jean | mars 2024 | 3 jours | 30 € | + | Dupuis Jean | mars 2024 | 3 jours | | Et la rangée "Dupuis Jean" contient "Sorti(e) de la classe" Scénario: Le personnel peut valider les PFMPs des anciens élèves diff --git a/features/retour_des_paiments.feature b/features/retour_des_paiments.feature index bd1464dba..b58a27b7d 100644 --- a/features/retour_des_paiments.feature +++ b/features/retour_des_paiments.feature @@ -20,7 +20,7 @@ Fonctionnalité: Gestion des retours de l'ASP Et que la tâche de lecture des paiements démarre Et que toutes les tâches de fond sont terminées Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "En traitement" de 30 euros + Alors je peux voir une demande de paiement "En traitement" Scénario: L'individu n'a pas pu être intégré sur le serveur de l'ASP Sachant que l'ASP a rejetté le dossier de "Marie Curie" avec un motif de "mauvais code postal" diff --git a/features/step_definitions/api_steps.rb b/features/step_definitions/api_steps.rb index fa25fba6d..78d32a2c9 100644 --- a/features/step_definitions/api_steps.rb +++ b/features/step_definitions/api_steps.rb @@ -43,7 +43,12 @@ mef = Mef.find_by!(label: mef) - payload = FactoryBot.build_list(:sygne_student, count, classe: classe, mef: mef.code.concat("0")).tap do |students| + payload = FactoryBot.build_list( + :sygne_student, + count, + classe: classe, + mef_value: mef.code.concat("0") + ).tap do |students| students.last["prenom"] = first_name students.last["nom"] = last_name end diff --git a/mock b/mock index c03818522..6ad1b6e92 160000 --- a/mock +++ b/mock @@ -1 +1 @@ -Subproject commit c038185228d3aebf1630a1a0be73d31e4c7f7eaa +Subproject commit 6ad1b6e92e2871ed575f590c67ceebb3673bc25f diff --git a/spec/jobs/fetch_student_information_job_spec.rb b/spec/jobs/fetch_student_information_job_spec.rb index a3b917ae5..bd28189b6 100644 --- a/spec/jobs/fetch_student_information_job_spec.rb +++ b/spec/jobs/fetch_student_information_job_spec.rb @@ -27,6 +27,27 @@ end end + shared_examples "updates the schooling status" do |factory_name| + let(:factory) { factory_name.to_sym } + let(:status) { :apprentice } + + let(:student_data) do + build( + factory, + status, + classe_label: schooling.classe.label, + ine: student.ine, + uai: establishment.uai, + mef_value: schooling.classe.mef.code.concat("0") + ).to_json + end + + it "updates the schooling's status code" do + expect { described_class.new(schooling).perform_now } + .to change { schooling.reload.status }.from(nil).to(status.to_s) + end + end + context "when the student is from SYGNE" do let(:establishment) { create(:establishment, :sygne_provider) } @@ -40,6 +61,10 @@ end include_examples "maps all the extra fields correctly" + + include_examples "updates the schooling status", "sygne_student_info" do + let(:payload) { student_data } + end end context "when the student is from FREGATA" do @@ -53,6 +78,10 @@ end include_examples "maps all the extra fields correctly" + + include_examples "updates the schooling status", "fregata_student" do + let(:payload) { [JSON.parse(student_data)].to_json } + end end end # rubocop:enable RSpec/MultipleMemoizedHelpers diff --git a/spec/jobs/generate_attributive_decision_job_spec.rb b/spec/jobs/generate_attributive_decision_job_spec.rb index 32918d3eb..0252b6095 100644 --- a/spec/jobs/generate_attributive_decision_job_spec.rb +++ b/spec/jobs/generate_attributive_decision_job_spec.rb @@ -13,7 +13,10 @@ ActiveJob::Base.queue_adapter = :test WebmockHelpers.mock_sygne_token - WebmockHelpers.mock_sygne_student_endpoint_with(student.ine, build(:sygne_student, ine_value: student.ine).to_json) + WebmockHelpers.mock_sygne_student_endpoint_with( + student.ine, + build(:sygne_student_info, ine_value: student.ine).to_json + ) end # NOTE: there's the much nicer diff --git a/spec/models/student/info_mappers/fregata_spec.rb b/spec/models/student/info_mappers/fregata_spec.rb index e67e7efd0..ca62b19c0 100644 --- a/spec/models/student/info_mappers/fregata_spec.rb +++ b/spec/models/student/info_mappers/fregata_spec.rb @@ -5,7 +5,7 @@ require "./mock/factories/api_student" describe Student::InfoMappers::Fregata do - subject(:attributes) { described_class.new(data).attributes } + subject(:attributes) { described_class.new(data, "001").attributes } let!(:fixture) { Rails.root.join("mock/data/fregata-students.json").read } let(:data) { JSON.parse(fixture).first } @@ -25,4 +25,12 @@ it { is_expected.to include({ birthplace_country_insee_code: "99100" }) } it { is_expected.to include({ biological_sex: 1 }) } end + + describe "schooling attributes" do + subject(:attributes) { described_class.new(data, "007").schooling_attributes } + + let(:data) { build(:fregata_student, :apprentice) } + + it { is_expected.to include({ status: :apprentice }) } + end end diff --git a/spec/models/student/info_mappers/sygne_spec.rb b/spec/models/student/info_mappers/sygne_spec.rb index 8044de693..eb9840dcf 100644 --- a/spec/models/student/info_mappers/sygne_spec.rb +++ b/spec/models/student/info_mappers/sygne_spec.rb @@ -19,7 +19,7 @@ let(:birthplace_country) { data["inseePaysNaissance"] } describe "mapper" do - subject { described_class.new(data).attributes } + subject { described_class.new(data, "0").attributes } it { is_expected.to include(address_line1: line_one) } it { is_expected.to include(address_line2: line_two) } diff --git a/spec/models/student/mappers/fregata_spec.rb b/spec/models/student/mappers/fregata_spec.rb index 034518663..64b0dc670 100644 --- a/spec/models/student/mappers/fregata_spec.rb +++ b/spec/models/student/mappers/fregata_spec.rb @@ -66,6 +66,16 @@ end end + context "when the student is an apprentice" do + let(:data) { build_list(:fregata_student, 1, :apprentice) } + + it "updates the schooling status" do + mapper.new(data, uai).parse! + + expect(Schooling.last).to be_apprentice + end + end + context "when there are multiple entries for the same student" do subject(:student) { create(:student) } From bc9d0beabb10e4600c9d958ca3de2b623259b7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Wed, 3 Apr 2024 12:27:55 +0200 Subject: [PATCH 06/14] asp: guard the payment request against apprentice schoolings --- .../asp/payment_request_state_machine.rb | 6 +++- .../payment_requests_steps.rb | 1 + features/step_definitions/student_steps.rb | 12 ++++++-- spec/factories/asp/payment_requests.rb | 10 +++++-- spec/factories/schoolings.rb | 5 ++++ .../fetch_student_information_job_spec.rb | 2 +- .../asp/payment_request_state_machine_spec.rb | 30 ++++++++++++++++--- 7 files changed, 54 insertions(+), 12 deletions(-) diff --git a/app/models/asp/payment_request_state_machine.rb b/app/models/asp/payment_request_state_machine.rb index a5e401d56..d54f83f44 100644 --- a/app/models/asp/payment_request_state_machine.rb +++ b/app/models/asp/payment_request_state_machine.rb @@ -37,6 +37,10 @@ class PaymentRequestStateMachine ASP::StudentFileEligibilityChecker.new(request.student).ready? end + guard_transition(to: :ready) do |request| + request.schooling.student? + end + guard_transition(to: :ready) do |request| !request.student.adult_without_personal_rib? end @@ -46,7 +50,7 @@ class PaymentRequestStateMachine end guard_transition(to: :ready) do |request| - request.pfmp.schooling.attributive_decision.attached? + request.schooling.attributive_decision.attached? end guard_transition(from: :ready, to: :sent) do |request| diff --git a/features/step_definitions/payment_requests_steps.rb b/features/step_definitions/payment_requests_steps.rb index b0ffdcd38..2dfba56cf 100644 --- a/features/step_definitions/payment_requests_steps.rb +++ b/features/step_definitions/payment_requests_steps.rb @@ -5,6 +5,7 @@ ) do |name, classe| steps %( Quand les informations personnelles ont été récupérées pour l'élève "#{name}" + Et que l'élève "#{name}" a bien le statut étudiant Et que je renseigne les coordonnées bancaires de l'élève "#{name}" de la classe "#{classe}" Et que l'élève "#{name}" a une adresse en France et son propre RIB Et que je génère les décisions d'attribution de mon établissement diff --git a/features/step_definitions/student_steps.rb b/features/step_definitions/student_steps.rb index f34396523..ed70eb29d 100644 --- a/features/step_definitions/student_steps.rb +++ b/features/step_definitions/student_steps.rb @@ -11,9 +11,7 @@ end Sachantque("les informations personnelles ont été récupérées pour l'élève {string}") do |name| - first_name, last_name = name.split - - student = Student.find_by(first_name: first_name, last_name: last_name) + student = find_student_by_full_name(name) FetchStudentInformationJob.perform_now(student.current_schooling) end @@ -48,3 +46,11 @@ schooling.rattach_attributive_decision!(StringIO.new("hello")) end end + +# FIXME: we should mock the API step instead and have the correct +# schooling + status returned in the data. +Quand("l'élève {string} a bien le statut étudiant") do |name| + student = find_student_by_full_name(name) + + student.current_schooling.update!(status: :student) +end diff --git a/spec/factories/asp/payment_requests.rb b/spec/factories/asp/payment_requests.rb index 5e999da21..e11b211be 100644 --- a/spec/factories/asp/payment_requests.rb +++ b/spec/factories/asp/payment_requests.rb @@ -8,17 +8,21 @@ trait :pending - trait :ready do + trait :sendable do after(:create) do |req| student = create(:student, :with_all_asp_info, :underage) schooling = create(:schooling, :with_attributive_decision, student: student) req.pfmp.update!(schooling: schooling) - - req.mark_ready! end end + trait :ready do + sendable + + after(:create, &:mark_ready!) + end + trait :incomplete do after(:create, &:mark_incomplete!) end diff --git a/spec/factories/schoolings.rb b/spec/factories/schoolings.rb index ef4c936f5..1fe3609e5 100644 --- a/spec/factories/schoolings.rb +++ b/spec/factories/schoolings.rb @@ -5,6 +5,7 @@ student classe start_date { "2023-08-26" } + status { :student } trait :with_attributive_decision do after(:create) do |schooling| @@ -14,6 +15,10 @@ end end + trait :apprentice do + status { :apprentice } + end + trait :closed do end_date { Date.yesterday } end diff --git a/spec/jobs/fetch_student_information_job_spec.rb b/spec/jobs/fetch_student_information_job_spec.rb index bd28189b6..5655ad90e 100644 --- a/spec/jobs/fetch_student_information_job_spec.rb +++ b/spec/jobs/fetch_student_information_job_spec.rb @@ -44,7 +44,7 @@ it "updates the schooling's status code" do expect { described_class.new(schooling).perform_now } - .to change { schooling.reload.status }.from(nil).to(status.to_s) + .to change { schooling.reload.status }.to(status.to_s) end end diff --git a/spec/models/asp/payment_request_state_machine_spec.rb b/spec/models/asp/payment_request_state_machine_spec.rb index 91e4ee4f4..d20dbc527 100644 --- a/spec/models/asp/payment_request_state_machine_spec.rb +++ b/spec/models/asp/payment_request_state_machine_spec.rb @@ -10,6 +10,14 @@ it { is_expected.to be_in_state :pending } describe "mark_ready!" do + let(:asp_payment_request) { create(:asp_payment_request, :sendable) } + + context "with the default factory" do + it "can transition properly" do + expect { asp_payment_request.mark_ready! }.not_to raise_error + end + end + context "when the request is incomplete" do let(:asp_payment_request) { create(:asp_payment_request, :incomplete) } @@ -18,6 +26,22 @@ end end + context "when the schooling status is unknown" do + before { asp_payment_request.schooling.update!(status: nil) } + + it "blocks the transition" do + expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError + end + end + + context "when the schooling is for an apprentice" do + before { asp_payment_request.schooling.update!(status: :apprentice) } + + it "blocks the transition" do + expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError + end + end + context "when the request is missing information" do before { student.rib&.destroy } @@ -36,10 +60,8 @@ context "when the request belongs to a student over 18 with an external rib" do before do - student.update!( - rib: create(:rib, student: student, personal: false), - birthdate: 20.years.ago - ) + student.update!(birthdate: 20.years.ago) + student.rib.update!(personal: false) end it "blocks the transition" do From c420c691222736c9d2a8607512f9d3970219d6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Wed, 3 Apr 2024 13:53:04 +0200 Subject: [PATCH 07/14] features: remove duplication we cover pretty much the same thing in paiements.feature --- features/paiements.feature | 1 + features/retour_des_paiments.feature | 53 ---------------------------- 2 files changed, 1 insertion(+), 53 deletions(-) delete mode 100644 features/retour_des_paiments.feature diff --git a/features/paiements.feature b/features/paiements.feature index 04f3bf4d7..4151a6296 100644 --- a/features/paiements.feature +++ b/features/paiements.feature @@ -44,6 +44,7 @@ Fonctionnalité: Gestion des paiements Sachant que la dernière PFMP de "Marie Curie" en classe de "A1" a une requête de paiement rejetée Quand je consulte la dernière PFMP Alors je peux voir une demande de paiement "rejetée" de 90 euros + Et la page contient "La demande a été rejetée : mauvais RIB" Et je peux changer le nombre de jours de la PFMP à 8 Scénario: une PFMP avec une requête de paiement liquidé ne peut être modifiée diff --git a/features/retour_des_paiments.feature b/features/retour_des_paiments.feature deleted file mode 100644 index b58a27b7d..000000000 --- a/features/retour_des_paiments.feature +++ /dev/null @@ -1,53 +0,0 @@ -# language: fr -Fonctionnalité: Gestion des retours de l'ASP - Contexte: - Sachant que je suis un personnel MENJ directeur de l'établissement "DINUM" - Et que l'API SYGNE renvoie 10 élèves dans une classe "A1" dont "Marie Curie" pour l'établissement "DINUM" - Et que je me connecte en tant que personnel MENJ - Et que toutes les tâches de fond sont terminées - Et que les informations personnelles ont été récupérées pour l'élève "Marie Curie" - Et que je passe l'écran d'accueil - Et que je génère les décisions d'attribution de mon établissement - Et que l'élève "Marie Curie" a déjà des coordonnées bancaires - Et que l'élève "Marie Curie" a une adresse en France et son propre RIB - Et que je consulte le profil de "Marie Curie" dans la classe de "A1" - Et que je renseigne et valide une PFMP de 3 jours - Et que les tâches de préparation et d'envoi des paiements sont passées - Quand je consulte le profil de "Marie Curie" dans la classe de "A1" - - Scénario: Il n'y a pas de fichiers sur le serveur de l'ASP - Sachant qu'il n'y a pas de fichiers sur le serveur de l'ASP - Et que la tâche de lecture des paiements démarre - Et que toutes les tâches de fond sont terminées - Quand je consulte la dernière PFMP - Alors je peux voir une demande de paiement "En traitement" - - Scénario: L'individu n'a pas pu être intégré sur le serveur de l'ASP - Sachant que l'ASP a rejetté le dossier de "Marie Curie" avec un motif de "mauvais code postal" - Quand la tâche de lecture des paiements démarre - Et que toutes les tâches de fond sont terminées - Et que je consulte la dernière PFMP - Alors la page contient "Demande rejetée" - Et la page contient "mauvais code postal" - - Scénario: L'individu a été intégré sur le serveur de l'ASP - Sachant que l'ASP a accepté le dossier de "Marie Curie" - Quand la tâche de lecture des paiements est passée - Et que je consulte la dernière PFMP - Alors la page contient "La demande a été intégrée" - - Scénario: Le paiement a été liquidé sur le serveur de l'ASP - Sachant que l'ASP a accepté le dossier de "Marie Curie" - Quand la tâche de lecture des paiements est passée - Et que l'ASP a liquidé le paiement de "Marie Curie" - Et que la tâche de lecture des paiements est passée - Et que je consulte la dernière PFMP - Alors la page contient "Paiement envoyé" - - Scénario: Le paiement n'a pas pu être liquider sur le serveur de l'ASP - Sachant que l'ASP a accepté le dossier de "Marie Curie" - Quand la tâche de lecture des paiements est passée - Et que l'ASP n'a pas pu liquider le paiement de "Marie Curie" - Et que la tâche de lecture des paiements est passée - Et que je consulte la dernière PFMP - Alors la page contient "Paiement échoué" From 9cf96485cf0a64dc6ce709ff7c7f2c579d04abd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Wed, 3 Apr 2024 14:38:15 +0200 Subject: [PATCH 08/14] api: store when a student is suddenly lost on the API Sometimes the APIs stop returning data for a certain INE. This usually means that the student has changed INE (which happens more frequently than we thought), and means we end up with duplicated data that we cannot delete or ignore. Store the result on the student table for now, and we can deal with how to deal with them later. --- app/jobs/fetch_student_information_job.rb | 2 ++ .../20240403123520_add_lost_attribute_to_students.rb | 7 +++++++ db/schema.rb | 3 ++- spec/jobs/fetch_student_information_job_spec.rb | 12 ++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20240403123520_add_lost_attribute_to_students.rb diff --git a/app/jobs/fetch_student_information_job.rb b/app/jobs/fetch_student_information_job.rb index 9399eea0d..9074468fc 100644 --- a/app/jobs/fetch_student_information_job.rb +++ b/app/jobs/fetch_student_information_job.rb @@ -17,6 +17,8 @@ def perform(schooling) update_student!(schooling, mapper) update_schooling!(mapper) end + rescue Faraday::ResourceNotFound + schooling.student.update!(lost: true) end def update_student!(schooling, mapper) diff --git a/db/migrate/20240403123520_add_lost_attribute_to_students.rb b/db/migrate/20240403123520_add_lost_attribute_to_students.rb new file mode 100644 index 000000000..c4d535a2d --- /dev/null +++ b/db/migrate/20240403123520_add_lost_attribute_to_students.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddLostAttributeToStudents < ActiveRecord::Migration[7.1] + def change + add_column :students, :lost, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 8bd76517e..105a3c8c9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_04_02_160524) do +ActiveRecord::Schema[7.1].define(version: 2024_04_03_123520) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -230,6 +230,7 @@ t.string "birthplace_country_insee_code" t.integer "biological_sex", default: 0 t.string "asp_individu_id" + t.boolean "lost", default: false, null: false t.index ["asp_file_reference"], name: "index_students_on_asp_file_reference", unique: true t.index ["ine"], name: "index_students_on_ine", unique: true end diff --git a/spec/jobs/fetch_student_information_job_spec.rb b/spec/jobs/fetch_student_information_job_spec.rb index 5655ad90e..42423f008 100644 --- a/spec/jobs/fetch_student_information_job_spec.rb +++ b/spec/jobs/fetch_student_information_job_spec.rb @@ -65,6 +65,18 @@ include_examples "updates the schooling status", "sygne_student_info" do let(:payload) { student_data } end + + context "when the API responds with a 404" do + before do + WebMock + .stub_request(:get, %r{#{ENV.fetch('APLYPRO_SYGNE_URL')}eleves/#{student.ine}}) + .to_return status: 404 + end + + it "stores it on the student" do + expect { described_class.perform_now(schooling) }.to change(student, :lost).from(false).to(true) + end + end end context "when the student is from FREGATA" do From 92f48c17c44fbbfe0f29235dd29f9972b20957d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Wed, 3 Apr 2024 16:14:29 +0200 Subject: [PATCH 09/14] rib: add some convenience for reused RIBs --- app/models/rib.rb | 8 ++++++++ spec/models/rib_spec.rb | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/models/rib.rb b/app/models/rib.rb index cb6c7a126..3ee2a0e1f 100644 --- a/app/models/rib.rb +++ b/app/models/rib.rb @@ -36,4 +36,12 @@ def active? def inactive? !active? end + + def reused? + siblings.any? + end + + def siblings + Rib.where(iban: iban).excluding(self) + end end diff --git a/spec/models/rib_spec.rb b/spec/models/rib_spec.rb index bf1158f97..fa44fd59f 100644 --- a/spec/models/rib_spec.rb +++ b/spec/models/rib_spec.rb @@ -47,4 +47,20 @@ end end end + + describe "reused?" do + context "without other ribs" do + it "is false" do + expect(rib).not_to be_reused + end + end + + context "with multiple RIBS with the same IBAN" do + before { create(:rib, iban: rib.iban) } + + it "returns true" do + expect(rib).to be_reused + end + end + end end From 0eb5bfe76839e943288d218842acea21d522f5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Wed, 3 Apr 2024 18:13:35 +0200 Subject: [PATCH 10/14] payment requests: block if the student is a lost record --- app/models/asp/payment_request_state_machine.rb | 4 ++++ app/models/student.rb | 1 + spec/models/asp/payment_request_state_machine_spec.rb | 8 ++++++++ 3 files changed, 13 insertions(+) diff --git a/app/models/asp/payment_request_state_machine.rb b/app/models/asp/payment_request_state_machine.rb index d54f83f44..989559727 100644 --- a/app/models/asp/payment_request_state_machine.rb +++ b/app/models/asp/payment_request_state_machine.rb @@ -41,6 +41,10 @@ class PaymentRequestStateMachine request.schooling.student? end + guard_transition(to: :ready) do |request| + !request.student.lost + end + guard_transition(to: :ready) do |request| !request.student.adult_without_personal_rib? end diff --git a/app/models/student.rb b/app/models/student.rb index acd88223b..e59ed09c9 100644 --- a/app/models/student.rb +++ b/app/models/student.rb @@ -33,6 +33,7 @@ class Student < ApplicationRecord scope :asp_ready, lambda { where(biological_sex: [1, 2]) .where.not(address_postal_code: nil) + .where.not(lost: true) .where.not(address_country_code: %w[995 990] + [nil]) .where.not(birthplace_country_insee_code: %w[995 990] + [nil]) .where.not( diff --git a/spec/models/asp/payment_request_state_machine_spec.rb b/spec/models/asp/payment_request_state_machine_spec.rb index d20dbc527..07c35ff87 100644 --- a/spec/models/asp/payment_request_state_machine_spec.rb +++ b/spec/models/asp/payment_request_state_machine_spec.rb @@ -42,6 +42,14 @@ end end + context "when the student is a lost record" do + before { asp_payment_request.student.update!(lost: true) } + + it "blocks the transition" do + expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError + end + end + context "when the request is missing information" do before { student.rib&.destroy } From 3c2d9e15e32508abcb86c74a5c0230fd1b8f985e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Wed, 3 Apr 2024 18:15:48 +0200 Subject: [PATCH 11/14] payment requests: block if the student's rib is reused --- app/models/asp/payment_request_state_machine.rb | 4 ++++ spec/models/asp/payment_request_state_machine_spec.rb | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/app/models/asp/payment_request_state_machine.rb b/app/models/asp/payment_request_state_machine.rb index 989559727..4a810dab5 100644 --- a/app/models/asp/payment_request_state_machine.rb +++ b/app/models/asp/payment_request_state_machine.rb @@ -41,6 +41,10 @@ class PaymentRequestStateMachine request.schooling.student? end + guard_transition(to: :ready) do |request| + !request.student.rib.reused? + end + guard_transition(to: :ready) do |request| !request.student.lost end diff --git a/spec/models/asp/payment_request_state_machine_spec.rb b/spec/models/asp/payment_request_state_machine_spec.rb index 07c35ff87..606aa641a 100644 --- a/spec/models/asp/payment_request_state_machine_spec.rb +++ b/spec/models/asp/payment_request_state_machine_spec.rb @@ -50,6 +50,14 @@ end end + context "when the RIB has been reused somewhere else" do + before { create(:rib, iban: asp_payment_request.student.rib.iban) } + + it "blocks the transition" do + expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError + end + end + context "when the request is missing information" do before { student.rib&.destroy } From 2c5e1fa7d71d122ca6c188b6ab71e1916f3f2a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Thu, 4 Apr 2024 08:13:28 +0200 Subject: [PATCH 12/14] payment requests: block if PFMP or RIBs instances are not valid This means including all the model validation like correct dates and SEPA-ribs, etc. --- .../asp/payment_request_state_machine.rb | 8 +++ spec/factories/ribs.rb | 4 ++ .../asp/payment_request_state_machine_spec.rb | 52 ++++++++++--------- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/app/models/asp/payment_request_state_machine.rb b/app/models/asp/payment_request_state_machine.rb index 4a810dab5..a0a83f1be 100644 --- a/app/models/asp/payment_request_state_machine.rb +++ b/app/models/asp/payment_request_state_machine.rb @@ -45,6 +45,14 @@ class PaymentRequestStateMachine !request.student.rib.reused? end + guard_transition(to: :ready) do |request| + request.student.rib.valid? + end + + guard_transition(to: :ready) do |request| + request.pfmp.valid? + end + guard_transition(to: :ready) do |request| !request.student.lost end diff --git a/spec/factories/ribs.rb b/spec/factories/ribs.rb index 66efca41b..8abf39b3f 100644 --- a/spec/factories/ribs.rb +++ b/spec/factories/ribs.rb @@ -8,5 +8,9 @@ archived_at { nil } name { Faker::Name.name } personal { Faker::Boolean.boolean } + + trait :outside_sepa do + iban { Faker::Bank.iban(country_code: "sa") } + end end end diff --git a/spec/models/asp/payment_request_state_machine_spec.rb b/spec/models/asp/payment_request_state_machine_spec.rb index 606aa641a..759c73e6e 100644 --- a/spec/models/asp/payment_request_state_machine_spec.rb +++ b/spec/models/asp/payment_request_state_machine_spec.rb @@ -9,6 +9,12 @@ it { is_expected.to be_in_state :pending } + shared_examples "a blocked request" do + it "cannot transition to ready" do + expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError + end + end + describe "mark_ready!" do let(:asp_payment_request) { create(:asp_payment_request, :sendable) } @@ -29,49 +35,51 @@ context "when the schooling status is unknown" do before { asp_payment_request.schooling.update!(status: nil) } - it "blocks the transition" do - expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError - end + it_behaves_like "a blocked request" end context "when the schooling is for an apprentice" do before { asp_payment_request.schooling.update!(status: :apprentice) } - it "blocks the transition" do - expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError - end + it_behaves_like "a blocked request" end context "when the student is a lost record" do before { asp_payment_request.student.update!(lost: true) } - it "blocks the transition" do - expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError - end + it_behaves_like "a blocked request" end context "when the RIB has been reused somewhere else" do before { create(:rib, iban: asp_payment_request.student.rib.iban) } - it "blocks the transition" do - expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError - end + it_behaves_like "a blocked request" end + # rubocop:disable Rails/SkipsModelValidations + context "when the PFMP is not valid" do + before { asp_payment_request.pfmp.update_column(:start_date, Date.new(2002, 1, 1)) } + + it_behaves_like "a blocked request" + end + + context "when the rib is not valid" do + before { asp_payment_request.student.rib.update_columns(attributes_for(:rib, :outside_sepa)) } + + it_behaves_like "a blocked request" + end + # rubocop:enable Rails/SkipsModelValidations + context "when the request is missing information" do before { student.rib&.destroy } - it "blocks the transition" do - expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError - end + it_behaves_like "a blocked request" end context "when the PFMP is zero-amount" do before { asp_payment_request.pfmp.update!(amount: 0) } - it "raises an error" do - expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError - end + it_behaves_like "a blocked request" end context "when the request belongs to a student over 18 with an external rib" do @@ -80,9 +88,7 @@ student.rib.update!(personal: false) end - it "blocks the transition" do - expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError - end + it_behaves_like "a blocked request" end context "when the attributive decision has not been attached" do @@ -90,9 +96,7 @@ asp_payment_request.pfmp.schooling.attributive_decision.detach end - it "blocks the transition" do - expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError - end + it_behaves_like "a blocked request" end end From cf261a6dec1dbc3167083936ee442e324c85b685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Thu, 4 Apr 2024 09:20:54 +0200 Subject: [PATCH 13/14] payment requests: block if there are duplicated validated PFMPs --- .../asp/payment_request_state_machine.rb | 6 ++++ app/models/pfmp.rb | 6 ++++ .../asp/payment_request_state_machine_spec.rb | 30 +++++++++++++++++-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/app/models/asp/payment_request_state_machine.rb b/app/models/asp/payment_request_state_machine.rb index a0a83f1be..060fef270 100644 --- a/app/models/asp/payment_request_state_machine.rb +++ b/app/models/asp/payment_request_state_machine.rb @@ -69,6 +69,12 @@ class PaymentRequestStateMachine request.schooling.attributive_decision.attached? end + guard_transition(to: :ready) do |request| + request.pfmp.duplicates.none? do |pfmp| + pfmp.in_state?(:validated) + end + end + guard_transition(from: :ready, to: :sent) do |request| request.asp_request.present? end diff --git a/app/models/pfmp.rb b/app/models/pfmp.rb index 571133ce8..6c1d43252 100644 --- a/app/models/pfmp.rb +++ b/app/models/pfmp.rb @@ -112,4 +112,10 @@ def can_be_modified? def payment_due? day_count.present? end + + def duplicates + student.pfmps.excluding(self).select do |other| + other.start_date == start_date && other.end_date == end_date + end + end end diff --git a/spec/models/asp/payment_request_state_machine_spec.rb b/spec/models/asp/payment_request_state_machine_spec.rb index 759c73e6e..2313d1f2a 100644 --- a/spec/models/asp/payment_request_state_machine_spec.rb +++ b/spec/models/asp/payment_request_state_machine_spec.rb @@ -92,12 +92,36 @@ end context "when the attributive decision has not been attached" do - before do - asp_payment_request.pfmp.schooling.attributive_decision.detach - end + before { asp_payment_request.pfmp.schooling.attributive_decision.detach } it_behaves_like "a blocked request" end + + context "when there is another duplicated PFMP" do + let(:duplicate) do + pfmp = asp_payment_request.pfmp + + create( + :pfmp, + schooling: pfmp.schooling, + start_date: pfmp.start_date, + end_date: pfmp.end_date, + day_count: pfmp.day_count + ) + end + + context "when it is validated" do + before { duplicate.validate! } + + it_behaves_like "a blocked request" + end + + context "when it's not validated" do + it "allows the transition" do + expect { asp_payment_request.mark_ready! }.not_to raise_error + end + end + end end describe "mark_as_sent!" do From dc94cc64ff857f5a7862e2bcf15b84d9ba14ab36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Maniaci?= Date: Wed, 3 Apr 2024 18:11:53 +0200 Subject: [PATCH 14/14] release: 1.13 --- config/initializers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/version.rb b/config/initializers/version.rb index af9b4bc53..9911b865a 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Aplypro - VERSION = "1.12.1" + VERSION = "1.13" end