18F/identity-idp

View on GitHub
app/services/flow/flow_state_machine.rb

Summary

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

module Flow
  module FlowStateMachine
    extend ActiveSupport::Concern
    include OptInHelper

    included do
      before_action :initialize_flow_state_machine
      before_action :ensure_correct_step, only: :show
    end

    attr_accessor :flow

    def index
      redirect_to_step(next_step)
    end

    def show
      track_step_visited
      render_step(current_step, flow.flow_session)
    end

    def update
      step = current_step

      return render_not_found unless flow.step_handler_instance(step).present?

      result = flow.handle(step)

      analytics.public_send(
        flow.step_handler_instance(step).analytics_submitted_event,
        **result.to_h.merge(analytics_properties),
      )

      register_update_step(step, result)
      if flow.json
        render json: flow.json, status: flow.http_status
        return
      end
      flow_finish and return unless next_step
      render_update(step, result)
    end

    def poll_with_meta_refresh(seconds)
      @meta_refresh = seconds
    end

    private

    def current_step
      params[:step]&.underscore
    end

    def track_step_visited
      analytics.public_send(
        flow.step_handler(current_step).analytics_visited_event,
        **analytics_properties,
      )

      Funnel::DocAuth::RegisterStep.new(user_id, issuer).call(current_step, :view, true)
    end

    def user_id
      current_user ? current_user.id : user_id_from_token
    end

    def user_id_from_token
      current_session[:doc_capture_user_id]
    end

    def register_update_step(step, result)
      Funnel::DocAuth::RegisterStep.new(user_id, issuer).call(step, :update, result.success?)
    end

    def issuer
      sp_session[:issuer]
    end

    def initialize_flow_state_machine
      klass = self.class
      flow = klass::FLOW_STATE_MACHINE_SETTINGS[:flow]
      @name = klass.name.underscore.gsub('_controller', '')
      @namespace = flow.name.split('::').first.underscore
      @step_url = klass::FLOW_STATE_MACHINE_SETTINGS[:step_url]
      @final_url = klass::FLOW_STATE_MACHINE_SETTINGS[:final_url]
      @analytics_id = klass::FLOW_STATE_MACHINE_SETTINGS[:analytics_id]
      @view = klass::FLOW_STATE_MACHINE_SETTINGS[:view]

      current_session[@name] ||= {}
      @flow = flow.new(self, current_session, @name)
    end

    def render_update(step, result)
      redirect_to next_step and return if next_step_is_url
      move_to_next_step and return if result.success?
      ensure_correct_step and return
      set_error_and_render(step, result)
    end

    def set_error_and_render(step, result)
      flow_session = flow.flow_session
      flow_session[:error_message] = result.first_error_message
      render_step(step, flow_session)
    end

    def move_to_next_step
      current_session[@name] = flow.flow_session
      redirect_to_step(next_step)
    end

    def render_step(step, flow_session)
      @params = params
      @request = request
      return if call_optional_show_step(step)
      step_params = flow.extra_view_variables(step)
      local_params = step_params.merge(
        flow_namespace: @namespace,
        flow_session: flow_session,
        step_indicator: step_indicator_params,
        step_template: "#{@view || @name}/#{step}",
      )
      render template: 'layouts/flow_step', locals: local_params
    end

    def call_optional_show_step(optional_step)
      return unless @flow.class.const_defined?(:OPTIONAL_SHOW_STEPS)
      optional_show_step = @flow.class::OPTIONAL_SHOW_STEPS.with_indifferent_access[optional_step]
      return unless optional_show_step
      result = optional_show_step.new(@flow).base_call

      optional_show_step_name = optional_show_step.to_s.demodulize.underscore
      optional_properties = result.to_h.merge(
        step: optional_show_step_name,
        analytics_id: @analytics_id,
      )

      analytics.public_send(
        optional_show_step.analytics_optional_step_event,
        **optional_properties,
      )

      if next_step.to_s != optional_step
        if next_step_is_url
          redirect_to next_step
        else
          redirect_to_step(next_step)
        end
        return true
      end
      false
    end

    def step_indicator_params
      return if !flow.class.const_defined?(:STEP_INDICATOR_STEPS)
      handler = flow.step_handler(current_step)
      return if !handler || !handler.const_defined?(:STEP_INDICATOR_STEP)
      {
        steps: flow.class::STEP_INDICATOR_STEPS,
        current_step: handler::STEP_INDICATOR_STEP,
      }
    end

    def ensure_correct_step
      redirect_to_step(next_step) if next_step.to_s != current_step
    end

    def flow_finish
      redirect_to send(@final_url)
    end

    def redirect_to_step(step)
      flow_finish and return unless next_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
      {
        flow_path: flow.flow_path,
        step: current_step,
        analytics_id: @analytics_id,
      }.merge(flow.extra_analytics_properties).
        merge(**opt_in_analytics_properties)
    end

    def current_step_name
      "#{current_step}_#{action_name}"
    end

    def next_step
      flow.next_step
    end

    def next_step_is_url
      next_step.to_s.index(':')
    end

    def current_session
      user_session || session
    end
  end
end

# sample usage:
#
# class FooController
#   include Flow::FlowStateMachine
#
#   FLOW_STATE_MACHINE_SETTINGS = {
#     step_url: :foo_step_url,
#     final_url: :after_foo_url,
#     flow: FooFlow,
#     analytics_id: Analytics::FOO,
#   }.freeze
# end