diff --git a/app/controllers/case_distribution_levers_tests_controller.rb b/app/controllers/case_distribution_levers_tests_controller.rb
index b13763b071d..221c8411164 100644
--- a/app/controllers/case_distribution_levers_tests_controller.rb
+++ b/app/controllers/case_distribution_levers_tests_controller.rb
@@ -40,6 +40,13 @@ def run_demo_docket_priority
head :ok
end
+ def run_demo_non_avlj_appeals
+ Rake::Task["db:seed:non_ssc_avlj_legacy_appeals"].reenable
+ Rake::Task["db:seed:non_ssc_avlj_legacy_appeals"].invoke
+
+ head :ok
+ end
+
def appeals_ready_to_distribute
csv_data = AppealsReadyForDistribution.process
@@ -66,6 +73,15 @@ def appeals_non_priority_ready_to_distribute
send_data csv_data, filename: filename
end
+ def run_return_legacy_appeals_to_board
+ result = ReturnLegacyAppealsToBoardJob.perform_now(params[:fail_job])
+ if params[:fail_job] && result.include?("Job failed with error")
+ return render json: { error: result }, status: :unprocessable_entity
+ end
+
+ head :ok
+ end
+
def appeals_distributed
# change this to the correct class
csv_data = AppealsDistributed.process
@@ -80,6 +96,20 @@ def appeals_distributed
send_data csv_data, filename: filename
end
+ def appeals_in_location_63_in_past_2_days
+ # change this to the correct class
+ csv_data = AppealsInLocation63InPast2Days.process
+
+ # Get the current date and time for dynamic filename
+ current_datetime = Time.zone.now.strftime("%Y%m%d-%H%M")
+
+ # Set dynamic filename with current date and time
+ filename = "appeals_in_location_63_past_2_days_#{current_datetime}.csv"
+
+ # Send CSV as a response with dynamic filename
+ send_data csv_data, filename: filename
+ end
+
def ineligible_judge_list
# change this to the correct class
csv_data = IneligibleJudgeList.process
@@ -94,6 +124,19 @@ def ineligible_judge_list
send_data csv_data, filename: filename
end
+ def appeals_tied_to_non_ssc_avlj
+ csv_data = AppealsTiedToNonSscAvljQuery.process
+
+ # Get the current date and time for dynamic filename
+ current_datetime = Time.zone.now.strftime("%Y%m%d-%H%M")
+
+ # Set dynamic filename with current date and time
+ filename = "appeals_tied_to_non_ssc_avljs_#{current_datetime}.csv"
+
+ # Send CSV as a response with dynamic filename
+ send_data csv_data, filename: filename
+ end
+
private
def check_environment
diff --git a/app/jobs/push_priority_appeals_to_judges_job.rb b/app/jobs/push_priority_appeals_to_judges_job.rb
index 7907a9eca08..8a1e8cfff68 100644
--- a/app/jobs/push_priority_appeals_to_judges_job.rb
+++ b/app/jobs/push_priority_appeals_to_judges_job.rb
@@ -20,6 +20,7 @@ def perform
@genpop_distributions = distribute_genpop_priority_appeals
perform_later_or_now(UpdateAppealAffinityDatesJob)
+ perform_later_or_now(ReturnLegacyAppealsToBoardJob)
slack_service.send_notification(generate_report.join("\n"), self.class.name)
rescue StandardError => error
diff --git a/app/jobs/return_legacy_appeals_to_board_job.rb b/app/jobs/return_legacy_appeals_to_board_job.rb
new file mode 100644
index 00000000000..07a45a2cc2b
--- /dev/null
+++ b/app/jobs/return_legacy_appeals_to_board_job.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+class ReturnLegacyAppealsToBoardJob < CaseflowJob
+ # For time_ago_in_words()
+ include ActionView::Helpers::DateHelper
+ # include RunAsyncable
+
+ queue_as :low_priority
+ application_attr :queue
+
+ def perform(fail_job = false)
+ begin
+ returned_appeal_job = create_returned_appeal_job
+ fail if fail_job
+
+ move_qualifying_appeals(LegacyDocket.new.appeals_tied_to_non_ssc_avljs)
+ complete_returned_appeal_job(returned_appeal_job, "Job completed successfully", appeals)
+ send_job_slack_report
+ rescue StandardError => error
+ message = "Job failed with error: #{error.message}"
+ errored_returned_appeal_job(returned_appeal_job, message)
+ start_time ||= Time.zone.now # temporary fix to get this job to succeed
+ duration = time_ago_in_words(start_time)
+ slack_msg = "\n [ERROR] after running for #{duration}: #{error.message}"
+ slack_service.send_notification(slack_msg, self.class.name)
+ log_error(error)
+ message
+ ensure
+ @start_time ||= Time.zone.now
+ metrics_service_report_runtime(metric_group_name: "return_legacy_appeals_to_board_job")
+ end
+ end
+
+ private
+
+ def move_qualifying_appeals(appeals)
+ qualifying_appeals = []
+
+ non_ssc_avljs.each do |non_ssc_avlj|
+ tied_appeals = appeals.select { |appeal| appeal["vlj"] == non_ssc_avlj.sattyid }
+
+ unless tied_appeals.empty?
+ tied_appeals = tied_appeals.sort_by { |t_appeal| [-t_appeal["priority"], t_appeal["bfd19"]] }
+ end
+
+ if appeals.count < 2
+ qualifying_appeals.push(tied_appeals).flatten
+ else
+ qualifying_appeals.push(tied_appeals[0..1]).flatten
+ end
+ end
+
+ unless qualifying_appeals.empty?
+ qualifying_appeals = qualifying_appeals
+ .flatten
+ .sort_by { |appeal| [-appeal["priority"], appeal["bfd19"]] }
+ end
+
+ VACOLS::Case.batch_update_vacols_location("63", qualifying_appeals.map { |q_appeal| q_appeal["bfkey"] })
+ end
+
+ def non_ssc_avljs
+ VACOLS::Staff.where("sactive = 'A' AND svlj = 'A' AND sattyid <> smemgrp")
+ end
+
+ def create_returned_appeal_job
+ ReturnedAppealJob.create!(
+ started_at: Time.zone.now,
+ stats: { message: "Job started" }.to_json
+ )
+ end
+
+ def complete_returned_appeal_job(returned_appeal_job, message, appeals)
+ returned_appeal_job.update!(
+ completed_at: Time.zone.now,
+ stats: { message: message }.to_json,
+ returned_appeals: appeals.map { |appeal| appeal["bfkey"] }
+ )
+ end
+
+ def errored_returned_appeal_job(returned_appeal_job, message)
+ returned_appeal_job.update!(
+ errored_at: Time.zone.now,
+ stats: { message: message }.to_json
+ )
+ end
+
+ def send_job_slack_report
+ slack_service.send_notification(slack_report.join("\n"), self.class.name)
+ end
+
+ def slack_report
+ report = []
+ report << "Job performed successfully"
+ report
+ end
+end
diff --git a/app/models/dockets/legacy_docket.rb b/app/models/dockets/legacy_docket.rb
index 96c3eb30f87..c7e7338cd9d 100644
--- a/app/models/dockets/legacy_docket.rb
+++ b/app/models/dockets/legacy_docket.rb
@@ -14,6 +14,14 @@ def ready_to_distribute_appeals
LegacyAppeal.repository.ready_to_distribute_appeals
end
+ def appeals_tied_to_non_ssc_avljs
+ LegacyAppeal.repository.appeals_tied_to_non_ssc_avljs
+ end
+
+ def loc_63_appeals
+ LegacyAppeal.repository.loc_63_appeals
+ end
+
# rubocop:disable Metrics/CyclomaticComplexity
def count(priority: nil, ready: nil)
counts_by_priority_and_readiness.inject(0) do |sum, row|
diff --git a/app/models/returned_appeal_job.rb b/app/models/returned_appeal_job.rb
new file mode 100644
index 00000000000..90b3ce5aa15
--- /dev/null
+++ b/app/models/returned_appeal_job.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class ReturnedAppealJob < ApplicationRecord
+end
diff --git a/app/models/vacols/case_docket.rb b/app/models/vacols/case_docket.rb
index 02c0062f55d..9e72d5fcd63 100644
--- a/app/models/vacols/case_docket.rb
+++ b/app/models/vacols/case_docket.rb
@@ -78,7 +78,8 @@ class DocketNumberCentennialLoop < StandardError; end
"
SELECT_READY_APPEALS = "
- select BFKEY, BFD19, BFDLOOUT, BFMPRO, BFCURLOC, BFAC, BFHINES, TINUM, TITRNUM, AOD
+ select BFKEY, BFD19, BFCORLID, BFDLOOUT, BFMPRO, BFCURLOC, BFAC, BFHINES, TINUM, TITRNUM, AOD,
+ BFMEMID, BFDPDCN
#{FROM_READY_APPEALS}
"
@@ -92,7 +93,7 @@ class DocketNumberCentennialLoop < StandardError; end
JOIN_ASSOCIATED_VLJS_BY_HEARINGS = "
left join (
- select distinct TITRNUM, TINUM,
+ select distinct TITRNUM, TINUM, HEARING_DATE,
first_value(BOARD_MEMBER) over (partition by TITRNUM, TINUM order by HEARING_DATE desc) VLJ
from HEARSCHED
inner join FOLDER on FOLDER.TICKNUM = HEARSCHED.FOLDER_NR
@@ -103,6 +104,21 @@ class DocketNumberCentennialLoop < StandardError; end
and (VLJ_HEARINGS.TINUM is null or VLJ_HEARINGS.TINUM = BRIEFF.TINUM)
"
+ # Provide access to legacy appeal decisions for more complete appeals history queries
+ JOIN_PREVIOUS_APPEALS = "
+ left join (
+ select B.BFKEY as PREV_BFKEY, B.BFCORLID as PREV_BFCORLID, B.BFDDEC as PREV_BFDDEC,
+ B.BFMEMID as PREV_DECIDING_JUDGE, B.BFAC as PREV_TYPE_ACTION, F.TINUM as PREV_TINUM,
+ F.TITRNUM as PREV_TITRNUM
+ from BRIEFF B
+ inner join FOLDER F on F.TICKNUM = B.BFKEY
+ where B.BFMPRO = 'HIS' and B.BFMEMID not in ('000', '888', '999') and B.BFATTID is not null
+ ) PREV_APPEAL
+ on PREV_APPEAL.PREV_BFKEY != BRIEFF.BFKEY and PREV_APPEAL.PREV_BFCORLID = BRIEFF.BFCORLID
+ and PREV_APPEAL.PREV_TINUM = BRIEFF.TINUM and PREV_APPEAL.PREV_TITRNUM = BRIEFF.TITRNUM
+ and PREV_APPEAL.PREV_BFDDEC = BRIEFF.BFDPDCN
+ "
+
SELECT_PRIORITY_APPEALS = "
select BFKEY, BFDLOOUT, VLJ
from (
@@ -161,8 +177,28 @@ class DocketNumberCentennialLoop < StandardError; end
)
"
- # this query should not be used during distribution it is only intended for reporting usage
+ # selects both priority and non-priority appeals that are ready to distribute
SELECT_READY_TO_DISTRIBUTE_APPEALS_ORDER_BY_BFD19 = "
+ select APPEALS.BFKEY, APPEALS.TINUM, APPEALS.BFD19, APPEALS.BFDLOOUT,
+ case when APPEALS.BFAC = '7' or APPEALS.AOD = 1 then 1 else 0 end PRIORITY,
+ APPEALS.VLJ, APPEALS.PREV_DECIDING_JUDGE, APPEALS.HEARING_DATE, APPEALS.PREV_BFDDEC
+ from (
+ select BRIEFF.BFKEY, BRIEFF.TINUM, BFD19, BFDLOOUT, BFAC, AOD,
+ case when BFHINES is null or BFHINES <> 'GP' then VLJ_HEARINGS.VLJ end VLJ
+ , PREV_APPEAL.PREV_DECIDING_JUDGE PREV_DECIDING_JUDGE
+ , VLJ_HEARINGS.HEARING_DATE HEARING_DATE
+ , PREV_APPEAL.PREV_BFDDEC PREV_BFDDEC
+ from (
+ #{SELECT_READY_APPEALS}
+ ) BRIEFF
+ #{JOIN_ASSOCIATED_VLJS_BY_HEARINGS}
+ #{JOIN_PREVIOUS_APPEALS}
+ order by BFD19
+ ) APPEALS
+ "
+
+ # this query should not be used during distribution it is only intended for reporting usage
+ SELECT_READY_TO_DISTRIBUTE_APPEALS_ORDER_BY_BFD19_ADDITIONAL_COLS = "
select APPEALS.BFKEY, APPEALS.TINUM, APPEALS.BFD19, APPEALS.BFDLOOUT, APPEALS.AOD, APPEALS.BFCORLID,
CORRES.SNAMEF, CORRES.SNAMEL, CORRES.SSN,
STAFF.SNAMEF as VLJ_NAMEF, STAFF.SNAMEL as VLJ_NAMEL,
@@ -181,7 +217,50 @@ class DocketNumberCentennialLoop < StandardError; end
order by BFD19
"
+ FROM_LOC_63_APPEALS = "
+ from BRIEFF
+ #{VACOLS::Case::JOIN_AOD}
+ inner join FOLDER on FOLDER.TICKNUM = BRIEFF.BFKEY
+ where BRIEFF.BFCURLOC in ('63')
+ and BRIEFF.BFBOX is null
+ and BRIEFF.BFAC is not null
+ and BRIEFF.BFD19 is not null
+ "
+
+ SELECT_LOC_63_APPEALS = "
+ select BFKEY, BFD19, BFDLOCIN, BFCORLID, BFDLOOUT, BFMPRO, BFCORKEY, BFCURLOC, BFAC, BFHINES, TINUM, TITRNUM, AOD,
+ BFMEMID, BFDPDCN
+ #{FROM_LOC_63_APPEALS}
+ "
+
# rubocop:disable Metrics/MethodLength
+ SELECT_APPEALS_IN_LOCATION_63_FROM_PAST_2_DAYS = "
+ select APPEALS.BFKEY, APPEALS.TINUM, APPEALS.BFD19, APPEALS.BFMEMID, APPEALS.BFCURLOC,
+ APPEALS.BFDLOCIN, APPEALS.BFCORLID, APPEALS.BFDLOOUT,
+ case when APPEALS.BFAC = '7' or APPEALS.AOD = 1 then 1 else 0 end AOD,
+ case when APPEALS.BFAC = '7' then 1 else 0 end CAVC,
+ APPEALS.VLJ, APPEALS.PREV_DECIDING_JUDGE, APPEALS.HEARING_DATE, APPEALS.PREV_BFDDEC,
+ CORRES.SNAMEF, CORRES.SNAMEL, CORRES.SSN,
+ STAFF.SNAMEF as VLJ_NAMEF, STAFF.SNAMEL as VLJ_NAMEL
+ from (
+ select BRIEFF.BFKEY, BRIEFF.TINUM, BFD19, BFDLOOUT, BFAC, BFCORKEY, BFMEMID, BFCURLOC,
+ BRIEFF.BFDLOCIN, BFCORLID, AOD,
+ case when BFHINES is null or BFHINES <> 'GP' then VLJ_HEARINGS.VLJ end VLJ
+ , PREV_APPEAL.PREV_DECIDING_JUDGE PREV_DECIDING_JUDGE
+ , VLJ_HEARINGS.HEARING_DATE HEARING_DATE
+ , PREV_APPEAL.PREV_BFDDEC PREV_BFDDEC
+ from (
+ #{SELECT_LOC_63_APPEALS}
+ ) BRIEFF
+ #{JOIN_ASSOCIATED_VLJS_BY_HEARINGS}
+ #{JOIN_PREVIOUS_APPEALS}
+ where BRIEFF.BFDLOCIN >= CURRENT_DATE - 2
+ order by BFD19
+ ) APPEALS
+ left join CORRES on APPEALS.BFCORKEY = CORRES.STAFKEY
+ left join STAFF on APPEALS.VLJ = STAFF.SATTYID
+ "
+
def self.counts_by_priority_and_readiness
query = <<-SQL
select count(*) N, PRIORITY, READY
@@ -416,7 +495,37 @@ def self.priority_ready_appeal_vacols_ids
def self.ready_to_distribute_appeals
query = <<-SQL
+ #{SELECT_READY_TO_DISTRIBUTE_APPEALS_ORDER_BY_BFD19_ADDITIONAL_COLS}
+ SQL
+
+ fmtd_query = sanitize_sql_array([query])
+ connection.exec_query(fmtd_query).to_a
+ end
+
+ def self.loc_63_appeals
+ query = <<-SQL
+ #{SELECT_APPEALS_IN_LOCATION_63_FROM_PAST_2_DAYS}
+ SQL
+
+ fmtd_query = sanitize_sql_array([query])
+ connection.exec_query(fmtd_query).to_a
+ end
+
+ def self.appeals_tied_to_non_ssc_avljs
+ query = <<-SQL
+ with non_ssc_avljs as (
+ #{VACOLS::Staff::NON_SSC_AVLJS}
+ )
#{SELECT_READY_TO_DISTRIBUTE_APPEALS_ORDER_BY_BFD19}
+ where APPEALS.VLJ in (select * from non_ssc_avljs)
+ and (
+ APPEALS.PREV_DECIDING_JUDGE is null or
+ (
+ APPEALS.PREV_DECIDING_JUDGE = APPEALS.VLJ
+ AND APPEALS.HEARING_DATE <= APPEALS.PREV_BFDDEC
+ )
+ )
+ order by BFD19
SQL
fmtd_query = sanitize_sql_array([query])
diff --git a/app/models/vacols/staff.rb b/app/models/vacols/staff.rb
index 3a7bc6100de..32859086865 100644
--- a/app/models/vacols/staff.rb
+++ b/app/models/vacols/staff.rb
@@ -14,6 +14,14 @@ class VACOLS::Staff < VACOLS::Record
scope :judge, -> { pure_judge.or(acting_judge) }
scope :attorney, -> { pure_attorney.or(acting_judge) }
+ NON_SSC_AVLJS = "
+ select sattyid
+ from staff
+ where sattyid <> smemgrp
+ and svlj = 'A'
+ and sactive = 'A'
+ "
+
def self.find_by_css_id(css_id)
find_by(sdomainid: css_id)
end
diff --git a/app/queries/appeals_in_location_63_in_past_2_days.rb b/app/queries/appeals_in_location_63_in_past_2_days.rb
new file mode 100644
index 00000000000..1781e0a8ab5
--- /dev/null
+++ b/app/queries/appeals_in_location_63_in_past_2_days.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+class AppealsInLocation63InPast2Days
+ HEADERS = {
+ docket_number: "Docket Number",
+ aod: "AOD",
+ cavc: "CAVC",
+ receipt_date: "Receipt Date",
+ ready_for_distribution_at: "Ready for Distribution at",
+ veteran_file_number: "Veteran File number",
+ veteran_name: "Veteran",
+ hearing_judge_id: "Most Recent Hearing Judge ID",
+ hearing_judge_name: "Most Recent Hearing Judge Name",
+ deciding_judge_id: "Most Recent Deciding Judge ID",
+ deciding_judge_name: "Most Recent Deciding Judge Name",
+ affinity_start_date: "Affinity Start Date",
+ moved_date_time: "Date/Time Moved",
+ bfcurloc: "BFCURLOC"
+ }.freeze
+
+ def self.generate_rows(record)
+ HEADERS.keys.map { |key| record[key] }
+ end
+
+ def self.process
+ # Convert results to CSV format
+
+ CSV.generate(headers: true) do |csv|
+ # Add headers to CSV
+ csv << HEADERS.values
+
+ # Iterate through results and add each row to CSV
+ loc_63_appeals.each do |record|
+ csv << generate_rows(record)
+ end
+ end
+ end
+
+ def self.loc_63_appeals
+ docket_coordinator = DocketCoordinator.new
+
+ docket_coordinator.dockets
+ .flat_map do |sym, docket|
+ if sym == :legacy
+ appeals = docket.loc_63_appeals
+ legacy_rows(appeals)
+ else
+ []
+ end
+ end
+ end
+
+ def self.legacy_rows(appeals)
+ unsorted_result = appeals.map do |appeal|
+ calculated_values = calculate_field_values(appeal)
+ {
+ docket_number: appeal["tinum"],
+ aod: appeal["aod"] == 1,
+ cavc: appeal["cavc"] == 1,
+ receipt_date: appeal["bfd19"],
+ ready_for_distribution_at: appeal["bfdloout"],
+ veteran_file_number: appeal["ssn"] || appeal["bfcorlid"],
+ veteran_name: calculated_values[:veteran_name],
+ hearing_judge_id: calculated_values[:hearing_judge_id],
+ hearing_judge_name: calculated_values[:hearing_judge_name],
+ deciding_judge_id: calculated_values[:deciding_judge_id],
+ deciding_judge_name: calculated_values[:deciding_judge_name],
+ affinity_start_date: calculated_values[:appeal_affinity]&.affinity_start_date,
+ moved_date_time: appeal["bfdlocin"],
+ bfcurloc: appeal["bfcurloc"]
+ }
+ end
+
+ unsorted_result.sort_by { |appeal| appeal[:moved_date_time] }.reverse
+ end
+
+ def self.calculate_field_values(appeal)
+ vlj_name = FullName.new(appeal["vlj_namef"], nil, appeal["vlj_namel"]).to_s
+ {
+ veteran_name: FullName.new(appeal["snamef"], nil, appeal["snamel"]).to_s,
+ hearing_judge_id: appeal["vlj"].blank? ? nil : legacy_hearing_judge(appeal),
+ hearing_judge_name: vlj_name.empty? ? nil : vlj_name,
+ deciding_judge_id: appeal["prev_deciding_judge"].blank? ? nil : legacy_original_deciding_judge(appeal),
+ deciding_judge_name: appeal["prev_deciding_judge"].blank? ? nil : legacy_original_deciding_judge_name(appeal),
+ appeal_affinity: AppealAffinity.find_by(case_id: appeal["bfkey"], case_type: "VACOLS::Case")
+ }
+ end
+
+ def self.legacy_hearing_judge(appeal)
+ staff = VACOLS::Staff.find_by(sattyid: appeal["vlj"])
+ staff&.sdomainid || appeal["vlj"]
+ end
+
+ def self.legacy_original_deciding_judge(appeal)
+ staff = VACOLS::Staff.find_by(sattyid: appeal["prev_deciding_judge"])
+ staff&.sdomainid || appeal["prev_deciding_judge"]
+ end
+
+ def self.legacy_original_deciding_judge_name(appeal)
+ staff = VACOLS::Staff.find_by(sattyid: appeal["prev_deciding_judge"])
+ deciding_judge_name = FullName.new(staff["snamef"], nil, appeal["snamel"]).to_s
+ deciding_judge_name.empty? ? nil : deciding_judge_name
+ end
+end
diff --git a/app/queries/appeals_tied_to_non_ssc_avlj_query.rb b/app/queries/appeals_tied_to_non_ssc_avlj_query.rb
new file mode 100644
index 00000000000..532459966fb
--- /dev/null
+++ b/app/queries/appeals_tied_to_non_ssc_avlj_query.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+class AppealsTiedToNonSscAvljQuery
+ # define CSV headers and use this to pull fields to maintain order
+
+ HEADERS = {
+ docket_number: "Docket number",
+ docket: "Docket type",
+ priority: "Priority",
+ receipt_date: "Receipt Date",
+ veteran_file_number: "File Number",
+ veteran_name: "Veteran Name",
+ non_ssc_avlj: "Non-SSC AVLJ's Name",
+ hearing_judge: "Most-recent hearing judge",
+ most_recent_signing_judge: "Most-recent judge who signed decision",
+ bfcurloc: "BFCURLOC"
+ }.freeze
+
+ def self.generate_rows(record)
+ HEADERS.keys.map { |key| record[key] }
+ end
+
+ def self.process
+ # Convert results to CSV format
+
+ CSV.generate(headers: true) do |csv|
+ # Add headers to CSV
+ csv << HEADERS.values
+
+ # Iterate through results and add each row to CSV
+ tied_appeals.each do |record|
+ csv << generate_rows(record)
+ end
+ end
+ end
+
+ # Uses DocketCoordinator to pull appeals ready for distribution
+ # DocketCoordinator is used by Automatic Case Distribution so this will give us the most accurate list of appeals
+ def self.tied_appeals
+ docket_coordinator = DocketCoordinator.new
+
+ docket_coordinator.dockets
+ .flat_map do |sym, docket|
+ if sym == :legacy
+ appeals = docket.appeals_tied_to_non_ssc_avljs
+ unique_appeals = legacy_rows(appeals, sym).uniq { |record| record[:veteran_file_number] }
+
+ unique_appeals
+ else
+ []
+ end
+ end
+ end
+
+ def self.legacy_rows(appeals, sym)
+ appeals.map do |appeal|
+ calculated_values = calculate_field_values(appeal)
+ {
+ docket_number: appeal["tinum"],
+ docket: sym.to_s,
+ priority: appeal["priority"] == 1 ? "True" : "",
+ receipt_date: appeal["bfd19"],
+ veteran_file_number: calculated_values[:veteran_file_number],
+ veteran_name: calculated_values[:veteran_name],
+ non_ssc_avlj: calculated_values[:non_ssc_avlj],
+ hearing_judge: calculated_values[:hearing_judge],
+ most_recent_signing_judge: calculated_values[:most_recent_signing_judge],
+ bfcurloc: calculated_values[:bfcurloc]
+ }
+ end
+ end
+
+ def self.calculate_field_values(appeal)
+ avlj_name = get_avlj_name(appeal)
+ prev_judge_name = get_prev_judge_name(appeal)
+ vacols_case = VACOLS::Case.find_by(bfkey: appeal["bfkey"])
+ veteran_record = VACOLS::Correspondent.find_by(stafkey: vacols_case.bfcorkey)
+ {
+ veteran_file_number: veteran_record.ssn || vacols_case&.bfcorlid,
+ veteran_name: get_name_from_record(veteran_record),
+ non_ssc_avlj: avlj_name,
+ hearing_judge: avlj_name,
+ most_recent_signing_judge: prev_judge_name,
+ bfcurloc: vacols_case&.bfcurloc
+ }
+ end
+
+ def self.get_avlj_name(appeal)
+ if appeal["vlj"].nil?
+ avlj_name = nil
+ else
+ avlj_record = VACOLS::Staff.find_by(sattyid: appeal["vlj"])
+ avlj_name = get_name_from_record(avlj_record)
+ end
+
+ avlj_name
+ end
+
+ def self.get_prev_judge_name(appeal)
+ if appeal["prev_deciding_judge"].nil?
+ prev_judge_name = nil
+ else
+ prev_judge_record = VACOLS::Staff.find_by(sattyid: appeal["prev_deciding_judge"])
+ prev_judge_name = get_name_from_record(prev_judge_record)
+ end
+
+ prev_judge_name
+ end
+
+ def self.get_name_from_record(record)
+ FullName.new(record["snamef"], nil, record["snamel"]).to_s
+ end
+end
diff --git a/app/repositories/appeal_repository.rb b/app/repositories/appeal_repository.rb
index ba473615590..f5688415146 100644
--- a/app/repositories/appeal_repository.rb
+++ b/app/repositories/appeal_repository.rb
@@ -848,6 +848,7 @@ def distribute_nonpriority_appeals(judge, genpop, range, limit, bust_backlog)
end
end
+ # currently this is used for reporting needs
def ready_to_distribute_appeals
MetricsService.record("VACOLS: ready_to_distribute_appeals",
name: "ready_to_distribute_appeals",
@@ -856,6 +857,22 @@ def ready_to_distribute_appeals
end
end
+ def appeals_tied_to_non_ssc_avljs
+ MetricsService.record("VACOLS: appeals_tied_to_non_ssc_avljs",
+ name: "appeals_tied_to_non_ssc_avljs",
+ service: :vacols) do
+ VACOLS::CaseDocket.appeals_tied_to_non_ssc_avljs
+ end
+ end
+
+ def loc_63_appeals
+ MetricsService.record("VACOLS: loc_63_appeals",
+ name: "loc_63_appeals",
+ service: :vacols) do
+ VACOLS::CaseDocket.loc_63_appeals
+ end
+ end
+
private
# NOTE: this should be called within a transaction where you are closing an appeal
diff --git a/client/app/caseDistribution/test.jsx b/client/app/caseDistribution/test.jsx
index 68b83c16f55..5f741f76398 100644
--- a/client/app/caseDistribution/test.jsx
+++ b/client/app/caseDistribution/test.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines */
/* eslint-disable react/prop-types */
import React from 'react';
@@ -11,6 +12,8 @@ import Footer from '@department-of-veterans-affairs/caseflow-frontend-toolkit/co
import CaseSearchLink from '../components/CaseSearchLink';
import ApiUtil from '../util/ApiUtil';
import Button from '../components/Button';
+import Alert from 'app/components/Alert';
+import uuid from 'uuid';
class CaseDistributionTest extends React.PureComponent {
constructor(props) {
@@ -19,7 +22,12 @@ class CaseDistributionTest extends React.PureComponent {
isReseedingAod: false,
isReseedingNonAod: false,
isReseedingAmaDocketGoals: false,
- isReseedingDocketPriority: false
+ isReseedingDocketPriority: false,
+ isReturnLegacyAppeals: false,
+ isFailReturnLegacyAppeals: false,
+ showLegacyAppealsAlert: false,
+ showAlert: false,
+ alertType: 'success',
};
}
@@ -28,11 +36,16 @@ class CaseDistributionTest extends React.PureComponent {
ApiUtil.post('/case_distribution_levers_tests/run_demo_aod_hearing_seeds').then(() => {
this.setState({
isReseedingAod: false,
+ showAlert: true,
+ alertMsg: 'Successfully Completed Seeding Aod Hearing Held Appeals.',
});
}, (err) => {
console.warn(err);
this.setState({
isReseedingAod: false,
+ showAlert: true,
+ alertMsg: err,
+ alertType: 'error',
});
});
};
@@ -42,11 +55,16 @@ class CaseDistributionTest extends React.PureComponent {
ApiUtil.post('/case_distribution_levers_tests/run_demo_non_aod_hearing_seeds').then(() => {
this.setState({
isReseedingNonAod: false,
+ showAlert: true,
+ alertMsg: 'Successfully Completed Seeding Non Aod Hearing Held Appeals.',
});
}, (err) => {
console.warn(err);
this.setState({
isReseedingNonAod: false,
+ showAlert: true,
+ alertMsg: err,
+ alertType: 'error',
});
});
};
@@ -56,25 +74,95 @@ class CaseDistributionTest extends React.PureComponent {
ApiUtil.post('/case_distribution_levers_tests/run-demo-ama-docket-goals').then(() => {
this.setState({
isReseedingAmaDocketGoals: false,
+ showAlert: true,
+ alertMsg: 'Successfully Completed Seeding Ama Docket Time Goal Non Priority Appeals.',
});
}, (err) => {
console.warn(err);
this.setState({
isReseedingAmaDocketGoals: false,
+ showAlert: true,
+ alertMsg: err,
+ alertType: 'error',
});
});
};
reseedDocketPriority = () => {
this.setState({ isReseedingDocketPriority: true });
- ApiUtil.post('/case_distribution_levers_tests/run-demo-docket-priority').then(() => {
+ ApiUtil.post('/case_distribution_levers_tests/run_demo_docket_priority').then(() => {
this.setState({
isReseedingDocketPriority: false,
+ showAlert: true,
+ alertMsg: 'Successfully Completed Seeding Docket Type Appeals.',
});
}, (err) => {
console.warn(err);
this.setState({
isReseedingDocketPriority: false,
+ showAlert: true,
+ alertMsg: err,
+ alertType: 'error',
+ });
+ });
+ };
+
+ reseedNonSSCAVLJAppeals = () => {
+ this.setState({ isReseedingNonSSCAVLJAppeals: true });
+ ApiUtil.post('/case_distribution_levers_tests/run_demo_non_avlj_appeals').then(() => {
+ this.setState({
+ isReseedingNonSSCAVLJAppeals: false,
+ showAlert: true,
+ alertMsg: 'Successfully Completed Seeding non-SSC AVLJ and Appeals.',
+ });
+ }, (err) => {
+ console.warn(err);
+ this.setState({
+ isReseedingNonSSCAVLJAppeals: false,
+ showAlert: true,
+ alertMsg: err,
+ alertType: 'error',
+ });
+ });
+ };
+
+ returnLegacyAppealsToBoard = () => {
+ this.setState({ isReturnLegacyAppeals: true });
+ ApiUtil.post('/case_distribution_levers_tests/run_return_legacy_appeals_to_board').then(() => {
+ this.setState({
+ isReturnLegacyAppeals: false,
+ showLegacyAppealsAlert: true,
+ legacyAppealsAlertType: 'success',
+ legacyAppealsAlertMsg: 'Successfully Completed Return Legacy Appeals To Board Job.',
+ });
+ }, (err) => {
+ console.warn(err);
+ this.setState({
+ isReturnLegacyAppeals: false,
+ showLegacyAppealsAlert: true,
+ legacyAppealsAlertType: 'error',
+ legacyAppealsAlertMsg: err
+ });
+ });
+ };
+
+ failReturnLegacyAppealsToBoard = () => {
+ this.setState({ isFailReturnLegacyAppeals: true });
+ ApiUtil.post('/case_distribution_levers_tests/run_return_legacy_appeals_to_board?fail_job=true').then(() => {
+ this.setState({
+ isFailReturnLegacyAppeals: false,
+ showLegacyAppealsAlert: true,
+ });
+ }, (err) => {
+ const id = uuid.v4();
+ const error = JSON.parse(err.response.text).error;
+ const message = `Return Legacy Appeals To Board ${error} UUID: ${id}.`;
+
+ this.setState({
+ isFailReturnLegacyAppeals: false,
+ showLegacyAppealsAlert: true,
+ legacyAppealsAlertType: 'error',
+ legacyAppealsAlertMsg: message
});
});
};
@@ -255,9 +343,25 @@ class CaseDistributionTest extends React.PureComponent {
+
+
+
+
+
+
+
+
+
+
Run Seed Files
+ { this.state.showAlert &&
+ {this.state.alertMsg}
+ }
+
+ Case Movement
+ { this.state.showLegacyAppealsAlert &&
+
+ {this.state.legacyAppealsAlertMsg}
+
+ }
+
diff --git a/client/app/styles/caseDistribution/_test_seeds.scss b/client/app/styles/caseDistribution/_test_seeds.scss
index a3f5292a83f..257a9e240f8 100644
--- a/client/app/styles/caseDistribution/_test_seeds.scss
+++ b/client/app/styles/caseDistribution/_test_seeds.scss
@@ -2,6 +2,7 @@ $seed-table-border-color: #d6d7d9;
$seed-button-background-color: #0071bc;
$seed-button-font-color: #fff;
$seed-table-preview-bg-color: #f1f1f1;
+$case-movement-button-bg-color: #07648d;
.test-seeds-num-field {
// width: auto;
@@ -122,3 +123,11 @@ $seed-table-preview-bg-color: #f1f1f1;
justify-content: flex-end;
flex-direction: row;
}
+
+.usa-button-case-movement {
+ background: $case-movement-button-bg-color;
+}
+
+.usa-button-case-movement:hover {
+ background: $case-movement-button-bg-color;
+}
diff --git a/client/constants/ACD_LEVERS.json b/client/constants/ACD_LEVERS.json
index 92eb0942637..67b22361cde 100644
--- a/client/constants/ACD_LEVERS.json
+++ b/client/constants/ACD_LEVERS.json
@@ -25,7 +25,8 @@
"affinity": "affinity",
"docket_distribution_prior": "docket_distribution_prior",
"docket_time_goal": "docket_time_goal",
- "docket_levers": "docket_levers"
+ "docket_levers": "docket_levers",
+ "internal": "internal"
},
"validation_error_message": {
"minimum_not_met": "Please enter a value greater than or equal to 0",
diff --git a/client/constants/DISTRIBUTION.json b/client/constants/DISTRIBUTION.json
index d4b1809cd9c..6f73df724ca 100644
--- a/client/constants/DISTRIBUTION.json
+++ b/client/constants/DISTRIBUTION.json
@@ -54,5 +54,7 @@
"disable_ama_priority_direct_review": "disable_ama_priority_direct_review",
"disable_ama_priority_direct_review_title": "ACD Disable AMA Priority Direct Review",
"disable_ama_priority_evidence_submission": "disable_ama_priority_evidence_submission",
- "disable_ama_priority_evidence_submission_title": "ACD Disable AMA Priority Evidence Submission"
+ "disable_ama_priority_evidence_submission_title": "ACD Disable AMA Priority Evidence Submission",
+ "enable_nonsscavlj": "enable_nonsscavlj",
+ "enable_nonsscavlj_title": "Enable Non-SSC/AVLJ"
}
diff --git a/config/routes.rb b/config/routes.rb
index d19c45fd713..b29652caa34 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -34,11 +34,15 @@
get 'appeals_ready_to_distribute'
get 'appeals_non_priority_ready_to_distribute'
get 'appeals_distributed'
+ get 'appeals_in_location_63_in_past_2_days'
get 'ineligible_judge_list'
+ get 'appeals_tied_to_non_ssc_avlj'
post 'run_demo_aod_hearing_seeds'
post 'run_demo_non_aod_hearing_seeds'
post 'run-demo-ama-docket-goals'
- post 'run-demo-docket-priority'
+ post 'run_demo_non_avlj_appeals'
+ post 'run_demo_docket_priority'
+ post 'run_return_legacy_appeals_to_board'
end
end
diff --git a/db/migrate/20240717034659_create_returned_appeal_jobs.rb b/db/migrate/20240717034659_create_returned_appeal_jobs.rb
new file mode 100644
index 00000000000..1b28a665322
--- /dev/null
+++ b/db/migrate/20240717034659_create_returned_appeal_jobs.rb
@@ -0,0 +1,13 @@
+class CreateReturnedAppealJobs < Caseflow::Migration
+ def change
+ create_table :returned_appeal_jobs do |t|
+ t.timestamp :started_at
+ t.timestamp :completed_at
+ t.timestamp :errored_at
+ t.json :stats
+ t.text :returned_appeals, array: true, default: []
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c8fdc3e9149..8eca584ae5b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1898,6 +1898,16 @@
t.index ["user_id"], name: "index_request_issues_updates_on_user_id"
end
+ create_table "returned_appeal_jobs", force: :cascade do |t|
+ t.datetime "completed_at"
+ t.datetime "created_at", null: false
+ t.datetime "errored_at"
+ t.text "returned_appeals", default: [], array: true
+ t.datetime "started_at"
+ t.json "stats"
+ t.datetime "updated_at", null: false
+ end
+
create_table "schedule_periods", force: :cascade do |t|
t.datetime "created_at", null: false
t.date "end_date", null: false
diff --git a/db/seeds/case_distribution_levers.rb b/db/seeds/case_distribution_levers.rb
index c60dec34694..88e281f2028 100644
--- a/db/seeds/case_distribution_levers.rb
+++ b/db/seeds/case_distribution_levers.rb
@@ -784,6 +784,18 @@ def levers
}
]
},
+ {
+ item: Constants.DISTRIBUTION.enable_nonsscavlj,
+ title: Constants.DISTRIBUTION.enable_nonsscavlj_title,
+ description: "This is the internal lever used to enable and disable Non-SSC AVLJ work.",
+ data_type: Constants.ACD_LEVERS.data_types.boolean,
+ value: true,
+ unit: "",
+ is_disabled_in_ui: true,
+ algorithms_used: [],
+ lever_group: Constants.ACD_LEVERS.lever_groups.internal,
+ lever_group_order: 0
+ },
]
end
@@ -824,7 +836,6 @@ def full_update(item)
# DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER
def full_update_lever(lever)
existing_lever = CaseDistributionLever.find_by_item(lever[:item])
-
existing_lever.update(
title: lever[:title],
description: lever[:description],
diff --git a/db/seeds/non_ssc_avlj_legacy_appeals.rb b/db/seeds/non_ssc_avlj_legacy_appeals.rb
new file mode 100644
index 00000000000..e4e30740a0a
--- /dev/null
+++ b/db/seeds/non_ssc_avlj_legacy_appeals.rb
@@ -0,0 +1,366 @@
+# frozen_string_literal: true
+
+module Seeds
+ class NonSscAvljLegacyAppeals < Base
+ def initialize
+ # initialize_np_legacy_appeals_file_number_and_participant_id
+ # initialize_priority_legacy_appeals_file_number_and_participant_id
+ end
+
+ def seed!
+ RequestStore[:current_user] = User.system_user
+ create_avljs
+ create_legacy_appeals
+ end
+
+ private
+
+ def create_avljs
+ create_non_ssc_avlj("NONSSCAN01", "Four Priority")
+ create_non_ssc_avlj("NONSSCAN02", "Four non-priority")
+ create_non_ssc_avlj("NONSSCAN03", "Four-pri h-and-d")
+ create_non_ssc_avlj("NONSSCAN04", "Four-non-pri h-and-d")
+ create_non_ssc_avlj("NONSSCAN05", "For-mix-of both-h-only")
+ create_non_ssc_avlj("NONSSCAN06", "For-mix-of both-h-and-d")
+ create_non_ssc_avlj("NONSSCAN07", "Do-not-get moved-pri")
+ create_non_ssc_avlj("NONSSCAN08", "Do-not-get moved-nonpri")
+ create_non_ssc_avlj("NONSSCAN09", "Do-not-get moved-mix")
+ create_non_ssc_avlj("NONSSCAN10", "Some-moved some-not")
+ create_ssc_avlj("SSCA11", "Does-not qualify for-mvmt")
+ create_non_ssc_avlj("NONSSCAN12", "Two-judges last-is-SSC")
+ create_non_ssc_avlj("NONSSCAN13", "Two-judges both-non-SSC")
+
+ create_non_ssc_avlj("SIGNAVLJLGC", "NonSSC Signing-AVLJ")
+ create_non_ssc_avlj("AVLJLGC2", "Alternate NonSSC-AVLJ")
+ create_ssc_avlj("SSCAVLJLGC", "SSC AVLJ1")
+ end
+
+ def create_legacy_appeals
+ # the naming comes from the acceptance criteria of APPEALS-45208
+ create_four_priority_appeals_tied_to_a_non_ssc_avlj
+ create_four_non_priority_appeals_tied_to_a_non_ssc_avlj
+ create_four_priority_appeals_tied_to_and_signed_by_a_non_ssc_avlj
+ create_four_non_priority_appeals_tied_to_and_signed_by_a_non_ssc_avlj
+ create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj
+ create_four_alternating_priority_by_age_appeals_tied_to_and_signed_by_a_non_ssc_avlj
+ create_four_priority_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj
+ create_four_non_priority_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj
+ create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj
+ create_two_sets_of_seven_types_of_appeals_tied_to_a_non_ssc_avlj
+ create_four_alternating_priority_by_age_appeals_tied_to_a_ssc_avlj
+ create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_with_a_second_hearing_held_by_a_ssc_avlj
+ create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_with_a_second_hearing_held_by_another_non_ssc_avlj
+ end
+
+ def create_four_priority_appeals_tied_to_a_non_ssc_avlj
+ # A non-SSC AVLJ that Only has 4 priority cases where they held the last hearing
+ avlj = User.find_by(css_id: "NONSSCAN01")
+ create_legacy_appeal(priority=true, avlj, 300.days.ago)
+ create_legacy_appeal(priority=true, avlj, 200.days.ago)
+ create_legacy_appeal(priority=true, avlj, 100.days.ago)
+ create_legacy_appeal(priority=true, avlj, 30.days.ago)
+ end
+
+ def create_four_non_priority_appeals_tied_to_a_non_ssc_avlj
+ # A non-SSC AVLJ that Only has 4 non-priority cases where they held the last hearing
+ avlj = User.find_by(css_id: "NONSSCAN02")
+ create_legacy_appeal(priority=false, avlj, 350.days.ago)
+ create_legacy_appeal(priority=false, avlj, 250.days.ago)
+ create_legacy_appeal(priority=false, avlj, 150.days.ago)
+ create_legacy_appeal(priority=false, avlj, 50.days.ago)
+ end
+
+ def create_four_priority_appeals_tied_to_and_signed_by_a_non_ssc_avlj
+ assigned_avlj = User.find_by(css_id: "NONSSCAN03")
+ signing_avlj = User.find_by(css_id: "NONSSCAN03")
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 100.days.ago)
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 80.days.ago)
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 60.days.ago)
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 30.days.ago)
+ end
+
+ def create_four_non_priority_appeals_tied_to_and_signed_by_a_non_ssc_avlj
+ # A non-SSC AVLJ that Only has 4 non-priority cases where they held the last hearing and signed the most recent decision
+ assigned_avlj = User.find_by(css_id: "NONSSCAN04")
+ signing_avlj = User.find_by(css_id: "NONSSCAN04")
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 110.days.ago)
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 90.days.ago)
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 70.days.ago)
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 40.days.ago)
+ end
+
+ def create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj
+ # A non-SSC AVLJ that Has 4 in alternating order by age of BRIEFF.BFD19 (Docket Date)
+ # priority cases where they held the last hearing
+ # non-priority cases where they held the last hearing
+ avlj = User.find_by(css_id: "NONSSCAN05")
+ create_legacy_appeal(priority=false, avlj, 600.days.ago) #oldest
+ create_legacy_appeal(priority=true, avlj, 425.days.ago)
+ create_legacy_appeal(priority=false, avlj, 400.days.ago)
+ create_legacy_appeal(priority=true, avlj, 40.days.ago) #most recent
+ end
+
+ def create_four_alternating_priority_by_age_appeals_tied_to_and_signed_by_a_non_ssc_avlj
+ # A non-SSC AVLJ that Has 4 in alternating order by age of BRIEFF.BFD19 (Docket Date)
+ # priority cases where they held the last hearing and signed the most recent decision
+ # non-priority cases where they held the last hearing and signed the most recent decision
+ signing_avlj = User.find_by(css_id: "NONSSCAN06")
+ assigned_avlj = User.find_by(css_id: "NONSSCAN06")
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 120.days.ago) #oldest
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 110.days.ago)
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 100.days.ago)
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 50.days.ago) #most recent
+ end
+
+ def create_four_priority_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj
+ # A non-SSC AVLJ that Only has 4 priority cases where they held the last hearing and did NOT sign the most recent decision
+ # These cases should NOT be returned to the board
+ assigned_avlj = User.find_by(css_id: "NONSSCAN07")
+ signing_avlj = User.find_by(css_id: "SIGNAVLJLGC")
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 120.days.ago)
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 110.days.ago)
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 100.days.ago)
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 50.days.ago)
+ end
+
+ def create_four_non_priority_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj
+ # A non-SSC AVLJ that Only has 4 non-priority cases where they held the last hearing and did NOT sign the most recent decision
+ # These cases should NOT be returned to the board
+ assigned_avlj = User.find_by(css_id: "NONSSCAN08")
+ signing_avlj = User.find_by(css_id: "SIGNAVLJLGC")
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 120.days.ago)
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 110.days.ago)
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 100.days.ago)
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 50.days.ago)
+ end
+
+ def create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_signed_by_another_avlj
+ # A non-SSC AVLJ that Has 4 in alternating order by age of BRIEFF.BFD19 (Docket Date)
+ # priority cases where they held the last hearing and did NOT sign the most recent decision
+ # These cases should NOT be returned to the board
+ # non-priority cases where they held the last hearing and did NOT sign the most recent decision
+ # These cases should NOT be returned to the board
+ assigned_avlj = User.find_by(css_id: "NONSSCAN09")
+ signing_avlj = User.find_by(css_id: "SIGNAVLJLGC")
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 220.days.ago) #oldest
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 210.days.ago)
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 200.days.ago)
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 150.days.ago) #most recent
+ end
+
+ def create_two_sets_of_seven_types_of_appeals_tied_to_a_non_ssc_avlj
+ # A non-SSC AVLJ that Has 12 appeals
+ # Notes
+ # Cycle through the groups before creating the second appeal in the group, make each created appeal newer by BRIEFF.BFD19 (Docket Date) than the previous one
+ # Appeals in the same group should not be grouped next to each other
+ # appeals
+ # 1. priority cases where they held the last hearing and did NOT sign the most recent decision
+ # These cases should NOT be returned to the board
+ # 2. non-priority cases where they held the last hearing and did NOT sign the most recent decision
+ # These cases should NOT be returned to the board
+ # 3. priority cases where they held the last hearing
+ # 4. non-priority cases where they held the last hearing
+ # 5. priority cases where they held the last hearing and signed the most recent decision
+ # 6. non-priority cases where they held the last hearing and signed the most recent decision
+ # 7. has an appeal with a hearing where they were the judge but the appeal is NOT ready to distribute
+ # This case would NOT show up in the ready to distribute query, but we could look it up by veteran ID to verify that it didn't get moved.
+
+ assigned_avlj = User.find_by(css_id: "NONSSCAN10")
+ signing_avlj = User.find_by(css_id: "SIGNAVLJLGC")
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 220.days.ago) #oldest
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 210.days.ago)
+ create_legacy_appeal(priority=true, assigned_avlj, 200.days.ago)
+ create_legacy_appeal(priority=false, assigned_avlj, 190.days.ago)
+ create_signed_legacy_appeal(priority=false, assigned_avlj, assigned_avlj, 180.days.ago)
+ create_signed_legacy_appeal(priority=true, assigned_avlj, assigned_avlj, 170.days.ago)
+ legacy_appeal = create_legacy_appeal(priority=true, assigned_avlj, 160.days.ago)
+ make_legacy_appeal_not_ready_for_distribution(legacy_appeal)
+
+ create_signed_legacy_appeal(priority=false, signing_avlj, assigned_avlj, 150.days.ago)
+ create_signed_legacy_appeal(priority=true, signing_avlj, assigned_avlj, 140.days.ago)
+ create_legacy_appeal(priority=true, assigned_avlj, 130.days.ago)
+ create_legacy_appeal(priority=false, assigned_avlj, 120.days.ago)
+ create_signed_legacy_appeal(priority=false, assigned_avlj, assigned_avlj, 110.days.ago)
+ create_signed_legacy_appeal(priority=true, assigned_avlj, assigned_avlj, 100.days.ago)
+ legacy_appeal = create_legacy_appeal(priority=true, assigned_avlj, 90.days.ago)
+ make_legacy_appeal_not_ready_for_distribution(legacy_appeal)#most recent
+ end
+
+ def create_four_alternating_priority_by_age_appeals_tied_to_a_ssc_avlj
+ # A SSC AVLJ that has 4 appeals for which they held the last hearing.
+ # These cases should NOT be returned to the board
+ ssc_avlj = User.find_by(css_id: "SSCA11")
+ create_legacy_appeal(priority=true, ssc_avlj, 325.days.ago)
+ create_legacy_appeal(priority=false, ssc_avlj, 275.days.ago)
+ create_legacy_appeal(priority=true, ssc_avlj, 175.days.ago)
+ create_legacy_appeal(priority=false, ssc_avlj, 75.days.ago)
+ end
+
+ def create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_with_a_second_hearing_held_by_a_ssc_avlj
+ # A non-SSC AVLJ that has 4 appeals where the non-SSC AVLJ held a hearing first, but a second hearing was held by an SSC AVLJ.
+ # These cases should NOT be returned to the board
+ avlj = User.find_by(css_id: "NONSSCAN12")
+ ssc_avlj = User.find_by(css_id: "SSCAVLJLGC")
+ legacy_appeal = create_legacy_appeal(priority=true, avlj, 90.days.ago)
+ create_second_hearing_for_legacy_appeal(legacy_appeal, 90.days.ago, ssc_avlj)
+
+ legacy_appeal = create_legacy_appeal(priority=false, avlj, 60.days.ago)
+ create_second_hearing_for_legacy_appeal(legacy_appeal, 30.days.ago, ssc_avlj)
+
+ legacy_appeal = create_legacy_appeal(priority=true, avlj, 30.days.ago)
+ create_second_hearing_for_legacy_appeal(legacy_appeal, 15.days.ago, ssc_avlj)
+
+ legacy_appeal = create_legacy_appeal(priority=false, avlj, 15.days.ago)
+ create_second_hearing_for_legacy_appeal(legacy_appeal, 5.days.ago, ssc_avlj)
+ end
+
+ def create_four_alternating_priority_by_age_appeals_tied_to_a_non_ssc_avlj_with_a_second_hearing_held_by_another_non_ssc_avlj
+ # A non-SSC AVLJ that has 4 appeals where the non-SSC AVLJ held a hearing first, but a second hearing was held by different non-SSC AVLJ.
+ avlj = User.find_by(css_id: "NONSSCAN13")
+ avlj2 = User.find_by(css_id: "AVLJLGC2")
+ legacy_appeal = create_legacy_appeal(priority=true, avlj, 95.days.ago)
+ create_second_hearing_for_legacy_appeal(legacy_appeal, 65.days.ago, avlj2)
+
+ legacy_appeal = create_legacy_appeal(priority=false, avlj, 65.days.ago)
+ create_second_hearing_for_legacy_appeal(legacy_appeal, 35.days.ago, avlj2)
+
+ legacy_appeal = create_legacy_appeal(priority=true, avlj, 35.days.ago)
+ create_second_hearing_for_legacy_appeal(legacy_appeal, 25.days.ago, avlj2)
+
+ legacy_appeal = create_legacy_appeal(priority=false, avlj, 20.days.ago)
+ create_second_hearing_for_legacy_appeal(legacy_appeal, 10.days.ago, avlj2)
+ end
+
+ def create_non_ssc_avlj(css_id, full_name)
+ User.find_by_css_id(css_id) ||
+ create(:user, :non_ssc_avlj_user, css_id: css_id, full_name: full_name)
+ end
+
+ def create_ssc_avlj(css_id, full_name)
+ User.find_by_css_id(css_id) ||
+ create(:user, :ssc_avlj_user, css_id: css_id, full_name: full_name)
+ end
+
+ def demo_regional_office
+ 'RO17'
+ end
+
+ def create_signed_legacy_appeal(priority, signing_avlj, assigned_avlj, docket_date)
+ Timecop.travel(docket_date) do
+ traits = priority ? [:type_cavc_remand] : [:type_original]
+ create(:legacy_signed_appeal, *traits, signing_avlj: signing_avlj, assigned_avlj: assigned_avlj)
+ end
+ end
+
+ def create_legacy_appeal(priority, avlj, docket_date)
+ Timecop.travel(docket_date)
+ veteran = create_demo_veteran_for_legacy_appeal
+
+ correspondent = create(:correspondent,
+ snamef: veteran.first_name, snamel: veteran.last_name,
+ ssalut: "", ssn: veteran.file_number)
+
+
+ vacols_case = priority ? create_priority_video_vacols_case(veteran,
+ correspondent,
+ avlj,
+ docket_date) :
+ create_non_priority_video_vacols_case(veteran,
+ correspondent,
+ avlj,
+ docket_date)
+
+ legacy_appeal = create(
+ :legacy_appeal,
+ :with_root_task,
+ vacols_case: vacols_case,
+ closest_regional_office: demo_regional_office
+ )
+
+ create(:available_hearing_locations, demo_regional_office, appeal: legacy_appeal)
+ Timecop.return
+
+ legacy_appeal
+ end
+
+ def create_priority_video_vacols_case(veteran, correspondent, associated_judge, days_ago)
+ create(
+ :case,
+ :aod,
+ :tied_to_judge,
+ :video_hearing_requested,
+ :type_original,
+ :ready_for_distribution,
+ tied_judge: associated_judge,
+ correspondent: correspondent,
+ bfcorlid: "#{veteran.file_number}S",
+ case_issues: create_list(:case_issue, 3, :compensation),
+ bfd19: days_ago
+ )
+ end
+
+ def create_non_priority_video_vacols_case(veteran, correspondent, associated_judge, days_ago)
+ create(
+ :case,
+ :tied_to_judge,
+ :video_hearing_requested,
+ :type_original,
+ :ready_for_distribution,
+ tied_judge: associated_judge,
+ correspondent: correspondent,
+ bfcorlid: "#{veteran.file_number}S",
+ case_issues: create_list(:case_issue, 3, :compensation),
+ bfd19: days_ago
+ )
+ end
+
+ def random_demo_file_number_and_participant_id
+ random_file_number = Random.rand(100_000_000...989_999_999)
+ random_participant_id = random_file_number + 100000
+
+ while find_demo_veteran(random_file_number)
+ random_file_number += 2000
+ random_participant_id += 2000
+ end
+
+ return random_file_number, random_participant_id
+ end
+
+ def find_demo_veteran(file_number)
+ Veteran.find_by(file_number: format("%09d", n: file_number + 1))
+ end
+
+ def create_demo_veteran(options = {})
+ params = {
+ file_number: format("%09d", n: options[:file_number]),
+ participant_id: format("%09d", n: options[:participant_id])
+ }
+
+ Veteran.find_by_participant_id(params[:participant_id]) || create(:veteran, params.merge(options))
+ end
+
+ def create_demo_veteran_for_legacy_appeal
+ file_number, participant_id = random_demo_file_number_and_participant_id
+ create_demo_veteran(
+ file_number: file_number,
+ participant_id: participant_id
+ )
+ end
+
+ def create_second_hearing_for_legacy_appeal(legacy_appeal, docket_date, avlj)
+ case_hearing = create(
+ :case_hearing,
+ :disposition_held,
+ folder_nr: legacy_appeal.vacols_id,
+ hearing_date: docket_date.to_date,
+ user: avlj
+ )
+
+ create(:legacy_hearing, appeal: legacy_appeal, case_hearing: case_hearing)
+ end
+
+ def make_legacy_appeal_not_ready_for_distribution(legacy_appeal)
+ VACOLS::Case.find(legacy_appeal.vacols_id).update!(bfcurloc: "01")
+ end
+ end
+end
diff --git a/spec/controllers/case_distribution_levers_controller_spec.rb b/spec/controllers/case_distribution_levers_controller_spec.rb
index 9ab8fc26579..e4faa69a6e4 100644
--- a/spec/controllers/case_distribution_levers_controller_spec.rb
+++ b/spec/controllers/case_distribution_levers_controller_spec.rb
@@ -133,7 +133,7 @@
end
it "renders a page with the grouped levers and lever history" do
- lever_keys = %w[static batch affinity docket_distribution_prior docket_time_goal docket_levers]
+ lever_keys = %w[static batch affinity docket_distribution_prior docket_time_goal docket_levers internal]
User.authenticate!(user: lever_user)
OrganizationsUser.make_user_admin(lever_user, CDAControlGroup.singleton)
get "levers"
diff --git a/spec/factories/document.rb b/spec/factories/document.rb
index 9998a19dbda..cb00816f922 100644
--- a/spec/factories/document.rb
+++ b/spec/factories/document.rb
@@ -2,8 +2,7 @@
FactoryBot.define do
factory :document do
- sequence(:vbms_document_id, 10_000) # start with initial high value to reserve manual assignment range
-
+ vbms_document_id { (10_000..999_999).to_a.sample }
type { "VA 8 Certification of Appeal" }
end
end
diff --git a/spec/factories/returned_appeal_job.rb b/spec/factories/returned_appeal_job.rb
new file mode 100644
index 00000000000..ac17c3f28d8
--- /dev/null
+++ b/spec/factories/returned_appeal_job.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :returned_appeal_job do
+ started_at { Time.zone.now }
+ completed_at { Time.zone.now + 1.hour }
+ errored_at { nil }
+ stats { { success: true, message: "Job completed successfully" }.to_json }
+ returned_appeals { [] }
+ end
+end
diff --git a/spec/factories/user.rb b/spec/factories/user.rb
index 61281cdd239..efdde2dbaa8 100644
--- a/spec/factories/user.rb
+++ b/spec/factories/user.rb
@@ -240,6 +240,18 @@
end
end
+ trait :non_ssc_avlj_user do
+ after(:create) do |user|
+ create(:staff, :non_ssc_avlj, user: user)
+ end
+ end
+
+ trait :ssc_avlj_user do
+ after(:create) do |user|
+ create(:staff, :ssc_avlj, user: user)
+ end
+ end
+
after(:create) do |user, evaluator|
if evaluator.vacols_uniq_id
create(:staff, slogid: evaluator.vacols_uniq_id, user: user)
diff --git a/spec/factories/vacols/case.rb b/spec/factories/vacols/case.rb
index 52cfd37c019..f8d245f7d4f 100644
--- a/spec/factories/vacols/case.rb
+++ b/spec/factories/vacols/case.rb
@@ -14,13 +14,13 @@
bfcorkey { generate :vacols_correspondent_key }
bfcorlid { "#{generate :veteran_file_number}S" }
- association :correspondent, factory: :correspondent
+ correspondent { association :correspondent }
transient do
docket_number { "150000#{bfkey}" }
end
# folder.tinum is the docket_number
- folder { association :folder, ticknum: bfkey, tinum: docket_number }
+ folder { association :folder, ticknum: bfkey, tinum: docket_number, titrnum: bfcorlid }
bfregoff { "RO18" }
@@ -196,6 +196,114 @@
end
end
end
+
+ # The judge and attorney should be the VACOLS::Staff records of those users
+ # This factory uses the :aod trait to mark it AOD instead of a transient attribute
+ # Pass `tied_to: false` to create an original appeal without a previous hearing
+ factory :legacy_signed_appeal do
+ transient do
+ judge { nil }
+ signing_avlj { nil }
+ assigned_avlj { nil }
+ attorney { nil }
+ cavc { false }
+ appeal_affinity { true }
+ affinity_start_date { 2.months.ago }
+ tied_to { true }
+ end
+
+ status_active
+
+ bfdpdcn { 1.month.ago }
+ bfcurloc { "81" }
+
+ after(:create) do |new_case, evaluator|
+ signing_judge =
+ if evaluator.signing_avlj.present?
+ VACOLS::Staff.find_by_sdomainid(evaluator.signing_avlj.css_id)
+ else
+ evaluator.judge || create(:user, :judge, :with_vacols_judge_record).vacols_staff
+ end
+
+ hearing_judge =
+ if evaluator.assigned_avlj.present?
+ VACOLS::Staff.find_by_sdomainid(evaluator.assigned_avlj.css_id)
+ else
+ evaluator.judge || create(:user, :judge, :with_vacols_judge_record).vacols_staff
+ end
+
+ signing_sattyid = signing_judge.sattyid
+
+ original_attorney = evaluator.attorney || create(:user, :with_vacols_attorney_record).vacols_staff
+
+ new_case.correspondent.update!(ssn: new_case.bfcorlid.chomp("S")) unless new_case.correspondent.ssn
+
+ veteran = Veteran.find_by_file_number_or_ssn(new_case.correspondent.ssn)
+
+ if veteran
+ new_case.correspondent.update!(snamef: veteran.first_name, snamel: veteran.last_name)
+ else
+ create(
+ :veteran,
+ first_name: new_case.correspondent.snamef,
+ last_name: new_case.correspondent.snamel,
+ name_suffix: new_case.correspondent.ssalut,
+ ssn: new_case.correspondent.ssn,
+ file_number: new_case.correspondent.ssn
+ )
+ end
+
+ # Build these instead of create so the folder after_create hooks don't execute and create another case
+ # until the original case has been created and the associations saved
+ original_folder = build(
+ :folder,
+ new_case.folder.attributes.except!("ticknum", "tidrecv", "tidcls", "tiaduser",
+ "tiadtime", "tikeywrd", "tiread2", "tioctime", "tiocuser",
+ "tidktime", "tidkuser")
+ )
+
+ original_issues = new_case.case_issues.map do |issue|
+ build(
+ :case_issue,
+ issue.attributes.except("isskey", "issaduser", "issadtime", "issmduser", "issmdtime", "issdcls"),
+ issdc: "3"
+ )
+ end
+
+ original_case = create(
+ :case,
+ :status_complete,
+ :disposition_remanded,
+ bfac: evaluator.cavc ? "7" : "1",
+ bfcorkey: new_case.bfcorkey,
+ bfcorlid: new_case.bfcorlid,
+ bfdnod: new_case.bfdnod,
+ bfdsoc: new_case.bfdsoc,
+ bfd19: new_case.bfd19,
+ bfcurloc: "99",
+ bfddec: new_case.bfdpdcn,
+ bfmemid: signing_sattyid,
+ bfattid: original_attorney.sattyid,
+ folder: original_folder,
+ correspondent: new_case.correspondent,
+ case_issues: original_issues
+ )
+
+ if evaluator.tied_to
+ create(
+ :case_hearing,
+ :disposition_held,
+ folder_nr: original_case.bfkey,
+ hearing_date: original_case.bfddec - 1.month,
+ user: User.find_by_css_id(hearing_judge&.sdomainid)
+ )
+ end
+
+ if evaluator.appeal_affinity
+ create(:appeal_affinity, appeal: new_case, affinity_start_date: evaluator.affinity_start_date)
+ end
+ end
+ end
end
end
end
@@ -229,6 +337,22 @@
end
end
+ trait :tied_to_previous_judge do
+ transient do
+ previous_tied_judge { nil }
+ end
+
+ after(:create) do |vacols_case, evaluator|
+ create(
+ :case_hearing,
+ :disposition_held,
+ folder_nr: vacols_case.bfkey,
+ hearing_date: 5.days.ago.to_date,
+ user: evaluator.previous_tied_judge
+ )
+ end
+ end
+
trait :type_original do
bfac { "1" }
end
@@ -401,6 +525,38 @@
end
end
+ transient do
+ folder_number_equal { false }
+ original_case { nil }
+
+ after(:create) do |vacols_case, evaluator|
+ if evaluator.folder_number_equal
+ folder_json = evaluator.original_case.folder.to_json
+ folder_attributes = JSON.parse(folder_json)
+ folder_attributes.except!("bfkey", "ticknum", "tidrecv", "tidcls", "tiaduser",
+ "tiadtime", "tikeywrd", "tiread2", "tioctime", "tiocuser",
+ "tidktime", "tidkuser")
+ vacols_case.folder.assign_attributes(folder_attributes)
+ vacols_case.folder.save(validate: false)
+ end
+ end
+ end
+
+ transient do
+ case_issues_equal { false }
+ original_case_issues { [] }
+
+ after(:create) do |vacols_case, evaluator|
+ if evaluator.case_issues_equal
+ evaluator.original_case_issues.each do |case_issue, i|
+ vacols_case.case_issues[i] = case_issue.attributes.except("issaduser", "issadtime", "issmduser",
+ "issmdtime", "issdc", "issdcls")
+ vacols_case.case_issues[i].save
+ end
+ end
+ end
+ end
+
transient do
staff { nil }
end
diff --git a/spec/factories/vacols/staff.rb b/spec/factories/vacols/staff.rb
index 77519c47a42..7c8a4860124 100644
--- a/spec/factories/vacols/staff.rb
+++ b/spec/factories/vacols/staff.rb
@@ -12,6 +12,16 @@
new_sattyid
end
+
+ judge do
+ judge_staff = VACOLS::Staff.find_by(slogid: "STAFF_FCT_JUDGE") ||
+ create(:staff, :judge_role, slogid: "STAFF_FCT_JUDGE")
+ judge_staff
+ end
+
+ generated_smemgrp_not_equal_to_sattyid do
+ judge.sattyid
+ end
end
sequence(:stafkey) do |n|
@@ -117,6 +127,18 @@
sattyid { generated_sattyid }
end
+ trait :non_ssc_avlj do
+ svlj { "A" }
+ sattyid { generated_sattyid }
+ smemgrp { generated_smemgrp_not_equal_to_sattyid }
+ end
+
+ trait :ssc_avlj do
+ svlj { "A" }
+ sattyid { generated_sattyid }
+ smemgrp { sattyid }
+ end
+
after(:build) do |staff, evaluator|
if evaluator.user&.full_name
staff.snamef = evaluator.user.full_name.split(" ").first
diff --git a/spec/jobs/push_priority_appeals_to_judges_job_spec.rb b/spec/jobs/push_priority_appeals_to_judges_job_spec.rb
index c7bc2ebe8d2..6f1d0adf238 100644
--- a/spec/jobs/push_priority_appeals_to_judges_job_spec.rb
+++ b/spec/jobs/push_priority_appeals_to_judges_job_spec.rb
@@ -23,8 +23,6 @@ def to_judge_hash(arr)
before do
expect_any_instance_of(PushPriorityAppealsToJudgesJob)
.to receive(:distribute_genpop_priority_appeals).and_return([])
- expect_any_instance_of(PushPriorityAppealsToJudgesJob)
- .to receive(:generate_report).and_return([])
end
after { FeatureToggle.disable!(:acd_distribute_by_docket_date) }
@@ -51,6 +49,13 @@ def to_judge_hash(arr)
subject
end
+
+ it "calls send_job_report method" do
+ expect_any_instance_of(PushPriorityAppealsToJudgesJob)
+ .to receive(:generate_report).and_return([])
+
+ subject
+ end
end
context ".distribute_genpop_priority_appeals" do
@@ -358,7 +363,6 @@ def to_judge_hash(arr)
it "using By Docket Date Distribution module" do
FeatureToggle.enable!(:acd_distribute_by_docket_date)
-
today = Time.zone.now.to_date
legacy_days_waiting = (today - legacy_priority_case.bfd19.to_date).to_i
direct_review_days_waiting = (today - ready_priority_direct_case.receipt_date).to_i
diff --git a/spec/jobs/return_legacy_appeals_to_board_job_spec.rb b/spec/jobs/return_legacy_appeals_to_board_job_spec.rb
new file mode 100644
index 00000000000..a517a9fe358
--- /dev/null
+++ b/spec/jobs/return_legacy_appeals_to_board_job_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+describe ReturnLegacyAppealsToBoardJob, :all_dbs do
+ describe "#perform" do
+ let(:job) { described_class.new }
+
+ it "creates a ReturnedAppealJob instance and updates its status" do
+ expect do
+ job.perform
+ end.to change { ReturnedAppealJob.count }.by(1)
+
+ returned_appeal_job = ReturnedAppealJob.last
+ expect(returned_appeal_job.started_at).to be_present
+ expect(returned_appeal_job.completed_at).to be_present
+ expect(JSON.parse(returned_appeal_job.stats)["message"]).to eq("Job completed successfully")
+ end
+
+ it "sends a job report via Slack" do
+ expect_any_instance_of(SlackService).to receive(:send_notification)
+ job.perform
+ end
+
+ it "record runtime metrics" do
+ allow(MetricsService).to receive(:record_runtime)
+ expect do
+ job.perform
+ end.to change { ReturnedAppealJob.count }.by(1)
+ expect(MetricsService).to have_received(:record_runtime).with(
+ hash_including(metric_group: "return_legacy_appeals_to_board_job")
+ )
+ end
+
+ context "when an error occurs" do
+ let(:error_message) { "Something went wrong" }
+
+ before do
+ allow(job).to receive(:send_job_slack_report).and_raise(StandardError, error_message)
+ end
+
+ it "updates the ReturnedAppealJob with error details" do
+ expect do
+ job.perform
+ end.to change { ReturnedAppealJob.count }.by(1)
+
+ returned_appeal_job = ReturnedAppealJob.last
+ expect(returned_appeal_job.errored_at).to be_present
+ expect(JSON.parse(returned_appeal_job.stats)["message"]).to include("Job failed with error: #{error_message}")
+ end
+
+ it "sends an error notification via Slack" do
+ expect_any_instance_of(SlackService).to receive(:send_notification).with(/#{error_message}/, job.class.name)
+ job.perform
+ end
+
+ it "logs the error" do
+ expect(job).to receive(:log_error).with(instance_of(StandardError))
+ job.perform
+ end
+ end
+ end
+end
diff --git a/spec/models/returned_appeal_job_spec.rb b/spec/models/returned_appeal_job_spec.rb
new file mode 100644
index 00000000000..8c8e5989df8
--- /dev/null
+++ b/spec/models/returned_appeal_job_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.describe ReturnedAppealJob, :all_dbs do
+ describe "factory" do
+ it "is valid" do
+ expect(build(:returned_appeal_job)).to be_valid
+ end
+ end
+end
diff --git a/spec/seeds/case_distribution_test_data_spec.rb b/spec/seeds/case_distribution_test_data_spec.rb
index 373982fd017..ea4a5f166c4 100644
--- a/spec/seeds/case_distribution_test_data_spec.rb
+++ b/spec/seeds/case_distribution_test_data_spec.rb
@@ -45,7 +45,7 @@
seed.seed!
# checking CaseDistributionlevers count
- expect(CaseDistributionLever.count).to eq 28
+ expect(CaseDistributionLever.count).to eq 29
expect(Appeal.where(docket_type: "direct_review").count).to eq 38
expect(Appeal.where(docket_type: "direct_review").first.receipt_date).to eq(Time.zone.today - (20.years + 1.day))