diff --git a/app/models/claim.rb b/app/models/claim.rb index bb0491bc35..cc1a6d3828 100644 --- a/app/models/claim.rb +++ b/app/models/claim.rb @@ -284,7 +284,12 @@ def approvable? (!decision_made? || awaiting_qa?) && !payment_prevented_by_other_claims? && attributes_flagged_by_risk_indicator.none? && - policy.approvable?(self) + policy.approvable?(self) && + !precluded_by_previous_claim? + end + + def approved? + decision_made? && latest_decision.approved? end def rejectable? @@ -296,7 +301,7 @@ def holdable? end def flaggable_for_qa? - decision_made? && latest_decision.approved? && below_min_qa_threshold? && !awaiting_qa? && !qa_completed? + approved? && below_min_qa_threshold? && !awaiting_qa? && !qa_completed? end # This method's intention is to help make a decision on whether a claim should @@ -570,4 +575,18 @@ def submittable_mobile_details? def submittable_email_details? email_address.present? && email_verified == true end + + def claims_from_same_claimant + @claims_from_same_claimant ||= MatchingAttributeFinder.new(self).matching_claims + end + + def precluded_by_previous_claim? + approved_claims = claims_from_same_claimant.select(&:approved?) + + other_claimed_policies = approved_claims.map(&:policy) + + policies_claimed = (other_claimed_policies + [policy]).uniq + + Policies.prohibited_policy_combination?(policies_claimed) + end end diff --git a/app/models/policies.rb b/app/models/policies.rb index 0b6a7ea8c2..5521d95628 100644 --- a/app/models/policies.rb +++ b/app/models/policies.rb @@ -41,4 +41,23 @@ def self.constantize(policy) def self.with_attribute(attr) POLICIES.select { |policy| policy::Eligibility.has_attribute?(attr) } end + + # Claimants can't claim for these policy combinations in the same academic + # year + INVALID_POLICY_COMBINATIONS = [ + [EarlyCareerPayments, EarlyYearsPayments], + [EarlyCareerPayments, FurtherEducationPayments], + [EarlyCareerPayments, LevellingUpPremiumPayments], + [EarlyYearsPayments, FurtherEducationPayments], + [EarlyYearsPayments, LevellingUpPremiumPayments], + [FurtherEducationPayments, LevellingUpPremiumPayments], + [FurtherEducationPayments, StudentLoans], + [FurtherEducationPayments, InternationalRelocationPayments] + ] + + def self.prohibited_policy_combination?(policies) + policies.combination(2).any? do |policy1, policy2| + INVALID_POLICY_COMBINATIONS.include?([policy1, policy2].sort_by(&:to_s)) + end + end end diff --git a/spec/factories/claims.rb b/spec/factories/claims.rb index 6a4cfc9fb7..12e442a1b0 100644 --- a/spec/factories/claims.rb +++ b/spec/factories/claims.rb @@ -2,6 +2,7 @@ sequence(:email_address) { |n| "person#{n}@example.com" } sequence(:teacher_reference_number, 1000000) { |n| n } sequence(:national_insurance_number, 100000) { |n| "QQ#{n}C" } + sequence(:name) { |n| "Name #{n}" } factory :claim do started_at { Time.zone.now } @@ -67,6 +68,14 @@ national_insurance_number { generate(:national_insurance_number) } end + trait :random_personal_details do + email_address { generate(:email_address) } + first_name { generate(:name) } + surname { generate(:name) } + date_of_birth { rand(18..65).years.ago.to_date } + national_insurance_number { generate(:national_insurance_number) } + end + trait :eligible do eligibility_trait { :eligible } end diff --git a/spec/factories/payments.rb b/spec/factories/payments.rb index 63a37cd35a..595497abfc 100644 --- a/spec/factories/payments.rb +++ b/spec/factories/payments.rb @@ -5,17 +5,20 @@ end claims do - personal_details = { - national_insurance_number: generate(:national_insurance_number), - email_address: "email@example.com", - bank_sort_code: "220011", - bank_account_number: "12345678", - eligibility_attributes: {teacher_reference_number: generate(:teacher_reference_number)} - } + personal_details = attributes_for( + :claim, + :random_personal_details, + :with_bank_details + ).except(:reference).merge( + eligibility_attributes: { + teacher_reference_number: generate(:teacher_reference_number) + } + ) claim_policies.map do |policy| association(:claim, :approved, personal_details.merge(policy: policy)) end end + association(:payroll_run, factory: :payroll_run) award_amount { claims.map(&:award_amount).compact.sum } diff --git a/spec/features/admin/admin_payroll_runs_spec.rb b/spec/features/admin/admin_payroll_runs_spec.rb index 25735793b0..85836482cc 100644 --- a/spec/features/admin/admin_payroll_runs_spec.rb +++ b/spec/features/admin/admin_payroll_runs_spec.rb @@ -9,10 +9,10 @@ scenario "Service operator creates a payroll run" do click_on "Payroll" - create(:claim, :approved, policy: Policies::StudentLoans) - create(:claim, :approved, policy: Policies::StudentLoans) - create(:claim, :approved, policy: Policies::EarlyCareerPayments) - create(:claim, :approved, policy: Policies::LevellingUpPremiumPayments) + create(:claim, :approved, :random_personal_details, policy: Policies::StudentLoans) + create(:claim, :approved, :random_personal_details, policy: Policies::StudentLoans) + create(:claim, :approved, :random_personal_details, policy: Policies::EarlyCareerPayments) + create(:claim, :approved, :random_personal_details, policy: Policies::LevellingUpPremiumPayments) paid_lup_claim = nil travel_to 2.months.ago do diff --git a/spec/features/admin/claim_with_unpermitted_policy_combo_spec.rb b/spec/features/admin/claim_with_unpermitted_policy_combo_spec.rb new file mode 100644 index 0000000000..ce3557ff89 --- /dev/null +++ b/spec/features/admin/claim_with_unpermitted_policy_combo_spec.rb @@ -0,0 +1,77 @@ +require "rails_helper" + +RSpec.describe "Claim with unpermitted policy combo" do + context "when one of the claims has been approved" do + it "doesn't allow the admin to approve the other claim" do + create( + :claim, + :approved, + :current_academic_year, + policy: Policies::InternationalRelocationPayments, + email_address: "duplicate@example.com" + ) + + duplicate_claim = create( + :claim, + :submitted, + :current_academic_year, + policy: Policies::FurtherEducationPayments, + email_address: "duplicate@example.com" + ) + + sign_in_as_service_operator + + visit new_admin_claim_decision_path(duplicate_claim) + + approve_option = find("input[type=radio][value=approved]") + + expect(approve_option).to be_disabled + end + end + + context "when neither of the claims have been approved" do + it "allows the admin to approve one of the claims" do + irp_claim = create( + :claim, + :submitted, + :current_academic_year, + policy: Policies::InternationalRelocationPayments, + email_address: "duplicate@example.com" + ) + + fe_claim = create( + :claim, + :submitted, + :current_academic_year, + policy: Policies::FurtherEducationPayments, + email_address: "duplicate@example.com" + ) + + sign_in_as_service_operator + + visit new_admin_claim_decision_path(irp_claim) + + approve_option = find("input[type=radio][value=approved]") + + expect(approve_option).not_to be_disabled + + visit new_admin_claim_decision_path(fe_claim) + + approve_option = find("input[type=radio][value=approved]") + + expect(approve_option).not_to be_disabled + + choose "Approve" + + fill_in "Decision notes", with: "LGTM" + + click_on "Confirm decision" + + visit new_admin_claim_decision_path(irp_claim) + + approve_option = find("input[type=radio][value=approved]") + + expect(approve_option).to be_disabled + end + end +end diff --git a/spec/jobs/payroll_run_job_spec.rb b/spec/jobs/payroll_run_job_spec.rb index 8d2989def4..17201e7a72 100644 --- a/spec/jobs/payroll_run_job_spec.rb +++ b/spec/jobs/payroll_run_job_spec.rb @@ -6,7 +6,7 @@ describe "#perform" do context "when successful" do - let(:claims) { Policies.all.map { |policy| create(:claim, :approved, policy: policy) } } + let(:claims) { Policies.all.map { |policy| create(:claim, :approved, :random_personal_details, policy: policy) } } let(:topups) { [] } before do diff --git a/spec/models/claim_spec.rb b/spec/models/claim_spec.rb index 1196df1bff..117277cac1 100644 --- a/spec/models/claim_spec.rb +++ b/spec/models/claim_spec.rb @@ -392,6 +392,54 @@ expect(subject).not_to be_approvable end end + + context "when the claimant has claimed for a policy that precludes them from this policy" do + subject do + create( + :claim, + :submitted, + :current_academic_year, + policy: Policies::FurtherEducationPayments, + email_address: "duplicate@example.com" + ) + end + + context "when the other claim has been approved" do + it "is not approvable" do + create( + :claim, + :approved, + :current_academic_year, + policy: Policies::EarlyCareerPayments, + email_address: "duplicate@example.com" + ) + + expect(subject).not_to be_approvable + end + end + + context "when the other claim has not been approved" do + it "is approvable" do + create( + :claim, + :submitted, + :current_academic_year, + policy: Policies::EarlyCareerPayments, + email_address: "duplicate@example.com" + ) + + create( + :claim, + :rejected, + :current_academic_year, + policy: Policies::EarlyCareerPayments, + email_address: "duplicate@example.com" + ) + + expect(subject).to be_approvable + end + end + end end describe "#rejectable?" do diff --git a/spec/models/policies_spec.rb b/spec/models/policies_spec.rb index 6a1fb701cf..90baa6c324 100644 --- a/spec/models/policies_spec.rb +++ b/spec/models/policies_spec.rb @@ -58,4 +58,12 @@ end end end + + describe "::INVALID_POLICY_COMBINATIONS" do + it "contains subarrays that are sorted alphabetically" do + Policies::INVALID_POLICY_COMBINATIONS.each do |subarray| + expect(subarray).to eq(subarray.sort_by(&:to_s)) + end + end + end end