diff --git a/Brewfile b/Brewfile index fae834dac18..ac26eeacf37 100644 --- a/Brewfile +++ b/Brewfile @@ -1,4 +1,4 @@ -brew 'postgresql@13' +brew 'postgresql@14' brew 'redis' brew 'node@16' brew 'yarn' diff --git a/Makefile b/Makefile index 08925bfd1fa..4de4b18ca2d 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ ARTIFACT_DESTINATION_FILE ?= ./tmp/idp.tar.gz help \ lint \ lint_analytics_events \ + lint_analytics_events_sorted \ lint_tracker_events \ lint_country_dialing_codes \ lint_erb \ @@ -75,6 +76,7 @@ endif @echo "--- analytics_events ---" make lint_analytics_events make lint_tracker_events + make lint_analytics_events_sorted @echo "--- brakeman ---" bundle exec brakeman @echo "--- bundler-audit ---" @@ -102,6 +104,7 @@ endif @echo "--- lint migrations ---" make lint_migrations + lint_erb: ## Lints ERB files bundle exec erblint app/views app/components @@ -247,6 +250,10 @@ analytics_events: public/api/_analytics-events.json ## Generates a JSON file tha lint_analytics_events: .yardoc ## Checks that all methods on AnalyticsEvents are documented bundle exec ruby lib/analytics_events_documenter.rb --class-name="AnalyticsEvents" --check $< +lint_analytics_events_sorted: + @test "$(shell grep '^ def ' app/services/analytics_events.rb)" = "$(shell grep '^ def ' app/services/analytics_events.rb | sort)" \ + || (echo 'Error: methods in analytics_events.rb are not sorted alphabetically' && exit 1) + lint_tracker_events: .yardoc ## Checks that all methods on AnalyticsEvents are documented bundle exec ruby lib/analytics_events_documenter.rb --class-name="IrsAttemptsApi::TrackerEvents" --check --skip-extra-params $< diff --git a/app/assets/stylesheets/components/_icon.scss b/app/assets/stylesheets/components/_icon.scss index 708d8447270..661fd037fb0 100644 --- a/app/assets/stylesheets/components/_icon.scss +++ b/app/assets/stylesheets/components/_icon.scss @@ -20,22 +20,3 @@ $icon-min-padding: 2px; margin-right: #{0.5rem - px-to-rem($icon-min-padding)}; } } - -.ico-absolute { - background-repeat: no-repeat; - background-size: $h4; - position: relative; - - &-success { - &::before { - background-image: url('/alert/success.svg'); - content: ''; - display: block; - height: $h4; - left: units(neg-4); - position: absolute; - top: (lh('body', $theme-body-line-height) - $h4) * 0.5; - width: $h4; - } - } -} diff --git a/app/components/memorable_date_component.html.erb b/app/components/memorable_date_component.html.erb index ceb2eb1fe7f..69c1944c253 100644 --- a/app/components/memorable_date_component.html.erb +++ b/app/components/memorable_date_component.html.erb @@ -35,11 +35,12 @@ minLength: 1, maxLength: 2, aria: { - invalid: false, + invalid: has_errors?, labelledby: [ "memorable-date-month-label-#{unique_id}", "memorable-date-month-hint-#{unique_id}", ], + describedby: ["validated-field-error-#{unique_id}"], }, value: month, }, @@ -71,11 +72,13 @@ minLength: 1, maxLength: 2, aria: { - invalid: false, + invalid: has_errors?, labelledby: [ "memorable-date-day-label-#{unique_id}", "memorable-date-day-hint-#{unique_id}", ], + describedby: ["validated-field-error-#{unique_id}"], + }, value: day, }, @@ -107,11 +110,12 @@ minLength: 4, maxLength: 4, aria: { - invalid: false, + invalid: has_errors?, labelledby: [ "memorable-date-year-label-#{unique_id}", "memorable-date-year-hint-#{unique_id}", ], + describedby: ["validated-field-error-#{unique_id}"], }, value: year, }, @@ -125,4 +129,8 @@ <% end -%> - +
+ <% if has_errors? %> + <%= error_msg %> + <% end %> +
diff --git a/app/components/memorable_date_component.rb b/app/components/memorable_date_component.rb index 1e096caa389..bb33200139c 100644 --- a/app/components/memorable_date_component.rb +++ b/app/components/memorable_date_component.rb @@ -122,6 +122,14 @@ def i18n_long_format(date) end end + def has_errors? + form.object.respond_to?(:errors) && form.object.errors.key?(name) + end + + def error_msg + form.object.errors[name]&.first + end + # Configure default generic error messages for component, # then integrate any overrides def generate_error_messages(label, min, max, override_error_messages) diff --git a/app/components/password_confirmation_component.html.erb b/app/components/password_confirmation_component.html.erb index d3d52b0d219..683da5ccd90 100644 --- a/app/components/password_confirmation_component.html.erb +++ b/app/components/password_confirmation_component.html.erb @@ -43,6 +43,4 @@ > <%= t('components.password_confirmation.toggle_label') %> - - <%= form.hidden_field :confirmation_enabled, value: true %> <% end %> diff --git a/app/controllers/concerns/idv/step_utilities_concern.rb b/app/controllers/concerns/idv/step_utilities_concern.rb index 3d50f23bf4a..51e4ac581e0 100644 --- a/app/controllers/concerns/idv/step_utilities_concern.rb +++ b/app/controllers/concerns/idv/step_utilities_concern.rb @@ -4,7 +4,7 @@ module StepUtilitiesConcern include AcuantConcern def irs_reproofing? - effective_user&.reproof_for_irs?( + current_user&.reproof_for_irs?( service_provider: current_sp, ).present? end diff --git a/app/controllers/concerns/idv_session.rb b/app/controllers/concerns/idv_session.rb index 46091d2720d..75f1d59df5c 100644 --- a/app/controllers/concerns/idv_session.rb +++ b/app/controllers/concerns/idv_session.rb @@ -1,16 +1,15 @@ module IdvSession extend ActiveSupport::Concern - include EffectiveUser included do - before_action :redirect_unless_effective_user + before_action :redirect_unless_idv_session_user before_action :redirect_if_sp_context_needed end def confirm_idv_needed - return if effective_user.active_profile.blank? || + return if idv_session_user.active_profile.blank? || decorated_session.requested_more_recent_verification? || - effective_user.reproof_for_irs?(service_provider: current_sp) + idv_session_user.reproof_for_irs?(service_provider: current_sp) redirect_to idv_activated_url end @@ -29,20 +28,26 @@ def confirm_phone_or_address_confirmed def idv_session @idv_session ||= Idv::Session.new( user_session: user_session, - current_user: effective_user, + current_user: idv_session_user, service_provider: current_sp, ) end - def redirect_unless_effective_user - redirect_to root_url if !effective_user + def redirect_unless_idv_session_user + redirect_to root_url if !idv_session_user end def redirect_if_sp_context_needed return if sp_from_sp_session.present? return unless IdentityConfig.store.idv_sp_required - return if effective_user.profiles.any? + return if idv_session_user.profiles.any? redirect_to account_url end + + def idv_session_user + return User.find_by(id: session[:doc_capture_user_id]) if !current_user && hybrid_session? + + current_user + end end diff --git a/app/controllers/concerns/rate_limit_concern.rb b/app/controllers/concerns/rate_limit_concern.rb index 0d3b5ad5130..055539253bb 100644 --- a/app/controllers/concerns/rate_limit_concern.rb +++ b/app/controllers/concerns/rate_limit_concern.rb @@ -47,8 +47,7 @@ def throttle_and_controller_match(throttle_type) self.instance_of?(Idv::VerifyInfoController) || self.instance_of?(Idv::InPerson::VerifyInfoController) when :idv_doc_auth - self.instance_of?(Idv::DocumentCaptureController) || - self.instance_of?(Idv::HybridMobile::DocumentCaptureController) + self.instance_of?(Idv::DocumentCaptureController) when :proof_address self.instance_of?(Idv::PhoneController) end @@ -56,7 +55,7 @@ def throttle_and_controller_match(throttle_type) def idv_attempter_rate_limited?(throttle_type) Throttle.new( - user: effective_user, + user: idv_session_user, throttle_type: throttle_type, ).throttled? end diff --git a/app/controllers/frontend_log_controller.rb b/app/controllers/frontend_log_controller.rb index 589a6152a57..3c4b4d19652 100644 --- a/app/controllers/frontend_log_controller.rb +++ b/app/controllers/frontend_log_controller.rb @@ -22,6 +22,8 @@ class FrontendLogController < ApplicationController 'IdV: user clicked sp link on ready to verify page' => :idv_in_person_ready_to_verify_sp_link_clicked, 'IdV: user clicked what to bring link on ready to verify page' => :idv_in_person_ready_to_verify_what_to_bring_link_clicked, 'IdV: consent checkbox toggled' => :idv_consent_checkbox_toggled, + 'User prompted before navigation' => :user_prompted_before_navigation, + 'User prompted before navigation and still on page' => :user_prompted_before_navigation_and_still_on_page, }.transform_values { |method| AnalyticsEvents.instance_method(method) }.freeze # rubocop:enable Layout/LineLength diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 9175f873079..044de86df5e 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -54,7 +54,11 @@ def extra_view_variables def confirm_upload_step_complete return if flow_session[:flow_path].present? - redirect_to idv_doc_auth_url + if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled + redirect_to idv_hybrid_handoff_url + else + redirect_to idv_doc_auth_url + end end def confirm_document_capture_needed diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 09459ebd55e..fdf07d77a04 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -7,11 +7,10 @@ class HybridHandoffController < ApplicationController include StepUtilitiesConcern before_action :confirm_two_factor_authenticated - before_action :render_404_if_hybrid_handoff_controller_disabled before_action :confirm_agreement_step_complete + before_action :confirm_hybrid_handoff_needed, only: :show def show - flow_session[:flow_path] = 'standard' analytics.idv_doc_auth_upload_visited(**analytics_arguments) Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]).call( @@ -45,6 +44,7 @@ def handle_phone_submission return throttled_failure if throttle.throttled? idv_session.phone_for_mobile_flow = params[:doc_auth][:phone] flow_session[:phone_for_mobile_flow] = idv_session.phone_for_mobile_flow + flow_session[:flow_path] = 'hybrid' telephony_result = send_link telephony_form_response = build_telephony_form_response(telephony_result) @@ -60,14 +60,17 @@ def handle_phone_submission ) if !failure_reason - flow_session[:flow_path] = 'hybrid' redirect_to idv_link_sent_url + + # for the 50/50 state + flow_session['Idv::Steps::UploadStep'] = true else redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil end analytics.idv_doc_auth_upload_submitted( - **analytics_arguments.merge(form_response(destination: :link_sent).to_h), + **analytics_arguments.merge(telephony_form_response.to_h), ) end @@ -100,6 +103,7 @@ def build_telephony_form_response(telephony_result) extra: { telephony_response: telephony_result.to_h, destination: :link_sent, + flow_path: flow_session[:flow_path], }, ) end @@ -118,11 +122,14 @@ def bypass_send_link_steps flow_session[:flow_path] = 'standard' redirect_to idv_document_capture_url - response = form_response(destination: :document_capture) + # for the 50/50 state + flow_session['Idv::Steps::UploadStep'] = true + analytics.idv_doc_auth_upload_submitted( - **analytics_arguments.merge(response.to_h), + **analytics_arguments.merge( + form_response(destination: :document_capture).to_h, + ), ) - response end def extra_view_variables @@ -158,18 +165,9 @@ def analytics_arguments step: 'upload', analytics_id: 'Doc Auth', irs_reproofing: irs_reproofing?, - flow_path: flow_session[:flow_path], }.merge(**acuant_sdk_ab_test_analytics_args) end - def mark_link_sent_step_complete - flow_session['Idv::Steps::LinkSentStep'] = true - end - - def mark_upload_step_complete - flow_session['Idv::Steps::UploadStep'] = true - end - def form_response(destination:) FormResponse.new( success: true, @@ -177,6 +175,7 @@ def form_response(destination:) extra: { destination: destination, skip_upload_step: mobile_device?, + flow_path: flow_session[:flow_path], }, ) end @@ -210,16 +209,22 @@ def failure(message, extra = nil) FormResponse.new(**form_response_params) end - def render_404_if_hybrid_handoff_controller_disabled - render_not_found unless IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled - end - def confirm_agreement_step_complete return if flow_session['Idv::Steps::AgreementStep'] redirect_to idv_doc_auth_url end + def confirm_hybrid_handoff_needed + return if !flow_session[:flow_path] + + if flow_session[:flow_path] == 'standard' + redirect_to idv_document_capture_url + elsif flow_session[:flow_path] == 'hybrid' + redirect_to idv_link_sent_url + end + end + def formatted_destination_phone raw_phone = params.require(:doc_auth).permit(:phone) PhoneFormatter.format(raw_phone, country_code: 'US') diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index ab7f362fbd1..a225ce44cd1 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -80,6 +80,7 @@ def handle_document_verification_success(get_results_response) def render_document_capture_cancelled if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil else mark_upload_step_incomplete redirect_to idv_doc_auth_url # was idv_url, why? diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb index e00ec967e05..e58df4ce1f0 100644 --- a/app/controllers/idv/personal_key_controller.rb +++ b/app/controllers/idv/personal_key_controller.rb @@ -74,7 +74,7 @@ def personal_key def profile return idv_session.profile if idv_session.profile - current_user.active_profile + current_user.active_or_pending_profile end def generate_personal_key diff --git a/app/controllers/idv/review_controller.rb b/app/controllers/idv/review_controller.rb index d7912a0be5e..2cb7ad61914 100644 --- a/app/controllers/idv/review_controller.rb +++ b/app/controllers/idv/review_controller.rb @@ -11,6 +11,8 @@ class ReviewController < ApplicationController before_action :confirm_address_step_complete before_action :confirm_current_password, only: [:create] + helper_method :step_indicator_step + rescue_from UspsInPersonProofing::Exception::RequestEnrollException, with: :handle_request_enroll_exception @@ -30,7 +32,6 @@ def confirm_current_password end def new - @applicant = idv_session.applicant Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer). call(:encrypt, :view, true) analytics.idv_review_info_visited(address_verification_method: address_verification_method) @@ -41,6 +42,8 @@ def new flash_now[:error] = t('idv.errors.mail_limit_reached') elsif idv_session.phone_confirmed? flash_now[:success] = t('idv.messages.review.phone_verified') + elsif address_verification_method == 'gpo' + flash_now[:info] = t('idv.messages.review.gpo_pending') end end @@ -74,10 +77,15 @@ def create session[:last_gpo_confirmation_code] = idv_session.gpo_otp end + def step_indicator_step + return :secure_account unless address_verification_method == 'gpo' + :get_a_letter + end + private def address_verification_method - user_session.dig('idv', 'address_verification_mechanism') + user_session.with_indifferent_access.dig('idv', 'address_verification_mechanism') end def init_profile diff --git a/app/controllers/idv/session_errors_controller.rb b/app/controllers/idv/session_errors_controller.rb index 64cf2659e91..b46d128caed 100644 --- a/app/controllers/idv/session_errors_controller.rb +++ b/app/controllers/idv/session_errors_controller.rb @@ -1,7 +1,6 @@ module Idv class SessionErrorsController < ApplicationController include IdvSession - include EffectiveUser include StepIndicatorConcern before_action :confirm_two_factor_authenticated_or_user_id_in_session @@ -15,7 +14,7 @@ def exception def warning throttle = Throttle.new( - user: effective_user, + user: idv_session_user, throttle_type: :idv_resolution, ) @@ -29,7 +28,7 @@ def state_id_warning def failure throttle = Throttle.new( - user: effective_user, + user: idv_session_user, throttle_type: :idv_resolution, ) @expires_at = throttle.expires_at @@ -53,7 +52,7 @@ def ssn_failure end def throttled - throttle = Throttle.new(user: effective_user, throttle_type: :idv_doc_auth) + throttle = Throttle.new(user: idv_session_user, throttle_type: :idv_doc_auth) log_event(based_on_throttle: throttle) @expires_at = throttle.expires_at end diff --git a/app/controllers/idv/ssn_controller.rb b/app/controllers/idv/ssn_controller.rb index bc53eae9798..bc45e21e254 100644 --- a/app/controllers/idv/ssn_controller.rb +++ b/app/controllers/idv/ssn_controller.rb @@ -9,6 +9,7 @@ class SsnController < ApplicationController before_action :confirm_verify_info_step_needed before_action :confirm_document_capture_complete + before_action :confirm_repeat_ssn, only: :show before_action :override_csp_for_threat_metrix_no_fsm attr_accessor :error_message @@ -50,6 +51,13 @@ def update private + def confirm_repeat_ssn + return if !pii_from_doc[:ssn] + return if request.referer == idv_verify_info_url + + redirect_to idv_verify_info_url + end + def next_url if pii_from_doc[:state] == 'PR' idv_address_url diff --git a/app/controllers/sign_up/passwords_controller.rb b/app/controllers/sign_up/passwords_controller.rb index 7dd4076a806..bfb209a862b 100644 --- a/app/controllers/sign_up/passwords_controller.rb +++ b/app/controllers/sign_up/passwords_controller.rb @@ -50,8 +50,7 @@ def track_analytics(result) def permitted_params params.require(:password_form).permit( - :confirmation_token, :password, :password_confirmation, - :confirmation_enabled + :confirmation_token, :password, :password_confirmation ) end diff --git a/app/controllers/users/verify_password_controller.rb b/app/controllers/users/verify_password_controller.rb index 2e656413473..fd006b6a8f7 100644 --- a/app/controllers/users/verify_password_controller.rb +++ b/app/controllers/users/verify_password_controller.rb @@ -6,12 +6,9 @@ class VerifyPasswordController < ApplicationController before_action :confirm_password_reset_profile before_action :confirm_personal_key - def new - @decrypted_pii = decrypted_pii - end + def new; end def update - @decrypted_pii = decrypted_pii result = verify_password_form.submit irs_attempts_api_tracker.logged_in_profile_change_reauthentication_submitted( @@ -32,13 +29,6 @@ def confirm_personal_key redirect_to root_url end - # rubocop:disable Naming/MemoizedInstanceVariableName - # @return [Pii::Attributes, nil] - def decrypted_pii - @_decrypted_pii ||= reactivate_account_session.decrypted_pii - end - # rubocop:enable Naming/MemoizedInstanceVariableName - def handle_success(result) flash[:personal_key] = result.extra[:personal_key] irs_attempts_api_tracker.idv_personal_key_generated @@ -50,7 +40,7 @@ def verify_password_form VerifyPasswordForm.new( user: current_user, password: params.require(:user).permit(:password)[:password], - decrypted_pii: decrypted_pii, + decrypted_pii: reactivate_account_session.decrypted_pii, ) end end diff --git a/app/forms/idv/in_person/address_form.rb b/app/forms/idv/in_person/address_form.rb index d53513a6e43..43d5b422cca 100644 --- a/app/forms/idv/in_person/address_form.rb +++ b/app/forms/idv/in_person/address_form.rb @@ -19,13 +19,14 @@ def self.model_name def submit(params) consume_params(params) + validation_success = valid? cleaned_errors = errors.dup cleaned_errors.delete(:city, :nontransliterable_field) cleaned_errors.delete(:address1, :nontransliterable_field) cleaned_errors.delete(:address2, :nontransliterable_field) FormResponse.new( - success: valid?, + success: validation_success, errors: cleaned_errors, ) end diff --git a/app/forms/idv/state_id_form.rb b/app/forms/idv/state_id_form.rb index 388309d3d46..2225225344a 100644 --- a/app/forms/idv/state_id_form.rb +++ b/app/forms/idv/state_id_form.rb @@ -19,7 +19,7 @@ def initialize(pii) def submit(params) consume_params(params) - + validation_success = valid? cleaned_errors = errors.dup cleaned_errors.delete(:first_name, :nontransliterable_field) cleaned_errors.delete(:last_name, :nontransliterable_field) @@ -28,7 +28,7 @@ def submit(params) cleaned_errors.delete(:identity_doc_address2, :nontransliterable_field) FormResponse.new( - success: valid?, + success: validation_success, errors: cleaned_errors, ) end diff --git a/app/forms/password_form.rb b/app/forms/password_form.rb index 3aebe12ff63..d0820c8d52d 100644 --- a/app/forms/password_form.rb +++ b/app/forms/password_form.rb @@ -11,7 +11,6 @@ def submit(params) @password = params[:password] @password_confirmation = params[:password_confirmation] @request_id = params.fetch(:request_id, '') - @confirmation_enabled = params[:confirmation_enabled].presence FormResponse.new(success: valid?, errors: errors, extra: extra_analytics_attributes) end diff --git a/app/forms/verify_password_form.rb b/app/forms/verify_password_form.rb index d894f187dc2..e7457a9a908 100644 --- a/app/forms/verify_password_form.rb +++ b/app/forms/verify_password_form.rb @@ -34,8 +34,7 @@ def valid_password? def reencrypt_pii personal_key = profile.encrypt_pii(decrypted_pii, password) - profile.update(deactivation_reason: nil, active: true) - profile.save! + profile.activate_after_password_reset personal_key end diff --git a/app/javascript/packages/address-search/index.tsx b/app/javascript/packages/address-search/index.tsx index 63bc2527b11..960f02d8613 100644 --- a/app/javascript/packages/address-search/index.tsx +++ b/app/javascript/packages/address-search/index.tsx @@ -1,6 +1,6 @@ import { TextInput } from '@18f/identity-components'; import { useState, useRef, useEffect, useCallback } from 'react'; -import { useI18n } from '@18f/identity-react-i18n'; +import { t } from '@18f/identity-i18n'; import { request } from '@18f/identity-request'; import ValidatedField from '@18f/identity-validated-field/validated-field'; import SpinnerButton, { SpinnerButtonRefHandle } from '@18f/identity-spinner-button/spinner-button'; @@ -104,7 +104,6 @@ function useUspsLocations() { // raw text input that is set when user clicks search const [addressQuery, setAddressQuery] = useState(''); const validatedFieldRef = useRef(null); - const { t } = useI18n(); const handleAddressSearch = useCallback((event, unvalidatedAddressInput) => { event.preventDefault(); validatedFieldRef.current?.setCustomValidity(''); @@ -179,7 +178,6 @@ function AddressSearch({ onError = () => undefined, disabled = false, }: AddressSearchProps) { - const { t } = useI18n(); const spinnerButtonRef = useRef(null); const [textInput, setTextInput] = useState(''); const { diff --git a/app/javascript/packages/document-capture-polling/index.ts b/app/javascript/packages/document-capture-polling/index.ts index 384d8db0afe..e9329e35125 100644 --- a/app/javascript/packages/document-capture-polling/index.ts +++ b/app/javascript/packages/document-capture-polling/index.ts @@ -1,4 +1,5 @@ import { trackEvent as defaultTrackEvent } from '@18f/identity-analytics'; +import { promptOnNavigate } from '@18f/identity-prompt-on-navigate'; export const DOC_CAPTURE_TIMEOUT = 1000 * 60 * 25; // 25 minutes export const DOC_CAPTURE_POLL_INTERVAL = 5000; @@ -44,6 +45,8 @@ export class DocumentCapturePolling { pollAttempts = 0; + cleanUpPromptOnNavigate: (() => void) | undefined; + constructor({ elements, statusEndpoint, @@ -70,12 +73,15 @@ export class DocumentCapturePolling { * @param {boolean} shouldPrompt Whether to bind or unbind page unload behavior. */ bindPromptOnNavigate(shouldPrompt) { - window.onbeforeunload = shouldPrompt - ? (event) => { - event.preventDefault(); - event.returnValue = ''; - } - : null; + const isAlreadyBound = !!this.cleanUpPromptOnNavigate; + + if (shouldPrompt && !isAlreadyBound) { + this.cleanUpPromptOnNavigate = promptOnNavigate(); + } else if (!shouldPrompt && isAlreadyBound) { + const cleanUp = this.cleanUpPromptOnNavigate ?? (() => {}); + this.cleanUpPromptOnNavigate = undefined; + cleanUp(); + } } onMaxPollAttempts() { diff --git a/app/javascript/packages/form-steps/prompt-on-navigate.ts b/app/javascript/packages/form-steps/prompt-on-navigate.ts index 00b198d70b1..581f5f345af 100644 --- a/app/javascript/packages/form-steps/prompt-on-navigate.ts +++ b/app/javascript/packages/form-steps/prompt-on-navigate.ts @@ -1,4 +1,5 @@ import { useLayoutEffect } from 'react'; +import { promptOnNavigate } from '@18f/identity-prompt-on-navigate'; /** * While mounted, prompts the user to confirm navigation. @@ -7,17 +8,7 @@ function PromptOnNavigate() { // Use `useLayoutEffect` to guarantee that event unbinding occurs synchronously. // // See: https://reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing - useLayoutEffect(() => { - function onBeforeUnload(event) { - event.preventDefault(); - event.returnValue = ''; - } - - window.onbeforeunload = onBeforeUnload; - return () => { - window.onbeforeunload = null; - }; - }); + useLayoutEffect(promptOnNavigate); return null; } diff --git a/app/javascript/packages/prompt-on-navigate/README.md b/app/javascript/packages/prompt-on-navigate/README.md new file mode 100644 index 00000000000..085ec401180 --- /dev/null +++ b/app/javascript/packages/prompt-on-navigate/README.md @@ -0,0 +1,28 @@ +# `@18f/identity-prompt-on-navigate` + +Configures an `onbeforeunload` event handler such that the browser will prompt the user before they reload or navigate away from the page. + +## Usage + +```js +import { promptOnNavigate } from "@18f/identity-prompt-on-navigate"; + +// Set the onbeforeunload event handler. +const cleanUp = promptOnNavigate(); + +// ...some time later, call cleanUp() to restore any previous onbeforeunload handler and cancel any pending timers (this is important). +cleanUp(); +``` + +## Analytics + +By default, `promptOnNavigate` will call `trackEvent` to log a `User prompted before navigation` event when the onbeforeunload handler is called. It will then log `User prompted before navigation and still on page` events at 5, 15, and 30 seconds after the onbeforeunload handler is called (the `seconds` property on the event will contain the number of seconds since the initial prompt). + +You can customize these intervals by passing a `stillOnPageIntervalsInSeconds` option: + +```js +promptOnNavigate({ + // Log a 'User prompted before navigation and still on page' event 7 and 11 seconds after the initial prompt. + stillOnPageIntervalsInSeconds: [7, 11], +}); +``` diff --git a/app/javascript/packages/prompt-on-navigate/index.spec.ts b/app/javascript/packages/prompt-on-navigate/index.spec.ts new file mode 100644 index 00000000000..bbd4c0bbc48 --- /dev/null +++ b/app/javascript/packages/prompt-on-navigate/index.spec.ts @@ -0,0 +1,144 @@ +import { useSandbox } from '@18f/identity-test-helpers'; +import * as analytics from '@18f/identity-analytics'; +import { PROMPT_EVENT, STILL_ON_PAGE_EVENT, promptOnNavigate } from '.'; + +describe('promptOnNavigate', () => { + const sandbox = useSandbox({ useFakeTimers: true }); + + it('prompts on navigate', () => { + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + window.dispatchEvent(event); + + expect(event.defaultPrevented).to.be.true(); + expect(event.returnValue).to.be.false(); + }); + + it('logs an event', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.calledOnceWith(PROMPT_EVENT); + }); + + it('logs a second event when the user stays on the page for 5s', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.calledOnceWith(PROMPT_EVENT); + trackEvent.resetHistory(); + + sandbox.clock.tick(2000); + expect(trackEvent).not.to.have.been.called(); + + sandbox.clock.tick(3000); + expect(trackEvent).to.have.been.calledWith(STILL_ON_PAGE_EVENT, { + seconds: 5, + }); + }); + + it('logs a third event when the user stays on the page for 15s', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.calledOnceWith(PROMPT_EVENT); + trackEvent.resetHistory(); + + sandbox.clock.tick(5000); + expect(trackEvent).to.have.been.called(); + trackEvent.resetHistory(); + + sandbox.clock.tick(10000); + expect(trackEvent).to.have.been.calledWith(STILL_ON_PAGE_EVENT, { + seconds: 15, + }); + }); + + it('logs a fourth event when the user stays on the page for 30s', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.called(); + trackEvent.resetHistory(); + + sandbox.clock.tick(5000); + expect(trackEvent).to.have.been.called(); + trackEvent.resetHistory(); + + sandbox.clock.tick(10000); + expect(trackEvent).to.have.been.called(); + trackEvent.resetHistory(); + + sandbox.clock.tick(15000); + expect(trackEvent).to.have.been.calledWith(STILL_ON_PAGE_EVENT, { + seconds: 30, + }); + }); + + it('cleans up after itself', () => { + window.onbeforeunload = null; + + const cleanup = promptOnNavigate(); + + expect(window.onbeforeunload).not.to.be.null(); + + cleanup(); + + expect(window.onbeforeunload).to.be.null(); + }); + + it("does not clean up someone else's handler", () => { + const clean = promptOnNavigate(); + const custom = () => {}; + window.onbeforeunload = custom; + clean(); + expect(window.onbeforeunload).to.eql(custom); + }); + + it('does not fire second analytics event after cleanup', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + const cleanup = promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.calledOnceWith(PROMPT_EVENT); + trackEvent.resetHistory(); + + sandbox.clock.tick(2000); + expect(trackEvent).not.to.have.been.called(); + + cleanup(); + + sandbox.clock.tick(10000); + expect(trackEvent).not.to.have.been.called(); + }); + + it('does not throw if you call cleanup a bunch', () => { + const cleanup = promptOnNavigate(); + for (let i = 0; i < 10; i++) { + cleanup(); + } + }); +}); diff --git a/app/javascript/packages/prompt-on-navigate/index.ts b/app/javascript/packages/prompt-on-navigate/index.ts new file mode 100644 index 00000000000..60461e35c86 --- /dev/null +++ b/app/javascript/packages/prompt-on-navigate/index.ts @@ -0,0 +1,70 @@ +import { trackEvent } from '@18f/identity-analytics'; + +export type PromptOnNavigateOptions = { + stillOnPageIntervalsInSeconds: number[]; +}; + +const defaults = { + stillOnPageIntervalsInSeconds: [5, 15, 30], +}; + +export const PROMPT_EVENT = 'User prompted before navigation'; + +export const STILL_ON_PAGE_EVENT = 'User prompted before navigation and still on page'; + +/** + * Configures the window.onbeforeunload handler such that the user will be prompted before + * reloading or navigating away + * @param options {PromptOnNavigateOptions} + * @returns {() => void} A function that, when called, will "clean up" -- restore the prior onbeforeunload handler and cancel any pending timeouts. + */ +export function promptOnNavigate(options: PromptOnNavigateOptions = defaults): () => void { + let stillOnPageTimer: number | undefined; + + function handleBeforeUnload(ev: BeforeUnloadEvent) { + ev.preventDefault(); + ev.returnValue = ''; + + trackEvent(PROMPT_EVENT); + + const stillOnPageIntervalsInSeconds = [...options.stillOnPageIntervalsInSeconds]; + let elapsed = 0; + + function scheduleNextStillOnPagePing() { + const interval = stillOnPageIntervalsInSeconds.shift(); + if (interval === undefined) { + return; + } + + if (stillOnPageTimer) { + clearTimeout(stillOnPageTimer); + stillOnPageTimer = undefined; + } + + const offsetFromNow = interval - elapsed; + elapsed = interval; + + stillOnPageTimer = window.setTimeout(() => { + trackEvent(STILL_ON_PAGE_EVENT, { + seconds: elapsed, + }); + scheduleNextStillOnPagePing(); + }, offsetFromNow * 1000); + } + + scheduleNextStillOnPagePing(); + } + + const prevHandler = window.onbeforeunload; + window.onbeforeunload = handleBeforeUnload; + + return () => { + if (window.onbeforeunload === handleBeforeUnload) { + window.onbeforeunload = prevHandler; + } + if (stillOnPageTimer) { + window.clearTimeout(stillOnPageTimer); + stillOnPageTimer = undefined; + } + }; +} diff --git a/app/javascript/packages/prompt-on-navigate/package.json b/app/javascript/packages/prompt-on-navigate/package.json new file mode 100644 index 00000000000..eeef56c2de5 --- /dev/null +++ b/app/javascript/packages/prompt-on-navigate/package.json @@ -0,0 +1,5 @@ +{ + "name": "@18f/identity-prompt-on-navigate", + "version": "1.0.0", + "private": true +} diff --git a/app/javascript/packages/test-helpers/use-sandbox.ts b/app/javascript/packages/test-helpers/use-sandbox.ts index 8899a8cac84..1c3c68f9677 100644 --- a/app/javascript/packages/test-helpers/use-sandbox.ts +++ b/app/javascript/packages/test-helpers/use-sandbox.ts @@ -22,12 +22,32 @@ function useSandbox(config?: Partial) { sandbox.clock.restore(); } + // useFakeTimers overrides global.setTimeout, etc. (callable as setTimeout()), but does not + // override window.setTimeout. So we'll do that. + const originalWindowMethods = ( + [ + 'clearImmediate', + 'clearInterval', + 'clearTimeout', + 'setImmediate', + 'setInterval', + 'setTimeout', + ] as const + ).reduce((methods, method) => { + methods[method] = window[method]; + return methods; + }, {}); + beforeEach(() => { // useFakeTimers overrides global timer functions as soon as sandbox is created, thus leaking // across tests. Instead, wait until tests start to initialize. if (useFakeTimers) { Object.assign(clockImpl, sandbox.useFakeTimers()); } + + Object.keys(originalWindowMethods).forEach((method) => { + window[method] = global[method]; + }); }); afterEach(() => { @@ -36,6 +56,10 @@ function useSandbox(config?: Partial) { if (useFakeTimers) { sandbox.clock.restore(); + + Object.keys(originalWindowMethods).forEach((method) => { + window[method] = originalWindowMethods[method]; + }); } }); diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 669677b7ad4..b0a1a0b59ac 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -14,7 +14,8 @@ class GetUspsProofingResultsJob < ApplicationJob queue_as :long_running def perform(_now) - return true unless IdentityConfig.store.in_person_proofing_enabled + return true unless ipp_enabled? + return true if ipp_ready_job_enabled? @enrollment_outcomes = { enrollments_checked: 0, @@ -35,23 +36,16 @@ def perform(_now) analytics.idv_in_person_usps_proofing_results_job_started( enrollments_count: enrollments.count, reprocess_delay_minutes: reprocess_delay_minutes, + job_name: self.class.name, ) check_enrollments(enrollments) - percent_enrollments_errored = 0 - if enrollment_outcomes[:enrollments_checked] > 0 - percent_enrollments_errored = - (enrollment_outcomes[:enrollments_errored].fdiv( - enrollment_outcomes[:enrollments_checked], - ) * 100).round(2) - end - analytics.idv_in_person_usps_proofing_results_job_completed( **enrollment_outcomes, duration_seconds: (Time.zone.now - started_at).seconds.round(2), - # Calculate % of errored enrollments - percent_enrollments_errored:, + percent_enrollments_errored: percent_errored, + job_name: self.class.name, ) true @@ -69,6 +63,14 @@ def proofer @proofer ||= UspsInPersonProofing::Proofer.new end + def ipp_enabled? + IdentityConfig.store.in_person_proofing_enabled == true + end + + def ipp_ready_job_enabled? + IdentityConfig.store.in_person_enrollments_ready_job_enabled == true + end + def check_enrollments(enrollments) last_enrollment_index = enrollments.length - 1 enrollments.each_with_index do |enrollment, idx| @@ -92,14 +94,10 @@ def check_enrollment(enrollment) rescue Faraday::BadRequestError => err # 400 status code. This is used for some status updates and some common client errors handle_bad_request_error(err, enrollment) - rescue Faraday::ClientError, Faraday::ServerError => err - # 4xx or 5xx status code. These are unexpected but will have some sort of - # response body that we can try to log data from + rescue Faraday::ClientError, Faraday::ServerError, Faraday::Error => err + # 4xx, 5xx and any other Faraday error besides a 400 status code. + # These errors may or may not have a response body that we can pull info from. handle_client_or_server_error(err, enrollment) - rescue Faraday::Error => err - # Timeouts, failed connections, parsing errors, and other HTTP errors. These - # generally won't have a response body - handle_faraday_error(err, enrollment) rescue StandardError => err handle_standard_error(err, enrollment) else @@ -113,48 +111,15 @@ def analytics(user: AnonymousUser.new) Analytics.new(user: user, request: nil, session: {}, sp: nil) end - def email_analytics_attributes(enrollment) - { - enrollment_code: enrollment.enrollment_code, - timestamp: Time.zone.now, - service_provider: enrollment.issuer, - wait_until: mail_delivery_params(enrollment.proofed_at)[:wait_until], - } - end - - def enrollment_analytics_attributes(enrollment, complete:) - { - enrollment_code: enrollment.enrollment_code, - enrollment_id: enrollment.id, - minutes_since_last_status_check: enrollment.minutes_since_last_status_check, - minutes_since_last_status_check_completed: - enrollment.minutes_since_last_status_check_completed, - minutes_since_last_status_update: enrollment.minutes_since_last_status_update, - minutes_since_established: enrollment.minutes_since_established, - minutes_to_completion: complete ? enrollment.minutes_since_established : nil, - issuer: enrollment.issuer, - } - end - - def response_analytics_attributes(response) - return { response_present: false } unless response.present? - - { - fraud_suspected: response['fraudSuspected'], - primary_id_type: response['primaryIdType'], - secondary_id_type: response['secondaryIdType'], - failure_reason: response['failureReason'], - transaction_end_date_time: parse_usps_timestamp(response['transactionEndDateTime']), - transaction_start_date_time: parse_usps_timestamp(response['transactionStartDateTime']), - status: response['status'], - assurance_level: response['assuranceLevel'], - proofing_post_office: response['proofingPostOffice'], - proofing_city: response['proofingCity'], - proofing_state: response['proofingState'], - scan_count: response['scanCount'], - response_message: response['responseMessage'], - response_present: true, - } + def percent_errored + error_rate = 0 + if enrollment_outcomes[:enrollments_checked] > 0 + error_rate = + (enrollment_outcomes[:enrollments_errored].fdiv( + enrollment_outcomes[:enrollments_checked], + ) * 100).round(2) + end + error_rate end def handle_bad_request_error(err, enrollment) @@ -178,16 +143,7 @@ def handle_bad_request_error(err, enrollment) 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_body), - exception_class: err.class.to_s, - exception_message: err.message, - reason: 'Request exception', - response_status_code: err.response_status, - ) - enrollment_outcomes[:enrollments_errored] += 1 + handle_client_or_server_error(err, enrollment) end end @@ -200,21 +156,7 @@ def handle_client_or_server_error(err, enrollment) exception_message: err.message, reason: 'Request exception', response_status_code: err.response_status, - ) - enrollment_outcomes[:enrollments_errored] += 1 - end - - def handle_faraday_error(err, enrollment) - NewRelic::Agent.notice_error(err) - analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( - **enrollment_analytics_attributes(enrollment, complete: false), - # There probably isn't a response body or status for these types of errors but we try to log - # them in case there is - **response_analytics_attributes(err.response_body), - response_status_code: err.response_status, - exception_class: err.class.to_s, - exception_message: err.message, - reason: 'Request exception', + job_name: self.class.name, ) enrollment_outcomes[:enrollments_errored] += 1 end @@ -222,32 +164,35 @@ def handle_faraday_error(err, enrollment) def handle_standard_error(err, enrollment) NewRelic::Agent.notice_error(err) response_attributes = response_analytics_attributes(nil) - enrollment_outcomes[:enrollments_errored] += 1 analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( **enrollment_analytics_attributes(enrollment, complete: false), **response_attributes, exception_class: err.class.to_s, exception_message: err.message, reason: 'Request exception', + job_name: self.class.name, ) + enrollment_outcomes[:enrollments_errored] += 1 end def handle_response_is_not_a_hash(enrollment) - enrollment_outcomes[:enrollments_errored] += 1 analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( **enrollment_analytics_attributes(enrollment, complete: false), reason: 'Bad response structure', + job_name: self.class.name, ) + enrollment_outcomes[:enrollments_errored] += 1 end def handle_unsupported_status(enrollment, response) - enrollment_outcomes[:enrollments_errored] += 1 analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( **enrollment_analytics_attributes(enrollment, complete: false), **response_analytics_attributes(response), reason: 'Unsupported status', status: response['status'], + job_name: self.class.name, ) + enrollment_outcomes[:enrollments_errored] += 1 end def handle_unsupported_id_type(enrollment, response) @@ -259,6 +204,7 @@ def handle_unsupported_id_type(enrollment, response) passed: false, primary_id_type: response['primaryIdType'], reason: 'Unsupported ID type', + job_name: self.class.name, ) enrollment.update( status: :failed, @@ -270,6 +216,7 @@ def handle_unsupported_id_type(enrollment, response) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Failed unsupported ID type', + job_name: self.class.name, ) end @@ -279,8 +226,10 @@ def handle_incomplete_status_update(enrollment, response_message) idv_in_person_usps_proofing_results_job_enrollment_incomplete( **enrollment_analytics_attributes(enrollment, complete: false), response_message: response_message, + job_name: self.class.name, ) enrollment.update(status_check_completed_at: Time.zone.now) + enrollment.update(status_check_completed_at: Time.zone.now) end def handle_expired_status_update(enrollment, response, response_message) @@ -290,6 +239,7 @@ def handle_expired_status_update(enrollment, response, response_message) **response_analytics_attributes(response[:body]), passed: false, reason: 'Enrollment has expired', + job_name: self.class.name, ) enrollment.update( status: :expired, @@ -305,12 +255,14 @@ def handle_expired_status_update(enrollment, response, response_message) enrollment_id: enrollment.id, exception_class: err.class.to_s, exception_message: err.message, + job_name: self.class.name, ) else analytics(user: enrollment.user). idv_in_person_usps_proofing_results_job_deadline_passed_email_initiated( **email_analytics_attributes(enrollment), enrollment_id: enrollment.id, + job_name: self.class.name, ) enrollment.update(deadline_passed_sent: true) end @@ -337,6 +289,7 @@ def handle_unexpected_response(enrollment, response_message, reason:, cancel: tr **enrollment_analytics_attributes(enrollment, complete: cancel), response_message: response_message, reason: reason, + job_name: self.class.name, ) end @@ -348,6 +301,7 @@ def handle_failed_status(enrollment, response) **response_analytics_attributes(response), passed: false, reason: 'Failed status', + job_name: self.class.name, ) enrollment.update( @@ -360,12 +314,14 @@ def handle_failed_status(enrollment, response) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Failed fraud suspected', + job_name: self.class.name, ) else send_failed_email(enrollment.user, enrollment) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Failed', + job_name: self.class.name, ) end end @@ -378,6 +334,7 @@ def handle_successful_status_update(enrollment, response) **response_analytics_attributes(response), passed: true, reason: 'Successful status update', + job_name: self.class.name, ) enrollment.profile.activate_after_passing_in_person enrollment.update( @@ -389,6 +346,7 @@ def handle_successful_status_update(enrollment, response) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Success', + job_name: self.class.name, ) end @@ -400,6 +358,7 @@ def handle_unsupported_secondary_id(enrollment, response) **response_analytics_attributes(response), passed: false, reason: 'Provided secondary proof of address', + job_name: self.class.name, ) enrollment.update( status: :failed, @@ -410,6 +369,7 @@ def handle_unsupported_secondary_id(enrollment, response) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Failed unsupported secondary ID', + job_name: self.class.name, ) end @@ -484,6 +444,50 @@ def mail_delivery_params(proofed_at) return { wait_until: wait_until, queue: :intentionally_delayed } end + def email_analytics_attributes(enrollment) + { + enrollment_code: enrollment.enrollment_code, + timestamp: Time.zone.now, + service_provider: enrollment.issuer, + wait_until: mail_delivery_params(enrollment.proofed_at)[:wait_until], + } + end + + def enrollment_analytics_attributes(enrollment, complete:) + { + enrollment_code: enrollment.enrollment_code, + enrollment_id: enrollment.id, + minutes_since_last_status_check: enrollment.minutes_since_last_status_check, + minutes_since_last_status_check_completed: + enrollment.minutes_since_last_status_check_completed, + minutes_since_last_status_update: enrollment.minutes_since_last_status_update, + minutes_since_established: enrollment.minutes_since_established, + minutes_to_completion: complete ? enrollment.minutes_since_established : nil, + issuer: enrollment.issuer, + } + end + + def response_analytics_attributes(response) + return { response_present: false } unless response.present? + + { + fraud_suspected: response['fraudSuspected'], + primary_id_type: response['primaryIdType'], + secondary_id_type: response['secondaryIdType'], + failure_reason: response['failureReason'], + transaction_end_date_time: parse_usps_timestamp(response['transactionEndDateTime']), + transaction_start_date_time: parse_usps_timestamp(response['transactionStartDateTime']), + status: response['status'], + assurance_level: response['assuranceLevel'], + proofing_post_office: response['proofingPostOffice'], + proofing_city: response['proofingCity'], + proofing_state: response['proofingState'], + scan_count: response['scanCount'], + response_message: response['responseMessage'], + response_present: true, + } + end + def parse_usps_timestamp(usps_timestamp) return unless usps_timestamp # Parse timestamps eg 12/17/2020 033855 => Thu, 17 Dec 2020 03:38:55 -0600 diff --git a/app/jobs/get_usps_ready_proofing_results_job.rb b/app/jobs/get_usps_ready_proofing_results_job.rb new file mode 100644 index 00000000000..89d63b6c293 --- /dev/null +++ b/app/jobs/get_usps_ready_proofing_results_job.rb @@ -0,0 +1,40 @@ +class GetUspsReadyProofingResultsJob < GetUspsProofingResultsJob + queue_as :long_running + + def perform(_now) + return true unless ipp_enabled? && ipp_ready_job_enabled? + + @enrollment_outcomes = { + enrollments_checked: 0, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 0, + } + + reprocess_delay_minutes = IdentityConfig.store. + get_usps_proofing_results_job_reprocess_delay_minutes + enrollments = InPersonEnrollment.needs_status_check_on_ready_enrollments( + ...reprocess_delay_minutes.minutes.ago, + ) + + started_at = Time.zone.now + analytics.idv_in_person_usps_proofing_results_job_started( + enrollments_count: enrollments.count, + reprocess_delay_minutes: reprocess_delay_minutes, + job_name: self.class.name, + ) + + check_enrollments(enrollments) + + analytics.idv_in_person_usps_proofing_results_job_completed( + **enrollment_outcomes, + duration_seconds: (Time.zone.now - started_at).seconds.round(2), + percent_enrollments_errored: percent_errored, + job_name: self.class.name, + ) + + true + end +end diff --git a/app/jobs/get_usps_waiting_proofing_results_job.rb b/app/jobs/get_usps_waiting_proofing_results_job.rb new file mode 100644 index 00000000000..a848e3ff540 --- /dev/null +++ b/app/jobs/get_usps_waiting_proofing_results_job.rb @@ -0,0 +1,40 @@ +class GetUspsWaitingProofingResultsJob < GetUspsProofingResultsJob + queue_as :long_running + + def perform(_now) + return true unless ipp_enabled? && ipp_ready_job_enabled? + + @enrollment_outcomes = { + enrollments_checked: 0, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 0, + } + + reprocess_delay_minutes = IdentityConfig.store. + get_usps_proofing_results_job_reprocess_delay_minutes + enrollments = InPersonEnrollment.needs_status_check_on_waiting_enrollments( + ...reprocess_delay_minutes.minutes.ago, + ) + + started_at = Time.zone.now + analytics.idv_in_person_usps_proofing_results_job_started( + enrollments_count: enrollments.count, + reprocess_delay_minutes: reprocess_delay_minutes, + job_name: self.class.name, + ) + + check_enrollments(enrollments) + + analytics.idv_in_person_usps_proofing_results_job_completed( + **enrollment_outcomes, + duration_seconds: (Time.zone.now - started_at).seconds.round(2), + percent_enrollments_errored: percent_errored, + job_name: self.class.name, + ) + + true + end +end diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb index 41a5a2a2e26..46fb9cb6ba9 100644 --- a/app/models/in_person_enrollment.rb +++ b/app/models/in_person_enrollment.rb @@ -63,6 +63,26 @@ def needs_usps_status_check?(check_interval) ) end + # Find enrollments that are ready for a status check via the USPS API + def self.needs_status_check_on_ready_enrollments(check_interval) + needs_usps_status_check(check_interval).where(ready_for_status_check: true) + end + + # Does this ready enrollment need a status check via the USPS API? + def needs_status_check_on_ready_enrollment?(check_interval) + needs_usps_status_check?(check_interval) && ready_for_status_check? + end + + # Find waiting enrollments that need a status check via the USPS API + def self.needs_status_check_on_waiting_enrollments(check_interval) + needs_usps_status_check(check_interval).where(ready_for_status_check: false) + end + + # Does this waiting enrollment need a status check via the USPS API? + def needs_status_check_on_waiting_enrollment?(check_interval) + needs_usps_status_check?(check_interval) && !ready_for_status_check? + end + def minutes_since_established return unless enrollment_established_at.present? (Time.zone.now - enrollment_established_at).seconds.in_minutes.round(2) diff --git a/app/models/profile.rb b/app/models/profile.rb index 3c00b3ac313..d113a180ec1 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -92,6 +92,15 @@ def activate_after_passing_in_person activate end + def activate_after_password_reset + if password_reset? + update!( + deactivation_reason: nil, + ) + activate + end + end + def deactivate(reason) update!(active: false, deactivation_reason: reason) end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 457a6dd8406..9f6bd0551c9 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1,6 +1,33 @@ # frozen_string_literal: true +# ______________________________________ +# / Adding something new in here? Please \ +# \ keep methods sorted alphabetically. / +# -------------------------------------- +# \ ^__^ +# \ (oo)\_______ +# (__)\ )\/\ +# ||----w | +# || || + module AnalyticsEvents + # @param [Boolean] success + # When a user submits a form to delete their account + def account_delete_submitted(success:, **extra) + track_event('Account Delete submitted', success: success, **extra) + end + + # When a user visits the page to delete their account + def account_delete_visited + track_event('Account Delete visited') + end + + # @param [String] request_came_from the controller/action the request came from + # When a user deletes their account + def account_deletion(request_came_from:, **extra) + track_event('Account Deletion Requested', request_came_from: request_came_from, **extra) + end + # @identity.idp.previous_event_name Account Reset # @param [String] user_id # @param [String, nil] message_id from AWS Pinpoint API @@ -18,6 +45,19 @@ def account_reset_cancel(user_id:, message_id: nil, request_id: nil, **extra) ) end + # @identity.idp.previous_event_name Account Reset + # @param [String] user_id + # @param [Hash] errors + # Validates the token used for cancelling an account reset + def account_reset_cancel_token_validation(user_id:, errors: nil, **extra) + track_event( + 'Account Reset: cancel token validation', + user_id: user_id, + errors: errors, + **extra, + ) + end + # @identity.idp.previous_event_name Account Reset # @param [Boolean] success # @param [String] user_id @@ -45,6 +85,31 @@ def account_reset_delete( ) end + # @identity.idp.previous_event_name Account Reset + # @param [String] user_id + # @param [Hash] errors + # Validates the granted token for account reset + def account_reset_granted_token_validation(user_id: nil, errors: nil, **extra) + track_event( + 'Account Reset: granted token validation', + user_id: user_id, + errors: errors, + **extra, + ) + end + + # @identity.idp.previous_event_name Account Reset + # @param [Integer] count number of email notifications sent + # Account reset was performed, logs the number of email notifications sent + def account_reset_notifications(count:, **extra) + track_event('Account Reset: notifications', count: count, **extra) + end + + # Tracks users visiting the recovery options page + def account_reset_recovery_options_visit + track_event('Account Reset: Recovery Options Visited') + end + # @identity.idp.previous_event_name Account Reset # @param [Boolean] success # @param [Boolean] sms_phone does the user have a phone factor configured? @@ -79,61 +144,11 @@ def account_reset_request( ) end - # @identity.idp.previous_event_name Account Reset - # @param [String] user_id - # @param [Hash] errors - # Validates the token used for cancelling an account reset - def account_reset_cancel_token_validation(user_id:, errors: nil, **extra) - track_event( - 'Account Reset: cancel token validation', - user_id: user_id, - errors: errors, - **extra, - ) - end - - # @identity.idp.previous_event_name Account Reset - # @param [String] user_id - # @param [Hash] errors - # Validates the granted token for account reset - def account_reset_granted_token_validation(user_id: nil, errors: nil, **extra) - track_event( - 'Account Reset: granted token validation', - user_id: user_id, - errors: errors, - **extra, - ) - end - - # @identity.idp.previous_event_name Account Reset - # @param [Integer] count number of email notifications sent - # Account reset was performed, logs the number of email notifications sent - def account_reset_notifications(count:, **extra) - track_event('Account Reset: notifications', count: count, **extra) - end - # User visited the account deletion and reset page def account_reset_visit track_event('Account deletion and reset visited') end - # @param [Boolean] success - # When a user submits a form to delete their account - def account_delete_submitted(success:, **extra) - track_event('Account Delete submitted', success: success, **extra) - end - - # When a user visits the page to delete their account - def account_delete_visited - track_event('Account Delete visited') - end - - # @param [String] request_came_from the controller/action the request came from - # When a user deletes their account - def account_deletion(request_came_from:, **extra) - track_event('Account Deletion Requested', request_came_from: request_came_from, **extra) - end - # When a user views the account page def account_visit track_event('Account Page Visited') @@ -146,6 +161,25 @@ def add_email_confirmation(user_id:, success: nil, **extra) track_event('Add Email: Email Confirmation', user_id: user_id, success: success, **extra) end + # @param [Boolean] success + # @param [Hash] errors + # Tracks request for adding new emails to an account + def add_email_request(success:, errors:, **extra) + track_event( + 'Add Email Requested', + success: success, + errors: errors, + **extra, + ) + end + + # Tracks When users visit the add phone page + def add_phone_setup_visit + track_event( + 'Phone Setup Visited', + ) + end + # When a user views the "you are already signed in with the following email" screen def authentication_confirmation track_event('Authentication Confirmation') @@ -220,6 +254,27 @@ def broken_personal_key_regenerated track_event('Broken Personal Key: Regenerated') end + # Tracks users going back or cancelling acoount recovery + def cancel_account_reset_recovery + track_event('Account Reset: Cancel Account Recovery Options') + end + + # @param [String] redirect_url URL user was directed to + # @param [String, nil] step which step + # @param [String, nil] location which part of a step, if applicable + # @param ["idv", String, nil] flow which flow + # User was redirected to the login.gov contact page + def contact_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extra) + track_event( + 'Contact Page Redirect', + redirect_url: redirect_url, + step: step, + location: location, + flow: flow, + **extra, + ) + end + # @param [String, nil] error error message # @param [String, nil] uuid document capture session uuid # @param [String, nil] result_id document capture session result id @@ -280,39 +335,29 @@ def email_deletion_request(success:, errors:, **extra) # @param [Boolean] success # @param [Hash] errors - # Tracks request for adding new emails to an account - def add_email_request(success:, errors:, **extra) + # Tracks if Email Language is updated + def email_language_updated(success:, errors:, **extra) track_event( - 'Add Email Requested', + 'Email Language: Updated', success: success, errors: errors, **extra, ) end - # @param [Boolean] success - # Tracks request for resending confirmation for new emails to an account - def resend_add_email_request(success:, **extra) - track_event( - 'Resend Add Email Requested', - success: success, - **extra, - ) - end - # Tracks if Email Language is visited def email_language_visited track_event('Email Language: Visited') end - # @param [Boolean] success - # @param [Hash] errors - # Tracks if Email Language is updated - def email_language_updated(success:, errors:, **extra) + # Logs after an email is sent + # @param [String] action type of email being sent + # @param [String, nil] ses_message_id AWS SES Message ID + def email_sent(action:, ses_message_id:, **extra) track_event( - 'Email Language: Updated', - success: success, - errors: errors, + 'Email Sent', + action: action, + ses_message_id: ses_message_id, **extra, ) end @@ -489,55 +534,31 @@ def idv_address_visit track_event('IdV: address visited') end - # @param [String] step the step that the user was on when they clicked cancel - # @param [String] request_came_from the controller and action from the - # source such as "users/sessions#new" - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user clicked cancel during IDV (presented with an option to go back or confirm) - def idv_cancellation_visited( - step:, - request_came_from:, - proofing_components: nil, - **extra - ) - track_event( - 'IdV: cancellation visited', - step: step, - request_came_from: request_came_from, - proofing_components: proofing_components, - **extra, - ) - end - - # @param [Integer] failed_capture_attempts Number of failed Acuant SDK attempts - # @param [Integer] failed_submission_attempts Number of failed Acuant doc submissions - # @param [String] field Image form field - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The number of acceptable failed attempts (maxFailedAttemptsBeforeNativeCamera) has been met - # or exceeded, and the system has forced the use of the native camera, rather than Acuant's - # camera, on mobile devices. - def idv_native_camera_forced( - failed_capture_attempts:, - failed_submission_attempts:, - field:, - flow_path:, + # Tracks if request to get address candidates from ArcGIS fails + # @param [String] exception_class + # @param [String] exception_message + # @param [Boolean] response_body_present + # @param [Hash] response_body + # @param [Integer] response_status_code + def idv_arcgis_request_failure( + exception_class:, + exception_message:, + response_body_present:, + response_body:, + response_status_code:, **extra ) track_event( - 'IdV: Native camera forced after failed attempts', - failed_capture_attempts: failed_capture_attempts, - failed_submission_attempts: failed_submission_attempts, - field: field, - flow_path: flow_path, + 'Request ArcGIS Address Candidates: request failed', + exception_class: exception_class, + exception_message: exception_message, + response_body_present: response_body_present, + response_body: response_body, + response_status_code: response_status_code, **extra, ) end - # The user visited the gpo confirm cancellation screen - def idv_gpo_confirm_start_over_visited - track_event('IdV: gpo confirm start over visited') - end - # @param [String] step the step that the user was on when they clicked cancel # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components # The user confirmed their choice to cancel going through IDV @@ -562,3358 +583,3364 @@ def idv_cancellation_go_back(step:, proofing_components: nil, **extra) ) end - # The user checked or unchecked the "By checking this box..." checkbox on the idv agreement step. - # (This is a frontend event.) - # @param [Boolean] checked Whether the user checked the checkbox - def idv_consent_checkbox_toggled(checked:, **extra) - track_event( - 'IdV: consent checkbox toggled', - checked: checked, - **extra, - ) - end - - # The user visited the "come back later" page shown during the GPO mailing flow + # @param [String] step the step that the user was on when they clicked cancel + # @param [String] request_came_from the controller and action from the + # source such as "users/sessions#new" # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - def idv_come_back_later_visit(proofing_components: nil, **extra) - track_event( - 'IdV: come back later visited', - proofing_components: proofing_components, - **extra, - ) - end - - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # @param [String] in_person_cta_variant Variant testing bucket label - # The user clicked the troubleshooting option to start in-person proofing - def idv_verify_in_person_troubleshooting_option_clicked(flow_path:, in_person_cta_variant:, - **extra) + # The user clicked cancel during IDV (presented with an option to go back or confirm) + def idv_cancellation_visited( + step:, + request_came_from:, + proofing_components: nil, + **extra + ) track_event( - 'IdV: verify in person troubleshooting option clicked', - flow_path: flow_path, - in_person_cta_variant: in_person_cta_variant, + 'IdV: cancellation visited', + step: step, + request_came_from: request_came_from, + proofing_components: proofing_components, **extra, ) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # @param [String] in_person_cta_variant Variant testing bucket label - # The user visited the in person proofing location step - def idv_in_person_location_visited(flow_path:, in_person_cta_variant:, **extra) + # The user visited the "come back later" page shown during the GPO mailing flow + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + def idv_come_back_later_visit(proofing_components: nil, **extra) track_event( - 'IdV: in person proofing location visited', - flow_path: flow_path, - in_person_cta_variant: in_person_cta_variant, + 'IdV: come back later visited', + proofing_components: proofing_components, **extra, ) end - # @param [Boolean] success - # @param [Integer] result_total - # @param [String] errors - # @param [String] exception_class - # @param [String] exception_message - # @param [Integer] response_status_code - # User submitted a search on the location search page and response received - def idv_in_person_locations_searched( - success:, - result_total: 0, - errors: nil, - exception_class: nil, - exception_message: nil, - response_status_code: nil, - **extra - ) + # The user checked or unchecked the "By checking this box..." checkbox on the idv agreement step. + # (This is a frontend event.) + # @param [Boolean] checked Whether the user checked the checkbox + def idv_consent_checkbox_toggled(checked:, **extra) track_event( - 'IdV: in person proofing location search submitted', - success: success, - result_total: result_total, - errors: errors, - exception_class: exception_class, - exception_message: exception_message, - response_status_code: response_status_code, + 'IdV: consent checkbox toggled', + checked: checked, **extra, ) end - # @param [String] selected_location Selected in-person location - # @param [String] in_person_cta_variant Variant testing bucket label - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The user submitted the in person proofing location step - def idv_in_person_location_submitted(selected_location:, in_person_cta_variant:, flow_path:, - **extra) - track_event( - 'IdV: in person proofing location submitted', - selected_location: selected_location, - flow_path: flow_path, - in_person_cta_variant: in_person_cta_variant, - **extra, - ) + # User has consented to share information with document upload and may + # view the "hybrid handoff" step next unless "skip_upload" param is true + def idv_doc_auth_agreement_submitted(**extra) + track_event('IdV: doc auth agreement submitted', **extra) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The user visited the in person proofing prepare step - def idv_in_person_prepare_visited(flow_path:, **extra) - track_event('IdV: in person proofing prepare visited', flow_path: flow_path, **extra) + def idv_doc_auth_agreement_visited(**extra) + track_event('IdV: doc auth agreement visited', **extra) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # @param [String] in_person_cta_variant Variant testing bucket label - # The user submitted the in person proofing prepare step - def idv_in_person_prepare_submitted(flow_path:, in_person_cta_variant:, **extra) + def idv_doc_auth_cancel_link_sent_submitted(**extra) + track_event('IdV: doc auth cancel_link_sent submitted', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing cancel_update_ssn submitted + def idv_doc_auth_cancel_update_ssn_submitted(**extra) + track_event('IdV: doc auth cancel_update_ssn submitted', **extra) + end + + def idv_doc_auth_capture_complete_visited(**extra) + track_event('IdV: doc auth capture_complete visited', **extra) + end + + def idv_doc_auth_document_capture_submitted(**extra) + track_event('IdV: doc auth document_capture submitted', **extra) + end + + def idv_doc_auth_document_capture_visited(**extra) + track_event('IdV: doc auth document_capture visited', **extra) + end + + # @param [String] step_name which step the user was on + # @param [Integer] remaining_attempts how many attempts the user has left before we throttle them + # The user visited an error page due to an encountering an exception talking to a proofing vendor + def idv_doc_auth_exception_visited(step_name:, remaining_attempts:, **extra) track_event( - 'IdV: in person proofing prepare submitted', - flow_path: flow_path, - in_person_cta_variant: in_person_cta_variant, + 'IdV: doc auth exception visited', + step_name: step_name, + remaining_attempts: remaining_attempts, **extra, ) end - # @param [String] nontransliterable_characters - # Nontransliterable characters submitted by user - def idv_in_person_proofing_nontransliterable_characters_submitted( - nontransliterable_characters:, - **extra - ) + # @identity.idp.previous_event_name IdV: doc auth send_link submitted + def idv_doc_auth_link_sent_submitted(**extra) + track_event('IdV: doc auth link_sent submitted', **extra) + end + + def idv_doc_auth_link_sent_visited(**extra) + track_event('IdV: doc auth link_sent visited', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing optional verify_wait submitted + def idv_doc_auth_optional_verify_wait_submitted(**extra) + track_event('IdV: doc auth optional verify_wait submitted', **extra) + end + + def idv_doc_auth_randomizer_defaulted track_event( - 'IdV: in person proofing characters submitted could not be transliterated', - nontransliterable_characters: nontransliterable_characters, - **extra, + 'IdV: doc_auth random vendor error', + error: 'document_capture_session_uuid_key missing', ) end - def idv_in_person_proofing_residential_address_submitted(**extra) - track_event('IdV: in person proofing residential address submitted', **extra) + # @identity.idp.previous_event_name IdV: in person proofing redo_address submitted + def idv_doc_auth_redo_address_submitted(**extra) + track_event('IdV: doc auth redo_address submitted', **extra) + end + + def idv_doc_auth_redo_document_capture_submitted(**extra) + track_event('IdV: doc auth redo_document_capture submitted', **extra) + end + + def idv_doc_auth_redo_ssn_submitted(**extra) + track_event('IdV: doc auth redo_ssn submitted', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing ssn submitted + def idv_doc_auth_ssn_submitted(**extra) + track_event('IdV: doc auth ssn submitted', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing ssn visited + def idv_doc_auth_ssn_visited(**extra) + track_event('IdV: doc auth ssn visited', **extra) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing # @param [Boolean] success # @param [Hash] errors - # @param [Boolean] same_address_as_id - # address submitted by user - def idv_in_person_proofing_address_submitted( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, + # @param [Integer] attempts + # @param [Integer] remaining_attempts + # @param [String] user_id + # @param [String] flow_path + # The document capture image uploaded was locally validated during the IDV process + def idv_doc_auth_submitted_image_upload_form( + success:, + errors:, + remaining_attempts:, + flow_path:, + attempts: nil, + user_id: nil, **extra ) track_event( - 'IdV: in person proofing address submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, + 'IdV: doc auth image upload form submitted', success: success, errors: errors, - same_address_as_id: same_address_as_id, + attempts: attempts, + remaining_attempts: remaining_attempts, + user_id: user_id, + flow_path: flow_path, **extra, ) end + # @param [Boolean] success + # @param [Hash] errors + # @param [String] exception + # @param [Boolean] billed + # @param [String] doc_auth_result + # @param [String] state + # @param [String] state_id_type + # @param [Boolean] async + # @param [Integer] attempts + # @param [Integer] remaining_attempts + # @param [Hash] client_image_metrics # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # address page visited - def idv_in_person_proofing_address_visited( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, + # The document capture image was uploaded to vendor during the IDV process + def idv_doc_auth_submitted_image_upload_vendor( + success:, + errors:, + exception:, + state:, + state_id_type:, + async:, attempts:, + remaining_attempts:, + client_image_metrics:, + flow_path:, + billed: nil, + doc_auth_result: nil, **extra ) track_event( - 'IdV: in person proofing address visited', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, - **extra, - ) - end - - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean, nil] same_address_as_id - # User clicked cancel on update address page - def idv_in_person_proofing_cancel_update_address( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, - **extra - ) - track_event( - 'IdV: in person proofing cancel_update_address submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, + 'IdV: doc auth image upload vendor submitted', success: success, errors: errors, - same_address_as_id: same_address_as_id, + exception: exception, + billed: billed, + doc_auth_result: doc_auth_result, + state: state, + state_id_type: state_id_type, + async: async, + attempts: attempts, + remaining_attempts: remaining_attempts, + client_image_metrics: client_image_metrics, + flow_path: flow_path, **extra, ) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing # @param [Boolean] success # @param [Hash] errors - # @param [Boolean] same_address_as_id - # User clicked cancel on update state id page - def idv_in_person_proofing_cancel_update_state_id( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, + # @param [String] user_id + # @param [Integer] remaining_attempts + # @param [Hash] pii_like_keypaths + # @param [String] flow_path + # The PII that came back from the document capture vendor was validated + def idv_doc_auth_submitted_pii_validation( + success:, + errors:, + remaining_attempts:, + pii_like_keypaths:, + flow_path:, + user_id: nil, **extra ) track_event( - 'IdV: in person proofing cancel_update_state_id submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, + 'IdV: doc auth image upload vendor pii validation', success: success, errors: errors, - same_address_as_id: same_address_as_id, + user_id: user_id, + remaining_attempts: remaining_attempts, + pii_like_keypaths: pii_like_keypaths, + flow_path: flow_path, **extra, ) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean] same_address_as_id - # User submitted state id on redo state id page - def idv_in_person_proofing_redo_state_id_submitted( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, - **extra - ) - track_event( - 'IdV: in person proofing redo_state_id submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, - success: success, - errors: errors, - same_address_as_id: same_address_as_id, - **extra, - ) + # The "hybrid handoff" step: Desktop user has submitted their choice to + # either continue via desktop ("document_capture" destination) or switch + # to mobile phone ("send_link" destination) to perform document upload. + # Mobile users still log this event but with skip_upload_step = true + def idv_doc_auth_upload_submitted(**extra) + track_event('IdV: doc auth upload submitted', **extra) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean, nil] same_address_as_id - # User submitted state id - def idv_in_person_proofing_state_id_submitted( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, - **extra - ) - track_event( - 'IdV: in person proofing state_id submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, - success: success, - errors: errors, - same_address_as_id: same_address_as_id, - **extra, - ) + # Desktop user has reached the above "hybrid handoff" view + def idv_doc_auth_upload_visited(**extra) + track_event('IdV: doc auth upload visited', **extra) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # State id page visited - def idv_in_person_proofing_state_id_visited( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, + # @identity.idp.previous_event_name IdV: doc auth optional verify_wait submitted + def idv_doc_auth_verify_proofing_results(**extra) + track_event('IdV: doc auth verify proofing results', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing verify submitted + def idv_doc_auth_verify_submitted(**extra) + track_event('IdV: doc auth verify submitted', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing verify visited + def idv_doc_auth_verify_visited(**extra) + track_event('IdV: doc auth verify visited', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing verify_wait visited + def idv_doc_auth_verify_wait_step_visited(**extra) + track_event('IdV: doc auth verify_wait visited', **extra) + end + + # @param [String] step_name + # @param [Integer] remaining_attempts + # The user was sent to a warning page during the IDV flow + def idv_doc_auth_warning_visited( + step_name:, + remaining_attempts:, **extra ) track_event( - 'IdV: in person proofing state_id visited', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, + 'IdV: doc auth warning visited', + step_name: step_name, + remaining_attempts: remaining_attempts, **extra, ) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The user visited the in person proofing switch_back step - def idv_in_person_switch_back_visited(flow_path:, **extra) - track_event('IdV: in person proofing switch_back visited', flow_path: flow_path, **extra) + def idv_doc_auth_welcome_submitted(**extra) + track_event('IdV: doc auth welcome submitted', **extra) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The user submitted the in person proofing switch_back step - def idv_in_person_switch_back_submitted(flow_path:, **extra) - track_event('IdV: in person proofing switch_back submitted', flow_path: flow_path, **extra) + def idv_doc_auth_welcome_visited(**extra) + track_event('IdV: doc auth welcome visited', **extra) end - # @param [String] in_person_cta_variant Variant testing bucket label + # @param [Boolean] success + # @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any. + # @param [Boolean] fraud_review_pending Profile is under review for fraud + # @param [Boolean] fraud_rejection Profile is rejected due to fraud + # @param [Boolean] gpo_verification_pending Profile is awaiting gpo verificaiton # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user visited the "ready to verify" page for the in person proofing flow - def idv_in_person_ready_to_verify_visit(in_person_cta_variant: nil, proofing_components: nil, - **extra) + # Tracks the last step of IDV, indicates the user successfully proofed + def idv_final( + success:, + fraud_review_pending:, + fraud_rejection:, + gpo_verification_pending:, + deactivation_reason: nil, + proofing_components: nil, + **extra + ) track_event( - 'IdV: in person ready to verify visited', - in_person_cta_variant: in_person_cta_variant, + 'IdV: final resolution', + success: success, + fraud_review_pending: fraud_review_pending, + fraud_rejection: fraud_rejection, + gpo_verification_pending: gpo_verification_pending, + deactivation_reason: deactivation_reason, proofing_components: proofing_components, **extra, ) end - # The user clicked the sp link on the "ready to verify" page - def idv_in_person_ready_to_verify_sp_link_clicked(**extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User visited forgot password page + def idv_forgot_password(proofing_components: nil, **extra) track_event( - 'IdV: user clicked sp link on ready to verify page', + 'IdV: forgot password visited', + proofing_components: proofing_components, **extra, ) end - # The user clicked the what to bring link on the "ready to verify" page - def idv_in_person_ready_to_verify_what_to_bring_link_clicked(**extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User confirmed forgot password + def idv_forgot_password_confirmed(proofing_components: nil, **extra) track_event( - 'IdV: user clicked what to bring link on ready to verify page', + 'IdV: forgot password confirmed', + proofing_components: proofing_components, **extra, ) end - # A job to check USPS notifications about in-person enrollment status updates has started - def idv_in_person_proofing_enrollments_ready_for_status_check_job_started(**extra) - track_event( - 'InPersonEnrollmentsReadyForStatusCheckJob: Job started', + # @param [DateTime] enqueued_at + # @param [Boolean] resend + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # GPO letter was enqueued and the time at which it was enqueued + def idv_gpo_address_letter_enqueued(enqueued_at:, resend:, proofing_components: nil, **extra) + track_event( + 'IdV: USPS address letter enqueued', + enqueued_at: enqueued_at, + resend: resend, + proofing_components: proofing_components, **extra, ) end - # A job to check USPS notifications about in-person enrollment status updates has completed - # @param [Integer] fetched_items items fetched - # @param [Integer] processed_items items fetched and processed - # @param [Integer] deleted_items items fetched, processed, and then deleted from the queue - # @param [Integer] valid_items items that could be successfully used to update a record - # @param [Integer] invalid_items items that couldn't be used to update a record - # @param [Integer] incomplete_items fetched items not processed nor deleted from the queue - # @param [Integer] deletion_failed_items processed items that we failed to delete - def idv_in_person_proofing_enrollments_ready_for_status_check_job_completed( - fetched_items:, - processed_items:, - deleted_items:, - valid_items:, - invalid_items:, - incomplete_items:, - deletion_failed_items:, - **extra - ) + # @param [Boolean] resend + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # GPO letter was requested + def idv_gpo_address_letter_requested(resend:, proofing_components: nil, **extra) track_event( - 'InPersonEnrollmentsReadyForStatusCheckJob: Job completed', - fetched_items:, - processed_items:, - deleted_items:, - valid_items:, - invalid_items:, - incomplete_items:, - deletion_failed_items:, + 'IdV: USPS address letter requested', + resend: resend, + proofing_components: proofing_components, **extra, ) end - # A job to check USPS notifications about in-person enrollment status updates - # has encountered an error - # @param [String] exception_class - # @param [String] exception_message - def idv_in_person_proofing_enrollments_ready_for_status_check_job_ingestion_error( - exception_class:, - exception_message:, + # @param [Boolean] letter_already_sent + # GPO address visited + def idv_gpo_address_visited( + letter_already_sent:, **extra ) track_event( - 'InPersonEnrollmentsReadyForStatusCheckJob: Ingestion error', - exception_class:, - exception_message:, + 'IdV: USPS address visited', + letter_already_sent: letter_already_sent, **extra, ) end - # User has consented to share information with document upload and may - # view the "hybrid handoff" step next unless "skip_upload" param is true - def idv_doc_auth_agreement_submitted(**extra) - track_event('IdV: doc auth agreement submitted', **extra) - end - - def idv_doc_auth_agreement_visited(**extra) - track_event('IdV: doc auth agreement visited', **extra) - end - - def idv_doc_auth_cancel_link_sent_submitted(**extra) - track_event('IdV: doc auth cancel_link_sent submitted', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing cancel_update_ssn submitted - def idv_doc_auth_cancel_update_ssn_submitted(**extra) - track_event('IdV: doc auth cancel_update_ssn submitted', **extra) - end - - def idv_doc_auth_capture_complete_visited(**extra) - track_event('IdV: doc auth capture_complete visited', **extra) - end - - def idv_doc_auth_document_capture_visited(**extra) - track_event('IdV: doc auth document_capture visited', **extra) - end - - def idv_doc_auth_document_capture_submitted(**extra) - track_event('IdV: doc auth document_capture submitted', **extra) + # The user visited the gpo confirm cancellation screen + def idv_gpo_confirm_start_over_visited + track_event('IdV: gpo confirm start over visited') end - # @param [String] step_name which step the user was on - # @param [Integer] remaining_attempts how many attempts the user has left before we throttle them - # The user visited an error page due to an encountering an exception talking to a proofing vendor - def idv_doc_auth_exception_visited(step_name:, remaining_attempts:, **extra) + # @identity.idp.previous_event_name Account verification submitted + # @param [Boolean] success + # @param [Hash] errors + # @param [Hash] pii_like_keypaths + # GPO verification submitted + def idv_gpo_verification_submitted( + success:, + errors:, + pii_like_keypaths:, + **extra + ) track_event( - 'IdV: doc auth exception visited', - step_name: step_name, - remaining_attempts: remaining_attempts, + 'IdV: GPO verification submitted', + success: success, + errors: errors, + pii_like_keypaths: pii_like_keypaths, **extra, ) end - # @identity.idp.previous_event_name IdV: doc auth send_link submitted - def idv_doc_auth_link_sent_submitted(**extra) - track_event('IdV: doc auth link_sent submitted', **extra) - end - - def idv_doc_auth_link_sent_visited(**extra) - track_event('IdV: doc auth link_sent visited', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing optional verify_wait submitted - def idv_doc_auth_optional_verify_wait_submitted(**extra) - track_event('IdV: doc auth optional verify_wait submitted', **extra) + # @identity.idp.previous_event_name Account verification visited + # GPO verification visited + def idv_gpo_verification_visited + track_event('IdV: GPO verification visited') end - def idv_doc_auth_redo_document_capture_submitted(**extra) - track_event('IdV: doc auth redo_document_capture submitted', **extra) + # 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 - # @identity.idp.previous_event_name IdV: in person proofing ssn submitted - def idv_doc_auth_ssn_submitted(**extra) - track_event('IdV: doc auth ssn submitted', **extra) + # 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 - # @identity.idp.previous_event_name IdV: in person proofing ssn visited - def idv_doc_auth_ssn_visited(**extra) - track_event('IdV: doc auth ssn visited', **extra) + # @param [String] selected_location Selected in-person location + # @param [String] in_person_cta_variant Variant testing bucket label + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The user submitted the in person proofing location step + def idv_in_person_location_submitted(selected_location:, in_person_cta_variant:, flow_path:, + **extra) + track_event( + 'IdV: in person proofing location submitted', + selected_location: selected_location, + flow_path: flow_path, + in_person_cta_variant: in_person_cta_variant, + **extra, + ) end - # @identity.idp.previous_event_name IdV: in person proofing redo_address submitted - def idv_doc_auth_redo_address_submitted(**extra) - track_event('IdV: doc auth redo_address submitted', **extra) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # @param [String] in_person_cta_variant Variant testing bucket label + # The user visited the in person proofing location step + def idv_in_person_location_visited(flow_path:, in_person_cta_variant:, **extra) + track_event( + 'IdV: in person proofing location visited', + flow_path: flow_path, + in_person_cta_variant: in_person_cta_variant, + **extra, + ) end - def idv_doc_auth_redo_ssn_submitted(**extra) - track_event('IdV: doc auth redo_ssn submitted', **extra) + # Tracks if request to get USPS in-person proofing locations fails + # @param [String] exception_class + # @param [String] exception_message + # @param [Boolean] response_body_present + # @param [Hash] response_body + # @param [Integer] response_status_code + def idv_in_person_locations_request_failure( + exception_class:, + exception_message:, + response_body_present:, + response_body:, + response_status_code:, + **extra + ) + track_event( + 'Request USPS IPP locations: request failed', + exception_class: exception_class, + exception_message: exception_message, + response_body_present: response_body_present, + response_body: response_body, + response_status_code: response_status_code, + **extra, + ) end # @param [Boolean] success - # @param [Hash] errors - # @param [Integer] attempts - # @param [Integer] remaining_attempts - # @param [String] user_id - # @param [String] flow_path - # The document capture image uploaded was locally validated during the IDV process - def idv_doc_auth_submitted_image_upload_form( + # @param [Integer] result_total + # @param [String] errors + # @param [String] exception_class + # @param [String] exception_message + # @param [Integer] response_status_code + # User submitted a search on the location search page and response received + def idv_in_person_locations_searched( success:, - errors:, - remaining_attempts:, - flow_path:, - attempts: nil, - user_id: nil, + result_total: 0, + errors: nil, + exception_class: nil, + exception_message: nil, + response_status_code: nil, **extra ) track_event( - 'IdV: doc auth image upload form submitted', + 'IdV: in person proofing location search submitted', success: success, + result_total: result_total, errors: errors, - attempts: attempts, - remaining_attempts: remaining_attempts, - user_id: user_id, - flow_path: flow_path, + exception_class: exception_class, + exception_message: exception_message, + response_status_code: response_status_code, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [String] exception - # @param [Boolean] billed - # @param [String] doc_auth_result - # @param [String] state - # @param [String] state_id_type - # @param [Boolean] async - # @param [Integer] attempts - # @param [Integer] remaining_attempts - # @param [Hash] client_image_metrics - # @param [String] flow_path - # The document capture image was uploaded to vendor during the IDV process - def idv_doc_auth_submitted_image_upload_vendor( - success:, - errors:, - exception:, - state:, - state_id_type:, - async:, attempts:, - remaining_attempts:, - client_image_metrics:, - flow_path:, - billed: nil, - doc_auth_result: nil, - **extra - ) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # @param [String] in_person_cta_variant Variant testing bucket label + # The user submitted the in person proofing prepare step + def idv_in_person_prepare_submitted(flow_path:, in_person_cta_variant:, **extra) track_event( - 'IdV: doc auth image upload vendor submitted', - success: success, - errors: errors, - exception: exception, - billed: billed, - doc_auth_result: doc_auth_result, - state: state, - state_id_type: state_id_type, - async: async, - attempts: attempts, - remaining_attempts: remaining_attempts, - client_image_metrics: client_image_metrics, + 'IdV: in person proofing prepare submitted', flow_path: flow_path, + in_person_cta_variant: in_person_cta_variant, **extra, ) end - def idv_doc_auth_randomizer_defaulted - track_event( - 'IdV: doc_auth random vendor error', - error: 'document_capture_session_uuid_key missing', - ) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The user visited the in person proofing prepare step + def idv_in_person_prepare_visited(flow_path:, **extra) + track_event('IdV: in person proofing prepare visited', flow_path: flow_path, **extra) end + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing # @param [Boolean] success # @param [Hash] errors - # @param [String] user_id - # @param [Integer] remaining_attempts - # @param [Hash] pii_like_keypaths - # @param [String] flow_path - # The PII that came back from the document capture vendor was validated - def idv_doc_auth_submitted_pii_validation( - success:, - errors:, - remaining_attempts:, - pii_like_keypaths:, - flow_path:, - user_id: nil, + # @param [Boolean] same_address_as_id + # address submitted by user + def idv_in_person_proofing_address_submitted( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, **extra ) track_event( - 'IdV: doc auth image upload vendor pii validation', + 'IdV: in person proofing address submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, success: success, errors: errors, - user_id: user_id, - remaining_attempts: remaining_attempts, - pii_like_keypaths: pii_like_keypaths, - flow_path: flow_path, + same_address_as_id: same_address_as_id, **extra, ) end - # The "hybrid handoff" step: Desktop user has submitted their choice to - # either continue via desktop ("document_capture" destination) or switch - # to mobile phone ("send_link" destination) to perform document upload. - # Mobile users still log this event but with skip_upload_step = true - def idv_doc_auth_upload_submitted(**extra) - track_event('IdV: doc auth upload submitted', **extra) - end - - # Desktop user has reached the above "hybrid handoff" view - def idv_doc_auth_upload_visited(**extra) - track_event('IdV: doc auth upload visited', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing verify submitted - def idv_doc_auth_verify_submitted(**extra) - track_event('IdV: doc auth verify submitted', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing verify visited - def idv_doc_auth_verify_visited(**extra) - track_event('IdV: doc auth verify visited', **extra) - end - - # @identity.idp.previous_event_name IdV: doc auth optional verify_wait submitted - def idv_doc_auth_verify_proofing_results(**extra) - track_event('IdV: doc auth verify proofing results', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing verify_wait visited - def idv_doc_auth_verify_wait_step_visited(**extra) - track_event('IdV: doc auth verify_wait visited', **extra) - end - - # @param [String] step_name - # @param [Integer] remaining_attempts - # The user was sent to a warning page during the IDV flow - def idv_doc_auth_warning_visited( - step_name:, - remaining_attempts:, + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing + # address page visited + def idv_in_person_proofing_address_visited( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, **extra ) track_event( - 'IdV: doc auth warning visited', - step_name: step_name, - remaining_attempts: remaining_attempts, + 'IdV: in person proofing address visited', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, **extra, ) end - def idv_doc_auth_welcome_submitted(**extra) - track_event('IdV: doc auth welcome submitted', **extra) - end - - def idv_doc_auth_welcome_visited(**extra) - track_event('IdV: doc auth welcome visited', **extra) + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing + # @param [Boolean] success + # @param [Hash] errors + # @param [Boolean, nil] same_address_as_id + # User clicked cancel on update address page + def idv_in_person_proofing_cancel_update_address( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, + **extra + ) + track_event( + 'IdV: in person proofing cancel_update_address submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, + success: success, + errors: errors, + same_address_as_id: same_address_as_id, + **extra, + ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User visited forgot password page - def idv_forgot_password(proofing_components: nil, **extra) + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing + # @param [Boolean] success + # @param [Hash] errors + # @param [Boolean] same_address_as_id + # User clicked cancel on update state id page + def idv_in_person_proofing_cancel_update_state_id( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, + **extra + ) track_event( - 'IdV: forgot password visited', - proofing_components: proofing_components, + 'IdV: in person proofing cancel_update_state_id submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, + success: success, + errors: errors, + same_address_as_id: same_address_as_id, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User confirmed forgot password - def idv_forgot_password_confirmed(proofing_components: nil, **extra) + # A job to check USPS notifications about in-person enrollment status updates has completed + # @param [Integer] fetched_items items fetched + # @param [Integer] processed_items items fetched and processed + # @param [Integer] deleted_items items fetched, processed, and then deleted from the queue + # @param [Integer] valid_items items that could be successfully used to update a record + # @param [Integer] invalid_items items that couldn't be used to update a record + # @param [Integer] incomplete_items fetched items not processed nor deleted from the queue + # @param [Integer] deletion_failed_items processed items that we failed to delete + def idv_in_person_proofing_enrollments_ready_for_status_check_job_completed( + fetched_items:, + processed_items:, + deleted_items:, + valid_items:, + invalid_items:, + incomplete_items:, + deletion_failed_items:, + **extra + ) track_event( - 'IdV: forgot password confirmed', - proofing_components: proofing_components, + 'InPersonEnrollmentsReadyForStatusCheckJob: Job completed', + fetched_items:, + processed_items:, + deleted_items:, + valid_items:, + invalid_items:, + incomplete_items:, + deletion_failed_items:, **extra, ) end - # @param [DateTime] enqueued_at - # @param [Boolean] resend - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # GPO letter was enqueued and the time at which it was enqueued - def idv_gpo_address_letter_enqueued(enqueued_at:, resend:, proofing_components: nil, **extra) + # A job to check USPS notifications about in-person enrollment status updates + # has encountered an error + # @param [String] exception_class + # @param [String] exception_message + def idv_in_person_proofing_enrollments_ready_for_status_check_job_ingestion_error( + exception_class:, + exception_message:, + **extra + ) track_event( - 'IdV: USPS address letter enqueued', - enqueued_at: enqueued_at, - resend: resend, - proofing_components: proofing_components, + 'InPersonEnrollmentsReadyForStatusCheckJob: Ingestion error', + exception_class:, + exception_message:, **extra, ) end - # @param [Boolean] resend - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # GPO letter was requested - def idv_gpo_address_letter_requested(resend:, proofing_components: nil, **extra) + # A job to check USPS notifications about in-person enrollment status updates has started + def idv_in_person_proofing_enrollments_ready_for_status_check_job_started(**extra) track_event( - 'IdV: USPS address letter requested', - resend: resend, - proofing_components: proofing_components, + 'InPersonEnrollmentsReadyForStatusCheckJob: Job started', **extra, ) end - # @param [Boolean] letter_already_sent - # GPO address visited - def idv_gpo_address_visited( - letter_already_sent:, + # @param [String] nontransliterable_characters + # Nontransliterable characters submitted by user + def idv_in_person_proofing_nontransliterable_characters_submitted( + nontransliterable_characters:, **extra ) track_event( - 'IdV: USPS address visited', - letter_already_sent: letter_already_sent, + 'IdV: in person proofing characters submitted could not be transliterated', + nontransliterable_characters: nontransliterable_characters, **extra, ) end - # @identity.idp.previous_event_name Account verification submitted + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing # @param [Boolean] success # @param [Hash] errors - # @param [Hash] pii_like_keypaths - # GPO verification submitted - def idv_gpo_verification_submitted( - success:, - errors:, - pii_like_keypaths:, + # @param [Boolean] same_address_as_id + # User submitted state id on redo state id page + def idv_in_person_proofing_redo_state_id_submitted( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, **extra ) track_event( - 'IdV: GPO verification submitted', + 'IdV: in person proofing redo_state_id submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, success: success, errors: errors, - pii_like_keypaths: pii_like_keypaths, + same_address_as_id: same_address_as_id, **extra, ) end - # @identity.idp.previous_event_name Account verification visited - # GPO verification visited - def idv_gpo_verification_visited - track_event('IdV: GPO verification visited') - end - - # User visits IdV - def idv_intro_visit - track_event('IdV: intro visited') + def idv_in_person_proofing_residential_address_submitted(**extra) + track_event('IdV: in person proofing residential address submitted', **extra) end + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing # @param [Boolean] success - # @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any. - # @param [Boolean] fraud_review_pending Profile is under review for fraud - # @param [Boolean] fraud_rejection Profile is rejected due to fraud - # @param [Boolean] gpo_verification_pending Profile is awaiting gpo verificaiton - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # Tracks the last step of IDV, indicates the user successfully proofed - def idv_final( - success:, - fraud_review_pending:, - fraud_rejection:, - gpo_verification_pending:, - deactivation_reason: nil, - proofing_components: nil, + # @param [Hash] errors + # @param [Boolean, nil] same_address_as_id + # User submitted state id + def idv_in_person_proofing_state_id_submitted( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, **extra ) track_event( - 'IdV: final resolution', + 'IdV: in person proofing state_id submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, success: success, - fraud_review_pending: fraud_review_pending, - fraud_rejection: fraud_rejection, - gpo_verification_pending: gpo_verification_pending, - deactivation_reason: deactivation_reason, - proofing_components: proofing_components, - **extra, - ) - end - - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User visited IDV personal key page - def idv_personal_key_visited(proofing_components: nil, **extra) - track_event( - 'IdV: personal key visited', - proofing_components: proofing_components, + errors: errors, + same_address_as_id: same_address_as_id, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # @param [String, nil] deactivation_reason Reason profile was deactivated. - # @param [Boolean] fraud_review_pending Profile is under review for fraud - # @param [Boolean] fraud_rejection Profile is rejected due to fraud - # User submitted IDV personal key page - def idv_personal_key_submitted( - fraud_review_pending:, - fraud_rejection:, - proofing_components: nil, - deactivation_reason: nil, + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing + # State id page visited + def idv_in_person_proofing_state_id_visited( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, **extra ) track_event( - 'IdV: personal key submitted', - deactivation_reason: deactivation_reason, - fraud_review_pending: fraud_review_pending, - fraud_rejection: fraud_rejection, - proofing_components: proofing_components, + 'IdV: in person proofing state_id visited', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, **extra, ) end - # A user has downloaded their backup codes - def multi_factor_auth_backup_code_download - track_event('Multi-Factor Authentication: download backup code') - end - - # A user has downloaded their personal key. This event is no longer emitted. - # @identity.idp.previous_event_name IdV: download personal key - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - def idv_personal_key_downloaded(proofing_components: nil, **extra) + # The user clicked the sp link on the "ready to verify" page + def idv_in_person_ready_to_verify_sp_link_clicked(**extra) track_event( - 'IdV: personal key downloaded', - proofing_components: proofing_components, + 'IdV: user clicked sp link on ready to verify page', **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param ["sms", "voice"] otp_delivery_preference + # @param [String] in_person_cta_variant Variant testing bucket label # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user submitted their phone on the phone confirmation page - def idv_phone_confirmation_form_submitted( - success:, - otp_delivery_preference:, - errors:, - proofing_components: nil, - **extra - ) + # The user visited the "ready to verify" page for the in person proofing flow + def idv_in_person_ready_to_verify_visit(in_person_cta_variant: nil, proofing_components: nil, + **extra) track_event( - 'IdV: phone confirmation form', - success: success, - errors: errors, - otp_delivery_preference: otp_delivery_preference, + 'IdV: in person ready to verify visited', + in_person_cta_variant: in_person_cta_variant, proofing_components: proofing_components, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user was rate limited for submitting too many OTPs during the IDV phone step - def idv_phone_confirmation_otp_rate_limit_attempts(proofing_components: nil, **extra) + # The user clicked the what to bring link on the "ready to verify" page + def idv_in_person_ready_to_verify_what_to_bring_link_clicked(**extra) track_event( - 'Idv: Phone OTP attempts rate limited', - proofing_components: proofing_components, + 'IdV: user clicked what to bring link on ready to verify page', **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user was locked out for hitting the phone OTP rate limit during IDV - def idv_phone_confirmation_otp_rate_limit_locked_out(proofing_components: nil, **extra) - track_event( - 'Idv: Phone OTP rate limited user', - proofing_components: proofing_components, - **extra, - ) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The user submitted the in person proofing switch_back step + def idv_in_person_switch_back_submitted(flow_path:, **extra) + track_event('IdV: in person proofing switch_back submitted', flow_path: flow_path, **extra) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user was rate limited for requesting too many OTPs during the IDV phone step - def idv_phone_confirmation_otp_rate_limit_sends(proofing_components: nil, **extra) - track_event( - 'Idv: Phone OTP sends rate limited', - proofing_components: proofing_components, - **extra, - ) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The user visited the in person proofing switch_back step + def idv_in_person_switch_back_visited(flow_path:, **extra) + track_event('IdV: in person proofing switch_back visited', flow_path: flow_path, **extra) end - # @param [Boolean] success - # @param [Hash] errors - # @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 [Hash] telephony_response response from Telephony gem - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user resent an OTP during the IDV phone step - def idv_phone_confirmation_otp_resent( - success:, - errors:, - otp_delivery_preference:, - country_code:, - area_code:, - rate_limit_exceeded:, - telephony_response:, - proofing_components: nil, + # GetUspsProofingResultsJob has completed. Includes counts of various outcomes encountered + # @param [Float] duration_seconds number of minutes the job was running + # @param [Integer] enrollments_checked number of enrollments eligible for status check + # @param [Integer] enrollments_errored number of enrollments for which we encountered an error + # @param [Integer] enrollments_expired number of enrollments which expired + # @param [Integer] enrollments_failed number of enrollments which failed identity proofing + # @param [Integer] enrollments_in_progress number of enrollments which did not have any change + # @param [Integer] enrollments_passed number of enrollments which passed identity proofing + def idv_in_person_usps_proofing_results_job_completed( + duration_seconds:, + enrollments_checked:, + enrollments_errored:, + enrollments_expired:, + enrollments_failed:, + enrollments_in_progress:, + enrollments_passed:, **extra ) track_event( - 'IdV: phone confirmation otp resent', - success: success, - errors: errors, - otp_delivery_preference: otp_delivery_preference, - country_code: country_code, - area_code: area_code, - rate_limit_exceeded: rate_limit_exceeded, - telephony_response: telephony_response, - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: duration_seconds, + enrollments_checked: enrollments_checked, + enrollments_errored: enrollments_errored, + enrollments_expired: enrollments_expired, + enrollments_failed: enrollments_failed, + enrollments_in_progress: enrollments_in_progress, + enrollments_passed: enrollments_passed, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @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:, - errors:, - otp_delivery_preference:, - country_code:, - area_code:, - rate_limit_exceeded:, - phone_fingerprint:, - telephony_response:, - adapter:, - proofing_components: nil, + # Tracks exceptions that are raised when initiating deadline email in GetUspsProofingResultsJob + # @param [String] enrollment_id + # @param [String] exception_class + # @param [String] exception_message + def idv_in_person_usps_proofing_results_job_deadline_passed_email_exception( + enrollment_id:, + exception_class: nil, + exception_message: nil, **extra ) track_event( - 'IdV: phone confirmation otp sent', - success: success, - errors: errors, - otp_delivery_preference: otp_delivery_preference, - country_code: country_code, - area_code: area_code, - rate_limit_exceeded: rate_limit_exceeded, - phone_fingerprint: phone_fingerprint, - telephony_response: telephony_response, - adapter: adapter, - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Exception raised when attempting to send deadline passed email', + enrollment_id: enrollment_id, + exception_class: exception_class, + exception_message: exception_message, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The vendor finished the process of confirming the users phone - def idv_phone_confirmation_vendor_submitted( - success:, - errors:, - proofing_components: nil, + # Tracks deadline email initiated during GetUspsProofingResultsJob + # @param [String] enrollment_id + def idv_in_person_usps_proofing_results_job_deadline_passed_email_initiated( + enrollment_id:, **extra ) track_event( - 'IdV: phone confirmation vendor', - success: success, - errors: errors, - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: deadline passed email initiated', + enrollment_id: enrollment_id, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean] code_expired if the one-time code expired - # @param [Boolean] code_matches - # @param [Integer] second_factor_attempts_count number of attempts to confirm this phone - # @param [Time, nil] second_factor_locked_at timestamp when the phone was locked out - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # When a user attempts to confirm posession of a new phone number during the IDV process - def idv_phone_confirmation_otp_submitted( - success:, - errors:, - code_expired:, - code_matches:, - second_factor_attempts_count:, - second_factor_locked_at:, - proofing_components: nil, + # Tracks emails that are initiated during GetUspsProofingResultsJob + # @param [String] email_type success, failed or failed fraud + def idv_in_person_usps_proofing_results_job_email_initiated( + email_type:, **extra ) track_event( - 'IdV: phone confirmation otp submitted', - success: success, - errors: errors, - code_expired: code_expired, - code_matches: code_matches, - second_factor_attempts_count: second_factor_attempts_count, - second_factor_locked_at: second_factor_locked_at, - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Success or failure email initiated', + email_type: email_type, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # When a user visits the page to confirm posession of a new phone number during the IDV process - def idv_phone_confirmation_otp_visit(proofing_components: nil, **extra) + # Tracks incomplete enrollments checked via the USPS API + # @param [String] enrollment_code + # @param [String] enrollment_id + # @param [Float] minutes_since_established + # @param [String] response_message + def idv_in_person_usps_proofing_results_job_enrollment_incomplete( + enrollment_code:, + enrollment_id:, + minutes_since_established:, + response_message:, + **extra + ) track_event( - 'IdV: phone confirmation otp visited', - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Enrollment incomplete', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + minutes_since_established: minutes_since_established, + response_message: response_message, **extra, ) end - # @param ['warning','jobfail','failure'] type - # @param [Time] throttle_expires_at when the throttle expires - # @param [Integer] remaining_attempts number of attempts remaining - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # When a user gets an error during the phone finder flow of IDV - def idv_phone_error_visited( - type:, - proofing_components: nil, - throttle_expires_at: nil, - remaining_attempts: nil, + # Tracks individual enrollments that are updated during GetUspsProofingResultsJob + # @param [String] enrollment_code + # @param [String] enrollment_id + # @param [Float] minutes_since_established + # @param [Boolean] fraud_suspected + # @param [Boolean] passed did this enrollment pass or fail? + # @param [String] reason why did this enrollment pass or fail? + def idv_in_person_usps_proofing_results_job_enrollment_updated( + enrollment_code:, + enrollment_id:, + minutes_since_established:, + fraud_suspected:, + passed:, + reason:, **extra ) track_event( - 'IdV: phone error visited', - { - type: type, - proofing_components: proofing_components, - throttle_expires_at: throttle_expires_at, - remaining_attempts: remaining_attempts, - **extra, - }.compact, + 'GetUspsProofingResultsJob: Enrollment status updated', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + minutes_since_established: minutes_since_established, + fraud_suspected: fraud_suspected, + passed: passed, + reason: reason, + **extra, ) end - # @param ["sms", "voice"] otp_delivery_preference - # @param [Boolean] success - # @param [Hash] errors - # @param [Hash] error_details - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - def idv_phone_otp_delivery_selection_submitted( - success:, - otp_delivery_preference:, - proofing_components: nil, - errors: nil, - error_details: nil, + # Tracks exceptions that are raised when running GetUspsProofingResultsJob + # @param [String] reason why was the exception raised? + # @param [String] enrollment_id + # @param [String] exception_class + # @param [String] exception_message + # @param [String] enrollment_code + # @param [Float] minutes_since_established + # @param [Float] minutes_since_last_status_check + # @param [Float] minutes_since_last_status_update + # @param [Float] minutes_to_completion + # @param [Boolean] fraud_suspected + # @param [String] primary_id_type + # @param [String] secondary_id_type + # @param [String] failure_reason + # @param [String] transaction_end_date_time + # @param [String] transaction_start_date_time + # @param [String] status + # @param [String] assurance_level + # @param [String] proofing_post_office + # @param [String] proofing_city + # @param [String] proofing_state + # @param [String] scan_count + # @param [String] response_message + # @param [Integer] response_status_code + def idv_in_person_usps_proofing_results_job_exception( + reason:, + enrollment_id:, + minutes_since_established:, + exception_class: nil, + exception_message: nil, + enrollment_code: nil, + minutes_since_last_status_check: nil, + minutes_since_last_status_update: nil, + minutes_to_completion: nil, + fraud_suspected: nil, + primary_id_type: nil, + secondary_id_type: nil, + failure_reason: nil, + transaction_end_date_time: nil, + transaction_start_date_time: nil, + status: nil, + assurance_level: nil, + proofing_post_office: nil, + proofing_city: nil, + proofing_state: nil, + scan_count: nil, + response_message: nil, + response_status_code: nil, **extra ) track_event( - 'IdV: Phone OTP Delivery Selection Submitted', - { - success: success, - errors: errors, - error_details: error_details, - otp_delivery_preference: otp_delivery_preference, - proofing_components: proofing_components, - **extra, - }.compact, + 'GetUspsProofingResultsJob: Exception raised', + reason: reason, + enrollment_id: enrollment_id, + exception_class: exception_class, + exception_message: exception_message, + enrollment_code: enrollment_code, + minutes_since_established: minutes_since_established, + minutes_since_last_status_check: minutes_since_last_status_check, + minutes_since_last_status_update: minutes_since_last_status_update, + minutes_to_completion: minutes_to_completion, + fraud_suspected: fraud_suspected, + primary_id_type: primary_id_type, + secondary_id_type: secondary_id_type, + failure_reason: failure_reason, + transaction_end_date_time: transaction_end_date_time, + transaction_start_date_time: transaction_start_date_time, + status: status, + assurance_level: assurance_level, + proofing_post_office: proofing_post_office, + proofing_city: proofing_city, + proofing_state: proofing_state, + scan_count: scan_count, + response_message: response_message, + response_status_code: response_status_code, + **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User visited idv phone of record - def idv_phone_of_record_visited(proofing_components: nil, **extra) + # GetUspsProofingResultsJob is beginning. Includes some metadata about what the job will do + # @param [Integer] enrollments_count number of enrollments eligible for status check + # @param [Integer] reprocess_delay_minutes minimum delay since last status check + def idv_in_person_usps_proofing_results_job_started( + enrollments_count:, + reprocess_delay_minutes:, + **extra + ) track_event( - 'IdV: phone of record visited', - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Job started', + enrollments_count: enrollments_count, + reprocess_delay_minutes: reprocess_delay_minutes, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User visited idv phone OTP delivery selection - def idv_phone_otp_delivery_selection_visit(proofing_components: nil, **extra) + # Tracks unexpected responses from the USPS API + # @param [String] enrollment_code + # @param [String] enrollment_id + # @param [Float] minutes_since_established + # @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:, + minutes_since_established:, + response_message:, + reason:, + **extra + ) track_event( - 'IdV: Phone OTP delivery Selection Visited', - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Unexpected response received', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + minutes_since_established: minutes_since_established, + response_message: response_message, + reason: reason, + **extra, + ) + end + + # Tracks if USPS in-person proofing enrollment request fails + # @param [String] context + # @param [String] reason + # @param [Integer] enrollment_id + # @param [String] exception_class + # @param [String] exception_message + def idv_in_person_usps_request_enroll_exception( + context:, + reason:, + enrollment_id:, + exception_class:, + exception_message:, + **extra + ) + track_event( + 'USPS IPPaaS enrollment failed', + context: context, + enrollment_id: enrollment_id, + exception_class: exception_class, + exception_message: exception_message, + reason: reason, + **extra, + ) + end + + # User visits IdV + def idv_intro_visit + track_event('IdV: intro visited') + end + + # Tracks whether the user's device appears to be mobile device with a camera attached. + # @param [Boolean] is_camera_capable_mobile Whether we think the device _could_ have a camera. + # @param [Boolean,nil] camera_present Whether the user's device _actually_ has a camera available. + # @param [Integer,nil] grace_time Extra time allowed for browser to report camera availability. + # @param [Integer,nil] duration Time taken for browser to report camera availability. + def idv_mobile_device_and_camera_check( + is_camera_capable_mobile:, + camera_present: nil, + grace_time: nil, + duration: nil, + **extra + ) + track_event( + 'IdV: Mobile device and camera check', + is_camera_capable_mobile: is_camera_capable_mobile, + camera_present: camera_present, + grace_time: grace_time, + duration: duration, + **extra, + ) + end + + # @param [Integer] failed_capture_attempts Number of failed Acuant SDK attempts + # @param [Integer] failed_submission_attempts Number of failed Acuant doc submissions + # @param [String] field Image form field + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The number of acceptable failed attempts (maxFailedAttemptsBeforeNativeCamera) has been met + # or exceeded, and the system has forced the use of the native camera, rather than Acuant's + # camera, on mobile devices. + def idv_native_camera_forced( + failed_capture_attempts:, + failed_submission_attempts:, + field:, + flow_path:, + **extra + ) + track_event( + 'IdV: Native camera forced after failed attempts', + failed_capture_attempts: failed_capture_attempts, + failed_submission_attempts: failed_submission_attempts, + field: field, + flow_path: flow_path, **extra, ) end + # Tracks when user reaches verify errors due to being rejected due to fraud + def idv_not_verified_visited + track_event('IdV: Not verified visited') + end + + # Tracks if a user clicks the 'acknowledge' checkbox during personal + # key creation # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # @param [String] step the step the user was on when they clicked use a different phone number - # User decided to use a different phone number in idv - def idv_phone_use_different(step:, proofing_components: nil, **extra) + # @param [boolean] checked whether the user checked or un-checked + # the box with this click + def idv_personal_key_acknowledgment_toggled(checked:, proofing_components:, **extra) track_event( - 'IdV: use different phone number', - step: step, + 'IdV: personal key acknowledgment toggled', + checked: checked, proofing_components: proofing_components, **extra, ) end + # A user has downloaded their personal key. This event is no longer emitted. + # @identity.idp.previous_event_name IdV: download personal key # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The system encountered an error and the proofing results are missing - def idv_proofing_resolution_result_missing(proofing_components: nil, **extra) + def idv_personal_key_downloaded(proofing_components: nil, **extra) track_event( - 'Proofing Resolution Result Missing', + 'IdV: personal key downloaded', proofing_components: proofing_components, **extra, ) end - # User submitted IDV password confirm page - # @param [Boolean] success - # @param [Boolean] fraud_review_pending - # @param [Boolean] fraud_rejection - # @param [Boolean] gpo_verification_pending # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any. - def idv_review_complete( - success:, + # @param [String, nil] deactivation_reason Reason profile was deactivated. + # @param [Boolean] fraud_review_pending Profile is under review for fraud + # @param [Boolean] fraud_rejection Profile is rejected due to fraud + # User submitted IDV personal key page + def idv_personal_key_submitted( fraud_review_pending:, fraud_rejection:, - gpo_verification_pending:, - deactivation_reason: nil, proofing_components: nil, + deactivation_reason: nil, **extra ) track_event( - 'IdV: review complete', - success: success, + 'IdV: personal key submitted', deactivation_reason: deactivation_reason, fraud_review_pending: fraud_review_pending, - gpo_verification_pending: gpo_verification_pending, fraud_rejection: fraud_rejection, proofing_components: proofing_components, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's - # current proofing components - # @param [String] 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, - address_verification_method: nil, - **extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User visited IDV personal key page + def idv_personal_key_visited(proofing_components: nil, **extra) track_event( - 'IdV: review info visited', - address_verification_method: address_verification_method, + 'IdV: personal key visited', proofing_components: proofing_components, **extra, ) end - # @param [String] step - # @param [String] location + # @param [Boolean] success + # @param [Hash] errors + # @param ["sms", "voice"] otp_delivery_preference # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User started over idv - def idv_start_over( - step:, - location:, + # The user submitted their phone on the phone confirmation page + def idv_phone_confirmation_form_submitted( + success:, + otp_delivery_preference:, + errors:, proofing_components: nil, **extra ) track_event( - 'IdV: start over', - step: step, - location: location, + 'IdV: phone confirmation form', + success: success, + errors: errors, + otp_delivery_preference: otp_delivery_preference, proofing_components: proofing_components, **extra, ) end - # @param [String] controller - # @param [Boolean] user_signed_in - # Authenticity token (CSRF) is invalid - def invalid_authenticity_token( - controller:, - user_signed_in: nil, - **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The user was rate limited for submitting too many OTPs during the IDV phone step + def idv_phone_confirmation_otp_rate_limit_attempts(proofing_components: nil, **extra) track_event( - 'Invalid Authenticity Token', - controller: controller, - user_signed_in: user_signed_in, + 'Idv: Phone OTP attempts rate limited', + proofing_components: proofing_components, **extra, ) end - # @param [String] controller - # @param [String] referer - # @param [Boolean] user_signed_in - # Redirect was almost sent to an invalid external host unexpectedly - def unsafe_redirect_error( - controller:, - referer:, - user_signed_in: nil, - **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The user was locked out for hitting the phone OTP rate limit during IDV + def idv_phone_confirmation_otp_rate_limit_locked_out(proofing_components: nil, **extra) track_event( - 'Unsafe Redirect', - controller: controller, - referer: referer, - user_signed_in: user_signed_in, - **extra, - ) - end - - # @param [Integer] rendered_event_count how many events were rendered in the API response - # @param [Boolean] authenticated whether the request was successfully authenticated - # @param [Float] elapsed_time the amount of time the function took to run - # @param [Boolean] success - # An IRS Attempt API client has requested events - def irs_attempts_api_events( - rendered_event_count:, - authenticated:, - elapsed_time:, - success:, - **extra - ) - track_event( - 'IRS Attempt API: Events submitted', - rendered_event_count: rendered_event_count, - authenticated: authenticated, - elapsed_time: elapsed_time, - success: success, + 'Idv: Phone OTP rate limited user', + proofing_components: proofing_components, **extra, ) end - # @param [String] event_type - # @param [Integer] unencrypted_payload_num_bytes size of payload as JSON data - # @param [Boolean] recorded if the full event was recorded or not - def irs_attempts_api_event_metadata( - event_type:, - unencrypted_payload_num_bytes:, - recorded:, - **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The user was rate limited for requesting too many OTPs during the IDV phone step + def idv_phone_confirmation_otp_rate_limit_sends(proofing_components: nil, **extra) track_event( - 'IRS Attempt API: Event metadata', - event_type: event_type, - unencrypted_payload_num_bytes: unencrypted_payload_num_bytes, - recorded: recorded, + 'Idv: Phone OTP sends rate limited', + proofing_components: proofing_components, **extra, ) end # @param [Boolean] success - # @param [String] client_id - # @param [Boolean] client_id_parameter_present - # @param [Boolean] id_token_hint_parameter_present - # @param [Boolean] sp_initiated - # @param [Boolean] oidc - # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [Hash] error_details - # @param [String] method - # Logout Initiated - def logout_initiated( - success: nil, - client_id: nil, - sp_initiated: nil, - oidc: nil, - client_id_parameter_present: nil, - id_token_hint_parameter_present: nil, - saml_request_valid: nil, - errors: nil, - error_details: nil, - method: nil, + # @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 [Hash] telephony_response response from Telephony gem + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The user resent an OTP during the IDV phone step + def idv_phone_confirmation_otp_resent( + success:, + errors:, + otp_delivery_preference:, + country_code:, + area_code:, + rate_limit_exceeded:, + telephony_response:, + proofing_components: nil, **extra ) track_event( - 'Logout Initiated', + 'IdV: phone confirmation otp resent', success: success, - client_id: client_id, - client_id_parameter_present: client_id_parameter_present, - id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - error_details: error_details, - sp_initiated: sp_initiated, - oidc: oidc, - saml_request_valid: saml_request_valid, - method: method, + otp_delivery_preference: otp_delivery_preference, + country_code: country_code, + area_code: area_code, + rate_limit_exceeded: rate_limit_exceeded, + telephony_response: telephony_response, + proofing_components: proofing_components, **extra, ) end # @param [Boolean] success - # @param [String] client_id - # @param [Boolean] client_id_parameter_present - # @param [Boolean] id_token_hint_parameter_present - # @param [Boolean] sp_initiated - # @param [Boolean] oidc - # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [Hash] error_details - # @param [String] method - # OIDC Logout Requested - def oidc_logout_requested( - success: nil, - client_id: nil, - sp_initiated: nil, - oidc: nil, - client_id_parameter_present: nil, - id_token_hint_parameter_present: nil, - saml_request_valid: nil, - errors: nil, - error_details: nil, - method: nil, + # @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:, + errors:, + otp_delivery_preference:, + country_code:, + area_code:, + rate_limit_exceeded:, + phone_fingerprint:, + telephony_response:, + adapter:, + proofing_components: nil, **extra ) track_event( - 'OIDC Logout Requested', + 'IdV: phone confirmation otp sent', success: success, - client_id: client_id, - client_id_parameter_present: client_id_parameter_present, - id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - error_details: error_details, - sp_initiated: sp_initiated, - oidc: oidc, - saml_request_valid: saml_request_valid, - method: method, + otp_delivery_preference: otp_delivery_preference, + country_code: country_code, + area_code: area_code, + rate_limit_exceeded: rate_limit_exceeded, + phone_fingerprint: phone_fingerprint, + telephony_response: telephony_response, + adapter: adapter, + proofing_components: proofing_components, **extra, ) end # @param [Boolean] success - # @param [String] client_id - # @param [Boolean] client_id_parameter_present - # @param [Boolean] id_token_hint_parameter_present - # @param [Boolean] sp_initiated - # @param [Boolean] oidc - # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [Hash] error_details - # @param [String] method - # OIDC Logout Visited - def oidc_logout_visited( - success: nil, - client_id: nil, - sp_initiated: nil, - oidc: nil, - client_id_parameter_present: nil, - id_token_hint_parameter_present: nil, - saml_request_valid: nil, - errors: nil, - error_details: nil, - method: nil, + # @param [Boolean] code_expired if the one-time code expired + # @param [Boolean] code_matches + # @param [Integer] second_factor_attempts_count number of attempts to confirm this phone + # @param [Time, nil] second_factor_locked_at timestamp when the phone was locked out + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # When a user attempts to confirm posession of a new phone number during the IDV process + def idv_phone_confirmation_otp_submitted( + success:, + errors:, + code_expired:, + code_matches:, + second_factor_attempts_count:, + second_factor_locked_at:, + proofing_components: nil, **extra ) track_event( - 'OIDC Logout Page Visited', + 'IdV: phone confirmation otp submitted', success: success, - client_id: client_id, - client_id_parameter_present: client_id_parameter_present, - id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - error_details: error_details, - sp_initiated: sp_initiated, - oidc: oidc, - saml_request_valid: saml_request_valid, - method: method, + code_expired: code_expired, + code_matches: code_matches, + second_factor_attempts_count: second_factor_attempts_count, + second_factor_locked_at: second_factor_locked_at, + proofing_components: proofing_components, + **extra, + ) + end + + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # When a user visits the page to confirm posession of a new phone number during the IDV process + def idv_phone_confirmation_otp_visit(proofing_components: nil, **extra) + track_event( + 'IdV: phone confirmation otp visited', + proofing_components: proofing_components, **extra, ) end # @param [Boolean] success - # @param [String] client_id - # @param [Boolean] client_id_parameter_present - # @param [Boolean] id_token_hint_parameter_present - # @param [Boolean] sp_initiated - # @param [Boolean] oidc - # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [Hash] error_details - # @param [String] method - # OIDC Logout Submitted - def oidc_logout_submitted( - success: nil, - client_id: nil, - sp_initiated: nil, - oidc: nil, - client_id_parameter_present: nil, - id_token_hint_parameter_present: nil, - saml_request_valid: nil, - errors: nil, - error_details: nil, - method: nil, + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The vendor finished the process of confirming the users phone + def idv_phone_confirmation_vendor_submitted( + success:, + errors:, + proofing_components: nil, **extra ) track_event( - 'OIDC Logout Submitted', + 'IdV: phone confirmation vendor', success: success, - client_id: client_id, - client_id_parameter_present: client_id_parameter_present, - id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - error_details: error_details, - sp_initiated: sp_initiated, - oidc: oidc, - saml_request_valid: saml_request_valid, - method: method, + proofing_components: proofing_components, **extra, ) end - # @param [Boolean] success Whether authentication was successful - # @param [Hash] errors Authentication error reasons, if unsuccessful - # @param [String] context - # @param [String] multi_factor_auth_method - # @param [Integer] auth_app_configuration_id - # @param [Integer] piv_cac_configuration_id - # @param [Integer] key_id - # @param [Integer] webauthn_configuration_id - # @param [Integer] phone_configuration_id - # @param [Boolean] confirmation_for_add_phone - # @param [String] area_code - # @param [String] country_code - # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 - # Multi-Factor Authentication - def multi_factor_auth( - success:, - errors: nil, - context: nil, - multi_factor_auth_method: nil, - auth_app_configuration_id: nil, - piv_cac_configuration_id: nil, - key_id: nil, - webauthn_configuration_id: nil, - confirmation_for_add_phone: nil, - phone_configuration_id: nil, - pii_like_keypaths: nil, - area_code: nil, - country_code: nil, - phone_fingerprint: nil, + # @param ['warning','jobfail','failure'] type + # @param [Time] throttle_expires_at when the throttle expires + # @param [Integer] remaining_attempts number of attempts remaining + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # When a user gets an error during the phone finder flow of IDV + def idv_phone_error_visited( + type:, + proofing_components: nil, + throttle_expires_at: nil, + remaining_attempts: nil, **extra ) track_event( - 'Multi-Factor Authentication', - success: success, - errors: errors, - context: context, - multi_factor_auth_method: multi_factor_auth_method, - auth_app_configuration_id: auth_app_configuration_id, - piv_cac_configuration_id: piv_cac_configuration_id, - key_id: key_id, - webauthn_configuration_id: webauthn_configuration_id, - confirmation_for_add_phone: confirmation_for_add_phone, - phone_configuration_id: phone_configuration_id, - pii_like_keypaths: pii_like_keypaths, - area_code: area_code, - country_code: country_code, - phone_fingerprint: phone_fingerprint, - **extra, - ) - end - - # Tracks when the the user has added the MFA method phone to their account - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_added_phone(enabled_mfa_methods_count:, **extra) - track_event( - 'Multi-Factor Authentication: Added phone', + 'IdV: phone error visited', { - method_name: :phone, - enabled_mfa_methods_count: enabled_mfa_methods_count, + type: type, + proofing_components: proofing_components, + throttle_expires_at: throttle_expires_at, + remaining_attempts: remaining_attempts, **extra, }.compact, ) end - # Tracks when the user has added the MFA method piv_cac to their account - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_added_piv_cac(enabled_mfa_methods_count:, **extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User visited idv phone of record + def idv_phone_of_record_visited(proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: Added PIV_CAC', - { - method_name: :piv_cac, - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, - }.compact, + 'IdV: phone of record visited', + proofing_components: proofing_components, + **extra, ) end - # Tracks when the user has added the MFA method TOTP to their account - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_added_totp(enabled_mfa_methods_count:, **extra) + # @param ["sms", "voice"] otp_delivery_preference + # @param [Boolean] success + # @param [Hash] errors + # @param [Hash] error_details + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + def idv_phone_otp_delivery_selection_submitted( + success:, + otp_delivery_preference:, + proofing_components: nil, + errors: nil, + error_details: nil, + **extra + ) track_event( - 'Multi-Factor Authentication: Added TOTP', + 'IdV: Phone OTP Delivery Selection Submitted', { - method_name: :totp, - enabled_mfa_methods_count: enabled_mfa_methods_count, + success: success, + errors: errors, + error_details: error_details, + otp_delivery_preference: otp_delivery_preference, + proofing_components: proofing_components, **extra, }.compact, ) end - # Tracks when the user has added the MFA method webauthn to their account - # @param [Boolean] platform_authenticator indicates if webauthn_platform was used - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_added_webauthn( - platform_authenticator:, - enabled_mfa_methods_count:, **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User visited idv phone OTP delivery selection + def idv_phone_otp_delivery_selection_visit(proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: Added webauthn', - { - method_name: :webauthn, - platform_authenticator: platform_authenticator, - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, - }.compact, + 'IdV: Phone OTP delivery Selection Visited', + proofing_components: proofing_components, + **extra, ) end - # Tracks when the user visits the backup code confirmation setup page - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_enter_backup_code_confirmation_visit( - enabled_mfa_methods_count:, **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String] step the step the user was on when they clicked use a different phone number + # User decided to use a different phone number in idv + def idv_phone_use_different(step:, proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: enter backup code confirmation visited', - { - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, - }.compact, + 'IdV: use different phone number', + step: step, + proofing_components: proofing_components, + **extra, ) end - # @param ["authentication","reauthentication","confirmation"] context user session context - # User visited the page to enter a backup code as their MFA - def multi_factor_auth_enter_backup_code_visit(context:, **extra) + # @identity.idp.previous_event_name IdV: Verify setup errors visited + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # Tracks when the user reaches the verify please call page after failing proofing + def idv_please_call_visited(proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: enter backup code visited', - context: context, + 'IdV: Verify please call visited', + proofing_components: proofing_components, **extra, ) end - # @param ["authentication","reauthentication","confirmation"] context user session context - # User visited the page to enter a personal key as their mfa (legacy flow) - def multi_factor_auth_enter_personal_key_visit(context:, **extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The system encountered an error and the proofing results are missing + def idv_proofing_resolution_result_missing(proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: enter personal key visited', - context: context, + 'Proofing Resolution Result Missing', + proofing_components: proofing_components, **extra, ) end - # @param ["authentication","reauthentication","confirmation"] context user session context - # @param ["piv_cac"] multi_factor_auth_method - # @param [Integer, nil] piv_cac_configuration_id PIV/CAC configuration database ID - # User used a PIV/CAC as their mfa - def multi_factor_auth_enter_piv_cac( - context:, - multi_factor_auth_method:, - piv_cac_configuration_id:, + # User submitted IDV password confirm page + # @param [Boolean] success + # @param [Boolean] fraud_review_pending + # @param [Boolean] fraud_rejection + # @param [Boolean] gpo_verification_pending + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any. + def idv_review_complete( + success:, + fraud_review_pending:, + fraud_rejection:, + gpo_verification_pending:, + deactivation_reason: nil, + proofing_components: nil, **extra ) track_event( - 'Multi-Factor Authentication: enter PIV CAC visited', - context: context, - multi_factor_auth_method: multi_factor_auth_method, - piv_cac_configuration_id: piv_cac_configuration_id, + 'IdV: review complete', + success: success, + deactivation_reason: deactivation_reason, + fraud_review_pending: fraud_review_pending, + gpo_verification_pending: gpo_verification_pending, + fraud_rejection: fraud_rejection, + proofing_components: proofing_components, **extra, ) end - # @param [String] context - # @param [String] multi_factor_auth_method - # @param [Boolean] confirmation_for_add_phone - # @param [Integer] phone_configuration_id - # Multi-Factor Authentication enter OTP visited - def multi_factor_auth_enter_otp_visit( - context:, - multi_factor_auth_method:, - confirmation_for_add_phone:, - phone_configuration_id:, - **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's + # current proofing components + # @param [String] 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, + address_verification_method: nil, + **extra) track_event( - 'Multi-Factor Authentication: enter OTP visited', - context: context, - multi_factor_auth_method: multi_factor_auth_method, - confirmation_for_add_phone: confirmation_for_add_phone, - phone_configuration_id: phone_configuration_id, + 'IdV: review info visited', + address_verification_method: address_verification_method, + proofing_components: proofing_components, **extra, ) end - # @param ["authentication","reauthentication","confirmation"] context user session context - # User visited the page to enter a TOTP as their mfa - def multi_factor_auth_enter_totp_visit(context:, **extra) - track_event('Multi-Factor Authentication: enter TOTP visited', context: context, **extra) - end - - # @param ["authentication","reauthentication","confirmation"] context user session context - # @param ["webauthn","webauthn_platform"] multi_factor_auth_method which webauthn method was used, - # webauthn means a roaming authenticator like a yubikey, webauthn_platform means a platform - # authenticator like face or touch ID - # @param [Integer, nil] webauthn_configuration_id webauthn database ID - # User visited the page to authenticate with webauthn (yubikey, face ID or touch ID) - def multi_factor_auth_enter_webauthn_visit( - context:, - multi_factor_auth_method:, - webauthn_configuration_id:, + # Tracks when the user visits one of the the session error pages. + # @param [String] type + # @param [Integer,nil] attempts_remaining + def idv_session_error_visited( + type:, + attempts_remaining: nil, **extra ) track_event( - 'Multi-Factor Authentication: enter webAuthn authentication visited', - context: context, - multi_factor_auth_method: multi_factor_auth_method, - webauthn_configuration_id: webauthn_configuration_id, + 'IdV: session error visited', + type: type, + attempts_remaining: attempts_remaining, **extra, ) end - # Max multi factor auth attempts met - def multi_factor_auth_max_attempts - track_event('Multi-Factor Authentication: max attempts reached') - end - - # Multi factor selected from auth options list - # @param [Boolean] success - # @param [Hash] errors - # @param [String] selection - def multi_factor_auth_option_list(success:, errors:, selection:, **extra) + # @param [String] step + # @param [String] location + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User started over idv + def idv_start_over( + step:, + location:, + proofing_components: nil, + **extra + ) track_event( - 'Multi-Factor Authentication: option list', - success: success, - errors: errors, - selection: selection, + 'IdV: start over', + step: step, + location: location, + proofing_components: proofing_components, **extra, ) end - # User visited the list of multi-factor options to use - def multi_factor_auth_option_list_visit - track_event('Multi-Factor Authentication: option list visited') + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # @param [String] in_person_cta_variant Variant testing bucket label + # The user clicked the troubleshooting option to start in-person proofing + def idv_verify_in_person_troubleshooting_option_clicked(flow_path:, in_person_cta_variant:, + **extra) + track_event( + 'IdV: verify in person troubleshooting option clicked', + flow_path: flow_path, + in_person_cta_variant: in_person_cta_variant, + **extra, + ) end - # Multi factor auth phone setup - # @param [Boolean] success - # @param [Hash] errors - # @param [String] otp_delivery_preference - # @param [String] area_code - # @param [String] carrier - # @param [String] country_code - # @param [String] phone_type - # @param [Hash] types - def multi_factor_auth_phone_setup(success:, - errors:, - otp_delivery_preference:, - area_code:, - carrier:, - country_code:, - phone_type:, - types:, - **extra) - + # @param [String] controller + # @param [Boolean] user_signed_in + # Authenticity token (CSRF) is invalid + def invalid_authenticity_token( + controller:, + user_signed_in: nil, + **extra + ) track_event( - 'Multi-Factor Authentication: phone setup', - success: success, - errors: errors, - otp_delivery_preference: otp_delivery_preference, - area_code: area_code, - carrier: carrier, - country_code: country_code, - phone_type: phone_type, - types: types, + 'Invalid Authenticity Token', + controller: controller, + user_signed_in: user_signed_in, **extra, ) end - # Max multi factor max otp sends reached - def multi_factor_auth_max_sends - track_event('Multi-Factor Authentication: max otp sends reached') + # @param [String] event_type + # @param [Integer] unencrypted_payload_num_bytes size of payload as JSON data + # @param [Boolean] recorded if the full event was recorded or not + def irs_attempts_api_event_metadata( + event_type:, + unencrypted_payload_num_bytes:, + recorded:, + **extra + ) + track_event( + 'IRS Attempt API: Event metadata', + event_type: event_type, + unencrypted_payload_num_bytes: unencrypted_payload_num_bytes, + recorded: recorded, + **extra, + ) end - # Tracks when a user sets up a multi factor auth method - # @param [Boolean] success Whether authenticator setup was successful - # @param [Hash] errors Authenticator setup error reasons, if unsuccessful - # @param [String] multi_factor_auth_method - # @param [Boolean] in_multi_mfa_selection_flow - # @param [integer] enabled_mfa_methods_count - def multi_factor_auth_setup( + # @param [Integer] rendered_event_count how many events were rendered in the API response + # @param [Boolean] authenticated whether the request was successfully authenticated + # @param [Float] elapsed_time the amount of time the function took to run + # @param [Boolean] success + # An IRS Attempt API client has requested events + def irs_attempts_api_events( + rendered_event_count:, + authenticated:, + elapsed_time:, success:, - multi_factor_auth_method:, - enabled_mfa_methods_count:, - in_multi_mfa_selection_flow:, - errors: nil, **extra ) track_event( - 'Multi-Factor Authentication Setup', + 'IRS Attempt API: Events submitted', + rendered_event_count: rendered_event_count, + authenticated: authenticated, + elapsed_time: elapsed_time, success: success, - errors: errors, - multi_factor_auth_method: multi_factor_auth_method, - in_multi_mfa_selection_flow: in_multi_mfa_selection_flow, - enabled_mfa_methods_count: enabled_mfa_methods_count, **extra, ) end - # Tracks when an openid connect bearer token authentication request is made # @param [Boolean] success - # @param [Integer] ial - # @param [String] client_id Service Provider issuer + # @param [String] client_id + # @param [Boolean] client_id_parameter_present + # @param [Boolean] id_token_hint_parameter_present + # @param [Boolean] sp_initiated + # @param [Boolean] oidc + # @param [Boolean] saml_request_valid # @param [Hash] errors - def openid_connect_bearer_token(success:, ial:, client_id:, errors:, **extra) + # @param [Hash] error_details + # @param [String] method + # Logout Initiated + def logout_initiated( + success: nil, + client_id: nil, + sp_initiated: nil, + oidc: nil, + client_id_parameter_present: nil, + id_token_hint_parameter_present: nil, + saml_request_valid: nil, + errors: nil, + error_details: nil, + method: nil, + **extra + ) track_event( - 'OpenID Connect: bearer token authentication', + 'Logout Initiated', success: success, - ial: ial, client_id: client_id, + client_id_parameter_present: client_id_parameter_present, + id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, + error_details: error_details, + sp_initiated: sp_initiated, + oidc: oidc, + saml_request_valid: saml_request_valid, + method: method, **extra, ) end - # Tracks when openid authorization request is made - # @param [String] client_id - # @param [String] scope - # @param [Array] acr_values - # @param [Boolean] unauthorized_scope - # @param [Boolean] user_fully_authenticated - # @param [String] code_digest hash of returned "code" param - def openid_connect_request_authorization( - client_id:, - scope:, - acr_values:, - unauthorized_scope:, - user_fully_authenticated:, - code_digest:, + # @param [Boolean] success Whether authentication was successful + # @param [Hash] errors Authentication error reasons, if unsuccessful + # @param [String] context + # @param [String] multi_factor_auth_method + # @param [Integer] auth_app_configuration_id + # @param [Integer] piv_cac_configuration_id + # @param [Integer] key_id + # @param [Integer] webauthn_configuration_id + # @param [Integer] phone_configuration_id + # @param [Boolean] confirmation_for_add_phone + # @param [String] area_code + # @param [String] country_code + # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 + # Multi-Factor Authentication + def multi_factor_auth( + success:, + errors: nil, + context: nil, + multi_factor_auth_method: nil, + auth_app_configuration_id: nil, + piv_cac_configuration_id: nil, + key_id: nil, + webauthn_configuration_id: nil, + confirmation_for_add_phone: nil, + phone_configuration_id: nil, + pii_like_keypaths: nil, + area_code: nil, + country_code: nil, + phone_fingerprint: nil, **extra ) track_event( - 'OpenID Connect: authorization request', - client_id: client_id, - scope: scope, - acr_values: acr_values, - unauthorized_scope: unauthorized_scope, - user_fully_authenticated: user_fully_authenticated, - code_digest: code_digest, + 'Multi-Factor Authentication', + success: success, + errors: errors, + context: context, + multi_factor_auth_method: multi_factor_auth_method, + auth_app_configuration_id: auth_app_configuration_id, + piv_cac_configuration_id: piv_cac_configuration_id, + key_id: key_id, + webauthn_configuration_id: webauthn_configuration_id, + confirmation_for_add_phone: confirmation_for_add_phone, + phone_configuration_id: phone_configuration_id, + pii_like_keypaths: pii_like_keypaths, + area_code: area_code, + country_code: country_code, + phone_fingerprint: phone_fingerprint, **extra, ) end - # Tracks when an openid connect token request is made - # @param [String] client_id - # @param [String] user_id - # @param [String] code_digest hash of "code" param - def openid_connect_token(client_id:, user_id:, code_digest:, **extra) + # Tracks when the the user has added the MFA method phone to their account + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_added_phone(enabled_mfa_methods_count:, **extra) track_event( - 'OpenID Connect: token', - client_id: client_id, - user_id: user_id, - code_digest: code_digest, - **extra, + 'Multi-Factor Authentication: Added phone', + { + method_name: :phone, + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) end - # Tracks when user is redirected to OTP expired page - # @param [String] otp_sent_at - # @param [String] otp_expiration - def otp_expired_visited(otp_sent_at:, otp_expiration:, **extra) + # Tracks when the user has added the MFA method piv_cac to their account + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_added_piv_cac(enabled_mfa_methods_count:, **extra) track_event( - 'OTP Expired Page Visited', - otp_sent_at: otp_sent_at, - otp_expiration: otp_expiration, - **extra, - ) - end - - # Tracks if otp phone validation failed - # @identity.idp.previous_event_name Twilio Phone Validation Failed - # @param [String] error - # @param [String] context - # @param [String] country - def otp_phone_validation_failed(error:, context:, country:, **extra) - track_event( - 'Vendor Phone Validation failed', - error: error, - context: context, - country: country, - **extra, + 'Multi-Factor Authentication: Added PIV_CAC', + { + method_name: :piv_cac, + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) end - # User has been marked as authenticated - # @param [String] authentication_type - def user_marked_authed(authentication_type:, **extra) + # Tracks when the user has added the MFA method TOTP to their account + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_added_totp(enabled_mfa_methods_count:, **extra) track_event( - 'User marked authenticated', - authentication_type: authentication_type, - **extra, + 'Multi-Factor Authentication: Added TOTP', + { + method_name: :totp, + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) end - # User has attempted to access an action that requires re-authenticating - # @param [String] auth_method - # @param [String] authenticated_at - def user_2fa_reauthentication_required(auth_method:, authenticated_at:, **extra) + # Tracks when the user has added the MFA method webauthn to their account + # @param [Boolean] platform_authenticator indicates if webauthn_platform was used + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_added_webauthn( + platform_authenticator:, + enabled_mfa_methods_count:, **extra + ) track_event( - 'User 2FA Reauthentication Required', - auth_method: auth_method, - authenticated_at: authenticated_at, - **extra, + 'Multi-Factor Authentication: Added webauthn', + { + method_name: :webauthn, + platform_authenticator: platform_authenticator, + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) end - # User registration has been handed off to agency page - # @param [Boolean] ial2 - # @param [Integer] ialmax - # @param [String] service_provider_name - # @param [String] page_occurence - # @param [String] needs_completion_screen_reason - # @param [Array] sp_request_requested_attributes - # @param [Array] sp_session_requested_attributes - def user_registration_agency_handoff_page_visit( - ial2:, - service_provider_name:, - page_occurence:, - needs_completion_screen_reason:, - sp_session_requested_attributes:, - sp_request_requested_attributes: nil, - ialmax: nil, - **extra + # A user has downloaded their backup codes + def multi_factor_auth_backup_code_download + track_event('Multi-Factor Authentication: download backup code') + end + + # Tracks when the user visits the backup code confirmation setup page + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_enter_backup_code_confirmation_visit( + enabled_mfa_methods_count:, **extra + ) + track_event( + 'Multi-Factor Authentication: enter backup code confirmation visited', + { + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) + end + # @param ["authentication","reauthentication","confirmation"] context user session context + # User visited the page to enter a backup code as their MFA + def multi_factor_auth_enter_backup_code_visit(context:, **extra) track_event( - 'User registration: agency handoff visited', - ial2: ial2, - ialmax: ialmax, - service_provider_name: service_provider_name, - page_occurence: page_occurence, - needs_completion_screen_reason: needs_completion_screen_reason, - sp_request_requested_attributes: sp_request_requested_attributes, - sp_session_requested_attributes: sp_session_requested_attributes, + 'Multi-Factor Authentication: enter backup code visited', + context: context, **extra, ) end - # Tracks when user makes an otp delivery selection - # @param [String] otp_delivery_preference (sms or voice) - # @param [Boolean] resend - # @param [String] country_code - # @param [String] area_code - # @param ["authentication","reauthentication","confirmation"] context user session context - # @param [Hash] pii_like_keypaths - def otp_delivery_selection( - otp_delivery_preference:, - resend:, - country_code:, - area_code:, + # @param [String] context + # @param [String] multi_factor_auth_method + # @param [Boolean] confirmation_for_add_phone + # @param [Integer] phone_configuration_id + # Multi-Factor Authentication enter OTP visited + def multi_factor_auth_enter_otp_visit( context:, - pii_like_keypaths:, + multi_factor_auth_method:, + confirmation_for_add_phone:, + phone_configuration_id:, **extra ) track_event( - 'OTP: Delivery Selection', - otp_delivery_preference: otp_delivery_preference, - resend: resend, - country_code: country_code, - area_code: area_code, + 'Multi-Factor Authentication: enter OTP visited', context: context, - pii_like_keypaths: pii_like_keypaths, + multi_factor_auth_method: multi_factor_auth_method, + confirmation_for_add_phone: confirmation_for_add_phone, + phone_configuration_id: phone_configuration_id, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # The user updated their password - def password_changed(success:, errors:, **extra) - track_event('Password Changed', success: success, errors: errors, **extra) - end - - # @param [Boolean] success - # @param [Hash] errors - # The user added a password after verifying their email for account creation - def password_creation(success:, errors:, **extra) - track_event('Password Creation', success: success, errors: errors, **extra) - end - - # The user got their password incorrect the max number of times, their session was terminated - def password_max_attempts - track_event('Password Max Attempts Reached') - end - - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean, nil] confirmed if the account the reset is being requested for has a - # confirmed email - # @param [Boolean, nil] active_profile if the account the reset is being requested for has an - # active proofed profile - # The user entered an email address to request a password reset - def password_reset_email(success:, errors:, confirmed:, active_profile:, **extra) + # @param ["authentication","reauthentication","confirmation"] context user session context + # User visited the page to enter a personal key as their mfa (legacy flow) + def multi_factor_auth_enter_personal_key_visit(context:, **extra) track_event( - 'Password Reset: Email Submitted', - success: success, - errors: errors, - confirmed: confirmed, - active_profile: active_profile, + 'Multi-Factor Authentication: enter personal key visited', + context: context, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean] profile_deactivated if the active profile for the account was deactivated - # (the user will need to use their personal key to reactivate their profile) - # The user changed the password for their account via the password reset flow - def password_reset_password(success:, errors:, profile_deactivated:, **extra) + # @param ["authentication","reauthentication","confirmation"] context user session context + # @param ["piv_cac"] multi_factor_auth_method + # @param [Integer, nil] piv_cac_configuration_id PIV/CAC configuration database ID + # User used a PIV/CAC as their mfa + def multi_factor_auth_enter_piv_cac( + context:, + multi_factor_auth_method:, + piv_cac_configuration_id:, + **extra + ) track_event( - 'Password Reset: Password Submitted', - success: success, - errors: errors, - profile_deactivated: profile_deactivated, + 'Multi-Factor Authentication: enter PIV CAC visited', + context: context, + multi_factor_auth_method: multi_factor_auth_method, + piv_cac_configuration_id: piv_cac_configuration_id, **extra, ) end - # User has visited the page that lets them confirm if they want a new personal key - def profile_personal_key_visit - track_event('Profile: Visited new personal key') + # @param ["authentication","reauthentication","confirmation"] context user session context + # User visited the page to enter a TOTP as their mfa + def multi_factor_auth_enter_totp_visit(context:, **extra) + track_event('Multi-Factor Authentication: enter TOTP visited', context: context, **extra) end - # @param [Boolean] success - # @param [Hash] errors - # @param [String] user_id UUID of the user to receive password token - # A password token has been sent for user - def password_reset_token(success:, errors:, user_id:, **extra) + # @param ["authentication","reauthentication","confirmation"] context user session context + # @param ["webauthn","webauthn_platform"] multi_factor_auth_method which webauthn method was used, + # webauthn means a roaming authenticator like a yubikey, webauthn_platform means a platform + # authenticator like face or touch ID + # @param [Integer, nil] webauthn_configuration_id webauthn database ID + # User visited the page to authenticate with webauthn (yubikey, face ID or touch ID) + def multi_factor_auth_enter_webauthn_visit( + context:, + multi_factor_auth_method:, + webauthn_configuration_id:, + **extra + ) track_event( - 'Password Reset: Token Submitted', - success: success, - errors: errors, - user_id: user_id, + 'Multi-Factor Authentication: enter webAuthn authentication visited', + context: context, + multi_factor_auth_method: multi_factor_auth_method, + webauthn_configuration_id: webauthn_configuration_id, **extra, ) end - # Password reset form has been visited. - def password_reset_visit - track_event('Password Reset: Email Form Visited') - end - - # Pending account reset cancelled - def pending_account_reset_cancelled - track_event('Pending account reset cancelled') + # Max multi factor auth attempts met + def multi_factor_auth_max_attempts + track_event('Multi-Factor Authentication: max attempts reached') end - # Pending account reset visited - def pending_account_reset_visited - track_event('Pending account reset visited') + # Max multi factor max otp sends reached + def multi_factor_auth_max_sends + track_event('Multi-Factor Authentication: max otp sends reached') end + # Multi factor selected from auth options list # @param [Boolean] success # @param [Hash] errors - # Alert user if a personal key was used to sign in - def personal_key_alert_about_sign_in(success:, errors:, **extra) + # @param [String] selection + def multi_factor_auth_option_list(success:, errors:, selection:, **extra) track_event( - 'Personal key: Alert user about sign in', + 'Multi-Factor Authentication: option list', success: success, errors: errors, + selection: selection, **extra, ) end - # Account reactivated with personal key - def personal_key_reactivation - track_event('Personal key reactivation: Account reactivated with personal key') - end - - # Account reactivated with personal key as MFA - def personal_key_reactivation_sign_in - track_event( - 'Personal key reactivation: Account reactivated with personal key as MFA', - ) + # User visited the list of multi-factor options to use + def multi_factor_auth_option_list_visit + track_event('Multi-Factor Authentication: option list visited') end + # Multi factor auth phone setup # @param [Boolean] success # @param [Hash] errors - # @param [Hash] pii_like_keypaths - # Personal key form submitted - def personal_key_reactivation_submitted(success:, errors:, pii_like_keypaths:, **extra) + # @param [String] otp_delivery_preference + # @param [String] area_code + # @param [String] carrier + # @param [String] country_code + # @param [String] phone_type + # @param [Hash] types + def multi_factor_auth_phone_setup(success:, + errors:, + otp_delivery_preference:, + area_code:, + carrier:, + country_code:, + phone_type:, + types:, + **extra) track_event( - 'Personal key reactivation: Personal key form submitted', + 'Multi-Factor Authentication: phone setup', success: success, errors: errors, - pii_like_keypaths: pii_like_keypaths, + otp_delivery_preference: otp_delivery_preference, + area_code: area_code, + carrier: carrier, + country_code: country_code, + phone_type: phone_type, + types: types, **extra, ) end - # Personal key reactivation visited - def personal_key_reactivation_visited - track_event('Personal key reactivation: Personal key form visited') - end - - # @param [Boolean] personal_key_present if personal key is present - # Personal key viewed - def personal_key_viewed(personal_key_present:, **extra) + # Tracks when a user sets up a multi factor auth method + # @param [Boolean] success Whether authenticator setup was successful + # @param [Hash] errors Authenticator setup error reasons, if unsuccessful + # @param [String] multi_factor_auth_method + # @param [Boolean] in_multi_mfa_selection_flow + # @param [integer] enabled_mfa_methods_count + def multi_factor_auth_setup( + success:, + multi_factor_auth_method:, + enabled_mfa_methods_count:, + in_multi_mfa_selection_flow:, + errors: nil, + **extra + ) track_event( - 'Personal key viewed', - personal_key_present: personal_key_present, + 'Multi-Factor Authentication Setup', + success: success, + errors: errors, + multi_factor_auth_method: multi_factor_auth_method, + in_multi_mfa_selection_flow: in_multi_mfa_selection_flow, + enabled_mfa_methods_count: enabled_mfa_methods_count, **extra, ) end # @param [Boolean] success + # @param [String] client_id + # @param [Boolean] client_id_parameter_present + # @param [Boolean] id_token_hint_parameter_present + # @param [Boolean] sp_initiated + # @param [Boolean] oidc + # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [String] delivery_preference - # @param [Integer] phone_configuration_id - # @param [Boolean] make_default_number - # User has submitted a change in phone number - def phone_change_submitted( - success:, - errors:, - delivery_preference:, - phone_configuration_id:, - make_default_number:, + # @param [Hash] error_details + # @param [String] method + # OIDC Logout Requested + def oidc_logout_requested( + success: nil, + client_id: nil, + sp_initiated: nil, + oidc: nil, + client_id_parameter_present: nil, + id_token_hint_parameter_present: nil, + saml_request_valid: nil, + errors: nil, + error_details: nil, + method: nil, **extra ) track_event( - 'Phone Number Change: Form submitted', + 'OIDC Logout Requested', success: success, + client_id: client_id, + client_id_parameter_present: client_id_parameter_present, + id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - delivery_preference: delivery_preference, - phone_configuration_id: phone_configuration_id, - make_default_number: make_default_number, + error_details: error_details, + sp_initiated: sp_initiated, + oidc: oidc, + saml_request_valid: saml_request_valid, + method: method, **extra, ) end - # User has viewed the page to change their phone number - def phone_change_viewed - track_event('Phone Number Change: Visited') - end - # @param [Boolean] success - # @param [Integer] phone_configuration_id - # tracks a phone number deletion event - def phone_deletion(success:, phone_configuration_id:, **extra) + # @param [String] client_id + # @param [Boolean] client_id_parameter_present + # @param [Boolean] id_token_hint_parameter_present + # @param [Boolean] sp_initiated + # @param [Boolean] oidc + # @param [Boolean] saml_request_valid + # @param [Hash] errors + # @param [Hash] error_details + # @param [String] method + # OIDC Logout Submitted + def oidc_logout_submitted( + success: nil, + client_id: nil, + sp_initiated: nil, + oidc: nil, + client_id_parameter_present: nil, + id_token_hint_parameter_present: nil, + saml_request_valid: nil, + errors: nil, + error_details: nil, + method: nil, + **extra + ) track_event( - 'Phone Number Deletion: Submitted', + 'OIDC Logout Submitted', success: success, - phone_configuration_id: phone_configuration_id, + client_id: client_id, + client_id_parameter_present: client_id_parameter_present, + id_token_hint_parameter_present: id_token_hint_parameter_present, + errors: errors, + error_details: error_details, + sp_initiated: sp_initiated, + oidc: oidc, + saml_request_valid: saml_request_valid, + method: method, **extra, ) end # @param [Boolean] success + # @param [String] client_id + # @param [Boolean] client_id_parameter_present + # @param [Boolean] id_token_hint_parameter_present + # @param [Boolean] sp_initiated + # @param [Boolean] oidc + # @param [Boolean] saml_request_valid # @param [Hash] errors - # tracks piv cac login event - def piv_cac_login(success:, errors:, **extra) + # @param [Hash] error_details + # @param [String] method + # OIDC Logout Visited + def oidc_logout_visited( + success: nil, + client_id: nil, + sp_initiated: nil, + oidc: nil, + client_id_parameter_present: nil, + id_token_hint_parameter_present: nil, + saml_request_valid: nil, + errors: nil, + error_details: nil, + method: nil, + **extra + ) track_event( - 'PIV/CAC Login', + 'OIDC Logout Page Visited', success: success, + client_id: client_id, + client_id_parameter_present: client_id_parameter_present, + id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, + error_details: error_details, + sp_initiated: sp_initiated, + oidc: oidc, + saml_request_valid: saml_request_valid, + method: method, **extra, ) end - # @param [String] error - # Tracks if a Profile encryption is invalid - def profile_encryption_invalid(error:, **extra) - track_event('Profile Encryption: Invalid', error: error, **extra) + # Tracks when an openid connect bearer token authentication request is made + # @param [Boolean] success + # @param [Integer] ial + # @param [String] client_id Service Provider issuer + # @param [Hash] errors + def openid_connect_bearer_token(success:, ial:, client_id:, errors:, **extra) + track_event( + 'OpenID Connect: bearer token authentication', + success: success, + ial: ial, + client_id: client_id, + errors: errors, + **extra, + ) end - # @see #profile_personal_key_create_notifications - # User has chosen to receive a new personal key - def profile_personal_key_create - track_event('Profile: Created new personal key') + # Tracks when openid authorization request is made + # @param [String] client_id + # @param [String] scope + # @param [Array] acr_values + # @param [Boolean] unauthorized_scope + # @param [Boolean] user_fully_authenticated + # @param [String] code_digest hash of returned "code" param + def openid_connect_request_authorization( + client_id:, + scope:, + acr_values:, + unauthorized_scope:, + user_fully_authenticated:, + code_digest:, + **extra + ) + track_event( + 'OpenID Connect: authorization request', + client_id: client_id, + scope: scope, + acr_values: acr_values, + unauthorized_scope: unauthorized_scope, + user_fully_authenticated: user_fully_authenticated, + code_digest: code_digest, + **extra, + ) end - # @param [true] success this event always succeeds - # @param [Integer] emails number of email addresses the notification was sent to - # @param [Array] sms_message_ids AWS Pinpoint SMS message IDs for each phone number that - # was notified - # User has chosen to receive a new personal key, contains stats about notifications that - # were sent to phone numbers and email addresses for the user - def profile_personal_key_create_notifications(success:, emails:, sms_message_ids:, **extra) + # Tracks when an openid connect token request is made + # @param [String] client_id + # @param [String] user_id + # @param [String] code_digest hash of "code" param + def openid_connect_token(client_id:, user_id:, code_digest:, **extra) track_event( - 'Profile: Created new personal key notifications', - success: success, - emails: emails, - sms_message_ids: sms_message_ids, + 'OpenID Connect: token', + client_id: client_id, + user_id: user_id, + code_digest: code_digest, **extra, ) end - # @identity.idp.previous_event_name Proofing Address Timeout - # The job for address verification (PhoneFinder) did not record a result in the expected - # place during the expected time frame - def proofing_address_result_missing - track_event('Proofing Address Result Missing') - end - - # @identity.idp.previous_event_name Proofing Document Timeout - # The job for document authentication did not record a result in the expected - # place during the expected time frame - def proofing_document_result_missing - track_event('Proofing Document Result Missing') - end - - # Rate limit triggered - # @param [String] type - def rate_limit_triggered(type:, **extra) - track_event('Rate Limit Triggered', type: type, **extra) - end - - # The result of a reCAPTCHA verification request was received - # @param [Hash] recaptcha_result Full reCAPTCHA response body - # @param [Float] score_threshold Minimum value for considering passing result - # @param [Boolean] evaluated_as_valid Whether result was considered valid - # @param [String] validator_class Class name of validator - # @param [String, nil] exception_class Class name of exception, if error occurred - def recaptcha_verify_result_received( - recaptcha_result:, - score_threshold:, - evaluated_as_valid:, - validator_class:, - exception_class:, + # Tracks when user makes an otp delivery selection + # @param [String] otp_delivery_preference (sms or voice) + # @param [Boolean] resend + # @param [String] country_code + # @param [String] area_code + # @param ["authentication","reauthentication","confirmation"] context user session context + # @param [Hash] pii_like_keypaths + def otp_delivery_selection( + otp_delivery_preference:, + resend:, + country_code:, + area_code:, + context:, + pii_like_keypaths:, **extra ) track_event( - 'reCAPTCHA verify result received', - recaptcha_result:, - score_threshold:, - evaluated_as_valid:, - validator_class:, - exception_class:, + 'OTP: Delivery Selection', + otp_delivery_preference: otp_delivery_preference, + resend: resend, + country_code: country_code, + area_code: area_code, + context: context, + pii_like_keypaths: pii_like_keypaths, **extra, ) end - # User authenticated by a remembered device - # @param [DateTime] cookie_created_at time the remember device cookie was created - # @param [Integer] cookie_age_seconds age of the cookie in seconds - def remembered_device_used_for_authentication( - cookie_created_at:, - cookie_age_seconds:, - **extra - ) + # Tracks when user is redirected to OTP expired page + # @param [String] otp_sent_at + # @param [String] otp_expiration + def otp_expired_visited(otp_sent_at:, otp_expiration:, **extra) track_event( - 'Remembered device used for authentication', - cookie_created_at: cookie_created_at, - cookie_age_seconds: cookie_age_seconds, + 'OTP Expired Page Visited', + otp_sent_at: otp_sent_at, + otp_expiration: otp_expiration, **extra, ) end - # Service provider initiated remote logout - # @param [String] service_provider - # @param [Boolean] saml_request_valid - def remote_logout_initiated( - service_provider:, - saml_request_valid:, - **extra - ) + # Tracks if otp phone validation failed + # @identity.idp.previous_event_name Twilio Phone Validation Failed + # @param [String] error + # @param [String] context + # @param [String] country + def otp_phone_validation_failed(error:, context:, country:, **extra) track_event( - 'Remote Logout initiated', - service_provider: service_provider, - saml_request_valid: saml_request_valid, + 'Vendor Phone Validation failed', + error: error, + context: context, + country: country, **extra, ) end - # Service provider completed remote logout - # @param [String] service_provider - # @param [String] user_id - def remote_logout_completed( - service_provider:, - user_id:, - **extra - ) - track_event( - 'Remote Logout completed', - service_provider: service_provider, - user_id: user_id, - **extra, - ) + # @param [Boolean] success + # @param [Hash] errors + # The user updated their password + def password_changed(success:, errors:, **extra) + track_event('Password Changed', success: success, errors: errors, **extra) end - # A response timed out - # @param [String] backtrace - # @param [String] exception_message - # @param [String] exception_class - def response_timed_out( - backtrace:, - exception_message:, - exception_class:, - **extra - ) - track_event( - 'Response Timed Out', - backtrace: backtrace, - exception_message: exception_message, - exception_class: exception_class, - **extra, - ) + # @param [Boolean] success + # @param [Hash] errors + # The user added a password after verifying their email for account creation + def password_creation(success:, errors:, **extra) + track_event('Password Creation', success: success, errors: errors, **extra) end - # User cancelled the process and returned to the sp - # @param [String] redirect_url the url of the service provider - # @param [String] flow - # @param [String] step - # @param [String] location - def return_to_sp_cancelled( - redirect_url:, - step: nil, - location: nil, - flow: nil, - **extra - ) - track_event( - 'Return to SP: Cancelled', - redirect_url: redirect_url, - step: step, - location: location, - flow: flow, - **extra, - ) + # The user got their password incorrect the max number of times, their session was terminated + def password_max_attempts + track_event('Password Max Attempts Reached') end - # Tracks when a user is redirected back to the service provider after failing to proof. - # @param [String] redirect_url the url of the service provider - # @param [String] flow - # @param [String] step - # @param [String] location - def return_to_sp_failure_to_proof(redirect_url:, flow: nil, step: nil, location: nil, **extra) + # @param [Boolean] success + # @param [Hash] errors + # @param [Boolean, nil] confirmed if the account the reset is being requested for has a + # confirmed email + # @param [Boolean, nil] active_profile if the account the reset is being requested for has an + # active proofed profile + # The user entered an email address to request a password reset + def password_reset_email(success:, errors:, confirmed:, active_profile:, **extra) track_event( - 'Return to SP: Failed to proof', - redirect_url: redirect_url, - flow: flow, - step: step, - location: location, + 'Password Reset: Email Submitted', + success: success, + errors: errors, + confirmed: confirmed, + active_profile: active_profile, **extra, ) end - # Tracks when rules of use is visited - def rules_of_use_visit - track_event('Rules of Use Visited') - end - - # Tracks when rules of use is submitted with a success or failure # @param [Boolean] success # @param [Hash] errors - def rules_of_use_submitted(success: nil, errors: nil, **extra) + # @param [Boolean] profile_deactivated if the active profile for the account was deactivated + # (the user will need to use their personal key to reactivate their profile) + # The user changed the password for their account via the password reset flow + def password_reset_password(success:, errors:, profile_deactivated:, **extra) track_event( - 'Rules of Use Submitted', + 'Password Reset: Password Submitted', success: success, errors: errors, + profile_deactivated: profile_deactivated, **extra, ) end - # Tracks when security event is received # @param [Boolean] success - # @param [String] error_code # @param [Hash] errors - # @param [String] jti - # @param [String] user_id - # @param [String] client_id - def security_event_received( - success:, - error_code: nil, - errors: nil, - jti: nil, - user_id: nil, - client_id: nil, - **extra - ) + # @param [String] user_id UUID of the user to receive password token + # A password token has been sent for user + def password_reset_token(success:, errors:, user_id:, **extra) track_event( - 'RISC: Security event received', + 'Password Reset: Token Submitted', success: success, - error_code: error_code, errors: errors, - jti: jti, user_id: user_id, - client_id: client_id, **extra, ) end - # Tracks when a user is bounced back from the service provider due to an integration issue. - def sp_handoff_bounced_detected - track_event('SP handoff bounced detected') + # Password reset form has been visited. + def password_reset_visit + track_event('Password Reset: Email Form Visited') end - # Tracks when a user visits the bounced page. - def sp_handoff_bounced_visit - track_event('SP handoff bounced visited') + # Pending account reset cancelled + def pending_account_reset_cancelled + track_event('Pending account reset cancelled') end - # Tracks when a user visits the "This agency no longer uses Login.gov" page. - def sp_inactive_visit - track_event('SP inactive visited') + # Pending account reset visited + def pending_account_reset_visited + track_event('Pending account reset visited') end - # Tracks when a user is redirected back to the service provider - # @param [Integer] ial - # @param [Integer] billed_ial - def sp_redirect_initiated(ial:, billed_ial:, **extra) + # @param [Boolean] success + # @param [Hash] errors + # Alert user if a personal key was used to sign in + def personal_key_alert_about_sign_in(success:, errors:, **extra) track_event( - 'SP redirect initiated', - ial: ial, - billed_ial: billed_ial, + 'Personal key: Alert user about sign in', + success: success, + errors: errors, **extra, ) end - # Tracks when a user triggered a rate limit throttle - # @param [String] throttle_type - def throttler_rate_limit_triggered(throttle_type:, **extra) - track_event( - 'Throttler Rate Limit Triggered', - throttle_type: throttle_type, - **extra, - ) + # Account reactivated with personal key + def personal_key_reactivation + track_event('Personal key reactivation: Account reactivated with personal key') end - # Tracks when a user visits TOTP device setup - # @param [Boolean] user_signed_up - # @param [Boolean] totp_secret_present - # @param [Integer] enabled_mfa_methods_count - def totp_setup_visit( - user_signed_up:, - totp_secret_present:, - enabled_mfa_methods_count:, - **extra - ) + # Account reactivated with personal key as MFA + def personal_key_reactivation_sign_in track_event( - 'TOTP Setup Visited', - user_signed_up: user_signed_up, - totp_secret_present: totp_secret_present, - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, + 'Personal key reactivation: Account reactivated with personal key as MFA', ) end - # Tracks when a user disabled a TOTP device - def totp_user_disabled - track_event('TOTP: User Disabled') - end - - # Tracks when service provider consent is revoked - # @param [String] issuer issuer of the service provider consent to be revoked - def sp_revoke_consent_revoked(issuer:, **extra) + # @param [Boolean] success + # @param [Hash] errors + # @param [Hash] pii_like_keypaths + # Personal key form submitted + def personal_key_reactivation_submitted(success:, errors:, pii_like_keypaths:, **extra) track_event( - 'SP Revoke Consent: Revoked', - issuer: issuer, + 'Personal key reactivation: Personal key form submitted', + success: success, + errors: errors, + pii_like_keypaths: pii_like_keypaths, **extra, ) end - # Tracks when the page to revoke consent (unlink from) a service provider visited - # @param [String] issuer which issuer - def sp_revoke_consent_visited(issuer:, **extra) + # Personal key reactivation visited + def personal_key_reactivation_visited + track_event('Personal key reactivation: Personal key form visited') + end + + # @param [Boolean] personal_key_present if personal key is present + # Personal key viewed + def personal_key_viewed(personal_key_present:, **extra) track_event( - 'SP Revoke Consent: Visited', - issuer: issuer, + 'Personal key viewed', + personal_key_present: personal_key_present, **extra, ) end - # Record SAML authentication payload Hash # @param [Boolean] success # @param [Hash] errors - # @param [String] nameid_format - # @param [Array] authn_context - # @param [String] authn_context_comparison - # @param [String] service_provider - def saml_auth( + # @param [String] delivery_preference + # @param [Integer] phone_configuration_id + # @param [Boolean] make_default_number + # User has submitted a change in phone number + def phone_change_submitted( success:, errors:, - nameid_format:, - authn_context:, - authn_context_comparison:, - service_provider:, + delivery_preference:, + phone_configuration_id:, + make_default_number:, **extra ) track_event( - 'SAML Auth', + 'Phone Number Change: Form submitted', success: success, errors: errors, - nameid_format: nameid_format, - authn_context: authn_context, - authn_context_comparison: authn_context_comparison, - service_provider: service_provider, + delivery_preference: delivery_preference, + phone_configuration_id: phone_configuration_id, + make_default_number: make_default_number, **extra, ) end - # @param [Integer] requested_ial - # @param [String,nil] requested_aal_authn_context - # @param [Boolean,nil] force_authn - # @param [String] service_provider - # An external request for SAML Authentication was received - def saml_auth_request( - requested_ial:, - requested_aal_authn_context:, - force_authn:, - service_provider:, - **extra - ) - track_event( - 'SAML Auth Request', - { - requested_ial: requested_ial, - requested_aal_authn_context: requested_aal_authn_context, - force_authn: force_authn, - service_provider: service_provider, - **extra, - }.compact, - ) - end - - # tracks if the session is kept alive - def session_kept_alive - track_event('Session Kept Alive') - end - - # tracks if the session timed out - def session_timed_out - track_event('Session Timed Out') - end - - # tracks when a user's session is timed out - def session_total_duration_timeout - track_event('User Maximum Session Length Exceeded') + # User has viewed the page to change their phone number + def phone_change_viewed + track_event('Phone Number Change: Visited') end - # @param [String] flash - # @param [String] stored_location - # @param [String] sign_in_a_b_test_bucket - # tracks when a user visits the sign in page - def sign_in_page_visit(flash:, stored_location:, sign_in_a_b_test_bucket:, **extra) + # @param [Boolean] success + # @param [Integer] phone_configuration_id + # tracks a phone number deletion event + def phone_deletion(success:, phone_configuration_id:, **extra) track_event( - 'Sign in page visited', - flash: flash, - stored_location: stored_location, - sign_in_a_b_test_bucket:, + 'Phone Number Deletion: Submitted', + success: success, + phone_configuration_id: phone_configuration_id, **extra, ) end # @param [Boolean] success - # @param [Boolean] new_user - # @param [Boolean] has_other_auth_methods - # @param [Integer] phone_configuration_id - # tracks when a user opts into SMS - def sms_opt_in_submitted( - success:, - new_user:, - has_other_auth_methods:, - phone_configuration_id:, - **extra - ) + # @param [Hash] errors + # tracks piv cac login event + def piv_cac_login(success:, errors:, **extra) track_event( - 'SMS Opt-In: Submitted', + 'PIV/CAC Login', success: success, - new_user: new_user, - has_other_auth_methods: has_other_auth_methods, - phone_configuration_id: phone_configuration_id, + errors: errors, **extra, ) end - # @param [Boolean] new_user - # @param [Boolean] has_other_auth_methods - # @param [Integer] phone_configuration_id - # tracks when a user visits the sms opt in page - def sms_opt_in_visit( - new_user:, - has_other_auth_methods:, - phone_configuration_id:, - **extra - ) + # @param [String] redirect_url URL user was directed to + # @param [String, nil] step which step + # @param [String, nil] location which part of a step, if applicable + # @param ["idv", String, nil] flow which flow + # User was redirected to the login.gov policy page + def policy_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extra) track_event( - 'SMS Opt-In: Visited', - new_user: new_user, - has_other_auth_methods: has_other_auth_methods, - phone_configuration_id: phone_configuration_id, + 'Policy Page Redirect', + redirect_url: redirect_url, + step: step, + location: location, + flow: flow, **extra, ) end - # @param [String] area_code - # @param [String] country_code - # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 - # @param [String] context the context of the OTP, either "authentication" for confirmed phones - # or "confirmation" for unconfirmed - # @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( - area_code:, - country_code:, - phone_fingerprint:, - context:, - otp_delivery_preference:, - resend:, - telephony_response:, - adapter:, - success:, - **extra - ) - track_event( - 'Telephony: OTP sent', - { - area_code: area_code, - country_code: country_code, - phone_fingerprint: phone_fingerprint, - context: context, - otp_delivery_preference: otp_delivery_preference, - resend: resend, - telephony_response: telephony_response, - adapter: adapter, - success: success, - **extra, - }, - ) + # @param [String] error + # Tracks if a Profile encryption is invalid + def profile_encryption_invalid(error:, **extra) + track_event('Profile Encryption: Invalid', error: error, **extra) end - # Tracks When users visit the add phone page - def add_phone_setup_visit + # @see #profile_personal_key_create_notifications + # User has chosen to receive a new personal key + def profile_personal_key_create + track_event('Profile: Created new personal key') + end + + # @param [true] success this event always succeeds + # @param [Integer] emails number of email addresses the notification was sent to + # @param [Array] sms_message_ids AWS Pinpoint SMS message IDs for each phone number that + # was notified + # User has chosen to receive a new personal key, contains stats about notifications that + # were sent to phone numbers and email addresses for the user + def profile_personal_key_create_notifications(success:, emails:, sms_message_ids:, **extra) track_event( - 'Phone Setup Visited', + 'Profile: Created new personal key notifications', + success: success, + emails: emails, + sms_message_ids: sms_message_ids, + **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [String] sign_up_mfa_priority_bucket - # Tracks when the the user has selected and submitted additional MFA methods on user registration - def user_registration_2fa_additional_setup(success:, - sign_up_mfa_priority_bucket:, - errors: nil, - **extra) - track_event( - 'User Registration: Additional 2FA Setup', - { - success: success, - sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, - errors: errors, - **extra, - }.compact, - ) + # User has visited the page that lets them confirm if they want a new personal key + def profile_personal_key_visit + track_event('Profile: Visited new personal key') end - # Tracks when user visits additional MFA selection page - # @param [String] sign_up_mfa_priority_bucket - def user_registration_2fa_additional_setup_visit(sign_up_mfa_priority_bucket:, **extra) + # @identity.idp.previous_event_name Proofing Address Timeout + # The job for address verification (PhoneFinder) did not record a result in the expected + # place during the expected time frame + def proofing_address_result_missing + track_event('Proofing Address Result Missing') + end + + # @identity.idp.previous_event_name Proofing Document Timeout + # The job for document authentication did not record a result in the expected + # place during the expected time frame + def proofing_document_result_missing + track_event('Proofing Document Result Missing') + end + + # Rate limit triggered + # @param [String] type + def rate_limit_triggered(type:, **extra) + track_event('Rate Limit Triggered', type: type, **extra) + end + + # The result of a reCAPTCHA verification request was received + # @param [Hash] recaptcha_result Full reCAPTCHA response body + # @param [Float] score_threshold Minimum value for considering passing result + # @param [Boolean] evaluated_as_valid Whether result was considered valid + # @param [String] validator_class Class name of validator + # @param [String, nil] exception_class Class name of exception, if error occurred + def recaptcha_verify_result_received( + recaptcha_result:, + score_threshold:, + evaluated_as_valid:, + validator_class:, + exception_class:, + **extra + ) track_event( - 'User Registration: Additional 2FA Setup visited', - sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, + 'reCAPTCHA verify result received', + recaptcha_result:, + score_threshold:, + evaluated_as_valid:, + validator_class:, + exception_class:, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [Integer] enabled_mfa_methods_count - # @param [Integer] selected_mfa_count - # @param ['voice', 'auth_app'] selection - # @param [String] sign_up_mfa_priority_bucket - # Tracks when the the user has selected and submitted MFA auth methods on user registration - def user_registration_2fa_setup( - success:, - sign_up_mfa_priority_bucket:, - errors: nil, - selected_mfa_count: nil, - enabled_mfa_methods_count: nil, - selection: nil, + # User authenticated by a remembered device + # @param [DateTime] cookie_created_at time the remember device cookie was created + # @param [Integer] cookie_age_seconds age of the cookie in seconds + def remembered_device_used_for_authentication( + cookie_created_at:, + cookie_age_seconds:, **extra ) track_event( - 'User Registration: 2FA Setup', - { - success: success, - errors: errors, - selected_mfa_count: selected_mfa_count, - enabled_mfa_methods_count: enabled_mfa_methods_count, - sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, - selection: selection, - **extra, - }.compact, + 'Remembered device used for authentication', + cookie_created_at: cookie_created_at, + cookie_age_seconds: cookie_age_seconds, + **extra, ) end - # @param [String] mfa_method - # Tracks when the the user fully registered by submitting their first MFA method into the system - def user_registration_user_fully_registered( - mfa_method:, + # Service provider completed remote logout + # @param [String] service_provider + # @param [String] user_id + def remote_logout_completed( + service_provider:, + user_id:, **extra ) track_event( - 'User Registration: User Fully Registered', - { - mfa_method: mfa_method, - **extra, - }.compact, + 'Remote Logout completed', + service_provider: service_provider, + user_id: user_id, + **extra, ) end - # @param [Boolean] success - # @param [Hash] mfa_method_counts - # @param [Integer] enabled_mfa_methods_count - # @param [Hash] pii_like_keypaths - # Tracks when a user has completed MFA setup - def user_registration_mfa_setup_complete( - success:, - mfa_method_counts:, - enabled_mfa_methods_count:, - pii_like_keypaths:, + # Service provider initiated remote logout + # @param [String] service_provider + # @param [Boolean] saml_request_valid + def remote_logout_initiated( + service_provider:, + saml_request_valid:, **extra ) track_event( - 'User Registration: MFA Setup Complete', - { - success: success, - mfa_method_counts: mfa_method_counts, - enabled_mfa_methods_count: enabled_mfa_methods_count, - pii_like_keypaths: pii_like_keypaths, - **extra, - }.compact, + 'Remote Logout initiated', + service_provider: service_provider, + saml_request_valid: saml_request_valid, + **extra, ) end - # Tracks when user's piv cac is disabled - def user_registration_piv_cac_disabled - track_event('User Registration: piv cac disabled') - end - - # Tracks when user's piv cac setup - def user_registration_piv_cac_setup_visit(**extra) + # @param [Boolean] success + # Tracks request for resending confirmation for new emails to an account + def resend_add_email_request(success:, **extra) track_event( - 'User Registration: piv cac setup visited', + 'Resend Add Email Requested', + success: success, **extra, ) end - # Tracks when user visits Suggest Another MFA Page - def user_registration_suggest_another_mfa_notice_visited - track_event('User Registration: Suggest Another MFA Notice visited') - end - - # Tracks when user skips Suggest Another MFA Page - def user_registration_suggest_another_mfa_notice_skipped - track_event('User Registration: Suggest Another MFA Notice Skipped') - end - - # Tracks when user visits MFA selection page - # @param [String] sign_up_mfa_priority_bucket - def user_registration_2fa_setup_visit(sign_up_mfa_priority_bucket:, **extra) + # A response timed out + # @param [String] backtrace + # @param [String] exception_message + # @param [String] exception_class + def response_timed_out( + backtrace:, + exception_message:, + exception_class:, + **extra + ) track_event( - 'User Registration: 2FA Setup visited', - sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, + 'Response Timed Out', + backtrace: backtrace, + exception_message: exception_message, + exception_class: exception_class, **extra, ) end - # @param [Hash] vendor_status - # @param [String,nil] redirect_from - # Tracks when vendor has outage - def vendor_outage( - vendor_status:, - redirect_from: nil, + # User cancelled the process and returned to the sp + # @param [String] redirect_url the url of the service provider + # @param [String] flow + # @param [String] step + # @param [String] location + def return_to_sp_cancelled( + redirect_url:, + step: nil, + location: nil, + flow: nil, **extra ) track_event( - 'Vendor Outage', - redirect_from: redirect_from, - vendor_status: vendor_status, + 'Return to SP: Cancelled', + redirect_url: redirect_url, + step: step, + location: location, + flow: flow, **extra, ) end - # @param [Boolean] success - # @param [Integer] mfa_method_counts - # Tracks when WebAuthn is deleted - def webauthn_deleted(success:, mfa_method_counts:, pii_like_keypaths:, **extra) + # Tracks when a user is redirected back to the service provider after failing to proof. + # @param [String] redirect_url the url of the service provider + # @param [String] flow + # @param [String] step + # @param [String] location + def return_to_sp_failure_to_proof(redirect_url:, flow: nil, step: nil, location: nil, **extra) track_event( - 'WebAuthn Deleted', - success: success, - mfa_method_counts: mfa_method_counts, - pii_like_keypaths: pii_like_keypaths, + 'Return to SP: Failed to proof', + redirect_url: redirect_url, + flow: flow, + step: step, + location: location, **extra, ) end - # @param [Hash] platform_authenticator - # @param [Hash] errors - # @param [Integer] enabled_mfa_methods_count + # Tracks when rules of use is submitted with a success or failure # @param [Boolean] success - # Tracks when WebAuthn setup is visited - def webauthn_setup_visit(platform_authenticator:, errors:, enabled_mfa_methods_count:, success:, - **extra) + # @param [Hash] errors + def rules_of_use_submitted(success: nil, errors: nil, **extra) track_event( - 'WebAuthn Setup Visited', - platform_authenticator: platform_authenticator, - errors: errors, - enabled_mfa_methods_count: enabled_mfa_methods_count, + 'Rules of Use Submitted', success: success, + errors: errors, **extra, ) end - # Tracks when user visits enter email page - # @param [String] sign_in_a_b_test_bucket - # @param [Boolean] from_sign_in - def user_registration_enter_email_visit(sign_in_a_b_test_bucket:, from_sign_in:, **extra) - track_event( - 'User Registration: enter email visited', - sign_in_a_b_test_bucket:, - from_sign_in:, - **extra, - ) + # Tracks when rules of use is visited + def rules_of_use_visit + track_event('Rules of Use Visited') end - # @param [Integer] enabled_mfa_methods_count - # Tracks when user visits the phone setup step during registration - def user_registration_phone_setup_visit(enabled_mfa_methods_count:, **extra) - track_event( - 'User Registration: phone setup visited', - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, - ) - end - - # Tracks when user cancels registration - # @param [String] request_came_from the controller/action the request came from - def user_registration_cancellation(request_came_from:, **extra) - track_event( - 'User registration: cancellation visited', - request_came_from: request_came_from, - **extra, - ) - end - - # Tracks when user completes registration - # @param [Boolean] ial2 - # @param [Boolean] ialmax - # @param [String] service_provider_name - # @param [String] page_occurence - # @param [String] needs_completion_screen_reason - # @param [String] sign_in_a_b_test_bucket - # @param [Array] sp_request_requested_attributes - # @param [Array] sp_session_requested_attributes - def user_registration_complete( - ial2:, - service_provider_name:, - page_occurence:, - needs_completion_screen_reason:, - sign_in_a_b_test_bucket:, - sp_session_requested_attributes:, - sp_request_requested_attributes: nil, - ialmax: nil, + # Record SAML authentication payload Hash + # @param [Boolean] success + # @param [Hash] errors + # @param [String] nameid_format + # @param [Array] authn_context + # @param [String] authn_context_comparison + # @param [String] service_provider + def saml_auth( + success:, + errors:, + nameid_format:, + authn_context:, + authn_context_comparison:, + service_provider:, **extra ) track_event( - 'User registration: complete', - ial2: ial2, - ialmax: ialmax, - service_provider_name: service_provider_name, - page_occurence: page_occurence, - needs_completion_screen_reason: needs_completion_screen_reason, - sign_in_a_b_test_bucket:, - sp_request_requested_attributes: sp_request_requested_attributes, - sp_session_requested_attributes: sp_session_requested_attributes, + 'SAML Auth', + success: success, + errors: errors, + nameid_format: nameid_format, + authn_context: authn_context, + authn_context_comparison: authn_context_comparison, + service_provider: service_provider, **extra, ) end - # Tracks when user submits registration email - # @param [Boolean] success - # @param [Boolean] throttled - # @param [Hash] errors - # @param [Hash] error_details - # @param [String] user_id - # @param [String] domain_name - def user_registration_email( - success:, - throttled:, - errors:, - error_details: nil, - user_id: nil, - domain_name: nil, + # @param [Integer] requested_ial + # @param [String,nil] requested_aal_authn_context + # @param [Boolean,nil] force_authn + # @param [String] service_provider + # An external request for SAML Authentication was received + def saml_auth_request( + requested_ial:, + requested_aal_authn_context:, + force_authn:, + service_provider:, **extra ) track_event( - 'User Registration: Email Submitted', + 'SAML Auth Request', { - success: success, - throttled: throttled, - errors: errors, - error_details: error_details, - user_id: user_id, - domain_name: domain_name, + requested_ial: requested_ial, + requested_aal_authn_context: requested_aal_authn_context, + force_authn: force_authn, + service_provider: service_provider, **extra, }.compact, ) end - # Tracks when user confirms registration email + # Tracks when security event is received # @param [Boolean] success + # @param [String] error_code # @param [Hash] errors - # @param [Hash] error_details + # @param [String] jti # @param [String] user_id - def user_registration_email_confirmation( + # @param [String] client_id + def security_event_received( success:, - errors:, - error_details: nil, + error_code: nil, + errors: nil, + jti: nil, user_id: nil, + client_id: nil, **extra ) track_event( - 'User Registration: Email Confirmation', + 'RISC: Security event received', success: success, + error_code: error_code, errors: errors, - error_details: error_details, + jti: jti, user_id: user_id, + client_id: client_id, **extra, ) end - # Tracks if request to get address candidates from ArcGIS fails - # @param [String] exception_class - # @param [String] exception_message - # @param [Boolean] response_body_present - # @param [Hash] response_body - # @param [Integer] response_status_code - def idv_arcgis_request_failure( - exception_class:, - exception_message:, - response_body_present:, - response_body:, - response_status_code:, - **extra - ) - track_event( - 'Request ArcGIS Address Candidates: request failed', - exception_class: exception_class, - exception_message: exception_message, - response_body_present: response_body_present, - response_body: response_body, - response_status_code: response_status_code, - **extra, - ) + # tracks if the session is kept alive + def session_kept_alive + track_event('Session Kept Alive') end - # Tracks whether the user's device appears to be mobile device with a camera attached. - # @param [Boolean] is_camera_capable_mobile Whether we think the device _could_ have a camera. - # @param [Boolean,nil] camera_present Whether the user's device _actually_ has a camera available. - # @param [Integer,nil] grace_time Extra time allowed for browser to report camera availability. - # @param [Integer,nil] duration Time taken for browser to report camera availability. - def idv_mobile_device_and_camera_check( - is_camera_capable_mobile:, - camera_present: nil, - grace_time: nil, - duration: nil, - **extra - ) + # tracks if the session timed out + def session_timed_out + track_event('Session Timed Out') + end + + # tracks when a user's session is timed out + def session_total_duration_timeout + track_event('User Maximum Session Length Exceeded') + end + + # Tracks if a user clicks the "Show Password button" + # @param [String] path URL path where the click occurred + def show_password_button_clicked(path:, **extra) + track_event('Show Password Button Clicked', path: path, **extra) + end + + # @param [String] flash + # @param [String] stored_location + # @param [String] sign_in_a_b_test_bucket + # tracks when a user visits the sign in page + def sign_in_page_visit(flash:, stored_location:, sign_in_a_b_test_bucket:, **extra) track_event( - 'IdV: Mobile device and camera check', - is_camera_capable_mobile: is_camera_capable_mobile, - camera_present: camera_present, - grace_time: grace_time, - duration: duration, + 'Sign in page visited', + flash: flash, + stored_location: stored_location, + sign_in_a_b_test_bucket:, **extra, ) end - # Tracks when the user visits one of the the session error pages. - # @param [String] type - # @param [Integer,nil] attempts_remaining - def idv_session_error_visited( - type:, - attempts_remaining: nil, + # @param [Boolean] success + # @param [Boolean] new_user + # @param [Boolean] has_other_auth_methods + # @param [Integer] phone_configuration_id + # tracks when a user opts into SMS + def sms_opt_in_submitted( + success:, + new_user:, + has_other_auth_methods:, + phone_configuration_id:, **extra ) track_event( - 'IdV: session error visited', - type: type, - attempts_remaining: attempts_remaining, + 'SMS Opt-In: Submitted', + success: success, + new_user: new_user, + has_other_auth_methods: has_other_auth_methods, + phone_configuration_id: phone_configuration_id, **extra, ) end - # Tracks if request to get USPS in-person proofing locations fails - # @param [String] exception_class - # @param [String] exception_message - # @param [Boolean] response_body_present - # @param [Hash] response_body - # @param [Integer] response_status_code - def idv_in_person_locations_request_failure( - exception_class:, - exception_message:, - response_body_present:, - response_body:, - response_status_code:, + # @param [Boolean] new_user + # @param [Boolean] has_other_auth_methods + # @param [Integer] phone_configuration_id + # tracks when a user visits the sms opt in page + def sms_opt_in_visit( + new_user:, + has_other_auth_methods:, + phone_configuration_id:, **extra ) track_event( - 'Request USPS IPP locations: request failed', - exception_class: exception_class, - exception_message: exception_message, - response_body_present: response_body_present, - response_body: response_body, - response_status_code: response_status_code, + 'SMS Opt-In: Visited', + new_user: new_user, + has_other_auth_methods: has_other_auth_methods, + phone_configuration_id: phone_configuration_id, **extra, ) end - # Tracks when USPS in-person proofing enrollment is created - # @param [String] enrollment_code - # @param [Integer] enrollment_id - # @param [String] service_provider - def usps_ippaas_enrollment_created( - enrollment_code:, - enrollment_id:, - service_provider:, - **extra - ) - track_event( - 'USPS IPPaaS enrollment created', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - service_provider: service_provider, + # Tracks when a user is bounced back from the service provider due to an integration issue. + def sp_handoff_bounced_detected + track_event('SP handoff bounced detected') + end + + # Tracks when a user visits the bounced page. + def sp_handoff_bounced_visit + track_event('SP handoff bounced visited') + end + + # Tracks when a user visits the "This agency no longer uses Login.gov" page. + def sp_inactive_visit + track_event('SP inactive visited') + end + + # Tracks when a user is redirected back to the service provider + # @param [Integer] ial + # @param [Integer] billed_ial + def sp_redirect_initiated(ial:, billed_ial:, **extra) + track_event( + 'SP redirect initiated', + ial: ial, + billed_ial: billed_ial, **extra, ) end - # Tracks if USPS in-person proofing enrollment request fails - # @param [String] context - # @param [String] reason - # @param [Integer] enrollment_id - # @param [String] exception_class - # @param [String] exception_message - def idv_in_person_usps_request_enroll_exception( + # Tracks when service provider consent is revoked + # @param [String] issuer issuer of the service provider consent to be revoked + def sp_revoke_consent_revoked(issuer:, **extra) + track_event( + 'SP Revoke Consent: Revoked', + issuer: issuer, + **extra, + ) + end + + # Tracks when the page to revoke consent (unlink from) a service provider visited + # @param [String] issuer which issuer + def sp_revoke_consent_visited(issuer:, **extra) + track_event( + 'SP Revoke Consent: Visited', + issuer: issuer, + **extra, + ) + end + + # @param [String] area_code + # @param [String] country_code + # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 + # @param [String] context the context of the OTP, either "authentication" for confirmed phones + # or "confirmation" for unconfirmed + # @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( + area_code:, + country_code:, + phone_fingerprint:, context:, - reason:, - enrollment_id:, - exception_class:, - exception_message:, + otp_delivery_preference:, + resend:, + telephony_response:, + adapter:, + success:, **extra ) track_event( - 'USPS IPPaaS enrollment failed', - context: context, - enrollment_id: enrollment_id, - exception_class: exception_class, - exception_message: exception_message, - reason: reason, + 'Telephony: OTP sent', + { + area_code: area_code, + country_code: country_code, + phone_fingerprint: phone_fingerprint, + context: context, + otp_delivery_preference: otp_delivery_preference, + resend: resend, + telephony_response: telephony_response, + adapter: adapter, + success: success, + **extra, + }, + ) + end + + # Tracks when a user triggered a rate limit throttle + # @param [String] throttle_type + def throttler_rate_limit_triggered(throttle_type:, **extra) + track_event( + 'Throttler Rate Limit Triggered', + throttle_type: throttle_type, **extra, ) end - # GetUspsProofingResultsJob is beginning. Includes some metadata about what the job will do - # @param [Integer] enrollments_count number of enrollments eligible for status check - # @param [Integer] reprocess_delay_minutes minimum delay since last status check - def idv_in_person_usps_proofing_results_job_started( - enrollments_count:, - reprocess_delay_minutes:, + # Tracks when a user visits TOTP device setup + # @param [Boolean] user_signed_up + # @param [Boolean] totp_secret_present + # @param [Integer] enabled_mfa_methods_count + def totp_setup_visit( + user_signed_up:, + totp_secret_present:, + enabled_mfa_methods_count:, **extra ) track_event( - 'GetUspsProofingResultsJob: Job started', - enrollments_count: enrollments_count, - reprocess_delay_minutes: reprocess_delay_minutes, + 'TOTP Setup Visited', + user_signed_up: user_signed_up, + totp_secret_present: totp_secret_present, + enabled_mfa_methods_count: enabled_mfa_methods_count, **extra, ) end - # GetUspsProofingResultsJob has completed. Includes counts of various outcomes encountered - # @param [Float] duration_seconds number of minutes the job was running - # @param [Integer] enrollments_checked number of enrollments eligible for status check - # @param [Integer] enrollments_errored number of enrollments for which we encountered an error - # @param [Integer] enrollments_expired number of enrollments which expired - # @param [Integer] enrollments_failed number of enrollments which failed identity proofing - # @param [Integer] enrollments_in_progress number of enrollments which did not have any change - # @param [Integer] enrollments_passed number of enrollments which passed identity proofing - def idv_in_person_usps_proofing_results_job_completed( - duration_seconds:, - enrollments_checked:, - enrollments_errored:, - enrollments_expired:, - enrollments_failed:, - enrollments_in_progress:, - enrollments_passed:, + # Tracks when a user disabled a TOTP device + def totp_user_disabled + track_event('TOTP: User Disabled') + end + + # @param [String] controller + # @param [String] referer + # @param [Boolean] user_signed_in + # Redirect was almost sent to an invalid external host unexpectedly + def unsafe_redirect_error( + controller:, + referer:, + user_signed_in: nil, **extra ) track_event( - 'GetUspsProofingResultsJob: Job completed', - duration_seconds: duration_seconds, - enrollments_checked: enrollments_checked, - enrollments_errored: enrollments_errored, - enrollments_expired: enrollments_expired, - enrollments_failed: enrollments_failed, - enrollments_in_progress: enrollments_in_progress, - enrollments_passed: enrollments_passed, + 'Unsafe Redirect', + controller: controller, + referer: referer, + user_signed_in: user_signed_in, **extra, ) end - # Tracks exceptions that are raised when running GetUspsProofingResultsJob - # @param [String] reason why was the exception raised? - # @param [String] enrollment_id - # @param [String] exception_class - # @param [String] exception_message - # @param [String] enrollment_code - # @param [Float] minutes_since_established - # @param [Float] minutes_since_last_status_check - # @param [Float] minutes_since_last_status_update - # @param [Float] minutes_to_completion - # @param [Boolean] fraud_suspected - # @param [String] primary_id_type - # @param [String] secondary_id_type - # @param [String] failure_reason - # @param [String] transaction_end_date_time - # @param [String] transaction_start_date_time - # @param [String] status - # @param [String] assurance_level - # @param [String] proofing_post_office - # @param [String] proofing_city - # @param [String] proofing_state - # @param [String] scan_count - # @param [String] response_message - # @param [Integer] response_status_code - def idv_in_person_usps_proofing_results_job_exception( - reason:, - enrollment_id:, - minutes_since_established:, - exception_class: nil, - exception_message: nil, - enrollment_code: nil, - minutes_since_last_status_check: nil, - minutes_since_last_status_update: nil, - minutes_to_completion: nil, - fraud_suspected: nil, - primary_id_type: nil, - secondary_id_type: nil, - failure_reason: nil, - transaction_end_date_time: nil, - transaction_start_date_time: nil, - status: nil, - assurance_level: nil, - proofing_post_office: nil, - proofing_city: nil, - proofing_state: nil, - scan_count: nil, - response_message: nil, - response_status_code: nil, - **extra - ) + # User has attempted to access an action that requires re-authenticating + # @param [String] auth_method + # @param [String] authenticated_at + def user_2fa_reauthentication_required(auth_method:, authenticated_at:, **extra) track_event( - 'GetUspsProofingResultsJob: Exception raised', - reason: reason, - enrollment_id: enrollment_id, - exception_class: exception_class, - exception_message: exception_message, - enrollment_code: enrollment_code, - minutes_since_established: minutes_since_established, - minutes_since_last_status_check: minutes_since_last_status_check, - minutes_since_last_status_update: minutes_since_last_status_update, - minutes_to_completion: minutes_to_completion, - fraud_suspected: fraud_suspected, - primary_id_type: primary_id_type, - secondary_id_type: secondary_id_type, - failure_reason: failure_reason, - transaction_end_date_time: transaction_end_date_time, - transaction_start_date_time: transaction_start_date_time, - status: status, - assurance_level: assurance_level, - proofing_post_office: proofing_post_office, - proofing_city: proofing_city, - proofing_state: proofing_state, - scan_count: scan_count, - response_message: response_message, - response_status_code: response_status_code, + 'User 2FA Reauthentication Required', + auth_method: auth_method, + authenticated_at: authenticated_at, **extra, ) end - # Tracks deadline email initiated during GetUspsProofingResultsJob - # @param [String] enrollment_id - def idv_in_person_usps_proofing_results_job_deadline_passed_email_initiated( - enrollment_id:, - **extra - ) + # User has been marked as authenticated + # @param [String] authentication_type + def user_marked_authed(authentication_type:, **extra) track_event( - 'GetUspsProofingResultsJob: deadline passed email initiated', - enrollment_id: enrollment_id, + 'User marked authenticated', + authentication_type: authentication_type, **extra, ) end - # Tracks exceptions that are raised when initiating deadline email in GetUspsProofingResultsJob - # @param [String] enrollment_id - # @param [String] exception_class - # @param [String] exception_message - def idv_in_person_usps_proofing_results_job_deadline_passed_email_exception( - enrollment_id:, - exception_class: nil, - exception_message: nil, - **extra - ) + # User was shown an "Are you sure you want to navigate away from this page?" message from their + # browser (via onbeforeunload). (This is a frontend event.) + def user_prompted_before_navigation track_event( - 'GetUspsProofingResultsJob: Exception raised when attempting to send deadline passed email', - enrollment_id: enrollment_id, - exception_class: exception_class, - exception_message: exception_message, + 'User prompted before navigation', + ) + end + + # User was shown an "Are you sure you want to navigate away from this page?" prompt via + # onbeforeunload and was still on the page later. (This is a frontend event.) + # @param [Integer] seconds Amount of time user has been on page since prompt. + def user_prompted_before_navigation_and_still_on_page(seconds:, **extra) + track_event( + 'User prompted before navigation and still on page', + seconds: seconds, **extra, ) 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, + # @param [Boolean] success + # @param [Hash] errors + # @param [String] sign_up_mfa_priority_bucket + # Tracks when the the user has selected and submitted additional MFA methods on user registration + def user_registration_2fa_additional_setup(success:, + sign_up_mfa_priority_bucket:, + errors: nil, + **extra) + track_event( + 'User Registration: Additional 2FA Setup', + { + success: success, + sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, + errors: errors, + **extra, + }.compact, + ) + end + + # Tracks when user visits additional MFA selection page + # @param [String] sign_up_mfa_priority_bucket + def user_registration_2fa_additional_setup_visit(sign_up_mfa_priority_bucket:, **extra) + track_event( + 'User Registration: Additional 2FA Setup visited', + sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, + **extra, + ) + end + + # @param [Boolean] success + # @param [Hash] errors + # @param [Integer] enabled_mfa_methods_count + # @param [Integer] selected_mfa_count + # @param ['voice', 'auth_app'] selection + # @param [String] sign_up_mfa_priority_bucket + # Tracks when the the user has selected and submitted MFA auth methods on user registration + def user_registration_2fa_setup( + success:, + sign_up_mfa_priority_bucket:, + errors: nil, + selected_mfa_count: nil, + enabled_mfa_methods_count: nil, + selection: 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, + 'User Registration: 2FA Setup', + { + success: success, + errors: errors, + selected_mfa_count: selected_mfa_count, + enabled_mfa_methods_count: enabled_mfa_methods_count, + sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, + selection: selection, + **extra, + }.compact, + ) + end + + # Tracks when user visits MFA selection page + # @param [String] sign_up_mfa_priority_bucket + def user_registration_2fa_setup_visit(sign_up_mfa_priority_bucket:, **extra) + track_event( + 'User Registration: 2FA Setup visited', + sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, **extra, ) end - # Tracks individual enrollments that are updated during GetUspsProofingResultsJob - # @param [String] enrollment_code - # @param [String] enrollment_id - # @param [Float] minutes_since_established - # @param [Boolean] fraud_suspected - # @param [Boolean] passed did this enrollment pass or fail? - # @param [String] reason why did this enrollment pass or fail? - def idv_in_person_usps_proofing_results_job_enrollment_updated( - enrollment_code:, - enrollment_id:, - minutes_since_established:, - fraud_suspected:, - passed:, - reason:, + # User registration has been handed off to agency page + # @param [Boolean] ial2 + # @param [Integer] ialmax + # @param [String] service_provider_name + # @param [String] page_occurence + # @param [String] needs_completion_screen_reason + # @param [Array] sp_request_requested_attributes + # @param [Array] sp_session_requested_attributes + def user_registration_agency_handoff_page_visit( + ial2:, + service_provider_name:, + page_occurence:, + needs_completion_screen_reason:, + sp_session_requested_attributes:, + sp_request_requested_attributes: nil, + ialmax: nil, + **extra + ) + track_event( + 'User registration: agency handoff visited', + ial2: ial2, + ialmax: ialmax, + service_provider_name: service_provider_name, + page_occurence: page_occurence, + needs_completion_screen_reason: needs_completion_screen_reason, + sp_request_requested_attributes: sp_request_requested_attributes, + sp_session_requested_attributes: sp_session_requested_attributes, + **extra, + ) + end + + # Tracks when user cancels registration + # @param [String] request_came_from the controller/action the request came from + def user_registration_cancellation(request_came_from:, **extra) + track_event( + 'User registration: cancellation visited', + request_came_from: request_came_from, + **extra, + ) + end + + # Tracks when user completes registration + # @param [Boolean] ial2 + # @param [Boolean] ialmax + # @param [String] service_provider_name + # @param [String] page_occurence + # @param [String] needs_completion_screen_reason + # @param [String] sign_in_a_b_test_bucket + # @param [Array] sp_request_requested_attributes + # @param [Array] sp_session_requested_attributes + def user_registration_complete( + ial2:, + service_provider_name:, + page_occurence:, + needs_completion_screen_reason:, + sign_in_a_b_test_bucket:, + sp_session_requested_attributes:, + sp_request_requested_attributes: nil, + ialmax: nil, **extra ) track_event( - 'GetUspsProofingResultsJob: Enrollment status updated', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - minutes_since_established: minutes_since_established, - fraud_suspected: fraud_suspected, - passed: passed, - reason: reason, + 'User registration: complete', + ial2: ial2, + ialmax: ialmax, + service_provider_name: service_provider_name, + page_occurence: page_occurence, + needs_completion_screen_reason: needs_completion_screen_reason, + sign_in_a_b_test_bucket:, + sp_request_requested_attributes: sp_request_requested_attributes, + sp_session_requested_attributes: sp_session_requested_attributes, **extra, ) end - # Tracks emails that are initiated during GetUspsProofingResultsJob - # @param [String] email_type success, failed or failed fraud - def idv_in_person_usps_proofing_results_job_email_initiated( - email_type:, + # Tracks when user submits registration email + # @param [Boolean] success + # @param [Boolean] throttled + # @param [Hash] errors + # @param [Hash] error_details + # @param [String] user_id + # @param [String] domain_name + def user_registration_email( + success:, + throttled:, + errors:, + error_details: nil, + user_id: nil, + domain_name: nil, **extra ) track_event( - 'GetUspsProofingResultsJob: Success or failure email initiated', - email_type: email_type, - **extra, + 'User Registration: Email Submitted', + { + success: success, + throttled: throttled, + errors: errors, + error_details: error_details, + user_id: user_id, + domain_name: domain_name, + **extra, + }.compact, ) 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:, + # Tracks when user confirms registration email + # @param [Boolean] success + # @param [Hash] errors + # @param [Hash] error_details + # @param [String] user_id + def user_registration_email_confirmation( + success:, + errors:, + error_details: nil, + user_id: nil, **extra ) track_event( - 'InPerson::EmailReminderJob: Reminder email initiated', - email_type: email_type, - enrollment_id: enrollment_id, + 'User Registration: Email Confirmation', + success: success, + errors: errors, + error_details: error_details, + user_id: user_id, **extra, ) end - # Tracks incomplete enrollments checked via the USPS API - # @param [String] enrollment_code - # @param [String] enrollment_id - # @param [Float] minutes_since_established - # @param [String] response_message - def idv_in_person_usps_proofing_results_job_enrollment_incomplete( - enrollment_code:, - enrollment_id:, - minutes_since_established:, - response_message:, - **extra - ) + # Tracks when user visits enter email page + # @param [String] sign_in_a_b_test_bucket + # @param [Boolean] from_sign_in + def user_registration_enter_email_visit(sign_in_a_b_test_bucket:, from_sign_in:, **extra) track_event( - 'GetUspsProofingResultsJob: Enrollment incomplete', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - minutes_since_established: minutes_since_established, - response_message: response_message, + 'User Registration: enter email visited', + sign_in_a_b_test_bucket:, + from_sign_in:, **extra, ) end - # Tracks unexpected responses from the USPS API - # @param [String] enrollment_code - # @param [String] enrollment_id - # @param [Float] minutes_since_established - # @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:, - minutes_since_established:, - response_message:, - reason:, + # @param [Boolean] success + # @param [Hash] mfa_method_counts + # @param [Integer] enabled_mfa_methods_count + # @param [Hash] pii_like_keypaths + # Tracks when a user has completed MFA setup + def user_registration_mfa_setup_complete( + success:, + mfa_method_counts:, + enabled_mfa_methods_count:, + pii_like_keypaths:, **extra ) track_event( - 'GetUspsProofingResultsJob: Unexpected response received', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - minutes_since_established: minutes_since_established, - response_message: response_message, - reason: reason, - **extra, + 'User Registration: MFA Setup Complete', + { + success: success, + mfa_method_counts: mfa_method_counts, + enabled_mfa_methods_count: enabled_mfa_methods_count, + pii_like_keypaths: pii_like_keypaths, + **extra, + }.compact, ) end - # Tracks users visiting the recovery options page - def account_reset_recovery_options_visit - track_event('Account Reset: Recovery Options Visited') + # @param [Integer] enabled_mfa_methods_count + # Tracks when user visits the phone setup step during registration + def user_registration_phone_setup_visit(enabled_mfa_methods_count:, **extra) + track_event( + 'User Registration: phone setup visited', + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + ) end - # Tracks users going back or cancelling acoount recovery - def cancel_account_reset_recovery - track_event('Account Reset: Cancel Account Recovery Options') + # Tracks when user's piv cac is disabled + def user_registration_piv_cac_disabled + track_event('User Registration: piv cac disabled') end - # @identity.idp.previous_event_name IdV: Verify setup errors visited - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # Tracks when the user reaches the verify please call page after failing proofing - def idv_please_call_visited(proofing_components: nil, **extra) + # Tracks when user's piv cac setup + def user_registration_piv_cac_setup_visit(**extra) track_event( - 'IdV: Verify please call visited', - proofing_components: proofing_components, + 'User Registration: piv cac setup visited', **extra, ) end - # Tracks when user reaches verify errors due to being rejected due to fraud - def idv_not_verified_visited - track_event('IdV: Not verified visited') + # Tracks when user skips Suggest Another MFA Page + def user_registration_suggest_another_mfa_notice_skipped + track_event('User Registration: Suggest Another MFA Notice Skipped') end - # @param [String] redirect_url URL user was directed to - # @param [String, nil] step which step - # @param [String, nil] location which part of a step, if applicable - # @param ["idv", String, nil] flow which flow - # User was redirected to the login.gov contact page - def contact_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extra) + # Tracks when user visits Suggest Another MFA Page + def user_registration_suggest_another_mfa_notice_visited + track_event('User Registration: Suggest Another MFA Notice visited') + end + + # @param [String] mfa_method + # Tracks when the the user fully registered by submitting their first MFA method into the system + def user_registration_user_fully_registered( + mfa_method:, + **extra + ) track_event( - 'Contact Page Redirect', - redirect_url: redirect_url, - step: step, - location: location, - flow: flow, - **extra, + 'User Registration: User Fully Registered', + { + mfa_method: mfa_method, + **extra, + }.compact, ) end - # @param [String] redirect_url URL user was directed to - # @param [String, nil] step which step - # @param [String, nil] location which part of a step, if applicable - # @param ["idv", String, nil] flow which flow - # User was redirected to the login.gov policy page - def policy_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extra) + # Tracks when USPS in-person proofing enrollment is created + # @param [String] enrollment_code + # @param [Integer] enrollment_id + # @param [String] service_provider + def usps_ippaas_enrollment_created( + enrollment_code:, + enrollment_id:, + service_provider:, + **extra + ) track_event( - 'Policy Page Redirect', - redirect_url: redirect_url, - step: step, - location: location, - flow: flow, + 'USPS IPPaaS enrollment created', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + service_provider: service_provider, **extra, ) end - # Tracks if a user clicks the "Show Password button" - # @param [String] path URL path where the click occurred - def show_password_button_clicked(path:, **extra) - track_event('Show Password Button Clicked', path: path, **extra) + # @param [Hash] vendor_status + # @param [String,nil] redirect_from + # Tracks when vendor has outage + def vendor_outage( + vendor_status:, + redirect_from: nil, + **extra + ) + track_event( + 'Vendor Outage', + redirect_from: redirect_from, + vendor_status: vendor_status, + **extra, + ) end - # Tracks if a user clicks the 'acknowledge' checkbox during personal - # key creation - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # @param [boolean] checked whether the user checked or un-checked - # the box with this click - def idv_personal_key_acknowledgment_toggled(checked:, proofing_components:, **extra) + # @param [Boolean] success + # @param [Integer] mfa_method_counts + # Tracks when WebAuthn is deleted + def webauthn_deleted(success:, mfa_method_counts:, pii_like_keypaths:, **extra) track_event( - 'IdV: personal key acknowledgment toggled', - checked: checked, - proofing_components: proofing_components, + 'WebAuthn Deleted', + success: success, + mfa_method_counts: mfa_method_counts, + pii_like_keypaths: pii_like_keypaths, **extra, ) end - # Logs after an email is sent - # @param [String] action type of email being sent - # @param [String, nil] ses_message_id AWS SES Message ID - def email_sent(action:, ses_message_id:, **extra) + # @param [Hash] platform_authenticator + # @param [Hash] errors + # @param [Integer] enabled_mfa_methods_count + # @param [Boolean] success + # Tracks when WebAuthn setup is visited + def webauthn_setup_visit(platform_authenticator:, errors:, enabled_mfa_methods_count:, success:, + **extra) track_event( - 'Email Sent', - action: action, - ses_message_id: ses_message_id, + 'WebAuthn Setup Visited', + platform_authenticator: platform_authenticator, + errors: errors, + enabled_mfa_methods_count: enabled_mfa_methods_count, + success: success, **extra, ) end diff --git a/app/services/idv/actions/cancel_link_sent_action.rb b/app/services/idv/actions/cancel_link_sent_action.rb index 66dc4ba49ae..60393229121 100644 --- a/app/services/idv/actions/cancel_link_sent_action.rb +++ b/app/services/idv/actions/cancel_link_sent_action.rb @@ -8,6 +8,7 @@ def self.analytics_submitted_event def call if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil else mark_step_incomplete(:upload) end diff --git a/app/services/idv/actions/redo_document_capture_action.rb b/app/services/idv/actions/redo_document_capture_action.rb index 2054bc349ce..40ffbe4aabe 100644 --- a/app/services/idv/actions/redo_document_capture_action.rb +++ b/app/services/idv/actions/redo_document_capture_action.rb @@ -11,6 +11,7 @@ def call redirect_to idv_document_capture_url elsif IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil else mark_step_incomplete(:upload) end diff --git a/app/services/idv/steps/agreement_step.rb b/app/services/idv/steps/agreement_step.rb index e5eb9076bfc..21942060dd9 100644 --- a/app/services/idv/steps/agreement_step.rb +++ b/app/services/idv/steps/agreement_step.rb @@ -16,6 +16,7 @@ def call if flow_session[:skip_upload_step] redirect_to idv_document_capture_url + flow_session[:flow_path] = 'standard' else redirect_to idv_hybrid_handoff_url end @@ -30,9 +31,6 @@ def form_submit def skip_to_capture # See: Idv::DocAuthController#update_if_skipping_upload flow_session[:skip_upload_step] = true - if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled - flow_session[:flow_path] = 'standard' - end end def consent_form_params diff --git a/app/services/usps_in_person_proofing/date_validator.rb b/app/services/usps_in_person_proofing/date_validator.rb new file mode 100644 index 00000000000..02b66c6df4d --- /dev/null +++ b/app/services/usps_in_person_proofing/date_validator.rb @@ -0,0 +1,37 @@ +module UspsInPersonProofing + # Validator that can be attached to a form or other model + # to verify that specific supported fields are comparable to other dates. + # Since it's a subclass of ComparisonValidator, it share the same options. + # == Example + # + # validates_with UspsInPersonProofing::DateValidator, + # attributes: [:dob], + # message: "error", + # less_than_or_equal_to: ->(_rec) { + # Time.zone.today - IdentityConfig.store.idv_min_age_years.years + # } + # .... + # + class DateValidator < ActiveModel::Validations::ComparisonValidator + private + + def prepare_value_for_validation(value, _record, _attr_name) + val_to_date(value) + rescue + nil + end + + # @param [String,Date,#to_hash] param + # @return [Date] + # It's caller's responsibility to ensure the param contains required entries + def val_to_date(param) + case param + when String, Date + DateParser.parse_legacy(param) + else + h = param.to_hash.with_indifferent_access + Date.new(h[:year].to_i, h[:month].to_i, h[:day].to_i) + end + end + end +end diff --git a/app/validators/form_password_validator.rb b/app/validators/form_password_validator.rb index 4e363d3c1d2..2f83c7e86b6 100644 --- a/app/validators/form_password_validator.rb +++ b/app/validators/form_password_validator.rb @@ -2,7 +2,7 @@ module FormPasswordValidator extend ActiveSupport::Concern included do - attr_accessor :password, :password_confirmation, :validate_confirmation, :confirmation_enabled + attr_accessor :password, :password_confirmation, :validate_confirmation attr_reader :user validates :password, @@ -11,7 +11,7 @@ module FormPasswordValidator validates :password_confirmation, presence: true, length: { in: Devise.password_length }, - if: -> { confirmation_enabled && validate_confirmation } + if: -> { validate_confirmation } validate :password_graphemes_length, :strong_password, :not_pwned, :passwords_match end @@ -54,7 +54,7 @@ def not_pwned end def passwords_match - return unless confirmation_enabled && validate_confirmation + return unless validate_confirmation if password != password_confirmation errors.add( diff --git a/app/validators/idv/form_state_id_validator.rb b/app/validators/idv/form_state_id_validator.rb index 89c4dc03d18..6e409bd9916 100644 --- a/app/validators/idv/form_state_id_validator.rb +++ b/app/validators/idv/form_state_id_validator.rb @@ -2,6 +2,7 @@ module Idv module FormStateIdValidator extend ActiveSupport::Concern + # rubocop:disable Metrics/BlockLength included do validates :first_name, :last_name, @@ -29,6 +30,17 @@ module FormStateIdValidator char_list: invalid_chars.join(', '), ) end + # rubocop:disable Layout/LineLength + validates_with UspsInPersonProofing::DateValidator, + attributes: [:dob], less_than_or_equal_to: ->(_rec) { + Time.zone.today - IdentityConfig.store.idv_min_age_years.years + }, + message: I18n.t( + 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age', + app_name: APP_NAME, + ) + # rubocop:enable Layout/LineLength end + # rubocop:enable Metrics/BlockLength end end diff --git a/app/views/idv/gpo/index.html.erb b/app/views/idv/gpo/index.html.erb index 69ec0f3f07b..58664779d38 100644 --- a/app/views/idv/gpo/index.html.erb +++ b/app/views/idv/gpo/index.html.erb @@ -9,14 +9,24 @@ ) %> <% end %> -<%= render AlertComponent.new(message: t('idv.messages.gpo.info_alert'), class: 'margin-bottom-4') %> - -<%= render PageHeadingComponent.new.with_content(@presenter.title) %> - -

