Skip to content

Commit

Permalink
Merge branch 'master' into feature/APPEALS-32376
Browse files Browse the repository at this point in the history
  • Loading branch information
ThorntonMatthew authored Feb 12, 2024
2 parents 99f4731 + 78da891 commit ec38118
Show file tree
Hide file tree
Showing 41 changed files with 1,148 additions and 127 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Style/StringLiterals:

Style/FrozenStringLiteralComment:
Enabled: true
Exclude:
- 'client/constants/*'

Style/Documentation:
Enabled: false
Expand Down
47 changes: 47 additions & 0 deletions app/jobs/ineligible_judges_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

# A scheduled job that caches the list of ineligible judges within Caseflow and Vacols for Case Distribution use.
# This job is ran once a week, with a cache that lasts a week.
class IneligibleJudgesJob < CaseflowJob
# For time_ago_in_words()
include ActionView::Helpers::DateHelper
queue_with_priority :low_priority
application_attr :queue

def perform
@start_time ||= Time.zone.now
case_distribution_ineligible_judges

log_success(@start_time)
rescue StandardError => error
log_error(error)
end

private

# {Grabs both vacols and caseflow ineligible judges then merges into one list with duplicates merged
# if they have the same CSS_ID/SDOMAINID}
def case_distribution_ineligible_judges
# delete the cache key to ensure the cache will be updated when the job runs even if less than 1 week has passed
Rails.cache.delete("case_distribution_ineligible_judges")

Rails.cache.fetch("case_distribution_ineligible_judges", expires_in: 1.week) do
[*CaseDistributionIneligibleJudges.vacols_judges_with_caseflow_records,
*CaseDistributionIneligibleJudges.caseflow_judges_with_vacols_records]
.group_by { |h| h[:sdomainid] || h[:css_id] }
.flat_map do |k, v|
next v unless k

v.reduce(&:merge)
end
end
end

def log_success(start_time)
duration = time_ago_in_words(start_time)
msg = "#{self.class.name} completed after running for #{duration}."
Rails.logger.info(msg)

slack_service.send_notification("[INFO] #{msg}", self.class.to_s) # may not need this
end
end
6 changes: 3 additions & 3 deletions app/jobs/push_priority_appeals_to_judges_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ def send_job_report
def slack_report # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
report = []
if use_by_docket_date?
total_cases = @genpop_distributions.map(&:distributed_batch_size).sum
total_cases = @genpop_distributions.map(&:distributed_cases_count).sum
report << "*Number of cases distributed*: " \
"#{total_cases}"
else
tied_distributions_sum = @tied_distributions.map(&:distributed_batch_size).sum
genpop_distributions_sum = @genpop_distributions.map(&:distributed_batch_size).sum
tied_distributions_sum = @tied_distributions.map(&:distributed_cases_count).sum
genpop_distributions_sum = @genpop_distributions.map(&:distributed_cases_count).sum
report << "*Number of cases tied to judges distributed*: " \
"#{tied_distributions_sum}"
report << "*Number of general population cases distributed*: " \
Expand Down
43 changes: 42 additions & 1 deletion app/models/concerns/by_docket_date_distribution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ def requested_distribution

