diff --git a/app/controllers/admin/fraud_risk_csv_downloads_controller.rb b/app/controllers/admin/fraud_risk_csv_downloads_controller.rb new file mode 100644 index 0000000000..c7cf4bed4e --- /dev/null +++ b/app/controllers/admin/fraud_risk_csv_downloads_controller.rb @@ -0,0 +1,25 @@ +module Admin + class FraudRiskCsvDownloadsController < BaseAdminController + before_action :ensure_service_operator + + def show + respond_to do |format| + format.csv do + send_data(csv, filename: "fraud_risk.csv") + end + end + end + + private + + def csv + CSV.generate do |csv| + csv << %w[field value] + + RiskIndicator.order(created_at: :asc).pluck(:field, :value).each do |row| + csv << row + end + end + end + end +end diff --git a/app/controllers/admin/fraud_risk_csv_uploads_controller.rb b/app/controllers/admin/fraud_risk_csv_uploads_controller.rb new file mode 100644 index 0000000000..6924ac5c13 --- /dev/null +++ b/app/controllers/admin/fraud_risk_csv_uploads_controller.rb @@ -0,0 +1,28 @@ +module Admin + class FraudRiskCsvUploadsController < BaseAdminController + before_action :ensure_service_operator + + def new + @form = FraudRiskCsvUploadForm.new + end + + def create + @form = FraudRiskCsvUploadForm.new(fraud_risk_csv_upload_params) + + if @form.save + redirect_to( + new_admin_fraud_risk_csv_upload_path, + notice: "Fraud prevention list uploaded successfully." + ) + else + render :new + end + end + + private + + def fraud_risk_csv_upload_params + params.fetch(:admin_fraud_risk_csv_upload_form, {}).permit(:file) + end + end +end diff --git a/app/controllers/admin/tasks_controller.rb b/app/controllers/admin/tasks_controller.rb index aaf8521f18..9ad419a8df 100644 --- a/app/controllers/admin/tasks_controller.rb +++ b/app/controllers/admin/tasks_controller.rb @@ -6,7 +6,7 @@ class Admin::TasksController < Admin::BaseAdminController def index @claim_checking_tasks = ClaimCheckingTasks.new(@claim) - @has_matching_claims = Claim::MatchingAttributeFinder.new(@claim).matching_claims.exists? + @banner_messages = set_banner_messages end def show @@ -78,4 +78,29 @@ def load_matching_claims def current_task_name @task.name end + + def set_banner_messages + messages = [] + + if Claim::MatchingAttributeFinder.new(@claim).matching_claims.exists? + claims_link = view_context.link_to( + "Multiple claims", + admin_claim_task_path(claim_id: @claim.id, name: "matching_details"), + class: "govuk-notification-banner__link" + ) + + messages << "#{claims_link} with matching details have been made in this claim window.".html_safe + end + + if @claim.attributes_flagged_by_risk_indicator.any? + messages << <<~MSG.html_safe + This claim has been flagged as the + #{@claim.attributes_flagged_by_risk_indicator.map(&:humanize).to_sentence.downcase} + #{@claim.attributes_flagged_by_risk_indicator.many? ? "are" : "is"} + included on the fraud prevention list. Speak to a manager. + MSG + end + + messages + end end diff --git a/app/forms/admin/fraud_risk_csv_upload_form.rb b/app/forms/admin/fraud_risk_csv_upload_form.rb new file mode 100644 index 0000000000..4e48fa680a --- /dev/null +++ b/app/forms/admin/fraud_risk_csv_upload_form.rb @@ -0,0 +1,87 @@ +module Admin + class FraudRiskCsvUploadForm + include ActiveModel::Model + + attr_accessor :file + + validates :file, presence: {message: "CSV file is required"} + + validate :csv_has_required_headers, if: -> { file.present? } + + validate :all_rows_are_valid, if: -> { file.present? && csv_has_required_headers? } + + def initialize(params = {}) + super + end + + def save + return false unless valid? + + ApplicationRecord.transaction do + RiskIndicator.where.not(id: records.map(&:id)).destroy_all + + records.each(&:save!) + + claims_to_note.each do |claim| + AutomatedChecks::ClaimVerifiers::FraudRisk.new(claim: claim).perform + end + end + + true + end + + private + + def csv + @csv ||= CSV.parse(file.read, headers: true, skip_blanks: true) + end + + def records + @records ||= csv.map do |row| + RiskIndicator.find_or_initialize_by(row.to_h) + end.uniq { |record| record.attributes.slice("field", "value") } + end + + def all_rows_are_valid + csv.each do |row| + unless RiskIndicator::SUPPORTED_FIELDS.include?(row["field"]) + errors.add( + :base, + "'#{row["field"]}' is not a valid attribute - " \ + "must be one of #{RiskIndicator::SUPPORTED_FIELDS.join(", ")}" + ) + end + + if row["value"].blank? + errors.add(:base, "'value' can't be blank") + end + end + end + + def csv_has_required_headers + unless csv_has_required_headers? + errors.add(:base, "csv is missing required headers `field`, `value`") + end + end + + def csv_has_required_headers? + csv.headers.include?("field") && csv.headers.include?("value") + end + + def claims_to_note + flagged_eligibility_claim_ids = Policies.with_attribute(:teacher_reference_number).flat_map do |policy| + policy::Eligibility + .where(teacher_reference_number: RiskIndicator.teacher_reference_number.select(:value)) + .joins(:claim) + .select("claims.id") + end + + Claim + .where( + "LOWER(national_insurance_number) IN (?)", + RiskIndicator.national_insurance_number.select("LOWER(value)") + ) + .or(Claim.where(id: flagged_eligibility_claim_ids)) + end + end +end diff --git a/app/models/automated_checks/claim_verifiers/fraud_risk.rb b/app/models/automated_checks/claim_verifiers/fraud_risk.rb new file mode 100644 index 0000000000..a0a47c77df --- /dev/null +++ b/app/models/automated_checks/claim_verifiers/fraud_risk.rb @@ -0,0 +1,35 @@ +module AutomatedChecks + module ClaimVerifiers + class FraudRisk + TASK_NAME = "fraud_risk".freeze + + def initialize(claim:) + @claim = claim + end + + def perform + return unless claim.attributes_flagged_by_risk_indicator.any? + + flagged_attributes = @claim + .attributes_flagged_by_risk_indicator + .map(&:humanize) + .to_sentence + .downcase + + plural_verbs = claim.attributes_flagged_by_risk_indicator.many? ? "are" : "is" + + body = "This claim has been flagged as the #{flagged_attributes} " \ + "#{plural_verbs} included on the fraud prevention list." + + claim.notes.create!( + body: body, + label: TASK_NAME + ) + end + + private + + attr_reader :claim + end + end +end diff --git a/app/models/claim.rb b/app/models/claim.rb index d579a4612e..cf6482cbce 100644 --- a/app/models/claim.rb +++ b/app/models/claim.rb @@ -249,7 +249,7 @@ def submittable? end def approvable? - submitted? && !held? && !payroll_gender_missing? && (!decision_made? || awaiting_qa?) && !payment_prevented_by_other_claims? + submitted? && !held? && !payroll_gender_missing? && (!decision_made? || awaiting_qa?) && !payment_prevented_by_other_claims? && attributes_flagged_by_risk_indicator.none? end def rejectable? @@ -457,6 +457,10 @@ def has_early_years_policy? policy == Policies::EarlyYearsPayments end + def attributes_flagged_by_risk_indicator + @attributes_flagged_by_risk_indicator ||= RiskIndicator.flagged_attributes(self) + end + private def one_login_idv_name_match? diff --git a/app/models/policies.rb b/app/models/policies.rb index 06015e1106..0b6a7ea8c2 100644 --- a/app/models/policies.rb +++ b/app/models/policies.rb @@ -37,4 +37,8 @@ def self.[](policy_type) def self.constantize(policy) "Policies::#{policy}".constantize end + + def self.with_attribute(attr) + POLICIES.select { |policy| policy::Eligibility.has_attribute?(attr) } + end end diff --git a/app/models/policies/early_career_payments.rb b/app/models/policies/early_career_payments.rb index 2248580d05..3495cd8e56 100644 --- a/app/models/policies/early_career_payments.rb +++ b/app/models/policies/early_career_payments.rb @@ -19,7 +19,8 @@ module EarlyCareerPayments AutomatedChecks::ClaimVerifiers::Induction, AutomatedChecks::ClaimVerifiers::CensusSubjectsTaught, AutomatedChecks::ClaimVerifiers::Employment, - AutomatedChecks::ClaimVerifiers::StudentLoanPlan + AutomatedChecks::ClaimVerifiers::StudentLoanPlan, + AutomatedChecks::ClaimVerifiers::FraudRisk ].freeze POLICY_START_YEAR = AcademicYear.new(2021).freeze diff --git a/app/models/policies/further_education_payments.rb b/app/models/policies/further_education_payments.rb index 7fe6b406ee..02d032c6d1 100644 --- a/app/models/policies/further_education_payments.rb +++ b/app/models/policies/further_education_payments.rb @@ -20,7 +20,8 @@ module FurtherEducationPayments AutomatedChecks::ClaimVerifiers::Identity, AutomatedChecks::ClaimVerifiers::ProviderVerification, AutomatedChecks::ClaimVerifiers::Employment, - AutomatedChecks::ClaimVerifiers::StudentLoanPlan + AutomatedChecks::ClaimVerifiers::StudentLoanPlan, + AutomatedChecks::ClaimVerifiers::FraudRisk ] # Options shown to admins when rejecting a claim diff --git a/app/models/policies/international_relocation_payments.rb b/app/models/policies/international_relocation_payments.rb index ed4861b78a..963bb5d07d 100644 --- a/app/models/policies/international_relocation_payments.rb +++ b/app/models/policies/international_relocation_payments.rb @@ -3,6 +3,10 @@ module InternationalRelocationPayments include BasePolicy extend self + VERIFIERS = [ + AutomatedChecks::ClaimVerifiers::FraudRisk + ].freeze + ELIGIBILITY_MATCHING_ATTRIBUTES = [["passport_number"]].freeze OTHER_CLAIMABLE_POLICIES = [] diff --git a/app/models/policies/levelling_up_premium_payments.rb b/app/models/policies/levelling_up_premium_payments.rb index 827a46e72b..2ecc3a79b0 100644 --- a/app/models/policies/levelling_up_premium_payments.rb +++ b/app/models/policies/levelling_up_premium_payments.rb @@ -9,7 +9,8 @@ module LevellingUpPremiumPayments AutomatedChecks::ClaimVerifiers::Qualifications, AutomatedChecks::ClaimVerifiers::CensusSubjectsTaught, AutomatedChecks::ClaimVerifiers::Employment, - AutomatedChecks::ClaimVerifiers::StudentLoanPlan + AutomatedChecks::ClaimVerifiers::StudentLoanPlan, + AutomatedChecks::ClaimVerifiers::FraudRisk ].freeze # Used in diff --git a/app/models/policies/student_loans.rb b/app/models/policies/student_loans.rb index 5de4c4828b..54687727ff 100644 --- a/app/models/policies/student_loans.rb +++ b/app/models/policies/student_loans.rb @@ -18,7 +18,8 @@ module StudentLoans AutomatedChecks::ClaimVerifiers::Qualifications, AutomatedChecks::ClaimVerifiers::CensusSubjectsTaught, AutomatedChecks::ClaimVerifiers::Employment, - AutomatedChecks::ClaimVerifiers::StudentLoanAmount + AutomatedChecks::ClaimVerifiers::StudentLoanAmount, + AutomatedChecks::ClaimVerifiers::FraudRisk ].freeze POLICY_START_YEAR = AcademicYear.new(2013).freeze diff --git a/app/models/risk_indicator.rb b/app/models/risk_indicator.rb new file mode 100644 index 0000000000..50d9caeb23 --- /dev/null +++ b/app/models/risk_indicator.rb @@ -0,0 +1,26 @@ +class RiskIndicator < ApplicationRecord + SUPPORTED_FIELDS = %w[ + teacher_reference_number + national_insurance_number + ].freeze + + enum field: SUPPORTED_FIELDS.index_by(&:itself) + + validates :value, presence: { + message: "'value' can't be blank" + } + + validates :value, uniqueness: {scope: :field} + + def self.flagged_attributes(claim) + where( + "field = 'national_insurance_number' AND LOWER(value) = :value", + value: claim.national_insurance_number&.downcase + ).or( + where( + field: "teacher_reference_number", + value: claim.eligibility.try(:teacher_reference_number) + ) + ).pluck(:field).compact + end +end diff --git a/app/views/admin/claims/index.html.erb b/app/views/admin/claims/index.html.erb index 3d6cf32b9e..85599ec91b 100644 --- a/app/views/admin/claims/index.html.erb +++ b/app/views/admin/claims/index.html.erb @@ -15,6 +15,7 @@ <%= link_to "Upload School Workforce Census data", new_admin_school_workforce_census_data_upload_path, class: "govuk-button govuk-button--secondary", data: { module: "govuk-button" }, role: :button %> <%= link_to "Upload TPS data", new_admin_tps_data_upload_path, class: "govuk-button govuk-button--secondary", data: { module: "govuk-button" }, role: :button %> <%= link_to "Upload SLC data", new_admin_student_loans_data_upload_path, class: "govuk-button govuk-button--secondary", data: { module: "govuk-button" }, role: :button %> + <%= link_to "Upload fraud prevention data", new_admin_fraud_risk_csv_upload_path, class: "govuk-button govuk-button--secondary", data: { module: "govuk-button" }, role: :button %> <%= render "allocations_form" %> diff --git a/app/views/admin/decisions/_decision_form.html.erb b/app/views/admin/decisions/_decision_form.html.erb index ffe191fdcf..c32f6c73b2 100644 --- a/app/views/admin/decisions/_decision_form.html.erb +++ b/app/views/admin/decisions/_decision_form.html.erb @@ -29,6 +29,21 @@ <% end %> +<% if claim.attributes_flagged_by_risk_indicator.any? %> +
+ + + Warning +

