department-of-veterans-affairs/caseflow

View on GitHub
app/models/appeal_series.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

class AppealSeries < CaseflowRecord
  has_many :appeals, class_name: "LegacyAppeal", dependent: :nullify

  # Timeliness is returned as a range of integer months from 50 to 84.1%tile.
  # TODO: Replace these hardcoded values with dynamic data
  SOC_TIMELINESS           = [10, 26].freeze # 75%tile = 20
  SSOC_TIMELINESS          = [5, 13].freeze  # 75%tile = 10
  CERTIFICATION_TIMELINESS = [2, 8].freeze   # 75%tile = 5
  DECISION_TIMELINESS      = [1, 2].freeze   # 75%tile = 1
  REMAND_TIMELINESS        = [16, 29].freeze # 75%tile = 25
  REMAND_SSOC_TIMELINESS   = [3, 11].freeze  # 75%tile = 9
  RETURN_TIMELINESS        = [1, 2].freeze   # 75%tile = 2

  delegate :vacols_id,
           :active?,
           :type_code,
           :representative_name,
           :aod,
           :ramp_election,
           :eligible_for_ramp?,
           :form9_date,
           to: :latest_appeal

  def vacols_ids
    appeals.map(&:vacols_id)
  end

  def latest_appeal
    @latest_appeal ||= fetch_latest_appeal
  end

  def api_sort_key
    earliest_nod = appeals.map(&:nod_date).compact.min
    earliest_nod ? earliest_nod.in_time_zone.to_f : Float::INFINITY
  end

  def location
    %w[Advance Remand].include?(latest_appeal.status) ? :aoj : :bva
  end

  def program
    programs = appeals.flat_map { |appeal| appeal.issues.map(&:program) }.reject(&:nil?).uniq

    (programs.length > 1) ? :multiple : programs.first
  end

  def aoj
    appeals.lazy.flat_map(&:issues).map(&:aoj).find { |aoj| !aoj.nil? } || :other
  end

  def status
    @status ||= fetch_status
  end

  def docket
    @docket ||= fetch_docket
  end

  def docket_hash
    docket.try(:to_hash)
  end

  def at_front
    docket.try(:at_front)
  end

  # Appeals from the same series contain many of the same events. We unique them,
  # using the property of AppealEvent that any two events with the same type and
  # date are considered equal.
  def events
    appeals.flat_map(&:events).uniq.sort_by(&:date)
  end

  def alerts
    @alerts ||= AppealSeriesAlerts.new(appeal_series: self).all
  end

  def issues
    @issues ||= AppealSeriesIssues.new(appeal_series: self).all
  end

  def description
    ordered_issues = latest_appeal.issues
      .select(&:codes?)
      .sort_by(&:vacols_sequence_id)
      .partition(&:diagnostic_code).flatten

    return "VA needs to record issues" if ordered_issues.empty?

    marquee_issue_description = ordered_issues.first.friendly_description_without_new_material

    return marquee_issue_description if issues.length == 1

    comma = (marquee_issue_description.count(",") > 0) ? "," : ""
    issue_count = issues.count - 1

    "#{marquee_issue_description}#{comma} and #{issue_count} #{'other'.pluralize(issue_count)}"
  end

  # rubocop:disable Metrics/CyclomaticComplexity
  def fetch_status
    case latest_appeal.status
    when "Advance"
      disambiguate_status_advance
    when "Active"
      disambiguate_status_active
    when "Complete"
      disambiguate_status_complete
    when "Remand"
      disambiguate_status_remand
    when "Motion"
      :motion
    when "CAVC"
      :cavc
    end
  end

  # rubocop:disable Metrics/MethodLength
  def fetch_details_for_status
    case status
    when :scheduled_hearing
      hearing = latest_appeal.scheduled_hearings.min_by(&:scheduled_for)

      {
        date: hearing.scheduled_for.to_date,
        type: hearing.readable_request_type.downcase,
        location: hearing.request_type_location
      }
    when :pending_hearing_scheduling
      { type: latest_appeal.current_hearing_request_type }
    when :pending_form9, :pending_certification, :pending_certification_ssoc
      {
        last_soc_date: last_soc_date,
        certification_timeliness: CERTIFICATION_TIMELINESS.dup,
        ssoc_timeliness: SSOC_TIMELINESS.dup
      }
    when :pending_soc
      { soc_timeliness: SOC_TIMELINESS.dup }
    when :at_vso
      { vso_name: representative_name }
    when :decision_in_progress
      { decisionTimeliness: DECISION_TIMELINESS.dup }
    when :remand
      {
        issues: issues_for_last_decision,
        remand_timeliness: REMAND_TIMELINESS.dup
      }
    when :remand_ssoc
      {
        last_soc_date: last_soc_date,
        return_timeliness: RETURN_TIMELINESS.dup,
        remand_ssoc_timeliness: REMAND_SSOC_TIMELINESS.dup
      }
    when :bva_decision
      { issues: issues_for_last_decision }
    else
      {}
    end
  end
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength

  private

  def fetch_latest_appeal
    latest_active_appeal_by_last_location_change_date || latest_appeal_by_decision_date
  end

  def latest_active_appeal_by_last_location_change_date
    appeals.select(&:active?).max_by(&:last_location_change_date)
  end

  def latest_appeal_by_decision_date
    # explicit cast to_i to allow for nil comparison with Time object
    appeals.max_by { |appeal| appeal.decision_date.to_i }
  end

  def fetch_docket
    return unless active? && %w[original post_remand].include?(type_code) && form9_date && !aod

    DocketSnapshot.latest.docket_tracer_for_form9_date(form9_date)
  end

  def last_soc_date
    events.reverse.detect { |event| [:soc, :ssoc].include? event.type }.date.to_date
  end

  def issues_for_last_decision
    latest_appeal.issues.select { |issue| [:allowed, :remanded, :denied].include? issue.disposition }.map do |issue|
      {
        description: issue.friendly_description,
        disposition: issue.disposition
      }
    end
  end

  # rubocop:disable Metrics/CyclomaticComplexity
  def disambiguate_status_advance
    if latest_appeal.certification_date
      return :scheduled_hearing if latest_appeal.hearing_scheduled?
      return :pending_hearing_scheduling if latest_appeal.hearing_pending?

      return :on_docket
    end

    if latest_appeal.form9_date
      return :pending_certification_ssoc if latest_appeal.ssoc_dates.present?

      return :pending_certification
    end

    return :pending_form9 if latest_appeal.soc_date

    :pending_soc
  end

  def disambiguate_status_active
    return :scheduled_hearing if latest_appeal.hearing_scheduled?

    case latest_appeal.location_code
    when "49"
      :stayed
    when "55"
      :at_vso
    when "19", "20"
      :bva_development
    when "14", "16", "18", "24"
      latest_appeal.case_assignment_exists ? :bva_development : :on_docket
    else
      latest_appeal.case_assignment_exists ? :decision_in_progress : :on_docket
    end
  end

  # rubocop:disable  Metrics/MethodLength
  def disambiguate_status_complete
    case latest_appeal.disposition
    when "Allowed", "Denied"
      :bva_decision
    when "Advance Allowed in Field", "Benefits Granted by AOJ"
      :field_grant
    when "Withdrawn", "Advance Withdrawn by Appellant/Rep",
         "Recon Motion Withdrawn", "Withdrawn from Remand"
      :withdrawn
    when "Advance Failure to Respond", "Remand Failure to Respond"
      :ftr
    when "RAMP Opt-in"
      :ramp
    when "AMA SOC/SSOC Opt-in"
      :statutory_opt_in
    when "Dismissed, Death", "Advance Withdrawn Death of Veteran"
      :death
    when "Reconsideration by Letter"
      :reconsideration
    when "Merged Appeal"
      :merged
    else
      :other_close
    end
  end
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength

  def disambiguate_status_remand
    post_decision_ssocs = latest_appeal.ssoc_dates&.select { |ssoc| ssoc > latest_appeal.decision_date }
    return :remand_ssoc if !post_decision_ssocs.empty?

    :remand
  end
end