From 8ca7a349eaa8d8a289f20ab849debf12abb2e557 Mon Sep 17 00:00:00 2001 From: pskl Date: Thu, 6 Feb 2025 18:25:02 +0100 Subject: [PATCH] Add StudentMerger service object to merge students with double INE (#1375) --- app/services/student_merger.rb | 64 +++++++++++++++++++ app/views/pfmps/_pfmp_student_table.html.haml | 2 +- config/initializers/version.rb | 2 +- spec/services/student_merger_spec.rb | 56 ++++++++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 app/services/student_merger.rb create mode 100644 spec/services/student_merger_spec.rb diff --git a/app/services/student_merger.rb b/app/services/student_merger.rb new file mode 100644 index 000000000..8bff6dec8 --- /dev/null +++ b/app/services/student_merger.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# Service to safely merge duplicate student records (double INE) while preserving data integrity +class StudentMerger + class StudentMergerError < StandardError; end + class InvalidStudentsArrayError < StudentMergerError; end + class ActiveSchoolingError < StudentMergerError; end + + attr_reader :students + + def initialize(students) + @students = students + end + + def merge! + ActiveRecord::Base.transaction do + validate_students! + determine_target_and_merge_student! + validate_schoolings! + transfer_asp_individu_id! + transfer_schoolings! + + @student_to_merge.destroy! + + Rails.logger.info( + "Merged student #{@student_to_merge.id} into #{@target_student.id}" + ) + true + end + end + + private + + def validate_students! + raise InvalidStudentsArrayError unless students.is_a?(Array) && students.length == 2 + end + + def determine_target_and_merge_student! + sorted_students = students.sort_by do |student| + latest_payment_request = student.pfmps.map(&:latest_payment_request).compact.max_by(&:created_at) + latest_payment_request&.created_at || Time.zone.at(0) + end + + @target_student = sorted_students.last + @student_to_merge = sorted_students.first + end + + def validate_schoolings! + return unless @student_to_merge.schoolings.exists?(end_date: nil) + + raise ActiveSchoolingError, "Cannot merge students with active schoolings" + end + + def transfer_asp_individu_id! + id = @student_to_merge.asp_individu_id || @target_student.asp_individu_id + + @student_to_merge.update!(asp_individu_id: nil) + @target_student.update!(asp_individu_id: id) + end + + def transfer_schoolings! + @student_to_merge.schoolings.update!(student_id: @target_student.id) + end +end diff --git a/app/views/pfmps/_pfmp_student_table.html.haml b/app/views/pfmps/_pfmp_student_table.html.haml index fa8c8a550..5df53b5c6 100644 --- a/app/views/pfmps/_pfmp_student_table.html.haml +++ b/app/views/pfmps/_pfmp_student_table.html.haml @@ -1,5 +1,5 @@ +%h2.fr-h4 Scolarités - schoolings.sort_by{ |schooling| schooling.classe.school_year.start_year}.reverse.each do |schooling| - %h2 Scolarités - pfmps = schooling.pfmps - classe = schooling.classe - school_year = classe.school_year diff --git a/config/initializers/version.rb b/config/initializers/version.rb index 39a78d033..0bba69f2f 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Aplypro - VERSION = "2.1.5" + VERSION = "2.2.0" end diff --git a/spec/services/student_merger_spec.rb b/spec/services/student_merger_spec.rb new file mode 100644 index 000000000..57e7f8901 --- /dev/null +++ b/spec/services/student_merger_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe StudentMerger do + describe "#merge!" do + let(:source_student) { create(:schooling, :closed).student } + let(:target_student) { create(:schooling, :closed).student } + let(:students) { [source_student, target_student] } + let(:merger) { described_class.new(students) } + + context "with invalid inputs" do + it "raises error when not given exactly two students" do + merger = described_class.new([source_student]) + expect { merger.merge! }.to raise_error(StudentMerger::InvalidStudentsArrayError) + end + end + + context "when merging students with payment requests" do # rubocop:disable RSpec/MultipleMemoizedHelpers + let(:older_payment_request) { create(:asp_payment_request, created_at: 1.month.ago) } + let(:newer_payment_request) { create(:asp_payment_request, created_at: 1.day.ago) } + let(:source_student) { older_payment_request.student } + let(:target_student) { newer_payment_request.student } + + before do + source_student.current_schooling.update!(end_date: 2.days.ago) + end + + it "keeps the student with the most recent payment request" do + merger.merge! + expect(Student.exists?(source_student.id)).to be false + end + + it "raises error when trying to transfer active schoolings" do + create(:schooling, student: source_student) + expect { merger.merge! }.to raise_error(StudentMerger::ActiveSchoolingError) + end + end + + context "when transferring asp_individu_id" do + let(:source_student) { create(:schooling, :closed).student } + let(:target_student) { create(:schooling, :closed).student } + + before do + source_student.update!(asp_individu_id: "123ABC") + target_student.update!(asp_individu_id: nil) + end + + it "transfers asp_individu_id from source to target student" do + merger.merge! + + expect(target_student.reload.asp_individu_id).to eq("123ABC") + end + end + end +end