Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jcroteau/APPEALS-23894 - Close VeteranRecordRequest tasks for Veterans Readiness and Employment (VRE) business line #18948

Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5846d99
✨ Introduce method object CancelTasksAndDescendants
jcroteau Jun 27, 2023
11ebd18
✨ Introduce rake task remediations:cancel_vrr_tasks_open_for_vre
jcroteau Jun 27, 2023
39cd008
🚚 Move VeteranRecordRequestsOpenForVREQuery into its own namespace
jcroteau Jun 27, 2023
2791072
:bulb: Add to comment
jcroteau Jul 1, 2023
fd89a81
:recycle: Reference Constants::BENEFIT_TYPES instead of local constant
jcroteau Jul 1, 2023
58306c7
:recycle: Extract factory for VRE business line
jcroteau Jul 1, 2023
4e6dc9b
:pencil2: Fix typo
jcroteau Jul 1, 2023
d4b8136
:loud_sound: Log time elapsed
jcroteau Jul 5, 2023
7ca851e
:recycle: Extract constant
jcroteau Jul 5, 2023
f5c6ed6
:recycle: Refactor tests
jcroteau Jul 5, 2023
dcf89c3
:loud_sound: Log total tasks for cancellation
jcroteau Jul 5, 2023
4d04b76
:loud_sound: Log cancelled task ids
jcroteau Jul 6, 2023
28b4eed
:loud_sound: Log errored cancellations
jcroteau Jul 6, 2023
863788a
:recycle: Extract method
jcroteau Jul 6, 2023
4a4540c
:bug: Task #descendants already includes self
jcroteau Jul 6, 2023
008c750
:bug: Only include open tasks in total
jcroteau Jul 6, 2023
ab2f930
:bug: Only log cancellable (open) task ids
jcroteau Jul 6, 2023
e307156
:recycle: Extract method
jcroteau Jul 6, 2023
ebd3baa
:recycle: Replace query with temp
jcroteau Jul 6, 2023
1074d5b
:loud_sound: Log total cancelled
jcroteau Jul 6, 2023
7905710
:recycle: Slide statements into block
jcroteau Jul 6, 2023
a12f8f9
:recycle: Combine logging of cancellable tasks before/after into sing…
jcroteau Jul 6, 2023
fdbeaa0
:recycle: Rearrange method definitions
jcroteau Jul 6, 2023
ac40f89
:recycle: Rename method
jcroteau Jul 6, 2023
229d4be
:bulb: Add comments
jcroteau Jul 6, 2023
c9b1216
:loud_sound: Append logs to stdout
jcroteau Jul 6, 2023
7fbca3a
:necktie: Ensure PaperTrail versions reflect appropriate whodunnit an…
jcroteau Jul 6, 2023
277e309
Merge branch 'master' into jcroteau/APPEALS-23894-close-vrr-tasks-for…
jcroteau Jul 7, 2023
c85ad83
Merge branch 'master' into jcroteau/APPEALS-23894-close-vrr-tasks-for…
jcroteau Jul 10, 2023
d7fc53a
:rotating_light: Lint
jcroteau Jul 10, 2023
fdc394b
:white_check_mark: Fix broken spec
jcroteau Jul 10, 2023
55094c9
Merge branch 'master' into jcroteau/APPEALS-23894-close-vrr-tasks-for…
jcroteau Jul 13, 2023
a2f50de
Merge branch 'master' into jcroteau/APPEALS-23894-close-vrr-tasks-for…
raymond-hughes Jul 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/queries/veteran_record_requests_open_for_vre_query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class VeteranRecordRequestsOpenForVREQuery
# @return [ActiveRecord::Relation] VeteranRecordRequest tasks that are
# both open and assigned to the 'Veterans Readiness and Employment' business
# line (f.k.a 'Vocational Rehabilitation and Employment')
def self.call
vre_business_line =
BusinessLine.where(name: Constants::BENEFIT_TYPES["voc_rehab"])

VeteranRecordRequest.open.where(assigned_to: vre_business_line)
end
end
117 changes: 117 additions & 0 deletions lib/helpers/cancel_tasks_and_descendants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# frozen_string_literal: true

require "securerandom"

class CancelTasksAndDescendants
LOG_TAG = "CancelTasksAndDescendants"

# Cancels all tasks and descendant tasks for given Task relation
#
# @param task_relation [ActiveRecord::Relation] tasks to be cancelled
# @return [NilClass]
def self.call(task_relation = Task.none)
new(task_relation).__send__(:call)
jcroteau marked this conversation as resolved.
Show resolved Hide resolved
end

private

def initialize(task_relation)
@task_relation = task_relation
@request_id = SecureRandom.uuid
@logs = []
end

def call
RequestStore[:current_user] = User.system_user

with_paper_trail_options do
log_time_elapsed { log_task_count_before_and_after { cancel_tasks } }
print_logs_to_stdout
end
end
Comment on lines +24 to +31

This comment was marked as resolved.

This comment was marked as resolved.


