-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create nightly job for DispositionTask when disposition changes (#9942)
* Create nightly job for DispositionTask when disposition changes * Rename key, skip duplicates, and log hearing IDs * Rely on existing relationship constraint * Create two smaller functions * Fix fasterer complaint * Use delegation to acceess hearing * Add test stub * Add final test for logging to job * Start adding tests * Move function into task scope * Add tests for .modify_task_by_dispisition * Add more tests * Update job now that PR for #9540 has been merged * Lengthen variable names and fix test * Add job to jobs controller * update method name * refactor task_count_for, use symbols consistently
- Loading branch information
Showing
5 changed files
with
437 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# frozen_string_literal: true | ||
|
||
require "action_view" | ||
|
||
class HearingDispositionChangeJob < CaseflowJob | ||
# For time_ago_in_words() | ||
include ActionView::Helpers::DateHelper | ||
queue_as :low_priority | ||
|
||
def perform | ||
start_time = Time.zone.now | ||
error_count = 0 | ||
task_count_keys = Constants.HEARING_DISPOSITION_TYPES.to_h.values.map(&:to_sym) + | ||
[:between_one_and_two_days_old, :stale, :unknown_disposition] | ||
task_count_for = Hash[task_count_keys.map { |key| [key, 0] }] | ||
|
||
# Set user to system_user to avoid sensitivity errors | ||
RequestStore.store[:current_user] = User.system_user | ||
|
||
tasks = DispositionTask.ready_for_action | ||
hearing_ids = tasks.map { |task| task.hearing.id } | ||
|
||
tasks.each do |task| | ||
label = update_task_by_hearing_disposition(task) | ||
task_count_for[label.to_sym] += 1 | ||
rescue StandardError => error | ||
# Rescue from errors so we attempt to change disposition even if we hit individual errors. | ||
Raven.capture_exception(error, extra: { task_id: task.id }) | ||
error_count += 1 | ||
end | ||
|
||
log_info(start_time, task_count_for, error_count, hearing_ids) | ||
rescue StandardError => error | ||
log_info(start_time, task_count_for, error_count, hearing_ids, error) | ||
end | ||
|
||
# rubocop:disable Metrics/CyclomaticComplexity | ||
def update_task_by_hearing_disposition(task) | ||
hearing = task.hearing | ||
label = hearing.disposition | ||
|
||
# rubocop:disable Lint/EmptyWhen | ||
case hearing.disposition | ||
when Constants.HEARING_DISPOSITION_TYPES.held | ||
task.hold! | ||
when Constants.HEARING_DISPOSITION_TYPES.cancelled | ||
task.cancel! | ||
when Constants.HEARING_DISPOSITION_TYPES.postponed | ||
# Postponed hearings should be acted on immediately and the related tasks should be closed. Do not take any | ||
# action here. | ||
when Constants.HEARING_DISPOSITION_TYPES.no_show | ||
task.no_show! | ||
when nil | ||
# We allow judges and hearings staff 2 days to make changes to the hearing's disposition. If it has been more | ||
# than 2 days since the hearing was held and there is no disposition then remind the hearings staff. | ||
label = if hearing.scheduled_for < 48.hours.ago | ||
# Logic will be added as part of #9833. | ||
:stale | ||
else | ||
:between_one_and_two_days_old | ||
end | ||
else | ||
# Expect to never reach this block since all dispositions should be accounted for above. If we run into this | ||
# case we ignore it and will investigate and potentially incorporate that fix here. Until then we're fine. | ||
label = :unknown_disposition | ||
end | ||
# rubocop:enable Lint/EmptyWhen | ||
|
||
label | ||
end | ||
# rubocop:enable Metrics/CyclomaticComplexity | ||
|
||
def log_info(start_time, task_count_for, error_count, hearing_ids, err = nil) | ||
duration = time_ago_in_words(start_time) | ||
result = err ? "failed" : "completed" | ||
|
||
msg = "#{self.class.name} #{result} after running for #{duration}." | ||
task_count_for.each do |label, task_count| | ||
msg += " Processed #{task_count} #{label.to_s.humanize} hearings." | ||
end | ||
msg += " Encountered errors for #{error_count} hearings." | ||
msg += " Fatal error: #{err.message}" if err | ||
|
||
Rails.logger.info(msg) | ||
Rails.logger.info(hearing_ids) | ||
Rails.logger.info(err.backtrace.join("\n")) if err | ||
|
||
slack_service.send_notification(msg) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,269 @@ | ||
# frozen_string_literal: true | ||
|
||
require "rails_helper" | ||
|
||
describe HearingDispositionChangeJob do | ||
def create_disposition_task_ancestry(disposition: nil, scheduled_for: nil, associated_hearing: true) | ||
appeal = FactoryBot.create(:appeal) | ||
root_task = FactoryBot.create(:root_task, appeal: appeal) | ||
distribution_task = FactoryBot.create(:distribution_task, appeal: appeal, parent: root_task) | ||
parent_hearing_task = FactoryBot.create(:hearing_task, appeal: appeal, parent: distribution_task) | ||
|
||
hearing = FactoryBot.create(:hearing, appeal: appeal, disposition: disposition) | ||
if scheduled_for | ||
hearing = FactoryBot.create( | ||
:hearing, | ||
appeal: appeal, | ||
disposition: disposition, | ||
scheduled_time: scheduled_for | ||
) | ||
hearing_day = FactoryBot.create(:hearing_day, scheduled_for: scheduled_for) | ||
hearing.update!(hearing_day: hearing_day) | ||
end | ||
|
||
HearingTaskAssociation.create!(hearing: hearing, hearing_task: parent_hearing_task) if associated_hearing | ||
DispositionTask.create!(appeal: appeal, parent: parent_hearing_task, assigned_to: Bva.singleton) | ||
end | ||
|
||
describe ".update_task_by_hearing_disposition" do | ||
subject { HearingDispositionChangeJob.new.update_task_by_hearing_disposition(task) } | ||
|
||
context "when hearing has a disposition" do | ||
let(:task) { create_disposition_task_ancestry(disposition: disposition) } | ||
|
||
context "when disposition is held" do | ||
let(:disposition) { Constants.HEARING_DISPOSITION_TYPES.held } | ||
it "returns a label matching the hearing disposition and call DispositionTask.hold!" do | ||
expect(task).to receive(:hold!).exactly(1).times | ||
expect(subject).to eq(disposition) | ||
end | ||
end | ||
|
||
context "when disposition is cancelled" do | ||
let(:disposition) { Constants.HEARING_DISPOSITION_TYPES.cancelled } | ||
it "returns a label matching the hearing disposition and call DispositionTask.cancel!" do | ||
expect(task).to receive(:cancel!).exactly(1).times | ||
expect(subject).to eq(disposition) | ||
end | ||
end | ||
|
||
context "when disposition is postponed" do | ||
let(:disposition) { Constants.HEARING_DISPOSITION_TYPES.postponed } | ||
it "returns a label matching the hearing disposition and not change the task" do | ||
attributes_before = task.attributes | ||
expect(subject).to eq(disposition) | ||
expect(task.attributes).to eq(attributes_before) | ||
end | ||
end | ||
|
||
context "when disposition is no_show" do | ||
let(:disposition) { Constants.HEARING_DISPOSITION_TYPES.no_show } | ||
it "returns a label matching the hearing disposition and call DispositionTask.no_show!" do | ||
expect(task).to receive(:no_show!).exactly(1).times | ||
expect(subject).to eq(disposition) | ||
end | ||
end | ||
|
||
context "when the disposition is not an expected disposition" do | ||
let(:disposition) { "FAKE_DISPOSITION" } | ||
it "returns a label indicating that the hearing disposition is unknown and not change the task" do | ||
attributes_before = task.attributes | ||
expect(subject).to eq(:unknown_disposition) | ||
expect(task.attributes).to eq(attributes_before) | ||
end | ||
end | ||
end | ||
|
||
context "when hearing has no disposition" do | ||
let(:task) { create_disposition_task_ancestry(disposition: nil, scheduled_for: scheduled_for) } | ||
|
||
context "when hearing was scheduled to take place more than 2 days ago" do | ||
let(:scheduled_for) { 3.days.ago } | ||
|
||
it "returns a label indicating that the hearing is stale and does not change the task" do | ||
attributes_before = task.attributes | ||
expect(subject).to eq(:stale) | ||
expect(task.attributes).to eq(attributes_before) | ||
end | ||
end | ||
|
||
context "when hearing was scheduled to take place less than 2 days ago" do | ||
let(:scheduled_for) { 25.hours.ago } | ||
|
||
it "returns a label indicating that the hearing was recently held and does not change the task" do | ||
attributes_before = task.attributes | ||
expect(subject).to eq(:between_one_and_two_days_old) | ||
expect(task.attributes).to eq(attributes_before) | ||
end | ||
end | ||
end | ||
end | ||
|
||
describe ".log_info" do | ||
let(:start_time) { 5.minutes.ago } | ||
let(:task_count_for) { {} } | ||
let(:error_count) { 0 } | ||
let(:hearing_ids) { [] } | ||
let(:error) { nil } | ||
|
||
context "when the job runs successfully" do | ||
it "logs and sends the correct message to slack" do | ||
slack_msg = "" | ||
allow_any_instance_of(SlackService).to receive(:send_notification) { |_, first_arg| slack_msg = first_arg } | ||
|
||
expect(Rails.logger).to receive(:info).exactly(2).times | ||
|
||
HearingDispositionChangeJob.new.log_info(start_time, task_count_for, error_count, hearing_ids, error) | ||
|
||
expected_msg = "HearingDispositionChangeJob completed after running for .*." \ | ||
" Encountered errors for #{error_count} hearings." | ||
expect(slack_msg).to match(/#{expected_msg}/) | ||
end | ||
end | ||
|
||
context "when there is are elements in the input task_count_for hash" do | ||
let(:task_count_for) { { first_key: 0, second_key: 13 } } | ||
|
||
it "includes a sentence in the output message for each element of the hash" do | ||
slack_msg = "" | ||
allow_any_instance_of(SlackService).to receive(:send_notification) { |_, first_arg| slack_msg = first_arg } | ||
|
||
HearingDispositionChangeJob.new.log_info(start_time, task_count_for, error_count, hearing_ids, error) | ||
|
||
expected_msg = "HearingDispositionChangeJob completed after running for .*." \ | ||
" Processed 0 First key hearings." \ | ||
" Processed 13 Second key hearings." \ | ||
" Encountered errors for #{error_count} hearings." | ||
expect(slack_msg).to match(/#{expected_msg}/) | ||
end | ||
end | ||
|
||
context "when the job encounters a fatal error" do | ||
let(:err_msg) { "Example error text" } | ||
# Throw and then catch the error so it has a stack trace. | ||
let(:error) do | ||
fail StandardError, err_msg | ||
rescue StandardError => e | ||
e | ||
end | ||
|
||
it "logs an error message and sends the correct message to slack" do | ||
slack_msg = "" | ||
allow_any_instance_of(SlackService).to receive(:send_notification) { |_, first_arg| slack_msg = first_arg } | ||
|
||
expect(Rails.logger).to receive(:info).exactly(3).times | ||
|
||
HearingDispositionChangeJob.new.log_info(start_time, task_count_for, error_count, hearing_ids, error) | ||
|
||
expected_msg = "HearingDispositionChangeJob failed after running for .*." \ | ||
" Encountered errors for #{error_count} hearings. Fatal error: #{err_msg}" | ||
expect(slack_msg).to match(/#{expected_msg}/) | ||
end | ||
end | ||
end | ||
|
||
describe ".perform" do | ||
subject { HearingDispositionChangeJob.new.perform } | ||
|
||
context "when there is an error outside of the loop" do | ||
let(:error_msg) { "FAKE ERROR MESSAGE HERE" } | ||
|
||
before { allow(DispositionTask).to receive(:ready_for_action).and_raise(error_msg) } | ||
|
||
it "sends the correct number of arguments to log_info" do | ||
args = Array.new(5, anything) | ||
expect_any_instance_of(HearingDispositionChangeJob).to receive(:log_info).with(*args).exactly(1).times | ||
subject | ||
end | ||
end | ||
|
||
context "when the job runs successfully" do | ||
let(:not_ready_for_action_count) { 4 } | ||
let(:error_count) { 13 } | ||
let(:task_count_for_dispositions) do | ||
{ | ||
Constants.HEARING_DISPOSITION_TYPES.held.to_sym => 8, | ||
Constants.HEARING_DISPOSITION_TYPES.cancelled.to_sym => 2, | ||
Constants.HEARING_DISPOSITION_TYPES.postponed.to_sym => 3, | ||
Constants.HEARING_DISPOSITION_TYPES.no_show.to_sym => 5 | ||
} | ||
end | ||
let(:task_count_for_others) do | ||
{ | ||
between_one_and_two_days_old: 6, | ||
stale: 7, | ||
unknown_disposition: 1 | ||
} | ||
end | ||
let(:task_count_for) { task_count_for_dispositions.merge(task_count_for_others) } | ||
|
||
before do | ||
not_ready_for_action_count.times do | ||
create_disposition_task_ancestry( | ||
disposition: Constants.HEARING_DISPOSITION_TYPES.held, | ||
scheduled_for: nil, | ||
associated_hearing: false | ||
) | ||
end | ||
|
||
ready_for_action_time = 36.hours.ago | ||
task_count_for_dispositions.each do |disposition, task_count| | ||
task_count.times do | ||
create_disposition_task_ancestry( | ||
disposition: disposition, | ||
scheduled_for: ready_for_action_time, | ||
associated_hearing: true | ||
) | ||
end | ||
end | ||
|
||
task_count_for_others[:between_one_and_two_days_old].times do | ||
create_disposition_task_ancestry( | ||
disposition: nil, | ||
scheduled_for: ready_for_action_time, | ||
associated_hearing: true | ||
) | ||
end | ||
|
||
task_count_for_others[:stale].times do | ||
create_disposition_task_ancestry( | ||
disposition: nil, | ||
scheduled_for: 5.days.ago, | ||
associated_hearing: true | ||
) | ||
end | ||
|
||
task_count_for_others[:unknown_disposition].times do | ||
create_disposition_task_ancestry( | ||
disposition: "FAKE_DISPOSITION", | ||
scheduled_for: ready_for_action_time, | ||
associated_hearing: true | ||
) | ||
end | ||
|
||
hearing_ids_to_error = Array.new(error_count) do | ||
create_disposition_task_ancestry( | ||
disposition: Constants.HEARING_DISPOSITION_TYPES.held, | ||
scheduled_for: ready_for_action_time, | ||
associated_hearing: true | ||
).hearing.id | ||
end | ||
|
||
disposition_for_hearing = Hearing.all.map { |hearing| [hearing.id, hearing.disposition] }.to_h | ||
|
||
allow_any_instance_of(Hearing).to receive(:disposition) do |hearing| | ||
fail "FAKE ERROR MESSAGE" if hearing_ids_to_error.include?(hearing.id) | ||
|
||
disposition_for_hearing[hearing.id] | ||
end | ||
end | ||
|
||
it "sends the correct arguments to log_info" do | ||
expect_any_instance_of(HearingDispositionChangeJob).to( | ||
receive(:log_info).with(anything, task_count_for, error_count, anything).exactly(1).times | ||
) | ||
subject | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.