Skip to content

Commit

Permalink
Add fraud check
Browse files Browse the repository at this point in the history
Adds a mechanisim for admins to upload a csv of know risky trns and
ninos that will cause claims with these attributes to be flagged in the
admin ui.

Flagged values can be removed by uploading a CSV without those values.

According to the govuk guidance we can't show multiple notification
banners on a page and should combine their content. We may want to use a
different method to display fraud prevention warnings to admins.
  • Loading branch information
rjlynch committed Oct 18, 2024
1 parent 8a46135 commit a2e1ee0
Show file tree
Hide file tree
Showing 18 changed files with 441 additions and 6 deletions.
25 changes: 25 additions & 0 deletions app/controllers/admin/fraud_risk_csv_downloads_controller.rb
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions app/controllers/admin/fraud_risk_csv_uploads_controller.rb
Original file line number Diff line number Diff line change
@@ -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.require(:admin_fraud_risk_csv_upload_form).permit(:file)
end
end
end
45 changes: 45 additions & 0 deletions app/forms/admin/fraud_risk_csv_upload_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Admin
class FraudRiskCsvUploadForm
include ActiveModel::Model

attr_accessor :file

validates :file, presence: true

validate :all_records_are_valid, if: -> { file.present? }

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!)
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_records_are_valid
records.select(&:invalid?).each do |record|
errors.add(:base, record.errors.map(&:message).join(", "))
end
end
end
end
4 changes: 4 additions & 0 deletions app/models/claim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,10 @@ def awaiting_provider_verification?
eligibility.awaiting_provider_verification?
end

def attributes_flagged_by_risk_indicator
@attributes_flagged_by_risk_indicator ||= RiskIndicator.flagged_attributes(self)
end

private

def one_login_idv_name_match?
Expand Down
34 changes: 34 additions & 0 deletions app/models/risk_indicator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class RiskIndicator < ApplicationRecord
SUPPORTED_FIELDS = %w[
teacher_reference_number
national_insurance_number
].freeze

validates :field, presence: {
message: "'field' can't be blank"
}

validates :value, presence: {
message: "'value' can't be blank"
}

validates :value, uniqueness: {scope: :field}

validates :field,
inclusion: {
in: SUPPORTED_FIELDS,
message: "'%{value}' is not a valid attribute - must be one of #{SUPPORTED_FIELDS.join(", ")}"
}

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)
end
end
31 changes: 31 additions & 0 deletions app/views/admin/fraud_risk_csv_uploads/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl">
Fraud risk CSV upload
</h1>

<%= 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"
) %>
</div>
</div>
25 changes: 20 additions & 5 deletions app/views/admin/tasks/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@
<%= govuk_back_link href: claims_backlink_path %>
<% end %>

<% if @has_matching_claims %>
<% if @has_matching_claims || @claim.attributes_flagged_by_risk_indicator.any? %>
<div class="govuk-notification-banner" role="region" aria-labelledby="govuk-notification-banner-title" data-module="govuk-notification-banner">
<div class="govuk-notification-banner__header">
<h2 class="govuk-notification-banner__title" id="govuk-notification-banner-title">
Important
</h2>
</div>
<div class="govuk-notification-banner__content">
<p class="govuk-notification-banner__heading">
<%= 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.
</p>
<div class="govuk-notification-banner__heading">
<% if @has_matching_claims %>
<p class="govuk-!-margin-0">
<%= 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.
</p>
<% end %>

<% if @claim.attributes_flagged_by_risk_indicator.any? %>
<p class="<%= class_names("govuk-!-margin-0", "govuk-!-margin-top-4" => @has_matching_claims.present?) %>">
This claim has been flagged for fraud prevention as it has a
<%= @claim.attributes_flagged_by_risk_indicator.map(&:humanize).to_sentence.downcase %>
that is present in the fraud risk CSV.
</p>
<% end %>
</div>
</div>
</div>
<% end %>
Expand Down
6 changes: 6 additions & 0 deletions config/analytics_blocklist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,9 @@
- verification
:journeys_sessions:
- answers
:risk_indicators:
- id
- field
- value
- created_at
- updated_at
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 12 additions & 0 deletions db/migrate/20241017160838_create_risk_indicators.rb
Original file line number Diff line number Diff line change
@@ -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
10 changes: 9 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2024_10_15_121145) do
ActiveRecord::Schema[7.0].define(version: 2024_10_17_160838) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_trgm"
Expand Down Expand Up @@ -420,6 +420,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
Expand Down
6 changes: 6 additions & 0 deletions spec/factories/risk_indicators.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FactoryBot.define do
factory :risk_indicator do
field { "MyString" }
value { "MyString" }
end
end
77 changes: 77 additions & 0 deletions spec/features/admin/admin_fraud_prevention_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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

before do
sign_in_as_service_operator
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"
)

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 for fraud prevention as it has a " \
"teacher reference number that is present in the fraud risk CSV."
)

visit admin_claim_tasks_path(flagged_claim_nino)

expect(page).to have_content(
"This claim has been flagged for fraud prevention as it has a " \
"national insurance number that is present in the fraud risk CSV."
)

visit admin_claim_tasks_path(flagged_claim_trn_and_nino)

expect(page).to have_content(
"This claim has been flagged for fraud prevention as it has a " \
"national insurance number and teacher reference number that is " \
"present in the fraud risk CSV."
)
end
end

it "allows for downloading the csv" do
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
end
4 changes: 4 additions & 0 deletions spec/fixtures/files/fraud_risk.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
field,value
national_insurance_number,qq123456c
teacher_reference_number,1234567

5 changes: 5 additions & 0 deletions spec/fixtures/files/fraud_risk_missing_value.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
field,value
national_insurance_number,qq123456c
teacher_reference_number,1234567
teacher_reference_number,

5 changes: 5 additions & 0 deletions spec/fixtures/files/fraud_risk_unknown_attribute.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
field,value
national_insurance_number,qq123456c
teacher_reference_number,1234567
test,1234567
rest,1234567
Loading

0 comments on commit a2e1ee0

Please sign in to comment.