# @note Temporarily sets the PaperTrail request options and executes the given
# block. The request options are only in effect on the current thread for
# the duration of the block.
# This is needed so that the PaperTrail `versions` records for cancelled
# tasks reflect the appropriate `whodunnit` and `request_id`.
def with_paper_trail_options(&block)
options = { whodunnit: User.system_user.id,
controller_info: { request_id: @request_id } }

PaperTrail.request(options, &block)
end

def cancel_tasks
@task_relation.find_each do |task|
log_cancelled(task) { task.cancel_task_and_child_subtasks }
rescue StandardError => error
log_errored(task, error)
end
end

def log_cancelled(task, &block)
task_ids = cancellable_descendants_for(task).pluck(:id)
yield(block)
log("Task ids #{task_ids} cancelled successfully")
end

def log_errored(task, error)
task_ids = cancellable_descendants_for(task).pluck(:id)
log("Task ids #{task_ids} not cancelled due to error - #{error}",
level: :error)
end

def cancellable_descendants_for(task)
# Note: The result of `Task #descendants` also includes the instance itself
Task.open.where(id: task.descendants)
end
jcroteau marked this conversation as resolved.
Show resolved Hide resolved

def log_task_count_before_and_after(&block)
initial_count = count_of_cancellable_tasks
log_total_tasks_for_cancellation(initial_count)
yield(block)
final_count = initial_count - count_of_cancellable_tasks
log_cancelled_successfully(final_count)
end

def count_of_cancellable_tasks
sum = 0
@task_relation.find_each do |task|
sum += cancellable_descendants_for(task).count
end
sum
end

def log_total_tasks_for_cancellation(count)
log("Total tasks for cancellation: #{count}")
end

def log_cancelled_successfully(count)
log("Tasks cancelled successfully: #{count}")
end

def log_time_elapsed(&block)
time_elapsed_in_seconds = Benchmark.realtime(&block)
log("Elapsed time (sec): #{time_elapsed_in_seconds}")
end

def log(message, level: :info)
append_to_application_logs(level, message)
append_to_logs_for_stdout(message)
end

def append_to_application_logs(level, message)
Rails.logger.tagged(LOG_TAG, @request_id) do
Rails.logger.public_send(level, message)
end
end

def append_to_logs_for_stdout(message)
@logs << "[#{LOG_TAG}] [#{@request_id}] #{message}"
end

def print_logs_to_stdout
puts @logs
end
end
raymond-hughes marked this conversation as resolved.
Show resolved Hide resolved
13 changes: 13 additions & 0 deletions lib/tasks/remediations/cancel_vrr_tasks_open_for_vre.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require_relative "../../../lib/helpers/cancel_tasks_and_descendants"

namespace :remediations do
desc "Cancel VeteranRecordRequest tasks that are both open and assigned to " \
"the 'Veterans Readiness and Employment' business line"
task :cancel_vrr_tasks_open_for_vre => [:environment] do
CancelTasksAndDescendants.call(
VeteranRecordRequestsOpenForVREQuery.call
)
Comment on lines +9 to +11

This comment was marked as resolved.

end
end
5 changes: 5 additions & 0 deletions spec/factories/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
type { "BusinessLine" }
end

factory :vre_business_line, class: BusinessLine do
type { "BusinessLine" }
name { Constants::BENEFIT_TYPES["voc_rehab"] }
end

factory :hearings_management do
type { "HearingsManagement" }
name { "Hearings Management" }
Expand Down
192 changes: 192 additions & 0 deletions spec/helpers/cancel_tasks_and_descendants_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# frozen_string_literal: true

require "helpers/cancel_tasks_and_descendants"
require "securerandom"

describe CancelTasksAndDescendants do
describe ".call" do
context "when task_relation is not given" do
subject(:call) { described_class.call }

it "assigns RequestStore[:current_user]" do
expect { call }.to change { RequestStore[:current_user] }.
from(nil).to(User.system_user)
end

it "appends appropriate logs to application logs" do
rails_logger = Rails.logger
allow(Rails).to receive(:logger).and_return(rails_logger)

aggregate_failures do
expect(rails_logger).to receive(:info)
.with("Total tasks for cancellation: 0").ordered

expect(rails_logger).not_to receive(:info)
.with(/Task ids \[.+\] cancelled successfully/)

expect(rails_logger).to receive(:info)
.with(/Tasks cancelled successfully: 0/).ordered

expect(rails_logger).to receive(:info)
.with(/Elapsed time \(sec\):/).ordered
end

call
end

it "appends appropriate logs to stdout" do
allow(SecureRandom).to receive(:uuid) { "dummy-request-id" }

expect { call }.to output(
match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Total tasks for cancellation: 0"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Tasks cancelled successfully: 0"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Elapsed time (sec):"))
).to_stdout
end

it { is_expected.to be_nil }
end

context "when task_relation is given " do
subject(:call) { described_class.call(task_relation) }

