Skip to content

Commit

Permalink
Relax validation on ChangeSchedule service for certain participants
Browse files Browse the repository at this point in the history
We want to allow ECTs and mentors to change schedule/cohort even if they have
billable declarations under certain scenarios.

The participants eligible are identified by
`eligible_to_change_cohort_and_continue_training`.

By default the service will not allow eligible participants to transfer; it
needs to be called explicitly with
`attempt_to_transfer_leaving_billable_declarations` set to `true`. This is to
prevent lead providers performing these changes for now.

Participants that change cohort using this mechanism should be able to change
back to their original cohort providing no billable declarations were created
against the new cohort.
  • Loading branch information
ethax-ross committed May 24, 2024
1 parent c6b3265 commit 95fae5f
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 15 deletions.
19 changes: 16 additions & 3 deletions app/models/participant_profile/ecf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
class ParticipantProfile::ECF < ParticipantProfile
# self.ignored_columns = %i[school_id]

CHANGE_COHORT_AND_CONTINUE_TRAINING_DELTA = 3
POST_TRANSITIONAL_INDUCTION_START_DATE_DEADLINE = ActiveSupport::TimeZone["London"].local(2021, 9, 1).freeze
VALID_EVIDENCE_HELD = %w[training-event-attended self-study-material-completed other].freeze
COURSE_IDENTIFIERS = %w[ecf-mentor ecf-induction].freeze
Expand Down Expand Up @@ -53,14 +54,15 @@ def self.ransackable_associations(_auth_object = nil)
%w[cohort participant_identity school user teacher_profile induction_records]
end

def self.eligible_to_change_cohort_and_continue_training(restrict_to_participant_ids: [])
def self.eligible_to_change_cohort_and_continue_training(in_cohort_start_year:, restrict_to_participant_ids: [])
billable_states = %w[eligible payable paid].freeze

completed_billable_declarations = ParticipantDeclaration.billable.for_declaration(:completed)
completed_billable_declarations = completed_billable_declarations.where(participant_profile_id: restrict_to_participant_ids) if restrict_to_participant_ids.any?

