Skip to content

Commit

Permalink
[CPDLP-3586] Migrated over DQTRecordCheck logic to NPQ
Browse files Browse the repository at this point in the history
  • Loading branch information
mooktakim committed Oct 2, 2024
1 parent 86c9bd2 commit 8df2bad
Show file tree
Hide file tree
Showing 12 changed files with 628 additions and 37 deletions.
84 changes: 84 additions & 0 deletions app/services/dqt/record_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module Dqt
class RecordCheck
TITLES = %w[mr mrs miss ms dr prof rev].freeze

CheckResult = Struct.new(
:dqt_record,
:trn_matches,
:name_matches,
:dob_matches,
:nino_matches,
:total_matched,
:failure_reason,
)

def call
check_record
end

private

attr_reader :trn, :nino, :full_name, :date_of_birth, :check_first_name_only

def initialize(trn:, full_name:, date_of_birth:, nino: nil, check_first_name_only: true)
@trn = trn
@full_name = full_name&.strip
@date_of_birth = date_of_birth
@nino = nino
@check_first_name_only = check_first_name_only
end

def dqt_record(padded_trn)
V1::Teacher.find(trn: padded_trn, nino:, birthdate: date_of_birth)
end

def check_record
return check_failure(:trn_and_nino_blank) if trn.blank? && nino.blank?

@trn = "0000001" if trn.blank?

padded_trn = TeacherReferenceNumber.new(trn).formatted_trn
dqt_record = TeacherRecord.new(dqt_record(padded_trn))

return check_failure(:no_match_found) if dqt_record.blank?
return check_failure(:found_but_not_active) unless dqt_record.active?

trn_matches = dqt_record.trn == padded_trn
name_matches = name_matches?(dqt_name: dqt_record.name)
dob_matches = dqt_record.dob == date_of_birth
nino_matches = nino.present? && nino.downcase == dqt_record.ni_number&.downcase

matches = [trn_matches, name_matches, dob_matches, nino_matches].count(true)

if matches >= 3
CheckResult.new(dqt_record, trn_matches, name_matches, dob_matches, nino_matches, matches)
elsif matches < 3 && (trn_matches && trn != "1")
if matches == 2 && !name_matches && check_first_name_only
CheckResult.new(dqt_record, trn_matches, name_matches, dob_matches, nino_matches, matches)
else
# If a participant mistypes their TRN and enters someone else's, we should search by NINO instead
# The API first matches by (mandatory) TRN, then by NINO if it finds no results. This works around that.
@trn = "0000001"
check_record
end
else
# we found a record but not enough matched
check_failure(:no_match_found)
end
end

def name_matches?(dqt_name:)
return false if full_name.blank?
return false if full_name.in?(TITLES)
return false if dqt_name.blank?

NameMatcher.new(full_name, dqt_name, check_first_name_only:).matches?
end

def check_failure(reason)
CheckResult.new(nil, false, false, false, false, 0, reason)
end
end
end
18 changes: 18 additions & 0 deletions app/services/dqt/teacher_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Dqt
class TeacherRecord
include ActiveModel::Model

attr_accessor :trn,
:state_name,
:name,
:dob,
:ni_number,
:active_alert

def active?
state_name == "Active"
end
end
end
11 changes: 9 additions & 2 deletions app/services/dqt/v1/teacher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Teacher
base_uri ENV["DQT_API_URL"]
headers "Authorization" => "Bearer #{ENV["DQT_API_KEY"]}"