let(:task_relation) { Task.where(id: [task_1, task_2, task_3]) }
let(:task_1) { create(:veteran_record_request_task) }
let(:task_2) { create(:veteran_record_request_task) }
let(:task_3) { create(:veteran_record_request_task) }

before do
expect(task_relation).to receive(:find_each).at_least(:once)
.and_yield(task_1)
.and_yield(task_2)
.and_yield(task_3)

allow(task_1).to receive(:self_and_descendants) { [task_1] }
allow(task_2).to receive(:self_and_descendants) { [task_2] }
allow(task_3).to receive(:self_and_descendants) { [task_3] }
end

it "cancels each task and its descendants" do
aggregate_failures do
expect(task_1).to receive(:cancel_task_and_child_subtasks)
expect(task_2).to receive(:cancel_task_and_child_subtasks)
expect(task_3).to receive(:cancel_task_and_child_subtasks)
end
call
end

it "sets PaperTrail versions data appropriately for cancelled tasks" do
request_id = SecureRandom.uuid
allow(SecureRandom).to receive(:uuid) { request_id }

call

task_1_version = task_1.versions.last
expect(task_1_version.whodunnit).to eq(User.system_user.id.to_s)
expect(task_1_version.request_id).to eq(request_id)
end

it "appends appropriate logs to application logs" do
rails_logger = Rails.logger
allow(Rails).to receive(:logger).and_return(rails_logger)

aggregate_failures do
expect(rails_logger).to receive(:info)
.with("Total tasks for cancellation: 3").ordered

expect(rails_logger).to receive(:info)
.with(/Task ids \[#{task_1.id}\] cancelled successfully/).ordered

expect(rails_logger).to receive(:info)
.with(/Task ids \[#{task_2.id}\] cancelled successfully/).ordered

expect(rails_logger).to receive(:info)
.with(/Task ids \[#{task_3.id}\] cancelled successfully/).ordered

expect(rails_logger).to receive(:info)
.with(/Tasks cancelled successfully: 3/).ordered

expect(rails_logger).to receive(:info)
.with(/Elapsed time \(sec\):/).ordered
end

call
end

it "appends appropriate logs to stdout" do
allow(SecureRandom).to receive(:uuid) { "dummy-request-id" }

expect { call }.to output(
match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Total tasks for cancellation: 3"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_1.id}] cancelled successfully"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_2.id}] cancelled successfully"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_3.id}] cancelled successfully"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Tasks cancelled successfully: 3"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Elapsed time (sec):"))
).to_stdout
end

context "when a task fails to cancel" do
before do
expect(task_2).to receive(:cancel_task_and_child_subtasks).
and_raise(ActiveModel::ValidationError.new(Task.new))
end

it "does not prevent cancellation of other tasks in the relation" do
aggregate_failures do
expect(task_1).to receive(:cancel_task_and_child_subtasks)
expect(task_3).to receive(:cancel_task_and_child_subtasks)
end
call
end

it "does not raise error" do
expect { call }.not_to raise_error
end

it "appends appropriate logs to application logs" do
rails_logger = Rails.logger
allow(Rails).to receive(:logger).and_return(rails_logger)

aggregate_failures do
expect(rails_logger).to receive(:info)
.with("Total tasks for cancellation: 3").ordered

expect(rails_logger).to receive(:info)
.with(/Task ids \[#{task_1.id}\] cancelled successfully/).ordered

expect(rails_logger).to receive(:error).with(
/Task ids \[#{task_2.id}\] not cancelled due to error - Validation failed/
).ordered

expect(rails_logger).to receive(:info)
.with(/Task ids \[#{task_3.id}\] cancelled successfully/).ordered

expect(rails_logger).to receive(:info)
.with(/Tasks cancelled successfully: 2/).ordered

expect(rails_logger).to receive(:info)
.with(/Elapsed time \(sec\):/).ordered
end

call
end

it "appends appropriate logs to stdout" do
allow(SecureRandom).to receive(:uuid) { "dummy-request-id" }

expect { call }.to output(
match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Total tasks for cancellation: 3"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_1.id}] cancelled successfully"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_2.id}] not cancelled due to error - Validation failed"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_3.id}] cancelled successfully"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Tasks cancelled successfully: 2"))
.and match(Regexp.escape("[CancelTasksAndDescendants] [dummy-request-id] Elapsed time (sec):"))
).to_stdout
end

it { is_expected.to be_nil }
end
end
end
end
18 changes: 18 additions & 0 deletions spec/lib/tasks/remediations/cancel_vrr_tasks_open_for_vre_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

describe "remediations/cancel_vrr_tasks_open_for_vre" do
include_context "rake"

describe "remediations:cancel_vrr_tasks_open_for_vre" do
it "delegates to CancelTasksAndDescendants" do
task_relation = double("task_relation")

expect(CancelTasksAndDescendants::VeteranRecordRequestsOpenForVREQuery).
to receive(:call).and_return(task_relation)

expect(CancelTasksAndDescendants).to receive(:call).with(task_relation)

Rake::Task["remediations:cancel_vrr_tasks_open_for_vre"].invoke
end
end
end
Loading