Skip to content

Commit

Permalink
Add and populate aod_based_on_age column (#14763)
Browse files Browse the repository at this point in the history
* add aod_based_on_age column to appeals
* [Part 2] Populate age_aod column (#14764)
* add and use conditionally_set_aod_based_on_age
* add SetAppealAgeAodJob
* enable finer access to reasons for AOD
* exclude AttorneyClaimant from AOD logic
* improve test coverage
* handle case where aod_based_on_age changes to false
  • Loading branch information
yoomlam authored Jul 28, 2020
1 parent 30f7d8d commit c79035c
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 15 deletions.
1 change: 1 addition & 0 deletions app/controllers/api/v1/jobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Api::V1::JobsController < Api::ApplicationController
"prepare_establish_claim" => PrepareEstablishClaimTasksJob,
"reassign_old_tasks" => ReassignOldTasksJob,
"retrieve_documents_for_reader" => RetrieveDocumentsForReaderJob,
"set_appeal_age_aod" => SetAppealAgeAodJob,
"stats_collector" => StatsCollectorJob,
"sync_intake" => SyncIntakeJob,
"sync_reviews" => SyncReviewsJob,
Expand Down
66 changes: 66 additions & 0 deletions app/jobs/set_appeal_age_aod_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

# Early morning job that checks if the claimant meets the Advance-On-Docket age criteria.
# If criteria is satisfied, all active appeals associated with claimant will be marked as AOD.
# This job also handles the scenario where a claimant's DOB is updated such that their appeal(s) are no longer AOD.
class SetAppealAgeAodJob < CaseflowJob
include ActionView::Helpers::DateHelper

def perform
RequestStore.store[:current_user] = User.system_user

aod_appeals_to_unset = appeals_to_unset_age_based_aod
detail_msg = "IDs of appeals to remove age-related AOD: #{aod_appeals_to_unset.pluck(:id)}"
aod_appeals_to_unset.update_all(aod_based_on_age: false, updated_at: Time.now.utc)

# We expect there to be only one claimant on an appeal. Any claimant meeting the age criteria will cause AOD.
appeals_for_aod = appeals_to_set_age_based_aod
detail_msg += "\nIDs of appeals to be updated with age-related AOD: #{appeals_for_aod.pluck(:id)}"
appeals_for_aod.update_all(aod_based_on_age: true, updated_at: Time.now.utc)

log_success(detail_msg)
rescue StandardError => error
log_error(self.class.name, error, detail_msg)
end

protected

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

slack_service.send_notification("[INFO] #{msg}")
end

def log_error(collector_name, err, details)
duration = time_ago_in_words(start_time)
msg = "#{collector_name} failed after running for #{duration}. Fatal error: #{err.message}.\n#{details}"
Rails.logger.info(msg)
Rails.logger.info(err.backtrace.join("\n"))

Raven.capture_exception(err, extra: { stats_collector_name: collector_name })

slack_service.send_notification("[ERROR] #{msg}")
end

private

def appeals_to_unset_age_based_aod
active_appeals_with_age_based_aod.joins(claimants: :person).where("people.date_of_birth > ?", 75.years.ago)
end

def active_appeals_with_age_based_aod
Appeal.active.where(aod_based_on_age: true)
end

def appeals_to_set_age_based_aod
active_appeals_without_age_based_aod.joins(claimants: :person).where("people.date_of_birth <= ?", 75.years.ago)
end

def active_appeals_without_age_based_aod
# `aod_based_on_age` is initially nil
# `aod_based_on_age` being false means that it was once true (in the case where the claimant's DOB was updated)
Appeal.active.where(aod_based_on_age: [nil, false])
end
end
12 changes: 11 additions & 1 deletion app/models/appeal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class Appeal < DecisionReview
"de_novo": "de_novo"
}

after_create :conditionally_set_aod_based_on_age

after_save :set_original_stream_data

with_options on: :intake_review do
Expand Down Expand Up @@ -267,8 +269,16 @@ def regional_office_key
nil
end

def conditionally_set_aod_based_on_age
updated_aod_based_on_age = claimant&.advanced_on_docket_based_on_age?
update(aod_based_on_age: updated_aod_based_on_age) if aod_based_on_age != updated_aod_based_on_age
end

def advanced_on_docket?
claimant&.advanced_on_docket?(receipt_date)
conditionally_set_aod_based_on_age
# One of the AOD motion reasons is 'age'. Keep interrogation of any motions separate from `aod_based_on_age`,
# which reflects `claimant.advanced_on_docket_based_on_age?`.
aod_based_on_age || claimant&.advanced_on_docket_motion_granted?(receipt_date)
end

# Prefer aod? over aod going forward, as this function returns a boolean
Expand Down
12 changes: 12 additions & 0 deletions app/models/attorney_claimant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ class AttorneyClaimant < Claimant

delegate :name, to: :bgs_attorney

def advanced_on_docket?(_appeal_receipt_date)
false
end

def advanced_on_docket_based_on_age?
false
end

def advanced_on_docket_motion_granted?(_appeal_receipt_date)
false
end

private

def find_power_of_attorney
Expand Down
3 changes: 3 additions & 0 deletions app/models/claimant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

##
# The Claimant model associates a claimant to a decision review.
# There are several subclasses, such as VeteranClaimant, DependentClaimant, and AttorneyClaimant.

class Claimant < CaseflowRecord
include HasDecisionReviewUpdatedSince
Expand Down Expand Up @@ -43,6 +44,8 @@ def person

delegate :date_of_birth,
:advanced_on_docket?,
:advanced_on_docket_based_on_age?,
:advanced_on_docket_motion_granted?,
:name,
:first_name,
:last_name,
Expand Down
6 changes: 5 additions & 1 deletion app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def find_or_create_by_ssn(ssn)
end

def advanced_on_docket?(appeal_receipt_date)
advanced_on_docket_based_on_age? || AdvanceOnDocketMotion.granted_for_person?(id, appeal_receipt_date)
advanced_on_docket_based_on_age? || advanced_on_docket_motion_granted?(appeal_receipt_date)
end

def date_of_birth
Expand Down Expand Up @@ -105,6 +105,10 @@ def advanced_on_docket_based_on_age?
date_of_birth && date_of_birth < 75.years.ago
end

def advanced_on_docket_motion_granted?(appeal_receipt_date)
AdvanceOnDocketMotion.granted_for_person?(id, appeal_receipt_date)
end

def found?
return false if not_found?

Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20200724030847_add_age_aod_column_to_appeals.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddAgeAodColumnToAppeals < ActiveRecord::Migration[5.2]
def change
add_column :appeals, :aod_based_on_age, :boolean, comment: "If true, appeal is advance-on-docket due to claimant's age."
end
end
5 changes: 5 additions & 0 deletions db/migrate/20200724031142_add_age_aod_index_to_appeals.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddAgeAodIndexToAppeals < Caseflow::Migration
def change
add_safe_index :appeals, :aod_based_on_age
end
end
2 changes: 2 additions & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
end

create_table "appeals", comment: "Decision reviews intaken for AMA appeals to the board (also known as a notice of disagreement).", force: :cascade do |t|
t.boolean "aod_based_on_age", comment: "If true, appeal is advance-on-docket due to claimant's age."
t.string "closest_regional_office", comment: "The code for the regional office closest to the Veteran on the appeal."
t.datetime "created_at"
t.date "docket_range_date", comment: "Date that appeal was added to hearing docket range."
Expand All @@ -111,6 +112,7 @@
t.uuid "uuid", default: -> { "uuid_generate_v4()" }, null: false, comment: "The universally unique identifier for the appeal, which can be used to navigate to appeals/appeal_uuid. This allows a single ID to determine an appeal whether it is a legacy appeal or an AMA appeal."
t.string "veteran_file_number", null: false, comment: "The VBA corporate file number of the Veteran for this review. There can sometimes be more than one file number per Veteran."
t.boolean "veteran_is_not_claimant", comment: "Selected by the user during intake, indicates whether the Veteran is the claimant, or if the claimant is someone else such as a dependent. Must be TRUE if Veteran is deceased."
t.index ["aod_based_on_age"], name: "index_appeals_on_aod_based_on_age"
t.index ["docket_type"], name: "index_appeals_on_docket_type"
t.index ["established_at"], name: "index_appeals_on_established_at"
t.index ["updated_at"], name: "index_appeals_on_updated_at"
Expand Down
1 change: 1 addition & 0 deletions docs/schema/caseflow.csv
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ api_views,source,string,,,,,,
api_views,updated_at,datetime,,,,,,
api_views,vbms_id,string,,,,,,
appeals,,,,,,,,Decision reviews intaken for AMA appeals to the board (also known as a notice of disagreement).
appeals,aod_based_on_age,boolean,,,,,x,"If true, appeal is advance-on-docket due to claimant's age."
appeals,closest_regional_office,string,,,,,,The code for the regional office closest to the Veteran on the appeal.
appeals,created_at,datetime,,,,,,
appeals,docket_range_date,date,,,,,,Date that appeal was added to hearing docket range.
Expand Down
70 changes: 70 additions & 0 deletions spec/jobs/set_appeal_age_aod_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

describe SetAppealAgeAodJob, :postgres do
include_context "Metrics Reports"

# rubocop:disable Metrics/LineLength
let(:success_msg) do
"[INFO] SetAppealAgeAodJob completed after running for less than a minute."
end
# rubocop:enable Metrics/LineLength

describe "#perform" do
let(:non_aod_appeal) { create(:appeal, :with_schedule_hearing_tasks) }

# appeal with wrong date-of-birth causing AOD; it is fixed in the before block
let(:age_aod_appeal_wrong_dob) { create(:appeal, :with_schedule_hearing_tasks, :advanced_on_docket_due_to_age) }

let(:age_aod_appeal) { create(:appeal, :with_schedule_hearing_tasks, :advanced_on_docket_due_to_age) }
let(:motion_aod_appeal) { create(:appeal, :with_schedule_hearing_tasks, :advanced_on_docket_due_to_motion) }

let(:inactive_age_aod_appeal) { create(:appeal, :advanced_on_docket_due_to_age) }
let(:cancelled_age_aod_appeal) { create(:appeal, :advanced_on_docket_due_to_age, :cancelled) }

before do
allow_any_instance_of(SlackService).to receive(:send_notification) { |_, first_arg| @slack_msg = first_arg }

age_aod_appeal_wrong_dob.update(aod_based_on_age: true)
# simulate date-of-birth being corrected
age_aod_appeal_wrong_dob.claimant.person.update(date_of_birth: 50.years.ago)
end

it "sets aod_based_on_age for only active appeals with a claimant that satisfies the age criteria" do
expect(non_aod_appeal.active?).to eq(true)
expect(age_aod_appeal.active?).to eq(true)
expect(motion_aod_appeal.active?).to eq(true)
expect(inactive_age_aod_appeal.active?).to eq(false)
expect(cancelled_age_aod_appeal.active?).to eq(false)

expect(age_aod_appeal_wrong_dob.aod_based_on_age).to eq(true)

described_class.perform_now
expect(@slack_msg).to include(success_msg)

# `aod_based_on_age` will be nil
# `aod_based_on_age` being false means that it was once true (in the case where the claimant's DOB was updated)
expect(non_aod_appeal.reload.aod_based_on_age).not_to eq(true)
expect(inactive_age_aod_appeal.reload.aod_based_on_age).not_to eq(true)

expect(age_aod_appeal.reload.aod_based_on_age).to eq(true)
expect(motion_aod_appeal.reload.aod_based_on_age).to eq(false)

expect(age_aod_appeal_wrong_dob.reload.aod_based_on_age).to eq(false)
end

context "when the entire job fails" do
let(:error_msg) { "Some dummy error" }

it "sends a message to Slack that includes the error" do
slack_msg = ""
allow_any_instance_of(SlackService).to receive(:send_notification) { |_, first_arg| slack_msg = first_arg }

allow_any_instance_of(described_class).to receive(:appeals_to_set_age_based_aod).and_raise(error_msg)
described_class.perform_now

expected_msg = "#{described_class.name} failed after running for .*. Fatal error: #{error_msg}"
expect(slack_msg).to match(/#{expected_msg}/)
end
end
end
end
23 changes: 15 additions & 8 deletions spec/models/appeal_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -455,23 +455,30 @@
end

context "#advanced_on_docket?" do
context "when a claimant is advanced_on_docket?" do
let(:appeal) do
create(:appeal, claimants: [create(:claimant, :advanced_on_docket_due_to_age)])
end
context "when a claimant is advanced_on_docket? due to age" do
let(:appeal) { create(:appeal, claimants: [create(:claimant, :advanced_on_docket_due_to_age)]) }

it "returns true" do
expect(appeal.advanced_on_docket?).to eq(true)
expect(appeal.aod_based_on_age).to eq(true)
end
end

context "when no claimant is advanced_on_docket?" do
let(:appeal) do
create(:appeal)
end
context "when no claimant is advanced_on_docket? due to age" do
let(:appeal) { create(:appeal) }

it "returns false" do
expect(appeal.advanced_on_docket?).to eq(false)
expect(appeal.aod_based_on_age).to eq(false)
end
end

context "when a claimant is advanced_on_docket? due to motion" do
let(:appeal) { create(:appeal, :advanced_on_docket_due_to_motion) }

it "returns true" do
expect(appeal.advanced_on_docket?).to eq(true)
expect(appeal.aod_based_on_age).to eq(false)
end
end
end
Expand Down
64 changes: 59 additions & 5 deletions spec/models/claimant_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,25 +104,63 @@
end

context "#advanced_on_docket?" do
context "when claimant is over 75 years old" do
context "when claimant satisfies AOD age criteria" do
let(:claimant) { create(:claimant, :advanced_on_docket_due_to_age) }

it "returns true" do
claimant = create(:claimant, :advanced_on_docket_due_to_age)
expect(claimant.advanced_on_docket?(1.year.ago)).to eq(true)
expect(claimant.advanced_on_docket_based_on_age?).to eq(true)
end
end

context "when claimant has motion granted" do
it "returns true" do
claimant = create(:claimant)
let(:claimant) { create(:claimant) }

before do
create(:advance_on_docket_motion, person_id: claimant.person.id, granted: true)
end

it "returns true" do
expect(claimant.advanced_on_docket?(1.year.ago)).to eq(true)
expect(claimant.advanced_on_docket_based_on_age?).to eq(false)
expect(claimant.advanced_on_docket_motion_granted?(1.year.ago)).to eq(true)
end
end

context "when claimant is younger than 75 years old and has no motion granted" do
let(:claimant) { create(:claimant) }

it "returns false" do
expect(claimant.advanced_on_docket?(1.year.ago)).to eq(false)
expect(claimant.advanced_on_docket_based_on_age?).to eq(false)
expect(claimant.advanced_on_docket_motion_granted?(1.year.ago)).to eq(false)
end
end

context "when claimant satisfies AOD age criteria and has motion granted" do
let(:claimant) { create(:claimant, :advanced_on_docket_due_to_age) }

before do
create(:advance_on_docket_motion, person_id: claimant.person.id, granted: true)
end

it "returns true" do
expect(claimant.advanced_on_docket?(1.year.ago)).to eq(true)
expect(claimant.advanced_on_docket_based_on_age?).to eq(true)
expect(claimant.advanced_on_docket_motion_granted?(1.year.ago)).to eq(true)
end
end

context "when AttorneyClaimant satisfies AOD age criteria and has motion granted" do
let(:claimant) { create(:claimant, :advanced_on_docket_due_to_age, type: "AttorneyClaimant") }

before do
create(:advance_on_docket_motion, person_id: claimant.person.id, granted: true)
end

it "returns false" do
claimant = create(:claimant)
expect(claimant.advanced_on_docket_based_on_age?).to eq(false)
expect(claimant.advanced_on_docket_motion_granted?(1.year.ago)).to eq(false)
expect(claimant.advanced_on_docket?(1.year.ago)).to eq(false)
end
end
Expand Down Expand Up @@ -165,6 +203,22 @@
expect(bgs_service).to have_received(:fetch_poas_by_participant_ids).once
end
end

context "when claimant is AttorneyClaimant" do
let(:claimant) { create(:claimant, :advanced_on_docket_due_to_age, type: "AttorneyClaimant") }

before do
create(:bgs_attorney, participant_id: claimant.participant_id, name: "JOHN SMITH")
end

it "returns name of AttorneyClaimant" do
expect(claimant.name).to eq "JOHN SMITH"
end

it "returns BgsPowerOfAttorney" do
expect(subject).to be_a BgsPowerOfAttorney
end
end
end

context "#valid?" do
Expand Down

0 comments on commit c79035c

Please sign in to comment.