+ This claim cannot be approved because the + <%= @claim.attributes_flagged_by_risk_indicator.map(&:humanize).to_sentence.downcase %> + <%= @claim.attributes_flagged_by_risk_indicator.many? ? "are" : "is" %> + included on the fraud prevention list. +

+
+
+<% end %> + <%= form_for decision, url: admin_claim_decisions_path(claim), html: { id: "claim_decision_form" } do |form| %> <%= hidden_field_tag :qa, params[:qa] %> diff --git a/app/views/admin/fraud_risk_csv_uploads/new.html.erb b/app/views/admin/fraud_risk_csv_uploads/new.html.erb new file mode 100644 index 0000000000..8784f19c3f --- /dev/null +++ b/app/views/admin/fraud_risk_csv_uploads/new.html.erb @@ -0,0 +1,31 @@ +
+
+

+ Fraud risk CSV upload +

+ + <%= form_with( + url: admin_fraud_risk_csv_uploads_path, + model: @form, + builder: GOVUKDesignSystemFormBuilder::FormBuilder + ) do |f| %> + <%= f.govuk_error_summary %> + + <%= f.govuk_file_field( + :file, + label: { text: "Upload fraud risk CSV file" }, + hint: { + text: "Currently supported attributes are #{RiskIndicator::SUPPORTED_FIELDS.join(", ")}." + } + ) %> + + <%= f.govuk_submit "Upload CSV" %> + <% end %> + + <%= govuk_link_to( + "Download CSV", + admin_fraud_risk_csv_download_path(format: :csv), + class: "govuk-button" + ) %> +
+
diff --git a/app/views/admin/tasks/_banner.html.erb b/app/views/admin/tasks/_banner.html.erb new file mode 100644 index 0000000000..b50a12e503 --- /dev/null +++ b/app/views/admin/tasks/_banner.html.erb @@ -0,0 +1,26 @@ +
+
+

