From 6511542b1822f26e6c509428bc147956f491f518 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Fri, 15 Jul 2022 10:22:04 -0700 Subject: [PATCH 01/21] Prepping to create USPS enrollment --- app/decorators/api/user_bundle_decorator.rb | 5 +++++ app/forms/api/profile_creation_form.rb | 9 +++++++++ app/services/idv/session.rb | 1 + app/services/idv/steps/ipp/verify_step.rb | 1 + app/services/idv/user_bundle_tokenizer.rb | 1 + 5 files changed, 17 insertions(+) diff --git a/app/decorators/api/user_bundle_decorator.rb b/app/decorators/api/user_bundle_decorator.rb index c1aaaace8e5..b4c4ec67823 100644 --- a/app/decorators/api/user_bundle_decorator.rb +++ b/app/decorators/api/user_bundle_decorator.rb @@ -21,6 +21,11 @@ def gpo_address_verification? metadata[:address_verification_mechanism] == 'gpo' end + def usps_identity_verification? + binding.pry + metadata[:in_person_proofing] == 'usps' + end + def pii HashWithIndifferentAccess.new(jwt_payload['pii']) end diff --git a/app/forms/api/profile_creation_form.rb b/app/forms/api/profile_creation_form.rb index 9a84ceb3dd4..c46af7a1656 100644 --- a/app/forms/api/profile_creation_form.rb +++ b/app/forms/api/profile_creation_form.rb @@ -54,6 +54,7 @@ def cache_encrypted_pii def complete_session complete_profile if phone_confirmed? create_gpo_entry if user_bundle.gpo_address_verification? + create_usps_enrollment if user_bundle.usps_identity_verification? end def phone_confirmed? @@ -81,6 +82,14 @@ def create_gpo_entry @gpo_code = confirmation_maker.otp if FeatureManagement.reveal_gpo_code? end + def create_usps_enrollment + binding.pry + # create usps proofer + # get token + # create enrollment + # display error banner on failure + end + def build_profile_maker Idv::ProfileMaker.new( applicant: user_bundle.pii, diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 31220460131..887d226e4a3 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -6,6 +6,7 @@ class Session go_back_path idv_phone_step_document_capture_session_uuid idv_gpo_document_capture_session_uuid + in_person_proofing vendor_phone_confirmation user_phone_confirmation pii diff --git a/app/services/idv/steps/ipp/verify_step.rb b/app/services/idv/steps/ipp/verify_step.rb index b3b11b051ce..9b3e0ecf7ee 100644 --- a/app/services/idv/steps/ipp/verify_step.rb +++ b/app/services/idv/steps/ipp/verify_step.rb @@ -8,6 +8,7 @@ def call # WILLFIX: (LG-6498) make a call to Instant Verify before allowing the user to continue save_legacy_state add_proofing_component + idv_session[:in_person_proofing] = 'usps' delete_pii end diff --git a/app/services/idv/user_bundle_tokenizer.rb b/app/services/idv/user_bundle_tokenizer.rb index c077232f137..312bd9a08a1 100644 --- a/app/services/idv/user_bundle_tokenizer.rb +++ b/app/services/idv/user_bundle_tokenizer.rb @@ -33,6 +33,7 @@ def metadata data[:address_verification_mechanism] = idv_session.address_verification_mechanism data[:user_phone_confirmation] = idv_session.user_phone_confirmation data[:vendor_phone_confirmation] = idv_session.vendor_phone_confirmation + data[:in_person_proofing] = idv_session.in_person_proofing data end From 73e9d7bfbfc0619f3732ef1a9fa346241de54f33 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Fri, 15 Jul 2022 10:44:28 -0700 Subject: [PATCH 02/21] Begin calling usps Broken -- tries to call the real USPS which we don't have configured --- app/decorators/api/user_bundle_decorator.rb | 1 - app/forms/api/profile_creation_form.rb | 25 +++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/decorators/api/user_bundle_decorator.rb b/app/decorators/api/user_bundle_decorator.rb index b4c4ec67823..0e9f64c081f 100644 --- a/app/decorators/api/user_bundle_decorator.rb +++ b/app/decorators/api/user_bundle_decorator.rb @@ -22,7 +22,6 @@ def gpo_address_verification? end def usps_identity_verification? - binding.pry metadata[:in_person_proofing] == 'usps' end diff --git a/app/forms/api/profile_creation_form.rb b/app/forms/api/profile_creation_form.rb index c46af7a1656..39ca3ca5846 100644 --- a/app/forms/api/profile_creation_form.rb +++ b/app/forms/api/profile_creation_form.rb @@ -1,4 +1,9 @@ module Api + # todo: move this somewhere else + Applicant = Struct.new( + :unique_id, :first_name, :last_name, :address, :city, :state, :zip_code, + :email, keyword_init: true + ) class ProfileCreationForm include ActiveModel::Model @@ -83,11 +88,27 @@ def create_gpo_entry end def create_usps_enrollment - binding.pry # create usps proofer + proofer = UspsInPersonProofer.new # get token + proofer.retrieve_token! + # create applicant object + applicant = Applicant.new( + { + unique_id: 'test', + first_name: 'mary', + last_name: 'klein', + address: '404 Not Found', + city: 'Hypertext City', + state: 'Undefined', + zip_code: '20002', + email: 'not-used@so-so.com', + }, + ) # create enrollment - # display error banner on failure + response = proofer.request_enroll(applicant) + binding.pry + # todo: display error banner on failure end def build_profile_maker From bdd23ad7d31f87c34e5bb7cfa0bc41cea85d8361 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Fri, 15 Jul 2022 11:32:51 -0700 Subject: [PATCH 03/21] Add mock USPS proofer and create enrollment --- app/forms/api/profile_creation_form.rb | 21 +++++++++++++++++---- config/application.yml.default | 1 + lib/identity_config.rb | 1 + 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/forms/api/profile_creation_form.rb b/app/forms/api/profile_creation_form.rb index 39ca3ca5846..7237147518b 100644 --- a/app/forms/api/profile_creation_form.rb +++ b/app/forms/api/profile_creation_form.rb @@ -1,5 +1,5 @@ module Api - # todo: move this somewhere else + # todo: move this somewhere else, maybe the UspsInPersonProofer Applicant = Struct.new( :unique_id, :first_name, :last_name, :address, :city, :state, :zip_code, :email, keyword_init: true @@ -87,9 +87,17 @@ def create_gpo_entry @gpo_code = confirmation_maker.otp if FeatureManagement.reveal_gpo_code? end + def usps_proofer + if IdentityConfig.store.usps_mock_fallback + MockUspsInPersonProofer.new + else + UspsInPersonProofer.new + end + end + def create_usps_enrollment # create usps proofer - proofer = UspsInPersonProofer.new + proofer = usps_proofer # get token proofer.retrieve_token! # create applicant object @@ -105,9 +113,14 @@ def create_usps_enrollment email: 'not-used@so-so.com', }, ) - # create enrollment + # create enrollment in usps response = proofer.request_enroll(applicant) - binding.pry + # create an enrollment in the db. if this fails we could conveivably retry by querying the enrollment code from the USPS api. So may be create an upsert-like helper function to get an existing enrollment or create one if it doesn't exist + enrollment_code = response['enrollmentCode'] + InPersonEnrollment.create!( + user: profile.user, enrollment_code: enrollment_code, + status: :pending, profile: profile + ) # todo: display error banner on failure end diff --git a/config/application.yml.default b/config/application.yml.default index 82717a47e65..e42219701da 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -334,6 +334,7 @@ development: usps_upload_sftp_host: localhost usps_upload_sftp_password: test usps_upload_sftp_username: brady + usps_mock_fallback: true # These values serve as defaults for all production-like environments, which # includes *.identitysandbox.gov and *.login.gov. diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 4e98fef1dc6..7cbef0819a5 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -338,6 +338,7 @@ def self.build_store(config_map) config.add(:unauthorized_scope_enabled, type: :boolean) config.add(:use_dashboard_service_providers, type: :boolean) config.add(:use_kms, type: :boolean) + config.add(:usps_mock_fallback, type: :boolean) config.add(:usps_confirmation_max_days, type: :integer) config.add(:usps_ipp_password, type: :string) config.add(:usps_ipp_root_url, type: :string) From 3069b5ecd5e6f330c0496b396f33d48db4fa512f Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Fri, 15 Jul 2022 12:01:22 -0700 Subject: [PATCH 04/21] Add mock service --- app/services/mock_usps_in_person_proofer.rb | 126 ++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 app/services/mock_usps_in_person_proofer.rb diff --git a/app/services/mock_usps_in_person_proofer.rb b/app/services/mock_usps_in_person_proofer.rb new file mode 100644 index 00000000000..3a3f02e3442 --- /dev/null +++ b/app/services/mock_usps_in_person_proofer.rb @@ -0,0 +1,126 @@ +class MockUspsInPersonProofer + # todo: update the documentation for these methods + attr_reader :token, :token_expires_at + + PostOffice = Struct.new( + :distance, :address, :city, :phone, :name, :zip_code, :state, keyword_init: true + ) + + # 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 + JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_token_response.json') + 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] Facility locations + def request_facilities(location) + # todo: return constant list + 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) + # todo: return fake results here, including test error cases + JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_enroll_response.json') + 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) + # todo: mock this somehow, maybe not right now though + 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 sponsor_id + IdentityConfig.store.usps_ipp_sponsor_id.to_i + end + + def request_id + SecureRandom.uuid + end +end From 326565489ca0d9350248074fac8f25f6d0d78194 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Mon, 18 Jul 2022 18:39:18 -0700 Subject: [PATCH 05/21] Move usps integration to password controller --- .../api/verify/password_confirm_controller.rb | 41 +++++++++++++++++++ app/forms/api/profile_creation_form.rb | 38 ----------------- app/services/idv/steps/ipp/verify_step.rb | 1 - config/application.yml.default | 2 +- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index eb3d8943d38..9aad4f9e68d 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -10,6 +10,7 @@ def create user = User.find_by(uuid: result.extra[:user_uuid]) add_proofing_component(user) store_session_last_gpo_code(form.gpo_code) + save_in_person_enrollment(user) render json: { personal_key: personal_key, completion_url: completion_url(result, user), @@ -34,6 +35,46 @@ def store_session_last_gpo_code(code) session[:last_gpo_confirmation_code] = code if code && FeatureManagement.reveal_gpo_code? end + def usps_proofer + if IdentityConfig.store.usps_mock_fallback + MockUspsInPersonProofer.new + else + UspsInPersonProofer.new + end + end + + def save_in_person_enrollment(user) + return unless in_person_enrollment?(user) + + # create usps proofer + proofer = usps_proofer + # get token + proofer.retrieve_token! + # create applicant object + applicant = Applicant.new( + { + unique_id: 'test', + first_name: user_session['idv']['pii'].first_name, + last_name: user_session['idv']['pii'].last_name, + address: user_session['idv']['pii'].address1, + # do we need address2? + city: user_session['idv']['pii'].city, + state: user_session['idv']['pii'].state, + zip_code: user_session['idv']['pii'].zipcode, + email: 'not-used@so-so.com', + }, + ) + # create enrollment in usps + response = proofer.request_enroll(applicant) + # create an enrollment in the db. if this fails we could conveivably retry by querying the enrollment code from the USPS api. So may be create an upsert-like helper function to get an existing enrollment or create one if it doesn't exist + enrollment_code = response['enrollmentCode'] + InPersonEnrollment.create!( + user: user, enrollment_code: enrollment_code, + status: :pending, profile: user.active_profile + ) + # todo: display error banner on failure + end + def verify_params params.permit(:password, :user_bundle_token) end diff --git a/app/forms/api/profile_creation_form.rb b/app/forms/api/profile_creation_form.rb index 7237147518b..7e44eb633dd 100644 --- a/app/forms/api/profile_creation_form.rb +++ b/app/forms/api/profile_creation_form.rb @@ -59,7 +59,6 @@ def cache_encrypted_pii def complete_session complete_profile if phone_confirmed? create_gpo_entry if user_bundle.gpo_address_verification? - create_usps_enrollment if user_bundle.usps_identity_verification? end def phone_confirmed? @@ -87,43 +86,6 @@ def create_gpo_entry @gpo_code = confirmation_maker.otp if FeatureManagement.reveal_gpo_code? end - def usps_proofer - if IdentityConfig.store.usps_mock_fallback - MockUspsInPersonProofer.new - else - UspsInPersonProofer.new - end - end - - def create_usps_enrollment - # create usps proofer - proofer = usps_proofer - # get token - proofer.retrieve_token! - # create applicant object - applicant = Applicant.new( - { - unique_id: 'test', - first_name: 'mary', - last_name: 'klein', - address: '404 Not Found', - city: 'Hypertext City', - state: 'Undefined', - zip_code: '20002', - email: 'not-used@so-so.com', - }, - ) - # create enrollment in usps - response = proofer.request_enroll(applicant) - # create an enrollment in the db. if this fails we could conveivably retry by querying the enrollment code from the USPS api. So may be create an upsert-like helper function to get an existing enrollment or create one if it doesn't exist - enrollment_code = response['enrollmentCode'] - InPersonEnrollment.create!( - user: profile.user, enrollment_code: enrollment_code, - status: :pending, profile: profile - ) - # todo: display error banner on failure - end - def build_profile_maker Idv::ProfileMaker.new( applicant: user_bundle.pii, diff --git a/app/services/idv/steps/ipp/verify_step.rb b/app/services/idv/steps/ipp/verify_step.rb index 9b3e0ecf7ee..b3b11b051ce 100644 --- a/app/services/idv/steps/ipp/verify_step.rb +++ b/app/services/idv/steps/ipp/verify_step.rb @@ -8,7 +8,6 @@ def call # WILLFIX: (LG-6498) make a call to Instant Verify before allowing the user to continue save_legacy_state add_proofing_component - idv_session[:in_person_proofing] = 'usps' delete_pii end diff --git a/config/application.yml.default b/config/application.yml.default index e42219701da..b53be727841 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -262,6 +262,7 @@ usps_ipp_root_url: '' usps_ipp_request_timeout: 10 usps_ipp_sponsor_id: '' usps_ipp_username: '' +usps_mock_fallback: true gpo_allowed_for_strict_ial2: true voice_otp_pause_time: '0.5s' voice_otp_speech_rate: 'slow' @@ -334,7 +335,6 @@ development: usps_upload_sftp_host: localhost usps_upload_sftp_password: test usps_upload_sftp_username: brady - usps_mock_fallback: true # These values serve as defaults for all production-like environments, which # includes *.identitysandbox.gov and *.login.gov. From 05dd575cb88ece67a3f99258d9b127273be47031 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Mon, 18 Jul 2022 21:10:15 -0700 Subject: [PATCH 06/21] Use a more unique unique_id --- app/controllers/api/verify/password_confirm_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index 1898cb47190..f8ba7100150 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -53,7 +53,7 @@ def save_in_person_enrollment(user) # create applicant object applicant = Applicant.new( { - unique_id: 'test', + unique_id: user.uuid.delete('-').slice(0, 18), first_name: user_session['idv']['pii'].first_name, last_name: user_session['idv']['pii'].last_name, address: user_session['idv']['pii'].address1, From 0d6020abeb03fbfbdb538cdbc311c298501b8a00 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 14:10:48 -0700 Subject: [PATCH 07/21] Add usps in person proofing directory --- .../api/verify/password_confirm_controller.rb | 4 +- app/forms/api/profile_creation_form.rb | 2 +- app/services/mock_usps_in_person_proofer.rb | 126 ----------- app/services/usps_in_person_proofer.rb | 212 ----------------- app/services/usps_in_person_proofing/mock.rb | 128 +++++++++++ .../usps_in_person_proofing/proofer.rb | 214 ++++++++++++++++++ spec/services/usps_in_person_proofer_spec.rb | 4 +- 7 files changed, 347 insertions(+), 343 deletions(-) delete mode 100644 app/services/mock_usps_in_person_proofer.rb delete mode 100644 app/services/usps_in_person_proofer.rb create mode 100644 app/services/usps_in_person_proofing/mock.rb create mode 100644 app/services/usps_in_person_proofing/proofer.rb diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index f8ba7100150..1adb5b4659a 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -37,9 +37,9 @@ def store_session_last_gpo_code(code) def usps_proofer if IdentityConfig.store.usps_mock_fallback - MockUspsInPersonProofer.new + UspsInPersonProofing::MockProofer.new else - UspsInPersonProofer.new + UspsInPersonProofing::Proofer.new end end diff --git a/app/forms/api/profile_creation_form.rb b/app/forms/api/profile_creation_form.rb index 7e44eb633dd..7963e977744 100644 --- a/app/forms/api/profile_creation_form.rb +++ b/app/forms/api/profile_creation_form.rb @@ -1,5 +1,5 @@ module Api - # todo: move this somewhere else, maybe the UspsInPersonProofer + # todo: move this somewhere else, maybe the UspsInPersonProofing::Proofer Applicant = Struct.new( :unique_id, :first_name, :last_name, :address, :city, :state, :zip_code, :email, keyword_init: true diff --git a/app/services/mock_usps_in_person_proofer.rb b/app/services/mock_usps_in_person_proofer.rb deleted file mode 100644 index 3a3f02e3442..00000000000 --- a/app/services/mock_usps_in_person_proofer.rb +++ /dev/null @@ -1,126 +0,0 @@ -class MockUspsInPersonProofer - # todo: update the documentation for these methods - attr_reader :token, :token_expires_at - - PostOffice = Struct.new( - :distance, :address, :city, :phone, :name, :zip_code, :state, keyword_init: true - ) - - # 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 - JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_token_response.json') - 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] Facility locations - def request_facilities(location) - # todo: return constant list - 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) - # todo: return fake results here, including test error cases - JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_enroll_response.json') - 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) - # todo: mock this somehow, maybe not right now though - 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 sponsor_id - IdentityConfig.store.usps_ipp_sponsor_id.to_i - end - - def request_id - SecureRandom.uuid - end -end diff --git a/app/services/usps_in_person_proofer.rb b/app/services/usps_in_person_proofer.rb deleted file mode 100644 index b0716d4b2de..00000000000 --- a/app/services/usps_in_person_proofer.rb +++ /dev/null @@ -1,212 +0,0 @@ -class UspsInPersonProofer - attr_reader :token, :token_expires_at - - PostOffice = Struct.new( - :distance, :address, :city, :phone, :name, :zip_code, :state, keyword_init: true - ) - - # 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: IdentityConfig.store.usps_ipp_username, - password: IdentityConfig.store.usps_ipp_password, - grant_type: 'implicit', - response_type: 'token', - client_id: '424ada78-62ae-4c53-8e3a-0b737708a9db', - 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] 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( - distance: post_office['distance'], - address: post_office['streetAddress'], - city: post_office['city'], - phone: post_office['phone'], - name: post_office['name'], - zip_code: post_office['zip5'], - state: post_office['state'], - ) - 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 - IdentityConfig.store.usps_ipp_root_url - end - - def sponsor_id - IdentityConfig.store.usps_ipp_sponsor_id.to_i - end - - def request_id - SecureRandom.uuid - end - - def faraday - Faraday.new do |conn| - conn.options.timeout = IdentityConfig.store.usps_ipp_request_timeout - conn.options.read_timeout = IdentityConfig.store.usps_ipp_request_timeout - conn.options.open_timeout = IdentityConfig.store.usps_ipp_request_timeout - conn.options.write_timeout = IdentityConfig.store.usps_ipp_request_timeout - end - end - - def request_headers - { 'Content-Type' => 'application/json; charset=utf-8' } - end -end diff --git a/app/services/usps_in_person_proofing/mock.rb b/app/services/usps_in_person_proofing/mock.rb new file mode 100644 index 00000000000..265082faf08 --- /dev/null +++ b/app/services/usps_in_person_proofing/mock.rb @@ -0,0 +1,128 @@ +module UspsInPersonProofing + class MockProofer + # todo: update the documentation for these methods + attr_reader :token, :token_expires_at + + PostOffice = Struct.new( + :distance, :address, :city, :phone, :name, :zip_code, :state, keyword_init: true + ) + + # 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 + JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_token_response.json') + 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] Facility locations + def request_facilities(location) + # todo: return constant list + 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) + # todo: return fake results here, including test error cases + JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_enroll_response.json') + 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) + # todo: mock this somehow, maybe not right now though + 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 sponsor_id + IdentityConfig.store.usps_ipp_sponsor_id.to_i + end + + def request_id + SecureRandom.uuid + end + end +end diff --git a/app/services/usps_in_person_proofing/proofer.rb b/app/services/usps_in_person_proofing/proofer.rb new file mode 100644 index 00000000000..696fe984131 --- /dev/null +++ b/app/services/usps_in_person_proofing/proofer.rb @@ -0,0 +1,214 @@ +module UspsInPersonProofing + class Proofer + attr_reader :token, :token_expires_at + + PostOffice = Struct.new( + :distance, :address, :city, :phone, :name, :zip_code, :state, keyword_init: true + ) + + # 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: IdentityConfig.store.usps_ipp_username, + password: IdentityConfig.store.usps_ipp_password, + grant_type: 'implicit', + response_type: 'token', + client_id: '424ada78-62ae-4c53-8e3a-0b737708a9db', + 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] 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( + distance: post_office['distance'], + address: post_office['streetAddress'], + city: post_office['city'], + phone: post_office['phone'], + name: post_office['name'], + zip_code: post_office['zip5'], + state: post_office['state'], + ) + 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 + IdentityConfig.store.usps_ipp_root_url + end + + def sponsor_id + IdentityConfig.store.usps_ipp_sponsor_id.to_i + end + + def request_id + SecureRandom.uuid + end + + def faraday + Faraday.new do |conn| + conn.options.timeout = IdentityConfig.store.usps_ipp_request_timeout + conn.options.read_timeout = IdentityConfig.store.usps_ipp_request_timeout + conn.options.open_timeout = IdentityConfig.store.usps_ipp_request_timeout + conn.options.write_timeout = IdentityConfig.store.usps_ipp_request_timeout + end + end + + def request_headers + { 'Content-Type' => 'application/json; charset=utf-8' } + end + end +end diff --git a/spec/services/usps_in_person_proofer_spec.rb b/spec/services/usps_in_person_proofer_spec.rb index b95e790fa0f..a72bf446a9f 100644 --- a/spec/services/usps_in_person_proofer_spec.rb +++ b/spec/services/usps_in_person_proofer_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' -RSpec.describe UspsInPersonProofer do - let(:subject) { UspsInPersonProofer.new } +RSpec.describe UspsInPersonProofing::Proofer do + let(:subject) { UspsInPersonProofing::Proofer.new } describe '#retrieve_token!' do it 'sets token and token_expires_at' do From c978301dbde28dad2e064b3faf439a0eb03a30fa Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 16:00:38 -0700 Subject: [PATCH 08/21] Move structs into usps proofing directory --- .../api/verify/password_confirm_controller.rb | 4 +- app/forms/api/profile_creation_form.rb | 5 - .../usps_in_person_proofing/applicant.rb | 6 + app/services/usps_in_person_proofing/mock.rb | 226 +++++++++--------- .../usps_in_person_proofing/post_office.rb | 5 + .../usps_in_person_proofing/proofer.rb | 4 - 6 files changed, 125 insertions(+), 125 deletions(-) create mode 100644 app/services/usps_in_person_proofing/applicant.rb create mode 100644 app/services/usps_in_person_proofing/post_office.rb diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index 1adb5b4659a..a94f91379d8 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -37,7 +37,7 @@ def store_session_last_gpo_code(code) def usps_proofer if IdentityConfig.store.usps_mock_fallback - UspsInPersonProofing::MockProofer.new + UspsInPersonProofing::Mock::Proofer.new else UspsInPersonProofing::Proofer.new end @@ -51,7 +51,7 @@ def save_in_person_enrollment(user) # get token proofer.retrieve_token! # create applicant object - applicant = Applicant.new( + applicant = UspsInPersonProofing::Applicant.new( { unique_id: user.uuid.delete('-').slice(0, 18), first_name: user_session['idv']['pii'].first_name, diff --git a/app/forms/api/profile_creation_form.rb b/app/forms/api/profile_creation_form.rb index 7963e977744..9a84ceb3dd4 100644 --- a/app/forms/api/profile_creation_form.rb +++ b/app/forms/api/profile_creation_form.rb @@ -1,9 +1,4 @@ module Api - # todo: move this somewhere else, maybe the UspsInPersonProofing::Proofer - Applicant = Struct.new( - :unique_id, :first_name, :last_name, :address, :city, :state, :zip_code, - :email, keyword_init: true - ) class ProfileCreationForm include ActiveModel::Model diff --git a/app/services/usps_in_person_proofing/applicant.rb b/app/services/usps_in_person_proofing/applicant.rb new file mode 100644 index 00000000000..e5c3ae43c1f --- /dev/null +++ b/app/services/usps_in_person_proofing/applicant.rb @@ -0,0 +1,6 @@ +module UspsInPersonProofing + Applicant = Struct.new( + :unique_id, :first_name, :last_name, :address, :city, :state, :zip_code, + :email, keyword_init: true + ) +end diff --git a/app/services/usps_in_person_proofing/mock.rb b/app/services/usps_in_person_proofing/mock.rb index 265082faf08..30aed111ece 100644 --- a/app/services/usps_in_person_proofing/mock.rb +++ b/app/services/usps_in_person_proofing/mock.rb @@ -1,128 +1,126 @@ module UspsInPersonProofing - class MockProofer - # todo: update the documentation for these methods - attr_reader :token, :token_expires_at - - PostOffice = Struct.new( - :distance, :address, :city, :phone, :name, :zip_code, :state, keyword_init: true - ) - - # 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 - JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_token_response.json') - 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] Facility locations - def request_facilities(location) - # todo: return constant list - 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) - # todo: return fake results here, including test error cases - JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_enroll_response.json') - 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) - # todo: mock this somehow, maybe not right now though - 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, - }, - ) + module Mock + class Proofer + # todo: update the documentation for these methods + attr_reader :token, :token_expires_at + + # 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 - resp = faraday.post(url, body, headers) + def token_valid? + @token.present? && @token_expires_at.present? && @token_expires_at.future? + end - 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 } + # 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 + JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_token_response.json') 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 + # 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] Facility locations + def request_facilities(location) + # todo: return constant list + end - headers = request_headers.merge( - { - 'Authorization' => @token, - 'RequestID' => request_id, - }, - ) + # 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) + # todo: return fake results here, including test error cases + JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_enroll_response.json') + end - resp = faraday.post(url, body, headers) + # 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) + # todo: mock this somehow, maybe not right now though + 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 - if resp.success? - JSON.parse(resp.body) - else - resp + # 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 - end - def sponsor_id - IdentityConfig.store.usps_ipp_sponsor_id.to_i - end + def sponsor_id + IdentityConfig.store.usps_ipp_sponsor_id.to_i + end - def request_id - SecureRandom.uuid + def request_id + SecureRandom.uuid + end end end end diff --git a/app/services/usps_in_person_proofing/post_office.rb b/app/services/usps_in_person_proofing/post_office.rb new file mode 100644 index 00000000000..20ae83c5e4f --- /dev/null +++ b/app/services/usps_in_person_proofing/post_office.rb @@ -0,0 +1,5 @@ +module UspsInPersonProofing + PostOffice = Struct.new( + :distance, :address, :city, :phone, :name, :zip_code, :state, keyword_init: true + ) +end diff --git a/app/services/usps_in_person_proofing/proofer.rb b/app/services/usps_in_person_proofing/proofer.rb index 696fe984131..72fa28d1427 100644 --- a/app/services/usps_in_person_proofing/proofer.rb +++ b/app/services/usps_in_person_proofing/proofer.rb @@ -2,10 +2,6 @@ module UspsInPersonProofing class Proofer attr_reader :token, :token_expires_at - PostOffice = Struct.new( - :distance, :address, :city, :phone, :name, :zip_code, :state, keyword_init: true - ) - # Makes a request to retrieve a new OAuth token # and modifies self to store the token and when # it expires (15 minutes). From 1a722ab76cebc16d3c9660cb2d973a725ed84ac3 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 16:37:16 -0700 Subject: [PATCH 09/21] Fix broken specs --- .../api/verify/password_confirm_controller.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index a94f91379d8..155100b0c9c 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -10,7 +10,7 @@ def create user = User.find_by(uuid: result.extra[:user_uuid]) add_proofing_component(user) store_session_last_gpo_code(form.gpo_code) - save_in_person_enrollment(user) + save_in_person_enrollment(user, form.profile) render json: { personal_key: personal_key, completion_url: completion_url(result, user), @@ -43,7 +43,7 @@ def usps_proofer end end - def save_in_person_enrollment(user) + def save_in_person_enrollment(user, profile) return unless in_person_enrollment?(user) # create usps proofer @@ -54,13 +54,13 @@ def save_in_person_enrollment(user) applicant = UspsInPersonProofing::Applicant.new( { unique_id: user.uuid.delete('-').slice(0, 18), - first_name: user_session['idv']['pii'].first_name, - last_name: user_session['idv']['pii'].last_name, - address: user_session['idv']['pii'].address1, + first_name: user_session[:idv][:pii].first_name, + last_name: user_session[:idv][:pii].last_name, + address: user_session[:idv][:pii].address1, # do we need address2? - city: user_session['idv']['pii'].city, - state: user_session['idv']['pii'].state, - zip_code: user_session['idv']['pii'].zipcode, + city: user_session[:idv][:pii].city, + state: user_session[:idv][:pii].state, + zip_code: user_session[:idv][:pii].zipcode, email: 'not-used@so-so.com', }, ) @@ -70,7 +70,7 @@ def save_in_person_enrollment(user) enrollment_code = response['enrollmentCode'] InPersonEnrollment.create!( user: user, enrollment_code: enrollment_code, - status: :pending, profile: user.active_profile + status: :pending, profile: profile ) # todo: display error banner on failure end From 15c1a4c065e2b635eb7708c107daffba54144254 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 17:04:16 -0700 Subject: [PATCH 10/21] Move and simplify --- .../api/verify/password_confirm_controller.rb | 81 ++++++++++--------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index 155100b0c9c..3dd06eb8b7b 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -35,46 +35,6 @@ def store_session_last_gpo_code(code) session[:last_gpo_confirmation_code] = code if code && FeatureManagement.reveal_gpo_code? end - def usps_proofer - if IdentityConfig.store.usps_mock_fallback - UspsInPersonProofing::Mock::Proofer.new - else - UspsInPersonProofing::Proofer.new - end - end - - def save_in_person_enrollment(user, profile) - return unless in_person_enrollment?(user) - - # create usps proofer - proofer = usps_proofer - # get token - proofer.retrieve_token! - # create applicant object - applicant = UspsInPersonProofing::Applicant.new( - { - unique_id: user.uuid.delete('-').slice(0, 18), - first_name: user_session[:idv][:pii].first_name, - last_name: user_session[:idv][:pii].last_name, - address: user_session[:idv][:pii].address1, - # do we need address2? - city: user_session[:idv][:pii].city, - state: user_session[:idv][:pii].state, - zip_code: user_session[:idv][:pii].zipcode, - email: 'not-used@so-so.com', - }, - ) - # create enrollment in usps - response = proofer.request_enroll(applicant) - # create an enrollment in the db. if this fails we could conveivably retry by querying the enrollment code from the USPS api. So may be create an upsert-like helper function to get an existing enrollment or create one if it doesn't exist - enrollment_code = response['enrollmentCode'] - InPersonEnrollment.create!( - user: user, enrollment_code: enrollment_code, - status: :pending, profile: profile - ) - # todo: display error banner on failure - end - def verify_params params.permit(:password, :user_bundle_token) end @@ -100,6 +60,47 @@ def in_person_enrollment?(user) # WILLFIX: After LG-6872 and we have enrollment saved, reference enrollment instead. ProofingComponent.find_by(user: user)&.document_check == Idp::Constants::Vendors::USPS end + + def usps_proofer + if IdentityConfig.store.usps_mock_fallback + UspsInPersonProofing::Mock::Proofer.new + else + UspsInPersonProofing::Proofer.new + end + end + + def save_in_person_enrollment(user, profile) + return unless in_person_enrollment?(user) + + # create enrollment in usps + pii = user_session[:idv][:pii] + applicant = UspsInPersonProofing::Applicant.new( + { + unique_id: user.uuid.delete('-').slice(0, 18), + first_name: pii.first_name, + last_name: pii.last_name, + address: pii.address1, + # do we need address2? + city: pii.city, + state: pii.state, + zip_code: pii.zipcode, + email: 'no-reply@login.gov', + }, + ) + proofer = usps_proofer + proofer.retrieve_token! + # todo: any error handling? + response = proofer.request_enroll(applicant) + + # create an enrollment in the db + # todo: if this fails we could conveivably retry by querying the enrollment code from the USPS api. So may be create an upsert-like helper function to get an existing enrollment or create one if it doesn't exist + enrollment_code = response['enrollmentCode'] + InPersonEnrollment.create!( + user: user, enrollment_code: enrollment_code, + status: :pending, profile: profile + ) + # todo: display error banner on failure? check if raised exception rolls back the profile creation + end end end end From 948968d9a96691ab9c918ab8e898ae99d9593d34 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 17:23:33 -0700 Subject: [PATCH 11/21] Fix lint failures --- .../api/verify/password_confirm_controller.rb | 7 +++++-- app/services/usps_in_person_proofing/mock.rb | 16 ++++++++++------ app/services/usps_in_person_proofing/proofer.rb | 4 ++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index 3dd06eb8b7b..d0095ca1283 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -93,13 +93,16 @@ def save_in_person_enrollment(user, profile) response = proofer.request_enroll(applicant) # create an enrollment in the db - # todo: if this fails we could conveivably retry by querying the enrollment code from the USPS api. So may be create an upsert-like helper function to get an existing enrollment or create one if it doesn't exist + # todo: if this fails we could conveivably retry by querying the enrollment code from the + # USPS api. So may be create an upsert-like helper function to get an existing enrollment + # or create one if it doesn't exist enrollment_code = response['enrollmentCode'] InPersonEnrollment.create!( user: user, enrollment_code: enrollment_code, status: :pending, profile: profile ) - # todo: display error banner on failure? check if raised exception rolls back the profile creation + # todo: display error banner on failure? check if raised exception rolls back the profile + # creation end end end diff --git a/app/services/usps_in_person_proofing/mock.rb b/app/services/usps_in_person_proofing/mock.rb index 30aed111ece..5f77ced3649 100644 --- a/app/services/usps_in_person_proofing/mock.rb +++ b/app/services/usps_in_person_proofing/mock.rb @@ -23,7 +23,9 @@ def token_valid? # it expires (15 minutes). # @return [Hash] API response def request_token - JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_token_response.json') + JSON.load_file( + Rails.root.join('spec/fixtures/usps_ipp_responses/request_token_response.json'), + ) end # Makes HTTP request to get nearby in-person proofing facilities @@ -37,8 +39,8 @@ def request_facilities(location) 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. + # 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. @@ -46,7 +48,9 @@ def request_facilities(location) # @return [Hash] API response def request_enroll(_applicant) # todo: return fake results here, including test error cases - JSON.load_file(Rails.root.join 'spec/fixtures/usps_ipp_responses/request_enroll_response.json') + JSON.load_file( + Rails.root.join('spec/fixtures/usps_ipp_responses/request_enroll_response.json'), + ) end # Makes HTTP request to retrieve proofing status @@ -86,8 +90,8 @@ def request_proofing_results(unique_id, enrollment_code) # 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. + # 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 diff --git a/app/services/usps_in_person_proofing/proofer.rb b/app/services/usps_in_person_proofing/proofer.rb index 70bd0b32959..e4579cf6a03 100644 --- a/app/services/usps_in_person_proofing/proofer.rb +++ b/app/services/usps_in_person_proofing/proofer.rb @@ -80,8 +80,8 @@ def request_proofing_results(unique_id, enrollment_code) # 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. + # 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 From 1a2b6e68ce9b4292d714a44fbf08e0d444ccd20d Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 17:23:41 -0700 Subject: [PATCH 12/21] Use a different unique ID --- app/models/in_person_enrollment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb index 78fd1e94719..ea2800e7010 100644 --- a/app/models/in_person_enrollment.rb +++ b/app/models/in_person_enrollment.rb @@ -34,7 +34,7 @@ def needs_usps_status_check?(check_interval) # Returns the value to use for the USPS enrollment ID def usps_unique_id - user_id.to_s + user.uuid.delete('-').slice(0, 18) end private From 53b610155598b58a8ef96e344cd032b5cf76b3f1 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 17:32:58 -0700 Subject: [PATCH 13/21] Reorganize enrollment creation logic --- .../api/verify/password_confirm_controller.rb | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index d0095ca1283..3dc50caedcf 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -72,11 +72,17 @@ def usps_proofer def save_in_person_enrollment(user, profile) return unless in_person_enrollment?(user) + # create an enrollment in the db + enrollment = InPersonEnrollment.create!( + profile: profile, + user: user, + ) + # create enrollment in usps pii = user_session[:idv][:pii] applicant = UspsInPersonProofing::Applicant.new( { - unique_id: user.uuid.delete('-').slice(0, 18), + unique_id: enrollment.usps_unique_id, first_name: pii.first_name, last_name: pii.last_name, address: pii.address1, @@ -92,15 +98,11 @@ def save_in_person_enrollment(user, profile) # todo: any error handling? response = proofer.request_enroll(applicant) - # create an enrollment in the db - # todo: if this fails we could conveivably retry by querying the enrollment code from the - # USPS api. So may be create an upsert-like helper function to get an existing enrollment - # or create one if it doesn't exist - enrollment_code = response['enrollmentCode'] - InPersonEnrollment.create!( - user: user, enrollment_code: enrollment_code, - status: :pending, profile: profile - ) + # update the enrollment to status pending + enrollment.enrollment_code = response['enrollmentCode'] + enrollment.status = :pending + enrollment.save! + # todo: display error banner on failure? check if raised exception rolls back the profile # creation end From 1df16b05d76eb11dc88ba8e0e898cfbf772f2501 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 17:35:38 -0700 Subject: [PATCH 14/21] Remove unused changes --- app/decorators/api/user_bundle_decorator.rb | 4 ---- app/services/idv/session.rb | 1 - app/services/idv/user_bundle_tokenizer.rb | 1 - 3 files changed, 6 deletions(-) diff --git a/app/decorators/api/user_bundle_decorator.rb b/app/decorators/api/user_bundle_decorator.rb index 0e9f64c081f..c1aaaace8e5 100644 --- a/app/decorators/api/user_bundle_decorator.rb +++ b/app/decorators/api/user_bundle_decorator.rb @@ -21,10 +21,6 @@ def gpo_address_verification? metadata[:address_verification_mechanism] == 'gpo' end - def usps_identity_verification? - metadata[:in_person_proofing] == 'usps' - end - def pii HashWithIndifferentAccess.new(jwt_payload['pii']) end diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 887d226e4a3..31220460131 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -6,7 +6,6 @@ class Session go_back_path idv_phone_step_document_capture_session_uuid idv_gpo_document_capture_session_uuid - in_person_proofing vendor_phone_confirmation user_phone_confirmation pii diff --git a/app/services/idv/user_bundle_tokenizer.rb b/app/services/idv/user_bundle_tokenizer.rb index 312bd9a08a1..c077232f137 100644 --- a/app/services/idv/user_bundle_tokenizer.rb +++ b/app/services/idv/user_bundle_tokenizer.rb @@ -33,7 +33,6 @@ def metadata data[:address_verification_mechanism] = idv_session.address_verification_mechanism data[:user_phone_confirmation] = idv_session.user_phone_confirmation data[:vendor_phone_confirmation] = idv_session.vendor_phone_confirmation - data[:in_person_proofing] = idv_session.in_person_proofing data end From 07d69f7cfd99a635e3aa65d1f834d1916d4d42b8 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 18:32:42 -0700 Subject: [PATCH 15/21] Flesh out specs --- .../password_confirm_controller_spec.rb | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/spec/controllers/api/verify/password_confirm_controller_spec.rb b/spec/controllers/api/verify/password_confirm_controller_spec.rb index 0b613484ba3..e67a8960964 100644 --- a/spec/controllers/api/verify/password_confirm_controller_spec.rb +++ b/spec/controllers/api/verify/password_confirm_controller_spec.rb @@ -65,6 +65,8 @@ def stub_idv_session allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end + # todo add specs here too + it 'creates a profile and returns completion url' do post :create, params: { password: password, user_bundle_token: jwt } @@ -101,11 +103,46 @@ def stub_idv_session allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end + it 'uses a real proofer when the mock is disabled' + it 'creates a profile and returns completion url' do post :create, params: { password: password, user_bundle_token: jwt } expect(JSON.parse(response.body)['completion_url']).to eq(idv_come_back_later_url) end + + it 'creates a USPS enrollment' do + proofer = UspsInPersonProofing::Mock::Proofer.new + mock = double + + allow(mock).to receive(:retrieve_token!) + expect(UspsInPersonProofing::Mock::Proofer).to receive(:new).and_return(mock) + expect(mock).to receive(:request_enroll) do |applicant| + expect(applicant.first_name).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) + expect(applicant.last_name).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) + expect(applicant.address).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + expect(applicant.city).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:city]) + expect(applicant.state).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:state]) + expect(applicant.zip_code).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:zipcode]) + expect(applicant.email).to eq('no-reply@login.gov') + expect(applicant.unique_id).to be_a(String) + + proofer.request_enroll(applicant) + end + + post :create, params: { password: password, user_bundle_token: jwt } + end + + it 'creates an in-person enrollment record' do + expect(InPersonEnrollment.count).to be(0) + post :create, params: { password: password, user_bundle_token: jwt } + + expect(InPersonEnrollment.count).to be(1) + enrollment = InPersonEnrollment.where(user_id: user.id).first + expect(enrollment.status).to eq('pending') + expect(enrollment.user_id).to eq(user.id) + expect(enrollment.enrollment_code).to be_a(String) + end end end From c78d47e918875f70c5e028513b0c6ffb4e854857 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 18:34:17 -0700 Subject: [PATCH 16/21] Use new namespace and name --- app/jobs/get_usps_proofing_results_job.rb | 2 +- spec/jobs/get_usps_proofing_results_job_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 66ea17be5ba..1b50362a69f 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -21,7 +21,7 @@ class GetUspsProofingResultsJob < ApplicationJob def perform(_now) return true unless IdentityConfig.store.in_person_proofing_enabled - proofer = UspsInPersonProofer.new + proofer = UspsInPersonProofing::Proofer.new InPersonEnrollment.needs_usps_status_check(...5.minutes.ago).each do |enrollment| # Record and commit attempt to check enrollment status to database diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 392a18be6b2..a21a88b65a2 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -286,7 +286,7 @@ end it 'does not request any enrollment records' do - # no stubbing means this test will fail if the UspsInPersonProofer + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer # tries to connect to the USPS API job.perform Time.zone.now end From 5a7b661523d553d0d070f9806c7cf168be05edfb Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 18:35:16 -0700 Subject: [PATCH 17/21] changelog: Upcoming features, In-person proofing, Begin creating enrollments in USPS api when user completes flow From ee75065fef4319eaf7c8f6626bbb5ef0c769a054 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 19:03:45 -0700 Subject: [PATCH 18/21] Reorganize specs and code --- .../api/verify/password_confirm_controller.rb | 27 ++++--- .../password_confirm_controller_spec.rb | 80 ++++++++++--------- 2 files changed, 59 insertions(+), 48 deletions(-) diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index 3dc50caedcf..6e5faa5d1c9 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -69,20 +69,11 @@ def usps_proofer end end - def save_in_person_enrollment(user, profile) - return unless in_person_enrollment?(user) - - # create an enrollment in the db - enrollment = InPersonEnrollment.create!( - profile: profile, - user: user, - ) - - # create enrollment in usps + def create_usps_enrollment(unique_id) pii = user_session[:idv][:pii] applicant = UspsInPersonProofing::Applicant.new( { - unique_id: enrollment.usps_unique_id, + unique_id: unique_id, first_name: pii.first_name, last_name: pii.last_name, address: pii.address1, @@ -97,9 +88,21 @@ def save_in_person_enrollment(user, profile) proofer.retrieve_token! # todo: any error handling? response = proofer.request_enroll(applicant) + response['enrollmentCode'] + end + + def save_in_person_enrollment(user, profile) + return unless in_person_enrollment?(user) + + enrollment = InPersonEnrollment.create!( + profile: profile, + user: user, + ) + + enrollment_code = create_usps_enrollment(enrollment.usps_unique_id) # update the enrollment to status pending - enrollment.enrollment_code = response['enrollmentCode'] + enrollment.enrollment_code = enrollment_code enrollment.status = :pending enrollment.save! diff --git a/spec/controllers/api/verify/password_confirm_controller_spec.rb b/spec/controllers/api/verify/password_confirm_controller_spec.rb index e67a8960964..2d213c339f4 100644 --- a/spec/controllers/api/verify/password_confirm_controller_spec.rb +++ b/spec/controllers/api/verify/password_confirm_controller_spec.rb @@ -65,7 +65,17 @@ def stub_idv_session allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end - # todo add specs here too + context 'when in-person mocking is disabled' do + before do + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'uses a real proofer' do + expect(UspsInPersonProofing::Proofer).to receive(:new). + and_return(UspsInPersonProofing::Mock::Proofer.new) + post :create, params: { password: password, user_bundle_token: jwt } + end + end it 'creates a profile and returns completion url' do post :create, params: { password: password, user_bundle_token: jwt } @@ -74,6 +84,39 @@ def stub_idv_session idv_in_person_ready_to_verify_url, ) end + + it 'creates a USPS enrollment' do + proofer = UspsInPersonProofing::Mock::Proofer.new + mock = double + + allow(mock).to receive(:retrieve_token!) + expect(UspsInPersonProofing::Mock::Proofer).to receive(:new).and_return(mock) + expect(mock).to receive(:request_enroll) do |applicant| + expect(applicant.first_name).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) + expect(applicant.last_name).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) + expect(applicant.address).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + expect(applicant.city).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:city]) + expect(applicant.state).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:state]) + expect(applicant.zip_code).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:zipcode]) + expect(applicant.email).to eq('no-reply@login.gov') + expect(applicant.unique_id).to be_a(String) + + proofer.request_enroll(applicant) + end + + post :create, params: { password: password, user_bundle_token: jwt } + end + + it 'creates an in-person enrollment record' do + expect(InPersonEnrollment.count).to be(0) + post :create, params: { password: password, user_bundle_token: jwt } + + expect(InPersonEnrollment.count).to be(1) + enrollment = InPersonEnrollment.where(user_id: user.id).first + expect(enrollment.status).to eq('pending') + expect(enrollment.user_id).to eq(user.id) + expect(enrollment.enrollment_code).to be_a(String) + end end context 'with associated sp session' do @@ -103,46 +146,11 @@ def stub_idv_session allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end - it 'uses a real proofer when the mock is disabled' - it 'creates a profile and returns completion url' do post :create, params: { password: password, user_bundle_token: jwt } expect(JSON.parse(response.body)['completion_url']).to eq(idv_come_back_later_url) end - - it 'creates a USPS enrollment' do - proofer = UspsInPersonProofing::Mock::Proofer.new - mock = double - - allow(mock).to receive(:retrieve_token!) - expect(UspsInPersonProofing::Mock::Proofer).to receive(:new).and_return(mock) - expect(mock).to receive(:request_enroll) do |applicant| - expect(applicant.first_name).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) - expect(applicant.last_name).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) - expect(applicant.address).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) - expect(applicant.city).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:city]) - expect(applicant.state).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:state]) - expect(applicant.zip_code).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:zipcode]) - expect(applicant.email).to eq('no-reply@login.gov') - expect(applicant.unique_id).to be_a(String) - - proofer.request_enroll(applicant) - end - - post :create, params: { password: password, user_bundle_token: jwt } - end - - it 'creates an in-person enrollment record' do - expect(InPersonEnrollment.count).to be(0) - post :create, params: { password: password, user_bundle_token: jwt } - - expect(InPersonEnrollment.count).to be(1) - enrollment = InPersonEnrollment.where(user_id: user.id).first - expect(enrollment.status).to eq('pending') - expect(enrollment.user_id).to eq(user.id) - expect(enrollment.enrollment_code).to be_a(String) - end end end From e47cdc784094c97f539301a9b82a38b2a09b9a31 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 22:25:40 -0700 Subject: [PATCH 19/21] Add another spec, remove some todos --- .../api/verify/password_confirm_controller.rb | 7 ++----- .../verify/password_confirm_controller_spec.rb | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index 6e5faa5d1c9..530a4c2fc19 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -85,8 +85,7 @@ def create_usps_enrollment(unique_id) }, ) proofer = usps_proofer - proofer.retrieve_token! - # todo: any error handling? + response = proofer.request_enroll(applicant) response['enrollmentCode'] end @@ -100,14 +99,12 @@ def save_in_person_enrollment(user, profile) ) enrollment_code = create_usps_enrollment(enrollment.usps_unique_id) + return unless enrollment_code # update the enrollment to status pending enrollment.enrollment_code = enrollment_code enrollment.status = :pending enrollment.save! - - # todo: display error banner on failure? check if raised exception rolls back the profile - # creation end end end diff --git a/spec/controllers/api/verify/password_confirm_controller_spec.rb b/spec/controllers/api/verify/password_confirm_controller_spec.rb index 2d213c339f4..61e1ce59715 100644 --- a/spec/controllers/api/verify/password_confirm_controller_spec.rb +++ b/spec/controllers/api/verify/password_confirm_controller_spec.rb @@ -117,6 +117,21 @@ def stub_idv_session expect(enrollment.user_id).to eq(user.id) expect(enrollment.enrollment_code).to be_a(String) end + + it 'leaves the enrollment in establishing when no enrollment code is returned' do + proofer = UspsInPersonProofing::Mock::Proofer.new + expect(UspsInPersonProofing::Mock::Proofer).to receive(:new).and_return(proofer) + expect(proofer).to receive(:request_enroll).and_return({}) + expect(InPersonEnrollment.count).to be(0) + + post :create, params: { password: password, user_bundle_token: jwt } + + expect(InPersonEnrollment.count).to be(1) + enrollment = InPersonEnrollment.where(user_id: user.id).first + expect(enrollment.status).to eq('establishing') + expect(enrollment.user_id).to eq(user.id) + expect(enrollment.enrollment_code).to be_nil + end end context 'with associated sp session' do From 20a48913ab6129a587e00cdd04c957a33c5e0e9f Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 22:32:49 -0700 Subject: [PATCH 20/21] Remove unnecessary code and comments --- app/services/usps_in_person_proofing/mock.rb | 119 ------------------ .../password_confirm_controller_spec.rb | 1 - 2 files changed, 120 deletions(-) diff --git a/app/services/usps_in_person_proofing/mock.rb b/app/services/usps_in_person_proofing/mock.rb index 5f77ced3649..6e20843562b 100644 --- a/app/services/usps_in_person_proofing/mock.rb +++ b/app/services/usps_in_person_proofing/mock.rb @@ -1,130 +1,11 @@ module UspsInPersonProofing module Mock class Proofer - # todo: update the documentation for these methods - attr_reader :token, :token_expires_at - - # 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 - JSON.load_file( - Rails.root.join('spec/fixtures/usps_ipp_responses/request_token_response.json'), - ) - 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] Facility locations - def request_facilities(location) - # todo: return constant list - 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) - # todo: return fake results here, including test error cases JSON.load_file( Rails.root.join('spec/fixtures/usps_ipp_responses/request_enroll_response.json'), ) 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) - # todo: mock this somehow, maybe not right now though - 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 sponsor_id - IdentityConfig.store.usps_ipp_sponsor_id.to_i - end - - def request_id - SecureRandom.uuid - end end end end diff --git a/spec/controllers/api/verify/password_confirm_controller_spec.rb b/spec/controllers/api/verify/password_confirm_controller_spec.rb index 61e1ce59715..8b235e1e3b4 100644 --- a/spec/controllers/api/verify/password_confirm_controller_spec.rb +++ b/spec/controllers/api/verify/password_confirm_controller_spec.rb @@ -89,7 +89,6 @@ def stub_idv_session proofer = UspsInPersonProofing::Mock::Proofer.new mock = double - allow(mock).to receive(:retrieve_token!) expect(UspsInPersonProofing::Mock::Proofer).to receive(:new).and_return(mock) expect(mock).to receive(:request_enroll) do |applicant| expect(applicant.first_name).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) From cce86be5c6d89df5b92882dadb22abb0c6cb86a5 Mon Sep 17 00:00:00 2001 From: Sheldon Bachstein Date: Wed, 20 Jul 2022 22:42:49 -0700 Subject: [PATCH 21/21] Slight refactor --- app/controllers/api/verify/password_confirm_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb index 530a4c2fc19..0f347dd1f15 100644 --- a/app/controllers/api/verify/password_confirm_controller.rb +++ b/app/controllers/api/verify/password_confirm_controller.rb @@ -69,11 +69,11 @@ def usps_proofer end end - def create_usps_enrollment(unique_id) + def create_usps_enrollment(enrollment) pii = user_session[:idv][:pii] applicant = UspsInPersonProofing::Applicant.new( { - unique_id: unique_id, + unique_id: enrollment.usps_unique_id, first_name: pii.first_name, last_name: pii.last_name, address: pii.address1, @@ -98,7 +98,7 @@ def save_in_person_enrollment(user, profile) user: user, ) - enrollment_code = create_usps_enrollment(enrollment.usps_unique_id) + enrollment_code = create_usps_enrollment(enrollment) return unless enrollment_code # update the enrollment to status pending