diff --git a/app/controllers/idv/in_person/address_controller.rb b/app/controllers/idv/in_person/address_controller.rb index b451f3e5a62..91eed3491e8 100644 --- a/app/controllers/idv/in_person/address_controller.rb +++ b/app/controllers/idv/in_person/address_controller.rb @@ -109,7 +109,11 @@ def redirect_to_next_page def confirm_in_person_state_id_step_complete return if pii_from_user&.has_key?(:identity_doc_address1) - redirect_to idv_in_person_step_url(step: :state_id) + if IdentityConfig.store.in_person_state_id_controller_enabled + redirect_to idv_in_person_proofing_state_id_url + else + redirect_to idv_in_person_step_url(step: :state_id) + end end def confirm_in_person_address_step_needed diff --git a/app/controllers/idv/in_person/state_id_controller.rb b/app/controllers/idv/in_person/state_id_controller.rb new file mode 100644 index 00000000000..e4fc9e1fa1f --- /dev/null +++ b/app/controllers/idv/in_person/state_id_controller.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Idv + module InPerson + class StateIdController < ApplicationController + include Idv::AvailabilityConcern + include IdvStepConcern + + before_action :render_404_if_controller_not_enabled + before_action :redirect_unless_enrollment # confirm previous step is complete + + def show + flow_session[:pii_from_user] ||= {} + analytics.idv_in_person_proofing_state_id_visited(**analytics_arguments) + + render :show, locals: extra_view_variables + end + + def extra_view_variables + { + form:, + pii:, + parsed_dob:, + updating_state_id: updating_state_id?, + } + end + + private + + def render_404_if_controller_not_enabled + render_not_found unless + IdentityConfig.store.in_person_state_id_controller_enabled + end + + def redirect_unless_enrollment + redirect_to idv_document_capture_url unless current_user.establishing_in_person_enrollment + end + + def flow_session + user_session.fetch('idv/in_person', {}) + end + + def analytics_arguments + { + flow_path: idv_session.flow_path, + step: 'state_id', + analytics_id: 'In Person Proofing', + irs_reproofing: irs_reproofing?, + }.merge(ab_test_analytics_buckets). + merge(extra_analytics_properties) + end + + def updating_state_id? + flow_session[:pii_from_user].has_key?(:first_name) + end + + def parsed_dob + form_dob = pii[:dob] + if form_dob.instance_of?(String) + dob_str = form_dob + elsif form_dob.instance_of?(Hash) + dob_str = MemorableDateComponent.extract_date_param(form_dob) + end + Date.parse(dob_str) unless dob_str.nil? + rescue StandardError + # Catch date parsing errors + end + + def pii + data = flow_session[:pii_from_user] + data = data.merge(flow_params) if params.has_key?(:state_id) + data.deep_symbolize_keys + end + + def flow_params + params.require(:state_id).permit( + *Idv::StateIdForm::ATTRIBUTES, + dob: [ + :month, + :day, + :year, + ], + ) + end + + def form + @form ||= Idv::StateIdForm.new(current_user) + end + end + end + end diff --git a/app/services/flow/flow_state_machine.rb b/app/services/flow/flow_state_machine.rb index 496467167a5..074f08bcf97 100644 --- a/app/services/flow/flow_state_machine.rb +++ b/app/services/flow/flow_state_machine.rb @@ -173,7 +173,15 @@ def flow_finish def redirect_to_step(step) flow_finish and return unless next_step - redirect_to send(@step_url, step: step) + redirect_url(step) + end + + def redirect_url(step) + if IdentityConfig.store.in_person_state_id_controller_enabled + redirect_to idv_in_person_proofing_state_id_url + else + redirect_to send(@step_url, step: step) + end end def analytics_properties diff --git a/app/views/idv/in_person/state_id/show.html.erb b/app/views/idv/in_person/state_id/show.html.erb new file mode 100644 index 00000000000..fa3a4b8fccd --- /dev/null +++ b/app/views/idv/in_person/state_id/show.html.erb @@ -0,0 +1,244 @@ +<% content_for(:pre_flash_content) do %> + <%= render StepIndicatorComponent.new( + steps: Idv::Flows::InPersonFlow::STEP_INDICATOR_STEPS, + current_step: :verify_info, + locale_scope: 'idv', + class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', + ) %> +<% end %> +<% if updating_state_id %> + <% self.title = t('in_person_proofing.headings.update_state_id') %> + <%= render PageHeadingComponent.new.with_content(t('in_person_proofing.headings.update_state_id')) %> +<% else %> + <% self.title = t('in_person_proofing.headings.state_id_milestone_2') %> + <%= render PageHeadingComponent.new.with_content(t('in_person_proofing.headings.state_id_milestone_2')) %> +<% end %> + +

