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))