diff --git a/app/services/interactors/change_effort_event.rb b/app/services/interactors/change_effort_event.rb index a9ea27c8b..1fcffcd58 100644 --- a/app/services/interactors/change_effort_event.rb +++ b/app/services/interactors/change_effort_event.rb @@ -17,11 +17,13 @@ def initialize(args) end def perform! - existing_start_time = effort.start_time - effort.event = new_event - effort.start_time = existing_start_time - split_times.each { |st| st.split = splits_by_distance[st.distance_from_start] } - save_changes + unless errors.present? + existing_start_time = effort.start_time + effort.event = new_event + effort.start_time = existing_start_time + split_times.each { |st| st.split = splits_by_distance[st.distance_from_start] } + save_changes + end Interactors::Response.new(errors, response_message) end @@ -58,9 +60,9 @@ def response_message end def verify_compatibility - raise ArgumentError, "#{effort} cannot be assigned to #{new_event} because distances do not coincide" unless split_times.all? { |st| distances.include?(st.distance_from_start) } - raise ArgumentError, "#{effort} cannot be assigned to #{new_event} because sub splits do not coincide" unless split_times.all? { |st| splits_by_distance[st.distance_from_start].sub_split_bitkeys.include?(st.bitkey) } - raise ArgumentError, "#{effort} cannot be assigned to #{new_event} because laps exceed maximum required" unless split_times.all? { |st| maximum_lap >= st.lap } + errors << distance_mismatch_error(effort, new_event) and return unless split_times.all? { |st| distances.include?(st.distance_from_start) } + errors << sub_split_mismatch_error(effort, new_event) and return unless split_times.all? { |st| splits_by_distance[st.distance_from_start].sub_split_bitkeys.include?(st.bitkey) } + errors << lap_mismatch_error(effort, new_event) unless split_times.all? { |st| maximum_lap >= st.lap } end end end diff --git a/app/services/interactors/change_event_course.rb b/app/services/interactors/change_event_course.rb new file mode 100644 index 000000000..e0ceae82f --- /dev/null +++ b/app/services/interactors/change_event_course.rb @@ -0,0 +1,65 @@ +module Interactors + class ChangeEventCourse + include Interactors::Errors + + def self.perform!(args) + new(args).perform! + end + + def initialize(args) + ArgsValidator.validate(params: args, required: [:event, :new_course], exclusive: [:event, :new_course], class: self.class) + @event = args[:event] + @new_course = args[:new_course] + @old_course ||= event.course + @split_times ||= event.split_times + @errors = [] + verify_compatibility + end + + def perform! + unless errors.present? + event.course = new_course + event.splits = new_course.splits + split_times.each { |st| st.split = splits_by_distance[st.distance_from_start] } + save_changes + end + Interactors::Response.new(errors, response_message) + end + + private + + attr_reader :event, :new_course, :split_times, :errors + + def save_changes + ActiveRecord::Base.transaction do + event.save(validate: false) if event.changed? + split_times.each { |st| save_split_time(st) } + errors << resource_error_object(event) unless event.valid? + raise ActiveRecord::Rollback if errors.present? + end + end + + def save_split_time(st) + if st.changed? + errors << resource_error_object(split_time) unless st.save + end + end + + def distances + @distances ||= splits_by_distance.keys.to_set + end + + def splits_by_distance + @splits_by_distance ||= new_course.splits.index_by(&:distance_from_start) + end + + def response_message + errors.present? ? "#{event.name} could not be changed to #{new_course.name}. " : "#{event.name} was changed to #{new_course.name}. " + end + + def verify_compatibility + errors << distance_mismatch_error(event, new_course) and return unless split_times.all? { |st| distances.include?(st.distance_from_start) } + errors << sub_split_mismatch_error(event, new_course) unless split_times.all? { |st| splits_by_distance[st.distance_from_start].sub_split_bitkeys.include?(st.bitkey) } + end + end +end diff --git a/app/services/interactors/errors.rb b/app/services/interactors/errors.rb index ab13d0850..654fe2a2d 100644 --- a/app/services/interactors/errors.rb +++ b/app/services/interactors/errors.rb @@ -10,6 +10,21 @@ def active_record_error(exception) detail: {exception: exception}} end + def distance_mismatch_error(child, new_parent) + {title: 'Distances do not match', + detail: {messages: ["#{child} cannot be assigned to #{new_parent} because distances do not coincide"]}} + end + + def lap_mismatch_error(child, new_parent) + {title: 'Distances do not match', + detail: {messages: ["#{child} cannot be assigned to #{new_parent} because laps exceed maximum required"]}} + end + + def sub_split_mismatch_error(child, new_parent) + {title: 'Distances do not match', + detail: {messages: ["#{child} cannot be assigned to #{new_parent} because sub splits do not coincide"]}} + end + def mismatched_organization_error(old_event_group, new_event_group) {title: 'Event group organizations do not match', detail: {messages: ["The event cannot be updated because #{old_event_group} is organized under #{old_event_group.organization}, but #{new_event_group} is organized under #{new_event_group.organization}"]}} diff --git a/spec/services/interactors/change_effort_event_spec.rb b/spec/services/interactors/change_effort_event_spec.rb index 4edf9ef36..da19b33aa 100644 --- a/spec/services/interactors/change_effort_event_spec.rb +++ b/spec/services/interactors/change_effort_event_spec.rb @@ -7,7 +7,7 @@ let(:effort) { build_stubbed(:effort) } let(:new_event) { build_stubbed(:event) } - it 'initializes when provided with an effort and a new event_id' do + it 'initializes when provided with an effort and a new_event' do expect { subject }.not_to raise_error end @@ -103,20 +103,26 @@ split = new_event.ordered_splits.second split.update(distance_from_start: split.distance_from_start - 1) new_event.reload - expect { subject }.to raise_error(/distances do not coincide/) + response = subject.perform! + expect(response).not_to be_successful + expect(response.errors.first[:detail][:messages]).to include(/distances do not coincide/) end it 'raises an error if sub_splits do not coincide' do split = new_event.ordered_splits.second split.update(sub_split_bitmap: 1) new_event.reload - expect { subject }.to raise_error(/sub splits do not coincide/) + response = subject.perform! + expect(response).not_to be_successful + expect(response.errors.first[:detail][:messages]).to include(/sub splits do not coincide/) end it 'raises an error if laps are out of range' do split_time = effort.ordered_split_times.last split_time.update(lap: 2) - expect { subject }.to raise_error(/laps exceed maximum required/) + response = subject.perform! + expect(response).not_to be_successful + expect(response.errors.first[:detail][:messages]).to include(/laps exceed maximum required/) end end diff --git a/spec/services/interactors/change_event_course_spec.rb b/spec/services/interactors/change_event_course_spec.rb new file mode 100644 index 000000000..26376a7ac --- /dev/null +++ b/spec/services/interactors/change_event_course_spec.rb @@ -0,0 +1,105 @@ +require 'rails_helper' + +RSpec.describe Interactors::ChangeEventCourse do + subject { Interactors::ChangeEventCourse.new(event: event, new_course: new_course) } + + describe '#initialization' do + let(:event) { build_stubbed(:event) } + let(:new_course) { build_stubbed(:course) } + + it 'initializes when provided with an event and a new course_id' do + expect { subject }.not_to raise_error + end + + context 'if no event is provided' do + let(:event) { nil } + + it 'raises an error' do + expect { subject }.to raise_error(/must include event/) + end + end + + context 'if no new_course is provided' do + let(:new_course) { nil } + + it 'raises an error' do + expect { subject }.to raise_error(/must include new_course/) + end + end + end + + describe '#perform!' do + let(:event) { create(:event, course: old_course) } + let!(:old_course) { create(:course, splits: old_splits) } + let(:old_split_1) { create(:start_split) } + let(:old_split_2) { create(:split, distance_from_start: 10000) } + let(:old_split_3) { create(:split, distance_from_start: 20000) } + let(:old_splits) { [old_split_1, old_split_2, old_split_3] } + let!(:efforts) { create_list(:effort, 2, event: event) } + + context 'when the new course has splits with the same distances as the old' do + let(:new_course) { create(:course, splits: new_splits) } + let(:new_split_1) { create(:start_split) } + let(:new_split_2) { create(:split, distance_from_start: old_course.ordered_splits.second.distance_from_start) } + let(:new_split_3) { create(:split, distance_from_start: old_course.ordered_splits.third.distance_from_start) } + let(:new_split_4) { create(:split, distance_from_start: new_split_3.distance_from_start + 10000) } + let(:new_splits) { [new_split_1, new_split_2, new_split_3, new_split_4] } + + before do + FactoryBot.reload + old_course.reload + new_course.reload + event.splits << old_course.splits + create_split_times_for_event + end + + it 'updates the event course_id to the id of the provided course' do + expect(event.course_id).not_to eq(new_course.id) + response = subject.perform! + expect(event.course_id).to eq(new_course.id) + expect(response).to be_successful + expect(response.message).to match(/was changed to/) + end + + it 'changes the split_ids of event split_times to the corresponding split_ids of the new course' do + sub_splits = new_course.sub_splits.first(efforts.first.split_times.size) + efforts.each do |effort| + effort.reload + expect(effort.split_times.map(&:sub_split)).not_to match_array(sub_splits) + end + subject.perform! + efforts.each do |effort| + effort.reload + expect(effort.split_times.map(&:sub_split)).to match_array(sub_splits) + end + end + + it 'returns an unsuccessful response with errors if distances do not coincide' do + split = new_course.ordered_splits.second + split.update(distance_from_start: split.distance_from_start - 1) + new_course.reload + response = subject.perform! + expect(response).not_to be_successful + expect(response.errors.first[:detail][:messages]).to include(/distances do not coincide/) + end + + it 'raises an error if sub_splits do not coincide' do + split = new_course.ordered_splits.second + split.update(sub_split_bitmap: 1) + new_course.reload + response = subject.perform! + expect(response).not_to be_successful + expect(response.errors.first[:detail][:messages]).to include(/sub splits do not coincide/) + end + end + + def create_split_times_for_event + time_points = event.required_time_points + efforts.each do |effort| + time_points.each_with_index do |time_point, i| + create(:split_time, time_point: time_point, effort: effort, time_from_start: i * 1000) + end + end + end + end +end