Skip to content
Merged
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
4 changes: 4 additions & 0 deletions app/services/proofing/aamva/applicant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ module Aamva
:state_id_number,
:state_id_jurisdiction,
:state_id_type,
:state_id_issued,
:state_id_expiration,
keyword_init: true,
).freeze

Expand Down Expand Up @@ -64,6 +66,8 @@ def self.from_proofer_applicant(applicant)
state_id_number: applicant.dig(:state_id_number)&.gsub(/[^\w\d]/, ''),
state_id_jurisdiction: applicant[:state_id_jurisdiction],
state_id_type: applicant[:state_id_type],
state_id_issued: applicant[:state_id_issued],
state_id_expiration: applicant[:state_id_expiration],
)
end
end.freeze
Expand Down
52 changes: 33 additions & 19 deletions app/services/proofing/aamva/proofer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ class Proofer
],
).freeze

ADDRESS_ATTRIBUTES = [
:address1,
:address2,
:city,
:state,
:zipcode,
].to_set.freeze

OPTIONAL_ADDRESS_ATTRIBUTES = [:address2].freeze

REQUIRED_ADDRESS_ATTRIBUTES = (ADDRESS_ATTRIBUTES - OPTIONAL_ADDRESS_ATTRIBUTES).freeze

attr_reader :config

# Instance methods
Expand All @@ -31,6 +43,7 @@ def initialize(config)
def proof(applicant)
aamva_applicant =
Aamva::Applicant.from_proofer_applicant(OpenStruct.new(applicant))

response = Aamva::VerificationClient.new(
config,
).send_verification_request(
Expand All @@ -55,6 +68,7 @@ def build_result_from_response(verification_response)
exception: nil,
vendor_name: 'aamva:state_id',
transaction_id: verification_response.transaction_locator_id,
requested_attributes: requested_attributes(verification_response).index_with(1),
verified_attributes: verified_attributes(verification_response),
)
end
Expand All @@ -73,30 +87,30 @@ def parse_verification_errors(verification_response)
errors
end

def verified_attributes(verification_response)
attributes = Set.new
results = verification_response.verification_results

attributes.add :address if address_verified?(results)
def requested_attributes(verification_response)
attributes = verification_response.
verification_results.filter { |_, verified| !verified.nil? }.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this intended to reflect the requested attributes based on what we see from the response object? The response object iterates over all possible attributes so it will include attributes even if we did not request them:

