Skip to content

Commit

Permalink
Add duplicate claims check
Browse files Browse the repository at this point in the history
We're soon going to want to generate a report for the ops team of the
duplicate claims. This commit introduces a new claim verifier that
records the duplicate claims, if any.
Once existing duplicates on production have been back filled we can
replace other uses of `MatchingAttributeFinder#matching_claims` with
`claim.duplicates` and also join claims to their duplicates when
generating the duplicate claims report.
  • Loading branch information
rjlynch committed Dec 19, 2024
1 parent 7cc144b commit 07013ac
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module AutomatedChecks
module ClaimVerifiers
class DuplicateClaimsCheck
def initialize(claim:)
@claim = claim
end

def perform
matching_attribute_finder.matching_claims.each do |existing_claim|
if existing_claim.created_at < claim.created_at
original_claim = existing_claim
duplicate_claim = claim
else
original_claim = claim
duplicate_claim = existing_claim
end

unless Claims::ClaimDuplicate.exists?(original_claim: original_claim, duplicate_claim: duplicate_claim)
Claims::ClaimDuplicate.create!(
original_claim: original_claim,
duplicate_claim: duplicate_claim,
matching_attributes: matching_attribute_finder.matching_attributes(existing_claim)
)
end
end
end

private

attr_reader :claim

def matching_attribute_finder
@matching_attribute_finder ||= Claim::MatchingAttributeFinder.new(claim)
end
end
end
end
20 changes: 20 additions & 0 deletions app/models/claim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,26 @@ class Claim < ApplicationRecord
inverse_of: :assigned_claims,
optional: true

has_many :claim_duplicates_as_original_claim,
class_name: "Claims::ClaimDuplicate",
foreign_key: :original_claim_id,
dependent: :destroy

has_many :claim_duplicates_as_duplicate_claim,
class_name: "Claims::ClaimDuplicate",
foreign_key: :duplicate_claim_id,
dependent: :destroy

has_many :duplicates,
through: :claim_duplicates_as_original_claim,
source: :duplicate_claim,
class_name: "Claim"

has_many :originals,
through: :claim_duplicates_as_duplicate_claim,
source: :original_claim,
class_name: "Claim"

enum :payroll_gender, {
dont_know: 0,
female: 1,
Expand Down
5 changes: 5 additions & 0 deletions app/models/claims.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Claims
def self.table_name_prefix
"claims_"
end
end
28 changes: 28 additions & 0 deletions app/models/claims/claim_duplicate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Claims
class ClaimDuplicate < ApplicationRecord
belongs_to :original_claim, class_name: "Claim"
belongs_to :duplicate_claim, class_name: "Claim"

validates :duplicate_claim, uniqueness: {
scope: :original_claim,
message: "has already been registered as a duplicate"
}
validate :claims_are_not_the_same, if: -> { original_claim && duplicate_claim }
validate :original_claim_is_older, if: -> { original_claim && duplicate_claim }
validates :matching_attributes, presence: true

private

def claims_are_not_the_same
return unless original_claim == duplicate_claim

errors.add(:duplicate_claim, "can't be the same as the original claim")
end

def original_claim_is_older
return unless original_claim.created_at > duplicate_claim.created_at

errors.add(:original_claim, "must be older than the duplicate claim")
end
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 @@ -20,7 +20,8 @@ module EarlyCareerPayments
AutomatedChecks::ClaimVerifiers::CensusSubjectsTaught,
AutomatedChecks::ClaimVerifiers::Employment,
AutomatedChecks::ClaimVerifiers::StudentLoanPlan,
AutomatedChecks::ClaimVerifiers::FraudRisk
AutomatedChecks::ClaimVerifiers::FraudRisk,
AutomatedChecks::ClaimVerifiers::DuplicateClaimsCheck
].freeze

POLICY_START_YEAR = AcademicYear.new(2021).freeze
Expand Down
3 changes: 2 additions & 1 deletion app/models/policies/early_years_payments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ module EarlyYearsPayments

VERIFIERS = [
AutomatedChecks::ClaimVerifiers::StudentLoanPlan,
AutomatedChecks::ClaimVerifiers::EarlyYearsPayments::Identity
AutomatedChecks::ClaimVerifiers::EarlyYearsPayments::Identity,
AutomatedChecks::ClaimVerifiers::DuplicateClaimsCheck
]

# Attributes to delete from claims submitted before the current academic
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 @@ -22,7 +22,8 @@ module FurtherEducationPayments
AutomatedChecks::ClaimVerifiers::ProviderVerification,
AutomatedChecks::ClaimVerifiers::Employment,
AutomatedChecks::ClaimVerifiers::StudentLoanPlan,
AutomatedChecks::ClaimVerifiers::FraudRisk
AutomatedChecks::ClaimVerifiers::FraudRisk,
AutomatedChecks::ClaimVerifiers::DuplicateClaimsCheck
]

# Options shown to admins when rejecting a claim
Expand Down
3 changes: 2 additions & 1 deletion app/models/policies/international_relocation_payments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ module InternationalRelocationPayments
extend self

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

