diff --git a/app/jobs/fetch_student_information_job.rb b/app/jobs/fetch_student_information_job.rb index dfba7ebc6..9074468fc 100644 --- a/app/jobs/fetch_student_information_job.rb +++ b/app/jobs/fetch_student_information_job.rb @@ -12,12 +12,41 @@ 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 + rescue Faraday::ResourceNotFound + schooling.student.update!(lost: true) + 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/asp/payment_request_state_machine.rb b/app/models/asp/payment_request_state_machine.rb index a5e401d56..060fef270 100644 --- a/app/models/asp/payment_request_state_machine.rb +++ b/app/models/asp/payment_request_state_machine.rb @@ -37,6 +37,26 @@ 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.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 + guard_transition(to: :ready) do |request| !request.student.adult_without_personal_rib? end @@ -46,7 +66,13 @@ class PaymentRequestStateMachine end guard_transition(to: :ready) do |request| - request.pfmp.schooling.attributive_decision.attached? + 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| 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/models/pfmp.rb b/app/models/pfmp.rb index 9e68c066e..6c1d43252 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) } @@ -106,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/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/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.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/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/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/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 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/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/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/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 c12279ccc..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_03_21_161647) 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" @@ -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 @@ -203,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 @@ -228,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/features/anciens_eleves.feature b/features/anciens_eleves.feature index b00975de0..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" @@ -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" @@ -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/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..4151a6296 100644 --- a/features/paiements.feature +++ b/features/paiements.feature @@ -8,52 +8,53 @@ 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 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 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/retour_des_paiments.feature b/features/retour_des_paiments.feature deleted file mode 100644 index bd1464dba..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" de 30 euros - - 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é" 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/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/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/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/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/mock b/mock index c03818522..6ad1b6e92 160000 --- a/mock +++ b/mock @@ -1 +1 @@ -Subproject commit c038185228d3aebf1630a1a0be73d31e4c7f7eaa +Subproject commit 6ad1b6e92e2871ed575f590c67ceebb3673bc25f 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/establishments.rb b/spec/factories/establishments.rb index 949c8534e..d700736c0 100644 --- a/spec/factories/establishments.rb +++ b/spec/factories/establishments.rb @@ -16,6 +16,13 @@ students_provider { nil } ministry { "MINISTERE DE L'EDUCATION NATIONALE" } 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" } 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/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_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 diff --git a/spec/jobs/fetch_student_information_job_spec.rb b/spec/jobs/fetch_student_information_job_spec.rb index a3b917ae5..42423f008 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 }.to(status.to_s) + end + end + context "when the student is from SYGNE" do let(:establishment) { create(:establishment, :sygne_provider) } @@ -40,6 +61,22 @@ end include_examples "maps all the extra fields correctly" + + 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 @@ -53,6 +90,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/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" } } diff --git a/spec/models/asp/payment_request_state_machine_spec.rb b/spec/models/asp/payment_request_state_machine_spec.rb index 91e4ee4f4..2313d1f2a 100644 --- a/spec/models/asp/payment_request_state_machine_spec.rb +++ b/spec/models/asp/payment_request_state_machine_spec.rb @@ -9,7 +9,21 @@ 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) } + + 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,42 +32,94 @@ end end + context "when the schooling status is unknown" do + before { asp_payment_request.schooling.update!(status: nil) } + + 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_behaves_like "a blocked request" + end + + context "when the student is a lost record" do + before { asp_payment_request.student.update!(lost: true) } + + 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_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 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 - 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 - before do - asp_payment_request.pfmp.schooling.attributive_decision.detach + 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 - it "blocks the transition" do - expect { asp_payment_request.mark_ready! }.to raise_error Statesman::GuardFailedError + 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 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/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 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 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 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) }