+ Important +

+
+
+ <% if messages.many? %> +

+ This claim requires the following to be reviewed: +

+ + + <% else %> +

+ <%= messages.first %> +

+ <% end %> +
+
diff --git a/app/views/admin/tasks/index.html.erb b/app/views/admin/tasks/index.html.erb index 66889dda47..9c24b33456 100644 --- a/app/views/admin/tasks/index.html.erb +++ b/app/views/admin/tasks/index.html.erb @@ -4,19 +4,18 @@ <%= govuk_back_link href: claims_backlink_path %> <% end %> -<% if @has_matching_claims %> -
-
-

- Important -

-
-
-

- <%= link_to "Multiple claims", admin_claim_task_path(claim_id: @claim.id, name: "matching_details") %> - with matching details have been made in this claim window. +<% if @banner_messages.any? %> + <%= render partial: "banner", locals: { messages: @banner_messages } %> +<% end %> + +<% if false && @claim.attributes_flagged_by_risk_indicator.any? %> +

+ + + Warning +

-
+
<% end %> diff --git a/config/analytics_blocklist.yml b/config/analytics_blocklist.yml index 4de6714919..d05bf8bfb7 100644 --- a/config/analytics_blocklist.yml +++ b/config/analytics_blocklist.yml @@ -135,3 +135,9 @@ - provider_email_address - practitioner_first_name - practitioner_surname + :risk_indicators: + - id + - field + - value + - created_at + - updated_at diff --git a/config/routes.rb b/config/routes.rb index fb1b3ebfc4..87614e70ff 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -146,6 +146,8 @@ def matches?(request) resources :school_workforce_census_data_uploads, only: [:new, :create] resources :student_loans_data_uploads, only: [:new, :create] resources :tps_data_uploads, only: [:new, :create] + resources :fraud_risk_csv_uploads, only: [:new, :create] + resource :fraud_risk_csv_download, only: :show resources :payroll_runs, only: [:index, :new, :create, :show, :destroy] do resources :payment_confirmation_report_uploads, only: [:new, :create] diff --git a/db/migrate/20241017160838_create_risk_indicators.rb b/db/migrate/20241017160838_create_risk_indicators.rb new file mode 100644 index 0000000000..ff3f20cab7 --- /dev/null +++ b/db/migrate/20241017160838_create_risk_indicators.rb @@ -0,0 +1,12 @@ +class CreateRiskIndicators < ActiveRecord::Migration[7.0] + def change + create_table :risk_indicators, id: :uuid do |t| + t.string :field, null: false + t.string :value, null: false + + t.index %i[field value], unique: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 94eee59299..9a6c43d4ed 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -428,6 +428,14 @@ t.string "itt_subject" end + create_table "risk_indicators", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "field", null: false + t.string "value", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["field", "value"], name: "index_risk_indicators_on_field_and_value", unique: true + end + create_table "school_workforce_censuses", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "teacher_reference_number" t.datetime "created_at", null: false diff --git a/spec/factories/risk_indicators.rb b/spec/factories/risk_indicators.rb new file mode 100644 index 0000000000..718880baa5 --- /dev/null +++ b/spec/factories/risk_indicators.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :risk_indicator do + field { "teacher_reference_number" } + value { "1234567" } + end +end diff --git a/spec/features/admin/admin_fraud_prevention_spec.rb b/spec/features/admin/admin_fraud_prevention_spec.rb new file mode 100644 index 0000000000..8b8a9f5644 --- /dev/null +++ b/spec/features/admin/admin_fraud_prevention_spec.rb @@ -0,0 +1,294 @@ +require "rails_helper" + +RSpec.feature "Admin fraud prevention" do + let(:fraud_risk_csv) do + File.open(Rails.root.join("spec", "fixtures", "files", "fraud_risk.csv")) + end + + context "when updating the list of flagged attributes" do + it "flags any matching claims" do + flagged_claim_trn = create( + :claim, + :submitted, + eligibility_attributes: { + teacher_reference_number: "1234567" + } + ) + + flagged_claim_nino = create( + :claim, + :submitted, + national_insurance_number: "QQ123456C" + ) + + flagged_claim_trn_and_nino = create( + :claim, + :submitted, + eligibility_attributes: { + teacher_reference_number: "1234567" + }, + national_insurance_number: "QQ123456C" + ) + + sign_in_as_service_operator + visit new_admin_fraud_risk_csv_upload_path + attach_file "Upload fraud risk CSV file", fraud_risk_csv.path + click_on "Upload" + + expect(page).to have_content( + "Fraud prevention list uploaded successfully." + ) + + visit admin_claim_tasks_path(flagged_claim_trn) + + expect(page).to have_content( + "This claim has been flagged as the " \ + "teacher reference number is included on the fraud prevention list." + ) + + visit admin_claim_tasks_path(flagged_claim_nino) + + expect(page).to have_content( + "This claim has been flagged as the " \ + "national insurance number is included on the fraud prevention list." + ) + + visit admin_claim_tasks_path(flagged_claim_trn_and_nino) + + expect(page).to have_content( + "This claim has been flagged as the " \ + "national insurance number and teacher reference number are included " \ + "on the fraud prevention list." + ) + + visit new_admin_claim_decision_path(flagged_claim_trn) + + approval_option = find("input[type=radio][value=approved]") + + expect(approval_option).to be_disabled + + expect(page).to have_content( + "This claim cannot be approved because the teacher reference number " \ + "is included on the fraud prevention list." + ) + + visit new_admin_claim_decision_path(flagged_claim_nino) + + approval_option = find("input[type=radio][value=approved]") + + expect(approval_option).to be_disabled + + expect(page).to have_content( + "This claim cannot be approved because the national insurance number " \ + "is included on the fraud prevention list." + ) + + visit new_admin_claim_decision_path(flagged_claim_trn_and_nino) + + approval_option = find("input[type=radio][value=approved]") + + expect(approval_option).to be_disabled + + expect(page).to have_content( + "This claim cannot be approved because the national insurance number " \ + "and teacher reference number are included on the fraud prevention list." + ) + + visit admin_claim_notes_path(flagged_claim_trn) + + within(".hmcts-timeline:first-of-type") do + expect(page).to have_content( + "This claim has been flagged as the " \ + "teacher reference number is included on the fraud prevention list." + ) + end + + visit admin_claim_notes_path(flagged_claim_nino) + + within(".hmcts-timeline:first-of-type") do + expect(page).to have_content( + "This claim has been flagged as the " \ + "national insurance number is included on the fraud prevention list." + ) + end + + visit admin_claim_notes_path(flagged_claim_trn_and_nino) + + within(".hmcts-timeline:first-of-type") do + expect(page).to have_content( + "This claim has been flagged as the " \ + "national insurance number and teacher reference number are included " \ + "on the fraud prevention list." + ) + end + end + end + + it "allows for downloading the csv" do + sign_in_as_service_operator + visit new_admin_fraud_risk_csv_upload_path + attach_file "Upload fraud risk CSV file", fraud_risk_csv.path + click_on "Upload" + + click_on "Download" + expect(page.body).to eq(fraud_risk_csv.read.chomp) + end + + it "creates a note for submitted claims" do + create( + :risk_indicator, + field: "national_insurance_number", + value: "QQ123456C" + ) + + claim = submit_claim(national_insurance_number: "QQ123456C") + + # Stub dqt api call in verifiers job + dqt_teacher_resource = instance_double(Dqt::TeacherResource, find: nil) + dqt_client = instance_double(Dqt::Client, teacher: dqt_teacher_resource) + allow(Dqt::Client).to receive(:new).and_return(dqt_client) + + perform_enqueued_jobs + + sign_in_as_service_operator + + visit admin_claim_notes_path(claim) + + within(".hmcts-timeline:first-of-type") do + expect(page).to have_content( + "This claim has been flagged as the " \ + "national insurance number is included on the fraud prevention list." + ) + end + end + + def submit_claim(national_insurance_number: "QQ123456C") + create(:journey_configuration, :additional_payments) + + school = create(:school, :early_career_payments_eligible) + + visit landing_page_path(Journeys::AdditionalPaymentsForTeaching::ROUTING_NAME) + # - Landing (start) + click_on "Start now" + + click_on "Continue without signing in" + + # /additional-payments/current-school + fill_in :school_search, with: school.name + click_on "Continue" + # /additional-payments/current-school + choose school.name + click_on "Continue" + + # /additional-payments/nqt-in-academic-year-after-itt + choose "Yes" + click_on "Continue" + + # /additional-payments/induction-completed + choose "Yes" + click_on "Continue" + + # /additional-payments/supply-teacher + choose "Yes" + click_on "Continue" + + # /additional-payments/entire-term-contract + choose "Yes" + click_on "Continue" + + # /additional-payments/employed-directly + choose "Yes, I'm employed by my school" + click_on "Continue" + + # /additional-payments/poor-performance + within all(".govuk-fieldset")[0] do + choose("No") + end + within all(".govuk-fieldset")[1] do + choose("No") + end + click_on "Continue" + + # /additional-payments/qualification + choose "Postgraduate initial teacher training (ITT)" + click_on "Continue" + + # /additional-payments/itt-year + choose "2020 to 2021" + click_on "Continue" + + # /additional-payments/eligible-itt-subject + choose "Chemistry" + click_on "Continue" + + # /additional-payments/teaching-subject-now + choose "Yes" + click_on "Continue" + + # /additional-payments/check-your-answers-part-one + click_on "Continue" + + # /additional-payments/eligibility-confirmed + click_on "Apply now" + + # /additional-payments/information-provided + click_on "Continue" + + # /additional-payments/personal-details + fill_in "First name", with: "Seymour" + fill_in "Last name", with: "Skinner" + + fill_in "Day", with: "1" + fill_in "Month", with: "6" + fill_in "Year", with: "1980" + + fill_in "What is your National Insurance number?", with: national_insurance_number + + click_on "Continue" + + # /additional-payments/postcode-search + click_on "Enter your address manually" + + # /additional-payments/address + fill_in "House number or name", with: "123 Main Street" + fill_in "Building and street", with: "Downtown" + fill_in "Town or city", with: "Twin Peaks" + fill_in "County", with: "Washington" + fill_in "Postcode", with: "TE57 1NG" + + click_on "Continue" + + # /additional-payments/email-address + fill_in "Email address", with: "test@example.com" + click_on "Continue" + + # /additional-payments/email-verification + mail = ActionMailer::Base.deliveries.last + otp_in_mail_sent = mail[:personalisation].decoded.scan(/\b[0-9]{6}\b/).first + fill_in "Enter the 6-digit passcode", with: otp_in_mail_sent + click_on "Confirm" + + # /additional-payments/provide-mobile-number + choose "No" + click_on "Continue" + + # /additional-payments/personal-bank-account + fill_in "Name on your account", with: "Seymour Skinner" + fill_in "Sort code", with: "123456" + fill_in "Account number", with: "87654321" + click_on "Continue" + + # /additional-payments/gender + choose "Male" + click_on "Continue" + + # /additional-payments/teacher-reference-number + fill_in "What is your teacher reference number (TRN)?", with: "1234567" + click_on "Continue" + + # /additional-payments/check-your-answers + click_on "Accept and send" + + Claim.order(:created_at).last + end +end diff --git a/spec/fixtures/files/fraud_risk.csv b/spec/fixtures/files/fraud_risk.csv new file mode 100644 index 0000000000..26684e5091 --- /dev/null +++ b/spec/fixtures/files/fraud_risk.csv @@ -0,0 +1,4 @@ +field,value +national_insurance_number,qq123456c +teacher_reference_number,1234567 + diff --git a/spec/fixtures/files/fraud_risk_missing_headers.csv b/spec/fixtures/files/fraud_risk_missing_headers.csv new file mode 100644 index 0000000000..f1abd81ec3 --- /dev/null +++ b/spec/fixtures/files/fraud_risk_missing_headers.csv @@ -0,0 +1,4 @@ +national_insurance_number,qq123456c +teacher_reference_number,1234567 + + diff --git a/spec/fixtures/files/fraud_risk_missing_value.csv b/spec/fixtures/files/fraud_risk_missing_value.csv new file mode 100644 index 0000000000..7b7b74387f --- /dev/null +++ b/spec/fixtures/files/fraud_risk_missing_value.csv @@ -0,0 +1,5 @@ +field,value +national_insurance_number,qq123456c +teacher_reference_number,1234567 +teacher_reference_number, + diff --git a/spec/fixtures/files/fraud_risk_unknown_attribute.csv b/spec/fixtures/files/fraud_risk_unknown_attribute.csv new file mode 100644 index 0000000000..c7cb826776 --- /dev/null +++ b/spec/fixtures/files/fraud_risk_unknown_attribute.csv @@ -0,0 +1,5 @@ +field,value +national_insurance_number,qq123456c +teacher_reference_number,1234567 +test,1234567 +rest,1234567 diff --git a/spec/forms/admin/fraud_risk_csv_upload_form_spec.rb b/spec/forms/admin/fraud_risk_csv_upload_form_spec.rb new file mode 100644 index 0000000000..a027455cee --- /dev/null +++ b/spec/forms/admin/fraud_risk_csv_upload_form_spec.rb @@ -0,0 +1,182 @@ +require "rails_helper" + +RSpec.describe Admin::FraudRiskCsvUploadForm, type: :model do + describe "validations" do + context "without a file" do + it "is invalid" do + form = described_class.new + expect(form).not_to be_valid + expect(form.errors[:file]).to include("CSV file is required") + end + end + + context "with an invalid csv" do + it "is invalid" do + file_path = Rails.root.join( + "spec", "fixtures", "files", "fraud_risk_missing_headers.csv" + ) + + file = Rack::Test::UploadedFile.new(file_path) + + form = described_class.new(file: file) + + expect(form).not_to be_valid + expect(form.errors[:base]).to include( + "csv is missing required headers `field`, `value`" + ) + end + end + + context "with a missing value csv" do + it "is invalid" do + file_path = Rails.root.join( + "spec", "fixtures", "files", "fraud_risk_missing_value.csv" + ) + + file = Rack::Test::UploadedFile.new(file_path) + + form = described_class.new(file: file) + + expect(form).not_to be_valid + expect(form.errors[:base]).to include("'value' can't be blank") + end + end + + context "with an unsupported field" do + it "is invalid" do + file_path = Rails.root.join( + "spec", "fixtures", "files", "fraud_risk_unknown_attribute.csv" + ) + + file = Rack::Test::UploadedFile.new(file_path) + + form = described_class.new(file: file) + + expect(form).not_to be_valid + expect(form.errors[:base]).to include( + "'test' is not a valid attribute - must be one of teacher_reference_number, national_insurance_number" + ) + end + end + end + + describe "#save" do + let(:file) do + Rack::Test::UploadedFile.new( + Rails.root.join("spec", "fixtures", "files", "fraud_risk.csv") + ) + end + + let(:form) { described_class.new(file: file) } + + it "creates risk indicator records for each row" do + expect(form.save).to be true + + expect(RiskIndicator.count).to eq(2) + + expect( + RiskIndicator.where(field: "teacher_reference_number").first.value + ).to eq("1234567") + + expect( + RiskIndicator.where(field: "national_insurance_number").first.value + ).to eq("qq123456c") + end + + it "doesn't duplicate existing risk indicator records" do + RiskIndicator.create!( + field: "teacher_reference_number", + value: "1234567" + ) + + RiskIndicator.create!( + field: "national_insurance_number", + value: "qq123456c" + ) + + expect { form.save }.not_to change(RiskIndicator, :count) + end + + it "removes risk indicators that are no longer in the CSV" do + RiskIndicator.create!( + field: "teacher_reference_number", + value: "2234567" + ) + + RiskIndicator.create!( + field: "national_insurance_number", + value: "qq111111c" + ) + + _unchanged_risk_indicator = RiskIndicator.create!( + field: "national_insurance_number", + value: "qq123456c" + ) + + expect(form.save).to be true + + expect( + RiskIndicator.where( + field: "teacher_reference_number", + value: "2234567" + ) + ).to be_empty + + expect( + RiskIndicator.where( + field: "national_insurance_number", + value: "qq111111c" + ) + ).to be_empty + + expect( + RiskIndicator.where( + field: "national_insurance_number", + value: "qq123456c" + ) + ).to exist + end + + it "adds a note to claims that are flagged" do + claim_1 = create(:claim, national_insurance_number: "qq123456c") + + claim_2 = create( + :claim, + eligibility_attributes: {teacher_reference_number: "1234567"} + ) + + claim_3 = create( + :claim, + national_insurance_number: "qq123456c", + eligibility_attributes: {teacher_reference_number: "1234567"} + ) + + form.save + + expect(claim_1.notes.by_label("fraud_risk").last.body).to eq( + "This claim has been flagged as the national insurance number is " \ + "included on the fraud prevention list." + ) + + expect(claim_2.notes.by_label("fraud_risk").last.body).to eq( + "This claim has been flagged as the teacher reference number is " \ + "included on the fraud prevention list." + ) + + expect(claim_3.notes.by_label("fraud_risk").last.body).to eq( + "This claim has been flagged as the national insurance number and " \ + "teacher reference number are included on the fraud prevention list." + ) + end + + it "doesn't add a note to claims that aren't flagged" do + claim = create( + :claim, + national_insurance_number: "qq123456d", + eligibility_attributes: {teacher_reference_number: "1234568"} + ) + + expect { form.save }.not_to change { claim.notes.count } + end + end +end diff --git a/spec/models/automated_checks/claim_verifier_spec.rb b/spec/models/automated_checks/claim_verifier_spec.rb index 65fcde043d..f75939fb19 100644 --- a/spec/models/automated_checks/claim_verifier_spec.rb +++ b/spec/models/automated_checks/claim_verifier_spec.rb @@ -53,6 +53,7 @@ double(perform: Task.new), double(perform: Task.new), double(perform: Object.new), + double(perform: nil), double(perform: nil) ] end diff --git a/spec/models/automated_checks/claim_verifiers/fraud_risk_spec.rb b/spec/models/automated_checks/claim_verifiers/fraud_risk_spec.rb new file mode 100644 index 0000000000..873c20e6a4 --- /dev/null +++ b/spec/models/automated_checks/claim_verifiers/fraud_risk_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +RSpec.describe AutomatedChecks::ClaimVerifiers::FraudRisk do + describe "#perform" do + context "with a claim that has flagged attributes" do + it "creates a note" do + claim = create(:claim, national_insurance_number: "QQ123456C") + + create( + :risk_indicator, + field: "national_insurance_number", + value: "QQ123456C" + ) + + described_class.new(claim: claim).perform + + note = claim.notes.last + + expect(note.label).to eq("fraud_risk") + + expect(note.body).to eq( + "This claim has been flagged as the national insurance number is " \ + "included on the fraud prevention list." + ) + end + end + + context "with a claim that has no flagged attributes" do + it "doesn't create a note" do + claim = create(:claim, national_insurance_number: "QQ123456B") + + create( + :risk_indicator, + field: "national_insurance_number", + value: "QQ123456C" + ) + + expect { described_class.new(claim: claim).perform }.not_to( + change { claim.notes.count } + ) + end + end + end +end diff --git a/spec/models/claim_spec.rb b/spec/models/claim_spec.rb index fdd88b90f2..a526d9bafd 100644 --- a/spec/models/claim_spec.rb +++ b/spec/models/claim_spec.rb @@ -342,6 +342,13 @@ expect(create(:claim, :submitted, national_insurance_number: national_insurance_number, date_of_birth: 30.years.ago).approvable?).to eq false end + it "returns false if the claim is flagged by a fraud check" do + claim = create(:claim, :submitted, national_insurance_number: "QQ123456C") + create(:risk_indicator, field: "national_insurance_number", value: "QQ123456C") + + expect(claim.approvable?).to eq false + end + context "when the claim is held" do subject(:claim) { create(:claim, :held) } it { is_expected.not_to be_approvable } diff --git a/spec/models/early_career_payments_spec.rb b/spec/models/early_career_payments_spec.rb index 1c71e9cffd..40f6498a9a 100644 --- a/spec/models/early_career_payments_spec.rb +++ b/spec/models/early_career_payments_spec.rb @@ -10,7 +10,8 @@ AutomatedChecks::ClaimVerifiers::Induction, AutomatedChecks::ClaimVerifiers::CensusSubjectsTaught, AutomatedChecks::ClaimVerifiers::Employment, - AutomatedChecks::ClaimVerifiers::StudentLoanPlan + AutomatedChecks::ClaimVerifiers::StudentLoanPlan, + AutomatedChecks::ClaimVerifiers::FraudRisk ]) end diff --git a/spec/models/further_education_payments_spec.rb b/spec/models/further_education_payments_spec.rb index cc1b15704f..e6fec73fa5 100644 --- a/spec/models/further_education_payments_spec.rb +++ b/spec/models/further_education_payments_spec.rb @@ -8,7 +8,8 @@ AutomatedChecks::ClaimVerifiers::Identity, AutomatedChecks::ClaimVerifiers::ProviderVerification, AutomatedChecks::ClaimVerifiers::Employment, - AutomatedChecks::ClaimVerifiers::StudentLoanPlan + AutomatedChecks::ClaimVerifiers::StudentLoanPlan, + AutomatedChecks::ClaimVerifiers::FraudRisk ]) end diff --git a/spec/models/levelling_up_premium_payments_spec.rb b/spec/models/levelling_up_premium_payments_spec.rb index 16a7c769bb..702123cab7 100644 --- a/spec/models/levelling_up_premium_payments_spec.rb +++ b/spec/models/levelling_up_premium_payments_spec.rb @@ -9,7 +9,8 @@ AutomatedChecks::ClaimVerifiers::Qualifications, AutomatedChecks::ClaimVerifiers::CensusSubjectsTaught, AutomatedChecks::ClaimVerifiers::Employment, - AutomatedChecks::ClaimVerifiers::StudentLoanPlan + AutomatedChecks::ClaimVerifiers::StudentLoanPlan, + AutomatedChecks::ClaimVerifiers::FraudRisk ]) end diff --git a/spec/models/student_loans_spec.rb b/spec/models/student_loans_spec.rb index f02368f0b4..98caffdd89 100644 --- a/spec/models/student_loans_spec.rb +++ b/spec/models/student_loans_spec.rb @@ -11,7 +11,8 @@ AutomatedChecks::ClaimVerifiers::Qualifications, AutomatedChecks::ClaimVerifiers::CensusSubjectsTaught, AutomatedChecks::ClaimVerifiers::Employment, - AutomatedChecks::ClaimVerifiers::StudentLoanAmount + AutomatedChecks::ClaimVerifiers::StudentLoanAmount, + AutomatedChecks::ClaimVerifiers::FraudRisk ]) end