unless FeatureToggle.enabled?(:acd_disable_nonpriority_distributions, user: RequestStore.store[:current_user])
# Distribute the oldest nonpriority appeals from any docket if we haven't distributed {batch_size} appeals
distribute_nonpriority_appeals_from_all_dockets_by_age_to_limit(@rem) until @rem <= 0
# @nonpriority_iterations guards against an infinite loop if not enough cases are ready to distribute
until @rem <= 0 || @nonpriority_iterations >= batch_size
distribute_nonpriority_appeals_from_all_dockets_by_age_to_limit(@rem)
end
end
@appeals
end
Expand All @@ -50,6 +53,7 @@ def distribute_nonpriority_appeals_from_all_dockets_by_age_to_limit(limit, style
end
end

# rubocop:disable Metrics/MethodLength
def ama_statistics
priority_counts = { count: priority_count }
nonpriority_counts = { count: nonpriority_count }
Expand Down Expand Up @@ -78,9 +82,45 @@ def ama_statistics
priority_target: @push_priority_target || @request_priority_count,
priority: priority_counts,
nonpriority: nonpriority_counts,
distributed_cases_tied_to_ineligible_judges: {
ama: ama_distributed_cases_tied_to_ineligible_judges,
legacy: distributed_cases_tied_to_ineligible_judges
},
algorithm: "by_docket_date",
settings: settings
}
rescue StandardError => error
# There always needs to be a batch_size value for a completed distribution, else the priority push job will error
{
batch_size: @appeals.count,
message: "Distribution successful, but there was an error generating statistics: \
#{error.class}: #{error.message}, #{error.backtrace.first}"
}
end
# rubocop:enable Metrics/MethodLength

def ama_distributed_cases_tied_to_ineligible_judges
@appeals.filter_map do |appeal|
appeal[:case_id] if HearingRequestDistributionQuery.ineligible_judges_id_cache
&.include?(hearing_judge_id(appeal))
end
end

def distributed_cases_tied_to_ineligible_judges
@appeals.filter_map do |appeal|
appeal[:case_id] if Rails.cache.fetch("case_distribution_ineligible_judges")&.pluck(:sattyid)&.reject(&:blank?)
&.include?(hearing_judge_id(appeal))
end
end

def hearing_judge_id(appeal)
if appeal[:docket] == "legacy"
user_id = LegacyAppeal.find_by(vacols_id: appeal[:case_id])
&.hearings&.select(&:held?)&.max_by(&:scheduled_for)&.judge_id
VACOLS::Staff.find_by_sdomainid(User.find_by_id(user_id)&.css_id)&.sattyid
else
Appeal.find_by(uuid: appeal[:case_id])&.hearings&.select(&:held?)&.max_by(&:scheduled_for)&.judge_id
end
end

def num_oldest_priority_appeals_for_judge_by_docket(distribution, num)
Expand Down Expand Up @@ -113,3 +153,4 @@ def num_oldest_nonpriority_appeals_for_judge_by_docket(distribution, num)
.transform_values(&:count)
end
end
# rubocop:enable Metrics/ModuleLength
1 change: 1 addition & 0 deletions app/models/concerns/case_distribution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def docket_coordinator

def collect_appeals
appeals = yield
appeals.compact!
@rem -= appeals.count
@appeals += appeals
appeals
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/distribution_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def assign_judge_tasks_for_appeals(appeals, judge)
# If an appeal does not have an open DistributionTask, then it has already been distributed by automatic
# case distribution and a new JudgeAssignTask should not be created. This should only occur if two users
# request a distribution simultaneously.
next nil unless appeal.tasks.open.of_type(:DistributionTask).any?
next nil unless appeal.tasks.open.of_type(:DistributionTask).any? && appeal.can_redistribute_appeal?

distribution_task_assignee_id = appeal.tasks.of_type(:DistributionTask).first.assigned_to_id
Rails.logger.info("Calling JudgeAssignTaskCreator for appeal #{appeal.id} with judge #{judge.css_id}")
Expand Down
4 changes: 0 additions & 4 deletions app/models/distribution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ def distributed_cases_count
(status == "completed") ? distributed_cases.count : 0
end

def distributed_batch_size
statistics&.fetch("batch_size", 0) || 0
end

private

def mark_as_pending
Expand Down
10 changes: 9 additions & 1 deletion app/models/dockets/hearing_request_docket.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true


