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 known 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.

We've added some new functionality to the task/index notification
banner. The banner now supports rendering multiple messages. If there is
a single banner notification is falls back to the original view and
renders a p tag, with multiple notifications it now renders a list.
  • Loading branch information
rjlynch committed Oct 29, 2024
1 parent efd91ca commit c0d68d1
Show file tree
Hide file tree
Showing 30 changed files with 815 additions and 18 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.fetch(:admin_fraud_risk_csv_upload_form, {}).permit(:file)
end
end
end
27 changes: 26 additions & 1 deletion app/controllers/admin/tasks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
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: {message: "CSV file is required"}

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
35 changes: 35 additions & 0 deletions app/models/automated_checks/claim_verifiers/fraud_risk.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion app/models/claim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,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 @@ -455,6 +455,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
3 changes: 2 additions & 1 deletion app/models/policies/early_career_payments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion app/models/policies/further_education_payments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions app/models/policies/international_relocation_payments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ module InternationalRelocationPayments
include BasePolicy
extend self

VERIFIERS = [
AutomatedChecks::ClaimVerifiers::FraudRisk
].freeze

ELIGIBILITY_MATCHING_ATTRIBUTES = [["passport_number"]].freeze
OTHER_CLAIMABLE_POLICIES = []

Expand Down
3 changes: 2 additions & 1 deletion app/models/policies/levelling_up_premium_payments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion app/models/policies/student_loans.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
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>
26 changes: 26 additions & 0 deletions app/views/admin/tasks/_banner.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<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">
<% if messages.many? %>
<h3 class="govuk-notification-banner__heading">
This claim requires the following to be reviewed:
</h3>

<ul>
<% messages.each do |message| %>
<li>
<%= message %>
</li>
<% end %>
</ul>
<% else %>
<p class="govuk-notification-banner__heading">
<%= messages.first %>
</p>
<% end %>
</div>
</div>
Loading

0 comments on commit c0d68d1

Please sign in to comment.