+ <%= t('in_person_proofing.body.state_id.info_html') %> +

+ +<%= render AlertComponent.new( + type: :info, + class: 'margin-bottom-4', + text_tag: 'div', + ) do %> + <%= t('in_person_proofing.body.state_id.alert_message') %> + +

+ <%= t('in_person_proofing.body.state_id.questions') %> + <%= link_to( + MarketingSite.help_center_article_url( + category: 'verify-your-identity', + article: 'accepted-state-issued-identification', + ), + class: 'display-inline', + ) do %> + <%= t('in_person_proofing.body.state_id.learn_more_link') %> + <% end %> +

+<% end %> +<%= simple_form_for form, + url: url_for, + method: 'put', + html: { autocomplete: 'off', class: 'margin-y-5' } do |f| %> + +
+ <%= render ValidatedFieldComponent.new( + name: :first_name, + form: f, + input_html: { value: pii[:first_name] }, + label: t('in_person_proofing.form.state_id.first_name'), + label_html: { class: 'usa-label' }, + maxlength: 255, + required: true, + ) %> +
+ +
+ <%= render ValidatedFieldComponent.new( + name: :last_name, + form: f, + input_html: { value: pii[:last_name] }, + label: t('in_person_proofing.form.state_id.last_name'), + label_html: { class: 'usa-label' }, + maxlength: 255, + required: true, + ) %> +
+ +
+ <%= render MemorableDateComponent.new( + content_tag: 'memorable-date', + name: :dob, + day: parsed_dob&.day, + month: parsed_dob&.month, + year: parsed_dob&.year, + required: true, + min: '1900-01-01', + max: Time.zone.today, + hint: t('in_person_proofing.form.state_id.dob_hint'), + label: t('in_person_proofing.form.state_id.dob'), + form: f, + error_messages: { + missing_month_day_year: t('in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.missing_month_day_year'), + range_overflow: t('in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_overflow'), + }, + range_errors: [ + { + max: Time.zone.today - 13.years, + message: t( + 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age', + app_name: APP_NAME, + ), + }, + ], + ) + %> +
+ +
+ <%= render ValidatedFieldComponent.new( + name: :state_id_jurisdiction, + collection: us_states_territories, + form: f, + hint: t('in_person_proofing.form.state_id.state_id_jurisdiction_hint'), + input_html: { class: 'jurisdiction-state-selector' }, + label: t('in_person_proofing.form.state_id.state_id_jurisdiction'), + label_html: { class: 'usa-label' }, + prompt: t('in_person_proofing.form.state_id.state_id_jurisdiction_prompt'), + required: true, + selected: pii[:state_id_jurisdiction], + ) %> +
+
+ <% state_id_number_hint_default = capture do %> + <%= t('in_person_proofing.form.state_id.state_id_number_hint') %> + <% [ + [t('in_person_proofing.form.state_id.state_id_number_hint_spaces'), ' '], + [t('in_person_proofing.form.state_id.state_id_number_hint_forward_slashes'), '/'], + [t('in_person_proofing.form.state_id.state_id_number_hint_asterisks'), '*'], + [t('in_person_proofing.form.state_id.state_id_number_hint_dashes'), '-', true], + ].each do |text, symbol, last| %> + <%= text %><%= ',' if !last %> + + <% end %> + <% end %> + <% state_id_number_hint = capture do %> + <% [ + [:default, state_id_number_hint_default], + ['FL', t('in_person_proofing.form.state_id.state_id_number_florida_hint_html')], + ['TX', t('in_person_proofing.form.state_id.state_id_number_texas_hint')], + ].each do |state, hint| %> + <%= content_tag( + :span, + hint, + class: state == :default ? nil : 'display-none', + data: { state: }, + ) %> + <% end %> + <% end %> + <%= render ValidatedFieldComponent.new( + name: :state_id_number, + form: f, + hint: state_id_number_hint, + hint_html: { class: ['tablet:grid-col-10', 'jurisdiction-extras'] }, + input_html: { value: pii[:state_id_number] }, + label: t('in_person_proofing.form.state_id.state_id_number'), + label_html: { class: 'usa-label' }, + maxlength: 255, + required: true, + ) %> +
+ +

<%= t('in_person_proofing.headings.id_address') %>

+ <%= render ValidatedFieldComponent.new( + name: :identity_doc_address_state, + collection: us_states_territories, + form: f, + input_html: { class: 'address-state-selector' }, + label: t('in_person_proofing.form.state_id.identity_doc_address_state'), + label_html: { class: 'usa-label' }, + prompt: t('in_person_proofing.form.state_id.identity_doc_address_state_prompt'), + required: true, + selected: pii[:identity_doc_address_state], + ) %> + <%= render ValidatedFieldComponent.new( + name: :identity_doc_address1, + form: f, + hint_html: { class: ['display-none', 'puerto-rico-extras'] }, + hint: t('in_person_proofing.form.state_id.address1_hint'), + input_html: { value: pii[:identity_doc_address1] }, + label: t('in_person_proofing.form.state_id.address1'), + label_html: { class: 'usa-label' }, + maxlength: 255, + required: true, + ) %> + <%= render ValidatedFieldComponent.new( + name: :identity_doc_address2, + form: f, + hint: t('in_person_proofing.form.state_id.address2_hint'), + hint_html: { class: ['display-none', 'puerto-rico-extras'] }, + input_html: { value: pii[:identity_doc_address2] }, + label: t('in_person_proofing.form.state_id.address2'), + label_html: { class: 'usa-label' }, + maxlength: 255, + required: false, + ) %> + <%= render ValidatedFieldComponent.new( + name: :identity_doc_city, + form: f, + input_html: { value: pii[:identity_doc_city] }, + label: t('in_person_proofing.form.state_id.city'), + label_html: { class: 'usa-label' }, + maxlength: 255, + required: true, + ) %> +
+ <%# using :tel for mobile numeric keypad %> + <%= render ValidatedFieldComponent.new( + as: :tel, + error_messages: { patternMismatch: t('idv.errors.pattern_mismatch.zipcode') }, + form: f, + input_html: { value: pii[:identity_doc_zipcode], class: 'zipcode' }, + label: t('in_person_proofing.form.state_id.zipcode'), + label_html: { class: 'usa-label' }, + name: :identity_doc_zipcode, + pattern: '\d{5}([\-]\d{4})?', + required: true, + ) %> +
+ <%= render ValidatedFieldComponent.new( + as: :radio_buttons, + checked: pii[:same_address_as_id], + collection: [ + [t('in_person_proofing.form.state_id.same_address_as_id_yes'), true], + [t('in_person_proofing.form.state_id.same_address_as_id_no'), false], + ], + form: f, + label: t('in_person_proofing.form.state_id.same_address_as_id'), + legend_html: { class: 'h2' }, + name: :same_address_as_id, + required: true, + wrapper: :uswds_radio_buttons, + ) %> + <%= f.submit do %> + <% if updating_state_id %> + <%= t('forms.buttons.submit.update') %> + <% else %> + <%= t('forms.buttons.continue') %> + <% end %> + <% end %> +<% end %> +<% if updating_state_id %> + <%= render 'idv/shared/back', action: 'cancel_update_state_id' %> +<% else %> + <%= render 'idv/doc_auth/cancel', step: 'state_id' %> +<% end %> +<%= javascript_packs_tag_once('formatted-fields', 'state-guidance') %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index be4a54a2ac5..2f79a9ea44b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -397,6 +397,7 @@ # during the deprecation process. get '/in_person_proofing/address' => redirect('/verify/in_person/address', status: 307) put '/in_person_proofing/address' => redirect('/verify/in_person/address', status: 307) + get '/in_person_proofing/state_id' => 'in_person/state_id#show' get '/in_person' => 'in_person#index' get '/in_person/ready_to_verify' => 'in_person/ready_to_verify#show', diff --git a/spec/controllers/idv/in_person/address_controller_spec.rb b/spec/controllers/idv/in_person/address_controller_spec.rb index bf5644c2e7f..df06ee89e2f 100644 --- a/spec/controllers/idv/in_person/address_controller_spec.rb +++ b/spec/controllers/idv/in_person/address_controller_spec.rb @@ -27,11 +27,27 @@ describe 'before_actions' do context '#confirm_in_person_state_id_step_complete' do - it 'redirects to state id page if not complete' do - subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1) - get :show + context 'in_person_state_id_controller_enabled is enabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled). + and_return(true) + end - expect(response).to redirect_to idv_in_person_step_url(step: :state_id) + it 'redirects to state id page if not complete' do + subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1) + get :show + + expect(response).to redirect_to idv_in_person_proofing_state_id_url + end + end + + context 'in_person_state_id_controller_enabled is not enabled' do + it 'redirects to state id page if not complete' do + subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1) + get :show + + expect(response).to redirect_to idv_in_person_step_url(step: :state_id) + end end end diff --git a/spec/controllers/idv/in_person/state_id_controller_spec.rb b/spec/controllers/idv/in_person/state_id_controller_spec.rb new file mode 100644 index 00000000000..5bd6d2babd3 --- /dev/null +++ b/spec/controllers/idv/in_person/state_id_controller_spec.rb @@ -0,0 +1,115 @@ +require 'rails_helper' + +RSpec.describe Idv::InPerson::StateIdController do + include FlowPolicyHelper + include InPersonHelper + + let(:user) { build(:user) } + let(:enrollment) { InPersonEnrollment.new } + + let(:ab_test_args) do + { sample_bucket1: :sample_value1, sample_bucket2: :sample_value2 } + end + + before do + allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled). + and_return(true) + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled). + and_return(true) + stub_sign_in(user) + stub_up_to(:hybrid_handoff, idv_session: subject.idv_session) + allow(user).to receive(:establishing_in_person_enrollment).and_return(enrollment) + subject.user_session['idv/in_person'] = { pii_from_user: {} } + subject.idv_session.ssn = nil # This made specs pass. Might need more investigation. + stub_analytics + allow(@analytics).to receive(:track_event) + allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) + end + + describe 'before_actions' do + context '#render_404_if_controller_not_enabled' do + context 'flag not set' do + before do + allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled). + and_return(nil) + end + it 'renders a 404' do + get :show + + expect(response).to be_not_found + end + end + + context 'flag not enabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled). + and_return(false) + end + it 'renders a 404' do + get :show + + expect(response).to be_not_found + end + end + end + + context '#confirm_establishing_enrollment' do + let(:enrollment) { nil } + it 'redirects to document capture if not complete' do + get :show + + expect(response).to redirect_to idv_document_capture_url + end + end + end + + describe '#show' do + let(:analytics_name) { 'IdV: in person proofing state_id visited' } + let(:analytics_args) do + { + analytics_id: 'In Person Proofing', + flow_path: 'standard', + irs_reproofing: false, + opted_in_to_in_person_proofing: nil, + step: 'state_id', + pii_like_keypaths: [[:same_address_as_id], + [:proofing_results, :context, :stages, :state_id, + :state_id_jurisdiction]], + }.merge(ab_test_args) + end + + it 'renders the show template' do + get :show + + expect(response).to render_template :show + end + + context 'pii_from_user is nil' do + it 'renders the show template' do + subject.user_session['idv/in_person'].delete(:pii_from_user) + get :show + + expect(response).to render_template :show + end + end + + it 'logs idv_in_person_proofing_state_id_visited' do + get :show + + expect(@analytics).to have_received( + :track_event, + ).with(analytics_name, analytics_args) + end + + it 'has correct extra_view_variables' do + expect(subject.extra_view_variables).to include( + form: Idv::StateIdForm, + updating_state_id: false, + ) + + expect(subject.extra_view_variables[:pii]).to_not have_key( + :address1, + ) + end + end +end