diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 6fdf324c668..90e50e5130e 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -3,7 +3,9 @@ class GetUspsProofingResultsJob < ApplicationJob IPP_STATUS_PASSED = 'In-person passed' IPP_STATUS_FAILED = 'In-person failed' IPP_INCOMPLETE_ERROR_MESSAGE = 'Customer has not been to a post office to complete IPP' - IPP_EXPIRED_ERROR_MESSAGE = 'More than 30 days have passed since opt-in to IPP' + IPP_EXPIRED_ERROR_MESSAGE = /More than (?\d+) days have passed since opt-in to IPP/ + IPP_INVALID_ENROLLMENT_CODE_MESSAGE = 'Enrollment code %s does not exist' + IPP_INVALID_APPLICANT_MESSAGE = 'Applicant %s does not exist' SUPPORTED_ID_TYPES = [ "State driver's license", "State non-driver's identification card", @@ -138,18 +140,25 @@ def analytics(user: AnonymousUser.new) end def handle_bad_request_error(err, enrollment) - response = err.response_body - case response&.[]('responseMessage') - when IPP_INCOMPLETE_ERROR_MESSAGE + response_body = err.response_body + response_message = response_body&.[]('responseMessage') + + if response_message == IPP_INCOMPLETE_ERROR_MESSAGE # Customer has not been to post office for IPP enrollment_outcomes[:enrollments_in_progress] += 1 - when IPP_EXPIRED_ERROR_MESSAGE - handle_expired_status_update(enrollment, err.response) + elsif response_message&.match(IPP_EXPIRED_ERROR_MESSAGE) + handle_expired_status_update(enrollment, err.response, response_message) + elsif response_message == IPP_INVALID_ENROLLMENT_CODE_MESSAGE % enrollment.enrollment_code + handle_unexpected_response(enrollment, response_message, reason: 'Invalid enrollment code') + elsif response_message == IPP_INVALID_APPLICANT_MESSAGE % enrollment.unique_id + handle_unexpected_response( + enrollment, response_message, reason: 'Invalid applicant unique id' + ) else NewRelic::Agent.notice_error(err) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( **enrollment_analytics_attributes(enrollment, complete: false), - **response_analytics_attributes(response), + **response_analytics_attributes(response_body), exception_class: err.class.to_s, exception_message: err.message, reason: 'Request exception', @@ -231,7 +240,7 @@ def handle_unsupported_id_type(enrollment, response) enrollment.update(status: :failed) end - def handle_expired_status_update(enrollment, response) + def handle_expired_status_update(enrollment, response, response_message) enrollment_outcomes[:enrollments_expired] += 1 analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_enrollment_updated( **enrollment_analytics_attributes(enrollment, complete: true), @@ -259,6 +268,31 @@ def handle_expired_status_update(enrollment, response) ) enrollment.update(deadline_passed_sent: true) end + + # check for an unexpected number of days until expiration + match = response_message&.match(IPP_EXPIRED_ERROR_MESSAGE) + expired_after_days = match && match[:days] + if expired_after_days.present? && + expired_after_days.to_i != IdentityConfig.store.in_person_enrollment_validity_in_days + handle_unexpected_response( + enrollment, + response_message, + reason: 'Unexpected number of days before enrollment expired', + cancel: false, + ) + end + end + + def handle_unexpected_response(enrollment, response_message, reason:, cancel: true) + enrollment.cancelled! if cancel + + analytics(user: enrollment.user). + idv_in_person_usps_proofing_results_job_unexpected_response( + enrollment_code: enrollment.enrollment_code, + enrollment_id: enrollment.id, + response_message: response_message, + reason: reason, + ) end def handle_failed_status(enrollment, response) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index cb9c176ee8c..ccebbbfdf9e 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -3183,7 +3183,7 @@ def idv_in_person_usps_proofing_results_job_deadline_passed_email_initiated( ) end - # Tracks exceptions that are raised when initiating deadline email in GetUspsproofingResultsJob + # Tracks exceptions that are raised when initiating deadline email in GetUspsProofingResultsJob # @param [String] enrollment_id # @param [String] exception_class # @param [String] exception_message @@ -3275,6 +3275,28 @@ def idv_in_person_email_reminder_job_email_initiated( ) end + # Tracks unexpected responses from the USPS API + # @param [String] enrollment_code + # @param [String] enrollment_id + # @param [String] response_message + # @param [String] reason why was this error unexpected? + def idv_in_person_usps_proofing_results_job_unexpected_response( + enrollment_code:, + enrollment_id:, + response_message:, + reason:, + **extra + ) + track_event( + 'GetUspsProofingResultsJob: Unexpected response received', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + response_message: response_message, + reason: reason, + **extra, + ) + end + # Tracks users visiting the recovery options page def account_reset_recovery_options_visit track_event('Account Reset: Recovery Options Visited') diff --git a/app/services/usps_in_person_proofing/mock/fixtures.rb b/app/services/usps_in_person_proofing/mock/fixtures.rb index 8a508d6e128..da574339eb4 100644 --- a/app/services/usps_in_person_proofing/mock/fixtures.rb +++ b/app/services/usps_in_person_proofing/mock/fixtures.rb @@ -49,6 +49,18 @@ def self.request_expired_proofing_results_response load_response_fixture('request_expired_proofing_results_response.json') end + def self.request_unexpected_expired_proofing_results_response + load_response_fixture('request_unexpected_expired_proofing_results_response.json') + end + + def self.request_unexpected_invalid_applicant_response + load_response_fixture('request_unexpected_invalid_applicant_response.json') + end + + def self.request_unexpected_invalid_enrollment_code_response + load_response_fixture('request_unexpected_invalid_enrollment_code_response.json') + end + def self.request_no_post_office_proofing_results_response load_response_fixture('request_no_post_office_proofing_results_response.json') end diff --git a/app/services/usps_in_person_proofing/mock/responses/request_unexpected_expired_proofing_results_response.json b/app/services/usps_in_person_proofing/mock/responses/request_unexpected_expired_proofing_results_response.json new file mode 100644 index 00000000000..cc498c46091 --- /dev/null +++ b/app/services/usps_in_person_proofing/mock/responses/request_unexpected_expired_proofing_results_response.json @@ -0,0 +1,3 @@ +{ + "responseMessage": "More than 4 days have passed since opt-in to IPP" +} diff --git a/app/services/usps_in_person_proofing/mock/responses/request_unexpected_invalid_applicant_response.json b/app/services/usps_in_person_proofing/mock/responses/request_unexpected_invalid_applicant_response.json new file mode 100644 index 00000000000..8729ac5866b --- /dev/null +++ b/app/services/usps_in_person_proofing/mock/responses/request_unexpected_invalid_applicant_response.json @@ -0,0 +1,3 @@ +{ + "responseMessage": "Applicant 123456789abcdefghi does not exist" +} diff --git a/app/services/usps_in_person_proofing/mock/responses/request_unexpected_invalid_enrollment_code_response.json b/app/services/usps_in_person_proofing/mock/responses/request_unexpected_invalid_enrollment_code_response.json new file mode 100644 index 00000000000..059a4149d80 --- /dev/null +++ b/app/services/usps_in_person_proofing/mock/responses/request_unexpected_invalid_enrollment_code_response.json @@ -0,0 +1,3 @@ +{ + "responseMessage": "Enrollment code 1234567890123456 does not exist" +} diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 1efa3950047..cca9fd1e99c 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -561,6 +561,81 @@ end end + context 'when an enrollment expires unexpectedly' do + before(:each) do + stub_request_unexpected_expired_proofing_results + end + + it_behaves_like( + 'enrollment_with_a_status_update', + passed: false, + status: 'expired', + response_json: UspsInPersonProofing::Mock::Fixtures. + request_unexpected_expired_proofing_results_response, + ) + + it 'logs that the enrollment expired unexpectedly' do + allow(IdentityConfig.store).to( + receive(:in_person_enrollment_validity_in_days).and_return(30), + ) + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Enrollment status updated', + hash_including(reason: 'Enrollment has expired'), + ) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Unexpected response received', + hash_including(reason: 'Unexpected number of days before enrollment expired'), + ) + end + end + + context 'when an enrollment is reported as invalid' do + context 'when an enrollment code is invalid' do + # this enrollment code is hardcoded into the fixture + # request_unexpected_invalid_enrollment_code_response.json + let(:pending_enrollment) do + create(:in_person_enrollment, :pending, enrollment_code: '1234567890123456') + end + before(:each) do + stub_request_unexpected_invalid_enrollment_code + end + + it 'cancels the enrollment and logs that it was invalid' do + job.perform(Time.zone.now) + + expect(pending_enrollment.reload.cancelled?).to be_truthy + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Unexpected response received', + hash_including(reason: 'Invalid enrollment code'), + ) + end + end + + context 'when a unique id is invalid' do + # this unique id is hardcoded into the fixture + # request_unexpected_invalid_applicant_response.json + let(:pending_enrollment) do + create(:in_person_enrollment, :pending, unique_id: '123456789abcdefghi') + end + before(:each) do + stub_request_unexpected_invalid_applicant + end + + it 'cancels the enrollment and logs that it was invalid' do + job.perform(Time.zone.now) + + expect(pending_enrollment.reload.cancelled?).to be_truthy + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Unexpected response received', + hash_including(reason: 'Invalid applicant unique id'), + ) + end + end + end + context 'when USPS returns a non-hash response' do before(:each) do stub_request_proofing_results_with_responses({}) diff --git a/spec/support/usps_ipp_helper.rb b/spec/support/usps_ipp_helper.rb index fbd861cb7ef..24d1ea9461e 100644 --- a/spec/support/usps_ipp_helper.rb +++ b/spec/support/usps_ipp_helper.rb @@ -68,6 +68,51 @@ def request_expired_proofing_results_args } end + def stub_request_unexpected_expired_proofing_results + stub_request(:post, %r{/ivs-ippaas-api/IPPRest/resources/rest/getProofingResults}).to_return( + **request_unexpected_expired_proofing_results_args, + ) + end + + def request_unexpected_expired_proofing_results_args + { + status: 400, + body: UspsInPersonProofing::Mock::Fixtures. + request_unexpected_expired_proofing_results_response, + headers: { 'content-type' => 'application/json' }, + } + end + + def stub_request_unexpected_invalid_applicant + stub_request(:post, %r{/ivs-ippaas-api/IPPRest/resources/rest/getProofingResults}).to_return( + **request_unexpected_invalid_applicant_args, + ) + end + + def request_unexpected_invalid_applicant_args + { + status: 400, + body: UspsInPersonProofing::Mock::Fixtures. + request_unexpected_invalid_applicant_response, + headers: { 'content-type' => 'application/json' }, + } + end + + def stub_request_unexpected_invalid_enrollment_code + stub_request(:post, %r{/ivs-ippaas-api/IPPRest/resources/rest/getProofingResults}).to_return( + **request_unexpected_invalid_enrollment_code_args, + ) + end + + def request_unexpected_invalid_enrollment_code_args + { + status: 400, + body: UspsInPersonProofing::Mock::Fixtures. + request_unexpected_invalid_enrollment_code_response, + headers: { 'content-type' => 'application/json' }, + } + end + def stub_request_failed_proofing_results stub_request(:post, %r{/ivs-ippaas-api/IPPRest/resources/rest/getProofingResults}).to_return( **request_failed_proofing_results_args,