- <%= t('idv.messages.gpo.address_on_file_html') %> - <%= t('idv.messages.gpo.timeframe_html') %> -

+<% if @presenter.resend_requested? %> + <%= render StatusPageComponent.new(status: :warning) do |c| %> + <% c.with_header { @presenter.title } %> +

+ <%= t('idv.messages.gpo.resend_timeframe') %> +

+

+ <%= t('idv.messages.gpo.resend_code_warning') %> +

+ <% end %> +<% else %> + <%= render AlertComponent.new(message: t('idv.messages.gpo.info_alert'), class: 'margin-bottom-4') %> + <%= render PageHeadingComponent.new.with_content(@presenter.title) %> +

+ <%= t('idv.messages.gpo.address_on_file_html') %> + <%= t('idv.messages.gpo.timeframe_html') %> +

+<% end %>
<%= button_to @presenter.button, diff --git a/app/views/idv/review/new.html.erb b/app/views/idv/review/new.html.erb index 192a21354dc..53a36869575 100644 --- a/app/views/idv/review/new.html.erb +++ b/app/views/idv/review/new.html.erb @@ -3,7 +3,7 @@ <% content_for(:pre_flash_content) do %> <%= render StepIndicatorComponent.new( steps: step_indicator_steps, - current_step: :secure_account, + current_step: step_indicator_step, locale_scope: 'idv', class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', ) %> @@ -12,14 +12,9 @@ <%= render PageHeadingComponent.new.with_content(t('idv.titles.session.review', app_name: APP_NAME)) %>

- <%= t('idv.messages.sessions.review_message', app_name: APP_NAME) %> + <%= t('idv.messages.review.message', app_name: APP_NAME) %>

-<%= new_tab_link_to( - t('idv.messages.sessions.read_more_encrypt', app_name: APP_NAME), - MarketingSite.security_url, - ) %> - <%= simple_form_for( current_user, url: idv_review_path, @@ -35,14 +30,9 @@ }, }, ) %> -
+
<%= link_to(t('idv.forgot_password.link_text'), idv_forgot_password_url, class: 'margin-left-1') %>
- <%= render AccordionComponent.new do |c| %> - <% c.with_header { t('idv.messages.review.intro') } %> - <%= render 'shared/pii_review', pii: @applicant, - phone: PhoneFormatter.format(@applicant[:phone]) %> - <% end %> <%= f.submit t('forms.buttons.continue'), class: 'margin-top-5' %> <% end %> diff --git a/app/views/shared/_pii_review.html.erb b/app/views/shared/_pii_review.html.erb deleted file mode 100644 index 37c5c82f5a5..00000000000 --- a/app/views/shared/_pii_review.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -
-
- <%= t('idv.review.full_name') %> -
- -
- <%= pii[:first_name] %> <%= pii[:last_name] %> -
- -
- <%= t('idv.review.mailing_address') %> -
- -
- <%= render 'shared/address', address: pii %> -
- -
- <%= t('idv.review.dob') %> -
- -
- <%= DateParser.parse_legacy(pii[:dob]).to_formatted_s(:long) %> -
- -
- <%= t('idv.review.ssn') %> -
- -
- <%= pii[:ssn] %> -
- - <% if phone %> -
- <%= t('idv.messages.phone.phone_of_record') %> -
-
- <%= PhoneFormatter.format(phone) %> -
- <% end %> -
diff --git a/app/views/users/phone_setup/spam_protection.html.erb b/app/views/users/phone_setup/spam_protection.html.erb index 61d823d06f7..93147fbf77a 100644 --- a/app/views/users/phone_setup/spam_protection.html.erb +++ b/app/views/users/phone_setup/spam_protection.html.erb @@ -58,3 +58,9 @@ new_tab: true, ).with_content(t('two_factor_authentication.phone_verification.troubleshooting.learn_more')) %> <% end %> + +<% unless local_assigns[:two_factor_options_path].present? %> + <%= render PageFooterComponent.new do %> + <%= link_to t('links.cancel'), account_path %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/users/verify_password/new.html.erb b/app/views/users/verify_password/new.html.erb index 4062087225e..a05a0abd77f 100644 --- a/app/views/users/verify_password/new.html.erb +++ b/app/views/users/verify_password/new.html.erb @@ -23,10 +23,3 @@ ) %> <%= f.submit t('forms.buttons.continue'), class: 'margin-top-5' %> <% end %> - -
- <%= render AccordionComponent.new do |c| %> - <% c.with_header { t('idv.messages.review.intro') } %> - <%= render 'shared/pii_review', pii: @decrypted_pii, phone: @decrypted_pii[:phone] %> - <% end %> -
diff --git a/bin/setup b/bin/setup index e8431e847b2..4a92358cb1d 100755 --- a/bin/setup +++ b/bin/setup @@ -62,7 +62,7 @@ Dir.chdir APP_ROOT do puts "\n== Starting services ==" run "brew services start redis" if brew_installed - run "brew services start postgresql@13" if brew_installed + run "brew services start postgresql@14" if brew_installed puts "\n== Preparing database ==" run 'make clobber_db' diff --git a/config/application.yml.default b/config/application.yml.default index a63c02d1c93..e53c475b156 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -355,7 +355,9 @@ usps_ipp_sponsor_id: '' usps_ipp_username: '' usps_mock_fallback: true usps_ipp_transliteration_enabled: false -get_usps_proofing_results_job_cron: '0/10 * * * *' +get_usps_ready_proofing_results_job_cron: '0/10 * * * *' +get_usps_waiting_proofing_results_job_cron: '0/30 * * * *' +get_usps_proofing_results_job_cron: '0/30 * * * *' get_usps_proofing_results_job_reprocess_delay_minutes: 5 get_usps_proofing_results_job_request_delay_milliseconds: 1000 voice_otp_pause_time: '0.5s' diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index 675bb6c44ec..b6380ab6fab 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -116,6 +116,18 @@ cron: IdentityConfig.store.get_usps_proofing_results_job_cron, args: -> { [Time.zone.now] }, }, + # Queue usps proofing job to GoodJob for ready enrollments + get_usps_ready_proofing_results_job: { + class: 'GetUspsReadyProofingResultsJob', + cron: IdentityConfig.store.get_usps_ready_proofing_results_job_cron, + args: -> { [Time.zone.now] }, + }, + # Queue usps proofing job to GoodJob for waiting enrollments + get_usps_waiting_proofing_results_job: { + class: 'GetUspsWaitingProofingResultsJob', + cron: IdentityConfig.store.get_usps_waiting_proofing_results_job_cron, + args: -> { [Time.zone.now] }, + }, # Queue daily in-person proofing reminder email job email_reminder_job: { class: 'InPerson::EmailReminderJob', diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index 0ee13be812f..23976eacca7 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -200,6 +200,10 @@ en: info_alert: You’ll need to wait until your letter is delivered to finish verifying your identity. resend: Send me another letter + resend_code_warning: If you request a new letter now, the one-time code from + your current letter will remain valid for a limited time. You may + still use the one-time code from either letter. + resend_timeframe: Letters typically take 3 to 7 business days to arrive. timeframe_html: Letters are sent the next business day via USPS First Class Mail and typically take 3 to 7 business days to arrive. otp_delivery_method_description: If you entered a landline above, please select “Phone call” below. @@ -209,13 +213,15 @@ en: alert_html: 'Enter a phone number that is:' description: We’ll check this number with records and send you a one-time code. This is to help verify your identity. - phone_of_record: Phone of record rules: - Based in the United States (including U.S. territories) - Your primary number (the one you use the most often) return_to_profile: '‹ Return to your %{app_name} profile' review: - intro: Your verified information + gpo_pending: We’ll send your letter once you re-enter your password. + message: '%{app_name} will encrypt your information with your password. This + means that your information is secure and only you will be able to + access or change it.' phone_verified: We verified your phone number select_verification_with_sp: To protect you from identity fraud, we will contact you to confirm that this %{sp_name} account is legitimate. @@ -224,25 +230,19 @@ en: sessions: no_pii: TEST SITE - Do not use real personal information (demo purposes only) - TEST SITE - read_more_encrypt: Read more about how %{app_name} protects your personal information review_message: When you re-enter your password, %{app_name} will protect the information you’ve given us, so that only you can access it. verifying: Verifying… - review: - dob: Date of birth - full_name: Full name - mailing_address: Mailing address - ssn: Social Security number (SSN) titles: activated: Your identity has already been verified come_back_later: Your letter is on the way mail: - resend: Want another letter? + resend: Send another letter? verify: Want a letter? otp_delivery_method: How should we send a code? review: Review and submit session: - review: Re-enter your %{app_name} password to protect your data + review: Re-enter your %{app_name} password unavailable: 'We are working to resolve an error' troubleshooting: headings: diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index 636f3519321..8de261b44d7 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -212,6 +212,11 @@ es: info_alert: Tendrá que esperar a que su carta sea entregada para terminar de verificar su identidad. resend: Envíeme otra carta + resend_code_warning: Si solicitas una nueva carta ahora, el código de una sola + vez de tu carta actual seguirá siendo válido durante un tiempo + limitado. Puedes seguir utilizando el código de una sola vez de + cualquiera de las dos cartas. + resend_timeframe: Las cartas suelen tardar entre 3 y 7 días hábiles en llegar. timeframe_html: Las cartas se envían al día siguiente por First Class Mail de USPS y suelen tardar entre 3 y 7 días hábiles en llegar. @@ -223,13 +228,15 @@ es: alert_html: 'Introduzca un número de teléfono que sea:' description: Comprobaremos este número con los registros y le enviaremos un código único. Esto es para ayudar a verificar su identidad. - phone_of_record: Teléfono del registro rules: - Con base en Estados Unidos (incluidos los territorios de EE.UU.) - Su número principal (el que utiliza con más frecuencia) return_to_profile: '‹ Volver a tu perfil de %{app_name}' review: - intro: Su información verificada + gpo_pending: Enviaremos tu carta una vez que hayas ingresado tu contraseña. + message: '%{app_name} encriptará tu información con tu contraseña. Esto + significa que tu información estará segura y solo tú podrás + consultarla o modificarla.' phone_verified: Verificamos su número de teléfono select_verification_with_sp: Para protegerlo de robo de identidad, no puede utilizar su cuenta en %{sp_name} hasta que la active ingresando un @@ -239,25 +246,19 @@ es: sessions: no_pii: SITIO DE PRUEBA - No utilice información personal real (sólo para propósitos de demostración) - SITIO DE PRUEBA - read_more_encrypt: Lea más sobre cómo %{app_name} protege su información personal review_message: Cuando vuelva a ingresar su contraseña, %{app_name} cifrará sus datos para asegurarse de que nadie más pueda acceder a ellos. verifying: Verificando… - review: - dob: Fecha de nacimiento - full_name: Nombre completo - mailing_address: Dirección postal - ssn: Número de Seguro Social (SSN, sigla en inglés) titles: activated: Ya se verificó tu identidad. come_back_later: Su carta está en camino mail: - resend: '¿Desea otra carta?' + resend: '¿Enviar otra carta?' verify: '¿Desea una carta?' otp_delivery_method: '¿Cómo debemos enviar un código?' review: Revise y envíe session: - review: Vuelve a ingresar tu contraseña de %{app_name} para encriptar tus datos + review: 'Vuelve a ingresar tu contraseña de %{app_name}' unavailable: Estamos trabajando para resolver un error troubleshooting: headings: diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index d32f10c8eb0..32ee3c3b972 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -225,6 +225,12 @@ fr: info_alert: Vous devrez attendre la livraison de votre lettre pour compléter la vérification de votre identité. resend: Envoyez-moi une autre lettre + resend_code_warning: Si vous demandez une nouvelle lettre maintenant, le code à + usage unique de votre lettre actuelle restera valable pendant une + période limitée. Vous pouvez toujours utiliser le code à usage unique + de l’une ou l’autre lettre. + resend_timeframe: Les lettres mettent généralement entre trois et sept jours + ouvrables pour arriver. timeframe_html: Les lettres sont envoyées les jours ouvrables par courriel de première classe de USPS et prennent généralement entre trois à sept jours ouvrables pour être reçues. @@ -237,13 +243,16 @@ fr: alert_html: 'Entrez un numéro de téléphone qui est :' description: Nous vérifierons ce numéro dans nos archives et vous enverrons un code à usage unique. Ceci est pour aider à vérifier votre identité. - phone_of_record: numéro de téléphone enregistré rules: - Basé aux Etats-Unis (y compris les territoires américains) - Votre numéro principal (celui que vous utilisez le plus souvent) return_to_profile: '‹ Revenir à votre profil %{app_name}' review: - intro: Vos informations vérifiées + gpo_pending: Nous vous enverrons votre lettre une fois que vous aurez + réintroduit votre mot de passe. + message: '%{app_name} crypte vos informations avec votre mot de passe. Cela + signifie que vos informations sont sécurisées et que vous seul pourrez + y accéder ou les modifier.' phone_verified: Nous avons vérifié votre numéro de téléphone select_verification_with_sp: Afin de vous protéger des fraudes d’identité, vous ne pouvez pas utiliser votre compte au %{sp_name} tant que vous ne @@ -254,26 +263,19 @@ fr: sessions: no_pii: SITE DE TEST - N’utilisez pas de véritables données personnelles (il s’agit d’une démonstration seulement) - SITE DE TEST - read_more_encrypt: En savoir plus sur la façon dont %{app_name} protège vos - informations personnelles review_message: Lorsque vous entrez à nouveau votre mot de passe, %{app_name} crypte vos données pour vous assurer que personne ne peut y accéder. verifying: Vérification… - review: - dob: Date de naissance - full_name: Nom complet - mailing_address: Adresse postale - ssn: Numéro de sécurité sociale (SSN) titles: activated: Votre identité a déjà été vérifiée come_back_later: Votre lettre est en route mail: - resend: Vous voulez une autre lettre? + resend: Envoyer une autre lettre? verify: Vous voulez une lettre? otp_delivery_method: Comment envoyer un code? review: Réviser et soumettre session: - review: Entrez à nouveau votre mot de passe %{app_name} pour crypter vos données + review: 'Saisissez à nouveau votre mot de passe %{app_name}' unavailable: Nous travaillons à la résolution d’une erreur troubleshooting: headings: diff --git a/db/primary_migrate/20230601195606_add_columns_for_user_suspension.rb b/db/primary_migrate/20230601195606_add_columns_for_user_suspension.rb new file mode 100644 index 00000000000..8e687d37a07 --- /dev/null +++ b/db/primary_migrate/20230601195606_add_columns_for_user_suspension.rb @@ -0,0 +1,6 @@ +class AddColumnsForUserSuspension < ActiveRecord::Migration[7.0] + def change + add_column :users, :suspended_at, :datetime + add_column :users, :reinstated_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 0135afe0ba2..0443b612784 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_05_03_231037) do +ActiveRecord::Schema[7.0].define(version: 2023_06_01_195606) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pgcrypto" @@ -590,6 +590,8 @@ t.string "email_language", limit: 10 t.datetime "accepted_terms_at", precision: nil t.datetime "encrypted_recovery_code_digest_generated_at", precision: nil + t.datetime "suspended_at" + t.datetime "reinstated_at" t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["uuid"], name: "index_users_on_uuid", unique: true end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 583f5888d03..ca7e8011ed9 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -450,6 +450,8 @@ def self.build_store(config_map) config.add(:usps_ipp_request_timeout, type: :integer) config.add(:usps_upload_enabled, type: :boolean) config.add(:usps_ipp_transliteration_enabled, type: :boolean) + config.add(:get_usps_ready_proofing_results_job_cron, type: :string) + config.add(:get_usps_waiting_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_reprocess_delay_minutes, type: :integer) config.add(:get_usps_proofing_results_job_request_delay_milliseconds, type: :integer) diff --git a/lib/telephony/alert_sender.rb b/lib/telephony/alert_sender.rb index 7686fc248f3..a8e08bd2983 100644 --- a/lib/telephony/alert_sender.rb +++ b/lib/telephony/alert_sender.rb @@ -40,7 +40,7 @@ def send_personal_key_regeneration_notice(to:, country_code:) end def send_personal_key_sign_in_notice(to:, country_code:) - message = I18n.t('telephony.personal_key_sign_in_notice') + message = I18n.t('telephony.personal_key_sign_in_notice', app_name: APP_NAME) response = adapter.send(message: message, to: to, country_code: country_code) log_response(response, context: __method__.to_s.gsub(/^send_/, '')) response diff --git a/spec/components/memorable_date_component_spec.rb b/spec/components/memorable_date_component_spec.rb index 164ff54991d..02bb5e2f4dd 100644 --- a/spec/components/memorable_date_component_spec.rb +++ b/spec/components/memorable_date_component_spec.rb @@ -278,4 +278,17 @@ MemorableDateComponent.extract_date_param('abcd'), ).to be_nil end + + context 'backend validation error message' do + let(:backend_error) { 'backend error' } + it 'renders a visible error message element' do + allow(form_builder.object).to receive(:errors).and_return( + { + name => [backend_error], + }, + ) + expect(rendered).not_to have_css('.usa-error-message.display-none') + expect(rendered.css('.usa-error-message')).to have_text(backend_error) + end + end end diff --git a/spec/controllers/concerns/rate_limit_concern_spec.rb b/spec/controllers/concerns/rate_limit_concern_spec.rb index 848d2904944..d49a1bef4f2 100644 --- a/spec/controllers/concerns/rate_limit_concern_spec.rb +++ b/spec/controllers/concerns/rate_limit_concern_spec.rb @@ -6,6 +6,7 @@ module Idv class StepController < ApplicationController include RateLimitConcern + include IdvSession def show render plain: 'Hello' diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 7224ba5d670..1d7b7d5eebb 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -6,8 +6,7 @@ let(:flow_session) do { 'document_capture_session_uuid' => 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e', :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0', - :flow_path => 'standard', - 'Idv::Steps::UploadStep' => true } + :flow_path => 'standard' } end let(:user) { create(:user) } @@ -19,9 +18,6 @@ ) end - let(:default_sdk_version) { IdentityConfig.store.idv_acuant_sdk_version_default } - let(:alternate_sdk_version) { IdentityConfig.store.idv_acuant_sdk_version_alternate } - before do allow(subject).to receive(:flow_session).and_return(flow_session) stub_sign_in(user) @@ -102,6 +98,13 @@ end end + it 'does not use effective user outside of analytics_user in ApplicationControler' do + allow(subject).to receive(:analytics_user).and_return(subject.current_user) + expect(subject).not_to receive(:effective_user) + + get :show + end + context 'user is rate_limited' do it 'redirects to rate limited page' do user = create(:user) diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 62fa39f37d2..19e8b525529 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -28,13 +28,19 @@ :confirm_agreement_step_complete, ) end + + it 'checks that hybrid_handoff is needed' do + expect(subject).to have_actions( + :before, + :confirm_hybrid_handoff_needed, + ) + end end describe '#show' do let(:analytics_name) { 'IdV: doc auth upload visited' } let(:analytics_args) do - { flow_path: 'standard', - step: 'upload', + { step: 'upload', analytics_id: 'Doc Auth', irs_reproofing: false } end @@ -68,25 +74,68 @@ expect(response).to redirect_to(idv_doc_auth_url) end end + + context 'hybrid_handoff already visited' do + it 'redirects to document_capture in standard flow' do + subject.user_session['idv/doc_auth'][:flow_path] = 'standard' + + get :show + + expect(response).to redirect_to(idv_document_capture_url) + end + + it 'redirects to link_sent in hybrid flow' do + subject.user_session['idv/doc_auth'][:flow_path] = 'hybrid' + + get :show + + expect(response).to redirect_to(idv_link_sent_url) + end + end end describe '#update' do let(:analytics_name) { 'IdV: doc auth upload submitted' } - let(:analytics_args) do - { success: true, - errors: {}, - destination: :link_sent, - flow_path: 'hybrid', - step: 'upload', - analytics_id: 'Doc Auth', - irs_reproofing: false, - skip_upload_step: false } + + context 'hybrid flow' do + let(:analytics_args) do + { success: true, + errors: { message: nil }, + destination: :link_sent, + flow_path: 'hybrid', + step: 'upload', + analytics_id: 'Doc Auth', + irs_reproofing: false, + telephony_response: { errors: {}, + message_id: 'fake-message-id', + request_id: 'fake-message-request-id', + success: true } } + end + + it 'sends analytics_submitted event for hybrid' do + put :update, params: { doc_auth: { phone: '202-555-5555' } } + + expect(@analytics).to have_logged_event(analytics_name, analytics_args) + end end - it 'sends analytics_submitted event' do - put :update, params: { doc_auth: { phone: '202-555-5555' } } + context 'desktop flow' do + let(:analytics_args) do + { success: true, + errors: {}, + destination: :document_capture, + flow_path: 'standard', + step: 'upload', + analytics_id: 'Doc Auth', + irs_reproofing: false, + skip_upload_step: false } + end + + it 'sends analytics_submitted event for desktop' do + put :update, params: { type: 'desktop' } - expect(@analytics).to have_logged_event(analytics_name, analytics_args) + expect(@analytics).to have_logged_event(analytics_name, analytics_args) + end end end end diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index abfcd6748c6..c78f25b0b28 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -87,6 +87,38 @@ def index expect(response).to redirect_to account_path end end + + context 'profile is pending from a different session' do + context 'profile is pending due to fraud review' do + before do + profile.deactivate_for_fraud_review + subject.idv_session.profile_id = nil + end + + it 'does not redirect' do + get :index + + expect(profile.user.pending_profile?).to eq true + expect(profile.fraud_review_pending_at).to_not eq nil + expect(response).to_not be_redirect + end + end + + context 'profile is pending due to in person proofing' do + before do + profile.update!(deactivation_reason: :in_person_verification_pending) + subject.idv_session.profile_id = nil + end + + it 'does not redirect' do + get :index + + expect(profile.user.pending_profile?).to eq true + expect(profile.deactivation_reason).to eq('in_person_verification_pending') + expect(response).to_not be_redirect + end + end + end end end @@ -200,12 +232,10 @@ def index subject.idv_session.create_profile_from_applicant_with_password(password) end - context 'with gpo personal key after verification' do - it 'redirects to doc auth url' do - patch :update + it 'redirects to doc auth url' do + patch :update - expect(response).to redirect_to idv_doc_auth_url - end + expect(response).to redirect_to idv_doc_auth_url end end @@ -239,39 +269,17 @@ def index context 'with device profiling decisioning enabled' do before do - ProofingComponent.create(user: user, threatmetrix: true, threatmetrix_review_status: nil) allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) end - context 'threatmetrix review status is nil' do + context 'fraud_review_pending_at is nil' do it 'redirects to account path' do patch :update + expect(profile.fraud_review_pending_at).to eq nil expect(response).to redirect_to account_path end - it 'logs key submitted event' do - patch :update - - expect(@analytics).to have_logged_event( - 'IdV: personal key submitted', - address_verification_method: nil, - fraud_review_pending: false, - fraud_rejection: false, - deactivation_reason: nil, - proofing_components: nil, - ) - end - end - context 'device profiling passes' do - before do - ProofingComponent.find_by(user: user).update(threatmetrix_review_status: 'pass') - end - it 'redirects to account path' do - patch :update - - expect(response).to redirect_to account_path - end it 'logs key submitted event' do patch :update @@ -286,39 +294,14 @@ def index end end - context 'device profiling gets sent to review' do - before do - ProofingComponent.find_by(user: user).update(threatmetrix_review_status: 'review') - profile.deactivate_for_fraud_review - end - - it 'redirects to idv please call path' do - patch :update - expect(response).to redirect_to idv_please_call_path - end - - it 'logs key submitted event' do - patch :update - - expect(@analytics).to have_logged_event( - 'IdV: personal key submitted', - fraud_review_pending: true, - fraud_rejection: false, - address_verification_method: nil, - deactivation_reason: nil, - proofing_components: nil, - ) - end - end - - context 'device profiling fails' do + context 'profile is in fraud_review' do before do - ProofingComponent.find_by(user: user).update(threatmetrix_review_status: 'reject') profile.deactivate_for_fraud_review end it 'redirects to idv please call path' do patch :update + expect(profile.fraud_review_pending_at).to_not eq nil expect(response).to redirect_to idv_please_call_path end diff --git a/spec/controllers/idv/review_controller_spec.rb b/spec/controllers/idv/review_controller_spec.rb index 8e714d7a63f..97d4714ab48 100644 --- a/spec/controllers/idv/review_controller_spec.rb +++ b/spec/controllers/idv/review_controller_spec.rb @@ -159,14 +159,31 @@ def show ) end + it 'uses the correct step indicator step' do + indicator_step = subject.step_indicator_step + + expect(indicator_step).to eq(:secure_account) + end + context 'user is in gpo flow' do - it 'does not display success message' do + before do idv_session.vendor_phone_confirmation = false idv_session.address_verification_mechanism = 'gpo' + end + it 'displays info message about sending letter' do get :new expect(flash.now[:success]).to be_nil + expect(flash.now[:info]).to eq( + t('idv.messages.review.gpo_pending'), + ) + end + + it 'uses the correct step indicator step' do + indicator_step = subject.step_indicator_step + + expect(indicator_step).to eq(:get_a_letter) end end diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb index 31ada7f5c31..1ac5cfd11fb 100644 --- a/spec/controllers/idv/ssn_controller_spec.rb +++ b/spec/controllers/idv/ssn_controller_spec.rb @@ -10,6 +10,8 @@ :flow_path => 'standard' } end + let(:ssn) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] } + let(:user) { create(:user) } before do @@ -84,6 +86,31 @@ end end + context 'with an ssn in session' do + let(:referer) { idv_document_capture_url } + before do + flow_session['pii_from_doc'][:ssn] = ssn + request.env['HTTP_REFERER'] = referer + end + + context 'referer is not verify_info' do + it 'redirects to verify_info' do + get :show + + expect(response).to redirect_to(idv_verify_info_url) + end + end + + context 'referer is verify_info' do + let(:referer) { idv_verify_info_url } + it 'does not redirect' do + get :show + + expect(response).to render_template :show + end + end + end + it 'overrides Content Security Policies for ThreatMetrix' do allow(IdentityConfig.store).to receive(:proofing_device_profiling). and_return(:enabled) @@ -108,7 +135,6 @@ describe '#update' do context 'with valid ssn' do - let(:ssn) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] } let(:params) { { doc_auth: { ssn: ssn } } } let(:analytics_name) { 'IdV: doc auth ssn submitted' } let(:analytics_args) do diff --git a/spec/controllers/sign_up/passwords_controller_spec.rb b/spec/controllers/sign_up/passwords_controller_spec.rb index bda5a03d946..8dcf444c235 100644 --- a/spec/controllers/sign_up/passwords_controller_spec.rb +++ b/spec/controllers/sign_up/passwords_controller_spec.rb @@ -10,7 +10,6 @@ password_form: { password: password, password_confirmation: password_confirmation, - confirmation_enabled: true, }, confirmation_token: token, } @@ -77,80 +76,73 @@ stub_attempts_tracker end - context 'with temporary param confirmation_enabled' do - before do - params.merge(confirmation_enabled: true) + context 'with a password that is too short' do + let(:password) { 'NewVal' } + let(:password_confirmation) { 'NewVal' } + let(:errors) do + { + password: + [t( + 'errors.attributes.password.too_short.other', + count: Devise.password_length.first, + )], + password_confirmation: + ["is too short (minimum is #{Devise.password_length.first} characters)"], + } end + let(:error_details) do + { + password: [:too_short], + password_confirmation: [:too_short], + } + end + + it 'tracks an invalid password event' do + expect(@analytics).to receive(:track_event). + with( + 'User Registration: Email Confirmation', + { errors: {}, error_details: nil, success: true, user_id: user.uuid }, + ) + expect(@analytics).to receive(:track_event). + with('Password Creation', analytics_hash) + + expect(@irs_attempts_api_tracker).to receive(:user_registration_password_submitted). + with( + success: false, + failure_reason: error_details, + ) + expect(@irs_attempts_api_tracker).not_to receive(:user_registration_email_confirmation) + + subject + end + end - # modify this test - context 'with a password that is too short' do - let(:password) { 'NewVal' } - let(:password_confirmation) { 'NewVal' } - let(:errors) do - { - password: - [t( - 'errors.attributes.password.too_short.other', - count: Devise.password_length.first, - )], - password_confirmation: - ["is too short (minimum is #{Devise.password_length.first} characters)"], - } - end - let(:error_details) do - { - password: [:too_short], - password_confirmation: [:too_short], - } - end - - it 'tracks an invalid password event' do - expect(@analytics).to receive(:track_event). - with( - 'User Registration: Email Confirmation', - { errors: {}, error_details: nil, success: true, user_id: user.uuid }, - ) - expect(@analytics).to receive(:track_event). - with('Password Creation', analytics_hash) - - expect(@irs_attempts_api_tracker).to receive(:user_registration_password_submitted). - with( - success: false, - failure_reason: error_details, - ) - expect(@irs_attempts_api_tracker).not_to receive(:user_registration_email_confirmation) - - subject - end + context 'when password confirmation does not match' do + let(:password) { 'NewVal!dPassw0rd' } + let(:password_confirmation) { 'bad match password' } + let(:errors) do + { + password_confirmation: + [t('errors.messages.password_mismatch')], + } end + let(:error_details) do + { + password_confirmation: [t('errors.messages.password_mismatch')], + } + end + + it 'tracks invalid password_confirmation error' do + expect(@analytics).to receive(:track_event). + with( + 'User Registration: Email Confirmation', + { errors: {}, error_details: nil, success: true, user_id: user.uuid }, + ) + + expect(@analytics).to receive(:track_event). + with('Password Creation', analytics_hash) - context 'when password confirmation does not match' do - let(:password) { 'NewVal!dPassw0rd' } - let(:password_confirmation) { 'bad match password' } - let(:errors) do - { - password_confirmation: - [t('errors.messages.password_mismatch')], - } - end - let(:error_details) do - { - password_confirmation: [t('errors.messages.password_mismatch')], - } - end - - it 'tracks invalid password_confirmation error' do - expect(@analytics).to receive(:track_event). - with( - 'User Registration: Email Confirmation', - { errors: {}, error_details: nil, success: true, user_id: user.uuid }, - ) - - expect(@analytics).to receive(:track_event). - with('Password Creation', analytics_hash) - - subject - end + subject end end end diff --git a/spec/controllers/users/verify_password_controller_spec.rb b/spec/controllers/users/verify_password_controller_spec.rb index 34c1db19235..55604aa76a5 100644 --- a/spec/controllers/users/verify_password_controller_spec.rb +++ b/spec/controllers/users/verify_password_controller_spec.rb @@ -91,14 +91,12 @@ end context 'without valid password' do - let(:pii) { { dob: Time.zone.today } } let(:response_bad) { FormResponse.new(success: false, errors: {}) } render_views before do allow(form).to receive(:submit).and_return(response_bad) - allow(controller).to receive(:decrypted_pii).and_return(pii) put :update, params: user_params end diff --git a/spec/features/idv/actions/redo_document_capture_action_spec.rb b/spec/features/idv/actions/redo_document_capture_action_spec.rb index bc45ad8e775..77632d1602e 100644 --- a/spec/features/idv/actions/redo_document_capture_action_spec.rb +++ b/spec/features/idv/actions/redo_document_capture_action_spec.rb @@ -32,13 +32,10 @@ DocAuth::Mock::DocAuthMockClient.reset! attach_and_submit_images - expect(current_path).to eq(idv_ssn_path) - fill_out_ssn_form_with_ssn_that_fails_resolution - click_on t('forms.buttons.submit.update') expect(current_path).to eq(idv_verify_info_path) check t('forms.ssn.show') expect(page).to have_content(DocAuthHelper::SSN_THAT_FAILS_RESOLUTION) - expect(page).not_to have_css('[role="status"]') + expect(page).to have_css('[role="status"]') # We verified your ID end it 'shows a troubleshooting option to allow the user to cancel and return to SP' do @@ -71,13 +68,10 @@ DocAuth::Mock::DocAuthMockClient.reset! attach_and_submit_images - expect(current_path).to eq(idv_ssn_path) - fill_out_ssn_form_with_ssn_that_fails_resolution - click_on t('forms.buttons.submit.update') expect(current_path).to eq(idv_verify_info_path) check t('forms.ssn.show') expect(page).to have_content(DocAuthHelper::SSN_THAT_FAILS_RESOLUTION) - expect(page).not_to have_css('[role="status"]') + expect(page).to have_css('[role="status"]') # We verified your ID end end end diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index bfc16edd4fb..87c3d502eea 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -35,7 +35,7 @@ end it 'shows the new DocumentCapture page for desktop standard flow' do - expect(page).to have_current_path(idv_document_capture_url) + expect(page).to have_current_path(idv_document_capture_path) expect(page).to have_content(t('doc_auth.headings.document_capture').tr(' ', ' ')) expect(page).to have_content(t('step_indicator.flows.idv.verify_id')) @@ -48,6 +48,12 @@ irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, ) + + # it redirects here if trying to move earlier in the flow + visit(idv_doc_auth_agreement_step) + expect(page).to have_current_path(idv_document_capture_path) + visit(idv_doc_auth_upload_step) + expect(page).to have_current_path(idv_document_capture_path) end it 'logs return to sp link click' do diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index a15d437ba52..92583e99ed4 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'doc auth upload step' do +feature 'doc auth hybrid_handoff step' do include IdvStepHelper include DocAuthHelper include ActionView::Helpers::DateHelper @@ -19,14 +19,24 @@ allow(IdentityConfig.store).to receive(:doc_auth_hybrid_handoff_controller_enabled). and_return(new_controller_enabled) allow_any_instance_of(Idv::HybridHandoffController).to receive(:mobile_device?).and_return(true) - complete_doc_auth_steps_before_upload_step allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) allow_any_instance_of(ApplicationController).to receive(:irs_attempts_api_tracker). and_return(fake_attempts_tracker) end - context 'on a desktop device', js: true do + it 'does not skip ahead in standard desktop flow' do + visit(idv_hybrid_handoff_url) + expect(page).to have_current_path(idv_doc_auth_welcome_step) + complete_welcome_step + visit(idv_hybrid_handoff_url) + expect(page).to have_current_path(idv_doc_auth_agreement_step) + complete_agreement_step + expect(page).to have_current_path(idv_hybrid_handoff_path) + end + + context 'on a desktop device' do before do + complete_doc_auth_steps_before_upload_step allow_any_instance_of( Idv::HybridHandoffController, ).to receive( @@ -54,9 +64,12 @@ 'IdV: doc auth upload submitted', hash_including(step: 'upload', destination: :document_capture), ) + + visit(idv_hybrid_handoff_url) + expect(page).to have_current_path(idv_document_capture_path) end - it "defaults phone to user's 2fa phone number" do + it "defaults phone to user's 2fa phone number", :js do field = page.find_field(t('two_factor_authentication.phone_label')) expect(field.value).to eq('(202) 555-1212') end @@ -73,9 +86,12 @@ 'IdV: doc auth upload submitted', hash_including(step: 'upload', destination: :link_sent), ) + + visit(idv_hybrid_handoff_url) + expect(page).to have_current_path(idv_link_sent_path) end - it 'proceeds to the next page with valid info' do + it 'proceeds to the next page with valid info', :js do expect(fake_attempts_tracker).to receive( :idv_phone_upload_link_sent, ).with( @@ -96,7 +112,7 @@ expect(page).to have_current_path(idv_link_sent_path) end - it 'does not proceed to the next page with invalid info' do + it 'does not proceed to the next page with invalid info', :js do fill_in :doc_auth_phone, with: '' click_send_link diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index f601cd5f19d..54d73395bdd 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -51,6 +51,10 @@ check t('forms.ssn.show') expect(page).not_to have_text(DocAuthHelper::GOOD_SSN_MASKED) expect(page).to have_text(DocAuthHelper::GOOD_SSN) + + # navigating to earlier pages returns here + visit(idv_document_capture_url) + expect(page).to have_current_path(idv_verify_info_path) end it 'allows the user to enter in a new address and displays updated info' do diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb index 1318d633099..c7ad3927831 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb @@ -45,8 +45,7 @@ # Confirm that jumping to LinkSent page does not cause errors visit idv_link_sent_url expect(page).to have_current_path(root_url) - - visit idv_hybrid_mobile_capture_complete_url + visit idv_hybrid_mobile_document_capture_url attach_and_submit_images diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index 1e370990a92..d5dbbace4d5 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -388,7 +388,7 @@ t('step_indicator.flows.idv.verify_phone_or_address'), ) click_on t('idv.buttons.mail.send') - expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) complete_review_step expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) diff --git a/spec/features/idv/steps/gpo_step_spec.rb b/spec/features/idv/steps/gpo_step_spec.rb index 882c561ff6a..81839f8a57d 100644 --- a/spec/features/idv/steps/gpo_step_spec.rb +++ b/spec/features/idv/steps/gpo_step_spec.rb @@ -36,9 +36,18 @@ it 'allows the user to resend a letter and redirects to the come back later step' do complete_idv_and_return_to_gpo_step + # Confirm that we show the correct content on + # the GPO page for users requesting re-send + expect(page).to have_content(t('idv.titles.mail.resend')) + expect(page).to have_content(t('idv.messages.gpo.resend_timeframe')) + expect(page).to have_content(t('idv.messages.gpo.resend_code_warning')) + expect(page).to have_content(t('idv.buttons.mail.resend')) + expect(page).to_not have_content(t('idv.messages.gpo.info_alert')) + expect { click_on t('idv.buttons.mail.resend') }. to change { GpoConfirmation.count }.from(1).to(2) expect_user_to_be_unverified(user) + expect(page).to have_content(t('idv.titles.come_back_later')) expect(page).to have_current_path(idv_come_back_later_path) diff --git a/spec/features/idv/steps/review_step_spec.rb b/spec/features/idv/steps/review_step_spec.rb index 0a5f9b5eacb..f781d55951a 100644 --- a/spec/features/idv/steps/review_step_spec.rb +++ b/spec/features/idv/steps/review_step_spec.rb @@ -13,15 +13,7 @@ start_idv_from_sp complete_idv_steps_before_review_step - click_on t('idv.messages.review.intro') - - expect(page).to have_content('FAKEY') - expect(page).to have_content('MCFAKERSON') - expect(page).to have_content('1 FAKE RD') - expect(page).to have_content('GREAT FALLS, MT 59010') - expect(page).to have_content('October 06, 1938') - expect(page).to have_content(DocAuthHelper::GOOD_SSN) - expect(page).to have_content('+1 202-555-1212') + expect(page).to have_content(t('idv.messages.review.message', app_name: APP_NAME)) fill_in 'Password', with: 'this is not the right password' click_idv_continue diff --git a/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb b/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb index e7dee55b7aa..912037e2dfe 100644 --- a/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb @@ -9,9 +9,6 @@ raw_key = PersonalKeyGenerator.new(user).create old_key = user.reload.encrypted_recovery_code_digest - expect(Telephony).to receive(:send_personal_key_sign_in_notice). - with(to: '+1 (202) 345-6789', country_code: 'US') - sign_in_before_2fa(user) choose_another_security_option('personal_key') enter_personal_key(personal_key: raw_key) @@ -19,6 +16,10 @@ expect(user.reload.encrypted_recovery_code_digest).to_not eq old_key expect(current_path).to eq account_path + last_message = Telephony::Test::Message.messages.last + expect(last_message.body).to eq t('telephony.personal_key_sign_in_notice', app_name: APP_NAME) + expect(last_message.to).to eq user.phone_configurations.take.phone + expect_delivered_email_count(1) expect_delivered_email( to: [user.email_addresses.first.email], diff --git a/spec/forms/idv/in_person/address_form_spec.rb b/spec/forms/idv/in_person/address_form_spec.rb new file mode 100644 index 00000000000..aef7424b8d7 --- /dev/null +++ b/spec/forms/idv/in_person/address_form_spec.rb @@ -0,0 +1,103 @@ +require 'rails_helper' + +describe Idv::InPerson::AddressForm do + let(:pii) { Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS } + context 'test validation for transliteration after form submission' do + let(:good_params) do + { + address1: Faker::Address.street_address, + address2: Faker::Address.secondary_address, + zipcode: Faker::Address.zip_code, + state: Faker::Address.state_abbr, + city: Faker::Address.city, + } + end + let(:invalid_char) { '$' } + let(:bad_params) do + { + address1: invalid_char + Faker::Address.street_address, + address2: invalid_char + Faker::Address.secondary_address + invalid_char, + zipcode: Faker::Address.zip_code, + state: Faker::Address.state_abbr, + city: invalid_char + Faker::Address.city, + } + end + context 'when usps_ipp_transliteration_enabled is false' do + let(:subject) { described_class.new(capture_secondary_id_enabled: true) } + before(:each) do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(false) + end + it 'submit success with good params' do + good_params[:same_address_as_id] = true + result = subject.submit(good_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + + it 'submit success with good params' do + good_params[:same_address_as_id] = false + result = subject.submit(good_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + + it 'submit success with bad params' do + bad_params[:same_address_as_id] = false + result = subject.submit(bad_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + end + context 'when usps_ipp_transliteration_enabled is enabled ' do + let(:subject) { described_class.new(capture_secondary_id_enabled: true) } + before(:each) do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(true) + end + it 'submit success with good params' do + good_params[:same_address_as_id] = true + result = subject.submit(good_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + it 'submit failure with bad params' do + bad_params[:same_address_as_id] = false + result = subject.submit(bad_params) + expect(subject.errors.empty?).to be(false) + expect(subject.errors.to_hash).to include(:address1, :address2, :city) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(false) + expect(result.errors.empty?).to be(true) + end + end + context 'when usps_ipp_transliteration_enabled is enabled and validate on other field' do + before(:each) do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(true) + end + context 'when capture_secondary_id_enabled is true' do + let(:subject) { described_class.new(capture_secondary_id_enabled: true) } + it 'submit with missing same_address_as_id should be successful' do + missing_required_params = good_params.except(:same_address_as_id) + result = subject.submit(missing_required_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + end + context 'when capture_secondary_id_enabled is false' do + let(:subject) { described_class.new(capture_secondary_id_enabled: false) } + it 'submit with missing same_address_as_id will fail' do + missing_required_params = good_params.except(:same_address_as_id) + result = subject.submit(missing_required_params) + expect(subject.errors.empty?).to be(false) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(false) + expect(result.errors.keys).to include(:same_address_as_id) + end + end + end + end +end diff --git a/spec/forms/idv/state_id_form_spec.rb b/spec/forms/idv/state_id_form_spec.rb new file mode 100644 index 00000000000..30b8142ca27 --- /dev/null +++ b/spec/forms/idv/state_id_form_spec.rb @@ -0,0 +1,108 @@ +require 'rails_helper' + +describe Idv::StateIdForm do + let(:subject) { Idv::StateIdForm.new(pii) } + let(:valid_dob) do + valid_d = Time.zone.today - IdentityConfig.store.idv_min_age_years.years - 1.day + ActionController::Parameters.new( + { + year: valid_d.year, + month: valid_d.month, + day: valid_d.mday, + }, + ).permit(:year, :month, :day) + end + let(:too_young_dob) do + (Time.zone.today - IdentityConfig.store.idv_min_age_years.years + 1.day).to_s + end + let(:good_params) do + { + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: valid_dob, + identity_doc_address1: Faker::Address.street_address, + identity_doc_address2: Faker::Address.secondary_address, + identity_doc_zipcode: Faker::Address.zip_code, + identity_doc_address_state: Faker::Address.state_abbr, + same_address_as_id: 'true', + state_id_jurisdiction: 'AL', + state_id_number: Faker::IDNumber.valid, + } + end + let(:dob_min_age_name_error_params) do + { + first_name: Faker::Name.first_name + invalid_char, + last_name: Faker::Name.last_name, + dob: too_young_dob, + identity_doc_address1: Faker::Address.street_address, + identity_doc_address2: Faker::Address.secondary_address, + identity_doc_zipcode: Faker::Address.zip_code, + identity_doc_address_state: Faker::Address.state_abbr, + same_address_as_id: 'true', + state_id_jurisdiction: 'AL', + state_id_number: Faker::IDNumber.valid, + } + end + let(:invalid_char) { '1' } + let(:name_error_params) do + { + first_name: Faker::Name.first_name + invalid_char, + last_name: Faker::Name.last_name, + dob: valid_dob, + identity_doc_address1: Faker::Address.street_address, + identity_doc_address2: Faker::Address.secondary_address, + identity_doc_zipcode: Faker::Address.zip_code, + identity_doc_address_state: Faker::Address.state_abbr, + same_address_as_id: 'true', + state_id_jurisdiction: 'AL', + state_id_number: Faker::IDNumber.valid, + } + end + let(:pii) { nil } + describe '#submit' do + context 'when the form is valid' do + it 'returns a successful form response' do + result = subject.submit(good_params) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end + end + + context 'when there is an error with name' do + it 'returns a single name error when name is wrong' do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(true) + result = subject.submit(name_error_params) + expect(subject.errors.empty?).to be(false) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(subject.errors[:first_name]).to eq [ + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: [invalid_char].join(', '), + ), + ] + expect(result.errors.empty?).to be(true) + end + it 'returns both name and dob error when both fields are invalid' do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(true) + result = subject.submit(dob_min_age_name_error_params) + expect(subject.errors.empty?).to be(false) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(subject.errors[:first_name]).to eq [ + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: [invalid_char].join(', '), + ), + ] + expect(subject.errors[:dob]).to eq [ + I18n.t( + 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age', + app_name: APP_NAME, + ), + ] + end + end + end +end diff --git a/spec/forms/password_form_spec.rb b/spec/forms/password_form_spec.rb index ff96adccd19..b1ec0abe7a6 100644 --- a/spec/forms/password_form_spec.rb +++ b/spec/forms/password_form_spec.rb @@ -21,7 +21,6 @@ { password: password, password_confirmation: password_confirmation, - confirmation_enabled: true, } end diff --git a/spec/javascript/packages/document-capture-polling/index-spec.js b/spec/javascript/packages/document-capture-polling/index-spec.js index 598480cf85b..8cd21f90e78 100644 --- a/spec/javascript/packages/document-capture-polling/index-spec.js +++ b/spec/javascript/packages/document-capture-polling/index-spec.js @@ -44,6 +44,10 @@ describe('DocumentCapturePolling', () => { subject.bind(); }); + afterEach(() => { + subject.bindPromptOnNavigate(false); + }); + it('hides form', () => { expect(screen.getByText('Submit').closest('.display-none')).to.be.ok(); }); diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index aa24d69faf5..9e2b2779de3 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -43,6 +43,7 @@ status: response['status'], transaction_end_date_time: anything, transaction_start_date_time: anything, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -58,6 +59,7 @@ wait_until: anything, service_provider: pending_enrollment.issuer, timestamp: Time.zone.now, + job_name: 'GetUspsProofingResultsJob', ) else expect(job_analytics).to have_logged_event( @@ -67,6 +69,7 @@ wait_until: anything, service_provider: pending_enrollment.issuer, timestamp: Time.zone.now, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -111,6 +114,7 @@ reason: reason, response_message: response_message, response_status_code: response_status_code, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -155,6 +159,7 @@ reason: 'Request exception', response_present: false, exception_class: error_type.to_s, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -317,6 +322,7 @@ 'GetUspsProofingResultsJob: Job started', enrollments_count: 5, reprocess_delay_minutes: 2.0, + job_name: 'GetUspsProofingResultsJob', ) end @@ -342,7 +348,8 @@ enrollments_failed: 1, enrollments_in_progress: 1, enrollments_passed: 1, - percent_enrollments_errored: 20, + percent_enrollments_errored: 20.00, + job_name: 'GetUspsProofingResultsJob', ) expect( @@ -369,7 +376,8 @@ enrollments_failed: 0, enrollments_in_progress: 0, enrollments_passed: 5, - percent_enrollments_errored: 0, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsProofingResultsJob', ) expect( @@ -397,7 +405,8 @@ enrollments_failed: 0, enrollments_in_progress: 0, enrollments_passed: 0, - percent_enrollments_errored: 0, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsProofingResultsJob', ) expect( @@ -424,6 +433,7 @@ exception_message: error_message, exception_class: 'StandardError', reason: 'Request exception', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -492,6 +502,7 @@ service_provider: pending_enrollment.issuer, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -560,6 +571,7 @@ service_provider: anything, timestamp: anything, wait_until: wait_until, + job_name: 'GetUspsProofingResultsJob', ) end @@ -585,6 +597,7 @@ service_provider: anything, timestamp: anything, wait_until: wait_until, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -632,6 +645,7 @@ hash_including( reason: 'Successful status update', passed: true, + job_name: 'GetUspsProofingResultsJob', ), ) expect(job_analytics).to have_logged_event( @@ -641,6 +655,7 @@ service_provider: anything, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -668,6 +683,7 @@ hash_including( passed: false, reason: 'Failed status', + job_name: 'GetUspsProofingResultsJob', ), ) expect(job_analytics).to have_logged_event( @@ -677,6 +693,7 @@ service_provider: anything, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -705,6 +722,7 @@ fraud_suspected: true, passed: false, reason: 'Failed status', + job_name: 'GetUspsProofingResultsJob', ), ) expect(job_analytics).to have_logged_event( @@ -714,6 +732,7 @@ service_provider: anything, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -741,6 +760,7 @@ hash_including( passed: false, reason: 'Unsupported ID type', + job_name: 'GetUspsProofingResultsJob', ), ) @@ -751,6 +771,7 @@ service_provider: anything, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -779,6 +800,7 @@ reason: 'Enrollment has expired', transaction_end_date_time: nil, transaction_start_date_time: nil, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -799,6 +821,7 @@ 'GetUspsProofingResultsJob: Enrollment incomplete', hash_including( response_message: 'More than 30 days have passed since opt-in to IPP', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -830,12 +853,16 @@ hash_including( passed: false, reason: 'Enrollment has expired', + job_name: 'GetUspsProofingResultsJob', ), ) expect(job_analytics).to have_logged_event( 'GetUspsProofingResultsJob: Unexpected response received', - hash_including(reason: 'Unexpected number of days before enrollment expired'), + hash_including( + reason: 'Unexpected number of days before enrollment expired', + ), + job_name: 'GetUspsProofingResultsJob', ) end end @@ -860,6 +887,7 @@ hash_including( reason: 'Invalid enrollment code', response_message: /Enrollment code [0-9]{16} does not exist/, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -884,6 +912,7 @@ hash_including( reason: 'Invalid applicant unique id', response_message: /Applicant [0-9a-z]{18} does not exist/, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -920,6 +949,7 @@ 'GetUspsProofingResultsJob: Exception raised', hash_including( status: 'Not supported', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -1041,6 +1071,7 @@ 'GetUspsProofingResultsJob: Enrollment incomplete', hash_including( response_message: 'Customer has not been to a post office to complete IPP', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -1106,6 +1137,7 @@ hash_including( passed: false, reason: 'Provided secondary proof of address', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -1125,5 +1157,20 @@ job.perform Time.zone.now end end + + describe 'IPP Enrollments Ready Job Enabled' do + before do + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end end end diff --git a/spec/jobs/get_usps_ready_proofing_results_job_spec.rb b/spec/jobs/get_usps_ready_proofing_results_job_spec.rb new file mode 100644 index 00000000000..e73f034c826 --- /dev/null +++ b/spec/jobs/get_usps_ready_proofing_results_job_spec.rb @@ -0,0 +1,287 @@ +require 'rails_helper' + +RSpec.describe GetUspsReadyProofingResultsJob do + include UspsIppHelper + include ApproximatingHelper + + let(:reprocess_delay_minutes) { 2.0 } + let(:request_delay_ms) { 0 } + let(:job) { GetUspsReadyProofingResultsJob.new } + let(:job_analytics) { FakeAnalytics.new } + let(:transaction_start_date_time) do + ActiveSupport::TimeZone[-6].strptime( + '12/17/2020 033855', + '%m/%d/%Y %H%M%S', + ).in_time_zone('UTC') + end + let(:transaction_end_date_time) do + ActiveSupport::TimeZone[-6].strptime( + '12/17/2020 034055', + '%m/%d/%Y %H%M%S', + ).in_time_zone('UTC') + end + + before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) + ActiveJob::Base.queue_adapter = :test + allow(job).to receive(:analytics).and_return(job_analytics) + allow(IdentityConfig.store).to receive(:get_usps_proofing_results_job_reprocess_delay_minutes). + and_return(reprocess_delay_minutes) + stub_const( + 'GetUspsProofingResultsJob::REQUEST_DELAY_IN_SECONDS', + request_delay_ms / GetUspsProofingResultsJob::MILLISECONDS_PER_SECOND, + ) + stub_request_token + if respond_to?(:pending_enrollment) + pending_enrollment.update(enrollment_established_at: 3.days.ago) + end + end + + describe '#perform' do + describe 'IPP enabled' do + describe 'Ready Job enabled' do + let!(:pending_enrollments) do + [ + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'BALTIMORE' }, + issuer: 'http://localhost:3000', + ready_for_status_check: true + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'FRIENDSHIP' }, + ready_for_status_check: true + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'WASHINGTON' }, + ready_for_status_check: true + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'ARLINGTON' }, + ready_for_status_check: true + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'DEANWOOD' }, + ready_for_status_check: true + ), + ] + end + let(:pending_enrollment) { pending_enrollments[0] } + + before do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return([pending_enrollment]) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + end + + it 'requests the enrollments that need their status checked' do + stub_request_passed_proofing_results + + freeze_time do + job.perform(Time.zone.now) + + expect(InPersonEnrollment).to( + have_received(:needs_status_check_on_ready_enrollments). + with(...reprocess_delay_minutes.minutes.ago), + ) + end + end + + it 'records the last attempted status check regardless of response code and contents' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_failed_proofing_results_args, + request_in_progress_proofing_results_args, + request_in_progress_proofing_results_args, + request_failed_proofing_results_args, + ) + + expect(pending_enrollments.pluck(:status_check_attempted_at)).to( + all(eq nil), + 'failed test precondition: + pending enrollments must not set status check attempted time', + ) + + expect(pending_enrollments.pluck(:status_check_completed_at)).to( + all(eq nil), + 'failed test precondition: + pending enrollments must not set status check completed time', + ) + + freeze_time do + job.perform(Time.zone.now) + + expect( + pending_enrollments. + map(&:reload). + pluck(:status_check_attempted_at), + ).to( + all(eq Time.zone.now), + 'job must update status check attempted time for all pending enrollments', + ) + + expect( + pending_enrollments. + map(&:reload). + pluck(:status_check_completed_at), + ).to( + all(eq Time.zone.now), + 'job must update status check completed time for all pending enrollments', + ) + end + end + + it 'logs a message when the job starts' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_failed_proofing_results_args, + request_in_progress_proofing_results_args, + request_in_progress_proofing_results_args, + request_failed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job started', + enrollments_count: 5, + reprocess_delay_minutes: 2.0, + job_name: 'GetUspsReadyProofingResultsJob', + ) + end + + it 'logs a message with counts of various outcomes when the job completes (errored > 0)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + request_in_progress_proofing_results_args, + { status: 500 }, + request_failed_proofing_results_args, + request_expired_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 5, + enrollments_errored: 1, + enrollments_expired: 1, + enrollments_failed: 1, + enrollments_in_progress: 1, + enrollments_passed: 1, + percent_enrollments_errored: 20.00, + job_name: 'GetUspsReadyProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + + it 'logs a message with counts of various outcomes when the job completes (errored = 0)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 5, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 5, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsReadyProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + + it 'logs a message with counts of various outcomes when the job completes + (no enrollments)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return([]) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 0, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 0, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsReadyProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + end + end + + describe 'IPP disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(false) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end + + describe 'Ready Job disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(false), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end + end +end diff --git a/spec/jobs/get_usps_waiting_proofing_results_job_spec.rb b/spec/jobs/get_usps_waiting_proofing_results_job_spec.rb new file mode 100644 index 00000000000..2f21550a9cb --- /dev/null +++ b/spec/jobs/get_usps_waiting_proofing_results_job_spec.rb @@ -0,0 +1,287 @@ +require 'rails_helper' + +RSpec.describe GetUspsWaitingProofingResultsJob do + include UspsIppHelper + include ApproximatingHelper + + let(:reprocess_delay_minutes) { 2.0 } + let(:request_delay_ms) { 0 } + let(:job) { GetUspsWaitingProofingResultsJob.new } + let(:job_analytics) { FakeAnalytics.new } + let(:transaction_start_date_time) do + ActiveSupport::TimeZone[-6].strptime( + '12/17/2020 033855', + '%m/%d/%Y %H%M%S', + ).in_time_zone('UTC') + end + let(:transaction_end_date_time) do + ActiveSupport::TimeZone[-6].strptime( + '12/17/2020 034055', + '%m/%d/%Y %H%M%S', + ).in_time_zone('UTC') + end + + before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) + ActiveJob::Base.queue_adapter = :test + allow(job).to receive(:analytics).and_return(job_analytics) + allow(IdentityConfig.store).to receive(:get_usps_proofing_results_job_reprocess_delay_minutes). + and_return(reprocess_delay_minutes) + stub_const( + 'GetUspsProofingResultsJob::REQUEST_DELAY_IN_SECONDS', + request_delay_ms / GetUspsProofingResultsJob::MILLISECONDS_PER_SECOND, + ) + stub_request_token + if respond_to?(:pending_enrollment) + pending_enrollment.update(enrollment_established_at: 3.days.ago) + end + end + + describe '#perform' do + describe 'IPP enabled' do + describe 'Ready Job enabled' do + let!(:pending_enrollments) do + [ + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'BALTIMORE' }, + issuer: 'http://localhost:3000', + ready_for_status_check: false + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'FRIENDSHIP' }, + ready_for_status_check: false + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'WASHINGTON' }, + ready_for_status_check: false + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'ARLINGTON' }, + ready_for_status_check: false + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'DEANWOOD' }, + ready_for_status_check: false + ), + ] + end + let(:pending_enrollment) { pending_enrollments[0] } + + before do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return([pending_enrollment]) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + end + + it 'requests the enrollments that need their status checked' do + stub_request_passed_proofing_results + + freeze_time do + job.perform(Time.zone.now) + + expect(InPersonEnrollment).to( + have_received(:needs_status_check_on_waiting_enrollments). + with(...reprocess_delay_minutes.minutes.ago), + ) + end + end + + it 'records the last attempted status check regardless of response code and contents' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_failed_proofing_results_args, + request_in_progress_proofing_results_args, + request_in_progress_proofing_results_args, + request_failed_proofing_results_args, + ) + + expect(pending_enrollments.pluck(:status_check_attempted_at)).to( + all(eq nil), + 'failed test precondition: + pending enrollments must not set status check attempted time', + ) + + expect(pending_enrollments.pluck(:status_check_completed_at)).to( + all(eq nil), + 'failed test precondition: + pending enrollments must not set status check completed time', + ) + + freeze_time do + job.perform(Time.zone.now) + + expect( + pending_enrollments. + map(&:reload). + pluck(:status_check_attempted_at), + ).to( + all(eq Time.zone.now), + 'job must update status check attempted time for all pending enrollments', + ) + + expect( + pending_enrollments. + map(&:reload). + pluck(:status_check_completed_at), + ).to( + all(eq Time.zone.now), + 'job must update status check completed time for all pending enrollments', + ) + end + end + + it 'logs a message when the job starts' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_failed_proofing_results_args, + request_in_progress_proofing_results_args, + request_in_progress_proofing_results_args, + request_failed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job started', + enrollments_count: 5, + reprocess_delay_minutes: 2.0, + job_name: 'GetUspsWaitingProofingResultsJob', + ) + end + + it 'logs a message with counts of various outcomes when the job completes (errored > 0)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + request_in_progress_proofing_results_args, + { status: 500 }, + request_failed_proofing_results_args, + request_expired_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 5, + enrollments_errored: 1, + enrollments_expired: 1, + enrollments_failed: 1, + enrollments_in_progress: 1, + enrollments_passed: 1, + percent_enrollments_errored: 20.00, + job_name: 'GetUspsWaitingProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + + it 'logs a message with counts of various outcomes when the job completes (errored = 0)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 5, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 5, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsWaitingProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + + it 'logs a message with counts of various outcomes when the job completes + (no enrollments)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return([]) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 0, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 0, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsWaitingProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + end + end + + describe 'IPP disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(false) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end + + describe 'Ready Job disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(false), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end + end +end diff --git a/spec/lib/telephony/alert_sender_spec.rb b/spec/lib/telephony/alert_sender_spec.rb index 1484886511b..a206a54aeff 100644 --- a/spec/lib/telephony/alert_sender_spec.rb +++ b/spec/lib/telephony/alert_sender_spec.rb @@ -117,7 +117,9 @@ last_message = Telephony::Test::Message.messages.last expect(last_message.to).to eq(recipient) - expect(last_message.body).to eq(I18n.t('telephony.personal_key_sign_in_notice')) + expect(last_message.body).to eq( + t('telephony.personal_key_sign_in_notice', app_name: APP_NAME), + ) end end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index e451630f1c5..8096d825a83 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -15,8 +15,11 @@ it 'has a translation for every event type' do missing_translations = Event.event_types.keys.select do |event_type| - translation = I18n.t("event_types.#{event_type}", raise: true) - translation.empty? + I18n.t( + "event_types.#{event_type}", + raise: true, + ignore_test_helper_missing_interpolation: true, + ).empty? rescue I18n::MissingTranslationData true end diff --git a/spec/models/in_person_enrollment_spec.rb b/spec/models/in_person_enrollment_spec.rb index 2098b5662b7..02e8da27762 100644 --- a/spec/models/in_person_enrollment_spec.rb +++ b/spec/models/in_person_enrollment_spec.rb @@ -180,6 +180,115 @@ end end + describe 'status checks for ready and waiting enrollments' do + let(:check_interval) { ...1.hour.ago } + let!(:passed_enrollment) do + create(:in_person_enrollment, :passed, ready_for_status_check: true) + end + let!(:failing_enrollment) do + create(:in_person_enrollment, :failed, ready_for_status_check: true) + end + let!(:expired_enrollment) do + create(:in_person_enrollment, :expired, ready_for_status_check: true) + end + let!(:checked_pending_enrollment) do + create( + :in_person_enrollment, :pending, status_check_attempted_at: Time.zone.now, + ready_for_status_check: true + ) + end + let!(:ready_enrollments) do + [ + create(:in_person_enrollment, :pending, ready_for_status_check: true), + create(:in_person_enrollment, :pending, ready_for_status_check: true), + create(:in_person_enrollment, :pending, ready_for_status_check: true), + create(:in_person_enrollment, :pending, ready_for_status_check: true), + ] + end + let!(:needy_enrollments) do + [ + create(:in_person_enrollment, :pending, ready_for_status_check: false), + create(:in_person_enrollment, :pending, ready_for_status_check: false), + create(:in_person_enrollment, :pending, ready_for_status_check: false), + create(:in_person_enrollment, :pending, ready_for_status_check: false), + ] + end + + it 'returns only pending enrollments that are ready for status check' do + expect(InPersonEnrollment.count).to eq(12) + ready_results = InPersonEnrollment.needs_status_check_on_ready_enrollments(check_interval) + expect(ready_results.length).to eq ready_enrollments.length + expect(ready_results.pluck(:id)).to match_array ready_enrollments.pluck(:id) + expect(ready_results.pluck(:id)).not_to match_array needy_enrollments.pluck(:id) + ready_results.each do |result| + expect(result.pending?).to be_truthy + end + end + + it 'indicates whether a ready enrollment needs a status check' do + expect(passed_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + expect(failing_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + expect(expired_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + expect(checked_pending_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + needy_enrollments.each do |enrollment| + expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + end + ready_enrollments.each do |enrollment| + expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(true), + ) + end + end + + it 'returns only pending enrollments that are not ready for status check' do + expect(InPersonEnrollment.count).to eq(12) + waiting_results = InPersonEnrollment.needs_status_check_on_waiting_enrollments(check_interval) + expect(waiting_results.length).to eq needy_enrollments.length + expect(waiting_results.pluck(:id)).to match_array needy_enrollments.pluck(:id) + expect(waiting_results.pluck(:id)).not_to match_array ready_enrollments.pluck(:id) + waiting_results.each do |result| + expect(result.pending?).to be_truthy + end + end + + it 'indicates whether a waiting enrollment needs a status check' do + expect(passed_enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to( + be(false), + ) + expect( + failing_enrollment.needs_status_check_on_waiting_enrollment?(check_interval), + ).to( + be(false), + ) + expect( + expired_enrollment.needs_status_check_on_waiting_enrollment?(check_interval), + ).to( + be(false), + ) + expect( + checked_pending_enrollment.needs_status_check_on_waiting_enrollment?(check_interval), + ).to( + be(false), + ) + needy_enrollments.each do |enrollment| + expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to be(true) + end + ready_enrollments.each do |enrollment| + expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to be(false) + end + end + end + describe 'minutes_since_established' do let(:enrollment) do create( diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 39a1d7b51c6..17af2a7c199 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -319,6 +319,48 @@ end end + describe '#activate_after_password_reset' do + it 'activates a profile after password reset' do + profile = create( + :profile, + user: user, + active: false, + deactivation_reason: :password_reset, + ) + + profile.activate_after_password_reset + + expect(profile.active).to eq true + expect(profile.deactivation_reason).to eq nil + end + + it 'does not activate a profile if it has a pending reason' do + profile = create( + :profile, + user: user, + active: false, + deactivation_reason: :password_reset, + fraud_review_pending_at: 1.day.ago, + ) + + expect { profile.activate_after_password_reset }.to raise_error + end + + it 'does not activate a profile with non password_reset deactivation_reason' do + profile = create( + :profile, + user: user, + active: false, + deactivation_reason: :encryption_error, + ) + + profile.activate_after_password_reset + + expect(profile.active).to eq false + expect(profile.deactivation_reason).to_not eq nil + end + end + describe '#activate_after_passing_review' do it 'activates a profile if it passes fraud review' do profile = create( diff --git a/spec/support/i18n_helper.rb b/spec/support/i18n_helper.rb new file mode 100644 index 00000000000..139019a3084 --- /dev/null +++ b/spec/support/i18n_helper.rb @@ -0,0 +1,13 @@ +module I18n + class << self + prepend( + Module.new do + def t(*args, ignore_test_helper_missing_interpolation: false, **kwargs) + result = super(*args, **kwargs) + return result if ignore_test_helper_missing_interpolation || !result.include?('%{') + raise "Missing interpolation in translated string: #{result}" + end + end, + ) + end +end diff --git a/spec/views/idv/review/new.html.erb_spec.rb b/spec/views/idv/review/new.html.erb_spec.rb index 53e7c6b7892..aee5227aafb 100644 --- a/spec/views/idv/review/new.html.erb_spec.rb +++ b/spec/views/idv/review/new.html.erb_spec.rb @@ -11,58 +11,20 @@ allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:step_indicator_steps). and_return(Idv::Flows::DocAuthFlow::STEP_INDICATOR_STEPS) - @applicant = { - first_name: 'Some', - last_name: 'One', - ssn: '666-66-1234', - dob: dob, - address1: '123 Main St', - city: 'Somewhere', - state: 'MO', - zipcode: '12345', - phone: '+1 (213) 555-0000', - } + allow(view).to receive(:step_indicator_step).and_return(:secure_account) render end - it 'renders all steps' do - expect(rendered).to have_content('Some One') - expect(rendered).to have_content('123 Main St') - expect(rendered).to have_content('Somewhere') - expect(rendered).to have_content('MO') - expect(rendered).to have_content('12345') - expect(rendered).to have_content('666-66-1234') - expect(rendered).to have_content('+1 213-555-0000') - expect(rendered).to have_content('March 29, 1972') - end - it 'renders the correct content heading' do expect(rendered).to have_content t('idv.titles.session.review', app_name: APP_NAME) end - it 'contains an accordion with verified user information' do - accordion_selector = generate_class_selector('usa-accordion') - expect(rendered).to have_xpath("//#{accordion_selector}") - end - - it 'renders the correct header for the accordion' do - expect(rendered).to have_content(t('idv.messages.review.intro')) - end - it 'shows the step indicator' do expect(view.content_for(:pre_flash_content)).to have_css( '.step-indicator__step--current', text: t('step_indicator.flows.idv.secure_account'), ) end - - context 'with an american-style dob' do - let(:dob) { '12/31/1970' } - - it 'renders correctly' do - expect(rendered).to have_selector('.h4.text-bold', text: 'December 31, 1970') - end - end end end diff --git a/spec/views/phone_setup/spam_protection.html.erb_spec.rb b/spec/views/phone_setup/spam_protection.html.erb_spec.rb index f79897e0e1b..8d5ef4a66c1 100644 --- a/spec/views/phone_setup/spam_protection.html.erb_spec.rb +++ b/spec/views/phone_setup/spam_protection.html.erb_spec.rb @@ -43,6 +43,31 @@ href: two_factor_options_path, ) end + + it 'does not render cancel option' do + expect(rendered).to_not have_link( + t('links.cancel'), + href: account_path, + ) + end + end + + context 'fully registered user adding new phone' do + let(:user) { create(:user, :fully_registered) } + + it 'does not render additional troubleshooting option to two factor options' do + expect(rendered).to_not have_link( + t('two_factor_authentication.login_options_link_text'), + href: two_factor_options_path, + ) + end + + it 'renders cancel option' do + expect(rendered).to have_link( + t('links.cancel'), + href: account_path, + ) + end end context 'with configured recaptcha site key' do