query = joins(:participant_declarations, schedule: :cohort)
.where.not(cohorts: { payments_frozen_at: nil })
.where(cohorts: { start_year: in_cohort_start_year - CHANGE_COHORT_AND_CONTINUE_TRAINING_DELTA })
.where("participant_declarations.state IN (?) AND declaration_type != ?", billable_states, "completed")
.where.not(id: completed_billable_declarations.select(:participant_profile_id))
.distinct
Expand All @@ -70,6 +72,17 @@ def self.eligible_to_change_cohort_and_continue_training(restrict_to_participant
query
end

def eligible_to_change_cohort_back_to_their_payments_frozen_original?(to_cohort_start_year:)
to_cohort = Cohort.find_by_start_year(to_cohort_start_year)

return false unless schedule.cohort.start_year - to_cohort_start_year == CHANGE_COHORT_AND_CONTINUE_TRAINING_DELTA
return false unless cohort_changed_after_payments_frozen?
return false unless to_cohort.payments_frozen?
return false if participant_declarations.billable_or_changeable.where(cohort: schedule.cohort).exists?

true
end

def self.archivable(restrict_to_participant_ids: [])
unbillable_states = %i[ineligible voided submitted].freeze

Expand All @@ -92,8 +105,8 @@ def self.archivable(restrict_to_participant_ids: [])
end

# Instance Methods
def eligible_to_change_cohort_and_continue_training?
self.class.eligible_to_change_cohort_and_continue_training(restrict_to_participant_ids: [id]).exists?
def eligible_to_change_cohort_and_continue_training?(in_cohort_start_year:)
self.class.eligible_to_change_cohort_and_continue_training(in_cohort_start_year:, restrict_to_participant_ids: [id]).exists?
end

def archivable?
Expand Down
4 changes: 2 additions & 2 deletions app/models/participant_profile/ect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def role
"Early career teacher"
end

def self.eligible_to_change_cohort_and_continue_training(restrict_to_participant_ids: [])
super(restrict_to_participant_ids:).where(induction_completion_date: nil)
def self.eligible_to_change_cohort_and_continue_training(in_cohort_start_year:, restrict_to_participant_ids: [])
super(in_cohort_start_year:, restrict_to_participant_ids:).where(induction_completion_date: nil)
end
end
4 changes: 2 additions & 2 deletions app/models/participant_profile/mentor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def role
"Mentor"
end

def self.eligible_to_change_cohort_and_continue_training(restrict_to_participant_ids: [])
super(restrict_to_participant_ids:).where(mentor_completion_date: nil)
def self.eligible_to_change_cohort_and_continue_training(in_cohort_start_year:, restrict_to_participant_ids: [])
super(in_cohort_start_year:, restrict_to_participant_ids:).where(mentor_completion_date: nil)
end
end
22 changes: 22 additions & 0 deletions app/services/change_schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class ChangeSchedule
attribute :course_identifier
attribute :schedule_identifier
attribute :cohort, :integer
attribute :attempt_to_change_cohort_leaving_billable_declarations, :boolean, default: false

delegate :participant_profile_state, to: :participant_profile, allow_nil: true
delegate :lead_provider, to: :cpd_lead_provider, allow_nil: true
Expand Down Expand Up @@ -80,6 +81,19 @@ def cohort

private

def can_change_cohort_leaving_billable_declarations?
return false unless participant_profile.ecf?
return false unless attempt_to_change_cohort_leaving_billable_declarations

return true if participant_profile.eligible_to_change_cohort_and_continue_training?(in_cohort_start_year: cohort.start_year)

participant_profile.eligible_to_change_cohort_back_to_their_payments_frozen_original?(to_cohort_start_year: cohort.start_year)
end

def cohort_start_years_delta
cohort.start_year - participant_profile.schedule.cohort.start_year
end

def user
@user ||= participant_identity&.user
end
Expand Down Expand Up @@ -111,6 +125,12 @@ def update_participant_profile_schedule_references!
end

def update_school_cohort_and_schedule!
changing_cohort = new_schedule.cohort != participant_profile.schedule.cohort

if changing_cohort && can_change_cohort_leaving_billable_declarations?
participant_profile.toggle!(:cohort_changed_after_payments_frozen)
end

participant_profile.update!(school_cohort: target_school_cohort, schedule: new_schedule)
end

Expand Down Expand Up @@ -165,6 +185,7 @@ def not_already_withdrawn
def validate_new_schedule_valid_with_existing_declarations
return if user.blank? || participant_profile.blank?
return unless new_schedule
return if can_change_cohort_leaving_billable_declarations?

applicable_declarations.each do |declaration|
milestone = new_schedule.milestones.find_by!(declaration_type: declaration.declaration_type)
Expand Down Expand Up @@ -194,6 +215,7 @@ def validate_permitted_schedule_for_course

def validate_cannot_change_cohort_ecf
return unless participant_profile&.ecf?
return if can_change_cohort_leaving_billable_declarations?

if applicable_declarations.any? &&
relevant_induction_record &&
Expand Down
123 changes: 121 additions & 2 deletions spec/services/change_schedule_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,89 @@
end
end

RSpec.shared_examples "changing cohort and continuing training" do
%i[eligible payable paid].each do |state|
context "when there are #{state} declarations" do
let(:start_year) { participant_profile.schedule.cohort.start_year + ParticipantProfile::ECF::CHANGE_COHORT_AND_CONTINUE_TRAINING_DELTA }
let(:new_cohort) { create(:cohort, start_year:) }
let!(:new_schedule) { create(:ecf_schedule, cohort: new_cohort, schedule_identifier:) }

before { create(:participant_declaration, participant_profile:, state:, course_identifier:, cpd_lead_provider:) }

context "attempt_to_change_cohort_leaving_billable_declarations is true" do
let(:attempt_to_change_cohort_leaving_billable_declarations) { true }

context "when the participant is not eligible to transfer and continue training" do
it { is_expected.to be_invalid }
end

context "when the participant is eligible to transfer and continue training" do
before { participant_profile.schedule.cohort.update!(payments_frozen_at: Time.zone.now) }

it_behaves_like "changing the schedule of a participant"
it_behaves_like "reversing a change of schedule/cohort"

it { is_expected.to be_valid }
it { expect { service.call }.to change { participant_profile.reload.cohort_changed_after_payments_frozen }.to(true) }
end
end

context "when attempt_to_change_cohort_leaving_billable_declarations is false" do
let(:attempt_to_change_cohort_leaving_billable_declarations) { false }

context "when the participant is eligible to transfer and continue training" do
before { participant_profile.schedule.cohort.update!(payments_frozen_at: Time.zone.now) }

it { is_expected.to be_invalid }
end
end
end
end
end

RSpec.shared_examples "reversing a change of schedule/cohort" do
it "allows a change of schedule to be reversed" do
original_cohort = participant_profile.schedule.cohort

first_change_of_schedule = described_class.new(params)
expect(first_change_of_schedule).to be_valid
expect { first_change_of_schedule.call }.to change { ParticipantProfileSchedule.count }

second_change_of_schedule = described_class.new(params.merge({
cohort: original_cohort.start_year,
}))
expect(second_change_of_schedule).to be_valid
expect { second_change_of_schedule.call }.to change { ParticipantProfileSchedule.count }

expect(participant_profile.reload).not_to be_cohort_changed_after_payments_frozen
end

it "does not allow a change of schedule to be reversed if there are billable declarations in the new cohort" do
original_cohort = participant_profile.schedule.cohort

first_change_of_schedule = described_class.new(params)
expect(first_change_of_schedule).to be_valid
expect { first_change_of_schedule.call }.to change { ParticipantProfileSchedule.count }

second_change_of_schedule = described_class.new(params.merge({
cohort: original_cohort.start_year,
}))

travel_to(Date.new(new_cohort.start_year).end_of_year) do
create(:participant_declaration,
participant_profile: participant_profile.reload,
declaration_type: "retained-1",
state: :payable,
course_identifier:,
cpd_lead_provider:,
cohort: new_cohort)
end

expect(second_change_of_schedule).not_to be_valid
expect(second_change_of_schedule.errors.messages_for(:cohort)).to include("The property '#/cohort' cannot be changed")
end
end

RSpec.shared_examples "validating a participant is not already withdrawn for a change schedule" do
it "is invalid and returns an error message" do
is_expected.to be_invalid
Expand Down Expand Up @@ -122,6 +205,7 @@
participant_id:,
course_identifier:,
schedule_identifier:,
attempt_to_change_cohort_leaving_billable_declarations:,
}
end
let(:participant_identity) { create(:participant_identity) }
Expand All @@ -136,6 +220,7 @@
let(:participant_profile) { create(:ect, lead_provider: cpd_lead_provider.lead_provider, user:) }
let(:schedule_identifier) { "ecf-standard-september" }
let(:course_identifier) { "ecf-induction" }
let(:attempt_to_change_cohort_leaving_billable_declarations) { false }
let!(:schedule) { Finance::Schedule::ECF.find_by(schedule_identifier: "ecf-standard-september") }
let(:new_cohort) { Cohort.previous }
let!(:new_schedule) { create(:ecf_schedule, cohort: new_cohort, schedule_identifier: "ecf-replacement-april") }
Expand All @@ -149,17 +234,20 @@

