Skip to content

Commit

Permalink
Merge pull request #3331 from DFE-Digital/CAPT-1185/flag-known-ninos-…
Browse files Browse the repository at this point in the history
…and-trns

Add fraud check
  • Loading branch information
rjlynch authored Nov 1, 2024
2 parents eb1b25a + 5939d35 commit 8e8c405
Show file tree
Hide file tree
Showing 36 changed files with 920 additions and 22 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
87 changes: 87 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,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
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 @@ -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?
Expand Down Expand Up @@ -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?
Expand Down
4 changes: 4 additions & 0 deletions app/models/policies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
26 changes: 26 additions & 0 deletions app/models/risk_indicator.rb
Original file line number Diff line number Diff line change
@@ -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
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>
Loading

0 comments on commit 8e8c405

Please sign in to comment.