ELIGIBILITY_MATCHING_ATTRIBUTES = [["passport_number"]].freeze
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 @@ -10,7 +10,8 @@ module LevellingUpPremiumPayments
AutomatedChecks::ClaimVerifiers::CensusSubjectsTaught,
AutomatedChecks::ClaimVerifiers::Employment,
AutomatedChecks::ClaimVerifiers::StudentLoanPlan,
AutomatedChecks::ClaimVerifiers::FraudRisk
AutomatedChecks::ClaimVerifiers::FraudRisk,
AutomatedChecks::ClaimVerifiers::DuplicateClaimsCheck
].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 @@ -19,7 +19,8 @@ module StudentLoans
AutomatedChecks::ClaimVerifiers::CensusSubjectsTaught,
AutomatedChecks::ClaimVerifiers::Employment,
AutomatedChecks::ClaimVerifiers::StudentLoanAmount,
AutomatedChecks::ClaimVerifiers::FraudRisk
AutomatedChecks::ClaimVerifiers::FraudRisk,
AutomatedChecks::ClaimVerifiers::DuplicateClaimsCheck
].freeze

POLICY_START_YEAR = AcademicYear.new(2013).freeze
Expand Down
33 changes: 33 additions & 0 deletions db/migrate/20241219115223_create_claims_claim_duplicates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class CreateClaimsClaimDuplicates < ActiveRecord::Migration[8.0]
def change
create_table :claims_claim_duplicates, id: :uuid do |t|
t.belongs_to(
:original_claim,
null: false,
foreign_key: {
to_table: :claims
},
type: :uuid
)

t.belongs_to(
:duplicate_claim,
null: false,
foreign_key: {
to_table: :claims
},
type: :uuid
)

t.jsonb :matching_attributes, default: []

t.timestamps
end

add_index(
:claims_claim_duplicates,
[:original_claim_id, :duplicate_claim_id],
unique: true
)
end
end
15 changes: 14 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[8.0].define(version: 2024_11_26_105650) do
ActiveRecord::Schema[8.0].define(version: 2024_12_19_115223) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_catalog.plpgsql"
Expand Down Expand Up @@ -123,6 +123,17 @@
t.index ["submitted_at"], name: "index_claims_on_submitted_at"
end

create_table "claims_claim_duplicates", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "original_claim_id", null: false
t.uuid "duplicate_claim_id", null: false
t.jsonb "matching_attributes", default: []
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["duplicate_claim_id"], name: "index_claims_claim_duplicates_on_duplicate_claim_id"
t.index ["original_claim_id", "duplicate_claim_id"], name: "idx_on_original_claim_id_duplicate_claim_id_5ce2c01567", unique: true
t.index ["original_claim_id"], name: "index_claims_claim_duplicates_on_original_claim_id"
end

create_table "decisions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.integer "result"
t.uuid "claim_id"
Expand Down Expand Up @@ -585,6 +596,8 @@
add_foreign_key "claim_payments", "claims"
add_foreign_key "claim_payments", "payments"
add_foreign_key "claims", "journeys_sessions"
add_foreign_key "claims_claim_duplicates", "claims", column: "duplicate_claim_id"
add_foreign_key "claims_claim_duplicates", "claims", column: "original_claim_id"
add_foreign_key "decisions", "dfe_sign_in_users", column: "created_by_id"
add_foreign_key "early_career_payments_eligibilities", "schools", column: "current_school_id"
add_foreign_key "eligible_ey_providers", "local_authorities"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require "rails_helper"

RSpec.describe AutomatedChecks::ClaimVerifiers::DuplicateClaimsCheck do
describe "#perform" do
context "when the claim has duplicates" do
it "marks the claim as having duplicates" do
existing_claim = create(
:claim,
:submitted,
email_address: "[email protected]",
first_name: "one"
)

new_claim = create(
:claim,
:submitted,
email_address: "[email protected]",
first_name: "two"
)

described_class.new(claim: new_claim).perform

expect(existing_claim.reload.duplicates).to include(new_claim)
expect(new_claim.reload.originals).to include(existing_claim)

claim_duplicate = new_claim.claim_duplicates_as_duplicate_claim.last

expect(claim_duplicate.original_claim).to eq(existing_claim)
expect(claim_duplicate.duplicate_claim).to eq(new_claim)
expect(claim_duplicate.matching_attributes).to eq(["email_address"])
end

it "is idempotent" do
existing_claim = create(
:claim,
:submitted,
email_address: "[email protected]",
first_name: "one"
)

new_claim = create(
:claim,
:submitted,
email_address: "[email protected]",
first_name: "two"
)

Claims::ClaimDuplicate.create!(
original_claim: existing_claim,
duplicate_claim: new_claim,
matching_attributes: ["email_address"]
)

expect { described_class.new(claim: new_claim).perform }.not_to(
change(existing_claim.duplicates, :count)
)
end
end

context "when the claim has no duplicates" do
it "does not mark the claim as having duplicates" do
existing_claim = create(
:claim,
:submitted,
email_address: "[email protected]",
first_name: "one"
)

new_claim = create(
:claim,
:submitted,
email_address: "[email protected]",
first_name: "two"
)

described_class.new(claim: new_claim).perform

expect(existing_claim.reload.duplicates).not_to include(new_claim)
end
end
end
end
Loading

0 comments on commit 07013ac

Please sign in to comment.