context "when the cohort is changing" do
let!(:new_schedule) { Finance::Schedule::ECF.find_by(schedule_identifier:, cohort: new_cohort) }
let!(:new_school_cohort) { create(:school_cohort, :cip, :with_induction_programme, cohort: new_cohort, lead_provider: cpd_lead_provider.lead_provider, school: participant_profile.school) }
let!(:new_school_cohort) { create(:school_cohort, :fip, :with_induction_programme, cohort: new_cohort, lead_provider: cpd_lead_provider.lead_provider, school: participant_profile.school) }
let(:params) do
{
cpd_lead_provider:,
participant_id:,
course_identifier:,
schedule_identifier:,
cohort: new_cohort.start_year,
attempt_to_change_cohort_leaving_billable_declarations:,
}
end

it_behaves_like "reversing a change of schedule/cohort"

%i[submitted eligible payable paid].each do |state|
context "when there are #{state} declarations" do
before { create(:participant_declaration, participant_profile:, state:, course_identifier:, cpd_lead_provider:) }
Expand All @@ -174,6 +262,8 @@
end
end

it_behaves_like "changing cohort and continuing training"

context "when there are no submitted/eligible/payable/paid declarations" do
context "when changing to another cohort" do
describe ".call" do
Expand Down Expand Up @@ -298,6 +388,7 @@
participant_id:,
course_identifier:,
schedule_identifier:,
attempt_to_change_cohort_leaving_billable_declarations:,
cohort: new_cohort.start_year,
}
end
Expand Down Expand Up @@ -336,6 +427,7 @@
participant_id:,
course_identifier:,
schedule_identifier: new_schedule.schedule_identifier,
attempt_to_change_cohort_leaving_billable_declarations:,
cohort:,
}
end
Expand Down Expand Up @@ -384,6 +476,7 @@
let!(:participant_profile) { create(:mentor, lead_provider: cpd_lead_provider.lead_provider, user:) }
let(:schedule_identifier) { "ecf-extended-april" }
let(:course_identifier) { "ecf-mentor" }
let(:attempt_to_change_cohort_leaving_billable_declarations) { false }
let(:new_cohort) { Cohort.previous }
let!(:schedule) { create(:ecf_mentor_schedule, schedule_identifier: "ecf-extended-april") }