class HearingRequestDocket < Docket
def docket_type
Constants.AMA_DOCKETS.hearing
Expand Down Expand Up @@ -51,6 +50,8 @@ def distribute_appeals(distribution, priority: false, genpop: "any", limit: 1, s

appeals = self.class.limit_genpop_appeals(appeals, limit) if genpop.eql? "any"

appeals = self.class.limit_only_genpop_appeals(appeals, limit) if genpop.eql?("only_genpop") && limit

HearingRequestCaseDistributor.new(
appeals: appeals, genpop: genpop, distribution: distribution, priority: priority
).call
Expand All @@ -72,4 +73,11 @@ def self.limit_genpop_appeals(appeals_array, limit)
appeals_to_reject = appeals_array.flatten.sort_by(&:ready_for_distribution_at).drop(limit)
appeals_array.map { |appeals| appeals - appeals_to_reject }
end

def self.limit_only_genpop_appeals(appeals_array, limit)
# genpop 'only_genpop' returns 2 arrays of the limited base relation. This means if we only request 2 cases, appeals is a
# 2x2 array containing 4 cases overall and we will end up distributing 4 cases rather than 2. Instead, reinstate the
# limit here by filtering out the newest cases
appeals_array.flatten.sort_by(&:receipt_date).first(limit)
end
end
91 changes: 66 additions & 25 deletions app/models/vacols/case_docket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,15 @@ def self.age_of_n_oldest_priority_appeals_available_to_judge(judge, num)

query = <<-SQL
#{SELECT_PRIORITY_APPEALS_ORDER_BY_BFD19}
where (VLJ = ? or VLJ is null)
where (VLJ = ? or #{ineligible_judges_sattyid_cache} or VLJ is null)
and rownum <= ?
SQL

fmtd_query = sanitize_sql_array([query, judge.vacols_attorney_id, num])
fmtd_query = sanitize_sql_array([
query,
judge.vacols_attorney_id,
num
])

appeals = conn.exec_query(fmtd_query).to_hash
appeals.map { |appeal| appeal["bfd19"] }
Expand All @@ -304,11 +308,15 @@ def self.age_of_n_oldest_nonpriority_appeals_available_to_judge(judge, num)

query = <<-SQL
#{SELECT_NONPRIORITY_APPEALS_ORDER_BY_BFD19}
where (VLJ = ? or VLJ is null)
where (VLJ = ? or #{ineligible_judges_sattyid_cache} or VLJ is null)
and rownum <= ?
SQL

fmtd_query = sanitize_sql_array([query, judge.vacols_attorney_id, num])
fmtd_query = sanitize_sql_array([
query,
judge.vacols_attorney_id,
num
])

appeals = conn.exec_query(fmtd_query).to_hash
appeals.map { |appeal| appeal["bfd19"] }
Expand Down Expand Up @@ -348,35 +356,41 @@ def self.nonpriority_decisions_per_year
def self.priority_hearing_cases_for_judge_count(judge)
query = <<-SQL
#{SELECT_PRIORITY_APPEALS}
where (VLJ = ?)
where (VLJ = ? or #{ineligible_judges_sattyid_cache})
SQL

fmtd_query = sanitize_sql_array([query, judge.vacols_attorney_id])
fmtd_query = sanitize_sql_array([
query,
judge.vacols_attorney_id
])
connection.exec_query(fmtd_query).count
end

def self.nonpriority_hearing_cases_for_judge_count(judge)
query = <<-SQL
#{SELECT_NONPRIORITY_APPEALS}
where (VLJ = ?)
where (VLJ = ? or #{ineligible_judges_sattyid_cache})
SQL

fmtd_query = sanitize_sql_array([query, judge.vacols_attorney_id])
fmtd_query = sanitize_sql_array([
query,
judge.vacols_attorney_id
])
connection.exec_query(fmtd_query).count
end

def self.priority_ready_appeal_vacols_ids
connection.exec_query(SELECT_PRIORITY_APPEALS).to_hash.map { |appeal| appeal["bfkey"] }
end

# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists, Metrics/MethodLength
def self.distribute_nonpriority_appeals(judge, genpop, range, limit, bust_backlog, dry_run = false)
fail(DocketNumberCentennialLoop, COPY::MAX_LEGACY_DOCKET_NUMBER_ERROR_MESSAGE) if Time.zone.now.year >= 2030

if use_by_docket_date?
query = <<-SQL
#{SELECT_NONPRIORITY_APPEALS_ORDER_BY_BFD19}
where ((VLJ = ? and 1 = ?) or (VLJ is null and 1 = ?))
where (((VLJ = ? or #{ineligible_judges_sattyid_cache}) and 1 = ?) or (VLJ is null and 1 = ?))
and (DOCKET_INDEX <= ? or 1 = ?)
and rownum <= ?
SQL
Expand All @@ -396,7 +410,7 @@ def self.distribute_nonpriority_appeals(judge, genpop, range, limit, bust_backlo

query = <<-SQL
#{SELECT_NONPRIORITY_APPEALS}
where ((VLJ = ? and 1 = ?) or (VLJ is null and 1 = ?))
where (((VLJ = ? or #{ineligible_judges_sattyid_cache}) and 1 = ?) or (VLJ is null and 1 = ?))
and (DOCKET_INDEX <= ? or 1 = ?)
and rownum <= ?
SQL
Expand All @@ -414,22 +428,21 @@ def self.distribute_nonpriority_appeals(judge, genpop, range, limit, bust_backlo

distribute_appeals(fmtd_query, judge, dry_run)
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists

def self.distribute_priority_appeals(judge, genpop, limit, dry_run = false)
if use_by_docket_date?
query = <<-SQL
#{SELECT_PRIORITY_APPEALS_ORDER_BY_BFD19}
where ((VLJ = ? and 1 = ?) or (VLJ is null and 1 = ?))
and (rownum <= ? or 1 = ?)
SQL
else
query = <<-SQL
#{SELECT_PRIORITY_APPEALS}
where ((VLJ = ? and 1 = ?) or (VLJ is null and 1 = ?))
and (rownum <= ? or 1 = ?)
SQL
end
query = if use_by_docket_date?
<<-SQL
#{SELECT_PRIORITY_APPEALS_ORDER_BY_BFD19}
where (((VLJ = ? or #{ineligible_judges_sattyid_cache}) and 1 = ?) or (VLJ is null and 1 = ?))
and (rownum <= ? or 1 = ?)
SQL
else
<<-SQL
#{SELECT_PRIORITY_APPEALS}
where (((VLJ = ? or #{ineligible_judges_sattyid_cache}) and 1 = ?) or (VLJ is null and 1 = ?))
and (rownum <= ? or 1 = ?)
SQL
end

fmtd_query = sanitize_sql_array([
query,
Expand All @@ -442,6 +455,7 @@ def self.distribute_priority_appeals(judge, genpop, limit, dry_run = false)

distribute_appeals(fmtd_query, judge, dry_run)
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists, Metrics/MethodLength

# :nocov:

Expand Down Expand Up @@ -472,4 +486,31 @@ def self.distribute_appeals(query, judge, dry_run)
def self.use_by_docket_date?
FeatureToggle.enabled?(:acd_distribute_by_docket_date, user: RequestStore.store[:current_user])
end

def self.ineligible_judges_sattyid_cache # rubocop:disable Metrics/MethodLength
if FeatureToggle.enabled?(:acd_cases_tied_to_judges_no_longer_with_board) &&
!Rails.cache.fetch("case_distribution_ineligible_judges")&.pluck(:sattyid)&.reject(&:blank?).blank?
list = Rails.cache.fetch("case_distribution_ineligible_judges")&.pluck(:sattyid)&.reject(&:blank?)
split_lists = {}
num_of_lists = (list.size.to_f / 999).ceil

num_of_lists.times do |num|
split_lists[num] = []
999.times do
split_lists[num] << list.shift
end
split_lists[num].compact!
end

vljs_strings = split_lists.flat_map do |k, v|
base = "(#{v.join(', ')})"
base += " or VLJ in " unless k == split_lists.keys.last
base
end

"VLJ in #{vljs_strings.join}"
else
"VLJ = 'false'"
end
end
end
Loading

0 comments on commit ec38118

Please sign in to comment.