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
21 changes: 21 additions & 0 deletions app/services/proofing/aamva/applicant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ module Aamva
keyword_init: true,
).freeze

# @param applicant [Hash, Struct]
# @option applicant [String, nil] :uuid
# @option applicant [String, nil] :first_name
# @option applicant [String, nil] :middle_name
# @option applicant [String, nil] :last_name
# @option applicant [String, nil] :name_suffix
# @option applicant [String, nil] :dob
# @option applicant [String, nil] :sex
# @option applicant [Integer, nil] :height in inches
# @option applicant [String, nil] :weight
# @option applicant [String, nil] :eye_color
# @option applicant [String, nil] :address1
# @option applicant [String, nil] :address2
# @option applicant [String, nil] :city
# @option applicant [String, nil] :state
# @option applicant [String, nil] :zipcode
# @option applicant [String, nil] :state_id_number
# @option applicant [String, nil] :state_id_jurisdiction
# @option applicant [String, nil] :state_id_type
# @option applicant [String, nil] :state_id_issued
# @option applicant [String, nil] :state_id_expiration
# @return [Applicant]
def self.from_proofer_applicant(applicant)
new(
Expand Down
52 changes: 34 additions & 18 deletions app/services/proofing/aamva/proofer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,36 +49,42 @@ def initialize(config)
@config = Config.new(config)
end

# @param applicant [Hash]
def proof(applicant)
aamva_applicant = Aamva::Applicant.from_proofer_applicant(applicant)
verification_request = build_verification_request(aamva_applicant)
verification_response = verification_request.send
jurisdiction = applicant[:state_id_jurisdiction]

response = Aamva::VerificationClient.new(
config,
).send_verification_request(
applicant: aamva_applicant,
)

build_result_from_response(response, applicant[:state_id_jurisdiction])
build_result(verification_request:, verification_response:, jurisdiction:)
rescue => exception
Proofing::StateIdResult.new(
success: false, errors: {}, exception: exception, vendor_name: 'aamva:state_id',
transaction_id: nil, verified_attributes: [],
jurisdiction_in_maintenance_window: jurisdiction_in_maintenance_window?(
applicant[:state_id_jurisdiction],
)
success: false,
errors: {},
exception: exception,
vendor_name: 'aamva:state_id',
transaction_id: nil,
verified_attributes: [],
requested_attributes: requested_attributes(verification_request),
jurisdiction_in_maintenance_window: jurisdiction_in_maintenance_window?(jurisdiction),
)
end

private

def build_result_from_response(verification_response, jurisdiction)
def build_verification_request(applicant)
Aamva::VerificationClient.new(config)
.build_verification_request(applicant:)
end

def build_result(verification_request:, verification_response:, jurisdiction:)
Proofing::StateIdResult.new(
success: successful?(verification_response),
errors: parse_verification_errors(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),
requested_attributes: requested_attributes(verification_request),
verified_attributes: verified_attributes(verification_response),
jurisdiction_in_maintenance_window: jurisdiction_in_maintenance_window?(jurisdiction),
)
Expand All @@ -98,13 +104,23 @@ def parse_verification_errors(verification_response)
errors
end

def requested_attributes(verification_response)
attributes = verification_response
.verification_results.filter { |_, verified| !verified.nil? }
# @param verification_request [Proofing::Aamva::Request::VerificationRequest]
def requested_attributes(verification_request)
return if verification_request.nil?
present_attributes = verification_request
.requested_attributes
.compact
.filter { |_k, v| v == :present }
.keys
.to_set

normalize_address_attributes(attributes)
blank_attributes = verification_request
.requested_attributes
.filter { |_k, v| v == :missing }
.transform_values { |_v| 0 }

normalized = normalize_address_attributes(present_attributes).index_with(1)
normalized.merge(blank_attributes)
end

def verified_attributes(verification_response)
Expand Down
110 changes: 81 additions & 29 deletions app/services/proofing/aamva/request/verification_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,53 @@
module Proofing
module Aamva
module Request
RequestAttribute = Data.define(:xpath, :required).freeze
class VerificationRequest
CONTENT_TYPE = 'application/soap+xml;charset=UTF-8'
DEFAULT_VERIFICATION_URL =
'https://verificationservices-cert.aamva.org:18449/dldv/2.1/online'
SOAP_ACTION =
'"http://aamva.org/dldv/wsdl/2.1/IDLDVService21/VerifyDriverLicenseData"'

VERIFICATION_REQUESTED_ATTRS = {
first_name: RequestAttribute.new(xpath: '//nc:PersonGivenName', required: true),
middle_name: RequestAttribute.new(xpath: '//nc:PersonMiddleName', required: false),
last_name: RequestAttribute.new('//nc:PersonSurName', true),
name_suffix: RequestAttribute.new('//nc:PersonNameSuffixText', false),
dob: RequestAttribute.new('//aa:PersonBirthDate', true),
address1: RequestAttribute.new('//nc:AddressDeliveryPointText', true),
address2: RequestAttribute.new('//nc:AddressDeliveryPointText[2]', false),
city: RequestAttribute.new('//nc:LocationCityName', true),
state: RequestAttribute.new('//nc:LocationStateUsPostalServiceCode', true),
zipcode: RequestAttribute.new('//nc:LocationPostalCode', true),
state_id_number: RequestAttribute.new('//nc:IdentificationID', true),
state_id_type: RequestAttribute.new('//aa:DocumentCategoryCode', false),
state_id_expiration: RequestAttribute.new('//aa:DriverLicenseExpirationDate', false),
state_id_jurisdiction: RequestAttribute.new('//aa:MessageDestinationId', true),
state_id_issued: RequestAttribute.new('//aa:DriverLicenseIssueDate', false),
eye_color: RequestAttribute.new('//aa:PersonEyeColorCode', false),
height: RequestAttribute.new('//aa:PersonHeightMeasure', false),
sex: RequestAttribute.new('//aa:PersonSexCode', false),
weight: RequestAttribute.new('//aa:PersonWeightMeasure', false),
}.freeze

extend Forwardable

attr_reader :config, :body, :headers, :url

# @param applicant [Proofing::Aamva::Applicant]
def initialize(config:, applicant:, session_id:, auth_token:)
@config = config
@applicant = applicant
@transaction_id = session_id
@auth_token = auth_token
@requested_attributes = {}
@url = verification_url
@body = build_request_body
@headers = build_request_headers
end

# @return [Proofing::Aamva::Response::VerificationResponse]
def send
Response::VerificationResponse.new(
http_client.post(url, body, headers) do |req|
Expand All @@ -46,9 +72,21 @@ def verification_url
config.verification_url || DEFAULT_VERIFICATION_URL
end

# The requested attributes in the applicant PII hash. Values are:
# - +:present+ - value present
# - +:missing+ - field is required, but value was blank
#
# @see Proofing::Aamva::Applicant#from_proofer_applicant for fields
# @return [Hash{Symbol => Symbol}]
def requested_attributes
{ **@requested_attributes }
end

private

attr_reader :applicant, :transaction_id, :auth_token
# @return [Proofing::Aamva::Applicant]
attr_reader :applicant
attr_reader :transaction_id, :auth_token

def http_client
Faraday.new(request: { open_timeout: timeout, timeout: timeout }) do |faraday|
Expand All @@ -57,9 +95,10 @@ def http_client
end
end

def add_user_provided_data_to_body
def add_user_provided_data_to_body(body)
document = REXML::Document.new(body)
user_provided_data_map.each do |xpath, data|
user_provided_data_map.each do |attribute, data|
xpath = VERIFICATION_REQUESTED_ATTRS[attribute].xpath
REXML::XPath.first(document, xpath).add_text(data)
end

Expand Down Expand Up @@ -126,17 +165,18 @@ def add_user_provided_data_to_body
document,
)

@body = document.to_s
update_requested_attributes(document)
document.to_s
end

def add_state_id_type(id_type, document)
category_code = case id_type
when 'drivers_license'
1
when 'drivers_permit'
2
when 'state_id_card'
3
when 'drivers_license'
1
when 'drivers_permit'
2
when 'state_id_card'
3
end

if category_code
Expand All @@ -151,10 +191,10 @@ def add_state_id_type(id_type, document)

def add_sex_code(sex_value, document)
sex_code = case sex_value
when 'male'
1
when 'female'
2
when 'male'
1
when 'female'
2
end

if sex_code
Expand All @@ -181,10 +221,22 @@ def add_optional_element(name, value:, document:, inside: nil, after: nil)
end
end

# @param document [REXML::Document]
def update_requested_attributes(document)
VERIFICATION_REQUESTED_ATTRS.each do |attribute, rule|
Copy link
Contributor

Choose a reason for hiding this comment

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

This is good, I like that we are sourcing this from the actual XML we are building.

value = REXML::XPath.first(document, rule.xpath)&.text
if value.present?
@requested_attributes[attribute] = :present
elsif rule.required
@requested_attributes[attribute] = :missing
end
end
end

def build_request_body
renderer = ERB.new(request_body_template)
@body = renderer.result(binding)
add_user_provided_data_to_body
tmp_body = renderer.result(binding)
add_user_provided_data_to_body(tmp_body)
end

def build_request_headers
Expand Down Expand Up @@ -222,24 +274,24 @@ def transaction_locator_id

def user_provided_data_map
{
'//nc:IdentificationID' => state_id_number,
'//aa:MessageDestinationId' => message_destination_id,
'//nc:PersonGivenName' => applicant.first_name,
'//nc:PersonSurName' => applicant.last_name,
'//aa:PersonBirthDate' => applicant.dob,
'//nc:AddressDeliveryPointText' => applicant.address1,
'//nc:LocationCityName' => applicant.city,
'//nc:LocationStateUsPostalServiceCode' => applicant.state,
'//nc:LocationPostalCode' => applicant.zipcode,
state_id_number: state_id_number,
state_id_jurisdiction: message_destination_id,
first_name: applicant.first_name,
last_name: applicant.last_name,
dob: applicant.dob,
address1: applicant.address1,
city: applicant.city,
state: applicant.state,
zipcode: applicant.zipcode,
Copy link
Contributor

Choose a reason for hiding this comment

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

👏 Moving xpath selectors out makes this so much more pleasant. 😄

}
end

def state_id_number
case applicant.state_id_data.state_id_jurisdiction
when 'SC'
applicant.state_id_data.state_id_number.rjust(8, '0')
else
applicant.state_id_data.state_id_number
when 'SC'
applicant.state_id_data.state_id_number.rjust(8, '0')
else
applicant.state_id_data.state_id_number
end
end

Expand Down
8 changes: 6 additions & 2 deletions app/services/proofing/aamva/verification_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ def initialize(config)
@config = config
end

def send_verification_request(applicant:, session_id: nil)
def build_verification_request(applicant:, session_id: nil)
Request::VerificationRequest.new(
applicant: applicant,
session_id: session_id,
auth_token: auth_token,
config: config,
).send
)
end

def send_verification_request(applicant:, session_id: nil)
build_verification_request(applicant:, session_id:).send
end

private
Expand Down
Loading