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
204 changes: 204 additions & 0 deletions app/services/usps_in_person_proofer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
class UspsInPersonProofer
attr_reader :token, :token_expires_at

PostOffice = Struct.new(:distance, :address, :city, :phone, :name, :zip_code, :state)

# Makes a request to retrieve a new OAuth token
# and modifies self to store the token and when
# it expires (15 minutes).
# @return [String] the token
def retrieve_token!
body = request_token
@token_expires_at = Time.zone.now + body['expires_in']
@token = "#{body['token_type']} #{body['access_token']}"
end

def token_valid?
@token.present? && @token_expires_at.present? && @token_expires_at.future?
end

# Makes HTTP request to authentication endpoint
# and modifies self to store the token and when
# it expires (15 minutes).
# @return [Hash] API response
def request_token
url = "#{root_url}/oauth/authenticate"
body = {
username: AppConfig.env.usps_ipp_username,
password: AppConfig.env.usps_ipp_password,
grant_type: 'implicit',
response_type: 'token',
client_id: '424ada78-62ae-4c53-8e3a-0b737708a9db',
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.

can we make this a config as well?

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.

I don't think it changes as I understand it. Any IPP API will use that client_id.

scope: 'ivs.ippaas.apis',
}.to_json
resp = faraday.post(url, body, request_headers)

if resp.success?
JSON.parse(resp.body)
else
{ error: 'Failed to get token', response: resp }
end
end

# Makes HTTP request to get nearby in-person proofing facilities
# Requires address, city, state and zip code.
# The PostOffice objects have a subset of the fields
# returned by the API.
# @param location [Object]
# @return [Array<PostOffice>] Facility locations
def request_facilities(location)
url = "#{root_url}/ivs-ippaas-api/IPPRest/resources/rest/getIppFacilityList"
body = {
sponsorID: sponsor_id,
streetAddress: location.address,
city: location.city,
state: location.state,
zipCode: location.zip_code,
}.to_json

headers = request_headers.merge(
'Authorization' => @token,
'RequestID' => request_id,
)

resp = faraday.post(url, body, headers)

if resp.success?
JSON.parse(resp.body)['postOffices'].map do |post_office|
PostOffice.new(
post_office['distance'],
post_office['streetAddress'],
post_office['city'],
post_office['phone'],
post_office['name'],
post_office['zip5'],
post_office['state'],
Comment on lines +68 to +75
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.

I'd recommend keyword_init: true on the struct so we don't have to worry about ordering changes:

PostOffice.new(
  distance: post_office['distance'],
  address: post_office['street_address'],
  # etc ...

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.

ahh right, opened #4526 to fix this

)
end
else
{ error: 'failed to get facilities', response: resp }
end
end

# Makes HTTP request to enroll an applicant in in-person proofing.
# Requires first name, last name, address, city, state, zip code, email address and a generated
# unique ID. The unique ID must be no longer than 18 characters.
# USPS sends an email to the email address with instructions and the enrollment code.
# The API response also includes the enrollment code which should be
# stored with the unique ID to be able to request the status of proofing.
# @param applicant [Object]
# @return [Hash] API response
def request_enroll(applicant)
url = "#{root_url}/ivs-ippaas-api/IPPRest/resources/rest/optInIPPApplicant"
body = {
sponsorID: sponsor_id,
uniqueID: applicant.unique_id,
firstName: applicant.first_name,
lastName: applicant.last_name,
streetAddress: applicant.address,
city: applicant.city,
state: applicant.state,
zipCode: applicant.zip_code,
emailAddress: applicant.email,
IPPAssuranceLevel: '1.5',
}.to_json

headers = request_headers.merge({
'Authorization' => @token,
'RequestID' => request_id,
})

resp = faraday.post(url, body, headers)

if resp.success?
JSON.parse(resp.body)
else
{ error: 'failed to get enroll', response: resp }
end
end

# Makes HTTP request to retrieve proofing status
# Requires the applicant's enrollment code and unique ID.
# When proofing is complete the API returns 200 status.
# If the applicant has not been to the post office, has proofed recently,
# or there is another issue, the API returns a 400 status with an error message.
# @param unique_id [String]
# @param enrollment_code [String]
# @return [Hash] API response
def request_proofing_results(unique_id, enrollment_code)
url = "#{root_url}/ivs-ippaas-api/IPPRest/resources/rest/getProofingResults"
body = {
sponsorID: sponsor_id,
uniqueID: unique_id,
enrollmentCode: enrollment_code,
}.to_json

headers = request_headers.merge({
'Authorization' => @token,
'RequestID' => request_id,
})

resp = faraday.post(url, body, headers)

if resp.success?
JSON.parse(resp.body)
elsif resp.status == 400 && resp.headers['content-type'] == 'application/json'
JSON.parse(resp.body)
else
{ error: 'failed to get proofing results', response: resp }
end
end

# Makes HTTP request to retrieve enrollment code
# If an applicant has a currently valid enrollment code, it will be returned.
# If they do not, a new one will be generated and returned. USPS sends the applicant an email with
# instructions and the enrollment code.
# Requires the applicant's unique ID.
# @param unique_id [String]
# @return [Hash] API response
def request_enrollment_code(unique_id)
url = "#{root_url}/ivs-ippaas-api/IPPRest/resources/rest/requestEnrollmentCode"
body = {
sponsorID: sponsor_id,
uniqueID: unique_id,
}.to_json

headers = request_headers.merge({
'Authorization' => @token,
'RequestID' => request_id,
})

resp = faraday.post(url, body, headers)

if resp.success?
JSON.parse(resp.body)
else
resp
end
end

def root_url
AppConfig.env.usps_ipp_root_url
end

def sponsor_id
AppConfig.env.usps_ipp_sponsor_id.to_i
end

def request_id
SecureRandom.uuid
end

def faraday
Faraday.new do |conn|
conn.options.timeout = 10
conn.options.read_timeout = 10
conn.options.open_timeout = 10
conn.options.write_timeout = 10
end
end

def request_headers
{ 'Content-Type' => 'application/json; charset=utf-8' }
end
end
4 changes: 4 additions & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ usps_download_sftp_timeout: '5'
usps_upload_enabled: 'false'
usps_upload_sftp_timeout: '5'
valid_authn_contexts: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3", "http://idmanagement.gov/ns/assurance/ial/1", "http://idmanagement.gov/ns/assurance/ial/2", "http://idmanagement.gov/ns/assurance/ial/0", "http://idmanagement.gov/ns/assurance/ial/2?strict=true", "urn:gov:gsa:ac:classes:sp:PasswordProtectedTransport:duo", "http://idmanagement.gov/ns/assurance/aal/2", "http://idmanagement.gov/ns/assurance/aal/3", "http://idmanagement.gov/ns/assurance/aal/3?hspd12=true"]'
usps_ipp_password: ''
usps_ipp_root_url: ''
usps_ipp_sponsor_id: ''
usps_ipp_username: ''

development:
aal_authn_context_enabled: 'true'
Expand Down
4 changes: 4 additions & 0 deletions spec/fixtures/usps_ipp_responses/request_enroll_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"enrollmentCode": "2048702198804353",
"responseMessage": "Applicant 123456789 successfully processed"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"enrollmentCode": "2048702198804358",
"responseMessage": "Applicant 123456789 successfully processed"
}
Loading