18F/identity-idp

View on GitHub
app/controllers/idv/phone_controller.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

module Idv
  class PhoneController < ApplicationController
    include Idv::AvailabilityConcern
    include IdvStepConcern
    include StepIndicatorConcern
    include PhoneOtpRateLimitable
    include PhoneOtpSendable
    include Idv::VerifyByMailConcern

    attr_reader :idv_form

    before_action :confirm_not_rate_limited_for_phone_address_verification, except: [:new]
    before_action :confirm_step_allowed
    before_action :set_idv_form

    def new
      flash.keep(:success) if should_keep_flash_success?
      analytics.idv_phone_use_different(step: params[:step]) if params[:step]

      async_state = step.async_state

      # It's possible that create redirected here after a success and left the
      # rate_limiter maxed out. Check for success before checking rate_limiter.
      return async_state_done(async_state) if async_state.done?

      render 'shared/wait' and return if async_state.in_progress?

      return if confirm_not_rate_limited_for_phone_address_verification

      if async_state.none?
        Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer).
          call(:verify_phone, :view, true)

        analytics.idv_phone_of_record_visited(
          **ab_test_analytics_buckets,
        )
        render(
          :new, locals: { gpo_letter_available: gpo_verify_by_mail_policy.send_letter_available? }
        )
      elsif async_state.missing?
        analytics.proofing_address_result_missing
        flash.now[:error] = I18n.t('idv.failure.timeout')
        render(
          :new, locals: { gpo_letter_available: gpo_verify_by_mail_policy.send_letter_available? }
        )
      end
    end

    def create
      clear_future_steps!
      idv_session.invalidate_phone_step!
      result = idv_form.submit(step_params)
      Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer).
        call(:verify_phone, :update, result.success?)

      analytics.idv_phone_confirmation_form_submitted(**result.to_h, **ab_test_analytics_buckets)
      if result.success?
        submit_proofing_attempt
        redirect_to idv_phone_path
      else
        flash.now[:error] = result.first_error_message
        render(
          :new, locals: { gpo_letter_available: gpo_verify_by_mail_policy.send_letter_available? }
        )
      end
    end

    def self.step_info
      Idv::StepInfo.new(
        key: :phone,
        controller: self,
        action: :new,
        next_steps: [:otp_verification],
        preconditions: ->(idv_session:, user:) do
          idv_session.verify_info_step_complete? && !idv_session.verify_by_mail?
        end,
        undo_step: ->(idv_session:, user:) do
          idv_session.vendor_phone_confirmation = nil
          idv_session.address_verification_mechanism = nil
          idv_session.idv_phone_step_document_capture_session_uuid = nil
          idv_session.user_phone_confirmation_session = nil
          idv_session.previous_phone_step_params = nil
        end,
      )
    end

    private

    def rate_limiter
      @rate_limiter ||= RateLimiter.new(user: current_user, rate_limit_type: :proof_address)
    end

    def redirect_to_next_step
      if phone_confirmation_required?
        if OutageStatus.new.all_phone_vendor_outage?
          redirect_to vendor_outage_path(from: :idv_phone)
        else
          send_phone_confirmation_otp_and_handle_result
        end
      else
        redirect_to idv_enter_password_url
      end
    end

    def phone_confirmation_required?
      idv_session.user_phone_confirmation != true
    end

    def submit_proofing_attempt
      step.submit(step_params.to_h)
    end

    def send_phone_confirmation_otp_and_handle_result
      save_delivery_preference
      result = send_phone_confirmation_otp
      analytics.idv_phone_confirmation_otp_sent(
        **result.to_h.merge(adapter: Telephony.config.adapter),
      )
      if result.success?
        redirect_to idv_otp_verification_url
      else
        handle_send_phone_confirmation_otp_failure(result)
      end
    end

    def handle_send_phone_confirmation_otp_failure(result)
      if send_phone_confirmation_otp_rate_limited?
        handle_too_many_otp_sends
      else
        invalid_phone_number(result.extra[:telephony_response].error)
      end
    end

    def handle_proofing_failure
      redirect_to failure_url(step.failure_reason)
    end

    def step_name
      :phone
    end

    def step
      @step ||= Idv::PhoneStep.new(
        idv_session: idv_session,
        trace_id: amzn_trace_id,
        analytics: analytics,
      )
    end

    def step_params
      params.require(:idv_phone_form).permit(:phone, :international_code, :otp_delivery_preference)
    end

    def set_idv_form
      @idv_form = Idv::PhoneForm.new(
        user: current_user,
        previous_params: idv_session.previous_phone_step_params,
        allowed_countries:
          PhoneNumberCapabilities::ADDRESS_IDENTITY_PROOFING_SUPPORTED_COUNTRY_CODES,
        failed_phone_numbers: idv_session.failed_phone_step_numbers,
        hybrid_handoff_phone_number: idv_session.phone_for_mobile_flow,
      )
    end

    def failure_url(reason)
      case reason
      when :warning
        idv_phone_errors_warning_url
      when :timeout
        idv_phone_errors_timeout_url
      when :jobfail
        idv_phone_errors_jobfail_url
      when :fail
        idv_phone_errors_failure_url
      end
    end

    def async_state_done(async_state)
      form_result = step.async_state_done(async_state)

      analytics.idv_phone_confirmation_vendor_submitted(
        **form_result.to_h.merge(
          pii_like_keypaths: [
            [:errors, :phone],
            [:context, :stages, :address],
          ],
          new_phone_added: new_phone_added?,
          hybrid_handoff_phone_used: hybrid_handoff_phone_used?,
        ),
        **opt_in_analytics_properties,
      )

      if form_result.success?
        redirect_to_next_step
      else
        handle_proofing_failure
      end
    end

    def is_req_from_frontend?
      request.headers['HTTP_X_FORM_STEPS_WAIT'] == '1'
    end

    def is_req_from_verify_step?
      request.referer == idv_verify_info_url
    end

    def should_keep_flash_success?
      is_req_from_frontend? && is_req_from_verify_step?
    end

    def new_phone_added?
      context = MfaContext.new(current_user)
      configured_phones = context.phone_configurations.map(&:phone).map do |number|
        PhoneFormatter.format(number)
      end
      !configured_phones.include?(formatted_previous_phone_step_params_phone)
    end

    def hybrid_handoff_phone_used?
      formatted_previous_phone_step_params_phone ==
        PhoneFormatter.format(idv_session.phone_for_mobile_flow)
    end

    def formatted_previous_phone_step_params_phone
      PhoneFormatter.format(
        idv_session.previous_phone_step_params&.fetch('phone'),
      )
    end

    # Migrated from otp_delivery_method_controller
    def otp_sent_tracker_error(result)
      if send_phone_confirmation_otp_rate_limited?
        { rate_limited: true }
      else
        { telephony_error: result.extra[:telephony_response]&.error&.friendly_message }
      end
    end

    # Migrated from otp_delivery_method_controller
    def save_delivery_preference
      original_session = idv_session.user_phone_confirmation_session
      idv_session.user_phone_confirmation_session = Idv::PhoneConfirmationSession.new(
        code: original_session.code,
        phone: original_session.phone,
        sent_at: original_session.sent_at,
        delivery_method: original_session.delivery_method,
        user: current_user,
      )
    end
  end
end