diff --git a/Gemfile.lock b/Gemfile.lock index 246677ba18a..de081646de0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -385,7 +385,8 @@ GEM jsbundling-rails (1.1.2) railties (>= 6.0.0) json (2.13.2) - jwe (0.4.0) + jwe (1.1.1) + base64 jwt (2.7.1) knapsack (4.0.0) rake diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7f80f621e36..578d0bf95eb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -538,10 +538,15 @@ def find_device_profiling_result(type) ).last end + def user_in_one_account_verification_bucket? + ab_test_bucket(:ONE_ACCOUNT_USER_VERIFICATION_ENABLED) == :one_account_user_verification_enabled + end + def user_duplicate_profiles_detected? return false unless sp_eligible_for_one_account? profile = current_user&.active_profile return false unless profile + return false unless user_in_one_account_verification_bucket? user_session[:duplicate_profile_ids].present? end diff --git a/app/controllers/idv/phone_errors_controller.rb b/app/controllers/idv/phone_errors_controller.rb index 770be4cbe47..29332d569e3 100644 --- a/app/controllers/idv/phone_errors_controller.rb +++ b/app/controllers/idv/phone_errors_controller.rb @@ -7,6 +7,7 @@ class PhoneErrorsController < ApplicationController include StepIndicatorConcern include Idv::AbTestAnalyticsConcern include Idv::VerifyByMailConcern + include PhoneFormatter before_action :confirm_step_allowed, except: [:failure] before_action :set_gpo_letter_available @@ -20,6 +21,7 @@ def warning @country_code = idv_session.previous_phone_step_params[:international_code] end + @formatted_phone = PhoneFormatter.format(@phone, country_code: @country_code) track_event(type: :warning) end diff --git a/app/jobs/reports/irs_authentication_report.rb b/app/jobs/reports/irs_registration_funnel_report.rb similarity index 78% rename from app/jobs/reports/irs_authentication_report.rb rename to app/jobs/reports/irs_registration_funnel_report.rb index 10cffb00637..1c604594805 100644 --- a/app/jobs/reports/irs_authentication_report.rb +++ b/app/jobs/reports/irs_registration_funnel_report.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true require 'csv' -require 'reporting/irs_authentication_report' +require 'reporting/irs_registration_funnel_report' module Reports - class IrsAuthenticationReport < BaseReport - REPORT_NAME = 'irs-authentication-report' + class IrsRegistrationFunnelReport < BaseReport + REPORT_NAME = 'irs-registration-funnel-report' attr_reader :report_date @@ -19,7 +19,7 @@ def perform(date = Time.zone.yesterday.end_of_day) email_addresses = emails.select(&:present?) if email_addresses.empty? - Rails.logger.warn 'No email addresses received - Authentication Report NOT SENT' + Rails.logger.warn 'No email addresses received - Registration Funnel Report NOT SENT' return false end @@ -29,7 +29,7 @@ def perform(date = Time.zone.yesterday.end_of_day) ReportMailer.tables_report( email: email_addresses, - subject: "IRS Authentication Report - #{report_date.to_date}", + subject: "IRS Registration Funnel Report - #{report_date.to_date}", reports: reports, message: preamble, attachment_format: :csv, @@ -60,22 +60,22 @@ def preamble(env: Identity::Hostdata.env || 'local') end def reports - @reports ||= irs_authentication_report.as_emailable_reports + @reports ||= irs_registration_funnel_report.as_emailable_reports end - def irs_authentication_report - @irs_authentication_report ||= Reporting::IrsAuthenticationReport.new( + def irs_registration_funnel_report + @irs_registration_funnel_report ||= Reporting::IrsRegistrationFunnelReport.new( issuers: issuers, time_range: report_date.all_week, ) end def issuers - [*IdentityConfig.store.irs_authentication_issuers] + [*IdentityConfig.store.irs_registration_funnel_issuers] end def emails - [*IdentityConfig.store.irs_authentication_emails] + [*IdentityConfig.store.irs_registration_funnel_emails] end def upload_to_s3(report_body, report_name: nil) diff --git a/app/services/marketing_site.rb b/app/services/marketing_site.rb index b44d1b17cbb..ec705a00249 100644 --- a/app/services/marketing_site.rb +++ b/app/services/marketing_site.rb @@ -10,6 +10,7 @@ class UnknownArticleException < StandardError; end manage-your-account/add-or-change-your-authentication-method manage-your-account/delete-your-account manage-your-account/personal-key + manage-your-account/resolve-duplicate-accounts trouble-signing-in/face-or-touch-unlock trouble-signing-in/forgot-your-password trouble-signing-in/forgot-your-personal-key diff --git a/app/views/duplicate_profiles_detected/show.html.erb b/app/views/duplicate_profiles_detected/show.html.erb index ec486c7434c..24350e9bae6 100644 --- a/app/views/duplicate_profiles_detected/show.html.erb +++ b/app/views/duplicate_profiles_detected/show.html.erb @@ -3,7 +3,19 @@ <%= render StatusPageComponent.new(status: :warning) do |c| %> <% c.with_header { @dupe_profiles_detected_presenter.heading } %> -

<%= t('duplicate_profiles_detected.intro', app_name: APP_NAME) %>

+

+ <%= t( + 'duplicate_profiles_detected.intro_html', + link_html: new_tab_link_to( + t('duplicate_profiles_detected.intro.link'), + MarketingSite.help_center_article_url( + category: 'manage-your-account', + article: 'resolve-duplicate-accounts', + ), + ), + app_name: APP_NAME, + ) %> +

<%= t('duplicate_profiles_detected.intro2', app_name: APP_NAME) %>

diff --git a/app/views/idv/phone_errors/warning.html.erb b/app/views/idv/phone_errors/warning.html.erb index 8e3843c502a..a22331d8cbc 100644 --- a/app/views/idv/phone_errors/warning.html.erb +++ b/app/views/idv/phone_errors/warning.html.erb @@ -8,8 +8,7 @@ <% if @phone %>

- <%= t('idv.failure.phone.warning.you_entered') %> - <%= PhoneFormatter.format(@phone, country_code: @country_code) %> + <%= t('idv.failure.phone.warning.you_entered_html', formatted_phone: @formatted_phone) %>

<% end %> diff --git a/config/application.yml.default b/config/application.yml.default index e26c3a032d5..3764202bb72 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -235,13 +235,13 @@ in_person_results_delay_in_hours: 1 in_person_send_proofing_notifications_enabled: false in_person_stop_expiring_enrollments: false invalid_gpo_confirmation_zipcode: '00001' -irs_authentication_emails: '[]' -irs_authentication_issuers: '[]' irs_credential_tenure_report_config: '[]' irs_credential_tenure_report_issuers: '[]' irs_credentials_emails: '[]' irs_fraud_metrics_emails: '[]' irs_fraud_metrics_issuers: '[]' +irs_registration_funnel_emails: '[]' +irs_registration_funnel_issuers: '[]' irs_verification_report_config: '[]' irs_verification_report_issuers: '[]' @@ -307,6 +307,7 @@ minimum_wait_before_another_usps_letter_in_hours: 24 mx_timeout: 3 new_device_alert_delay_in_minutes: 5 newrelic_license_key: '' +one_account_user_verification_enabled_percentage: 0 openid_connect_content_security_form_action_enabled: false openid_connect_redirect: client_side_js otp_delivery_blocklist_findtime: 5 @@ -609,10 +610,10 @@ test: hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c hmac_fingerprinter_key_queue: '["old-key-one", "old-key-two"]' identity_pki_disabled: true - irs_authentication_emails: '["g@example.com", "h@example.com"]' - irs_authentication_issuers: '["urn:gov:gsa:openidconnect.profiles:sp:sso:agency_name:app_name"]' irs_fraud_metrics_emails: '["g@example.com", "h@example.com"]' irs_fraud_metrics_issuers: '["urn:gov:gsa:openidconnect.profiles:sp:sso:agency_name:app_name"]' + irs_registration_funnel_emails: '["g@example.com", "h@example.com"]' + irs_registration_funnel_issuers: '["urn:gov:gsa:openidconnect.profiles:sp:sso:agency_name:app_name"]' lexisnexis_trueid_account_id: 'test_account' lockout_period_in_minutes: 5 logins_per_email_and_ip_limit: 2 diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 3ffe11484cf..230ad2aec19 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -137,6 +137,23 @@ def self.all user&.uuid end.freeze + ONE_ACCOUNT_USER_VERIFICATION_ENABLED = AbTest.new( + experiment_name: 'One Account User Verification Enabled', + should_log: [ + 'Email and Password Authentication', + 'SP redirect initiated', + :one_account_duplicate_profiles_detected, + :one_account_unknown_profile_detected, + :one_account_recognize_all_profiles, + ].to_set, + buckets: { + one_account_user_verification_enabled_percentage: + IdentityConfig.store.one_account_user_verification_enabled_percentage, + }, + ) do |user:, user_session:, **| + user&.uuid + end.freeze + SOCURE_IDV_SHADOW_MODE_FOR_NON_DOCV_USERS = AbTest.new( experiment_name: 'Socure shadow mode', should_log: ['IdV: doc auth verify proofing results'].to_set, diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index 866a04f7850..05e06b7d741 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -245,8 +245,8 @@ args: -> { [Time.zone.yesterday.end_of_day] }, }, # Send previous week's authentication reports to irs - irs_weekly_authentication_report: { - class: 'Reports::IrsAuthenticationReport', + irs_weekly_registration_funnel_report: { + class: 'Reports::IrsRegistrationFunnelReport', cron: cron_every_monday, args: -> { [Time.zone.yesterday.end_of_day] }, }, diff --git a/config/locales/en.yml b/config/locales/en.yml index 13fb4b33352..d0e1700596f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -738,18 +738,19 @@ duplicate_profiles_detected.accounts_list.heading: Accounts with the same verifi duplicate_profiles_detected.cant_access: 'I can’t access an account' duplicate_profiles_detected.connected_acct_html: ' Connected agencies: %{count}' duplicate_profiles_detected.created_at_html: ' Created: %{timestamp_html}' -duplicate_profiles_detected.delete_duplicates.details_html: Sign in, authenticate, and delete the account from the ‘Your account’ page. %{link_html} -duplicate_profiles_detected.delete_duplicates.heading: 'Delete the duplicate accounts' +duplicate_profiles_detected.delete_duplicates.details_html: Sign in and delete the account from the “Your account” page. %{link_html} +duplicate_profiles_detected.delete_duplicates.heading: 'Delete the duplicate account(s)' duplicate_profiles_detected.delete_duplicates.link: How to delete your account. duplicate_profiles_detected.dont_recognize_account: I don’t recognize an account above duplicate_profiles_detected.duplicate: Duplicate duplicate_profiles_detected.get_help: Get Help duplicate_profiles_detected.heading: We found other accounts that may be yours -duplicate_profiles_detected.intro: The %{app_name} requires that you only have one identity verified %{app_name} account. Learn more about duplicate accounts -duplicate_profiles_detected.intro2: 'You need to delete the duplicate accounts before signing into %{app_name}. Here’s what to do:' +duplicate_profiles_detected.intro_html: '%{app_name} requires that you only have one identity verified %{app_name} account. %{link_html}' +duplicate_profiles_detected.intro.link: Learn more about duplicate accounts. +duplicate_profiles_detected.intro2: 'You need to delete duplicate accounts before signing into %{app_name}. Here’s what to do:' duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}' duplicate_profiles_detected.never_logged_in: Never logged in -duplicate_profiles_detected.select_an_account.details: Keep the account that you’ve connected to the most agencies. That way you don’t have to reconnect to all the agencies you use. +duplicate_profiles_detected.select_an_account.details: Keep the account you use the most. For example, the one tied to the most services. That way, you won’t have to reconnect services. duplicate_profiles_detected.select_an_account.heading: Choose an account to keep duplicate_profiles_detected.sign_back_in.details: Go back to the %{app_name} website and sign in using the one %{app_name} account you kept. duplicate_profiles_detected.sign_back_in.heading: Sign back into %{app_name} with one account @@ -1124,7 +1125,7 @@ idv.failure.phone.warning.heading: We couldn’t match you to this number idv.failure.phone.warning.learn_more_link: Learn more about what phone number to use idv.failure.phone.warning.next_steps_html: Try another number that you use often and have used for a long time. This can be a work or home number. idv.failure.phone.warning.try_again_button: Try another number -idv.failure.phone.warning.you_entered: 'We couldn’t find a record of you using this number:' +idv.failure.phone.warning.you_entered_html: "We couldn’t find a record of you using this number: %{formatted_phone}" idv.failure.sessions.exception: There was an internal error processing your request. idv.failure.sessions.fail_html: For your security, we limit the number of times you can attempt to verify personal information online. Try again in %{timeout}. idv.failure.sessions.heading: We couldn’t find records matching your personal information @@ -1946,7 +1947,7 @@ user_mailer.dupe_profile.review_complete.success_info: We completed a review of user_mailer.dupe_profile.review_complete.unable_heading: We are not able to sign you in user_mailer.dupe_profile.review_complete.unable_info: We completed a review of your accounts and are not able to sign you in. Please contact %{sp_or_app_name} for further assistance. user_mailer.dupe_profile.sign_in.description: Someone just signed into a %{app_name} that had previously been verified with your personal information. For your safety we have restricted access on all accounts with matching information and access to %{sp_or_app_name}. -user_mailer.dupe_profile.sign_in.description2_html: If this was you, you should delete the duplicate account by following the %{steps_link_html} and use only one account for. If this wasn’t you contact the %{help_center_link_html}. +user_mailer.dupe_profile.sign_in.description2_html: If this was you, you should delete the duplicate account by following the %{steps_link_html} and use only one account for your security. If this wasn’t you, contact the %{help_center_link_html}. user_mailer.dupe_profile.sign_in.heading: Another account using your personal information has signed in user_mailer.dupe_profile.sign_in.help_center_link: '%{app_name} Help Center' user_mailer.dupe_profile.sign_in.steps_link: steps outlined here diff --git a/config/locales/es.yml b/config/locales/es.yml index 27a9b54813c..de05e435224 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -745,26 +745,27 @@ doc_auth.tips.document_capture_selfie_text1: Quite cualquier prenda o accesorio doc_auth.tips.document_capture_selfie_text2: Tómese la foto en un lugar bien iluminado doc_auth.tips.document_capture_selfie_text3: Mantenga una expresión neutral doc_auth.tips.document_capture_selfie_text4: Revise que se vea su rostro completo dentro del círculo verde. -duplicate_profiles_detected.accounts_list.heading: Accounts with the same verified information -duplicate_profiles_detected.cant_access: 'I can’t access an account' +duplicate_profiles_detected.accounts_list.heading: Cuentas con la misma información verificada +duplicate_profiles_detected.cant_access: 'No puedo acceder a una cuenta' duplicate_profiles_detected.connected_acct_html: ' Connected agencies: %{count}' duplicate_profiles_detected.created_at_html: ' Created: %{timestamp_html}' -duplicate_profiles_detected.delete_duplicates.details_html: Sign in, authenticate, and delete the account from the ‘Your account’ page. %{link_html} -duplicate_profiles_detected.delete_duplicates.heading: 'Delete the duplicate accounts' -duplicate_profiles_detected.delete_duplicates.link: How to delete your account. -duplicate_profiles_detected.dont_recognize_account: I don’t recognize an account above +duplicate_profiles_detected.delete_duplicates.details_html: Inicie sesión y, en la página de “Su cuenta”, elimine la cuenta. %{link_html} +duplicate_profiles_detected.delete_duplicates.heading: 'Elimine las cuentas duplicadas.' +duplicate_profiles_detected.delete_duplicates.link: Cómo eliminar su cuenta. +duplicate_profiles_detected.dont_recognize_account: No reconozco una de estas cuentas duplicate_profiles_detected.duplicate: Duplicate -duplicate_profiles_detected.get_help: Get Help -duplicate_profiles_detected.heading: We found other accounts that may be yours -duplicate_profiles_detected.intro: The %{app_name} requires that you only have one identity verified %{app_name} account. Learn more about duplicate accounts -duplicate_profiles_detected.intro2: 'You need to delete the duplicate accounts before signing into %{app_name}. Here’s what to do:' +duplicate_profiles_detected.get_help: Obtener ayuda +duplicate_profiles_detected.heading: Encontramos otras cuentas que pueden ser suyas +duplicate_profiles_detected.intro_html: '%{app_name} requiere que usted tenga una sola cuenta de %{app_name} en la cual haya verificado su identidad. %{link_html}' +duplicate_profiles_detected.intro.link: Obtenga más información acerca de las cuentas duplicadas. +duplicate_profiles_detected.intro2: 'Antes de iniciar sesión en %{app_name}, necesita eliminar las cuentas duplicadas. Esto es lo que tiene que hacer:' duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}' duplicate_profiles_detected.never_logged_in: Never logged in -duplicate_profiles_detected.select_an_account.details: Keep the account that you’ve connected to the most agencies. That way you don’t have to reconnect to all the agencies you use. -duplicate_profiles_detected.select_an_account.heading: Choose an account to keep -duplicate_profiles_detected.sign_back_in.details: Go back to the %{app_name} website and sign in using the one %{app_name} account you kept. -duplicate_profiles_detected.sign_back_in.heading: Sign back into %{app_name} with one account -duplicate_profiles_detected.sign_out: Sign out +duplicate_profiles_detected.select_an_account.details: Conserve la cuenta que más usa. Por ejemplo, la que está vinculada a más servicios. Así, no tendrá que volver a conectar los servicios. +duplicate_profiles_detected.select_an_account.heading: Elija la cuenta que desea conservar. +duplicate_profiles_detected.sign_back_in.details: Vuelva al sitio web de %{app_name} e inicie sesión usando la cuenta de %{app_name} que conservó. +duplicate_profiles_detected.sign_back_in.heading: Vuelva a iniciar sesión en %{app_name} con una cuenta. +duplicate_profiles_detected.sign_out: Cerrar sesión duplicate_profiles_detected.signed_in: Signed In email_address.not_found: El correo electrónico no encontrado email_addresses.add.duplicate: Esta dirección de correo electrónico ya está registrada en su cuenta. @@ -1135,7 +1136,7 @@ idv.failure.phone.warning.heading: No pudimos asociarlo a este número idv.failure.phone.warning.learn_more_link: Obtenga más información sobre el número de teléfono que debe usar idv.failure.phone.warning.next_steps_html: Intente con otro número que use a menudo y haya usado por mucho tiempo. Puede ser el número del trabajo o de casa. idv.failure.phone.warning.try_again_button: Intentar con otro número -idv.failure.phone.warning.you_entered: 'No pudimos encontrar su registro con este número:' +idv.failure.phone.warning.you_entered_html: "No pudimos encontrar su registro con este número: %{formatted_phone}" idv.failure.sessions.exception: Hubo un error interno al procesar su solicitud. idv.failure.sessions.fail_html: Por su seguridad, limitamos el número de veces que puede intentar verificar la información personal en línea. Vuelva a intentarlo en %{timeout}. idv.failure.sessions.heading: No encontramos registros que coincidan con sus datos personales diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a3a67e7dcad..41b9d507d9f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -734,26 +734,27 @@ doc_auth.tips.document_capture_selfie_text1: Enlevez tout ce qui cache votre vis doc_auth.tips.document_capture_selfie_text2: Prenez votre photo dans un endroit bien éclairé doc_auth.tips.document_capture_selfie_text3: Gardez une expression neutre doc_auth.tips.document_capture_selfie_text4: Assurez-vous que l’ensemble de votre visage est visible dans le cercle vert -duplicate_profiles_detected.accounts_list.heading: Accounts with the same verified information -duplicate_profiles_detected.cant_access: 'I can’t access an account' +duplicate_profiles_detected.accounts_list.heading: Comptes ayant les mêmes informations vérifiées +duplicate_profiles_detected.cant_access: 'Je ne peux pas accéder à un compte' duplicate_profiles_detected.connected_acct_html: ' Connected agencies: %{count}' duplicate_profiles_detected.created_at_html: ' Created: %{timestamp_html}' -duplicate_profiles_detected.delete_duplicates.details_html: Sign in, authenticate, and delete the account from the ‘Your account’ page. %{link_html} -duplicate_profiles_detected.delete_duplicates.heading: 'Delete the duplicate accounts' -duplicate_profiles_detected.delete_duplicates.link: How to delete your account. -duplicate_profiles_detected.dont_recognize_account: I don’t recognize an account above +duplicate_profiles_detected.delete_duplicates.details_html: Connectez-vous au compte puis supprimez-le depuis la page « Votre compte » . %{link_html} +duplicate_profiles_detected.delete_duplicates.heading: 'Supprimer le(s) compte(s) en double' +duplicate_profiles_detected.delete_duplicates.link: 'Comment supprimer votre compte :' +duplicate_profiles_detected.dont_recognize_account: Je ne reconnais pas l’un des comptes ci-dessus duplicate_profiles_detected.duplicate: Duplicate -duplicate_profiles_detected.get_help: Get Help -duplicate_profiles_detected.heading: We found other accounts that may be yours -duplicate_profiles_detected.intro: The %{app_name} requires that you only have one identity verified %{app_name} account. Learn more about duplicate accounts -duplicate_profiles_detected.intro2: 'You need to delete the duplicate accounts before signing into %{app_name}. Here’s what to do:' +duplicate_profiles_detected.get_help: Obtenir de l’aide +duplicate_profiles_detected.heading: Nous avons trouvé d’autres comptes susceptibles de vous appartenir +duplicate_profiles_detected.intro_html: Pour accéder à %{app_name}, vous devez disposer d’un seul compte %{app_name} avec votre identité vérifiée. %{link_html} +duplicate_profiles_detected.intro.link: En savoir plus sur les comptes en double. +duplicate_profiles_detected.intro2: 'Vous devez supprimer les comptes en double avant de vous connecter à %{app_name}. Voici comment faire :' duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}' duplicate_profiles_detected.never_logged_in: Never logged in -duplicate_profiles_detected.select_an_account.details: Keep the account that you’ve connected to the most agencies. That way you don’t have to reconnect to all the agencies you use. -duplicate_profiles_detected.select_an_account.heading: Choose an account to keep -duplicate_profiles_detected.sign_back_in.details: Go back to the %{app_name} website and sign in using the one %{app_name} account you kept. -duplicate_profiles_detected.sign_back_in.heading: Sign back into %{app_name} with one account -duplicate_profiles_detected.sign_out: Sign out +duplicate_profiles_detected.select_an_account.details: Conservez le compte que vous utilisez le plus. Par exemple, celui qui est connecté au plus grand nombre de services. Ainsi, vous n’aurez pas à recommencer les démarches pour vous reconnecter à des services. +duplicate_profiles_detected.select_an_account.heading: Choisir le compte que vous voulez conserver +duplicate_profiles_detected.sign_back_in.details: Retournez sur le site web de %{app_name} et connectez-vous à l’aide du compte %{app_name} que vous avez conservé. +duplicate_profiles_detected.sign_back_in.heading: Reconnectez-vous à %{app_name} avec un seul compte +duplicate_profiles_detected.sign_out: Se déconnecter duplicate_profiles_detected.signed_in: Signed In email_address.not_found: Email non trouvé email_addresses.add.duplicate: Cette adresse e-mail est déjà enregistrée sur votre compte. @@ -1124,7 +1125,7 @@ idv.failure.phone.warning.heading: Nous n’avons pas pu vous associer à ce num idv.failure.phone.warning.learn_more_link: En savoir plus sur quel numéro de téléphone utiliser idv.failure.phone.warning.next_steps_html: Essayez un autre numéro que vous utilisez souvent et depuis longtemps. Il peut s’agir d’un numéro professionnel ou personnel. idv.failure.phone.warning.try_again_button: Essayez un autre numéro -idv.failure.phone.warning.you_entered: 'Nous n’avons pas trouvé de données vous associant au numéro suivant :' +idv.failure.phone.warning.you_entered_html: "Nous n’avons pas trouvé de données vous associant au numéro suivant: %{formatted_phone}" idv.failure.sessions.exception: Une erreur interne s’est produite lors du traitement de votre demande. idv.failure.sessions.fail_html: Pour votre sécurité, nous limitons le nombre de fois où vous pouvez tenter de vérifier des renseignements personnels en ligne. Réessayez dans %{timeout}. idv.failure.sessions.heading: Nous n’avons pas trouvé de dossiers correspondant à vos informations personnelles @@ -1138,7 +1139,7 @@ idv.failure.verify.fail_link_html: Obtenez de l’aide auprès de %{sp_n idv.failure.verify.fail_text: pour accéder aux services. idv.failure.verify.heading: Nous n’avons pas pu confirmer votre identité idv.failure.warning.attempts_html.one: Vous avez encore un essai. Vous devrez ensuite attendre 6 heures avant de réessayer. -idv.failure.warning.attempts_html.other: Vous pouvez encore essayer %{count} fois de plus. Ensuite vous devrez ensuite attendre 6 heures avant de réessayer. +idv.failure.warning.attempts_html.other: Vous pouvez encore essayer %{count} fois de plus. Ensuite, vous devrez attendre 6 heures avant de réessayer. idv.forgot_password.link_text: Mot de passe oublié? idv.forgot_password.modal_header: Êtes-vous sûr de ne pas pouvoir vous souvenir de votre mot de passe? idv.forgot_password.reset_password: Réinitialiser le mot de passe diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 602c8807d4f..7952e6857f6 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -83,15 +83,15 @@ account.index.reactivation.instructions: 你的用户资料因为重设密码最 account.index.reactivation.link: 现在重新激活你的用户资料。 account.index.sign_in_location_and_ip: 从 %{ip}(IP 地址可能位于 %{location})。 account.index.unknown_location: 未知地点 -account.index.verification.connect_idv_account.cta: 登录%{sp_name}来获得服务。 -account.index.verification.connect_idv_account.intro: 把你的账户连接到%{sp_name}。 +account.index.verification.connect_idv_account.cta: 登录%{sp_name} 来获得服务。 +account.index.verification.connect_idv_account.intro: 把你的账户与%{sp_name} 连接。 account.index.verification.continue_idv: 继续身份验证 account.index.verification.finish_verifying_html: 完成身份验证流程来获得访问 %{sp_name} 的权限。 account.index.verification.finish_verifying_no_sp: 完成身份验证流程来获得访问%{app_name} 合作伙伴机构的权限。 account.index.verification.identity_verification: 身份验证 account.index.verification.in_person_instructions_html: 你必须在 %{deadline} 之前去邮局完成验证你的身份。 account.index.verification.instructions: 输入你的验证码来完成身份验证。 -account.index.verification.learn_more_link: 了解更多有关验证你身份的信息。 +account.index.verification.learn_more_link: 了解更多验证你身份的信息。 account.index.verification.legacy_verified_html: %{date}你使用身份证件在%{app_name}上验证了你的身份。 account.index.verification.pending_badge: 待验证 account.index.verification.pending_badge_tooltip: 你的身份有待验证。 @@ -105,7 +105,7 @@ account.index.verification.verified_badge_tooltip: 你的身份已经验证。 account.index.verification.verified_facial_match_badge_tooltip: 你的身份和照片都已验证。 account.index.verification.verify_with_facial_match_html: 要访问 %{sp_name},请使用你本人照片再次验证身份。 account.index.verification.you_verified_your_facial_match_identity: 你使用以下信息验证了身份并验证了一张你本人的照片,从而获得了访问%{app_name}所有合作伙伴机构的权限。 -account.index.verification.you_verified_your_identity_html: 你使用以下信息向 %{sp_name} 验证了身份。 +account.index.verification.you_verified_your_identity_html: 你用以下信息为%{sp_name}验证了身份。 account.index.webauthn: 安全密钥 account.index.webauthn_add: 添加安全密钥 account.index.webauthn_platform: 人脸或触摸解锁 @@ -745,26 +745,27 @@ doc_auth.tips.document_capture_selfie_text1: 摘掉任何遮盖您面孔的东 doc_auth.tips.document_capture_selfie_text2: 在光线明亮的地方拍照 doc_auth.tips.document_capture_selfie_text3: 保持中性表情 doc_auth.tips.document_capture_selfie_text4: 确保您整个面孔都可以在绿色圆圈里看到 -duplicate_profiles_detected.accounts_list.heading: Accounts with the same verified information -duplicate_profiles_detected.cant_access: 'I can’t access an account' +duplicate_profiles_detected.accounts_list.heading: 以下账户具有相同的已验证信息 +duplicate_profiles_detected.cant_access: '我无法访问某个帐户' duplicate_profiles_detected.connected_acct_html: ' Connected agencies: %{count}' duplicate_profiles_detected.created_at_html: ' Created: %{timestamp_html}' -duplicate_profiles_detected.delete_duplicates.details_html: Sign in, authenticate, and delete the account from the ‘Your account’ page. %{link_html} -duplicate_profiles_detected.delete_duplicates.heading: 'Delete the duplicate accounts' -duplicate_profiles_detected.delete_duplicates.link: How to delete your account. -duplicate_profiles_detected.dont_recognize_account: I don’t recognize an account above +duplicate_profiles_detected.delete_duplicates.details_html: 登录后,从“你的帐户”页面删除该帐户。 %{link_html} +duplicate_profiles_detected.delete_duplicates.heading: '删除重复帐户' +duplicate_profiles_detected.delete_duplicates.link: 如何删除你的帐户。 +duplicate_profiles_detected.dont_recognize_account: 我不认识上边的一个帐户 duplicate_profiles_detected.duplicate: Duplicate -duplicate_profiles_detected.get_help: Get Help -duplicate_profiles_detected.heading: We found other accounts that may be yours -duplicate_profiles_detected.intro: The %{app_name} requires that you only have one identity verified %{app_name} account. Learn more about duplicate accounts -duplicate_profiles_detected.intro2: 'You need to delete the duplicate accounts before signing into %{app_name}. Here’s what to do:' +duplicate_profiles_detected.get_help: 获取帮助 +duplicate_profiles_detected.heading: 我们发现了其他可能属于你的帐户 +duplicate_profiles_detected.intro_html: '%{app_name} 规定你只能拥有一个身份经过验证的 %{app_name} 帐户。%{link_html}' +duplicate_profiles_detected.intro.link: 了解更多关于重复帐户的信息。 +duplicate_profiles_detected.intro2: '你需要在登录 %{app_name}. 之前删除重复帐户。操作步骤如下:' duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}' duplicate_profiles_detected.never_logged_in: Never logged in -duplicate_profiles_detected.select_an_account.details: Keep the account that you’ve connected to the most agencies. That way you don’t have to reconnect to all the agencies you use. -duplicate_profiles_detected.select_an_account.heading: Choose an account to keep -duplicate_profiles_detected.sign_back_in.details: Go back to the %{app_name} website and sign in using the one %{app_name} account you kept. -duplicate_profiles_detected.sign_back_in.heading: Sign back into %{app_name} with one account -duplicate_profiles_detected.sign_out: Sign out +duplicate_profiles_detected.select_an_account.details: 保留你最常用的那个帐户。例如,与最多机构绑定的那个帐户。这样,你就无需再重新连接这些机构。 +duplicate_profiles_detected.select_an_account.heading: 选择要保留的帐户 +duplicate_profiles_detected.sign_back_in.details: 返回 %{app_name} 网站,使用你保留的那个 %{app_name} 帐户登录。 +duplicate_profiles_detected.sign_back_in.heading: 使用一个帐户重新登录 %{app_name} +duplicate_profiles_detected.sign_out: 登出 duplicate_profiles_detected.signed_in: Signed In email_address.not_found: 未找到电子邮件 email_addresses.add.duplicate: 该电邮地址已注册到你的账户。 @@ -1137,7 +1138,7 @@ idv.failure.phone.warning.heading: 我们无法将你与该号码匹配。 idv.failure.phone.warning.learn_more_link: 了解有关使用什么号码的更多信息。 idv.failure.phone.warning.next_steps_html: 尝试 另一个 你经常使用并用了很久的号码。 工作或住宅号码都行。 idv.failure.phone.warning.try_again_button: 尝试另一个号码 -idv.failure.phone.warning.you_entered: 我们找不到你使用此号码的记录: +idv.failure.phone.warning.you_entered_html: "我们找不到你使用: %{formatted_phone} 电话号码的记录" idv.failure.sessions.exception: 处理你的请求时内部出错。 idv.failure.sessions.fail_html: 出于安全考虑,我们限制你在网上尝试验证个人信息的次数。 %{timeout}后再试。 idv.failure.sessions.heading: 我们找不到与你个人信息匹配的记录 @@ -1150,8 +1151,8 @@ idv.failure.verify.exit: 退出 %{app_name} idv.failure.verify.fail_link_html: 在%{sp_name} 得到 idv.failure.verify.fail_text: 帮助以获得服务。 idv.failure.verify.heading: 我们无法验证你的身份证件。 -idv.failure.warning.attempts_html.one: 您可以再试1次。然后您必须等6个小时才能再试。 -idv.failure.warning.attempts_html.other: 您可以再试%{count}次。然后您必须等6个小时才能再试。 +idv.failure.warning.attempts_html.one: 你可以再试1次。你后您必须等6个小时才能再试。 +idv.failure.warning.attempts_html.other: 你可以再试%{count}次。你后您必须等6个小时才能再试。 idv.forgot_password.link_text: 忘了密码? idv.forgot_password.modal_header: 你确定不记得密码吗? idv.forgot_password.reset_password: 重设密码 @@ -1664,7 +1665,7 @@ step_indicator.flows.idv.go_to_the_post_office: 去邮局 step_indicator.flows.idv.re_enter_password: 重新输入你的密码 step_indicator.flows.idv.secure_account: 保护你账户的安全 step_indicator.flows.idv.verify_address: 验证你的地址 -step_indicator.flows.idv.verify_id: 验证你的身份证件 +step_indicator.flows.idv.verify_id: 验证你的ID step_indicator.flows.idv.verify_info: 验证你的信息 step_indicator.flows.idv.verify_phone: 验证你的电话号码 step_indicator.status.complete: 完成了 diff --git a/db/primary_migrate/20250808215829_remove_foreign_key_duplicate_profile_confirmation.rb b/db/primary_migrate/20250808215829_remove_foreign_key_duplicate_profile_confirmation.rb new file mode 100644 index 00000000000..1c1603bdea6 --- /dev/null +++ b/db/primary_migrate/20250808215829_remove_foreign_key_duplicate_profile_confirmation.rb @@ -0,0 +1,5 @@ +class RemoveForeignKeyDuplicateProfileConfirmation < ActiveRecord::Migration[8.0] + def change + remove_foreign_key :duplicate_profile_confirmations, :profiles, if_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 8fbe40317cb..cc1dc1d388e 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[8.0].define(version: 2025_07_14_184424) do +ActiveRecord::Schema[8.0].define(version: 2025_08_08_215829) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_catalog.plpgsql" @@ -705,7 +705,6 @@ add_foreign_key "device_profiling_results", "users" add_foreign_key "document_capture_sessions", "users" - add_foreign_key "duplicate_profile_confirmations", "profiles" add_foreign_key "iaa_gtcs", "partner_accounts" add_foreign_key "iaa_orders", "iaa_gtcs" add_foreign_key "in_person_enrollments", "profiles" diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 82a303f94a8..fce12ef47eb 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -180,8 +180,8 @@ def self.store config.add(:facial_match_general_availability_enabled, type: :boolean) config.add(:feature_idv_force_gpo_verification_enabled, type: :boolean) config.add(:feature_idv_hybrid_flow_enabled, type: :boolean) - config.add(:irs_authentication_issuers, type: :json) - config.add(:irs_authentication_emails, type: :json) + config.add(:irs_registration_funnel_issuers, type: :json) + config.add(:irs_registration_funnel_emails, type: :json) config.add(:irs_fraud_metrics_issuers, type: :json) config.add(:irs_fraud_metrics_emails, type: :json) config.add(:geo_data_file_path, type: :string) @@ -319,6 +319,7 @@ def self.store config.add(:mx_timeout, type: :integer) config.add(:new_device_alert_delay_in_minutes, type: :integer) config.add(:newrelic_license_key, type: :string) + config.add(:one_account_user_verification_enabled_percentage, type: :integer) config.add( :openid_connect_redirect, type: :string, diff --git a/lib/reporting/irs_authentication_report.rb b/lib/reporting/irs_registration_funnel_report.rb similarity index 84% rename from lib/reporting/irs_authentication_report.rb rename to lib/reporting/irs_registration_funnel_report.rb index 3bf0b64ff61..58c8c4f08e1 100644 --- a/lib/reporting/irs_authentication_report.rb +++ b/lib/reporting/irs_registration_funnel_report.rb @@ -11,17 +11,15 @@ end module Reporting - class IrsAuthenticationReport + class IrsRegistrationFunnelReport include Reporting::CloudwatchQueryQuoting attr_reader :issuers, :time_range module Events - OIDC_AUTH_REQUEST = 'OpenID Connect: authorization request' EMAIL_CONFIRMATION = 'User Registration: Email Confirmation' TWO_FA_SETUP_VISITED = 'User Registration: 2FA Setup visited' USER_FULLY_REGISTERED = 'User Registration: User Fully Registered' - SP_REDIRECT = 'SP redirect initiated' def self.all_events constants.map { |c| const_get(c) } @@ -67,7 +65,7 @@ def as_emailable_reports filename: 'overview', ), Reporting::EmailableReport.new( - title: 'Authentication Funnel Metrics', + title: 'Registration Funnel Metrics', table: funnel_metrics_table, filename: 'funnel_metrics', ), @@ -115,14 +113,6 @@ def funnel_metrics_table user_fully_registered, format_as_percent(numerator: user_fully_registered, denominator: email_confirmation), ], - [ - 'Registration Success Rate', - sp_redirect_initiated_new_users, - format_as_percent( - numerator: sp_redirect_initiated_new_users, - denominator: email_confirmation, - ), - ], ] end @@ -197,23 +187,5 @@ def user_fully_registered @user_fully_registered ||= (data[Events::USER_FULLY_REGISTERED] & data[Events::EMAIL_CONFIRMATION]).count end - - def sp_redirect_initiated_new_users - @sp_redirect_initiated_new_users ||= - (data[Events::SP_REDIRECT] & data[Events::EMAIL_CONFIRMATION]).count - end - - def sp_redirect_initiated_all - data[Events::SP_REDIRECT].count - end - - def oidc_auth_request - data[Events::OIDC_AUTH_REQUEST].count - end - - def sp_redirect_initiated_after_oidc - @sp_redirect_initiated_after_oidc ||= - (data[Events::SP_REDIRECT] & data[Events::OIDC_AUTH_REQUEST]).count - end end end diff --git a/lib/reporting/irs_verification_report.rb b/lib/reporting/irs_verification_report.rb index 6fa55a9d7eb..11eeb0783e0 100644 --- a/lib/reporting/irs_verification_report.rb +++ b/lib/reporting/irs_verification_report.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true require 'csv' + begin require 'reporting/cloudwatch_client' require 'reporting/cloudwatch_query_quoting' require 'reporting/command_line_options' rescue LoadError => e - warn 'could not load paths, try running with "bundle exec rails runner"' + warn 'Could not load paths, try running with "bundle exec rails runner"' raise e end @@ -46,8 +47,7 @@ def progress? end def as_tables - [overview_table, - funnel_table] + [overview_table, funnel_table] end def as_emailable_reports @@ -69,28 +69,26 @@ def as_emailable_reports filename: 'Overview Report', ), Reporting::EmailableReport.new( - title: 'Funnel Metrics', + title: 'Verification Funnel Metrics', subtitle: '', float_as_percent: true, precision: 2, table: funnel_table, - filename: 'Funnel Metrics', + filename: 'Verification Funnel Metrics', ), ] end def to_csvs as_emailable_reports.map do |report| - CSV.generate do |csv| - report.table.each { |row| csv << row } - end + CSV.generate { |csv| report.table.each { |row| csv << row } } end end def overview_table [ ['Report Timeframe', "#{time_range.begin.to_date} to #{time_range.end.to_date}"], - ['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date + ['Report Generated', Time.zone.today.to_s], ['Issuer', issuers.join(', ')], ] end @@ -112,8 +110,7 @@ def funnel_table to_percent(info_validation_success, verification_demand)], ['Phone Verification Success', phone_verification_success, to_percent(phone_verification_success, verification_demand)], - ['Verification Successes', total_verified, - to_percent(total_verified, verification_demand)], + ['Verification Successes', total_verified, to_percent(total_verified, verification_demand)], ['Verification Failures', verification_demand - total_verified, to_percent(verification_demand - total_verified, verification_demand)], ] @@ -152,85 +149,71 @@ def cloudwatch_client ) end - # def quote(array) - # '[' + array.map { |e| %("#{e}") }.join(', ') + ']' - # end - - def query + def query_for_event(event_name) params = { + event_name: quote(event_name), issuers: quote(issuers), - event_names: quote(Events.all_events), } format(<<~QUERY, params) - filter properties.sp_request.facial_match - and name in %{event_names} - | fields - (name = '#{Events::VERIFICATION_DEMAND}') as @IdV_IAL2_start, - (name = '#{Events::DOCUMENT_AUTHENTICATION_SUCCESS}') as @Doc_auth_success, - (name = '#{Events::INFORMATION_VALIDATION_SUCCESS}') as @Verify_info_success, - (name = '#{Events::PHONE_VERIFICATION_SUCCESS}') as @Verify_phone_success, - (name = '#{Events::TOTAL_VERIFIED}' and properties.event_properties.ial2) as @Verified_1, - coalesce(@Verified_1, 0) as @Verified, - properties.user_id - | stats - max(@IdV_IAL2_start) as max_idv_ial2_start, - max(@Doc_auth_success) as max_doc_auth_success, - max(@Verify_info_success) as max_verify_info_success, - max(@Verify_phone_success) as max_verify_phone_success, - max(@Verified) as max_verified - by properties.user_id, bin(1y) - | stats - sum(max_idv_ial2_start) as IdV_IAL2_Start_User_count, - sum(max_doc_auth_success) as Doc_auth_success_User_count, - sum(max_verify_info_success) as Verify_info_success_User_count, - sum(max_verify_phone_success) as Verify_phone_success_User_count, - sum(max_verified) as Verified_User_count - | limit 10000 + filter properties.sp_request.facial_match + and name = %{event_name} + and properties.service_provider in %{issuers} + | fields properties.user_id + | limit 10000 QUERY end - def fetch_results - Rails.logger.info('Executing unified query') + def fetch_event_user_ids(event_name) + unless Events.all_events.include?(event_name) + Rails.logger.warn("Event name '#{event_name}' is not in the list of known events.") + return [] + end + + Rails.logger.info("Fetching results for event: #{event_name}") + results = cloudwatch_client.fetch( - query: query, + query: query_for_event(event_name), from: time_range.begin.beginning_of_day, to: time_range.end.end_of_day, ) - Rails.logger.info("Results: #{results.inspect}") - results + + if results.nil? || results.empty? + Rails.logger.warn("No results returned for event: #{event_name}") + return [] + end + + user_ids = results.map { |row| row['properties.user_id'] }.compact.uniq + Rails.logger.info("Fetched #{user_ids.count} unique user IDs for #{event_name}") + user_ids rescue StandardError => e - Rails.logger.error("Failed to fetch results for unified query: #{e.message}") + Rails.logger.error("Failed to fetch event #{event_name}: #{e.message}") [] end - def data - @data ||= fetch_results.first || {} - end - def verification_demand_results - data['IdV_IAL2_Start_User_count'].to_i || 0 + fetch_event_user_ids(Events::VERIFICATION_DEMAND).count end def document_authentication_success_results - data['Doc_auth_success_User_count'].to_i || 0 + fetch_event_user_ids(Events::DOCUMENT_AUTHENTICATION_SUCCESS).count end def information_validation_success_results - data['Verify_info_success_User_count'].to_i || 0 + fetch_event_user_ids(Events::INFORMATION_VALIDATION_SUCCESS).count end def phone_verification_success_results - data['Verify_phone_success_User_count'].to_i || 0 + fetch_event_user_ids(Events::PHONE_VERIFICATION_SUCCESS).count end def total_verified_results - data['Verified_User_count'].to_i || 0 + fetch_event_user_ids(Events::TOTAL_VERIFIED).count end def to_percent(numerator, denominator) return 0.0 if denominator.nil? || denominator.zero? - ((numerator.to_f / denominator)).round(2) + (numerator.to_f / denominator).round(2) end end end diff --git a/lib/reporting/monthly_idv_report.rb b/lib/reporting/monthly_idv_report.rb index 614c26d6ff4..f27403863b5 100644 --- a/lib/reporting/monthly_idv_report.rb +++ b/lib/reporting/monthly_idv_report.rb @@ -85,11 +85,7 @@ def parallel_reports end def monthly_subreports - ranges = [ - (end_date - 2.months).all_month, - (end_date - 1.month).all_month, - end_date.all_month, - ] + ranges = [end_date.all_month] ranges.map do |range| Reporting::IdentityVerificationReport.new( diff --git a/lib/reporting/proofing_rate_report.rb b/lib/reporting/proofing_rate_report.rb index 69c9c201313..210082f1c26 100644 --- a/lib/reporting/proofing_rate_report.rb +++ b/lib/reporting/proofing_rate_report.rb @@ -6,7 +6,7 @@ module Reporting class ProofingRateReport - DATE_INTERVALS = [30, 60, 90].freeze + DATE_INTERVALS = [30].freeze attr_reader :end_date, :wait_duration diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index a63f0de104f..543fda4c287 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -613,6 +613,8 @@ def index .and_return([issuer]) allow(controller).to receive(:sp_from_sp_session) .and_return(sp) + + allow(controller).to receive(:user_in_one_account_verification_bucket?).and_return(true) end context 'when SP is not eligible for one account' do @@ -646,24 +648,20 @@ def index context 'when user has active profile' do let!(:active_profile) { create(:profile, :active, user: user) } - context 'when no duplicate profile confirmations exist' do + context 'when no duplicate profile ids found in session' do it 'returns false' do get :index expect(response.body).to eq('false') end end - - context 'when duplicate profile confirmations exist but are already confirmed' do + context 'when duplicate profile ids found in session' do before do - create( - :duplicate_profile_confirmation, - profile: active_profile, confirmed_all: Time.zone.now, - ) + controller.user_session[:duplicate_profile_ids] = [active_profile.id] end - it 'returns false' do + it 'returns true' do get :index - expect(response.body).to eq('false') + expect(response.body).to eq('true') end end end diff --git a/spec/jobs/reports/irs_authentication_report_spec.rb b/spec/jobs/reports/irs_registration_funnel_report_spec.rb similarity index 82% rename from spec/jobs/reports/irs_authentication_report_spec.rb rename to spec/jobs/reports/irs_registration_funnel_report_spec.rb index f5a5606f9bf..b79a193e4de 100644 --- a/spec/jobs/reports/irs_authentication_report_spec.rb +++ b/spec/jobs/reports/irs_registration_funnel_report_spec.rb @@ -1,14 +1,14 @@ require 'rails_helper' -RSpec.describe Reports::IrsAuthenticationReport do +RSpec.describe Reports::IrsRegistrationFunnelReport do let(:report_date) { Date.new(2021, 3, 2).in_time_zone('UTC').end_of_day } let(:time_range) { report_date.all_month } - subject(:report) { Reports::IrsAuthenticationReport.new(report_date) } + subject(:report) { Reports::IrsRegistrationFunnelReport.new(report_date) } - let(:name) { 'irs-authentication-report' } + let(:name) { 'irs-registration-funnel-report' } let(:s3_report_bucket_prefix) { 'reports-bucket' } let(:report_folder) do - 'int/irs-authentication-report/2021/2021-03-02.irs-authentication-report' + 'int/irs-registration-funnel-report/2021/2021-03-02.irs-registration-funnel-report' end let(:expected_s3_paths) do @@ -53,17 +53,17 @@ }, } - allow(IdentityConfig.store).to receive(:irs_authentication_emails) + allow(IdentityConfig.store).to receive(:irs_registration_funnel_emails) .and_return(mock_test_auth_emails) - allow(report.irs_authentication_report).to receive(:funnel_metrics_table) + allow(report.irs_registration_funnel_report).to receive(:funnel_metrics_table) .and_return(mock_funnel_metrics_data) end it 'sends out a report to just to team data' do expect(ReportMailer).to receive(:tables_report).once.with( email: anything, - subject: 'IRS Authentication Report - 2021-03-02', + subject: 'IRS Registration Funnel Report - 2021-03-02', reports: anything, message: report.preamble, attachment_format: :csv, @@ -73,7 +73,7 @@ end it 'does not send out a report with no emails' do - allow(IdentityConfig.store).to receive(:irs_authentication_emails).and_return('') + allow(IdentityConfig.store).to receive(:irs_registration_funnel_emails).and_return('') expect(report).to_not receive(:reports) diff --git a/spec/jobs/reports/monthly_key_metrics_report_spec.rb b/spec/jobs/reports/monthly_key_metrics_report_spec.rb index c4bf44ccdd8..b73296ef42a 100644 --- a/spec/jobs/reports/monthly_key_metrics_report_spec.rb +++ b/spec/jobs/reports/monthly_key_metrics_report_spec.rb @@ -36,12 +36,12 @@ let(:mock_proofing_rate_data) do [ - ['Metric', 'Trailing 30d', 'Trailing 60d', 'Trailing 90d'], + ['Metric', 'Trailing 30d'], ] end let(:mock_monthly_idv_data) do [ - ['Metric', 'June 2024', 'July 2024', 'August 2024'], + ['Metric', 'Aug 2024'], ] end diff --git a/spec/lib/reporting/irs_authentication_report_spec.rb b/spec/lib/reporting/irs_registration_funnel_report_spec.rb similarity index 95% rename from spec/lib/reporting/irs_authentication_report_spec.rb rename to spec/lib/reporting/irs_registration_funnel_report_spec.rb index 2979cc9d6a4..2bb959319c9 100644 --- a/spec/lib/reporting/irs_authentication_report_spec.rb +++ b/spec/lib/reporting/irs_registration_funnel_report_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' -require 'reporting/irs_authentication_report' +require 'reporting/irs_registration_funnel_report' -RSpec.describe Reporting::IrsAuthenticationReport do +RSpec.describe Reporting::IrsRegistrationFunnelReport do let(:issuer) { 'my:example:issuer' } let(:time_range) { Date.new(2022, 1, 1).in_time_zone('UTC').all_week } let(:expected_definitions_table) do @@ -30,11 +30,10 @@ ['Registration Demand', 4, '100.0%'], ['Registration Failures', 2, '50.0%'], ['Registration Successes', 2, '50.0%'], - ['Registration Success Rate', 1, '25.0%'], ] end - subject(:report) { Reporting::IrsAuthenticationReport.new(issuers: [issuer], time_range:) } + subject(:report) { Reporting::IrsRegistrationFunnelReport.new(issuers: [issuer], time_range:) } before do travel_to Time.zone.now.beginning_of_day @@ -113,7 +112,7 @@ table: expected_overview_table, ), Reporting::EmailableReport.new( - title: 'Authentication Funnel Metrics', + title: 'Registration Funnel Metrics', filename: 'funnel_metrics', table: expected_funnel_metrics_table, ), diff --git a/spec/lib/reporting/irs_verification_report_spec.rb b/spec/lib/reporting/irs_verification_report_spec.rb index 1069aa9c2b0..5bcef7c60e4 100644 --- a/spec/lib/reporting/irs_verification_report_spec.rb +++ b/spec/lib/reporting/irs_verification_report_spec.rb @@ -14,7 +14,7 @@ def previous_week_range one_week = 7.days - last_sunday = Time.current.utc.to_date.beginning_of_week(:sunday) - one_week + last_sunday = Time.zone.now.to_date.beginning_of_week(:sunday) - one_week last_saturday = last_sunday + 6.days last_sunday..last_saturday end @@ -22,7 +22,7 @@ def previous_week_range describe '#overview_table' do it 'generates the overview table with the correct data' do freeze_time do - expected_generated_date = Time.current.utc.to_date.to_s + expected_generated_date = Time.zone.now.to_date.to_s table = report.overview_table diff --git a/spec/lib/reporting/monthly_idv_report_spec.rb b/spec/lib/reporting/monthly_idv_report_spec.rb index 6e0b6ba6595..21bb09cc32c 100644 --- a/spec/lib/reporting/monthly_idv_report_spec.rb +++ b/spec/lib/reporting/monthly_idv_report_spec.rb @@ -13,26 +13,6 @@ before do allow(idv_report).to receive(:reports).and_return( [ - instance_double( - 'Reporting::IdentityVerificationReport', - time_range: Date.new(2024, 6, 1).all_month, - idv_started: 1111, - successfully_verified_users: 1111, - blanket_proofing_rate: 0.1111, - idv_final_resolution: 1111, - idv_final_resolution_rate: 0.1111, - verified_user_count: 1111, - ), - instance_double( - 'Reporting::IdentityVerificationReport', - time_range: Date.new(2024, 7, 1).all_month, - idv_started: 2222, - successfully_verified_users: 2222, - blanket_proofing_rate: 0.2222, - idv_final_resolution: 2222, - idv_final_resolution_rate: 0.2222, - verified_user_count: 2222, - ), instance_double( 'Reporting::IdentityVerificationReport', time_range: Date.new(2024, 8, 1).all_month, @@ -49,19 +29,17 @@ let(:expected_table) do [ - ['Metric', 'Jun 2024', 'Jul 2024', 'Aug 2024'], - ['IDV started', 1111, 2222, 3333], - ['# of successfully verified users', 1111, 2222, 3333], - ['% IDV started to successfully verified', 0.1111, 0.2222, - 0.3333], - ['# of workflow completed', 1111, 2222, 3333], - ['% rate of workflow completed', 0.1111, 0.2222, - 0.3333], - ['# of users verified (total)', 1111, 2222, 3333], + ['Metric', 'Aug 2024'], + ['IDV started', 3333], + ['# of successfully verified users', 3333], + ['% IDV started to successfully verified', 0.3333], + ['# of workflow completed', 3333], + ['% rate of workflow completed', 0.3333], + ['# of users verified (total)', 3333], ] end - it 'reports 3 months of data' do + it 'reports 1 month of data' do idv_report.as_csv.zip(expected_table).each do |actual, expected| expect(actual).to eq(expected) end @@ -96,15 +74,11 @@ end describe '#monthly_subreports' do - let(:june) { Date.new(2024, 6, 1).in_time_zone('UTC').all_month } - let(:july) { Date.new(2024, 7, 1).in_time_zone('UTC').all_month } let(:august) { Date.new(2024, 8, 1).in_time_zone('UTC').all_month } - it 'returns IdV reports for the expected months' do - [june, july, august].each do |month| - expect(Reporting::IdentityVerificationReport).to receive(:new) - .with(issuers: nil, time_range: month, cloudwatch_client: anything) - end + it 'returns IdV report for the expected month' do + expect(Reporting::IdentityVerificationReport).to receive(:new) + .with(issuers: nil, time_range: august, cloudwatch_client: anything) subject.monthly_subreports end diff --git a/spec/lib/reporting/proofing_rate_report_spec.rb b/spec/lib/reporting/proofing_rate_report_spec.rb index 6ca3a7e1ccb..6135020cd23 100644 --- a/spec/lib/reporting/proofing_rate_report_spec.rb +++ b/spec/lib/reporting/proofing_rate_report_spec.rb @@ -28,60 +28,27 @@ idv_fraud_rejected: 0, time_range: (end_date - 30.days).beginning_of_day..end_date, ), - instance_double( - 'Reporting::IdentityVerificationReport', - blanket_proofing_rate: 0.4, - intent_proofing_rate: 0.5, - actual_proofing_rate: 0.6666666666666666, - industry_proofing_rate: 0.6666666666666666, - idv_started: 5, - idv_doc_auth_welcome_submitted: 4, - idv_doc_auth_socure_verification_data_requested: 0, - idv_doc_auth_image_vendor_submitted: 3, - successfully_verified_users: 2, - idv_doc_auth_rejected: 1, - idv_fraud_rejected: 1, - time_range: (end_date - 60.days).beginning_of_day..end_date, - ), - instance_double( - 'Reporting::IdentityVerificationReport', - blanket_proofing_rate: 0.5, - intent_proofing_rate: 0.6, - actual_proofing_rate: 0.75, - industry_proofing_rate: 0.75, - idv_started: 6, - idv_doc_auth_welcome_submitted: 5, - idv_doc_auth_image_vendor_submitted: 4, - idv_doc_auth_socure_verification_data_requested: 0, - successfully_verified_users: 3, - idv_doc_auth_rejected: 1, - idv_fraud_rejected: 2, - time_range: (end_date - 90.days).beginning_of_day..end_date, - ), ], ) end - it 'renders a report with 30, 60, 90 day numbers' do - # rubocop:disable Layout/LineLength + it 'renders a report with only 30 day numbers' do expected_csv = [ - ['Metric', 'Trailing 30d', 'Trailing 60d', 'Trailing 90d'], - ['Start Date', Date.new(2021, 12, 2), Date.new(2021, 11, 2), Date.new(2021, 10, 3)], - ['End Date', Date.new(2022, 1, 1), Date.new(2022, 1, 1), Date.new(2022, 1, 1)], - ['IDV Started', 4, 5, 6], - ['Welcome Submitted', 3, 4, 5], - ['Image Submitted', 2, 3, 4], - ['Socure', 0, 0, 0], - ['Successfully Verified', 1, 2, 3], - ['IDV Rejected (Non-Fraud)', 1, 1, 1], - ['IDV Rejected (Fraud)', 0, 1, 2], - ['Blanket Proofing Rate (IDV Started to Successfully Verified)', 1.0 / 4, 2.0 / 5, 3.0 / 6], - ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', 1.0 / 3, 2.0 / 4, 3.0 / 5], - ['Actual Proofing Rate (Image Submitted to Successfully Verified)', 1.0 / 2, 2.0 / 3, 3.0 / 4], - ['Industry Proofing Rate (Verified minus IDV Rejected)', 1.0 / 2, 2.0 / 3, 3.0 / 4], + ['Metric', 'Trailing 30d'], + ['Start Date', Date.new(2021, 12, 2)], + ['End Date', Date.new(2022, 1, 1)], + ['IDV Started', 4], + ['Welcome Submitted', 3], + ['Image Submitted', 2], + ['Socure', 0], + ['Successfully Verified', 1], + ['IDV Rejected (Non-Fraud)', 1], + ['IDV Rejected (Fraud)', 0], + ['Blanket Proofing Rate (IDV Started to Successfully Verified)', 1.0 / 4], + ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', 1.0 / 3], + ['Actual Proofing Rate (Image Submitted to Successfully Verified)', 1.0 / 2], + ['Industry Proofing Rate (Verified minus IDV Rejected)', 1.0 / 2], ] - # rubocop:enable Layout/LineLength - aggregate_failures do report.as_csv.zip(expected_csv).each do |actual, expected| expect(actual).to eq(expected) @@ -141,8 +108,6 @@ expect(report.reports.map(&:time_range)).to eq( [ (end_date - 30.days).beginning_of_day..end_date, - (end_date - 60.days).beginning_of_day..end_date, - (end_date - 90.days).beginning_of_day..end_date, ], ) @@ -152,17 +117,17 @@ cloudwatch_client: report.cloudwatch_client, ).once - expect(Reporting::IdentityVerificationReport).to have_received(:new).with( + expect(Reporting::IdentityVerificationReport).not_to have_received(:new).with( time_range: (end_date - 60.days).beginning_of_day..(end_date - 30.days).end_of_day, issuers: nil, cloudwatch_client: report.cloudwatch_client, - ).once + ) - expect(Reporting::IdentityVerificationReport).to have_received(:new).with( + expect(Reporting::IdentityVerificationReport).not_to have_received(:new).with( time_range: (end_date - 90.days).beginning_of_day..(end_date - 60.days).end_of_day, issuers: nil, cloudwatch_client: report.cloudwatch_client, - ).once + ) end end end diff --git a/spec/mailers/previews/report_mailer_preview.rb b/spec/mailers/previews/report_mailer_preview.rb index 3f007743850..8bab8a7940e 100644 --- a/spec/mailers/previews/report_mailer_preview.rb +++ b/spec/mailers/previews/report_mailer_preview.rb @@ -109,17 +109,17 @@ def fraud_metrics_report ) end - def irs_authentication_report - irs_authentication_report = Reports::IrsAuthenticationReport.new(Time.zone.yesterday) + def irs_registration_funnel_report + irs_registration_funnel_report = Reports::IrsRegistrationFunnelReport.new(Time.zone.yesterday) - stub_cloudwatch_client(irs_authentication_report.irs_authentication_report) + stub_cloudwatch_client(irs_registration_funnel_report.irs_registration_funnel_report) ReportMailer.tables_report( email: 'test@example.com', - subject: "Example IRS Authentication Report - #{Time.zone.now.to_date}", - message: irs_authentication_report.preamble, + subject: "Example IRS Registration Funnel Report - #{Time.zone.now.to_date}", + message: irs_registration_funnel_report.preamble, attachment_format: :csv, - reports: irs_authentication_report.reports, + reports: irs_registration_funnel_report.reports, ) end diff --git a/spec/views/idv/phone_errors/warning.html.erb_spec.rb b/spec/views/idv/phone_errors/warning.html.erb_spec.rb index ec058ce9b7a..c53b25e898d 100644 --- a/spec/views/idv/phone_errors/warning.html.erb_spec.rb +++ b/spec/views/idv/phone_errors/warning.html.erb_spec.rb @@ -17,6 +17,7 @@ assign(:remaining_submit_attempts, remaining_submit_attempts) assign(:country_code, country_code) assign(:phone, phone) + assign(:formatted_phone, formatted_phone) render end @@ -26,7 +27,9 @@ end it 'shows number entered' do - expect(rendered).to have_text(t('idv.failure.phone.warning.you_entered')) + expect(rendered).to have_content( + strip_tags(t('idv.failure.phone.warning.you_entered_html', formatted_phone: formatted_phone)), + ) expect(rendered).to have_text(formatted_phone) end @@ -104,7 +107,11 @@ context 'no phone' do let(:phone) { nil } it 'does not render "You entered:"' do - expect(rendered).not_to have_text(t('idv.failure.phone.warning.you_entered')) + expect(rendered).not_to have_content( + strip_tags( + t('idv.failure.phone.warning.you_entered_html', formatted_phone: formatted_phone), + ), + ) end end end