VERIFICATION_ATTRIBUTES_MAP.each_pair do |match_indicator_name, attribute_name|
attribute_node = node_for_match_indicator(match_indicator_name)
if attribute_node.nil?
handle_missing_attribute(attribute_name)
elsif attribute_node.text == 'true'
verification_results[attribute_name] = true
else
verification_results[attribute_name] = false
end
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's what the filter is about -- verification_results will have all attributes present, but any missing attributes will have nil values

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That should work for this issue. 2 things to keep in mind:

  1. When AAMVA fails to match a DLN it returns a false match indicator for DLN and no match indicator for other attributes. In that case it will appear that we only requested DLN even when we sent a request for all of the PII including issue and expiration date
  2. Cloudwatch has a tough time handling array parameters and parsing them into something useful. If we could find a way to return keys/values here that may be easier to query when we are trying to visualize this. The verified_attributes value is an array so we can use it in get-to-yes IIRC

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ooh, I did not know that about DLN. It would be a little more work to look at the request itself (currently the proofer just does client.send_verification_request, and doesn't actually get a copy of the request data).

I can change requested_attributes to a hash no prob, I might poke around and see if it is worth doing verified_attributes as well

keys.
to_set

results.delete :address1
results.delete :address2
results.delete :city
results.delete :state
results.delete :zipcode
normalize_address_attributes(attributes)
end

results.each do |attribute, verified|
attributes.add attribute if verified
end
def verified_attributes(verification_response)
attributes = verification_response.
verification_results.filter { |_, verified| verified }.
keys.
to_set

attributes
normalize_address_attributes(attributes)
end

def address_verified?(results)
results[:address1] &&
results[:city] &&
results[:state] &&
results[:zipcode]
def normalize_address_attributes(attribute_set)
all_present = REQUIRED_ADDRESS_ATTRIBUTES & attribute_set == REQUIRED_ADDRESS_ATTRIBUTES

(attribute_set - ADDRESS_ATTRIBUTES).tap do |result|
result.add(:address) if all_present
end
end

def send_to_new_relic(result)
Expand Down
53 changes: 31 additions & 22 deletions app/services/proofing/aamva/request/verification_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,34 +62,43 @@ def add_user_provided_data_to_body
user_provided_data_map.each do |xpath, data|
REXML::XPath.first(document, xpath).add_text(data)
end
add_street_address_line_2_to_rexml_document(document) if applicant.address2.present?

add_optional_element(
'ns2:AddressDeliveryPointText',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, is ns2 for optional attributes, while ns1 is for required? or...?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The AAMVA SOAP request template we use includes 4 namespaces:

Abbrev URL
xmlns:soap http://www.w3.org/2003/05/soap-envelope
xmlns:ns http://aamva.org/dldv/wsdl/2.1
xmlns:ns1 http://aamva.org/niem/extensions/1.0
xmlns:ns2 http://niem.gov/niem/niem-core/2.0

we could probably clean up those ns* abbreviations to be more like what they are.

value: applicant.address2,
document:,
after: '//ns1:Address/ns2:AddressDeliveryPointText',
)

add_optional_element(
'ns2:DriverLicenseIssueDate',
value: applicant.state_id_data.state_id_issued,
document:,
inside: '//ns:verifyDriverLicenseDataRequest',
)

add_optional_element(
'ns2:DriverLicenseExpirationDate',
value: applicant.state_id_data.state_id_expiration,
document:,
inside: '//ns:verifyDriverLicenseDataRequest',
)

@body = document.to_s
end

def add_street_address_line_2_to_rexml_document(document)
old_address_node = document.delete_element('//ns1:Address')
new_address_node = old_address_node.clone
old_address_node.children.each do |child_node|
next unless child_node.node_type == :element
def add_optional_element(name, value:, document:, inside: nil, after: nil)
return if value.blank?

new_element = child_node.clone
new_element.add_text(child_node.text)
new_address_node.add_element(new_element)
el = REXML::Element.new(name)
el.text = value

if child_node.name == 'AddressDeliveryPointText'
new_address_node.add_element(address_line_2_element)
end
if inside
REXML::XPath.first(document, inside).add_element(el)
elsif after
sibling = REXML::XPath.first(document, after)
sibling.parent.insert_after(sibling, el)
end
REXML::XPath.first(
document,
'//ns:verifyDriverLicenseDataRequest',
).add_element(new_address_node)
end

def address_line_2_element
element = REXML::Element.new('ns2:AddressDeliveryPointText')
element.add_text(applicant.address2)
element
end

def build_request_body
Expand Down
3 changes: 3 additions & 0 deletions app/services/proofing/aamva/response/verification_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module Aamva
module Response
class VerificationResponse
VERIFICATION_ATTRIBUTES_MAP = {
'DriverLicenseExpirationDateMatchIndicator' => :state_id_expiration,
'DriverLicenseIssueDateMatchIndicator' => :state_id_issued,
'DriverLicenseNumberMatchIndicator' => :state_id_number,
'DocumentCategoryMatchIndicator' => :state_id_type,
'PersonBirthDateMatchIndicator' => :dob,
Expand Down Expand Up @@ -62,6 +64,7 @@ def success?
REQUIRED_VERIFICATION_ATTRIBUTES.each do |verification_attribute|
return false unless verification_results[verification_attribute]
end

true
end

Expand Down
3 changes: 3 additions & 0 deletions app/services/proofing/state_id_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class StateIdResult
:success,
:vendor_name,
:transaction_id,
:requested_attributes,
:verified_attributes

def initialize(
Expand All @@ -19,13 +20,15 @@ def initialize(
exception: nil,
vendor_name: nil,
transaction_id: '',
requested_attributes: [],
verified_attributes: []
)
@success = success
@errors = errors
@exception = exception
@vendor_name = vendor_name
@transaction_id = transaction_id
@requested_attributes = requested_attributes
@verified_attributes = verified_attributes
end

Expand Down
8 changes: 4 additions & 4 deletions spec/jobs/resolution_proofing_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
expect(result_context_stages_state_id[:success]).to eq(true)
expect(result_context_stages_state_id[:timed_out]).to eq(false)
expect(result_context_stages_state_id[:transaction_id]).to eq('1234-abcd-efgh')
expect(result_context_stages_state_id[:verified_attributes]).to eq(
expect(result_context_stages_state_id[:verified_attributes]).to match_array(
%w[address state_id_number state_id_type dob last_name first_name],
)

Expand Down Expand Up @@ -165,7 +165,7 @@
expect(result_context_stages_state_id[:success]).to eq(true)
expect(result_context_stages_state_id[:timed_out]).to eq(false)
expect(result_context_stages_state_id[:transaction_id]).to eq('1234-abcd-efgh')
expect(result_context_stages_state_id[:verified_attributes]).to eq(
expect(result_context_stages_state_id[:verified_attributes]).to match_array(
%w[address state_id_number state_id_type dob last_name first_name],
)

Expand Down Expand Up @@ -249,7 +249,7 @@
# result[:context][:stages][:state_id]
expect(result_context_stages_state_id[:vendor_name]).to eq('aamva:state_id')
expect(result_context_stages_state_id[:success]).to eq(true)
expect(result_context_stages_state_id[:verified_attributes]).to eq(
expect(result_context_stages_state_id[:verified_attributes]).to match_array(
%w[address state_id_number state_id_type dob last_name first_name],
)
end
Expand Down Expand Up @@ -490,7 +490,7 @@
expect(result_context_stages_state_id[:success]).to eq(true)
expect(result_context_stages_state_id[:timed_out]).to eq(false)
expect(result_context_stages_state_id[:transaction_id]).to eq('1234-abcd-efgh')
expect(result_context_stages_state_id[:verified_attributes]).to eq(
expect(result_context_stages_state_id[:verified_attributes]).to match_array(
%w[address state_id_number state_id_type dob last_name first_name],
)

Expand Down
65 changes: 65 additions & 0 deletions spec/services/proofing/aamva/proofer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@
].to_set,
)
end

it 'includes requested_attributes' do
result = subject.proof(state_id_data)
expect(result.requested_attributes).to eq(
{
dob: 1,
state_id_number: 1,
state_id_type: 1,
last_name: 1,
first_name: 1,
address: 1,
},
)
end
end

context 'when verification is unsuccessful' do
Expand Down Expand Up @@ -98,6 +112,20 @@
].to_set,
)
end

it 'includes requested_attributes' do
result = subject.proof(state_id_data)
expect(result.requested_attributes).to eq(
{
dob: 1,
state_id_number: 1,
state_id_type: 1,
last_name: 1,
first_name: 1,
address: 1,
},
)
end
end

context 'when verification attributes are missing' do
Expand Down Expand Up @@ -128,6 +156,43 @@
].to_set,
)
end

it 'includes requested_attributes' do
result = subject.proof(state_id_data)
expect(result.requested_attributes).to eq(
{
state_id_number: 1,
state_id_type: 1,
last_name: 1,
first_name: 1,
address: 1,
},
)
end
end

context 'when issue / expiration present' do
let(:state_id_data) do
{
state_id_number: '1234567890',
state_id_jurisdiction: 'VA',
state_id_type: 'drivers_license',
state_id_issued: '2023-04-05',
state_id_expiration: '2030-01-02',
}
end

it 'includes them' do
expect(Proofing::Aamva::Request::VerificationRequest).to receive(:new).with(
hash_including(
applicant: satisfy do |a|
expect(a.state_id_data.state_id_issued).to eql('2023-04-05')
expect(a.state_id_data.state_id_expiration).to eql('2030-01-02')
end,
),
)
subject.proof(state_id_data)
end
end

context 'when AAMVA throws an exception' do
Expand Down
14 changes: 14 additions & 0 deletions spec/services/proofing/aamva/request/verification_request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@
],
)
end

it 'includes issue date if present' do
applicant.state_id_data.state_id_issued = '2024-05-06'
expect(subject.body).to include(
'<ns2:DriverLicenseIssueDate>2024-05-06</ns2:DriverLicenseIssueDate>',
)
end

it 'includes expiration date if present' do
applicant.state_id_data.state_id_expiration = '2030-01-02'
expect(subject.body).to include(
'<ns2:DriverLicenseExpirationDate>2030-01-02</ns2:DriverLicenseExpirationDate>',
)
end
end

describe '#headers' do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
end
let(:verification_results) do
{
state_id_expiration: nil,
state_id_issued: nil,
state_id_number: true,
state_id_type: true,
dob: true,
Expand Down