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 24, 2024
1 parent cd54613 commit 7bdfa3a
Show file tree
Hide file tree
Showing 21 changed files with 521 additions and 2 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
57 changes: 57 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,57 @@
module Admin
class FraudRiskCsvUploadForm
include ActiveModel::Model

attr_accessor :file

validates :file, presence: true

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

validate :all_records_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!)
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

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
end
end
6 changes: 5 additions & 1 deletion app/models/claim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,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?
Expand Down 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).compact
end
end
1 change: 1 addition & 0 deletions app/views/admin/claims/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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" %>

Expand Down
15 changes: 15 additions & 0 deletions app/views/admin/decisions/_decision_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@
</div>
<% end %>

<% if claim.attributes_flagged_by_risk_indicator.any? %>
<div class="govuk-warning-text">
<span class="govuk-warning-text__icon" aria-hidden="true">!</span>
<strong class="govuk-warning-text__text">
<span class="govuk-visually-hidden">Warning</span>
<p class="govuk-!-margin-top-0">
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.
</p>
</strong>
</div>
<% end %>

<%= form_for decision, url: admin_claim_decisions_path(claim), html: { id: "claim_decision_form" } do |form| %>
<%= hidden_field_tag :qa, params[:qa] %>

Expand Down
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>
15 changes: 15 additions & 0 deletions app/views/admin/tasks/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@
</div>
<% end %>

<% if @claim.attributes_flagged_by_risk_indicator.any? %>
<div class="govuk-warning-text">
<span class="govuk-warning-text__icon" aria-hidden="true">!</span>
<strong class="govuk-warning-text__text">
<span class="govuk-visually-hidden">Warning</span>
<p class="govuk-!-margin-top-0">
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.
</p>
</strong>
</div>
<% end %>

<div class="govuk-grid-row">
<%= render claim_summary_view, claim: @claim %>
</div>
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_17_113701) 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 @@ -421,6 +421,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 { "teacher_reference_number" }
value { "1234567" }
end
end
Loading

0 comments on commit 7bdfa3a

Please sign in to comment.