Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CPDLP-3586] NPQ Internal API - Implement DQT API calls in NPQ #1753

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ TRA_OIDC_REDIRECT_URI=http://localhost:3000/users/auth/tra_openid_connect/callba

QUALIFIED_TEACHERS_API_URL="https://qualified-teachers-api.example.com"
QUALIFIED_TEACHERS_API_KEY="some-apikey-guid"

DQT_API_URL="https://preprod.teacher-qualifications-api.education.gov.uk"
DQT_API_KEY="preprod-apikey"
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ ECF_APP_BASE_URL="https://ecf-app.gov.uk"
ECF_APP_BEARER_TOKEN="ECFAPPBEARERTOKEN"
QUALIFIED_TEACHERS_API_URL="https://qualified-teachers-api.example.com"
QUALIFIED_TEACHERS_API_KEY="some-apikey-guid"
DQT_API_URL="https://dqt-api.example.com"
DQT_API_KEY="test-apikey"
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
32 changes: 32 additions & 0 deletions app/services/dqt/v1/teacher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module Dqt
module V1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ticket mentions using v3 instead of v3, did something change/is this something different?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we discussed this with the TRA team and unfortunately V3 is missing some features so we had to go back to V1 for now. Since this will all be deprecated soon they agreed for us to go with V1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, we had a meeting about this and found v3 doesn't include some things we need. We're sticking with v1 for migration, and likely replace all this in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should have refreshed first lol

class Teacher
include HTTParty

format :json
base_uri ENV["DQT_API_URL"]
headers "Authorization" => "Bearer #{ENV["DQT_API_KEY"]}"

def self.find(trn:, birthdate:, nino: nil)
path = "/v1/teachers/#{trn}"
query = {
birthdate:,
}
query[:nino] = nino if nino

response = get(path, query:)

if response.success?
response.slice(
"trn",
"state_name",
"name",
"dob",
"ni_number",
"active_alert",
)
end
end
end
end
end
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
39 changes: 26 additions & 13 deletions app/services/participant_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,10 @@ def initialize(trn:, full_name:, date_of_birth:, national_insurance_number: nil)
end

def call
return if Feature.ecf_api_disabled?

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{config.bearer_token}"
request.set_form_data(payload)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl?, read_timeout: 20) do |http|
http.request(request)
end

if response.code == "404"
nil
if Feature.ecf_api_disabled?
call_with_dqt
else
OpenStruct.new(JSON.parse(response.body)["data"]["attributes"])
call_with_ecf
end
end

Expand Down Expand Up @@ -50,4 +40,27 @@ def use_ssl?
def dob_as_string
date_of_birth.iso8601
end

def call_with_dqt
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
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{config.bearer_token}"
request.set_form_data(payload)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl?, read_timeout: 20) do |http|
http.request(request)
end

if response.code == "404"
nil
else
OpenStruct.new(JSON.parse(response.body)["data"]["attributes"])
end
end
end
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
Loading
Loading