def self.validate_trn(trn:, birthdate:, nino: nil)
def self.find(trn:, birthdate:, nino: nil)
path = "/v1/teachers/#{trn}"
query = {
birthdate:,
Expand All @@ -17,7 +17,14 @@ def self.validate_trn(trn:, birthdate:, nino: nil)
response = get(path, query:)

if response.success?
response.slice("trn", "active_alert")
response.slice(
"trn",
"state_name",
"name",
"dob",
"ni_number",
"active_alert",
)
end
end
end
Expand Down
34 changes: 34 additions & 0 deletions app/services/name_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

class NameMatcher
attr_reader :name1, :name2, :check_first_name_only

TITLES = /\A((mr|mrs|miss|ms|dr|prof|rev)\.?)/

def initialize(name1, name2, check_first_name_only: true)
@name1 = name1
@name2 = name2
@check_first_name_only = check_first_name_only
end

def matches?
if check_first_name_only?
first_name(name1).downcase == first_name(name2).downcase
else
strip_title(name1).downcase == strip_title(name2).downcase
end
end

private

alias_method :check_first_name_only?, :check_first_name_only

def first_name(name)
strip_title(name).split(" ").first
end

def strip_title(str)
parts = str.split(" ")
parts.first.downcase =~ TITLES ? parts.drop(1).join(" ") : str
end
end
6 changes: 4 additions & 2 deletions app/services/participant_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ def dob_as_string
end

def call_with_dqt
record = Dqt::V1::Teacher.validate_trn(trn:, birthdate: dob_as_string, nino: national_insurance_number)
OpenStruct.new(record) if record
result = Dqt::RecordCheck.new(**payload.merge(check_first_name_only: true)).call
if result.total_matched >= 3
result.dqt_record
end
end

def call_with_ecf
Expand Down
36 changes: 36 additions & 0 deletions app/services/teacher_reference_number.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

class TeacherReferenceNumber
MIN_UNPADDED_TRN_LENGTH = 5
PADDED_TRN_LENGTH = 7

attr_reader :trn, :format_error

def initialize(trn)
@trn = trn
@format_error = nil
end

def formatted_trn
@formatted_trn ||= extract_trn_value
end

def valid?
formatted_trn.present?
end

private

def extract_trn_value
@format_error = :blank and return if trn.blank?

# remove any characters that are not digits
only_digits = trn.to_s.gsub(/[^\d]/, "")

@format_error = :invalid and return if only_digits.blank?
@format_error = :too_short and return if only_digits.length < MIN_UNPADDED_TRN_LENGTH
@format_error = :too_long and return if only_digits.length > PADDED_TRN_LENGTH

only_digits.rjust(PADDED_TRN_LENGTH, "0")
end
end
60 changes: 31 additions & 29 deletions spec/lib/services/participant_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,45 +117,47 @@
end

context "when ecf_api_disabled is true" do
let(:total_matched) { 3 }
let(:body) do
{
"trn" => trn,
"active_alert" => true,
}
end
let(:dqt_result) do
OpenStruct.new(
dqt_record: Dqt::TeacherRecord.new(body),
total_matched:,
)
end

before do
allow(Feature).to receive(:ecf_api_disabled?).and_return(true)

allow(Dqt::V1::Teacher).to receive(:validate_trn)
.with(trn:, birthdate: date_of_birth.iso8601, nino: national_insurance_number)
.and_return(body)
end
service = instance_double(Dqt::RecordCheck)
allow(service).to receive(:call).and_return(dqt_result)

context "when matching trn found" do
let(:body) do
{
"trn" => trn,
"active_alert" => false,
}
end

it "returns record with matching trn" do
expect(subject.trn).to eq(trn)
expect(subject.active_alert).to be(false)
end
allow(Dqt::RecordCheck).to receive(:new)
.with(
trn:,
full_name:,
date_of_birth: date_of_birth.iso8601,
nino: national_insurance_number,
check_first_name_only: true,
).and_return(service)
end

context "when different trn found by fuzzy matching" do
let(:body) do
{
"trn" => (trn.to_i + 1).to_s,
"active_alert" => false,
}
end
context "when total_matched is 3" do
let(:total_matched) { 3 }

it "returns record with diffrent trn" do
expect(subject.trn).to be_present
expect(subject.trn).not_to eql(trn)
expect(subject.active_alert).to be_falsey
it "returns teacher record" do
expect(subject.trn).to eq(trn)
expect(subject.active_alert).to be(true)
end
end

context "when no record could be found" do
let(:body) { nil }
context "when total_matched is 2" do
let(:total_matched) { 2 }

it "returns nil" do
expect(subject).to be_nil
Expand Down
Loading

0 comments on commit 8df2bad

Please sign in to comment.