+
<%= hint %>
<%= content_tag :'lg-memorable-date', min: min, max: max, **tag_options do -%>
@@ -22,7 +22,10 @@
pattern: '(1[0-2])|(0?[1-9])',
wrapper_html: { class: 'usa-form-group usa-form-group--month margin-bottom-0' },
label: t('components.memorable_date.month'),
- label_html: { class: 'usa-label' },
+ label_html: {
+ class: 'usa-label',
+ id: "memorable-date-month-label-#{unique_id}",
+ },
input_html: {
type: 'text',
class: 'validated-field__input memorable-date__month',
@@ -30,9 +33,10 @@
maxLength: 2,
aria: {
invalid: false,
- describedby: [
- "validated-field-error-#{unique_id}",
- "validated-field-hint-#{unique_id}",
+ describedby: "validated-field-error-#{unique_id}",
+ labelledby: [
+ "memorable-date-month-label-#{unique_id}",
+ "memorable-date-month-hint-#{unique_id}",
],
},
value: month,
@@ -41,6 +45,9 @@
required: required,
) %>
<% end %>
+
class="display-none">
+ <%= t('in_person_proofing.form.state_id.date_hint.month') %>
+
<%= f.simple_fields_for name do |p| %>
@@ -49,7 +56,10 @@
pattern: '(3[01])|([12][0-9])|(0?[1-9])',
wrapper_html: { class: 'usa-form-group usa-form-group--day margin-bottom-0' },
label: t('components.memorable_date.day'),
- label_html: { class: 'usa-label' },
+ label_html: {
+ class: 'usa-label',
+ id: "memorable-date-day-label-#{unique_id}",
+ },
input_html: {
type: 'text',
class: 'validated-field__input memorable-date__day',
@@ -57,9 +67,10 @@
maxLength: 2,
aria: {
invalid: false,
- describedby: [
- "validated-field-error-#{unique_id}",
- "validated-field-hint-#{unique_id}",
+ describedby: "validated-field-error-#{unique_id}",
+ labelledby: [
+ "memorable-date-day-label-#{unique_id}",
+ "memorable-date-day-hint-#{unique_id}",
],
},
value: day,
@@ -68,6 +79,9 @@
required: required,
) %>
<% end %>
+ class="display-none">
+ <%= t('in_person_proofing.form.state_id.date_hint.day') %>
+
<%= f.simple_fields_for name do |p| %>
@@ -76,7 +90,10 @@
pattern: '\d{4}',
wrapper_html: { class: 'usa-form-group usa-form-group--year margin-bottom-0' },
label: t('components.memorable_date.year'),
- label_html: { class: 'usa-label' },
+ label_html: {
+ class: 'usa-label',
+ id: "memorable-date-year-label-#{unique_id}",
+ },
input_html: {
type: 'text',
class: 'validated-field__input memorable-date__year',
@@ -84,9 +101,10 @@
maxLength: 4,
aria: {
invalid: false,
- describedby: [
- "validated-field-error-#{unique_id}",
- "validated-field-hint-#{unique_id}",
+ describedby: "validated-field-error-#{unique_id}",
+ labelledby: [
+ "memorable-date-year-label-#{unique_id}",
+ "memorable-date-year-hint-#{unique_id}",
],
},
value: year,
@@ -95,6 +113,9 @@
required: required,
) %>
<% end %>
+ class="display-none">
+ <%= t('in_person_proofing.form.state_id.date_hint.year') %>
+
<% end -%>
diff --git a/app/controllers/idv/capture_doc_controller.rb b/app/controllers/idv/capture_doc_controller.rb
index 700787c90b3..9af85b98390 100644
--- a/app/controllers/idv/capture_doc_controller.rb
+++ b/app/controllers/idv/capture_doc_controller.rb
@@ -33,7 +33,7 @@ def ensure_user_id_in_session
token.blank? &&
document_capture_session_uuid.blank?
- result = CaptureDoc::ValidateDocumentCaptureSession.new(document_capture_session_uuid).call
+ result = Idv::DocumentCaptureSessionForm.new(document_capture_session_uuid).submit
analytics.track_event(FLOW_STATE_MACHINE_SETTINGS[:analytics_id], result.to_h)
process_result(result)
diff --git a/app/controllers/idv/in_person/usps_locations_controller.rb b/app/controllers/idv/in_person/usps_locations_controller.rb
index 2af123ec9a7..ab84ab0b216 100644
--- a/app/controllers/idv/in_person/usps_locations_controller.rb
+++ b/app/controllers/idv/in_person/usps_locations_controller.rb
@@ -11,11 +11,22 @@ class UspsLocationsController < ApplicationController
before_action :confirm_authenticated_for_api, only: [:update]
- # get the list of all pilot Post Office locations
+ # retrieve the list of nearby IPP Post Office locations with a POST request
def index
usps_response = []
begin
- usps_response = Proofer.new.request_pilot_facilities
+ if IdentityConfig.store.arcgis_search_enabled
+ candidate = UspsInPersonProofing::Applicant.new(
+ address: search_params['street_address'],
+ city: search_params['city'], state: search_params['state'],
+ zip_code: search_params['zip_code']
+ )
+ usps_response = proofer.request_facilities(candidate)
+ else
+ usps_response = proofer.request_pilot_facilities
+ end
+ rescue ActionController::ParameterMissing
+ usps_response = proofer.request_pilot_facilities
rescue Faraday::ConnectionFailed => _error
nil
end
@@ -23,10 +34,14 @@ def index
render json: usps_response.to_json
end
+ def proofer
+ @proofer ||= Proofer.new
+ end
+
# save the Post Office location the user selected to an enrollment
def update
enrollment.update!(
- selected_location_details: permitted_params.as_json,
+ selected_location_details: update_params.as_json,
issuer: current_sp&.issuer,
)
@@ -47,7 +62,16 @@ def enrollment
)
end
- def permitted_params
+ def search_params
+ params.require(:address).permit(
+ :street_address,
+ :city,
+ :state,
+ :zip_code,
+ )
+ end
+
+ def update_params
params.require(:usps_location).permit(
:formatted_city_state_zip,
:name,
diff --git a/app/controllers/idv/otp_delivery_method_controller.rb b/app/controllers/idv/otp_delivery_method_controller.rb
index 51cbbbcd48b..c111c5fca8e 100644
--- a/app/controllers/idv/otp_delivery_method_controller.rb
+++ b/app/controllers/idv/otp_delivery_method_controller.rb
@@ -67,7 +67,9 @@ def render_new_with_error_message
def send_phone_confirmation_otp_and_handle_result
save_delivery_preference
result = send_phone_confirmation_otp
- analytics.idv_phone_confirmation_otp_sent(**result.to_h)
+ analytics.idv_phone_confirmation_otp_sent(
+ **result.to_h.merge(adapter: Telephony.config.adapter),
+ )
irs_attempts_api_tracker.idv_phone_otp_sent(
phone_number: @idv_phone,
@@ -92,7 +94,7 @@ def handle_send_phone_confirmation_otp_failure(result)
def save_delivery_preference
original_session = idv_session.user_phone_confirmation_session
- idv_session.user_phone_confirmation_session = PhoneConfirmation::ConfirmationSession.new(
+ idv_session.user_phone_confirmation_session = Idv::PhoneConfirmationSession.new(
code: original_session.code,
phone: original_session.phone,
sent_at: original_session.sent_at,
diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb
index 2d45fdea843..b78ad8e6191 100644
--- a/app/controllers/idv/personal_key_controller.rb
+++ b/app/controllers/idv/personal_key_controller.rb
@@ -10,7 +10,7 @@ class PersonalKeyController < ApplicationController
before_action :confirm_profile_has_been_created
def show
- analytics.idv_personal_key_visited
+ analytics.idv_personal_key_visited(address_verification_method: address_verification_method)
add_proofing_component
finish_idv_session
@@ -18,12 +18,17 @@ def show
def update
user_session[:need_personal_key_confirmation] = false
- analytics.idv_personal_key_submitted
+
+ analytics.idv_personal_key_submitted(address_verification_method: address_verification_method)
redirect_to next_step
end
private
+ def address_verification_method
+ user_session.dig('idv', 'address_verification_mechanism')
+ end
+
def next_step
if pending_profile? && idv_session.address_verification_mechanism == 'gpo'
idv_come_back_later_url
diff --git a/app/controllers/idv/review_controller.rb b/app/controllers/idv/review_controller.rb
index 4172962da8a..f5c3055eecc 100644
--- a/app/controllers/idv/review_controller.rb
+++ b/app/controllers/idv/review_controller.rb
@@ -36,7 +36,7 @@ def confirm_current_password
def new
@applicant = idv_session.applicant
- analytics.idv_review_info_visited
+ analytics.idv_review_info_visited(address_verification_method: address_verification_method)
gpo_mail_service = Idv::GpoMail.new(current_user)
flash_now = flash.now
@@ -67,6 +67,10 @@ def create
private
+ def address_verification_method
+ user_session.dig('idv', 'address_verification_mechanism')
+ end
+
def log_reproof_event
irs_attempts_api_tracker.idv_reproof
end
diff --git a/app/controllers/users/delete_controller.rb b/app/controllers/users/delete_controller.rb
index c3c014c0db4..3ab213ae094 100644
--- a/app/controllers/users/delete_controller.rb
+++ b/app/controllers/users/delete_controller.rb
@@ -21,7 +21,7 @@ def delete
def delete_user
ActiveRecord::Base.transaction do
- Db::DeletedUser::Create.call(current_user.id)
+ DeletedUser.create_from_user(current_user)
current_user.destroy!
end
end
diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb
index eeb01398869..9ac871a309d 100644
--- a/app/controllers/users/two_factor_authentication_controller.rb
+++ b/app/controllers/users/two_factor_authentication_controller.rb
@@ -219,6 +219,7 @@ def track_events(otp_delivery_preference:)
context: context,
otp_delivery_preference: otp_delivery_preference,
resend: params.dig(:otp_delivery_selection_form, :resend),
+ adapter: Telephony.config.adapter,
telephony_response: @telephony_result.to_h,
success: @telephony_result.success?,
)
@@ -271,19 +272,24 @@ def send_user_otp(method)
end
current_user.create_direct_otp
- params = {
+ otp_params = {
to: phone_to_deliver_to,
otp: current_user.direct_otp,
expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES,
channel: method.to_sym,
domain: IdentityConfig.store.domain_name,
country_code: parsed_phone.country,
+ extra_metadata: {
+ area_code: parsed_phone.area_code,
+ phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ resend: params.dig(:otp_delivery_selection_form, :resend),
+ },
}
if UserSessionContext.authentication_or_reauthentication_context?(context)
- Telephony.send_authentication_otp(**params)
+ Telephony.send_authentication_otp(**otp_params)
else
- Telephony.send_confirmation_otp(**params)
+ Telephony.send_confirmation_otp(**otp_params)
end
end
diff --git a/app/forms/add_user_email_form.rb b/app/forms/add_user_email_form.rb
index 6ff9243be84..3912d26071a 100644
--- a/app/forms/add_user_email_form.rb
+++ b/app/forms/add_user_email_form.rb
@@ -1,6 +1,7 @@
class AddUserEmailForm
include ActiveModel::Model
include FormAddEmailValidator
+ include ActionView::Helpers::TranslationHelper
attr_reader :email
diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb
index b792c826bd2..d8702477b3f 100644
--- a/app/forms/idv/api_image_upload_form.rb
+++ b/app/forms/idv/api_image_upload_form.rb
@@ -199,15 +199,6 @@ def as_readable(image_key)
end
end
- def track_event(event, attributes = {})
- if analytics.present?
- analytics.track_event(
- event,
- attributes,
- )
- end
- end
-
def update_analytics(client_response)
add_costs(client_response)
update_funnel(client_response)
@@ -216,16 +207,19 @@ def update_analytics(client_response)
client_image_metrics: image_metadata,
async: false,
flow_path: params[:flow_path],
- ).merge(native_camera_ab_test_data),
+ ).merge(native_camera_ab_test_data, acuant_sdk_upgrade_ab_test_data),
)
pii_from_doc = client_response.pii_from_doc || {}
- store_encrypted_images_if_required
+ stored_image_result = store_encrypted_images_if_required
irs_attempts_api_tracker.idv_document_upload_submitted(
success: client_response.success?,
document_state: pii_from_doc[:state],
document_number: pii_from_doc[:state_id_number],
document_issued: pii_from_doc[:state_id_issued],
document_expiration: pii_from_doc[:state_id_expiration],
+ document_front_image_filename: stored_image_result&.front_filename,
+ document_back_image_filename: stored_image_result&.back_filename,
+ document_image_encryption_key: stored_image_result&.encryption_key,
first_name: pii_from_doc[:first_name],
last_name: pii_from_doc[:last_name],
date_of_birth: pii_from_doc[:dob],
@@ -239,7 +233,9 @@ def store_encrypted_images_if_required
encrypted_document_storage_writer.encrypt_and_write_document(
front_image: front_image_bytes,
+ front_image_content_type: front.content_type,
back_image: back_image_bytes,
+ back_image_content_type: back.content_type,
)
end
@@ -259,6 +255,14 @@ def native_camera_ab_test_data
}
end
+ def acuant_sdk_upgrade_ab_test_data
+ return {} unless IdentityConfig.store.idv_acuant_sdk_upgrade_a_b_testing_enabled
+ {
+ acuant_sdk_upgrade_ab_test_bucket:
+ AbTests::ACUANT_SDK.bucket(document_capture_session.uuid),
+ }
+ end
+
def acuant_sdk_capture?
image_metadata.dig(:front, :source) == Idp::Constants::Vendors::ACUANT &&
image_metadata.dig(:back, :source) == Idp::Constants::Vendors::ACUANT
diff --git a/app/forms/idv/document_capture_session_form.rb b/app/forms/idv/document_capture_session_form.rb
new file mode 100644
index 00000000000..ecdc06aa59f
--- /dev/null
+++ b/app/forms/idv/document_capture_session_form.rb
@@ -0,0 +1,51 @@
+module Idv
+ class DocumentCaptureSessionForm
+ include ActiveModel::Model
+
+ validates :session_uuid, presence: { message: 'session missing' }
+ validate :session_exists, if: :session_uuid_present?
+ validate :session_not_expired, if: :session_uuid_present?
+
+ def initialize(session_uuid)
+ @session_uuid = session_uuid
+ end
+
+ def submit
+ @success = valid?
+
+ FormResponse.new(success: success, errors: errors, extra: extra_analytics_attributes)
+ end
+
+ private
+
+ attr_reader :success, :session_uuid
+
+ def extra_analytics_attributes
+ {
+ for_user_id: document_capture_session&.user_id,
+ user_id: 'anonymous-uuid',
+ event: 'Document capture session validation',
+ ial2_strict: document_capture_session&.ial2_strict?,
+ sp_issuer: document_capture_session&.issuer,
+ }
+ end
+
+ def session_exists
+ return if document_capture_session
+ errors.add(:session_uuid, 'invalid session', type: :doc_capture_sessions)
+ end
+
+ def session_not_expired
+ return unless document_capture_session&.expired?
+ errors.add(:session_uuid, 'session expired', type: :doc_capture_sessions)
+ end
+
+ def session_uuid_present?
+ session_uuid.present?
+ end
+
+ def document_capture_session
+ @document_capture_session ||= DocumentCaptureSession.find_by(uuid: session_uuid)
+ end
+ end
+end
diff --git a/app/forms/idv/inherited_proofing/base_form.rb b/app/forms/idv/inherited_proofing/base_form.rb
index 63559f6575a..16e144690ef 100644
--- a/app/forms/idv/inherited_proofing/base_form.rb
+++ b/app/forms/idv/inherited_proofing/base_form.rb
@@ -11,23 +11,9 @@ def model_name
def namespaced_model_name
self.to_s.gsub('::', '')
end
-
- def fields
- @fields ||= required_fields + optional_fields
- end
-
- def required_fields
- raise NotImplementedError,
- 'Override this method and return an Array of required field names as Symbols'
- end
-
- def optional_fields
- raise NotImplementedError,
- 'Override this method and return an Array of optional field names as Symbols'
- end
end
- private_class_method :namespaced_model_name, :required_fields, :optional_fields
+ private_class_method :namespaced_model_name
attr_reader :payload_hash
@@ -35,16 +21,12 @@ def initialize(payload_hash:)
raise ArgumentError, 'payload_hash is blank?' if payload_hash.blank?
raise ArgumentError, 'payload_hash is not a Hash' unless payload_hash.is_a? Hash
- self.class.attr_accessor(*self.class.fields)
-
@payload_hash = payload_hash.dup
populate_field_data
end
def submit
- validate
-
FormResponse.new(
success: valid?,
errors: errors,
diff --git a/app/forms/idv/inherited_proofing/va/form.rb b/app/forms/idv/inherited_proofing/va/form.rb
index 5f8a0874c47..97878154d4a 100644
--- a/app/forms/idv/inherited_proofing/va/form.rb
+++ b/app/forms/idv/inherited_proofing/va/form.rb
@@ -2,18 +2,34 @@ module Idv
module InheritedProofing
module Va
class Form < Idv::InheritedProofing::BaseForm
- class << self
- def required_fields
- @required_fields ||= %i[first_name last_name birth_date ssn address_street address_zip]
- end
-
- def optional_fields
- @optional_fields ||= %i[phone address_street2 address_city address_state
- address_country]
- end
- end
+ REQUIRED_FIELDS = %i[first_name
+ last_name
+ birth_date
+ ssn
+ address_street
+ address_zip].freeze
+ OPTIONAL_FIELDS = %i[phone
+ address_street2
+ address_city
+ address_state
+ address_country
+ service_error].freeze
+ FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze
+
+ attr_accessor(*FIELDS)
+ validate :add_service_error, if: :service_error?
+ validates(*REQUIRED_FIELDS, presence: true, unless: :service_error?)
- validates(*required_fields, presence: true)
+ def submit
+ extra = {}
+ extra = { service_error: service_error } if service_error?
+
+ FormResponse.new(
+ success: validate,
+ errors: errors,
+ extra: extra,
+ )
+ end
def user_pii
raise 'User PII is invalid' unless valid?
@@ -30,6 +46,22 @@ def user_pii
user_pii[:zipcode] = address_zip
user_pii
end
+
+ def service_error?
+ service_error.present?
+ end
+
+ private
+
+ def add_service_error
+ errors.add(
+ :service_provider,
+ # Use a "safe" error message for the model in case it's displayed
+ # to the user at any point.
+ I18n.t('inherited_proofing.errors.service_provider.communication'),
+ type: :service_error,
+ )
+ end
end
end
end
diff --git a/app/javascript/packages/document-capture/context/acuant.tsx b/app/javascript/packages/document-capture/context/acuant.tsx
index d4add292131..d81d64c4f1a 100644
--- a/app/javascript/packages/document-capture/context/acuant.tsx
+++ b/app/javascript/packages/document-capture/context/acuant.tsx
@@ -193,8 +193,8 @@ const getActualAcuantCamera = (): AcuantCameraInterface => {
};
function AcuantContextProvider({
- sdkSrc = '/acuant/11.7.0/AcuantJavascriptWebSdk.min.js',
- cameraSrc = '/acuant/11.7.0/AcuantCamera.min.js',
+ sdkSrc = '/acuant/11.7.1/AcuantJavascriptWebSdk.min.js',
+ cameraSrc = '/acuant/11.7.1/AcuantCamera.min.js',
credentials = null,
endpoint = null,
glareThreshold = DEFAULT_ACCEPTABLE_GLARE_SCORE,
diff --git a/app/javascript/packages/document-capture/context/index.ts b/app/javascript/packages/document-capture/context/index.ts
index 37e07f4a857..e3aaaf74af7 100644
--- a/app/javascript/packages/document-capture/context/index.ts
+++ b/app/javascript/packages/document-capture/context/index.ts
@@ -20,3 +20,7 @@ export {
default as NativeCameraABTestContext,
Provider as NativeCameraABTestContextProvider,
} from './native-camera-a-b-test';
+export {
+ default as AcuantSdkUpgradeABTestContext,
+ Provider as AcuantSdkUpgradeABTestContextProvider,
+} from './native-camera-a-b-test';
diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx
index 77c27c45334..1d86b87ca0b 100644
--- a/app/javascript/packs/document-capture.tsx
+++ b/app/javascript/packs/document-capture.tsx
@@ -30,6 +30,9 @@ interface AppRootData {
maxAttemptsBeforeNativeCamera: string;
nativeCameraABTestingEnabled: string;
nativeCameraOnly: string;
+ acuantSdkUpgradeABTestingEnabled: string;
+ useNewerSdk: string;
+ acuantVersion: string;
flowPath: FlowPath;
cancelUrl: string;
idvInPersonUrl?: string;
@@ -67,8 +70,15 @@ function getMetaContent(name): string | null {
const device: DeviceContextValue = { isMobile: isCameraCapableMobile() };
const trackEvent: typeof baseTrackEvent = (event, payload) => {
- const { flowPath } = appRoot.dataset;
- return baseTrackEvent(event, { ...payload, flow_path: flowPath });
+ const { flowPath, acuantSdkUpgradeABTestingEnabled, useNewerSdk, acuantVersion } =
+ appRoot.dataset;
+ return baseTrackEvent(event, {
+ ...payload,
+ flow_path: flowPath,
+ acuant_sdk_upgrade_a_b_testing_enabled: acuantSdkUpgradeABTestingEnabled,
+ use_newer_sdk: useNewerSdk,
+ acuant_version: acuantVersion,
+ });
};
(async () => {
@@ -110,6 +120,7 @@ const trackEvent: typeof baseTrackEvent = (event, payload) => {
maxSubmissionAttemptsBeforeNativeCamera,
nativeCameraABTestingEnabled,
nativeCameraOnly,
+ acuantVersion,
appName,
flowPath,
cancelUrl: cancelURL,
@@ -125,6 +136,8 @@ const trackEvent: typeof baseTrackEvent = (event, payload) => {
[
AcuantContextProvider,
{
+ sdkSrc: acuantVersion && `/acuant/${acuantVersion}/AcuantJavascriptWebSdk.min.js`,
+ cameraSrc: acuantVersion && `/acuant/${acuantVersion}/AcuantCamera.min.js`,
credentials: getMetaContent('acuant-sdk-initialization-creds'),
endpoint: getMetaContent('acuant-sdk-initialization-endpoint'),
glareThreshold,
diff --git a/app/jobs/identities_backfill_job.rb b/app/jobs/identities_backfill_job.rb
new file mode 100644
index 00000000000..fa57ddfdbcb
--- /dev/null
+++ b/app/jobs/identities_backfill_job.rb
@@ -0,0 +1,72 @@
+class IdentitiesBackfillJob < ApplicationJob
+ # This is a short-term solution to backfill data requiring a table scan.
+ # This job can be deleted once it's done.
+
+ queue_as :long_running
+
+ # Let's give us the option to fine-tune this on the fly
+ BATCH_SIZE_KEY = 'IdentitiesBackfillJob.batch_size'.freeze
+ SLICE_SIZE_KEY = 'IdentitiesBackfillJob.slice_size'.freeze
+ CACHE_KEY = 'IdentitiesBackfillJob.position'.freeze
+
+ def perform
+ start_time = Time.zone.now
+ max_id = ServiceProviderIdentity.last.id
+
+ if position > max_id
+ logger.info "backfill reached end; skipping. position=#{position} max_id=#{max_id}"
+ return
+ end
+
+ last_id = position
+
+ (batch_size / slice_size).times.each do |slice_num|
+ start_id = position + (slice_size * slice_num)
+ next if start_id > max_id
+
+ params = {
+ min_id: start_id,
+ max_id: start_id + slice_size,
+ }.transform_values { |v| ActiveRecord::Base.connection.quote(v) }
+ sp_query = format(<<~SQL, params)
+ UPDATE identities
+ SET last_consented_at = created_at
+ WHERE id > %{min_id}
+ AND id <= %{max_id}
+ AND deleted_at IS NULL
+ AND last_consented_at IS NULL
+ RETURNING id
+ SQL
+
+ result = ActiveRecord::Base.connection.execute(sp_query)
+
+ last_id = result.to_a&.first ? result.to_a.first['id'] : start_id + slice_size
+
+ logger.info "Processed rows #{start_id} through #{last_id} (updated=#{result.ntuples})"
+ end
+
+ elapsed_time = Time.zone.now - start_time
+ logger.info(
+ "Finished a full batch (#{batch_size} rows to id=#{last_id}) in #{elapsed_time} seconds",
+ )
+
+ # If we made it here without error, increment the counter for next time:
+ REDIS_POOL.with { |redis| redis.set(CACHE_KEY, last_id) }
+ end
+
+ def position
+ redis_get(CACHE_KEY, 0)
+ end
+
+ def batch_size
+ redis_get(BATCH_SIZE_KEY, 500_000)
+ end
+
+ def slice_size
+ redis_get(SLICE_SIZE_KEY, 10_000)
+ end
+
+ def redis_get(key, default)
+ (REDIS_POOL.with { |redis| redis.get(key) } || default).to_i
+ end
+end
diff --git a/app/jobs/in_person/email_reminder_job.rb b/app/jobs/in_person/email_reminder_job.rb
new file mode 100644
index 00000000000..1fcc68988b8
--- /dev/null
+++ b/app/jobs/in_person/email_reminder_job.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module InPerson
+ class EmailReminderJob < ApplicationJob
+ EMAIL_TYPE_EARLY = 'early'
+ EMAIL_TYPE_LATE = 'late'
+
+ queue_as :low
+
+ include GoodJob::ActiveJobExtensions::Concurrency
+
+ good_job_control_concurrency_with(
+ total_limit: 1,
+ key: 'in_person_email_reminder_job',
+ )
+
+ discard_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError
+
+ def perform(_now)
+ return true unless IdentityConfig.store.in_person_proofing_enabled
+
+ # send late emails first in case of job failure
+ late_enrollments = InPersonEnrollment.needs_late_email_reminder(
+ late_benchmark,
+ final_benchmark,
+ )
+ send_emails_for_enrollments(enrollments: late_enrollments, email_type: EMAIL_TYPE_LATE)
+
+ early_enrollments = InPersonEnrollment.needs_early_email_reminder(
+ early_benchmark,
+ late_benchmark,
+ )
+ send_emails_for_enrollments(enrollments: early_enrollments, email_type: EMAIL_TYPE_EARLY)
+ end
+
+ private
+
+ def analytics(user: AnonymousUser.new)
+ Analytics.new(user: user, request: nil, session: {}, sp: nil)
+ end
+
+ def send_emails_for_enrollments(enrollments:, email_type:)
+ enrollments.each do |enrollment|
+ send_reminder_email(enrollment.user, enrollment)
+ rescue StandardError => err
+ NewRelic::Agent.notice_error(err)
+ analytics(user: enrollment.user).idv_in_person_email_reminder_job_exception(
+ enrollment_id: enrollment.id,
+ exception_class: err.class.to_s,
+ exception_message: err.message,
+ )
+ else
+ analytics(user: enrollment.user).idv_in_person_email_reminder_job_email_initiated(
+ email_type: email_type,
+ enrollment_id: enrollment.id,
+ )
+ if email_type == EMAIL_TYPE_EARLY
+ enrollment.update!(early_reminder_sent: true)
+ elsif email_type == EMAIL_TYPE_LATE
+ enrollment.update!(late_reminder_sent: true)
+ end
+ end
+ end
+
+ def calculate_interval(benchmark)
+ days_until_expired = IdentityConfig.store.in_person_enrollment_validity_in_days.days
+ (Time.zone.now - days_until_expired) + benchmark.days
+ end
+
+ def early_benchmark
+ calculate_interval(IdentityConfig.store.in_person_email_reminder_early_benchmark_in_days)
+ end
+
+ def late_benchmark
+ calculate_interval(IdentityConfig.store.in_person_email_reminder_late_benchmark_in_days)
+ end
+
+ def final_benchmark
+ calculate_interval(IdentityConfig.store.in_person_email_reminder_final_benchmark_in_days)
+ end
+
+ def send_reminder_email(user, enrollment)
+ user.confirmed_email_addresses.each do |email_address|
+ # rubocop:disable IdentityIdp/MailLaterLinter
+ UserMailer.with(
+ user: user,
+ email_address: email_address,
+ ).in_person_ready_to_verify_reminder(
+ enrollment: enrollment,
+ ).deliver_later
+ # rubocop:enable IdentityIdp/MailLaterLinter
+ end
+ end
+ end
+end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 6c0b35d084f..67c8279e561 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -295,13 +295,13 @@ def in_person_ready_to_verify_reminder(enrollment:)
)
@header = t(
'user_mailer.in_person_ready_to_verify_reminder.heading',
- days_remaining: @presenter.days_remaining,
+ count: @presenter.days_remaining,
)
mail(
to: email_address.email,
subject: t(
'user_mailer.in_person_ready_to_verify_reminder.subject',
- days_remaining: @presenter.days_remaining,
+ count: @presenter.days_remaining,
),
)
end
diff --git a/app/models/deleted_user.rb b/app/models/deleted_user.rb
index 056401efbf5..2628a6e4d1f 100644
--- a/app/models/deleted_user.rb
+++ b/app/models/deleted_user.rb
@@ -1,2 +1,18 @@
class DeletedUser < ApplicationRecord
+ def self.create_from_user(user)
+ return unless user
+
+ ActiveRecord::Base.transaction(requires_new: true) do
+ create!(
+ user_id: user.id,
+ uuid: user.uuid,
+ user_created_at: user.created_at,
+ deleted_at: Time.zone.now,
+ )
+ rescue ActiveRecord::RecordNotUnique
+ raise ActiveRecord::Rollback
+ end
+
+ nil
+ end
end
diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb
index 647a72c8c96..2dec3f7b1d3 100644
--- a/app/models/in_person_enrollment.rb
+++ b/app/models/in_person_enrollment.rb
@@ -22,6 +22,28 @@ class InPersonEnrollment < ApplicationRecord
before_save(:on_status_updated, if: :will_save_change_to_status?)
before_create(:set_unique_id, unless: :unique_id)
+ def self.is_pending_and_established_between(early_benchmark, late_benchmark)
+ where(status: :pending).
+ and(
+ where(enrollment_established_at: late_benchmark...(early_benchmark.end_of_day)),
+ ).
+ order(enrollment_established_at: :asc)
+ end
+
+ def self.needs_early_email_reminder(early_benchmark, late_benchmark)
+ self.is_pending_and_established_between(
+ early_benchmark,
+ late_benchmark,
+ ).where(early_reminder_sent: false)
+ end
+
+ def self.needs_late_email_reminder(early_benchmark, late_benchmark)
+ self.is_pending_and_established_between(
+ early_benchmark,
+ late_benchmark,
+ ).where(late_reminder_sent: false)
+ end
+
# Find enrollments that need a status check via the USPS API
def self.needs_usps_status_check(check_interval)
where(status: :pending).
diff --git a/app/presenters/idv/in_person/ready_to_verify_presenter.rb b/app/presenters/idv/in_person/ready_to_verify_presenter.rb
index 630b597f889..bec1f1fe9e7 100644
--- a/app/presenters/idv/in_person/ready_to_verify_presenter.rb
+++ b/app/presenters/idv/in_person/ready_to_verify_presenter.rb
@@ -39,7 +39,7 @@ def service_provider
end
def sp_name
- service_provider ? service_provider.friendly_name : ''
+ service_provider ? service_provider.friendly_name : APP_NAME
end
private
diff --git a/app/services/account_reset/delete_account.rb b/app/services/account_reset/delete_account.rb
index 4208adf410a..f0ceee8745e 100644
--- a/app/services/account_reset/delete_account.rb
+++ b/app/services/account_reset/delete_account.rb
@@ -43,7 +43,7 @@ def handle_successful_submission
def destroy_user
ActiveRecord::Base.transaction do
- Db::DeletedUser::Create.call(user.id)
+ DeletedUser.create_from_user(user)
user.destroy!
end
end
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index b603263bd25..8edd13d3da2 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -621,6 +621,12 @@ def idv_inherited_proofing_get_started_visited(flow_path:, step:, **extra)
)
end
+ # Retry retrieving the user PII in the case where the first attempt fails
+ # in the agreement step, and the user initiates a "retry".
+ def idv_inherited_proofing_redo_retrieve_user_info_submitted(**extra)
+ track_event('IdV: inherited proofing retry retrieve user information submitted', **extra)
+ end
+
# @param [String] flow_path Document capture path ("hybrid" or "standard")
# The user visited the in person proofing location step
def idv_in_person_location_visited(flow_path:, **extra)
@@ -1146,7 +1152,7 @@ def idv_phone_confirmation_otp_rate_limit_sends(proofing_components: nil, **extr
# @param [Boolean] success
# @param [Hash] errors
- # @param ["sms","voice"] otp_delivery_preference which chaennel the OTP was delivered by
+ # @param ["sms","voice"] otp_delivery_preference which channel the OTP was delivered by
# @param [String] country_code country code of phone number
# @param [String] area_code area code of phone number
# @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt
@@ -1180,13 +1186,14 @@ def idv_phone_confirmation_otp_resent(
# @param [Boolean] success
# @param [Hash] errors
- # @param ["sms","voice"] otp_delivery_preference which chaennel the OTP was delivered by
+ # @param ["sms","voice"] otp_delivery_preference which channel the OTP was delivered by
# @param [String] country_code country code of phone number
# @param [String] area_code area code of phone number
# @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt
# @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164
# @param [Hash] telephony_response response from Telephony gem
# @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components
+ # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with
# The user requested an OTP to confirm their phone during the IDV phone step
def idv_phone_confirmation_otp_sent(
success:,
@@ -1197,6 +1204,7 @@ def idv_phone_confirmation_otp_sent(
rate_limit_exceeded:,
phone_fingerprint:,
telephony_response:,
+ adapter:,
proofing_components: nil,
**extra
)
@@ -1210,6 +1218,7 @@ def idv_phone_confirmation_otp_sent(
rate_limit_exceeded: rate_limit_exceeded,
phone_fingerprint: phone_fingerprint,
telephony_response: telephony_response,
+ adapter: adapter,
proofing_components: proofing_components,
**extra,
)
@@ -1379,11 +1388,17 @@ def idv_review_complete(success:, proofing_components: nil, **extra)
)
end
- # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components
+ # @param [Idv::ProofingComponentsLogging] proofing_components User's
+ # current proofing components
+ # @param address_verification_method The method (phone or gpo) being
+ # used to verify the user's identity
# User visited IDV password confirm page
- def idv_review_info_visited(proofing_components: nil, **extra)
+ def idv_review_info_visited(proofing_components: nil,
+ address_verification_method: nil,
+ **extra)
track_event(
'IdV: review info visited',
+ address_verification_method: address_verification_method,
proofing_components: proofing_components,
**extra,
)
@@ -2644,6 +2659,7 @@ def sms_opt_in_visit(
# @param ["sms","voice"] otp_delivery_preference the channel used to send the message
# @param [Boolean] resend
# @param [Hash] telephony_response
+ # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with
# @param [Boolean] success
# A phone one-time password send was attempted
def telephony_otp_sent(
@@ -2654,6 +2670,7 @@ def telephony_otp_sent(
otp_delivery_preference:,
resend:,
telephony_response:,
+ adapter:,
success:,
**extra
)
@@ -2667,6 +2684,7 @@ def telephony_otp_sent(
otp_delivery_preference: otp_delivery_preference,
resend: resend,
telephony_response: telephony_response,
+ adapter: adapter,
success: success,
**extra,
},
@@ -3088,6 +3106,25 @@ def idv_in_person_usps_proofing_results_job_exception(
)
end
+ # Tracks exceptions that are raised when running InPerson::EmailReminderJob
+ # @param [String] enrollment_id
+ # @param [String] exception_class
+ # @param [String] exception_message
+ def idv_in_person_email_reminder_job_exception(
+ enrollment_id:,
+ exception_class: nil,
+ exception_message: nil,
+ **extra
+ )
+ track_event(
+ 'InPerson::EmailReminderJob: Exception raised when attempting to send reminder email',
+ enrollment_id: enrollment_id,
+ exception_class: exception_class,
+ exception_message: exception_message,
+ **extra,
+ )
+ end
+
# Tracks individual enrollments that are updated during GetUspsProofingResultsJob
# @param [String] enrollment_code
# @param [String] enrollment_id
@@ -3126,6 +3163,22 @@ def idv_in_person_usps_proofing_results_job_email_initiated(
)
end
+ # Tracks emails that are initiated during InPerson::EmailReminderJob
+ # @param [String] email_type early or late
+ # @param [String] enrollment_id
+ def idv_in_person_email_reminder_job_email_initiated(
+ email_type:,
+ enrollment_id:,
+ **extra
+ )
+ track_event(
+ 'InPerson::EmailReminderJob: Reminder email initiated',
+ email_type: email_type,
+ enrollment_id: enrollment_id,
+ **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/capture_doc/validate_document_capture_session.rb b/app/services/capture_doc/validate_document_capture_session.rb
deleted file mode 100644
index 4cda3510eb5..00000000000
--- a/app/services/capture_doc/validate_document_capture_session.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module CaptureDoc
- class ValidateDocumentCaptureSession
- include ActiveModel::Model
- include Idv::DocumentCaptureSessionValidator
-
- def initialize(session_uuid)
- @session_uuid = session_uuid
- end
-
- def call
- @success = valid?
-
- FormResponse.new(success: success, errors: errors, extra: extra_analytics_attributes)
- end
-
- private
-
- attr_reader :success
-
- def extra_analytics_attributes
- {
- for_user_id: document_capture_session&.user_id,
- user_id: 'anonymous-uuid',
- event: 'Document capture session validation',
- ial2_strict: document_capture_session&.ial2_strict?,
- sp_issuer: document_capture_session&.issuer,
- }
- end
- end
-end
diff --git a/app/services/db/deleted_user/create.rb b/app/services/db/deleted_user/create.rb
deleted file mode 100644
index 1f226189679..00000000000
--- a/app/services/db/deleted_user/create.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Db
- module DeletedUser
- class Create
- def self.call(user_id)
- user = User.find_by(id: user_id)
- return unless user
-
- ActiveRecord::Base.transaction(requires_new: true) do
- ::DeletedUser.create!(
- user_id: user.id,
- uuid: user.uuid,
- user_created_at: user.created_at,
- deleted_at: Time.zone.now,
- )
- rescue ActiveRecord::RecordNotUnique
- raise ActiveRecord::Rollback
- end
-
- nil
- end
- end
- end
-end
diff --git a/app/services/encrypted_document_storage/document_writer.rb b/app/services/encrypted_document_storage/document_writer.rb
index 68c1f1c3496..6dbadc84114 100644
--- a/app/services/encrypted_document_storage/document_writer.rb
+++ b/app/services/encrypted_document_storage/document_writer.rb
@@ -1,21 +1,25 @@
module EncryptedDocumentStorage
class DocumentWriter
- def encrypt_and_write_document(front_image:, back_image:)
+ def encrypt_and_write_document(
+ front_image:,
+ front_image_content_type:,
+ back_image:,
+ back_image_content_type:
+ )
key = SecureRandom.bytes(32)
encrypted_front_image = aes_cipher.encrypt(front_image, key)
encrypted_back_image = aes_cipher.encrypt(back_image, key)
- front_image_uuid = SecureRandom.uuid
- back_image_uiid = SecureRandom.uuid
+ front_filename = build_filename_for_content_type(front_image_content_type)
+ back_filename = build_filename_for_content_type(back_image_content_type)
- storage.write_image(encrypted_image: encrypted_front_image, name: front_image_uuid)
- storage.write_image(encrypted_image: encrypted_back_image, name: back_image_uiid)
+ storage.write_image(encrypted_image: encrypted_front_image, name: front_filename)
+ storage.write_image(encrypted_image: encrypted_back_image, name: back_filename)
WriteDocumentResult.new(
- front_uuid: front_image_uuid,
- back_uuid: back_image_uiid,
- front_encryption_key: Base64.strict_encode64(key),
- back_encryption_key: Base64.strict_encode64(key),
+ front_filename: front_filename,
+ back_filename: back_filename,
+ encryption_key: Base64.strict_encode64(key),
)
end
@@ -32,5 +36,11 @@ def storage
def aes_cipher
@aes_cipher ||= Encryption::AesCipher.new
end
+
+ # @return [String] A new, unique S3 key for an image of the given content type.
+ def build_filename_for_content_type(content_type)
+ ext = Rack::Mime::MIME_TYPES.rassoc(content_type)&.first
+ "#{SecureRandom.uuid}#{ext}"
+ end
end
end
diff --git a/app/services/encrypted_document_storage/local_storage.rb b/app/services/encrypted_document_storage/local_storage.rb
index eb9a29da0b1..e55be4b1092 100644
--- a/app/services/encrypted_document_storage/local_storage.rb
+++ b/app/services/encrypted_document_storage/local_storage.rb
@@ -1,5 +1,11 @@
module EncryptedDocumentStorage
class LocalStorage
+ # Used in tests to verify results
+ def read_image(name:)
+ filepath = tmp_document_storage_dir.join(name)
+ File.read(filepath)
+ end
+
def write_image(encrypted_image:, name:)
FileUtils.mkdir_p(tmp_document_storage_dir)
filepath = tmp_document_storage_dir.join(name)
diff --git a/app/services/encrypted_document_storage/write_document_result.rb b/app/services/encrypted_document_storage/write_document_result.rb
index ea8fb247fa6..4e9a21e1107 100644
--- a/app/services/encrypted_document_storage/write_document_result.rb
+++ b/app/services/encrypted_document_storage/write_document_result.rb
@@ -1,9 +1,8 @@
module EncryptedDocumentStorage
WriteDocumentResult = Struct.new(
- :front_uuid,
- :back_uuid,
- :front_encryption_key,
- :back_encryption_key,
+ :front_filename,
+ :back_filename,
+ :encryption_key,
keyword_init: true,
)
end
diff --git a/app/services/flow/base_flow.rb b/app/services/flow/base_flow.rb
index 8bcf66618be..6f5779794e7 100644
--- a/app/services/flow/base_flow.rb
+++ b/app/services/flow/base_flow.rb
@@ -1,5 +1,7 @@
module Flow
class BaseFlow
+ include Failure
+
attr_accessor :flow_session
attr_reader :steps, :actions, :current_user, :current_sp, :params, :request, :json,
:http_status, :controller
diff --git a/app/services/flow/base_step.rb b/app/services/flow/base_step.rb
index f138d0d15df..c6f55650d25 100644
--- a/app/services/flow/base_step.rb
+++ b/app/services/flow/base_step.rb
@@ -1,6 +1,7 @@
module Flow
class BaseStep
include Rails.application.routes.url_helpers
+ include Failure
def initialize(flow, name)
@flow = flow
@@ -51,13 +52,6 @@ def form_submit
FormResponse.new(success: true)
end
- def failure(message, extra = nil)
- flow_session[:error_message] = message
- form_response_params = { success: false, errors: { message: message } }
- form_response_params[:extra] = extra unless extra.nil?
- FormResponse.new(**form_response_params)
- end
-
def flow_params
params[@name]
end
diff --git a/app/services/flow/failure.rb b/app/services/flow/failure.rb
new file mode 100644
index 00000000000..c84beedae71
--- /dev/null
+++ b/app/services/flow/failure.rb
@@ -0,0 +1,12 @@
+module Flow
+ module Failure
+ private
+
+ def failure(message, extra = nil)
+ flow_session[:error_message] = message
+ form_response_params = { success: false, errors: { message: message } }
+ form_response_params[:extra] = extra unless extra.nil?
+ FormResponse.new(**form_response_params)
+ end
+ end
+end
diff --git a/app/services/gpo_confirmation_exporter.rb b/app/services/gpo_confirmation_exporter.rb
index da7d4375bd4..009a245ed67 100644
--- a/app/services/gpo_confirmation_exporter.rb
+++ b/app/services/gpo_confirmation_exporter.rb
@@ -22,7 +22,7 @@ def run
def make_psv(csv)
csv << make_header_row(confirmations.size)
confirmations.each do |confirmation|
- csv << make_entry_row(confirmation.entry)
+ csv << make_entry_row(confirmation)
end
end
@@ -30,9 +30,11 @@ def make_header_row(num_entries)
[HEADER_ROW_ID, num_entries]
end
- def make_entry_row(entry)
+ def make_entry_row(confirmation)
now = current_date
- due = now + OTP_MAX_VALID_DAYS.days
+ due = confirmation.created_at + OTP_MAX_VALID_DAYS.days
+
+ entry = confirmation.entry
service_provider = ServiceProvider.find_by(issuer: entry[:issuer])
[
@@ -52,7 +54,7 @@ def make_entry_row(entry)
end
def format_date(date)
- "#{date.strftime('%-B %-e')}, #{date.year}"
+ date.in_time_zone('UTC').strftime('%-B %-e, %Y')
end
def current_date
diff --git a/app/services/idv/actions/inherited_proofing/redo_retrieve_user_info_action.rb b/app/services/idv/actions/inherited_proofing/redo_retrieve_user_info_action.rb
new file mode 100644
index 00000000000..a9fd7ed2c2e
--- /dev/null
+++ b/app/services/idv/actions/inherited_proofing/redo_retrieve_user_info_action.rb
@@ -0,0 +1,19 @@
+module Idv
+ module Actions
+ module InheritedProofing
+ class RedoRetrieveUserInfoAction < Idv::Steps::InheritedProofing::VerifyWaitStepShow
+ class << self
+ def analytics_submitted_event
+ :idv_inherited_proofing_redo_retrieve_user_info_submitted
+ end
+ end
+
+ def call
+ enqueue_job unless api_call_already_in_progress?
+
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/idv/data_url_image.rb b/app/services/idv/data_url_image.rb
index 073f80e39d6..5fa71a7a27c 100644
--- a/app/services/idv/data_url_image.rb
+++ b/app/services/idv/data_url_image.rb
@@ -10,6 +10,11 @@ def initialize(data_url)
@data = data
end
+ # @return [String]
+ def content_type
+ @header.split(';', 2).first
+ end
+
# @return [String]
def read
if base64_encoded?
diff --git a/app/services/idv/flows/inherited_proofing_flow.rb b/app/services/idv/flows/inherited_proofing_flow.rb
index 94a559c537c..2fc56d71103 100644
--- a/app/services/idv/flows/inherited_proofing_flow.rb
+++ b/app/services/idv/flows/inherited_proofing_flow.rb
@@ -18,7 +18,9 @@ class InheritedProofingFlow < Flow::BaseFlow
{ name: :secure_account },
].freeze
- ACTIONS = {}.freeze
+ ACTIONS = {
+ redo_retrieve_user_info: Idv::Actions::InheritedProofing::RedoRetrieveUserInfoAction,
+ }.freeze
attr_reader :idv_session
diff --git a/app/services/idv/inherited_proofing/va/mocks/service.rb b/app/services/idv/inherited_proofing/va/mocks/service.rb
index fc1ed38269f..e1150a9528d 100644
--- a/app/services/idv/inherited_proofing/va/mocks/service.rb
+++ b/app/services/idv/inherited_proofing/va/mocks/service.rb
@@ -36,7 +36,7 @@ class Service
}.freeze
ERROR_HASH = {
- errors: 'InheritedProofing::Errors::MHVIdentityDataNotFoundError',
+ service_error: 'the server responded with status 401',
}.freeze
def initialize(service_provider_data)
diff --git a/app/services/idv/inherited_proofing/va/service.rb b/app/services/idv/inherited_proofing/va/service.rb
index fc7e70cd91e..7e7c76a7e28 100644
--- a/app/services/idv/inherited_proofing/va/service.rb
+++ b/app/services/idv/inherited_proofing/va/service.rb
@@ -17,12 +17,35 @@ def initialize(service_provider_data)
def execute
raise 'The provided auth_code is blank?' if auth_code.blank?
- response = request
- payload_to_hash decrypt_payload(response)
+ begin
+ response = request
+ return payload_to_hash decrypt_payload(response) if response.status == 200
+
+ service_error(not_200_service_error(response.status))
+ rescue => error
+ service_error(error.message)
+ end
end
private
+ def service_error(message)
+ { service_error: message }
+ end
+
+ def not_200_service_error(http_status)
+ # Under certain circumstances, Faraday may return a nil http status.
+ # https://lostisland.github.io/faraday/middleware/raise-error
+ if http_status.blank?
+ http_status = 'unavailable'
+ http_status_description = 'unavailable'
+ else
+ http_status_description = Rack::Utils::HTTP_STATUS_CODES[http_status]
+ end
+ "The service provider API returned an http status other than 200: " \
+ "#{http_status} (#{http_status_description})"
+ end
+
def request
connection.get(request_uri) { |req| req.headers = request_headers }
end
diff --git a/app/services/phone_confirmation/confirmation_session.rb b/app/services/idv/phone_confirmation_session.rb
similarity index 85%
rename from app/services/phone_confirmation/confirmation_session.rb
rename to app/services/idv/phone_confirmation_session.rb
index 136cabf7a01..3730e413326 100644
--- a/app/services/phone_confirmation/confirmation_session.rb
+++ b/app/services/idv/phone_confirmation_session.rb
@@ -1,7 +1,13 @@
-module PhoneConfirmation
- class ConfirmationSession
+module Idv
+ class PhoneConfirmationSession
attr_reader :code, :phone, :sent_at, :delivery_method
+ def self.generate_code
+ OtpCodeGenerator.generate_alphanumeric_digits(
+ TwoFactorAuthenticatable::PROOFING_DIRECT_OTP_LENGTH,
+ )
+ end
+
def initialize(code:, phone:, sent_at:, delivery_method:)
@code = code
@phone = phone
@@ -11,7 +17,7 @@ def initialize(code:, phone:, sent_at:, delivery_method:)
def self.start(phone:, delivery_method:)
new(
- code: CodeGenerator.call,
+ code: generate_code,
phone: phone,
sent_at: Time.zone.now,
delivery_method: delivery_method,
@@ -20,7 +26,7 @@ def self.start(phone:, delivery_method:)
def regenerate_otp
self.class.new(
- code: CodeGenerator.call,
+ code: self.class.generate_code,
phone: phone,
sent_at: Time.zone.now,
delivery_method: delivery_method,
diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb
index 2fbf6799dd2..c84163e4361 100644
--- a/app/services/idv/phone_step.rb
+++ b/app/services/idv/phone_step.rb
@@ -113,7 +113,7 @@ def update_idv_session
end
def start_phone_confirmation_session
- idv_session.user_phone_confirmation_session = PhoneConfirmation::ConfirmationSession.start(
+ idv_session.user_phone_confirmation_session = Idv::PhoneConfirmationSession.start(
phone: PhoneFormatter.format(applicant[:phone]),
delivery_method: :sms,
)
diff --git a/app/services/idv/send_phone_confirmation_otp.rb b/app/services/idv/send_phone_confirmation_otp.rb
index 1198dd63f82..af338052bb2 100644
--- a/app/services/idv/send_phone_confirmation_otp.rb
+++ b/app/services/idv/send_phone_confirmation_otp.rb
@@ -60,6 +60,11 @@ def send_otp
channel: delivery_method,
domain: IdentityConfig.store.domain_name,
country_code: parsed_phone.country,
+ extra_metadata: {
+ area_code: parsed_phone.area_code,
+ phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ resend: nil,
+ },
)
otp_sent_response
end
diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb
index 3e61523e5a0..506b5a675cf 100644
--- a/app/services/idv/session.rb
+++ b/app/services/idv/session.rb
@@ -128,7 +128,7 @@ def address_mechanism_chosen?
def user_phone_confirmation_session
session_value = session[:user_phone_confirmation_session]
return if session_value.blank?
- PhoneConfirmation::ConfirmationSession.from_h(session_value)
+ Idv::PhoneConfirmationSession.from_h(session_value)
end
def user_phone_confirmation_session=(new_user_phone_confirmation_session)
diff --git a/app/services/idv/steps/document_capture_step.rb b/app/services/idv/steps/document_capture_step.rb
index 6768145303a..2f55173fbfe 100644
--- a/app/services/idv/steps/document_capture_step.rb
+++ b/app/services/idv/steps/document_capture_step.rb
@@ -29,7 +29,7 @@ def extra_view_variables
image_type: 'back',
transaction_id: flow_session[:document_capture_session_uuid],
),
- }.merge(native_camera_ab_testing_variables)
+ }.merge(native_camera_ab_testing_variables, acuant_sdk_upgrade_a_b_testing_variables)
end
private
@@ -44,6 +44,17 @@ def native_camera_ab_testing_variables
}
end
+ def acuant_sdk_upgrade_a_b_testing_variables
+ bucket = AbTests::ACUANT_SDK.bucket(flow_session[:document_capture_session_uuid])
+ acuant_version = (bucket == :use_newer_sdk) ? '11.7.1' : '11.7.0'
+ {
+ acuant_sdk_upgrade_a_b_testing_enabled:
+ IdentityConfig.store.idv_acuant_sdk_upgrade_a_b_testing_enabled,
+ use_newer_sdk: (bucket == :use_newer_sdk),
+ acuant_version: acuant_version,
+ }
+ end
+
def handle_stored_result
if stored_result&.success?
save_proofing_components
diff --git a/app/services/idv/steps/inherited_proofing/agreement_step.rb b/app/services/idv/steps/inherited_proofing/agreement_step.rb
index c3b125b24c0..b49e3d7ec7d 100644
--- a/app/services/idv/steps/inherited_proofing/agreement_step.rb
+++ b/app/services/idv/steps/inherited_proofing/agreement_step.rb
@@ -2,6 +2,8 @@ module Idv
module Steps
module InheritedProofing
class AgreementStep < VerifyBaseStep
+ include UserPiiJobInitiator
+
delegate :controller, :idv_session, to: :@flow
STEP_INDICATOR_STEP = :getting_started
@@ -24,28 +26,6 @@ def form_submit
def consent_form_params
params.require(:inherited_proofing).permit(:ial2_consent_given)
end
-
- def enqueue_job
- return if api_call_already_in_progress?
-
- doc_capture_session = create_document_capture_session(
- inherited_proofing_verify_step_document_capture_session_uuid_key,
- )
-
- doc_capture_session.create_proofing_session
-
- InheritedProofingJob.perform_later(
- controller.inherited_proofing_service_provider,
- controller.inherited_proofing_service_provider_data,
- doc_capture_session.uuid,
- )
- end
-
- def api_call_already_in_progress?
- DocumentCaptureSession.find_by(
- uuid: flow_session['inherited_proofing_verify_step_document_capture_session_uuid'],
- )&.in_progress?
- end
end
end
end
diff --git a/app/services/idv/steps/inherited_proofing/user_pii_job_initiator.rb b/app/services/idv/steps/inherited_proofing/user_pii_job_initiator.rb
new file mode 100644
index 00000000000..3d39f3b0599
--- /dev/null
+++ b/app/services/idv/steps/inherited_proofing/user_pii_job_initiator.rb
@@ -0,0 +1,35 @@
+module Idv
+ module Steps
+ module InheritedProofing
+ module UserPiiJobInitiator
+ private
+
+ def enqueue_job
+ return if api_call_already_in_progress?
+
+ create_document_capture_session(
+ inherited_proofing_verify_step_document_capture_session_uuid_key,
+ ).tap do |doc_capture_session|
+ doc_capture_session.create_proofing_session
+
+ InheritedProofingJob.perform_later(
+ controller.inherited_proofing_service_provider,
+ controller.inherited_proofing_service_provider_data,
+ doc_capture_session.uuid,
+ )
+ end
+ end
+
+ def api_call_already_in_progress?
+ DocumentCaptureSession.find_by(
+ uuid: flow_session[inherited_proofing_verify_step_document_capture_session_uuid_key],
+ ).present?
+ end
+
+ def delete_async
+ flow_session.delete(inherited_proofing_verify_step_document_capture_session_uuid_key)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb b/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb
index 45df7920b8c..611c8d8731d 100644
--- a/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb
+++ b/app/services/idv/steps/inherited_proofing/verify_wait_step_show.rb
@@ -2,6 +2,7 @@ module Idv
module Steps
module InheritedProofing
class VerifyWaitStepShow < VerifyBaseStep
+ include UserPiiJobInitiator
include UserPiiManagable
include Idv::InheritedProofing::ServiceProviderForms
delegate :controller, :idv_session, to: :@flow
@@ -21,17 +22,15 @@ def call
private
def process_async_state(current_async_state)
+ return if current_async_state.in_progress?
+
if current_async_state.none?
mark_step_incomplete(:agreement)
- elsif current_async_state.in_progress?
- nil
elsif current_async_state.missing?
flash[:error] = I18n.t('idv.failure.timeout')
- # Need to add path to error pages once they exist
- # LG-7257
- # This method overrides VerifyBaseStep#process_async_state:
- # See the VerifyBaseStep#process_async_state "elsif current_async_state.missing?"
- # logic as to what is typically needed/performed when hitting this logic path.
+ delete_async
+ mark_step_incomplete(:agreement)
+ @flow.analytics.idv_proofing_resolution_result_missing
elsif current_async_state.done?
async_state_done(current_async_state)
end
@@ -54,11 +53,16 @@ def async_state_done(_current_async_state)
)
form_response = form.submit
+ delete_async
+
if form_response.success?
inherited_proofing_save_user_pii_to_session!(form.user_pii)
mark_step_complete(:verify_wait)
+ elsif throttle.throttled?
+ idv_failure(form_response)
else
- mark_step_incomplete(:agreement)
+ mark_step_complete(:agreement)
+ idv_failure(form_response)
end
form_response
@@ -76,6 +80,34 @@ def document_capture_session
def api_job_result
document_capture_session.load_proofing_result
end
+
+ # Base class overrides
+
+ def throttle
+ @throttle ||= Throttle.new(
+ user: current_user,
+ throttle_type: :inherited_proofing,
+ )
+ end
+
+ def idv_failure_log_throttled
+ @flow.analytics.throttler_rate_limit_triggered(
+ throttle_type: throttle.throttle_type,
+ step_name: self.class.name,
+ )
+ end
+
+ def throttled_url
+ idv_inherited_proofing_errors_failure_url(flow: :inherited_proofing)
+ end
+
+ def exception_url
+ idv_inherited_proofing_errors_failure_url(flow: :inherited_proofing)
+ end
+
+ def warning_url
+ idv_inherited_proofing_errors_no_information_url(flow: :inherited_proofing)
+ end
end
end
end
diff --git a/app/services/idv/steps/verify_base_step.rb b/app/services/idv/steps/verify_base_step.rb
index 7a0e912f9f6..9865bac8fbb 100644
--- a/app/services/idv/steps/verify_base_step.rb
+++ b/app/services/idv/steps/verify_base_step.rb
@@ -78,26 +78,41 @@ def throttle
def idv_failure(result)
throttle.increment! if result.extra.dig(:proofing_results, :exception).blank?
if throttle.throttled?
- @flow.irs_attempts_api_tracker.idv_verification_rate_limited
- @flow.analytics.throttler_rate_limit_triggered(
- throttle_type: :idv_resolution,
- step_name: self.class.name,
- )
- redirect_to idv_session_errors_failure_url
+ idv_failure_log_throttled
+ redirect_to throttled_url
elsif result.extra.dig(:proofing_results, :exception).present?
- @flow.analytics.idv_doc_auth_exception_visited(
- step_name: self.class.name,
- remaining_attempts: throttle.remaining_count,
- )
+ idv_failure_log_error
redirect_to exception_url
else
- @flow.analytics.idv_doc_auth_warning_visited(
- step_name: self.class.name,
- remaining_attempts: throttle.remaining_count,
- )
+ idv_failure_log_warning
redirect_to warning_url
end
- result
+ end
+
+ def idv_failure_log_throttled
+ @flow.irs_attempts_api_tracker.idv_verification_rate_limited
+ @flow.analytics.throttler_rate_limit_triggered(
+ throttle_type: :idv_resolution,
+ step_name: self.class.name,
+ )
+ end
+
+ def idv_failure_log_error
+ @flow.analytics.idv_doc_auth_exception_visited(
+ step_name: self.class.name,
+ remaining_attempts: throttle.remaining_count,
+ )
+ end
+
+ def idv_failure_log_warning
+ @flow.analytics.idv_doc_auth_warning_visited(
+ step_name: self.class.name,
+ remaining_attempts: throttle.remaining_count,
+ )
+ end
+
+ def throttled_url
+ idv_session_errors_failure_url
end
def exception_url
diff --git a/app/services/irs_attempts_api/tracker_events.rb b/app/services/irs_attempts_api/tracker_events.rb
index 891994bb98f..4f8302a7e95 100644
--- a/app/services/irs_attempts_api/tracker_events.rb
+++ b/app/services/irs_attempts_api/tracker_events.rb
@@ -89,6 +89,9 @@ def idv_document_upload_rate_limited
# @param [String] document_number
# @param [String] document_issued
# @param [String] document_expiration
+ # @param [String] document_front_image_filename Filename in S3 w/ encrypted data for the front.
+ # @param [String] document_back_image_filename Filename in S3 w/ encrypted data for the back.
+ # @param [String] document_image_encryption_key Base64-encoded AES key used for images.
# @param [String] first_name
# @param [String] last_name
# @param [String] date_of_birth
@@ -101,6 +104,9 @@ def idv_document_upload_submitted(
document_number: nil,
document_issued: nil,
document_expiration: nil,
+ document_front_image_filename: nil,
+ document_back_image_filename: nil,
+ document_image_encryption_key: nil,
first_name: nil,
last_name: nil,
date_of_birth: nil,
@@ -114,6 +120,9 @@ def idv_document_upload_submitted(
document_number: document_number,
document_issued: document_issued,
document_expiration: document_expiration,
+ document_front_image_filename: document_front_image_filename,
+ document_back_image_filename: document_back_image_filename,
+ document_image_encryption_key: document_image_encryption_key,
first_name: first_name,
last_name: last_name,
date_of_birth: date_of_birth,
diff --git a/app/services/phone_confirmation/code_generator.rb b/app/services/phone_confirmation/code_generator.rb
deleted file mode 100644
index c356c695954..00000000000
--- a/app/services/phone_confirmation/code_generator.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'otp_code_generator'
-
-module PhoneConfirmation
- class CodeGenerator
- def self.call
- OtpCodeGenerator.generate_alphanumeric_digits(
- TwoFactorAuthenticatable::PROOFING_DIRECT_OTP_LENGTH,
- )
- end
- end
-end
diff --git a/app/services/pii/session_store.rb b/app/services/pii/session_store.rb
deleted file mode 100644
index f585395e428..00000000000
--- a/app/services/pii/session_store.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# Provides a way to access already-decrypted and cached PII from the redis
-# in an out-of-band fashion (using only the session UUID) instead of having access
-# to the user_session from Devise/Warden
-# Should only be used outside of a normal browser session (such as the OpenID Connect API)
-# See Pii::Cacher for accessing PII inside of a normal browser session
-module Pii
- class SessionStore
- attr_reader :session_accessor
-
- delegate :ttl, :destroy, to: :session_accessor
-
- def initialize(session_uuid)
- @session_accessor = OutOfBandSessionAccessor.new(session_uuid)
- end
-
- def load
- session = session_accessor.load
-
- Pii::Cacher.new(nil, session.dig('warden.user.user.session')).fetch
- end
-
- # @api private
- # Only used for convenience in tests
- # @param [Pii::Attributes] pii
- def put(pii, expiration = 5.minutes)
- session_data = {
- decrypted_pii: pii.to_h.to_json,
- }
-
- session_accessor.put(session_data, expiration)
- end
- end
-end
diff --git a/app/services/proofing/base.rb b/app/services/proofing/base.rb
deleted file mode 100644
index 3a76306ef59..00000000000
--- a/app/services/proofing/base.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-require 'set'
-
-module Proofing
- class Base
- @vendor_name = nil
- @required_attributes = []
- @optional_attributes = []
- @stage = nil
-
- class << self
- attr_reader :proofer
-
- def vendor_name(name = nil)
- @vendor_name = name || @vendor_name
- end
-
- def required_attributes(*required_attributes)
- return @required_attributes || [] if required_attributes.empty?
- @required_attributes = required_attributes
- end
-
- def optional_attributes(*optional_attributes)
- return @optional_attributes || [] if optional_attributes.empty?
- @optional_attributes = optional_attributes
- end
-
- def attributes
- [*required_attributes, *optional_attributes]
- end
-
- def stage(stage = nil)
- @stage = stage || @stage
- end
-
- def proof(sym = nil, &block)
- @proofer = sym || block
- end
- end
-
- def proof(applicant)
- vendor_applicant = restrict_attributes(applicant)
- validate_attributes(vendor_applicant)
- result = Proofing::Result.new
- execute_proof(proofer, vendor_applicant, result)
- result
- rescue => exception
- NewRelic::Agent.notice_error(exception)
- Proofing::Result.new(exception: exception)
- end
-
- private
-
- def execute_proof(proofer, *args)
- if proofer.is_a? Symbol
- send(proofer, *args)
- else
- instance_exec(*args, &proofer)
- end
- end
-
- def restrict_attributes(applicant)
- applicant.select { |attribute| attributes.include?(attribute) }
- end
-
- def validate_attributes(applicant)
- empty_attributes = applicant.select { |_, attribute| blank?(attribute) }.keys
- missing_attributes = attributes - applicant.keys
- bad_attributes = (empty_attributes | missing_attributes) - optional_attributes
- raise error_message(bad_attributes) if bad_attributes.any?
- end
-
- def error_message(required_attributes)
- "Required attributes #{required_attributes.join(', ')} are not present"
- end
-
- def required_attributes
- self.class.required_attributes
- end
-
- def optional_attributes
- self.class.optional_attributes
- end
-
- def attributes
- self.class.attributes
- end
-
- def stage
- self.class.stage
- end
-
- def proofer
- self.class.proofer
- end
-
- def blank?(val)
- !val || val.to_s.empty?
- end
- end
-end
diff --git a/app/services/proofing/lexis_nexis/ddp/proofer.rb b/app/services/proofing/lexis_nexis/ddp/proofer.rb
index 7a78f1164a7..adfd8cc733d 100644
--- a/app/services/proofing/lexis_nexis/ddp/proofer.rb
+++ b/app/services/proofing/lexis_nexis/ddp/proofer.rb
@@ -1,41 +1,78 @@
module Proofing
module LexisNexis
module Ddp
- class Proofer < LexisNexis::Proofer
- vendor_name 'lexisnexis:ddp'
-
- required_attributes :threatmetrix_session_id,
- :state_id_number,
- :first_name,
- :last_name,
- :dob,
- :ssn,
- :address1,
- :city,
- :state,
- :zipcode,
- :request_ip
-
- optional_attributes :address2, :phone, :email, :uuid_prefix
-
- stage :resolution
-
- proof do |applicant, result|
- proof_applicant(applicant, result)
+ class Proofer
+ class << self
+ def required_attributes
+ [:threatmetrix_session_id,
+ :state_id_number,
+ :first_name,
+ :last_name,
+ :dob,
+ :ssn,
+ :address1,
+ :city,
+ :state,
+ :zipcode,
+ :request_ip]
+ end
+
+ def vendor_name
+ 'lexisnexis'
+ end
+
+ def optional_attributes
+ [:address2, :phone, :email, :uuid_prefix]
+ end
+
+ def stage
+ :resolution
+ end
+ end
+
+ Config = RedactedStruct.new(
+ :instant_verify_workflow,
+ :phone_finder_workflow,
+ :account_id,
+ :base_url,
+ :username,
+ :password,
+ :request_mode,
+ :request_timeout,
+ :org_id,
+ :api_key,
+ keyword_init: true,
+ allowed_members: [
+ :instant_verify_workflow,
+ :phone_finder_workflow,
+ :base_url,
+ :request_mode,
+ :request_timeout,
+ ],
+ )
+
+ attr_reader :config
+
+ def initialize(attrs)
+ @config = Config.new(attrs)
end
def send_verification_request(applicant)
VerificationRequest.new(config: config, applicant: applicant).send
end
- def proof_applicant(applicant, result)
+ def proof(applicant)
response = send_verification_request(applicant)
- process_response(response, result)
+ process_response(response)
+ rescue => exception
+ NewRelic::Agent.notice_error(exception)
+ Proofing::Result.new(exception: exception)
end
private
- def process_response(response, result)
+ def process_response(response)
+ result = Proofing::Result.new
body = response.response_body
result.response_body = body
result.transaction_id = body['request_id']
@@ -44,6 +81,7 @@ def process_response(response, result)
result.review_status = review_status
result.add_error(:request_result, request_result) unless request_result == 'success'
result.add_error(:review_status, review_status) unless review_status == 'pass'
+ result
end
end
end
diff --git a/app/services/proofing/lexis_nexis/instant_verify/proofer.rb b/app/services/proofing/lexis_nexis/instant_verify/proofer.rb
index 3c177a5bc36..6eb80530694 100644
--- a/app/services/proofing/lexis_nexis/instant_verify/proofer.rb
+++ b/app/services/proofing/lexis_nexis/instant_verify/proofer.rb
@@ -5,7 +5,7 @@ class Proofer
attr_reader :config
def initialize(config)
- @config = LexisNexis::Proofer::Config.new(config)
+ @config = LexisNexis::Ddp::Proofer::Config.new(config)
end
def proof(applicant)
diff --git a/app/services/proofing/lexis_nexis/phone_finder/proofer.rb b/app/services/proofing/lexis_nexis/phone_finder/proofer.rb
index 269b6b2d1bf..f0d981a43dc 100644
--- a/app/services/proofing/lexis_nexis/phone_finder/proofer.rb
+++ b/app/services/proofing/lexis_nexis/phone_finder/proofer.rb
@@ -5,7 +5,7 @@ class Proofer
attr_reader :config
def initialize(config)
- @config = LexisNexis::Proofer::Config.new(config)
+ @config = LexisNexis::Ddp::Proofer::Config.new(config)
end
def proof(applicant)
diff --git a/app/services/proofing/lexis_nexis/proofer.rb b/app/services/proofing/lexis_nexis/proofer.rb
deleted file mode 100644
index 07cba134baa..00000000000
--- a/app/services/proofing/lexis_nexis/proofer.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require 'redacted_struct'
-
-module Proofing
- module LexisNexis
- class Proofer < Proofing::Base
- Config = RedactedStruct.new(
- :instant_verify_workflow,
- :phone_finder_workflow,
- :account_id,
- :base_url,
- :username,
- :password,
- :request_mode,
- :request_timeout,
- :org_id,
- :api_key,
- keyword_init: true,
- allowed_members: [
- :instant_verify_workflow,
- :phone_finder_workflow,
- :base_url,
- :request_mode,
- :request_timeout,
- ],
- )
-
- attr_reader :config
-
- def initialize(**attrs)
- @config = Config.new(**attrs)
- end
-
- def proof_applicant(applicant, result)
- response = send_verification_request(applicant)
- result.transaction_id = response.conversation_id
- result.reference = response.reference
- return if response.verification_status == 'passed'
-
- response.verification_errors.each do |key, error_message|
- result.add_error(key, error_message)
- end
- end
-
- private
-
- def send_verification_request
- raise NotImplementedError, "#{__method__} should be defined by a subclass"
- end
- end
- end
-end
diff --git a/app/services/proofing/lexis_nexis/response.rb b/app/services/proofing/lexis_nexis/response.rb
index 2373ae5e5a8..1037ddd2108 100644
--- a/app/services/proofing/lexis_nexis/response.rb
+++ b/app/services/proofing/lexis_nexis/response.rb
@@ -31,6 +31,14 @@ def reference
# @api private
def response_body
@response_body ||= JSON.parse(response.body)
+ rescue JSON::ParserError
+ # IF a JSON parse error occurs the resulting error message will contain the portion of the
+ # response body where the error occured. This portion of the response could potentially
+ # include sensitive informaiton. This commit scrubs the error message by raising a JSON
+ # parse error with a generic message.
+ content_type = response.headers&.[]('Content-Type')
+ error_message = "An error occured parsing the response body JSON, status=#{response.status} content_type=#{content_type}" # rubocop:disable Layout/LineLength
+ raise JSON::ParserError, error_message
end
private
diff --git a/app/services/proofing/mock/ddp_mock_client.rb b/app/services/proofing/mock/ddp_mock_client.rb
index 9a5bd8c77e8..f67314779cd 100644
--- a/app/services/proofing/mock/ddp_mock_client.rb
+++ b/app/services/proofing/mock/ddp_mock_client.rb
@@ -1,27 +1,38 @@
module Proofing
module Mock
- class DdpMockClient < Proofing::Base
- vendor_name 'DdpMock'
+ class DdpMockClient
+ class << self
+ def vendor_name
+ 'DdpMock'
+ end
- required_attributes :threatmetrix_session_id,
- :state_id_number,
- :first_name,
- :last_name,
- :dob,
- :ssn,
- :address1,
- :city,
- :state,
- :zipcode,
- :request_ip
+ def required_attributes
+ %I[threatmetrix_session_id
+ state_id_number
+ first_name
+ last_name
+ dob
+ ssn
+ address1
+ city
+ state
+ zipcode
+ request_ip]
+ end
- optional_attributes :address2, :phone, :email, :uuid_prefix
+ def optional_attributes
+ %I[address2 phone email uuid_prefix]
+ end
- stage :resolution
+ def stage
+ :resolution
+ end
+ end
TRANSACTION_ID = 'ddp-mock-transaction-id-123'
- proof do |applicant, result|
+ def proof(applicant)
+ result = Proofing::Result.new
result.transaction_id = TRANSACTION_ID
response_body = File.read(
@@ -36,6 +47,8 @@ class DdpMockClient < Proofing::Base
result.response_body = JSON.parse(response_body).tap do |json_body|
json_body['review_status'] = status
end
+
+ result
end
def review_status(session_id:)
diff --git a/app/services/proofing/mock/resolution_mock_client.rb b/app/services/proofing/mock/resolution_mock_client.rb
index 8677731c2f7..10e2cb71ce5 100644
--- a/app/services/proofing/mock/resolution_mock_client.rb
+++ b/app/services/proofing/mock/resolution_mock_client.rb
@@ -1,6 +1,6 @@
module Proofing
module Mock
- class ResolutionMockClient < Proofing::Base
+ class ResolutionMockClient
UNVERIFIABLE_ZIP_CODE = '00000'
NO_CONTACT_SSN = /000-?00-?0000/
TRANSACTION_ID = 'resolution-mock-transaction-id-123'
diff --git a/app/services/proofing/mock/state_id_mock_client.rb b/app/services/proofing/mock/state_id_mock_client.rb
index 23924635cff..7f01945df57 100644
--- a/app/services/proofing/mock/state_id_mock_client.rb
+++ b/app/services/proofing/mock/state_id_mock_client.rb
@@ -2,7 +2,7 @@
module Proofing
module Mock
- class StateIdMockClient < Proofing::Base
+ class StateIdMockClient
SUPPORTED_STATE_ID_TYPES = %w[
drivers_license drivers_permit state_id_card
].to_set.freeze
diff --git a/app/services/proofing/result.rb b/app/services/proofing/result.rb
index f3c366a0ccf..e07d9eb4328 100644
--- a/app/services/proofing/result.rb
+++ b/app/services/proofing/result.rb
@@ -4,13 +4,13 @@ class Result
attr_accessor :context, :transaction_id, :reference, :review_status, :response_body
def initialize(
- errors: {},
- context: {},
- exception: nil,
- transaction_id: nil,
- reference: nil,
- response_body: nil
- )
+ errors: {},
+ context: {},
+ exception: nil,
+ transaction_id: nil,
+ reference: nil,
+ response_body: nil
+ )
@errors = errors
@context = context
@exception = exception
diff --git a/app/services/throttle.rb b/app/services/throttle.rb
index df0e89c150e..c0fc4bf7e2b 100644
--- a/app/services/throttle.rb
+++ b/app/services/throttle.rb
@@ -49,6 +49,10 @@ class Throttle
max_attempts: IdentityConfig.store.phone_confirmation_max_attempts,
attempt_window: IdentityConfig.store.phone_confirmation_max_attempt_window_in_minutes,
},
+ inherited_proofing: {
+ max_attempts: IdentityConfig.store.inherited_proofing_max_attempts,
+ attempt_window: IdentityConfig.store.inherited_proofing_max_attempt_window_in_minutes,
+ },
}.with_indifferent_access.freeze
def initialize(throttle_type:, user: nil, target: nil)
diff --git a/app/services/x509/session_store.rb b/app/services/x509/session_store.rb
deleted file mode 100644
index 174b6cfb9d4..00000000000
--- a/app/services/x509/session_store.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# Provides a way to access already-decrypted and cached PII from the redis
-# in an out-of-band fashion (using only the session UUID) instead of having access
-# to the user_session from Devise/Warden
-# Should only be used outside of a normal browser session (such as the OpenID Connect API)
-# See X509::Cacher for accessing PII inside of a normal browser session
-module X509
- class SessionStore
- attr_reader :session_uuid
-
- def initialize(session_uuid)
- @session_uuid = session_uuid
- end
-
- def ttl
- uuid = session_uuid
- session_store.instance_eval { redis.ttl(prefixed(uuid)) }
- end
-
- def load
- session = session_store.send(:load_session_from_redis, session_uuid) || {}
- X509::Attributes.new_from_json(session.dig('warden.user.user.session', :decrypted_x509))
- end
-
- # @api private
- # Only used for convenience in tests
- # @param [X509::Attributes] x509
- def put(piv_cert_info, expiration = 5.minutes)
- session_data = {
- 'warden.user.user.session' => {
- decrypted_x509: piv_cert_info.to_h.to_json,
- },
- }
-
- session_store.
- send(:set_session, {}, session_uuid, session_data, expire_after: expiration.to_i)
- end
-
- private
-
- def session_store
- config = Rails.application.config
- config.session_store.new({}, config.session_options)
- end
- end
-end
diff --git a/app/validators/form_add_email_validator.rb b/app/validators/form_add_email_validator.rb
index b1539350f3a..9882e837ba2 100644
--- a/app/validators/form_add_email_validator.rb
+++ b/app/validators/form_add_email_validator.rb
@@ -13,10 +13,22 @@ module FormAddEmailValidator
mx_with_fallback: !ENV['RAILS_OFFLINE'],
ban_disposable_email: true,
}
+ validate :validate_domain
end
private
+ def validate_domain
+ return unless email.present? && errors.blank?
+ domain = Mail::Address.new(email).domain
+
+ if domain && !domain.ascii_only?
+ errors.add(:email, t('valid_email.validations.email.invalid'), type: :domain)
+ end
+ rescue Mail::Field::IncompleteParseError
+ errors.add(:email, t('valid_email.validations.email.invalid'), type: :domain)
+ end
+
def downcase_and_strip
self.email = email&.downcase&.strip
end
diff --git a/app/validators/form_email_validator.rb b/app/validators/form_email_validator.rb
index 814e474a55a..9b822eff5ef 100644
--- a/app/validators/form_email_validator.rb
+++ b/app/validators/form_email_validator.rb
@@ -11,10 +11,22 @@ module FormEmailValidator
mx_with_fallback: !ENV['RAILS_OFFLINE'],
ban_disposable_email: true,
}
+ validate :validate_domain
end
private
+ def validate_domain
+ return unless email.present? && errors.blank?
+ domain = Mail::Address.new(email).domain
+
+ if domain && !domain.ascii_only?
+ errors.add(:email, t('valid_email.validations.email.invalid'), type: :domain)
+ end
+ rescue Mail::Field::IncompleteParseError
+ errors.add(:email, t('valid_email.validations.email.invalid'), type: :domain)
+ end
+
def downcase_and_strip
self.email = email&.downcase&.strip
end
diff --git a/app/validators/idv/document_capture_session_validator.rb b/app/validators/idv/document_capture_session_validator.rb
deleted file mode 100644
index df034397619..00000000000
--- a/app/validators/idv/document_capture_session_validator.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Idv
- module DocumentCaptureSessionValidator
- extend ActiveSupport::Concern
-
- included do
- validates :session_uuid, presence: { message: 'session missing' }
- validate :session_exists, if: :session_uuid_present?
- validate :session_not_expired, if: :session_uuid_present?
- end
-
- private
-
- attr_reader :session_uuid
-
- def session_exists
- return if document_capture_session
- errors.add(:session_uuid, 'invalid session', type: :doc_capture_sessions)
- end
-
- def session_not_expired
- return unless document_capture_session&.expired?
- errors.add(:session_uuid, 'session expired', type: :doc_capture_sessions)
- end
-
- def session_uuid_present?
- session_uuid.present?
- end
-
- def document_capture_session
- @document_capture_session ||= DocumentCaptureSession.find_by(uuid: session_uuid)
- end
- end
-end
diff --git a/app/views/idv/capture_doc/document_capture.html.erb b/app/views/idv/capture_doc/document_capture.html.erb
index 164a21290d4..1de574ab186 100644
--- a/app/views/idv/capture_doc/document_capture.html.erb
+++ b/app/views/idv/capture_doc/document_capture.html.erb
@@ -8,4 +8,7 @@
back_image_upload_url: back_image_upload_url,
native_camera_a_b_testing_enabled: native_camera_a_b_testing_enabled,
native_camera_only: native_camera_only,
+ acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled,
+ use_newer_sdk: use_newer_sdk,
+ acuant_version: acuant_version,
) %>
diff --git a/app/views/idv/doc_auth/document_capture.html.erb b/app/views/idv/doc_auth/document_capture.html.erb
index c4978d730fe..561f3d73f9f 100644
--- a/app/views/idv/doc_auth/document_capture.html.erb
+++ b/app/views/idv/doc_auth/document_capture.html.erb
@@ -8,4 +8,7 @@
back_image_upload_url: back_image_upload_url,
native_camera_a_b_testing_enabled: native_camera_a_b_testing_enabled,
native_camera_only: native_camera_only,
+ acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled,
+ use_newer_sdk: use_newer_sdk,
+ acuant_version: acuant_version,
) %>
diff --git a/app/views/idv/inherited_proofing_errors/warning.html.erb b/app/views/idv/inherited_proofing_errors/warning.html.erb
index b06fa1930bb..58044bd9f77 100644
--- a/app/views/idv/inherited_proofing_errors/warning.html.erb
+++ b/app/views/idv/inherited_proofing_errors/warning.html.erb
@@ -8,7 +8,7 @@
<%= t('inherited_proofing.errors.cannot_retrieve.info') %>