Skip to content

Commit

Permalink
Add StudentMerger service object to merge students with double INE (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
pskl authored Feb 6, 2025
1 parent 1702620 commit 8ca7a34
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 2 deletions.
64 changes: 64 additions & 0 deletions app/services/student_merger.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion app/views/pfmps/_pfmp_student_table.html.haml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion config/initializers/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Aplypro
VERSION = "2.1.5"
VERSION = "2.2.0"
end
56 changes: 56 additions & 0 deletions spec/services/student_merger_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 8ca7a34

Please sign in to comment.