Expand All @@ -395,18 +488,21 @@
end

context "when the cohort is changing" do
let!(:new_school_cohort) { create(:school_cohort, :cip, :with_induction_programme, cohort: new_cohort, lead_provider: cpd_lead_provider.lead_provider, school: participant_profile.school) }
let!(:new_school_cohort) { create(:school_cohort, :fip, :with_induction_programme, cohort: new_cohort, lead_provider: cpd_lead_provider.lead_provider, school: participant_profile.school) }
let!(:new_schedule) { create(:ecf_mentor_schedule, schedule_identifier:, cohort: new_cohort) }
let(:params) do
{
cpd_lead_provider:,
participant_id:,
course_identifier:,
schedule_identifier:,
attempt_to_change_cohort_leaving_billable_declarations:,
cohort: new_cohort.start_year,
}
end

it_behaves_like "reversing a change of schedule/cohort"

%i[submitted eligible payable paid].each do |state|
context "when there are #{state} declarations" do
before { create(:participant_declaration, participant_profile:, state:, course_identifier:, cpd_lead_provider:) }
Expand All @@ -421,6 +517,8 @@
end
end

it_behaves_like "changing cohort and continuing training"

context "when there are no submitted/eligible/payable/paid declarations" do
context "when changing to another cohort" do
describe ".call" do
Expand Down Expand Up @@ -543,6 +641,7 @@
let(:participant_profile) { create(:npq_participant_profile, npq_lead_provider:, npq_course:, schedule:, user:) }
let(:course_identifier) { npq_course.identifier }
let(:schedule_identifier) { new_schedule.schedule_identifier }
let(:attempt_to_change_cohort_leaving_billable_declarations) { false }
let(:new_cohort) { Cohort.previous }
let(:new_schedule) { Finance::Schedule::NPQ.find_by(cohort: new_cohort, schedule_identifier: "npq-leadership-spring") }
let!(:npq_contract) { create(:npq_contract, :npq_senior_leadership, npq_lead_provider:, npq_course:) }
Expand All @@ -564,9 +663,12 @@
course_identifier:,
schedule_identifier:,
cohort: new_cohort.start_year,
attempt_to_change_cohort_leaving_billable_declarations:,
}
end

it_behaves_like "reversing a change of schedule/cohort"

%i[submitted eligible payable paid].each do |state|
context "when there are #{state} declarations" do
before { create(:participant_declaration, participant_profile:, state:, course_identifier:, cpd_lead_provider:) }
Expand All @@ -583,6 +685,23 @@
end
end

%i[eligible payable paid].each do |state|
context "when there are #{state} declarations" do
let(:new_cohort) { create(:cohort, start_year: participant_profile.schedule.cohort.start_year + ParticipantProfile::ECF::CHANGE_COHORT_AND_CONTINUE_TRAINING_DELTA) }
let!(:new_schedule) { create(:npq_leadership_schedule, cohort: new_cohort) }

before { create(:participant_declaration, participant_profile:, state:, course_identifier:, cpd_lead_provider:) }

context "attempt_to_change_cohort_leaving_billable_declarations is true" do
let(:attempt_to_change_cohort_leaving_billable_declarations) { true }

before { participant_profile.schedule.cohort.update!(payments_frozen_at: Time.zone.now) }

it { is_expected.not_to be_valid }
end
end
end

context "when there are no submitted/eligible/payable/paid declarations" do
context "when changing to another cohort" do
let(:new_cohort) { Cohort.previous }
Expand Down
Loading

0 comments on commit 95fae5f

Please sign in to comment.