diff --git a/db/schema.rb b/db/schema.rb index 8ca364172d..ef209c09ad 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1066,7 +1066,7 @@ add_foreign_key "appropriate_body_periods", "appropriate_bodies" add_foreign_key "contract_banded_fee_structure_bands", "contract_banded_fee_structures", column: "banded_fee_structure_id", on_delete: :cascade add_foreign_key "contracts", "contract_banded_fee_structures", column: "banded_fee_structure_id" - add_foreign_key "contracts", "contract_flat_rate_fee_structures", column: "flat_rate_fee_structure_id" + add_foreign_key "contracts", "contract_flat_rate_fee_structures", column: "flat_rate_fee_structure_id" add_foreign_key "contracts", "active_lead_providers" add_foreign_key "declarations", "delivery_partners", column: "delivery_partner_when_created_id" add_foreign_key "declarations", "statements", column: "clawback_statement_id" diff --git a/db/seeds/reuse_choices_scenarios.rb b/db/seeds/reuse_choices_scenarios.rb new file mode 100644 index 0000000000..4255bb3037 --- /dev/null +++ b/db/seeds/reuse_choices_scenarios.rb @@ -0,0 +1,11 @@ +if Rails.env.staging? || Rails.env.development? || Rails.env.review? + require Rails.root.join("db/seeds/support/seeds/reuse_choices") + + print_seed_info( + "Seeding reuse choices scenario schools (staging only)", + colour: :yellow, + blank_lines_before: 1 + ) + + Seeds::ReuseChoices.new(contract_period_year: 2025).call +end diff --git a/db/seeds/support/seeds/reuse_choices.rb b/db/seeds/support/seeds/reuse_choices.rb new file mode 100644 index 0000000000..9d80c2309d --- /dev/null +++ b/db/seeds/support/seeds/reuse_choices.rb @@ -0,0 +1,403 @@ +module Seeds + class ReuseChoices + BASE_URN = 9_100_100 + SCHEDULE_IDENTIFIER = "ecf-standard-september" + + LEAD_PROVIDER_REUSABLE_NAME = "Reuse – Lead Provider One" + LEAD_PROVIDER_NOT_AVAILABLE_IN_TARGET_YEAR_NAME = "Reuse – Lead Provider X (not available in target year)" + DELIVERY_PARTNER_REUSABLE_NAME = "Reuse – Delivery Partner One" + DELIVERY_PARTNER_NOT_REUSABLE_NAME = "Reuse – Delivery Partner Two" + PREFERRED_APPROPRIATE_BODY_NAME = "Golden Leaf Teaching School Hub" + + def initialize(contract_period_year:) + @contract_period_year = contract_period_year + end + + def call + ensure_contract_periods! + ensure_schedules! + ensure_reference_data! + ensure_matrix_appropriate_body_period! + ensure_target_year_availability_for_reusable_lead_provider! + + seed_blank_control_school! + seed_reusable_previous_scenarios! + seed_not_reusable_previous_scenarios! + end + + private + + attr_reader :contract_period_year + + def years + (2021..contract_period_year).to_a + end + + def target_contract_period + @target_contract_period ||= ContractPeriod.find_by!(year: contract_period_year) + end + + def ensure_contract_periods! + existing = ContractPeriod.where(year: years).index_by(&:year) + + years.each do |year| + next if existing.key?(year) + + ContractPeriod.create!( + year:, + started_on: Date.new(year, 6, 1), + finished_on: Date.new(year + 1, 5, 31), + enabled: true + ) + end + end + + def ensure_schedules! + existing_by_year = Schedule + .where(contract_period_year: years, identifier: SCHEDULE_IDENTIFIER) + .index_by(&:contract_period_year) + + years.each do |year| + next if existing_by_year.key?(year) + + Schedule.create!(contract_period_year: year, identifier: SCHEDULE_IDENTIFIER) + end + end + + def ensure_reference_data! + LeadProvider.find_or_create_by!(name: LEAD_PROVIDER_REUSABLE_NAME) + LeadProvider.find_or_create_by!(name: LEAD_PROVIDER_NOT_AVAILABLE_IN_TARGET_YEAR_NAME) + DeliveryPartner.find_or_create_by!(name: DELIVERY_PARTNER_REUSABLE_NAME) + DeliveryPartner.find_or_create_by!(name: DELIVERY_PARTNER_NOT_REUSABLE_NAME) + end + + def ensure_target_year_availability_for_reusable_lead_provider! + ActiveLeadProvider.find_or_create_by!( + lead_provider: reusable_lead_provider, + contract_period: target_contract_period + ) + end + + def reusable_lead_provider + @reusable_lead_provider ||= LeadProvider.find_by!(name: LEAD_PROVIDER_REUSABLE_NAME) + end + + def lead_provider_not_available_in_target_year + @lead_provider_not_available_in_target_year ||= LeadProvider.find_by!(name: LEAD_PROVIDER_NOT_AVAILABLE_IN_TARGET_YEAR_NAME) + end + + def reusable_delivery_partner + @reusable_delivery_partner ||= DeliveryPartner.find_by!(name: DELIVERY_PARTNER_REUSABLE_NAME) + end + + def not_reusable_delivery_partner + @not_reusable_delivery_partner ||= DeliveryPartner.find_by!(name: DELIVERY_PARTNER_NOT_REUSABLE_NAME) + end + + def ensure_matrix_appropriate_body_period! + matrix_appropriate_body_period + end + + def clear_attr!(record, attr_name) + record[attr_name] = nil if record.has_attribute?(attr_name) + end + + def matrix_appropriate_body_period + @matrix_appropriate_body_period ||= begin + abp = AppropriateBodyPeriod.find_or_create_by!(name: PREFERRED_APPROPRIATE_BODY_NAME) + + if abp.has_attribute?(:body_type) && abp.body_type != "teaching_school_hub" + abp.body_type = "teaching_school_hub" + end + + clear_attr!(abp, :dfe_sign_in_organisation_id) + clear_attr!(abp, :appropriate_body_id) + + abp.save! if abp.changed? + abp + end + end + + def set_last_chosen_appropriate_body!(school, chosen:) + return unless school.has_attribute?(:last_chosen_appropriate_body_id) + + school.last_chosen_appropriate_body_id = chosen ? matrix_appropriate_body_period.id : nil + end + + # + # Scenario group 1 – blank slate school + # + def seed_blank_control_school! + school = ensure_scenario_school!( + offset: 0, + gias_name: "Reuse scenario – blank slate (no previous programme)", + set_provider_led_last_chosen: false, + last_chosen_lead_provider: nil + ) + + ensure_school_partnership!( + school:, + lead_provider: reusable_lead_provider, + delivery_partner: reusable_delivery_partner, + year: contract_period_year + ) + end + + # + # Scenario group 2 – previous programme reusable in target year + # + def seed_reusable_previous_scenarios! + scenarios = [ + { offset: 1, previous_year: 2024, type: :partnership }, + { offset: 2, previous_year: 2024, type: :eoi }, + { offset: 3, previous_year: 2023, type: :partnership }, + { offset: 4, previous_year: 2023, type: :eoi }, + { offset: 5, previous_year: 2022, type: :partnership }, + { offset: 6, previous_year: 2022, type: :eoi }, + { offset: 7, previous_year: 2021, type: :partnership }, + { offset: 8, previous_year: 2021, type: :eoi } + ] + + scenarios.each { |scenario| seed_reusable_previous_scenario!(**scenario) } + end + + def seed_reusable_previous_scenario!(offset:, previous_year:, type:) + label = "Reuse scenario – #{previous_year} #{type_label(type)} (reusable)" + school = ensure_scenario_school!( + offset:, + gias_name: label, + set_provider_led_last_chosen: true, + last_chosen_lead_provider: reusable_lead_provider + ) + + seed_previous_teacher_and_training!( + school:, + previous_year:, + mode: type, + lead_provider: reusable_lead_provider, + delivery_partner_for_partnership: reusable_delivery_partner + ) + + if type == :partnership + ensure_school_partnership!( + school:, + lead_provider: reusable_lead_provider, + delivery_partner: reusable_delivery_partner, + year: contract_period_year + ) + else + ActiveLeadProvider.find_or_create_by!( + lead_provider: reusable_lead_provider, + contract_period: target_contract_period + ) + end + end + + # + # Scenario group 3 – previous programme NOT reusable in target year + # + def seed_not_reusable_previous_scenarios! + scenarios = [ + { offset: 9, previous_year: 2024, type: :partnership }, + { offset: 10, previous_year: 2024, type: :eoi }, + { offset: 11, previous_year: 2023, type: :partnership }, + { offset: 12, previous_year: 2023, type: :eoi }, + { offset: 13, previous_year: 2022, type: :partnership }, + { offset: 14, previous_year: 2022, type: :eoi }, + { offset: 15, previous_year: 2021, type: :partnership }, + { offset: 16, previous_year: 2021, type: :eoi } + ] + + scenarios.each { |scenario| seed_not_reusable_previous_scenario!(**scenario) } + end + + def seed_not_reusable_previous_scenario!(offset:, previous_year:, type:) + label = "Reuse scenario – #{previous_year} #{type_label(type)} (not reusable)" + last_chosen_lead_provider = type == :partnership ? reusable_lead_provider : lead_provider_not_available_in_target_year + + school = ensure_scenario_school!( + offset:, + gias_name: label, + set_provider_led_last_chosen: true, + last_chosen_lead_provider: + ) + + seed_previous_teacher_and_training!( + school:, + previous_year:, + mode: type, + lead_provider: last_chosen_lead_provider, + delivery_partner_for_partnership: not_reusable_delivery_partner + ) + + if type == :partnership + ActiveLeadProvider.find_or_create_by!( + lead_provider: reusable_lead_provider, + contract_period: target_contract_period + ) + else + ActiveLeadProvider.find_by( + lead_provider: lead_provider_not_available_in_target_year, + contract_period: target_contract_period + )&.destroy! + end + end + + def type_label(type) + case type + when :partnership then "partnership" + when :eoi then "expression of interest" + else type.to_s + end + end + + # + # ONE ECT + ONE TrainingPeriod per school (+ InductionPeriod) + # + def seed_previous_teacher_and_training!(school:, previous_year:, mode:, lead_provider:, delivery_partner_for_partnership:) + previous_contract_period = ContractPeriod.find_by!(year: previous_year) + previous_schedule = Schedule.find_by!(contract_period_year: previous_year, identifier: SCHEDULE_IDENTIFIER) + abp = matrix_appropriate_body_period + + teacher = FactoryBot.create(:teacher) + + ect_period = + FactoryBot.create( + :ect_at_school_period, + :finished, + school:, + teacher:, + started_on: Date.new(previous_year, 9, 1), + finished_on: Date.new(previous_year + 1, 7, 31), + school_reported_appropriate_body: abp + ) + + ect_period.update!(school_reported_appropriate_body: abp) if ect_period.school_reported_appropriate_body_id != abp.id + + induction_period = InductionPeriod.find_or_create_by!( + teacher: ect_period.teacher, + started_on: ect_period.started_on + ) do |ip| + ip.finished_on = ect_period.finished_on + ip.appropriate_body_period = abp + ip.induction_programme = "fip" + ip.training_programme = "provider_led" + ip.number_of_terms = 3 + end + + induction_period.update!(appropriate_body_period: abp) if induction_period.appropriate_body_period_id != abp.id + + active_lead_provider = + ActiveLeadProvider.find_or_create_by!( + lead_provider:, + contract_period: previous_contract_period + ) + + if school.has_attribute?(:last_chosen_training_programme) + school.last_chosen_training_programme = "provider_led" + end + school.last_chosen_lead_provider = active_lead_provider.lead_provider + set_last_chosen_appropriate_body!(school, chosen: true) + school.save! + + if mode == :partnership + school_partnership = + ensure_school_partnership!( + school:, + lead_provider:, + delivery_partner: delivery_partner_for_partnership, + year: previous_year + ) + + TrainingPeriod.find_or_create_by!( + ect_at_school_period: ect_period, + mentor_at_school_period: nil, + started_on: ect_period.started_on + ) do |tp| + tp.training_programme = "provider_led" + tp.schedule = previous_schedule + tp.school_partnership = school_partnership + tp.expression_of_interest = nil + tp.finished_on = ect_period.finished_on + tp.finished_on = tp.started_on + 1.day if tp.finished_on.present? && tp.finished_on <= tp.started_on + end + else + TrainingPeriod.find_or_create_by!( + ect_at_school_period: ect_period, + mentor_at_school_period: nil, + started_on: ect_period.started_on + ) do |tp| + tp.training_programme = "provider_led" + tp.schedule = previous_schedule + tp.school_partnership = nil + tp.expression_of_interest = active_lead_provider + tp.finished_on = ect_period.finished_on + tp.finished_on = tp.started_on + 1.day if tp.finished_on.present? && tp.finished_on <= tp.started_on + end + end + end + + def ensure_scenario_school!(offset:, gias_name:, set_provider_led_last_chosen:, last_chosen_lead_provider:) + urn = BASE_URN + offset + ensure_gias_school!(urn:, name: gias_name) + + school = School.find_or_initialize_by(urn:) + school.induction_tutor_name ||= "Reuse Tutor" + school.induction_tutor_email ||= "reuse@example.com" + + if set_provider_led_last_chosen + school.last_chosen_training_programme = "provider_led" if school.has_attribute?(:last_chosen_training_programme) + school.last_chosen_lead_provider = last_chosen_lead_provider + set_last_chosen_appropriate_body!(school, chosen: true) + else + school.last_chosen_training_programme = nil if school.has_attribute?(:last_chosen_training_programme) + school.last_chosen_lead_provider_id = nil if school.has_attribute?(:last_chosen_lead_provider_id) + school.last_chosen_lead_provider = nil + set_last_chosen_appropriate_body!(school, chosen: false) + end + + school.save! + school + end + + def ensure_gias_school!(urn:, name:) + desired = { + name:, + status: "open", + type_name: "Community school", + local_authority_code: 999, + in_england: true, + eligible: true, + opened_on: Date.new(2000, 1, 1), + section_41_approved: false + } + + record = GIAS::School.find_or_initialize_by(urn:) + permitted = desired.select { |k, _| record.has_attribute?(k) } + record.assign_attributes(permitted) + record.save! + record + end + + def ensure_school_partnership!(school:, lead_provider:, delivery_partner:, year:) + contract_period = ContractPeriod.find_by!(year:) + + active_lead_provider = + ActiveLeadProvider.find_or_create_by!( + lead_provider:, + contract_period: + ) + + lead_provider_delivery_partnership = + LeadProviderDeliveryPartnership.find_or_create_by!( + active_lead_provider:, + delivery_partner: + ) + + SchoolPartnership.find_or_create_by!( + school:, + lead_provider_delivery_partnership: + ) + end + end +end diff --git a/spec/seeds/reuse_choices_seed_spec.rb b/spec/seeds/reuse_choices_seed_spec.rb new file mode 100644 index 0000000000..c4a7b4d766 --- /dev/null +++ b/spec/seeds/reuse_choices_seed_spec.rb @@ -0,0 +1,184 @@ +RSpec.describe "Reuse choices scenarios seed" do + include Seeds::ReuseChoicesSeedHelpers + + let(:target_contract_period_year) { 2025 } + + before do + run_reuse_choices_seed!(contract_period_year: target_contract_period_year) + end + + describe "reference data" do + it "creates the reference lead providers, delivery partners and appropriate body" do + expect { reuse_reference_lead_provider }.not_to raise_error + expect { reuse_reference_lead_provider_not_available }.not_to raise_error + expect { reuse_reference_delivery_partner }.not_to raise_error + expect { reuse_reference_delivery_partner_not_reusable }.not_to raise_error + expect { reuse_reference_appropriate_body }.not_to raise_error + end + + it "ensures the reusable lead provider is available in the target year" do + expect( + target_year_active_lead_provider_exists?( + contract_period_year: target_contract_period_year, + lead_provider: reuse_reference_lead_provider + ) + ).to be(true) + end + end + + describe "scenario schools exist" do + it "creates 17 scenario schools (URNs BASE..BASE+16) with matching GIAS records" do + schools = School.where(urn: reuse_choices_urns).includes(:gias_school).order(:urn) + + expect(schools.size).to eq(17) + expect(schools.all? { |s| s.gias_school.present? }).to be(true) + end + end + + describe "blank slate scenario" do + it "creates the blank slate school with no last chosen programme choices" do + school = reuse_school(offset: 0) + + expect(school.last_chosen_training_programme).to be_nil + expect(school.last_chosen_lead_provider).to be_nil + end + + it "has at least one partnership option in the target year so the school can proceed" do + school = reuse_school(offset: 0) + + expect( + target_year_partnership_exists?( + school:, + contract_period_year: target_contract_period_year, + lead_provider: reuse_reference_lead_provider, + delivery_partner: reuse_reference_delivery_partner + ) + ).to be(true) + end + end + + describe "reusable previous programme scenarios" do + scenarios = [ + { offset: 1, previous_year: 2024, type: :partnership }, + { offset: 2, previous_year: 2024, type: :eoi }, + { offset: 3, previous_year: 2023, type: :partnership }, + { offset: 4, previous_year: 2023, type: :eoi }, + { offset: 5, previous_year: 2022, type: :partnership }, + { offset: 6, previous_year: 2022, type: :eoi }, + { offset: 7, previous_year: 2021, type: :partnership }, + { offset: 8, previous_year: 2021, type: :eoi } + ] + + scenarios.each do |scenario| + it "sets up #{scenario[:previous_year]} #{scenario[:type]} reusable (offset #{scenario[:offset]})" do + school = reuse_school(offset: scenario.fetch(:offset)) + previous_year = scenario.fetch(:previous_year) + type = scenario.fetch(:type) + + expect(school.last_chosen_training_programme).to eq("provider_led") + expect(school.last_chosen_lead_provider).to eq(reuse_reference_lead_provider) + + ect_period = scenario_ect_period_for_school!(school:, previous_year:) + expect(ect_period.school_reported_appropriate_body).to eq(reuse_reference_appropriate_body) + + induction_period = ect_period.teacher.induction_periods.find_by!(started_on: ect_period.started_on) + expect(induction_period.appropriate_body_period).to eq(reuse_reference_appropriate_body) + expect(induction_period.number_of_terms).to be_present + + training_period = scenario_provider_led_training_period_for_school!(school:, previous_year:) + expect(training_period.schedule.identifier).to eq(reuse_choices_schedule_identifier) + + case type + when :partnership + expect(training_period.school_partnership).to be_present + expect(training_period.expression_of_interest).to be_nil + + expect( + target_year_partnership_exists?( + school:, + contract_period_year: target_contract_period_year, + lead_provider: reuse_reference_lead_provider, + delivery_partner: reuse_reference_delivery_partner + ) + ).to be(true) + + when :eoi + expect(training_period.school_partnership).to be_nil + expect(training_period.expression_of_interest).to be_present + + expect( + target_year_active_lead_provider_exists?( + contract_period_year: target_contract_period_year, + lead_provider: reuse_reference_lead_provider + ) + ).to be(true) + else + raise "Unexpected type: #{type.inspect}" + end + end + end + end + + describe "not reusable previous programme scenarios" do + scenarios = [ + { offset: 9, previous_year: 2024, type: :partnership }, + { offset: 10, previous_year: 2024, type: :eoi }, + { offset: 11, previous_year: 2023, type: :partnership }, + { offset: 12, previous_year: 2023, type: :eoi }, + { offset: 13, previous_year: 2022, type: :partnership }, + { offset: 14, previous_year: 2022, type: :eoi }, + { offset: 15, previous_year: 2021, type: :partnership }, + { offset: 16, previous_year: 2021, type: :eoi } + ] + + scenarios.each do |scenario| + it "sets up #{scenario[:previous_year]} #{scenario[:type]} NOT reusable (offset #{scenario[:offset]})" do + school = reuse_school(offset: scenario.fetch(:offset)) + previous_year = scenario.fetch(:previous_year) + type = scenario.fetch(:type) + + expect(school.last_chosen_training_programme).to eq("provider_led") + + ect_period = scenario_ect_period_for_school!(school:, previous_year:) + induction_period = ect_period.teacher.induction_periods.find_by!(started_on: ect_period.started_on) + expect(induction_period.appropriate_body_period).to eq(reuse_reference_appropriate_body) + + training_period = scenario_provider_led_training_period_for_school!(school:, previous_year:) + expect(training_period.schedule.identifier).to eq(reuse_choices_schedule_identifier) + + case type + when :partnership + expect(training_period.school_partnership).to be_present + + expect( + target_year_active_lead_provider_exists?( + contract_period_year: target_contract_period_year, + lead_provider: reuse_reference_lead_provider + ) + ).to be(true) + + expect( + target_year_partnership_exists?( + school:, + contract_period_year: target_contract_period_year, + lead_provider: reuse_reference_lead_provider, + delivery_partner: reuse_reference_delivery_partner_not_reusable + ) + ).to be(false) + + when :eoi + expect(training_period.expression_of_interest).to be_present + + expect( + target_year_active_lead_provider_exists?( + contract_period_year: target_contract_period_year, + lead_provider: reuse_reference_lead_provider_not_available + ) + ).to be(false) + else + raise "Unexpected type: #{type.inspect}" + end + end + end + end +end diff --git a/spec/support/seeds/reuse_choices_seed_helpers.rb b/spec/support/seeds/reuse_choices_seed_helpers.rb new file mode 100644 index 0000000000..3680691179 --- /dev/null +++ b/spec/support/seeds/reuse_choices_seed_helpers.rb @@ -0,0 +1,79 @@ +module Seeds + module ReuseChoicesSeedHelpers + def run_reuse_choices_seed!(contract_period_year:) + Seeds::ReuseChoices.new(contract_period_year:).call + end + + def reuse_choices_base_urn + Seeds::ReuseChoices::BASE_URN + end + + def reuse_choices_schedule_identifier + Seeds::ReuseChoices::SCHEDULE_IDENTIFIER + end + + def reuse_choices_urns + base = reuse_choices_base_urn + (base..(base + 16)).to_a + end + + def reuse_school(offset:) + School.find_by!(urn: reuse_choices_base_urn + offset) + end + + def reuse_reference_lead_provider + LeadProvider.find_by!(name: Seeds::ReuseChoices::LEAD_PROVIDER_REUSABLE_NAME) + end + + def reuse_reference_lead_provider_not_available + LeadProvider.find_by!(name: Seeds::ReuseChoices::LEAD_PROVIDER_NOT_AVAILABLE_IN_TARGET_YEAR_NAME) + end + + def reuse_reference_delivery_partner + DeliveryPartner.find_by!(name: Seeds::ReuseChoices::DELIVERY_PARTNER_REUSABLE_NAME) + end + + def reuse_reference_delivery_partner_not_reusable + DeliveryPartner.find_by!(name: Seeds::ReuseChoices::DELIVERY_PARTNER_NOT_REUSABLE_NAME) + end + + def reuse_reference_appropriate_body + AppropriateBodyPeriod.find_by!(name: Seeds::ReuseChoices::PREFERRED_APPROPRIATE_BODY_NAME) + end + + def scenario_ect_period_for_school!(school:, previous_year:) + school.ect_at_school_periods.find_by!( + started_on: Date.new(previous_year, 9, 1), + finished_on: Date.new(previous_year + 1, 7, 31) + ) + end + + def scenario_provider_led_training_period_for_school!(school:, previous_year:) + ect_at_school_period = scenario_ect_period_for_school!(school:, previous_year:) + + TrainingPeriod.find_by!( + ect_at_school_period:, + training_programme: "provider_led", + started_on: ect_at_school_period.started_on + ) + end + + def target_year_partnership_exists?(school:, contract_period_year:, lead_provider:, delivery_partner:) + SchoolPartnership + .joins(lead_provider_delivery_partnership: [{ active_lead_provider: :contract_period }, :delivery_partner]) + .where(school:) + .where(contract_periods: { year: contract_period_year }) + .where(active_lead_providers: { lead_provider_id: lead_provider.id }) + .where(delivery_partners: { id: delivery_partner.id }) + .exists? + end + + def target_year_active_lead_provider_exists?(contract_period_year:, lead_provider:) + ActiveLeadProvider + .joins(:contract_period) + .where(lead_provider:) + .where(contract_periods: { year: contract_period_year }) + .exists? + end + end +end