ministryofjustice/Claim-for-Crown-Court-Defence

View on GitHub
app/controllers/external_users/claims_controller.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
97%
module ExternalUsers
  class ClaimsController < ExternalUsers::ApplicationController
    # This performs magic
    include PaginationHelpers
    include MultiparameterAttributeCleaner

    class ResourceClassNotDefined < StandardError; end

    skip_load_and_authorize_resource

    helper_method :sort_column, :sort_direction, :scheme

    respond_to :html

    prepend_before_action :clean_multiparameter_dates, only: %i[create update]
    before_action :set_user_and_provider
    before_action :set_presenters
    before_action :set_claims_context, only: %i[index archived outstanding authorised]
    before_action :set_financial_summary, only: %i[index outstanding authorised]

    before_action :set_and_authorize_claim, only: %i[show edit update unarchive clone_rejected destroy summary
                                                     confirmation show_message_controls messages]
    before_action :set_supplier_postcode, only: %i[edit update]
    before_action :set_form_step, only: %i[edit update]
    before_action :redirect_unless_editable, only: %i[edit update]
    before_action :generate_form_id, only: %i[new edit]
    before_action :initialize_submodel_counts

    include ReadMessages
    include MessageControlsDisplay

    before_action :enable_breadcrumb, only: %i[new create edit update]

    def index
      track_visit(url: 'external_user/claims', title: 'Your claims')

      @claims = @claims_context
                .dashboard_displayable_states
                .includes(:defendants, :case_type, :external_user, :assessment, :messages, :determinations)
      search if params[:search].present?
      sort_and_paginate(column: 'last_submitted_at', direction: 'asc')
    end

    def archived
      @claims = @claims_context.where(state: %w[archived_pending_delete archived_pending_review])
      search(:archived_pending_delete) if params[:search].present?
      sort_and_paginate(column: 'last_submitted_at', direction: 'desc')
    end

    def outstanding
      @claims = @financial_summary
                .outstanding_claims
                .includes(:defendants, :case_type, :external_user, :assessment, :messages, :determinations)
      sort_and_paginate(column: 'last_submitted_at', direction: 'asc')
      @total_value = @financial_summary.total_outstanding_claim_value
    end

    def authorised
      @claims = @financial_summary.authorised_claims
      sort_and_paginate(column: 'last_submitted_at', direction: 'desc')
      @total_value = @financial_summary.total_authorised_claim_value
    end

    def messages
      render template: 'messages/claim_messages'
    end

    def show
      @messages = @claim.messages.most_recent_last
      @message = @claim.messages.build

      track_visit({
                    url: 'external_user/%{type}/claim/show',
                    title: 'Show %{type} claim details'
                  }, claim_tracking_substitutions)
    end

    def summary
      track_visit({
                    url: 'external_user/%{type}/claim/%{action}/summary',
                    title: '%{action_t} %{type} claim summary'
                  }, claim_tracking_substitutions)
    end

    def confirmation
      track_visit({
                    url: 'external_user/%{type}/claim/%{action}/confirmation',
                    title: '%{action_t} %{type} claim confirmation'
                  }, claim_tracking_substitutions)
    end

    def clone_rejected
      draft = nil
      Timeout.timeout(20) do
        draft = claim_updater.clone_rejected
      end
      log('Redraft succeeded')
      redirect_to edit_polymorphic_path(draft), notice: t('external_users.claims.redraft.success')
    rescue StandardError => e
      log('Redraft failed', level: :error, error: e)
      redirect_to external_users_claims_url, alert: t('external_users.claims.redraft.error_html').html_safe
    end

    def new
      @claim = resource_klass.new
      @claim.form_step = params[:step] || @claim.submission_stages.first
      authorize! :new, @claim
      load_offences_and_case_types
      build_nested_resources
      track_visit({ url: 'external_user/%{type}/claim/new/%{step}', title: 'New %{type} claim %{step}' },
                  claim_tracking_substitutions)
    end

    def edit
      build_nested_resources
      load_offences_and_case_types
      @disable_assessment_input = true

      @claim.touch(:last_edited_at)

      track_visit({ url: 'external_user/%{type}/claim/edit/%{step}', title: 'Edit %{type} claim %{step}' },
                  claim_tracking_substitutions)
    end

    def create
      @claim = resource_klass.new(params_with_external_user_and_creator)
      @claim.form_step ||= @claim.submission_stages.first
      authorize! :create, @claim
      result = if submitting_to_laa?
                 Claims::CreateClaim.call(@claim)
               else
                 Claims::CreateDraft.call(@claim, validate: continue_claim?)
               end

      render_or_redirect(result)
    end

    def update
      result = if submitting_to_laa?
                 Claims::UpdateClaim.call(@claim, params: claim_params)
               else
                 Claims::UpdateDraft.call(@claim, params: claim_params, validate: continue_claim?)
               end

      render_or_redirect(result)
    end

    def destroy
      message = if @claim.draft?
                  flash_message_for :delete, claim_updater.delete
                elsif @claim.can_archive_pending_delete? || @claim.can_archive_pending_review?
                  flash_message_for :archive, claim_updater.archive
                else
                  { alert: 'This claim cannot be deleted' }
                end
      flash[message.keys.first.to_sym] = message.values.first
      respond_with @claim, location: external_users_claims_url
    end

    def unarchive
      claim_url = external_users_claim_url(@claim)
      return redirect_to claim_url, alert: t('.not_archived') unless unarchive_allowed?
      @claim = PreviousVersionOfClaim.new(@claim).call
      @claim.zeroise_nil_totals!
      @claim.save!(validate: false)
      redirect_to external_users_claims_url, notice: t('.unarchived')
    rescue StandardError
      redirect_to claim_url, alert: t('.unarchivable')
    end

    class << self
      def resource_klass(klass)
        @resource_klass ||= klass
      end

      def defined_resource_class
        @resource_klass || raise(ResourceClassNotDefined)
      end
    end

    protected

    def resource_klass
      self.class.defined_resource_class
    end

    private

    def log(message, error: nil, level: :info)
      log_data = {
        action: 'clone', claim_id: @claim.id, documents: @claim.documents.count,
        total_size: helpers.number_to_human_size(@claim.documents.sum { |doc| doc.document.byte_size })
      }
      if error
        log_data[:error] = "#{error.class}: #{error.message}"
        log_data[:backtrace] = error.backtrace
      end
      LogStuff.send(level, 'ExternalUsers::ClaimsController', **log_data) { message }
    end

    def generate_form_id
      @form_id = SecureRandom.uuid
    end

    def load_offences_and_case_types
      @offences = Claims::FetchEligibleOffences.for(@claim)
      @offence_descriptions = Offence.unique_name
      @case_types = @claim.respond_to?(:eligible_case_types) ? @claim&.eligible_case_types : []
    end

    def set_user_and_provider
      @external_user = current_user.persona
      @provider = @external_user.provider
    end

    def set_presenters
      @defendant_presenter = ExternalUsers::DefendantPresenter
    end

    def set_claims_context
      context = Claims::ContextMapper.new(@external_user, scheme:)
      @claims_context = context.available_claims
      @available_schemes = context.available_schemes
    end

    def set_financial_summary
      @financial_summary = Claims::FinancialSummary.new(@claims_context)
    end

    def search(states = nil)
      @claims = @claims.search(params[:search], states, *search_options)
    end

    def search_options
      options = %i[case_number defendant_name]
      options << :advocate_name if @external_user.admin?
      options
    end

    def sort_column
      @claims.sortable_by?(params[:sort]) ? params[:sort] : @sort_defaults[:column]
    end

    def sort_direction
      %w[asc desc].include?(params[:direction]) ? params[:direction] : @sort_defaults[:direction]
    end

    def sort_defaults(defaults = {})
      @sort_defaults = {  column: defaults.fetch(:column, 'last_submitted_at'),
                          direction: defaults.fetch(:direction, 'asc'),
                          pagination: defaults.fetch(:pagination, page_size) }
    end

    def sort_and_paginate(options = {})
      sort_defaults(options)
      @claims = @claims.sort_using(sort_column, sort_direction).page(current_page).per(@sort_defaults[:pagination])
    end

    def scheme
      %w[agfs lgfs].include?(params[:scheme]) ? params[:scheme].to_sym : :all
    end

    def set_and_authorize_claim
      @claim = Claim::BaseClaim.active.find(params[:id])
      authorize! params[:action].to_sym, @claim
    end

    def set_supplier_postcode
      @supplier_postcode = SupplierNumber.find_by(supplier_number: @claim&.supplier_number)&.postcode
    end

    def set_form_step
      return unless @claim
      @claim.form_step = params[:step] ||
                         (params.key?(:claim) && claim_params[:form_step]) ||
                         @claim.submission_stages.first
    end

    # rubocop:disable Metrics/MethodLength
    def claim_params
      params.require(:claim).permit(
        :form_id,
        :form_step,
        :advocate_category,
        :providers_ref,
        :source,
        :external_user_id,
        :supplier_number,
        :court_id,
        :case_transferred_from_another_court,
        :transfer_court_id,
        :transfer_case_number,
        :case_number,
        :case_type_id,
        :case_stage_id,
        :offence_id,
        :travel_expense_additional_information,
        :first_day_of_trial,
        :estimated_trial_length,
        :actual_trial_length,
        :trial_concluded_at,
        :retrial_started_at,
        :retrial_estimated_length,
        :retrial_actual_length,
        :main_hearing_date,
        :retrial_concluded_at,
        :retrial_reduction,
        :trial_fixed_notice_at,
        :trial_fixed_at,
        :trial_cracked_at,
        :case_concluded_at,
        :effective_pcmh_date,
        :legal_aid_transfer_date,
        :trial_cracked_at_third,
        :additional_information,
        :litigator_type,
        :elected_case,
        :transfer_stage_id,
        :transfer_date,
        :case_conclusion_id,
        :disk_evidence,
        :prosecution_evidence,
        evidence_checklist_ids: [],
        defendants_attributes: [
          :id,
          :claim_id,
          :first_name,
          :last_name,
          :date_of_birth,
          :order_for_judicial_apportionment,
          :_destroy,
          { representation_orders_attributes: %i[
            id
            document
            maat_reference
            representation_order_date
            _destroy
          ] }
        ],
        basic_fees_attributes: [
          :id,
          :claim_id,
          :fee_type_id,
          :fee_id,
          :quantity,
          :rate,
          :amount,
          :case_numbers,
          :price_calculated,
          :_destroy,
          common_dates_attended_attributes
        ],
        disbursements_attributes: %i[
          id
          claim_id
          disbursement_type_id
          net_amount
          vat_amount
          _destroy
        ],
        fixed_fees_attributes: common_fees_attributes, # agfs has_many
        fixed_fee_attributes: common_fees_attributes, # lgfs has_one
        misc_fees_attributes: common_fees_attributes,
        graduated_fee_attributes: %i[
          id
          claim_id
          fee_type_id
          quantity
          amount
          price_calculated
          date
        ],
        interim_fee_attributes: %i[
          id
          claim_id
          fee_type_id
          quantity
          amount
          price_calculated
          warrant_issued_date
          warrant_executed_date
        ],
        transfer_fee_attributes: %i[
          id
          claim_id
          fee_type_id
          amount
          price_calculated
          quantity
        ],
        warrant_fee_attributes: %i[
          id
          claim_id
          fee_type_id
          amount
          price_calculated
          warrant_issued_date
          warrant_executed_date
        ],
        hardship_fee_attributes: %i[
          id
          claim_id
          fee_type_id
          amount
          price_calculated
          quantity
        ],
        expenses_attributes: [
          :id,
          :claim_id,
          :expense_type_id,
          :location,
          :location_type,
          :quantity,
          :amount,
          :vat_amount,
          :rate,
          :reason_id,
          :reason_text,
          :distance,
          :calculated_distance,
          :mileage_rate_id,
          :hours,
          :date,
          :_destroy,
          common_dates_attended_attributes
        ],
        interim_claim_info_attributes: %i[
          warrant_fee_paid
          warrant_issued_date
          warrant_executed_date
        ]
      )
    end
    # rubocop:enable Metrics/MethodLength

    def params_with_external_user_and_creator
      form_params = claim_params
      form_params[:external_user_id] ||= @external_user.id
      form_params[:creator_id] = @external_user.id
      form_params
    end

    def build_nested_resources
      %i[defendants documents].each do |association|
        build_nested_resource(@claim, association)
      end

      @claim.defendants.each { |d| build_nested_resource(d, :representation_orders) }
    end

    def build_nested_resource(object, association)
      object.send(association).build if object.send(association).none?
    end

    def submitting_to_laa?
      params.key?(:commit_submit_claim)
    end

    def continue_claim?
      params.key?(:commit_continue)
    end

    def render_action_with_resources(action)
      present_errors
      build_nested_resources
      load_offences_and_case_types

      track_visit({
                    url: 'external_user/%{type}/claim/%{action}/%{step}',
                    title: '%{action_t} %{type} claim page %{step}'
                  }, claim_tracking_substitutions)

      render action:
    end

    def redirect_to_next_step
      track_visit({
                    url: 'external_user/%{type}/claim/%{action}/%{step}',
                    title: '%{action_t} %{type} claim page %{step}'
                  }, claim_tracking_substitutions)

      redirect_to edit_polymorphic_path(@claim, step: @claim.next_step)
    end

    def render_or_redirect(result)
      return render_or_redirect_error(result) unless result.success?
      render_or_redirect_success(result)
    end

    def render_or_redirect_success(result)
      if continue_claim?
        redirect_to_next_step
      elsif result.draft?
        redirect_to external_users_claims_path, notice: t('external_users.claims.update.success')
      else
        redirect_to summary_external_users_claim_url(@claim)
      end
    end

    def render_or_redirect_error(result)
      case result.error_code
      when :already_submitted
        redirect_to external_users_claims_path, alert: t('errors.already_submitted', scope: default_scope)
      when :already_saved
        redirect_to external_users_claims_path, alert: t('errors.already_saved', scope: default_scope)
      else # rollback done, show errors
        render_action_with_resources(result.action)
      end
    end

    def present_errors
      @error_presenter = ErrorMessage::Presenter.new(@claim)
    end

    def initialize_submodel_counts
      @defendant_count                = 0
      @representation_order_count     = 0
      @basic_fee_count                = 0
      @misc_fee_count                 = 0
      @fixed_fee_count                = 0
      @expense_count                  = 0
      @expense_date_attended_count    = 0
      @disbursement_count             = 0
    end

    def claim_tracking_substitutions
      {
        type: @claim.pretty_type,
        step: @claim.current_step,
        action: @claim.edition_state,
        action_t: @claim.edition_state.titleize
      }
    end

    def redirect_unless_editable
      return if @claim.current_step_editable?
      error_code = @claim.editable? ? :dependencies_missing : :not_editable
      options = redirect_options_for(error_code)
      redirect_to options[:url], alert: options[:message]
    end

    def redirect_options_for(error_code)
      {
        not_editable: {
          url: external_users_claims_url,
          message: t('errors.not_editable', scope: default_scope)
        },
        dependencies_missing: {
          url: summary_external_users_claim_path(@claim),
          message: t('errors.dependencies_missing', scope: default_scope)
        }
      }[error_code&.to_sym]
    end

    def default_scope
      %i[external_users claims]
    end

    def flash_message_for(event, status)
      status ? { notice: "Claim #{event}d" } : { alert: "Claim could not be #{event}d" }
    end

    def unarchive_allowed?
      @claim.archived_pending_delete? || @claim.archived_pending_review?
    end
  end
end