diff --git a/.circleci/config.yml b/.circleci/config.yml
index bbf3b835ef0..33a62028631 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -184,7 +184,7 @@ jobs:
# This is the circleci provided Redis container.
- image: circleci/redis:4.0.10
- parallelism: 9
+ parallelism: 12
resource_class: large
steps:
- attach_workspace:
@@ -212,17 +212,17 @@ jobs:
~/project/ci-bin/capture-log "bundle exec rake spec:setup_vacols"
- run:
- name: RSpec
+ name: RSpec via knapsack_pro Queue Mode
command: |
mkdir -p ~/test-results/rspec
- testfiles=$(circleci tests glob "spec/**/*spec.rb" | circleci tests split --split-by=timings)
- echo $testfiles > ~/project/tmp/testfiles/rspec_testfiles.txt
- ~/project/ci-bin/capture-log "bundle exec rspec --no-color --format documentation --format RspecJunitFormatter -o ~/test-results/rspec/rspec.xml -- ${testfiles}"
- # Uncomment to profile test performance in CircleCI
- #environment:
- # RDOC: 1
- # RD_PROF: 1
- # FPROF: 1
+ export RAILS_ENV=test
+ bundle exec rake "knapsack_pro:queue:rspec[--format documentation --no-color --format RspecJunitFormatter --out tmp/rspec.xml]"
+ environment:
+ KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
+ # Uncomment to profile test performance in CircleCI
+ # RDOC: 1
+ # RD_PROF: 1
+ # FPROF: 1
- store_test_results:
name: Store test results as summary
@@ -248,10 +248,6 @@ jobs:
name: Store bullet log
path: ~/project/log/bullet.log
- - store_artifacts:
- name: Store testfile ordering
- path: ~/project/tmp/testfiles
-
- store_artifacts:
name: Store run logs
path: ~/logs
diff --git a/.reek.yml b/.reek.yml
index d21f6cdf805..43865120642 100644
--- a/.reek.yml
+++ b/.reek.yml
@@ -89,6 +89,8 @@ detectors:
- FetchDocumentsForReaderJob#fetch_for_appeal
- FetchHearingLocationsForVeteransJob#sleep_before_retry_on_limit_error
- HearingSerializerBase
+ - HearingSchedule::GenerateHearingDaysSchedule#generate_co_hearing_days_schedule
+ - HearingSchedule::GenerateHearingDaysSchedule#get_fallback_date_for_co
- LegacyDocket#count
- RedistributedCase#legacy_appeal_relevant_tasks
- LegacyAppeal#cancel_open_caseflow_tasks!
diff --git a/Gemfile b/Gemfile
index 2fa418dab66..3eb9633a409 100644
--- a/Gemfile
+++ b/Gemfile
@@ -103,6 +103,7 @@ group :test, :development, :demo do
gem "jshint", platforms: :ruby
gem "pry"
gem "pry-byebug"
+ gem "rails-erd"
gem "rb-readline"
gem "rspec"
gem "rspec-rails"
@@ -128,11 +129,11 @@ group :development do
gem "fasterer", require: false
gem "foreman"
gem "meta_request"
- gem "rails-erd"
gem "ruby-prof", "~> 1.4"
end
group :test do
+ gem "knapsack_pro"
# For retrying failed feature tests. Read more: https://github.com/NoRedInk/rspec-retry
gem "rspec-retry"
gem "webmock"
diff --git a/Gemfile.lock b/Gemfile.lock
index 9fc8b37b528..d6efc264be7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -355,6 +355,8 @@ GEM
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
+ knapsack_pro (2.8.0)
+ rake
kramdown (2.3.0)
rexml
kramdown-parser-gfm (1.1.0)
@@ -698,6 +700,7 @@ DEPENDENCIES
jshint
json_schemer (~> 0.2.16)
kaminari
+ knapsack_pro
meta_request
moment_timezone-rails
multiverse
diff --git a/README.md b/README.md
index fb6fcfe8dec..652e5c43bdb 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# Caseflow
-[![CircleCI](https://circleci.com/gh/department-of-veterans-affairs/caseflow/tree/master.svg?style=svg)](https://circleci.com/gh/department-of-veterans-affairs/caseflow/tree/master)
+[![CircleCI](https://circleci.com/gh/department-of-veterans-affairs/caseflow/tree/master.svg?style=svg)](https://circleci.com/gh/department-of-veterans-affairs/caseflow/tree/master) [![Knapsack Pro Parallel CI builds for Caseflow RSpec Tests](https://img.shields.io/badge/Knapsack%20Pro-Parallel%20%2F%20Caseflow%20RSpec%20Tests-%230074ff)](https://knapsackpro.com/dashboard/organizations/1654/projects/1260/test_suites/1783/builds?utm_campaign=organization-id-1654&utm_content=test-suite-id-1783&utm_medium=readme&utm_source=knapsack-pro-badge&utm_term=project-id-1260)
Caseflow is a suite of web-based tools to manage VA appeals. It's currently in development by the Appeals Modernization team (est. 2016). It will replace the current system of record for appeals, the Veterans Appeals Control and Location System (VACOLS), which was created in 1979 on now-outdated infrastructure. Additionally, Caseflow will allow the Board of Veterans' Appeals to process appeals under the new guidelines created by the Veterans Appeals Improvement and Modernization Act of 2017, which goes into effect February 14th, 2019.
diff --git a/app/controllers/intakes_controller.rb b/app/controllers/intakes_controller.rb
index e6b85012339..b593cbc7356 100644
--- a/app/controllers/intakes_controller.rb
+++ b/app/controllers/intakes_controller.rb
@@ -111,7 +111,8 @@ def index_props
covidTimelinessExemption: FeatureToggle.enabled?(:covid_timeliness_exemption, user: current_user),
verifyUnidentifiedIssue: FeatureToggle.enabled?(:verify_unidentified_issue, user: current_user),
attorneyFees: FeatureToggle.enabled?(:attorney_fees, user: current_user),
- establishFiduciaryEps: FeatureToggle.enabled?(:establish_fiduciary_eps, user: current_user)
+ establishFiduciaryEps: FeatureToggle.enabled?(:establish_fiduciary_eps, user: current_user),
+ editEpClaimLabels: FeatureToggle.enabled?(:edit_ep_claim_labels, user: current_user)
}
}
rescue StandardError => error
diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb
index 46bbe4a8fe3..6e6e15d5b8e 100644
--- a/app/controllers/tasks_controller.rb
+++ b/app/controllers/tasks_controller.rb
@@ -33,6 +33,7 @@ class TasksController < ApplicationController
PulacCerulloTask: PulacCerulloTask,
QualityReviewTask: QualityReviewTask,
ScheduleHearingTask: ScheduleHearingTask,
+ SendCavcRemandProcessedLetterTask: SendCavcRemandProcessedLetterTask,
SpecialCaseMovementTask: SpecialCaseMovementTask,
Task: Task,
TranslationTask: TranslationTask
diff --git a/app/jobs/set_appeal_age_aod_job.rb b/app/jobs/set_appeal_age_aod_job.rb
index dd2f0d97a30..3a493d69a64 100644
--- a/app/jobs/set_appeal_age_aod_job.rb
+++ b/app/jobs/set_appeal_age_aod_job.rb
@@ -30,21 +30,21 @@ def perform
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)
+ msg_title = "[INFO] #{self.class.name} completed after running for #{duration}."
+ Rails.logger.info("#{msg_title}\n#{details}")
- slack_service.send_notification("[INFO] #{msg}")
+ slack_service.send_notification(details, msg_title)
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)
+ msg_title = "[ERROR] #{collector_name} failed after running for #{duration}. Fatal error: #{err.message}."
+ Rails.logger.info("#{msg_title}\n#{details}")
Rails.logger.info(err.backtrace.join("\n"))
Raven.capture_exception(err, extra: { stats_collector_name: collector_name })
- slack_service.send_notification("[ERROR] #{msg}")
+ slack_service.send_notification(details, msg_title)
end
private
diff --git a/app/jobs/stats_collector_job.rb b/app/jobs/stats_collector_job.rb
index 9deb55ab8d7..a8b8fdd84b7 100644
--- a/app/jobs/stats_collector_job.rb
+++ b/app/jobs/stats_collector_job.rb
@@ -96,7 +96,7 @@ def log_success
msg = "#{self.class.name} completed after running for #{duration}."
Rails.logger.info(msg)
- slack_service.send_notification("[INFO] #{msg}") # may not need this
+ slack_service.send_notification("[INFO] #{msg}", self.class.to_s) # may not need this
end
def log_error(collector_name, err)
@@ -107,6 +107,6 @@ def log_error(collector_name, err)
Raven.capture_exception(err, extra: { stats_collector_name: collector_name })
- slack_service.send_notification("[ERROR] #{msg}")
+ slack_service.send_notification("[ERROR] #{msg}", self.class.to_s)
end
end
diff --git a/app/jobs/update_appellant_representation_job.rb b/app/jobs/update_appellant_representation_job.rb
index bc6d2dbe761..1817e563e17 100644
--- a/app/jobs/update_appellant_representation_job.rb
+++ b/app/jobs/update_appellant_representation_job.rb
@@ -101,7 +101,7 @@ def log_error(start_time, err)
Raven.capture_exception(err)
- slack_service.send_notification("[ERROR] #{msg}")
+ slack_service.send_notification("[ERROR] #{msg}", self.class.to_s)
datadog_report_runtime(metric_group_name: METRIC_GROUP_NAME)
end
diff --git a/app/jobs/update_cached_appeals_attributes_job.rb b/app/jobs/update_cached_appeals_attributes_job.rb
index 52c6fb71746..c5f8b9ca756 100644
--- a/app/jobs/update_cached_appeals_attributes_job.rb
+++ b/app/jobs/update_cached_appeals_attributes_job.rb
@@ -107,9 +107,8 @@ def cached_appeal_service
end
def log_warning
- slack_msg = "[WARN] UpdateCachedAppealsAttributesJob first 100 warnings:"\
- "\n#{warning_msgs.join("\n")}"
- slack_service.send_notification(slack_msg)
+ slack_msg = warning_msgs.join("\n")
+ slack_service.send_notification(slack_msg, "[WARN] UpdateCachedAppealsAttributesJob: first 100 warnings")
end
def log_error(start_time, err)
@@ -121,9 +120,9 @@ def log_error(start_time, err)
Raven.capture_exception(err)
- slack_msg = "[ERROR] UpdateCachedAppealsAttributesJob failed after running for #{duration}. "\
- "See Sentry event #{Raven.last_event_id}"
- slack_service.send_notification(slack_msg) # do not leak PII
+ slack_msg = "See Sentry event #{Raven.last_event_id}"
+ slack_service.send_notification(slack_msg,
+ "[ERROR] UpdateCachedAppealsAttributesJob failed after running for #{duration}.")
datadog_report_runtime(metric_group_name: METRIC_GROUP_NAME)
end
diff --git a/app/models/appeal.rb b/app/models/appeal.rb
index b44b133cdf5..6917ae61ebd 100644
--- a/app/models/appeal.rb
+++ b/app/models/appeal.rb
@@ -27,12 +27,17 @@ class Appeal < DecisionReview
has_one :post_decision_motion
has_many :record_synced_by_job, as: :record
has_one :work_mode, as: :appeal
+ has_one :latest_informal_hearing_presentation_task, lambda {
+ not_cancelled
+ .order(closed_at: :desc, assigned_at: :desc)
+ .where(type: [InformalHearingPresentationTask.name, IhpColocatedTask.name], appeal_type: Appeal.name)
+ }, class_name: "Task", foreign_key: :appeal_id
enum stream_type: {
- "original": "original",
- "vacate": "vacate",
- "de_novo": "de_novo",
- "court_remand": "court_remand"
+ Constants.AMA_STREAM_TYPES.original.to_sym => Constants.AMA_STREAM_TYPES.original,
+ Constants.AMA_STREAM_TYPES.vacate.to_sym => Constants.AMA_STREAM_TYPES.vacate,
+ Constants.AMA_STREAM_TYPES.de_novo.to_sym => Constants.AMA_STREAM_TYPES.de_novo,
+ Constants.AMA_STREAM_TYPES.court_remand.to_sym => Constants.AMA_STREAM_TYPES.court_remand
}
after_create :conditionally_set_aod_based_on_age
diff --git a/app/models/decision_review.rb b/app/models/decision_review.rb
index 8c9bb751e32..585334d18dd 100644
--- a/app/models/decision_review.rb
+++ b/app/models/decision_review.rb
@@ -347,9 +347,13 @@ def veteran_invalid_fields
end
def request_issues_ui_hash
- request_issues.includes(
+ issues = request_issues.includes(
:decision_review, :contested_decision_issue
- ).active_or_ineligible_or_withdrawn.map(&:serialize)
+ )
+ active_issues = issues.active.sort_by { |issue| issue.end_product_establishment&.code }
+
+ # Sorts issues in the order that they appear on Add issues page, so that the numbering is sequential
+ [active_issues + issues.ineligible + issues.withdrawn].flatten.compact.map(&:serialize)
end
private
diff --git a/app/models/end_product_establishment.rb b/app/models/end_product_establishment.rb
index 222e8e1f518..bfbf5a6bf64 100644
--- a/app/models/end_product_establishment.rb
+++ b/app/models/end_product_establishment.rb
@@ -315,7 +315,7 @@ def status
ep_code = Constants::EP_CLAIM_TYPES[code]
if committed?
{
- ep_code: "#{modifier} #{ep_code ? ep_code['offical_label'] : 'Unknown'}",
+ ep_code: "#{modifier} #{ep_code ? ep_code['official_label'] : 'Unknown'}",
ep_status: [status_type, sync_status].compact.join(", ")
}
else
diff --git a/app/models/end_product_update.rb b/app/models/end_product_update.rb
index 1411731e524..ecf9eaba0ad 100644
--- a/app/models/end_product_update.rb
+++ b/app/models/end_product_update.rb
@@ -48,7 +48,8 @@ def update_issue_type_to_nonrating
def update_issue_type_to_rating
request_issues.each do |ri|
ri.update(
- type: "RatingRequestIssue"
+ type: "RatingRequestIssue",
+ edited_description: ri.nonrating_issue_description
)
end
end
diff --git a/app/models/legacy_appeal.rb b/app/models/legacy_appeal.rb
index 1ec7a4a17e9..fc4c69f5604 100644
--- a/app/models/legacy_appeal.rb
+++ b/app/models/legacy_appeal.rb
@@ -31,6 +31,11 @@ class LegacyAppeal < CaseflowRecord
has_many :claimants, -> { Claimant.none }
has_one :cached_vacols_case, class_name: "CachedAppeal", foreign_key: :vacols_id, primary_key: :vacols_id
has_one :work_mode, as: :appeal
+ has_one :latest_informal_hearing_presentation_task, lambda {
+ not_cancelled
+ .order(closed_at: :desc, assigned_at: :desc)
+ .where(type: [InformalHearingPresentationTask.name, IhpColocatedTask.name], appeal_type: LegacyAppeal.name)
+ }, class_name: "Task", foreign_key: :appeal_id
accepts_nested_attributes_for :worksheet_issues, allow_destroy: true
# Add Paper Trail configuration
@@ -387,6 +392,10 @@ def hearing_scheduled?
scheduled_hearings.any?
end
+ def any_held_hearings?
+ hearings.any?(&:held?)
+ end
+
def completed_hearing_on_previous_appeal?
vacols_ids = VACOLS::Case.where(bfcorlid: vbms_id).pluck(:bfkey)
hearings = HearingRepository.hearings_for_appeals(vacols_ids)
@@ -851,6 +860,10 @@ def cavc
type == "Court Remand"
end
+ def original?
+ type_code == "original"
+ end
+
alias cavc? cavc
# Adding anything to this to_hash can trigger a lazy load which slows down
diff --git a/app/models/queue_tab.rb b/app/models/queue_tab.rb
index 445d22d38dd..1fa840d32f6 100644
--- a/app/models/queue_tab.rb
+++ b/app/models/queue_tab.rb
@@ -122,7 +122,7 @@ def on_hold_task_children_and_timed_hold_parents
def task_includes
[
- { appeal: [:available_hearing_locations, :claimants, :work_mode] },
+ { appeal: [:available_hearing_locations, :claimants, :work_mode, :latest_informal_hearing_presentation_task] },
:assigned_by,
:assigned_to,
:children,
diff --git a/app/models/serializers/v2/appeal_status_serializer.rb b/app/models/serializers/v2/appeal_status_serializer.rb
index 05fb1e4ef66..d518f37ee6c 100644
--- a/app/models/serializers/v2/appeal_status_serializer.rb
+++ b/app/models/serializers/v2/appeal_status_serializer.rb
@@ -10,6 +10,7 @@ class V2::AppealStatusSerializer
set_id :appeal_status_id
attribute :appeal_ids, &:linked_review_ids
+ attribute :type
attribute :updated do
Time.zone.now.in_time_zone("Eastern Time (US & Canada)").round.iso8601
@@ -19,10 +20,6 @@ class V2::AppealStatusSerializer
false
end
- attribute :type do
- "original"
- end
-
attribute :active, &:active_status?
attribute :description
attribute :aod, &:advanced_on_docket?
diff --git a/app/models/serializers/work_queue/legacy_task_serializer.rb b/app/models/serializers/work_queue/legacy_task_serializer.rb
index da4810b4e64..bfbce9e354b 100644
--- a/app/models/serializers/work_queue/legacy_task_serializer.rb
+++ b/app/models/serializers/work_queue/legacy_task_serializer.rb
@@ -91,4 +91,10 @@ class WorkQueue::LegacyTaskSerializer
attribute :available_actions do |object, params|
object.available_actions_unwrapper(params[:user], params[:role])
end
+
+ attribute :latest_informal_hearing_presentation_task do |object|
+ task = object.appeal.latest_informal_hearing_presentation_task
+
+ task ? { requested_at: task.assigned_at, received_at: task.closed_at } : {}
+ end
end
diff --git a/app/models/serializers/work_queue/task_column_serializer.rb b/app/models/serializers/work_queue/task_column_serializer.rb
index d2e9bbb6fab..464a3dffd07 100644
--- a/app/models/serializers/work_queue/task_column_serializer.rb
+++ b/app/models/serializers/work_queue/task_column_serializer.rb
@@ -248,6 +248,16 @@ def self.serialize_attribute?(params, columns)
end
end
+ attribute :latest_informal_hearing_presentation_task do |object, params|
+ columns = [Constants.QUEUE_CONFIG.COLUMNS.TASK_TYPE.name, Constants.QUEUE_CONFIG.COLUMNS.DAYS_WAITING.name]
+
+ if serialize_attribute?(params, columns)
+ task = object.appeal.latest_informal_hearing_presentation_task
+
+ task ? { requested_at: task.assigned_at, received_at: task.closed_at } : {}
+ end
+ end
+
# UNUSED
attribute :assignee_name do
diff --git a/app/models/task.rb b/app/models/task.rb
index 1e0069a1e3d..d0037bf87a5 100644
--- a/app/models/task.rb
+++ b/app/models/task.rb
@@ -208,11 +208,13 @@ def joins_with_cached_appeals_clause
"and #{CachedAppeal.table_name}.appeal_type = #{Task.table_name}.appeal_type"
end
- def order_by_appeal_priority_clause
+ def order_by_appeal_priority_clause(order: "asc")
+ boolean_order_clause = (order == "asc") ? "0 ELSE 1" : "1 ELSE 0"
Arel.sql(
- "CASE WHEN #{CachedAppeal.table_name}.is_aod = TRUE THEN 0 ELSE 1 END, "\
- "CASE WHEN #{CachedAppeal.table_name}.case_type = 'Court Remand' THEN 0 ELSE 1 END, "\
- "#{Task.table_name}.created_at"
+ "CASE WHEN #{CachedAppeal.table_name}.is_aod = TRUE THEN #{boolean_order_clause} END, "\
+ "CASE WHEN #{CachedAppeal.table_name}.case_type = 'Court Remand' THEN #{boolean_order_clause} END, "\
+ "#{CachedAppeal.table_name}.docket_number #{order}, "\
+ "#{Task.table_name}.created_at #{order}"
)
end
end
diff --git a/app/models/task_sorter.rb b/app/models/task_sorter.rb
index 7b26411decd..1c1b29bb1b1 100644
--- a/app/models/task_sorter.rb
+++ b/app/models/task_sorter.rb
@@ -43,7 +43,7 @@ def sort_requires_case_norm?(col)
def order_clause
case column.name
when Constants.QUEUE_CONFIG.COLUMNS.APPEAL_TYPE.name
- Arel.sql(appeal_type_order_clause)
+ Task.order_by_appeal_priority_clause(order: sort_order)
when Constants.QUEUE_CONFIG.COLUMNS.TASK_TYPE.name
Arel.sql(task_type_order_clause)
when Constants.QUEUE_CONFIG.COLUMNS.TASK_ASSIGNER.name
@@ -73,12 +73,6 @@ def task_type_order_clause
"position(#{task_type_sort_position}) #{sort_order}"
end
- def appeal_type_order_clause
- "cached_appeal_attributes.is_aod DESC, "\
- "cached_appeal_attributes.case_type #{sort_order}, "\
- "cached_appeal_attributes.docket_number #{sort_order}"
- end
-
def assigner_order_clause
"substring(assigners.display_name,\'([a-zA-Z]+)$\') #{sort_order}"
end
diff --git a/app/models/tasks/cavc_task.rb b/app/models/tasks/cavc_task.rb
index 89ad9a6b792..7f2cffea15a 100644
--- a/app/models/tasks/cavc_task.rb
+++ b/app/models/tasks/cavc_task.rb
@@ -12,7 +12,7 @@ class CavcTask < Task
before_validation :set_assignee
def self.label
- "All CAVC-related tasks"
+ COPY::CAVC_TASK_LABEL
end
def available_actions(_user)
diff --git a/app/models/tasks/send_cavc_remand_processed_letter_task.rb b/app/models/tasks/send_cavc_remand_processed_letter_task.rb
new file mode 100644
index 00000000000..3bc4833d4ba
--- /dev/null
+++ b/app/models/tasks/send_cavc_remand_processed_letter_task.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+##
+# Task for Litigation Support to take necessary action before sending the CAVC-remand-processed letter to an appellant.
+# This task is for CAVC Remand appeal streams.
+# Expected parent: CavcTask
+# Expected assigned_to.type: User
+
+class SendCavcRemandProcessedLetterTask < Task
+ validates :parent, presence: true,
+ parentTask: { task_types: [CavcTask, SendCavcRemandProcessedLetterTask] },
+ on: :create
+
+ before_validation :set_assignee
+
+ USER_ACTIONS = [
+ Constants.TASK_ACTIONS.MARK_COMPLETE.to_h,
+ Constants.TASK_ACTIONS.REASSIGN_TO_PERSON.to_h
+ ].freeze
+
+ ADMIN_ACTIONS = [
+ Constants.TASK_ACTIONS.ASSIGN_TO_PERSON.to_h
+ ].freeze
+
+ def self.label
+ COPY::SEND_CAVC_REMAND_PROCESSED_LETTER_TASK_LABEL
+ end
+
+ def available_actions(user)
+ if task_is_assigned_to_users_organization?(user) && CavcLitigationSupport.singleton.user_is_admin?(user)
+ return ADMIN_ACTIONS
+ end
+
+ return USER_ACTIONS if assigned_to == user
+
+ []
+ end
+
+ private
+
+ def set_assignee
+ self.assigned_to = CavcLitigationSupport.singleton if assigned_to.nil?
+ end
+
+ def cascade_closure_from_child_task?(_child_task)
+ true
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 88f0e4ad53e..b6974102e40 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -119,7 +119,7 @@ def can_view_overtime_status?
end
def can_change_hearing_request_type?
- (can?("Admin Intake") || can?("Build HearSched") || can?("Edit HearSched")) &&
+ (can?("Build HearSched") || can?("Edit HearSched")) &&
FeatureToggle.enabled?(:convert_travel_board_to_video_or_virtual, user: self)
end
diff --git a/app/queries/appeals_with_no_tasks_or_all_tasks_on_hold_query.rb b/app/queries/appeals_with_no_tasks_or_all_tasks_on_hold_query.rb
index 782bb3897d2..e3ae90880b5 100644
--- a/app/queries/appeals_with_no_tasks_or_all_tasks_on_hold_query.rb
+++ b/app/queries/appeals_with_no_tasks_or_all_tasks_on_hold_query.rb
@@ -7,6 +7,7 @@ def call
appeals_with_zero_tasks,
appeals_with_one_task,
appeals_with_two_tasks_not_distribution,
+ appeals_with_fully_on_hold_subtree,
dispatched_appeals_on_hold
].flatten.uniq
end
@@ -54,6 +55,18 @@ def appeals_with_two_tasks_not_distribution
.having("count(tasks) = 2 AND count(case when tasks.type = ? then 1 end) = 0", DistributionTask.name)
end
+ # Confirm that all subtrees have an active task
+ def appeals_with_fully_on_hold_subtree
+ Appeal.where(id:
+ Task.left_outer_joins(:children).on_hold
+ .where.not(type: [RootTask.name, TrackVeteranTask.name, EvidenceSubmissionWindowTask.name, TimedHoldTask.name])
+ .group("tasks.id")
+ .having(
+ "count(case when children_tasks.status in (?) then 1 end) = 0",
+ Task.open_statuses
+ ).select(:appeal_id).distinct)
+ end
+
def tasks_for(klass_name)
Task.select(:appeal_id).where(appeal_type: klass_name)
end
diff --git a/app/repositories/task_action_repository.rb b/app/repositories/task_action_repository.rb
index 5cf4d94c454..2847e188207 100644
--- a/app/repositories/task_action_repository.rb
+++ b/app/repositories/task_action_repository.rb
@@ -75,12 +75,13 @@ def assign_to_hearings_user_data(task, user = nil)
def assign_to_user_data(task, user = nil)
users = potential_task_assignees(task)
-
extras = if task.is_a?(HearingAdminActionTask)
{
redirect_after: "/organizations/#{HearingsManagement.singleton.url}",
message_detail: COPY::HEARING_ASSIGN_TASK_SUCCESS_MESSAGE_DETAIL
}
+ elsif task.is_a?(SendCavcRemandProcessedLetterTask) && task.assigned_to_type == "Organization"
+ { redirect_after: "/organizations/#{CavcLitigationSupport.singleton.url}" }
else
{}
end
diff --git a/app/serializers/intake/decision_review_intake_serializer.rb b/app/serializers/intake/decision_review_intake_serializer.rb
index 736c6bfc796..633e76a64a0 100644
--- a/app/serializers/intake/decision_review_intake_serializer.rb
+++ b/app/serializers/intake/decision_review_intake_serializer.rb
@@ -51,7 +51,7 @@ class Intake::DecisionReviewIntakeSerializer < Intake::IntakeSerializer
object.veteran&.valid?(:bgs)
end
- attribute :detail_edit_url do |object|
+ attribute :edit_issues_url do |object|
object.detail&.reload&.caseflow_only_edit_issues_url
end
end
diff --git a/app/services/hearing_schedule/generate_hearing_days_schedule.rb b/app/services/hearing_schedule/generate_hearing_days_schedule.rb
index aed869e8c9d..d709311484c 100644
--- a/app/services/hearing_schedule/generate_hearing_days_schedule.rb
+++ b/app/services/hearing_schedule/generate_hearing_days_schedule.rb
@@ -13,10 +13,11 @@ class NoDaysAvailableForRO < StandardError; end
attr_reader :available_days, :ros
- MAX_NUMBER_OF_DAYS_PER_DATE = 12
- BVA_VIDEO_ROOMS = [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].freeze
+ BVA_VIDEO_ROOMS = Constants::HEARING_ROOMS_LIST.keys.map(&:to_i).freeze
+ MAX_NUMBER_OF_DAYS_PER_DATE = BVA_VIDEO_ROOMS.size
- CO_DAYS_OF_WEEK = [1, 2, 3, 4].freeze
+ CO_DAYS_OF_WEEK = [3].freeze # only create 1 Central docket(Wednesday) per week
+ CO_FALLBACK_DAYS_OF_WEEK = [1, 2, 4].freeze # if wednesday is a holiday, pick a another non-holiday starting monday
def initialize(schedule_period)
@amortized = 0
@@ -93,15 +94,26 @@ def sort_ros_by_rooms_and_allocated_days
def generate_co_hearing_days_schedule
co_schedule = []
- (@schedule_period.start_date..@schedule_period.end_date).each do |scheduled_for|
- next unless valid_day_to_schedule_co(scheduled_for)
+ co_schedule_args = {
+ request_type: HearingDay::REQUEST_TYPES[:central],
+ room: "2",
+ bva_poc: "CAROL COLEMAN-DEW"
+ }
- co_schedule.push(
- scheduled_for: scheduled_for,
- request_type: HearingDay::REQUEST_TYPES[:central],
- room: "2",
- bva_poc: "CAROL COLEMAN-DEW"
- )
+ (@schedule_period.start_date..@schedule_period.end_date).each do |scheduled_for|
+ # if CO_DAYS_OF_WEEK falls on an invalid day, pick a day of the week that is valid
+ if CO_DAYS_OF_WEEK.include?(scheduled_for.cwday) && weekend_or_holiday_or_not_available?(scheduled_for)
+ fallback_date_for_co = get_fallback_date_for_co(
+ scheduled_for, @schedule_period.start_date, @schedule_period.end_date
+ )
+ if fallback_date_for_co
+ co_schedule.push(**co_schedule_args, scheduled_for: fallback_date_for_co)
+ end
+ else
+ next unless valid_day_to_schedule_co(scheduled_for)
+
+ co_schedule.push(**co_schedule_args, scheduled_for: scheduled_for)
+ end
end
co_schedule
end
@@ -331,11 +343,25 @@ def co_not_available?(day)
@co_non_availability_days.find { |non_availability_day| non_availability_day.date == day }.present?
end
+ def weekend_or_holiday_or_not_available?(date)
+ weekend?(date) || holiday?(date) || co_not_available?(date)
+ end
+
+ # pick the first day from fallback days that is valid
+ def get_fallback_date_for_co(scheduled_for, start_date, end_date)
+ valid_cwday = CO_FALLBACK_DAYS_OF_WEEK.detect do |cwday|
+ # i.e, if cwday is 1 and since begining of week is always monday, this will evauluate to monday
+ date = scheduled_for.beginning_of_week + (cwday - 1).day
+
+ # fallback date we choose has to valid as well as within the scheduling period range
+ !weekend_or_holiday_or_not_available?(date) && date >= start_date && date <= end_date
+ end
+
+ valid_cwday ? scheduled_for.beginning_of_week + (valid_cwday - 1).day : nil
+ end
+
def valid_day_to_schedule_co(scheduled_for)
- CO_DAYS_OF_WEEK.include?(scheduled_for.cwday) &&
- !weekend?(scheduled_for) &&
- !holiday?(scheduled_for) &&
- !co_not_available?(scheduled_for)
+ CO_DAYS_OF_WEEK.include?(scheduled_for.cwday) && !weekend_or_holiday_or_not_available?(scheduled_for)
end
# Filters out the non-available RO days from the board available days for
diff --git a/app/services/slack_service.rb b/app/services/slack_service.rb
index 6a93c7badd3..c84a4fb570e 100644
--- a/app/services/slack_service.rb
+++ b/app/services/slack_service.rb
@@ -36,9 +36,13 @@ def http_service
end
def pick_color(title, msg)
- if title =~ /error/i || msg =~ /error/i
+ if /error/i.match?(title)
COLORS[:error]
- elsif title =~ /warn/i || msg =~ /warn/i
+ elsif /warn/i.match?(title)
+ COLORS[:warn]
+ elsif /error/i.match?(msg)
+ COLORS[:error]
+ elsif /warn/i.match?(msg)
COLORS[:warn]
else
COLORS[:info]
diff --git a/app/views/higher_level_reviews/edit.html.erb b/app/views/higher_level_reviews/edit.html.erb
index 2b1bf36e62a..dacc36ffe6b 100644
--- a/app/views/higher_level_reviews/edit.html.erb
+++ b/app/views/higher_level_reviews/edit.html.erb
@@ -14,7 +14,8 @@
unidentifiedIssueDecisionDate: FeatureToggle.enabled?(:unidentified_issue_decision_date, user: current_user),
verifyUnidentifiedIssue: FeatureToggle.enabled?(:verify_unidentified_issue, user: current_user),
covidTimelinessExemption: FeatureToggle.enabled?(:covid_timeliness_exemption, user: current_user),
- establishFiduciaryEps: FeatureToggle.enabled?(:establish_fiduciary_eps, user: current_user)
+ establishFiduciaryEps: FeatureToggle.enabled?(:establish_fiduciary_eps, user: current_user),
+ editEpClaimLabels: FeatureToggle.enabled?(:edit_ep_claim_labels, user: current_user)
}
}) %>
<% end %>
diff --git a/app/views/supplemental_claims/edit.html.erb b/app/views/supplemental_claims/edit.html.erb
index ce788373861..0b0004d6256 100644
--- a/app/views/supplemental_claims/edit.html.erb
+++ b/app/views/supplemental_claims/edit.html.erb
@@ -14,8 +14,8 @@
unidentifiedIssueDecisionDate: FeatureToggle.enabled?(:unidentified_issue_decision_date, user: current_user),
verifyUnidentifiedIssue: FeatureToggle.enabled?(:verify_unidentified_issue, user: current_user),
covidTimelinessExemption: FeatureToggle.enabled?(:covid_timeliness_exemption, user: current_user),
- establishFiduciaryEps: FeatureToggle.enabled?(:establish_fiduciary_eps, user: current_user)
-
+ establishFiduciaryEps: FeatureToggle.enabled?(:establish_fiduciary_eps, user: current_user),
+ editEpClaimLabels: FeatureToggle.enabled?(:edit_ep_claim_labels, user: current_user)
}
}) %>
<% end %>
diff --git a/app/workflows/initial_tasks_factory.rb b/app/workflows/initial_tasks_factory.rb
index 3f860dbd709..f5d5e2a144c 100644
--- a/app/workflows/initial_tasks_factory.rb
+++ b/app/workflows/initial_tasks_factory.rb
@@ -27,7 +27,8 @@ def create_subtasks!
distribution_task = DistributionTask.create!(appeal: @appeal, parent: @root_task)
if @appeal.cavc?
- CavcTask.create!(appeal: @appeal, parent: distribution_task)
+ cavc_task = CavcTask.create!(appeal: @appeal, parent: distribution_task)
+ SendCavcRemandProcessedLetterTask.create!(appeal: @appeal, parent: cavc_task)
elsif @appeal.evidence_submission_docket?
EvidenceSubmissionWindowTask.create!(appeal: @appeal, parent: distribution_task)
elsif @appeal.hearing_docket?
diff --git a/app/workflows/tasks_for_appeal.rb b/app/workflows/tasks_for_appeal.rb
index 2d6576bb706..144edacc128 100644
--- a/app/workflows/tasks_for_appeal.rb
+++ b/app/workflows/tasks_for_appeal.rb
@@ -62,7 +62,9 @@ def initialize_hearing_tasks_for_travel_board?
appeal.tasks.open.where(type: HearingTask.name).empty? &&
appeal.tasks.closed.where(type: ChangeHearingRequestTypeTask.name).empty? &&
appeal.current_hearing_request_type == :travel_board &&
- appeal.active?
+ appeal.active? &&
+ appeal.original? &&
+ !appeal.any_held_hearings?
end
def legacy_appeal_tasks
diff --git a/client/COPY.json b/client/COPY.json
index 4333d5cd8ef..d96132ade8c 100644
--- a/client/COPY.json
+++ b/client/COPY.json
@@ -368,8 +368,11 @@
"HEARING_TASK_ASSOCIATION_MISSING_MESASAGE": "Hearing task (%s) is missing an associated hearing. This means that either the hearing was deleted in VACOLS or the hearing association has been deleted.",
"HEARING_TASK_DEFAULT_INSTRUCTIONS": "This task will be auto-completed when all hearing-related tasks have been completed.",
+ "CAVC_TASK_LABEL": "All CAVC-related tasks",
"CAVC_TASK_DEFAULT_INSTRUCTIONS": "This task will be auto-completed when all CAVC-related tasks have been closed.",
+ "SEND_CAVC_REMAND_PROCESSED_LETTER_TASK_LABEL": "Send CAVC-Remand-Processed Letter Task",
+
"ASSIGN_HEARING_DISPOSITION_TASK_DEFAULT_INSTRUCTIONS": "Postpone or cancel a hearing prior to the hearing date. This task will be auto-completed after the hearing's scheduled date.",
"CHANGE_HEARING_DISPOSITION_TASK_DEFAULT_INSTRUCTIONS": "Change hearing disposition (Held, Canceled, No Show, Postponed) if it was marked in error.",
@@ -611,6 +614,9 @@
"DOCKET_SWITCH_RECOMMENDATION_TITLE": "Switch Docket: Send %s's Request to a Judge",
"DOCKET_SWITCH_RECOMMENDATION_INSTRUCTIONS": "Add and send your recommendation for this docket switch request to a Veteran's Law Judge.",
"DOCKET_SWITCH_RULING_TASK_LABEL": "Rule on Docket Switch",
+ "DOCKET_SWITCH_REQUEST_TITLE": "%s's docket switch has been requested by %s",
+ "DOCKET_SWITCH_REQUEST_MESSAGE": "This task will appear on your hold tab until it has been addressed by the VLJ.",
+
"CORRECT_REQUEST_ISSUES_LINK": "Correct issues",
"CORRECT_REQUEST_ISSUES_WITHDRAW": "Withdraw",
diff --git a/client/app/intake/pages/addIssues.jsx b/client/app/intake/pages/addIssues.jsx
index 81ac7dc9773..d7d05cbe874 100644
--- a/client/app/intake/pages/addIssues.jsx
+++ b/client/app/intake/pages/addIssues.jsx
@@ -17,7 +17,8 @@ import InlineForm from '../../components/InlineForm';
import DateSelector from '../../components/DateSelector';
import ErrorAlert from '../components/ErrorAlert';
import { REQUEST_STATE, PAGE_PATHS, VBMS_BENEFIT_TYPES, FORM_TYPES } from '../constants';
-import { formatAddedIssues, getAddIssuesFields } from '../util/issues';
+import EP_CLAIM_TYPES from '../../../constants/EP_CLAIM_TYPES';
+import { formatAddedIssues, getAddIssuesFields, formatIssuesBySection } from '../util/issues';
import Table from '../../components/Table';
import IssueList from '../components/IssueList';
@@ -102,9 +103,7 @@ class AddIssuesPage extends React.Component {
const { correctClaimReviews } = featureToggles;
return (
- !formType ||
- (this.editingClaimReview() && !processedAt) ||
- intakeData.isOutcoded ||
+ !formType || (this.editingClaimReview() && !processedAt) || intakeData.isOutcoded ||
(hasClearedEp && !correctClaimReviews)
);
}
@@ -143,7 +142,7 @@ class AddIssuesPage extends React.Component {
render() {
const { intakeForms, formType, veteran, featureToggles, editPage, addingIssue, userCanWithdrawIssues } = this.props;
const intakeData = intakeForms[formType];
- const { useAmaActivationDate } = featureToggles;
+ const { useAmaActivationDate, editEpClaimLabels } = featureToggles;
const hasClearedEp = intakeData && (intakeData.hasClearedRatingEp || intakeData.hasClearedNonratingEp);
if (this.willRedirect(intakeData, hasClearedEp)) {
@@ -162,10 +161,9 @@ class AddIssuesPage extends React.Component {
);
const issues = formatAddedIssues(intakeData.addedIssues, useAmaActivationDate);
- const requestedIssues = issues.filter((issue) => !issue.withdrawalPending && !issue.withdrawalDate);
- const previouslywithdrawnIssues = issues.filter((issue) => issue.withdrawalDate);
const issuesPendingWithdrawal = issues.filter((issue) => issue.withdrawalPending);
- const withdrawnIssues = previouslywithdrawnIssues.concat(issuesPendingWithdrawal);
+ const issuesBySection = formatIssuesBySection(issues, editEpClaimLabels);
+
const withdrawReview =
!_.isEmpty(issues) && _.every(issues, (issue) => issue.withdrawalPending || issue.withdrawalDate);
@@ -242,39 +240,60 @@ class AddIssuesPage extends React.Component {
let rowObjects = fieldsForFormType;
- if (!_.isEmpty(requestedIssues)) {
- rowObjects = fieldsForFormType.concat({
- field: 'Requested issues',
+ const issueSectionRow = (sectionIssues, fieldTitle) => {
+ return {
+ field: fieldTitle,
content: (
-
+
+ { !fieldTitle.includes('issues') && Requested issues }
+
+
)
- });
- }
+ };
+ };
- if (!_.isEmpty(withdrawnIssues)) {
- rowObjects = rowObjects.concat({
- field: 'Withdrawn issues',
+ const endProductLabelRow = (endProductCode) => {
+ return {
+ field: 'EP Claim Label',
content: (
-
+
+
+ { EP_CLAIM_TYPES[endProductCode].official_label }
+
+
+
+
+
)
+ };
+ };
+
+ Object.keys(issuesBySection).sort().
+ map((key) => {
+ const sectionIssues = issuesBySection[key];
+
+ if (key === 'requestedIssues') {
+ rowObjects = rowObjects.concat(issueSectionRow(sectionIssues, 'Requested issues'));
+ } else if (key === 'withdrawnIssues') {
+ rowObjects = rowObjects.concat(issueSectionRow(sectionIssues, 'Withdrawn issues'));
+ } else {
+ rowObjects = rowObjects.concat(endProductLabelRow(key));
+ rowObjects = rowObjects.concat(issueSectionRow(sectionIssues, ' '));
+ }
+
+ return rowObjects;
});
- }
const hideAddIssueButton = intakeData.isDtaError && _.isEmpty(intakeData.contestableIssues);
diff --git a/client/app/intake/pages/decisionReviewEditCompleted.jsx b/client/app/intake/pages/decisionReviewEditCompleted.jsx
index 96f09cb6d03..28438fe49ff 100644
--- a/client/app/intake/pages/decisionReviewEditCompleted.jsx
+++ b/client/app/intake/pages/decisionReviewEditCompleted.jsx
@@ -12,7 +12,7 @@ import SmallLoader from '../../components/SmallLoader';
import { LOGO_COLORS } from '../../constants/AppConstants';
import END_PRODUCT_CODES from '../../../constants/END_PRODUCT_CODES';
-const leadMessageList = ({ veteran, formName, requestIssues, addedIssues, detailEditUrl }) => {
+const leadMessageList = ({ veteran, formName, requestIssues, addedIssues, editIssuesUrl }) => {
const unidentifiedIssues = requestIssues.filter((ri) => ri.isUnidentified);
const eligibleRequestIssues = requestIssues.filter((ri) => !ri.ineligibleReason);
@@ -28,7 +28,7 @@ const leadMessageList = ({ veteran, formName, requestIssues, addedIssues, detail
const leadMessageArr = [
`${veteran.name}'s (ID #${veteran.fileNumber}) Request for ${formName} has been ${editMessage()}.`,
-
+
];
if (eligibleRequestIssues.length !== 0 && unidentifiedIssues.length > 0) {
@@ -65,7 +65,7 @@ class DecisionReviewEditCompletedPage extends React.PureComponent {
afterIssues,
updatedIssues,
addedIssues,
- detailEditUrl,
+ editIssuesUrl,
redirectTo
} = this.props;
@@ -109,7 +109,7 @@ class DecisionReviewEditCompletedPage extends React.PureComponent {
type="success"
leadMessageList={
leadMessageList({
- detailEditUrl,
+ editIssuesUrl,
veteran,
formName: selectedForm.name,
requestIssues: afterIssues,
@@ -164,6 +164,7 @@ export default connect(
(state) => ({
formType: state.formType,
veteran: state.veteran,
+ editIssuesUrl: state.editIssuesUrl,
beforeIssues: state.beforeIssues,
afterIssues: state.afterIssues,
updatedIssues: state.updatedIssues,
diff --git a/client/app/intake/pages/decisionReviewIntakeCompleted.jsx b/client/app/intake/pages/decisionReviewIntakeCompleted.jsx
index 16757ae613a..cf6dc1054b4 100644
--- a/client/app/intake/pages/decisionReviewIntakeCompleted.jsx
+++ b/client/app/intake/pages/decisionReviewIntakeCompleted.jsx
@@ -14,7 +14,7 @@ import { LOGO_COLORS } from '../../constants/AppConstants';
import COPY from '../../../COPY';
import UnidentifiedIssueAlert from '../components/UnidentifiedIssueAlert';
-const leadMessageList = ({ veteran, formName, requestIssues, asyncJobUrl, detailEditUrl, completedReview }) => {
+const leadMessageList = ({ veteran, formName, requestIssues, asyncJobUrl, editIssuesUrl, completedReview }) => {
const unidentifiedIssues = requestIssues.filter((ri) => ri.isUnidentified);
const eligibleRequestIssues = requestIssues.filter((ri) => !ri.ineligibleReason);
@@ -27,7 +27,7 @@ const leadMessageList = ({ veteran, formName, requestIssues, asyncJobUrl, detail
}
leadMessageArr.push(
-
+
);
if (asyncJobUrl) {
@@ -111,7 +111,7 @@ class DecisionReviewIntakeCompleted extends React.PureComponent {
formType,
intakeStatus,
asyncJobUrl,
- detailEditUrl
+ editIssuesUrl
} = this.props;
const selectedForm = _.find(FORM_TYPES, { key: formType });
const completedReview = this.props.decisionReviews[selectedForm.key];
@@ -152,7 +152,7 @@ class DecisionReviewIntakeCompleted extends React.PureComponent {
leadMessageList={
leadMessageList({
asyncJobUrl,
- detailEditUrl,
+ editIssuesUrl,
veteran,
formName: selectedForm.name,
requestIssues,
@@ -185,7 +185,7 @@ export default connect(
veteran: state.intake.veteran,
formType: state.intake.formType,
asyncJobUrl: state.intake.asyncJobUrl,
- detailEditUrl: state.intake.detailEditUrl,
+ editIssuesUrl: state.intake.editIssuesUrl,
decisionReviews: {
higher_level_review: state.higherLevelReview,
supplemental_claim: state.supplementalClaim,
diff --git a/client/app/intake/reducers/featureToggles.js b/client/app/intake/reducers/featureToggles.js
index 8f7c12a3b70..dd04fd0e338 100644
--- a/client/app/intake/reducers/featureToggles.js
+++ b/client/app/intake/reducers/featureToggles.js
@@ -28,7 +28,10 @@ const updateFromServerFeatures = (state, featureToggles) => {
},
establishFiduciaryEps: {
$set: Boolean(featureToggles.establishFiduciaryEps)
- }
+ },
+ editEpClaimLabels: {
+ $set: Boolean(featureToggles.editEpClaimLabels)
+ },
});
};
@@ -41,7 +44,8 @@ export const mapDataToFeatureToggle = (data = { featureToggles: {} }) =>
unidentifiedIssueDecisionDate: false,
verifyUnidentifiedIssue: false,
restrictAppealIntakes: false,
- establishFiduciaryEps: false
+ establishFiduciaryEps: false,
+ editEpClaimLabels: false
},
data.featureToggles
);
diff --git a/client/app/intake/reducers/intake.js b/client/app/intake/reducers/intake.js
index 9a13c20ba42..640a1f8b6dd 100644
--- a/client/app/intake/reducers/intake.js
+++ b/client/app/intake/reducers/intake.js
@@ -14,8 +14,8 @@ const updateFromServerIntake = (state, serverIntake) => {
asyncJobUrl: {
$set: serverIntake.async_job_url
},
- detailEditUrl: {
- $set: serverIntake.detailEditUrl
+ editIssuesUrl: {
+ $set: serverIntake.editIssuesUrl
},
unreadMessages: {
$set: serverIntake.unread_messages
@@ -41,7 +41,7 @@ export const mapDataToInitialIntake = (data = { serverIntake: {} }) => (
updateFromServerIntake({
id: null,
asyncJobUrl: null,
- detailEditUrl: null,
+ editIssuesUrl: null,
formType: null,
fileNumberSearch: '',
searchErrorCode: null,
diff --git a/client/app/intake/util/issues.js b/client/app/intake/util/issues.js
index 4076a8f5d0a..5ed4da3789d 100644
--- a/client/app/intake/util/issues.js
+++ b/client/app/intake/util/issues.js
@@ -330,6 +330,22 @@ export const getAddIssuesFields = (formType, veteran, intakeData) => {
return fields.concat(claimantField);
};
+export const formatIssuesBySection = (issues, editClaimLabelFeatureToggle) => {
+ return issues.reduce(
+ (result, issue) => {
+ if (issue.withdrawalDate || issue.withdrawalPending) {
+ (result.withdrawnIssues || (result.withdrawnIssues = [])).push(issue);
+ } else if (issue.endProductCode && editClaimLabelFeatureToggle) {
+ (result[issue.endProductCode] || (result[issue.endProductCode] = [])).push(issue);
+ } else {
+ (result.requestedIssues || (result.requestedIssues = [])).push(issue);
+ }
+
+ return result;
+ }, {}
+ );
+};
+
export const formatAddedIssues = (issues = [], useAmaActivationDate = false) => {
const amaActivationDate = new Date(useAmaActivationDate ? DATES.AMA_ACTIVATION : DATES.AMA_ACTIVATION_TEST);
@@ -349,6 +365,7 @@ export const formatAddedIssues = (issues = [], useAmaActivationDate = false) =>
withdrawalPending: issue.withdrawalPending,
withdrawalDate: issue.withdrawalDate,
endProductCleared: issue.endProductCleared,
+ endProductCode: issue.endProductCode,
correctionType: issue.correctionType,
editable: issue.editable,
examRequested: issue.examRequested,
@@ -396,6 +413,7 @@ export const formatAddedIssues = (issues = [], useAmaActivationDate = false) =>
withdrawalPending: issue.withdrawalPending,
withdrawalDate: issue.withdrawalDate,
endProductCleared: issue.endProductCleared,
+ endProductCode: issue.endProductCode,
editedDescription: issue.editedDescription,
correctionType: issue.correctionType,
editable: issue.editable,
@@ -430,6 +448,7 @@ export const formatAddedIssues = (issues = [], useAmaActivationDate = false) =>
withdrawalPending: issue.withdrawalPending,
withdrawalDate: issue.withdrawalDate,
endProductCleared: issue.endProductCleared,
+ endProductCode: issue.endProductCode,
category: issue.category,
editedDescription: issue.editedDescription,
correctionType: issue.correctionType,
diff --git a/client/app/queue/components/IhpDaysWaitingTooltip.jsx b/client/app/queue/components/IhpDaysWaitingTooltip.jsx
new file mode 100644
index 00000000000..e886451ed0d
--- /dev/null
+++ b/client/app/queue/components/IhpDaysWaitingTooltip.jsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { css } from 'glamor';
+import moment from 'moment';
+
+import Tooltip from '../../components/Tooltip';
+import { DateString } from '../../util/DateUtil';
+
+const listStyling = css({
+ listStyle: 'none',
+ textAlign: 'left',
+ padding: 0,
+ '& > li': {
+ marginBottom: 0
+ }
+});
+
+// Creates a tool tip that displays information about the status of an appeal's most recent Informal Hearing
+// Presentation task. If the ihp task is not complete, days waiting will show how long it has been since the IHP was
+// requested. If the ihp task is complete, days waiting will show how long took for the IHP task to be received.
+const IhpDaysWaitingTooltip = (props) => {
+ const { requestedAt, receivedAt, children } = props;
+
+ if (!requestedAt) {
+ return children;
+ }
+
+ const today = moment();
+ const daysSinceIhpReceived = receivedAt ? `(${today.diff(moment(receivedAt), 'd')} days)` : '';
+ const daysWaiting = `${(receivedAt ? moment(receivedAt) : today).diff(moment(requestedAt), 'd')} days`;
+
+ const tooltipText = (
+
+
This case has an IHP Request associated with it.
+
+ -
+ IHP Requested:
+
+ -
+ IHP Received: {daysSinceIhpReceived}
+
+ -
+ On hold for IHP: {daysWaiting}
+
+
+
+ );
+
+ return {children};
+};
+
+IhpDaysWaitingTooltip.propTypes = {
+ receivedAt: PropTypes.string,
+ requestedAt: PropTypes.string,
+ children: PropTypes.node
+};
+
+export default IhpDaysWaitingTooltip;
diff --git a/client/app/queue/components/IhpDaysWaitingTooltip.stories.js b/client/app/queue/components/IhpDaysWaitingTooltip.stories.js
new file mode 100644
index 00000000000..5e0b9cdf9f0
--- /dev/null
+++ b/client/app/queue/components/IhpDaysWaitingTooltip.stories.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import IhpDaysWaitingTooltip from './IhpDaysWaitingTooltip';
+
+export default {
+ title: 'queue/Components/IhpDaysWaitingTooltip',
+ component: IhpDaysWaitingTooltip,
+ parameters: {
+ controls: { expanded: true },
+ },
+ argTypes: {
+ requestedAt: { control: { type: 'text' } },
+ receivedAt: { control: { type: 'text' } },
+ },
+};
+
+const Template = (args) =>
+ 3 days (hover over me)
+;
+
+export const WaitingForIHP = Template.bind({});
+WaitingForIHP.args = { requestedAt: '2020-10-03T13:39:36.574-05:00', receivedAt: null };
+
+export const ReceivedIHP = Template.bind({});
+ReceivedIHP.args = { requestedAt: '2020-10-03T13:39:36.574-05:00', receivedAt: '2020-11-03T13:39:36.574-05:00' };
diff --git a/client/app/queue/components/TaskTableColumns.jsx b/client/app/queue/components/TaskTableColumns.jsx
index a5527842c09..75ee1441ba7 100644
--- a/client/app/queue/components/TaskTableColumns.jsx
+++ b/client/app/queue/components/TaskTableColumns.jsx
@@ -9,6 +9,7 @@ import CaseDetailsLink from '../CaseDetailsLink';
import ReaderLink from '../ReaderLink';
import ContinuousProgressBar from '../../components/ContinuousProgressBar';
import OnHoldLabel, { numDaysOnHold } from './OnHoldLabel';
+import IhpDaysWaitingTooltip from './IhpDaysWaitingTooltip';
import { taskHasCompletedHold, hasDASRecord, collapseColumn, regionalOfficeCity, renderAppealType } from '../utils';
import { DateString } from '../../util/DateUtil';
@@ -228,13 +229,13 @@ export const daysWaitingColumn = (requireDasRecord) => {
daysSincePlacedOnHold = moment().startOf('day').
diff(task.placedOnHoldAt, 'days');
- return
+ return
{daysSinceAssigned} {pluralize('day', daysSinceAssigned)}
{ taskHasCompletedHold(task) &&
}
- ;
+ ;
},
backendCanSort: true,
getSortValue: (task) => moment().startOf('day').
diff --git a/client/app/queue/docketSwitch/recommendDocketSwitch/RecommendDocketSwitchContainer.jsx b/client/app/queue/docketSwitch/recommendDocketSwitch/RecommendDocketSwitchContainer.jsx
index 1f2f789088a..8a748090bdc 100644
--- a/client/app/queue/docketSwitch/recommendDocketSwitch/RecommendDocketSwitchContainer.jsx
+++ b/client/app/queue/docketSwitch/recommendDocketSwitch/RecommendDocketSwitchContainer.jsx
@@ -7,6 +7,13 @@ import { appealWithDetailSelector } from '../../selectors';
import { dispositions } from '../constants';
import { createDocketSwitchRulingTask } from './recommendDocketSwitchSlice';
import { RecommendDocketSwitchForm } from './RecommendDocketSwitchForm';
+import {
+ DOCKET_SWITCH_REQUEST_TITLE,
+ DOCKET_SWITCH_REQUEST_MESSAGE,
+} from '../../../../COPY';
+
+import { sprintf } from 'sprintf-js';
+import { showSuccessMessage } from '../../uiReducer/uiActions';
// This takes form data and generates Markdown-formatted text to be saved as task instructions
export const formatDocketSwitchRecommendation = ({
@@ -55,6 +62,7 @@ export const RecommendDocketSwitchContainer = () => {
// eslint-disable-next-line no-console
const handleSubmit = async (formData) => {
+
const instructions = formatDocketSwitchRecommendation({ ...formData });
const newTask = {
parent_id: taskId,
@@ -69,10 +77,15 @@ export const RecommendDocketSwitchContainer = () => {
tasks: [newTask],
};
+ const successMessage = {
+ title: sprintf(DOCKET_SWITCH_REQUEST_TITLE, appeal.appellantFullName, formData.judge.label),
+ detail: DOCKET_SWITCH_REQUEST_MESSAGE,
+ };
+
try {
await dispatch(createDocketSwitchRulingTask(data));
- // Add logic for success banner
+ dispatch(showSuccessMessage(successMessage));
push('/queue');
} catch (error) {
// Perhaps show an alert that indicates error, advise trying again...?
diff --git a/client/app/queue/utils.js b/client/app/queue/utils.js
index 4d098d7950c..556cc1ab7c8 100644
--- a/client/app/queue/utils.js
+++ b/client/app/queue/utils.js
@@ -129,7 +129,11 @@ const taskAttributesFromRawTask = (task) => {
powerOfAttorneyName: task.attributes.power_of_attorney_name,
suggestedHearingLocation: task.attributes.suggested_hearing_location,
hearingRequestType: task.attributes.hearing_request_type,
- isFormerTravel: task.attributes.former_travel
+ isFormerTravel: task.attributes.former_travel,
+ latestInformalHearingPresentationTask: {
+ requestedAt: task.attributes.latest_informal_hearing_presentation_task?.requested_at,
+ receivedAt: task.attributes.latest_informal_hearing_presentation_task?.received_at
+ }
};
};
@@ -218,7 +222,11 @@ export const prepareLegacyTasksForStore = (tasks) => {
timelineTitle: task.attributes.timeline_title,
hideFromQueueTableView: task.attributes.hide_from_queue_table_view,
hideFromTaskSnapshot: task.attributes.hide_from_task_snapshot,
- hideFromCaseTimeline: task.attributes.hide_from_case_timeline
+ hideFromCaseTimeline: task.attributes.hide_from_case_timeline,
+ latestInformalHearingPresentationTask: {
+ requestedAt: task.attributes.latest_informal_hearing_presentation_task?.requested_at,
+ receivedAt: task.attributes.latest_informal_hearing_presentation_task?.received_at
+ }
};
});
diff --git a/client/app/styles/_intake.scss b/client/app/styles/_intake.scss
index 45f723de930..8e7e28a7a10 100644
--- a/client/app/styles/_intake.scss
+++ b/client/app/styles/_intake.scss
@@ -173,7 +173,24 @@
}
td {
- vertical-align: top;
+ .claim-label-row {
+ color: $color-gray-dark;
+
+ .claim-label {
+ display: inline-block;
+ padding: .75rem 0;
+ }
+
+ .edit-claim-label {
+ float: right;
+ width: 140px;
+
+ button {
+ padding: 1rem;
+ width: 140px;
+ }
+ }
+ }
.issues {
display: table;
@@ -267,9 +284,11 @@
}
}
- .issue-container {
+ .issue-container:not(:last-child) {
border-bottom: 1px solid $color-gray-lighter;
+ }
+ .issue-container {
.cf-form-textarea {
padding-left: 20px;
padding-right: 20px;
diff --git a/client/constants/AMA_STREAM_TYPES.json b/client/constants/AMA_STREAM_TYPES.json
new file mode 100644
index 00000000000..e35aec53bcd
--- /dev/null
+++ b/client/constants/AMA_STREAM_TYPES.json
@@ -0,0 +1,6 @@
+{
+ "original": "original",
+ "vacate": "vacate",
+ "de_novo": "de_novo",
+ "court_remand": "court_remand"
+}
\ No newline at end of file
diff --git a/client/constants/EP_CLAIM_TYPES.json b/client/constants/EP_CLAIM_TYPES.json
index 0be858210ce..f2a2c4eb016 100644
--- a/client/constants/EP_CLAIM_TYPES.json
+++ b/client/constants/EP_CLAIM_TYPES.json
@@ -3,14 +3,14 @@
"review_type": "supplemental_claim",
"benefit_type": "fiduciary",
"family": "040",
- "offical_label": "FID-Supplemental Claim Review",
+ "official_label": "FID-Supplemental Claim Review",
"issue_type": "nonrating"
},
"030HLRFID": {
"review_type": "higher_level_review",
"benefit_type": "fiduciary",
"family": "030",
- "offical_label": "FID-Higher-Level Review",
+ "official_label": "FID-Higher-Level Review",
"issue_type": "nonrating"
},
"040SCR": {
@@ -18,56 +18,56 @@
"benefit_type": "compensation",
"family": "040",
"issue_type": "rating",
- "offical_label": "Supplemental Claim Rating"
+ "official_label": "Supplemental Claim Rating"
},
"040SCNR": {
"review_type": "supplemental_claim",
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "040",
- "offical_label": "Supplemental Claim nonrating"
+ "official_label": "Supplemental Claim Non-Rating"
},
"030HLRR": {
"review_type": "higher_level_review",
"issue_type": "rating",
"family": "030",
"benefit_type": "compensation",
- "offical_label": "Higher-Level Review Rating"
+ "official_label": "Higher-Level Review Rating"
},
"030HLRNR": {
"review_type": "higher_level_review",
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "030",
- "offical_label": "Higher-Level Review nonrating"
+ "official_label": "Higher-Level Review Non-Rating"
},
"040SCNRPMC": {
"review_type": "supplemental_claim",
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "040",
- "offical_label": "PMC Supplemental Claim NonRating"
+ "official_label": "PMC Supplemental Claim Non-Rating"
},
"030HLRRPMC": {
"review_type": "higher_level_review",
"issue_type": "rating",
"benefit_type": "pension",
"family": "030",
- "offical_label": "PMC Higher-Level Review Rating"
+ "official_label": "PMC Higher-Level Review Rating"
},
"030HLRNRPMC": {
"review_type": "higher_level_review",
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "030",
- "offical_label": "PMC Higher-Level Review nonrating"
+ "official_label": "PMC Higher-Level Review Non-Rating"
},
"040ADONRPMC": {
"review_type": "supplemental_claim",
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "040",
- "offical_label": "PMC AMA Difference of Opinion - NR (040)",
+ "official_label": "PMC AMA Difference of Opinion - NR (040)",
"disposition_type": "difference_of_opinion"
},
"040ADORPMC": {
@@ -75,7 +75,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "040",
- "offical_label": "PMC AMA Difference of Opinion - Rating (040)",
+ "official_label": "PMC AMA Difference of Opinion - Rating (040)",
"disposition_type": "difference_of_opinion"
},
"040AMADONR": {
@@ -83,7 +83,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "040",
- "offical_label": "AMA Difference of Opinion - NR (040)",
+ "official_label": "AMA Difference of Opinion - NR (040)",
"disposition_type": "difference_of_opinion"
},
"040AMADOR": {
@@ -91,7 +91,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "040",
- "offical_label": "AMA Difference of Opinion - Rating (040)",
+ "official_label": "AMA Difference of Opinion - Rating (040)",
"disposition_type": "difference_of_opinion"
},
"040BDENR": {
@@ -99,7 +99,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "040",
- "offical_label": "Board DTA Error nonrating",
+ "official_label": "Board DTA Error Non-Rating",
"disposition_type": "board_remand"
},
"040BDENRPMC": {
@@ -107,7 +107,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "040",
- "offical_label": "PMC Board DTA Error nonrating",
+ "official_label": "PMC Board DTA Error Non-Rating",
"disposition_type": "board_remand"
},
"040BDER": {
@@ -115,7 +115,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "040",
- "offical_label": "Board DTA Error Rating",
+ "official_label": "Board DTA Error Rating",
"disposition_type": "board_remand"
},
"040HDENR": {
@@ -123,7 +123,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "040",
- "offical_label": "HLR DTA Error - nonrating",
+ "official_label": "HLR DTA Error - Non-Rating",
"disposition_type": "dta_error"
},
"040BDERPMC": {
@@ -131,7 +131,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "040",
- "offical_label": "PMC Board DTA Error Rating",
+ "official_label": "PMC Board DTA Error Rating",
"disposition_type": "dta_error"
},
"040HDENRPMC": {
@@ -139,7 +139,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "040",
- "offical_label": "PMC HLR DTA Error - nonrating",
+ "official_label": "PMC HLR DTA Error - Non-Rating",
"disposition_type": "dta_error"
},
"040HDER": {
@@ -147,7 +147,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "040",
- "offical_label": "HLR DTA Error - Rating",
+ "official_label": "HLR DTA Error - Rating",
"disposition_type": "dta_error"
},
"040HDERPMC": {
@@ -155,7 +155,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "040",
- "offical_label": "PMC HLR DTA Error - Rating",
+ "official_label": "PMC HLR DTA Error - Rating",
"disposition_type": "dta_error"
},
"040SCRPMC": {
@@ -163,14 +163,14 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "040",
- "offical_label": "PMC Supplemental Claim Rating"
+ "official_label": "PMC Supplemental Claim Rating"
},
"030BGNRPMC": {
"review_type": "appeal",
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "030",
- "offical_label": "PMC Board Grant nonrating",
+ "official_label": "PMC Board Grant Non-Rating",
"disposition_type": "allowed"
},
"030BGR": {
@@ -178,7 +178,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "030",
- "offical_label": "Board Grant Rating",
+ "official_label": "Board Grant Rating",
"disposition_type": "allowed"
},
"030BGRNR": {
@@ -186,7 +186,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "030",
- "offical_label": "Board Grant nonrating",
+ "official_label": "Board Grant Non-Rating",
"disposition_type": "allowed"
},
"030BGRPMC": {
@@ -194,7 +194,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "030",
- "offical_label": "PMC Board Grant Rating",
+ "official_label": "PMC Board Grant Rating",
"disposition_type": "allowed"
},
"930AHCNRLPMC": {
@@ -202,7 +202,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR Correction of nonrating LQE",
+ "official_label": "AMA PMC HLR Correction of Non-Rating LQE",
"correction_type": "local_quality_error"
},
"930AHCNRLQE": {
@@ -210,7 +210,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR Correction of nonrating LQE",
+ "official_label": "AMA HLR Correction of Non-Rating LQE",
"correction_type": "local_quality_error"
},
"930AHCNRNPMC": {
@@ -218,7 +218,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR Correction of nonrating NQE",
+ "official_label": "AMA PMC HLR Correction of Non-Rating NQE",
"correction_type": "national_quality_error"
},
"930AHCNRNQE": {
@@ -226,7 +226,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA PMC HLR Correction of nonrating NQE",
+ "official_label": "AMA PMC HLR Correction of Non-Rating NQE",
"correction_type": "national_quality_error"
},
"930AHCRLQPMC": {
@@ -234,7 +234,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR Correction of Rating LQE",
+ "official_label": "AMA PMC HLR Correction of Rating LQE",
"correction_type": "local_quality_error"
},
"930AHCRNQPMC": {
@@ -242,7 +242,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR Correction of Rating LQE",
+ "official_label": "AMA PMC HLR Correction of Rating LQE",
"correction_type": "national_quality_error"
},
"930AHDENLPMC": {
@@ -250,7 +250,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR DTA Error NR - Correction of LQE",
+ "official_label": "AMA PMC HLR DTA Error NR - Correction of LQE",
"disposition_type": "dta_error",
"correction_type": "local_quality_error"
},
@@ -259,7 +259,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR DTA Error NR - Correction of NQE",
+ "official_label": "AMA PMC HLR DTA Error NR - Correction of NQE",
"disposition_type": "dta_error",
"correction_type": "national_quality_error"
},
@@ -268,7 +268,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR DTA Error nonrating",
+ "official_label": "AMA PMC HLR DTA Error Non-Rating",
"disposition_type": "dta_error"
},
"930AHDERLPMC": {
@@ -276,7 +276,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR DTA Error Rating - Correction of LQE",
+ "official_label": "AMA PMC HLR DTA Error Rating - Correction of LQE",
"disposition_type": "dta_error",
"correction_type": "local_quality_error"
},
@@ -285,7 +285,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR DTA Error Rating - Correction of NQE",
+ "official_label": "AMA PMC HLR DTA Error Rating - Correction of NQE",
"disposition_type": "dta_error",
"correction_type": "national_quality_error"
},
@@ -294,7 +294,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR DTA Error Rating",
+ "official_label": "AMA PMC HLR DTA Error Rating",
"disposition_type": "dta_error"
},
"930AHNRCPMC": {
@@ -302,7 +302,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR nonrating Control",
+ "official_label": "AMA PMC HLR Non-Rating Control",
"disposition_type": "difference_of_opinion"
},
"930AMADONR": {
@@ -310,7 +310,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Difference of Opinion - NR (930)",
+ "official_label": "AMA Difference of Opinion - NR (930)",
"disposition_type": "difference_of_opinion"
},
"930AMADOR": {
@@ -318,7 +318,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Difference of Opinion - Rating (930)",
+ "official_label": "AMA Difference of Opinion - Rating (930)",
"disposition_type": "difference_of_opinion"
},
"930AMAHCRLQE": {
@@ -326,7 +326,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR Correction of Rating LQE",
+ "official_label": "AMA HLR Correction of Rating LQE",
"correction_type": "local_quality_error"
},
"930AMAHCRNQE": {
@@ -334,7 +334,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR Correction of Rating NQE",
+ "official_label": "AMA HLR Correction of Rating NQE",
"correction_type": "national_quality_error"
},
"930AMAHDENCL": {
@@ -342,7 +342,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR DTA Error NR - Correction of LQE",
+ "official_label": "AMA HLR DTA Error NR - Correction of LQE",
"correction_type": "local_quality_error",
"disposition_type": "dta_error"
},
@@ -351,7 +351,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR DTA Error NR - Correction of NQE",
+ "official_label": "AMA HLR DTA Error NR - Correction of NQE",
"correction_type": "national_quality_error",
"disposition_type": "dta_error"
},
@@ -360,7 +360,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR DTA Error nonrating",
+ "official_label": "AMA HLR DTA Error Non-Rating",
"correction_type": "control",
"disposition_type": "dta_error"
},
@@ -369,7 +369,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR DTA Error Rating",
+ "official_label": "AMA HLR DTA Error Rating",
"correction_type": "control",
"disposition_type": "dta_error"
},
@@ -378,7 +378,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR DTA Error Rating - Correction of LQE",
+ "official_label": "AMA HLR DTA Error Rating - Correction of LQE",
"correction_type": "local_quality_error",
"disposition_type": "dta_error"
},
@@ -387,7 +387,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR DTA Error Rating - Correction of NQE",
+ "official_label": "AMA HLR DTA Error Rating - Correction of NQE",
"correction_type": "national_quality_error",
"disposition_type": "dta_error"
},
@@ -396,7 +396,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR nonrating Control",
+ "official_label": "AMA HLR Non-Rating Control",
"correction_type": "control"
},
"930AMAHRC": {
@@ -404,7 +404,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA HLR Rating Control",
+ "official_label": "AMA HLR Rating Control",
"correction_type": "control"
},
"930AMAHRCPMC": {
@@ -412,7 +412,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC HLR Rating Control",
+ "official_label": "AMA PMC HLR Rating Control",
"correction_type": "control"
},
"930AMARNRC": {
@@ -420,7 +420,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Remand nonrating Control",
+ "official_label": "AMA Remand Non-Rating Control",
"correction_type": "control",
"disposition_type": "board_remand"
},
@@ -429,7 +429,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Remand Rating Control",
+ "official_label": "AMA Remand Rating Control",
"correction_type": "control",
"disposition_type": "board_remand"
},
@@ -438,7 +438,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Remand Rating Correction of LQE",
+ "official_label": "AMA Remand Rating Correction of LQE",
"correction_type": "local_quality_error",
"disposition_type": "board_remand"
},
@@ -447,7 +447,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Remand Rating Correction of NQE",
+ "official_label": "AMA Remand Rating Correction of NQE",
"correction_type": "national_quality_error",
"disposition_type": "board_remand"
},
@@ -456,7 +456,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Remand Rating Control",
+ "official_label": "AMA PMC Remand Rating Control",
"correction_type": "control",
"disposition_type": "board_remand"
},
@@ -465,7 +465,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Supp Correction of Rating LQE",
+ "official_label": "AMA Supp Correction of Rating LQE",
"correction_type": "local_quality_error"
},
"930AMASCRNQE": {
@@ -473,7 +473,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Supp Correction of Rating NQE",
+ "official_label": "AMA Supp Correction of Rating NQE",
"correction_type": "national_quality_error"
},
"930AMASNRC": {
@@ -481,7 +481,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Supp nonrating Control",
+ "official_label": "AMA Supp Non-Rating Control",
"correction_type": "control"
},
"930AMASRC": {
@@ -489,7 +489,7 @@
"issue_type": "rating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Supp Rating Control",
+ "official_label": "AMA Supp Rating Control",
"correction_type": "control"
},
"930AMASRCPMC": {
@@ -497,7 +497,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Supp Rating Control",
+ "official_label": "AMA PMC Supp Rating Control",
"correction_type": "control"
},
"930ARNRCLPMC": {
@@ -505,7 +505,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Remand nonrating Correction of LQE",
+ "official_label": "AMA PMC Remand Non-Rating Correction of LQE",
"correction_type": "local_quality_error",
"disposition_type": "board_remand"
},
@@ -514,7 +514,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Remand nonrating Correction of LQE",
+ "official_label": "AMA Remand Non-Rating Correction of LQE",
"correction_type": "local_quality_error",
"disposition_type": "board_remand"
},
@@ -523,7 +523,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Remand nonrating Correction of NQE",
+ "official_label": "AMA PMC Remand Non-Rating Correction of NQE",
"correction_type": "national_quality_error",
"disposition_type": "board_remand"
},
@@ -532,7 +532,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Remand nonrating Correction of NQE",
+ "official_label": "AMA Remand Non-Rating Correction of NQE",
"correction_type": "national_quality_error",
"disposition_type": "board_remand"
},
@@ -541,7 +541,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Remand nonrating Control",
+ "official_label": "AMA PMC Remand Non-Rating Control",
"correction_type": "control",
"disposition_type": "board_remand"
},
@@ -550,7 +550,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Remand Rating Correction of LQE",
+ "official_label": "AMA PMC Remand Rating Correction of LQE",
"correction_type": "local_quality_error",
"disposition_type": "board_remand"
},
@@ -559,7 +559,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Remand Rating Correction of NQE",
+ "official_label": "AMA PMC Remand Rating Correction of NQE",
"correction_type": "national_quality_error",
"disposition_type": "board_remand"
},
@@ -568,7 +568,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Supp Correction of nonrating LQE",
+ "official_label": "AMA PMC Supp Correction of Non-Rating LQE",
"correction_type": "local_quality_error"
},
"930ASCNRLQE": {
@@ -576,7 +576,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Supp Correction of nonrating LQE",
+ "official_label": "AMA Supp Correction of Non-Rating LQE",
"correction_type": "local_quality_error"
},
"930ASCNRNPMC": {
@@ -584,7 +584,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Supp Correction of nonrating NQE",
+ "official_label": "AMA PMC Supp Correction of Non-Rating NQE",
"correction_type": "national_quality_error"
},
"930ASCNRNQE": {
@@ -592,7 +592,7 @@
"issue_type": "nonrating",
"benefit_type": "compensation",
"family": "930",
- "offical_label": "AMA Supp Correction of nonrating NQE",
+ "official_label": "AMA Supp Correction of Non-Rating NQE",
"correction_type": "national_quality_error"
},
"930ASCRLQPMC": {
@@ -600,7 +600,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Supp Correction of Rating LQE",
+ "official_label": "AMA PMC Supp Correction of Rating LQE",
"correction_type": "local_quality_error"
},
"930ASCRNQPMC": {
@@ -608,7 +608,7 @@
"issue_type": "rating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Supp Correction of Rating NQE",
+ "official_label": "AMA PMC Supp Correction of Rating NQE",
"correction_type": "national_quality_error"
},
"930ASNRCPMC": {
@@ -616,7 +616,7 @@
"issue_type": "nonrating",
"benefit_type": "pension",
"family": "930",
- "offical_label": "AMA PMC Supp nonrating Control",
+ "official_label": "AMA PMC Supp Non-Rating Control",
"correction_type": "control"
}
}
diff --git a/client/test/app/intake/util/__snapshots__/issues.test.js.snap b/client/test/app/intake/util/__snapshots__/issues.test.js.snap
index ac456052d04..33a07c45af1 100644
--- a/client/test/app/intake/util/__snapshots__/issues.test.js.snap
+++ b/client/test/app/intake/util/__snapshots__/issues.test.js.snap
@@ -13,6 +13,7 @@ Array [
"eligibleForSocOptIn": undefined,
"eligibleForSocOptInWithExemption": undefined,
"endProductCleared": null,
+ "endProductCode": null,
"examRequested": null,
"id": null,
"index": 0,
@@ -47,6 +48,7 @@ Array [
"eligibleForSocOptIn": undefined,
"eligibleForSocOptInWithExemption": undefined,
"endProductCleared": null,
+ "endProductCode": null,
"examRequested": null,
"id": null,
"index": 1,
@@ -78,6 +80,7 @@ Array [
"eligibleForSocOptIn": undefined,
"eligibleForSocOptInWithExemption": undefined,
"endProductCleared": null,
+ "endProductCode": null,
"examRequested": null,
"id": "2",
"index": 0,
@@ -112,6 +115,7 @@ Array [
"eligibleForSocOptIn": undefined,
"eligibleForSocOptInWithExemption": undefined,
"endProductCleared": null,
+ "endProductCode": null,
"examRequested": null,
"id": "1",
"index": 1,
diff --git a/client/test/app/queue/components/IhpDaysWaitingTooltip.test.js b/client/test/app/queue/components/IhpDaysWaitingTooltip.test.js
new file mode 100644
index 00000000000..7472ee8275d
--- /dev/null
+++ b/client/test/app/queue/components/IhpDaysWaitingTooltip.test.js
@@ -0,0 +1,109 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import moment from 'moment';
+
+import { axe } from 'jest-axe';
+
+import IhpDaysWaitingTooltip from 'app/queue/components/IhpDaysWaitingTooltip';
+
+const SERIALIZED_DATE_FORMAT = 'YYYY-MM-DDTkk:mm:ss.SSSZ';
+const RENDERED_DATE_FORMAT = 'MM/DD/YY';
+const DEFAULT_DAYS_AGO = {
+ requestedAtDaysAgo: 20,
+ receivedAtDaysAgo: 10
+};
+
+const WRAPPED_CONTENT = 10 days;
+
+describe('IhpDaysWaitingTooltip', () => {
+ beforeEach(() => jest.clearAllMocks());
+
+ const setup = (props = {}) => {
+ return {
+ ...render(
+
+ {WRAPPED_CONTENT}
+
+ ),
+ };
+ };
+
+ const propifyDates = (daysAgo = DEFAULT_DAYS_AGO) => {
+ const { requestedAtDaysAgo, receivedAtDaysAgo } = daysAgo;
+ const requestedAt = isNaN(requestedAtDaysAgo) ? null : moment().subtract(requestedAtDaysAgo, 'd').
+ format(SERIALIZED_DATE_FORMAT);
+ const receivedAt = isNaN(receivedAtDaysAgo) ? null : moment().subtract(receivedAtDaysAgo, 'd').
+ format(SERIALIZED_DATE_FORMAT);
+
+ return {
+ requestedAt,
+ receivedAt
+ };
+ };
+
+ const renderifyDates = (daysAgo = DEFAULT_DAYS_AGO) => {
+ const { requestedAtDaysAgo, receivedAtDaysAgo } = daysAgo;
+ const expectedRequestedAt = isNaN(requestedAtDaysAgo) ? '' : moment().subtract(requestedAtDaysAgo, 'd').
+ format(RENDERED_DATE_FORMAT);
+ const expectedReceivedAt = isNaN(receivedAtDaysAgo) ? '' : `${moment().subtract(receivedAtDaysAgo, 'd').
+ format(RENDERED_DATE_FORMAT)} (${receivedAtDaysAgo} days)`;
+
+ return {
+ expectedRequestedAt,
+ expectedReceivedAt
+ };
+ };
+
+ it('renders correctly', async () => {
+ const { container } = setup(propifyDates());
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it('passes a11y testing', async () => {
+ const { container } = setup(propifyDates());
+
+ const results = await axe(container);
+
+ expect(results).toHaveNoViolations();
+ });
+
+ describe('no requested at date', () => {
+ it('does not render the tooltip', async () => {
+ setup();
+
+ expect(screen.queryByText('IHP Requested:')).toBeFalsy();
+ });
+ });
+
+ describe('non null requested at date', () => {
+ it('renders the tooltip', async () => {
+ setup(propifyDates());
+
+ expect(screen.queryByText('IHP Requested:')).toBeTruthy();
+ });
+
+ it('displays the correct requested at date, received at date, and number of days waiting', () => {
+ setup(propifyDates());
+
+ const { expectedRequestedAt, expectedReceivedAt } = renderifyDates();
+
+ expect(screen.getByTestId('ihp-requested').textContent).toEqual(`IHP Requested: ${expectedRequestedAt}`);
+ expect(screen.getByTestId('ihp-received').textContent).toEqual(`IHP Received: ${expectedReceivedAt}`);
+ expect(screen.getByTestId('ihp-days-waiting').textContent).toEqual('On hold for IHP: 10 days');
+ });
+
+ describe('ihp has not been received', () => {
+ it('displays the number of days waiting since today but not the received at date', () => {
+ const dates = {
+ requestedAtDaysAgo: DEFAULT_DAYS_AGO.requestedAtDaysAgo
+ };
+
+ setup(propifyDates(dates));
+
+ expect(screen.getByTestId('ihp-received').textContent).toEqual('IHP Received: ');
+ expect(screen.getByTestId('ihp-days-waiting').textContent).toEqual('On hold for IHP: 20 days');
+ });
+ });
+ });
+});
diff --git a/client/test/app/queue/components/__snapshots__/IhpDaysWaitingTooltip.test.js.snap b/client/test/app/queue/components/__snapshots__/IhpDaysWaitingTooltip.test.js.snap
new file mode 100644
index 00000000000..e8edc5f2642
--- /dev/null
+++ b/client/test/app/queue/components/__snapshots__/IhpDaysWaitingTooltip.test.js.snap
@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IhpDaysWaitingTooltip renders correctly 1`] = `
+
+
+
+ 10 days
+
+
+
+
+
+
+`;
diff --git a/client/test/karma/queue/QueueLoadingScreen-test.js b/client/test/karma/queue/QueueLoadingScreen-test.js
index d48baba8522..34d57c1aaa5 100644
--- a/client/test/karma/queue/QueueLoadingScreen-test.js
+++ b/client/test/karma/queue/QueueLoadingScreen-test.js
@@ -49,7 +49,8 @@ const serverData = {
status: 'Assigned',
hide_from_queue_table_view: false,
hide_from_case_timeline: false,
- hide_from_task_snapshot: false
+ hide_from_task_snapshot: false,
+ latest_informal_hearing_presentation_task: {}
},
id: '3625593',
type: 'judge_legacy_tasks'
@@ -103,7 +104,11 @@ describe('QueueLoadingScreen', () => {
type: 'LegacyJudgeTask',
hideFromQueueTableView: false,
hideFromCaseTimeline: false,
- hideFromTaskSnapshot: false
+ hideFromTaskSnapshot: false,
+ latestInformalHearingPresentationTask: {
+ requestedAt: undefined,
+ receivedAt: undefined
+ }
}
});
});
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index 1daffe506ed..caa5b20949e 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -27,7 +27,7 @@
"check_name": "SendFile",
"message": "Model attribute used in file name",
"file": "app/controllers/hearings/schedule_periods_controller.rb",
- "line": 59,
+ "line": 56,
"link": "https://brakemanscanner.org/docs/warning_types/file_access/",
"code": "send_file(SchedulePeriod.find(params[:schedule_period_id]).spreadsheet_location, :type => \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\", :disposition => (\"attachment; filename='#{SchedulePeriod.find(params[:schedule_period_id]).file_name}'\"))",
"render_path": null,
@@ -232,8 +232,28 @@
"user_input": "sort_order",
"confidence": "Weak",
"note": ""
+ },
+ {
+ "warning_type": "SQL Injection",
+ "warning_code": 0,
+ "fingerprint": "b2bd6bb603baecc6357c6dfb9641be1848b106ecec425bd7659844e27b860bf1",
+ "check_name": "SQL",
+ "message": "Possible SQL injection",
+ "file": "app/models/task.rb",
+ "line": 216,
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
+ "code": "Arel.sql(\"CASE WHEN #{CachedAppeal.table_name}.is_aod = TRUE THEN #{(\"0 ELSE 1\" or \"1 ELSE 0\")} END, CASE WHEN #{CachedAppeal.table_name}.case_type = 'Court Remand' THEN #{(\"0 ELSE 1\" or \"1 ELSE 0\")} END, #{CachedAppeal.table_name}.docket_number #{order}, #{Task.table_name}.created_at #{order}\")",
+ "render_path": null,
+ "location": {
+ "type": "method",
+ "class": "Task",
+ "method": "order_by_appeal_priority_clause"
+ },
+ "user_input": "order",
+ "confidence": "Medium",
+ "note": ""
}
],
- "updated": "2019-08-14 14:17:55 -0400",
- "brakeman_version": "4.5.1"
+ "updated": "2020-11-03 13:47:15 -0500",
+ "brakeman_version": "4.7.1"
}
diff --git a/db/seeds/mtv.rb b/db/seeds/mtv.rb
index b51be8ad3b6..7266ab4263b 100644
--- a/db/seeds/mtv.rb
+++ b/db/seeds/mtv.rb
@@ -18,7 +18,7 @@ def create_decided_appeal(file_number, mtv_judge, drafting_attorney)
:outcoded,
number_of_claimants: 1,
veteran_file_number: veteran.file_number,
- stream_type: "original"
+ stream_type: Constants.AMA_STREAM_TYPES.original
)
jdr_task = create(:ama_judge_decision_review_task, :completed,
diff --git a/docker-bin/tagnpush.sh b/docker-bin/tagnpush.sh
index 76f2b45482a..fd6b36a6e3b 100755
--- a/docker-bin/tagnpush.sh
+++ b/docker-bin/tagnpush.sh
@@ -1,22 +1,25 @@
#!/bin/bash
-echo "Tagging with Date"
-docker tag caseflow:latest 008577686731.dkr.ecr.us-gov-west-1.amazonaws.com/caseflow:$(date +%F)
-
-echo "Tagging with Latest"
-docker tag caseflow:latest 008577686731.dkr.ecr.us-gov-west-1.amazonaws.com/caseflow:latest
-
echo "Logging in to ECR"
eval $(aws ecr get-login --no-include-email --region us-gov-west-1)
+tag_name="latest"
+
+if [[ -n $1 ]]; then
+ tag_name=$1
+fi
+echo "Tagging with date"
+docker tag caseflow:latest 008577686731.dkr.ecr.us-gov-west-1.amazonaws.com/caseflow:$(date +%F)
+
+echo "Tagging with $tag_name"
+docker tag caseflow:latest 008577686731.dkr.ecr.us-gov-west-1.amazonaws.com/caseflow:$tag_name
+
echo "Pushing to ECR"
if docker push 008577686731.dkr.ecr.us-gov-west-1.amazonaws.com/caseflow:$(date +%F); then
echo "Success the latest docker image has been pushed."
-else
+else
echo "Failed. You likely need to sign in with MFA"
exit 1
fi
-
-# If all went right, also push the tag
-docker push 008577686731.dkr.ecr.us-gov-west-1.amazonaws.com/caseflow:latest
+docker push 008577686731.dkr.ecr.us-gov-west-1.amazonaws.com/caseflow:$tag_name
echo "Completed!"
\ No newline at end of file
diff --git a/docs/tech-specs/2020-10-16-reader-refactor.md b/docs/tech-specs/2020-10-16-reader-refactor.md
new file mode 100644
index 00000000000..3d2d7017725
--- /dev/null
+++ b/docs/tech-specs/2020-10-16-reader-refactor.md
@@ -0,0 +1,73 @@
+# Overview
+
+This refactor will be limited to the Reader codebase within the greater Caseflow application and will not introduce any new logic nor will it change any existing logic. The purpose of this refactor will be to fix the underlying bug that is causing the reader documents table to lose filters between screen changes. We also hope to add some additional changes that will improve the overall codebase, but be isolated to the Reader codebase at first.
+
+This refactor would also include a feature flag to hide the changes behind so that we can preserve the existing codebase while we are testing the refactored changes.
+
+## Changes
+
+Below are the changes that we are hoping to make during the refactor that we believe will bring the best value:
+
+- Move the logic that currently exists in individual components into a central Redux store (currently scoped to the Reader code, but that could be expanded upon later)
+- Introduce a new folder structure that can be used for the Reader code at first, but that could be expanded to be used by the entire application in the future (NOTE: this would preserve the existing structure, but add some additional folders that the rest of the codebase in addition to reader could be migrated to at some point). The proposed structure would be as follows:
+
+**NOTE:** `index.js` files will be used to export all components/functions from a given folder allowing us to also add tree shaking as a performance improvement
+```
+...
+app
+└── 2.0
+ ├── layouts // Components to structure screens in a standardized way
+ | ├── BaseLayout.jsx // Includes app banner, footer and site-wide navigation
+ | ├── TableLayout.jsx // A layout that primarily centers around a table
+ │ └── index.js
+ ├── routes // Holds the routing information for different groups of screens
+ | ├── reader.jsx
+ │ └── index.js
+ ├── screens // Contains top-level screens that are redux-connected and pass state down through props
+ │ ├── reader
+ | │ ├── DocumentsTable.jsx
+ | │ └── Document.jsx
+ │ └── index.js
+ ├── store
+ │ ├── actions
+ │ │ ├── reader
+ │ │ └── index.js
+ │ ├── dispatchers
+ │ │ ├── reader
+ │ │ └── index.js
+ │ └── reducers
+ │ ├── reader
+ │ └── index.js
+ └── components // Contains independent components used for constructing parts of individual screens
+ ├── reader
+ └── index.js
+...
+```
+- Add webpack aliasing to make it easier to move components around and know exactly where they are located ([#15439](https://github.com/department-of-veterans-affairs/caseflow/issues/15439))
+- Upgrade some of the outdated packages that belong to Reader including but not limited to the PDFjs package which is currently a full major version behind ([#12784](https://github.com/department-of-veterans-affairs/caseflow/issues/12784))
+- Lazy load components to reduce the overall size of the webpack bundle and speed up page loads ([#15435](https://github.com/department-of-veterans-affairs/caseflow/issues/15435))
+
+## Goals
+
+By implementing the above changes, we believe that we will be able to achieve the following in addition to resolving the [underlying Reader bug](https://github.com/department-of-veterans-affairs/caseflow/issues/15173)
+
+- Improve readability of the codebase
+- Improve the performance of the Reader application
+- Reduce the complexity of the codebase to increase the speed at which we are able to determine these types of issues
+- Mitigate future bugs by reducing the number of places that a bug could occur
+
+## Capacity
+
+We believe that the above refactor would require the following engineering capacity and no capacity from any other teams:
+
+**Engineers:** 1-2
+**Story Points:** 8-13 (1-2 Sprint)
+
+## Future Work
+
+This work will begin focused on the Reader application, however it could easily be expanded to the rest of the Caseflow applications by taking a few steps within each. All of the high-level caseflow applications including Hearings, Queue, and Intake all have an entry point in and `index.jsx` file which loads the actual frontend application. Because of this, we can insert a feature flag in front of this render that will either continue to render the existing application, or redirect to the refactored version based on which screen the user is attempting to access. In this way, we can provide an incremental adoption approach so that engineers will not be forced to make sweeping changes to the codebase, but rather move over a single screen at a time until the migration is complete.
+
+## Open Questions/Risks
+
+- Risk of losing functionality while moving code around
+ - We believe to have addressed this by isolating the changes behind a feature flag as well as maintaining the existing codebase and refactoring under a new folder structure
\ No newline at end of file
diff --git a/spec/controllers/hearings/schedule_periods_controller_spec.rb b/spec/controllers/hearings/schedule_periods_controller_spec.rb
index 708d5922c45..b5c7ef94360 100644
--- a/spec/controllers/hearings/schedule_periods_controller_spec.rb
+++ b/spec/controllers/hearings/schedule_periods_controller_spec.rb
@@ -117,7 +117,7 @@
@controller = Hearings::HearingDayController.new
get :index, params: { start_date: "2018-01-01", end_date: "2018-06-01" }, as: :json
expect(response).to be_successful
- expect(JSON.parse(response.body)["hearings"].size).to eq(427)
+ expect(JSON.parse(response.body)["hearings"].size).to eq(365)
end
it "persist twice and second request should return an error" do
diff --git a/spec/factories/appeal.rb b/spec/factories/appeal.rb
index 7e8e6799300..83424262866 100644
--- a/spec/factories/appeal.rb
+++ b/spec/factories/appeal.rb
@@ -100,6 +100,10 @@
end
end
+ trait :type_cavc_remand do
+ stream_type { Constants.AMA_STREAM_TYPES.court_remand }
+ end
+
trait :hearing_docket do
docket_type { Constants.AMA_DOCKETS.hearing }
end
diff --git a/spec/factories/docket_change.rb b/spec/factories/docket_change.rb
index 35433359df1..18f0e9d3111 100644
--- a/spec/factories/docket_change.rb
+++ b/spec/factories/docket_change.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :docket_change do
old_docket_stream { create(:appeal, docket_type: Constants.AMA_DOCKETS.evidence_submission) }
- new_docket_stream { create(:appeal, stream_type: "original") }
+ new_docket_stream { create(:appeal, stream_type: Constants.AMA_STREAM_TYPES.original) }
task { create(:docket_switch_mail_task) }
receipt_date { 5.days.ago }
docket_type { Constants.AMA_DOCKETS.hearing }
diff --git a/spec/factories/post_decision_motions.rb b/spec/factories/post_decision_motions.rb
index 57bce599e5c..ba4973ed06c 100644
--- a/spec/factories/post_decision_motions.rb
+++ b/spec/factories/post_decision_motions.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :post_decision_motion do
- appeal { create(:appeal, stream_type: "vacate") }
+ appeal { create(:appeal, stream_type: Constants.AMA_STREAM_TYPES.vacate) }
disposition { "granted" }
vacate_type { "straight_vacate" }
end
diff --git a/spec/factories/task.rb b/spec/factories/task.rb
index 4958f61f340..5c3c9a19c66 100644
--- a/spec/factories/task.rb
+++ b/spec/factories/task.rb
@@ -2,9 +2,9 @@
FactoryBot.define do
module FactoryBotHelper
- def self.find_first_task_or_create(appeal, task_type)
+ def self.find_first_task_or_create(appeal, task_type, **kwargs)
(appeal.tasks.open.where(type: task_type.name).first if appeal) ||
- FactoryBot.create(task_type.name.underscore.to_sym, appeal: appeal)
+ FactoryBot.create(task_type.name.underscore.to_sym, appeal: appeal, **kwargs) { |t| yield(t) if block_given? }
end
end
@@ -359,6 +359,11 @@ def self.find_first_task_or_create(appeal, task_type)
parent { FactoryBotHelper.find_first_task_or_create(appeal, DistributionTask) }
end
+ factory :send_cavc_remand_processed_letter_task, class: SendCavcRemandProcessedLetterTask do
+ assigned_to { CavcLitigationSupport.singleton }
+ parent { FactoryBotHelper.find_first_task_or_create(appeal, CavcTask) }
+ end
+
factory :hearing_task, class: HearingTask do
assigned_to { Bva.singleton }
parent { appeal.root_task || create(:root_task, appeal: appeal) }
diff --git a/spec/feature/api/v2/appeals_spec.rb b/spec/feature/api/v2/appeals_spec.rb
index 69c8d9a7b20..56754a6183b 100644
--- a/spec/feature/api/v2/appeals_spec.rb
+++ b/spec/feature/api/v2/appeals_spec.rb
@@ -463,7 +463,7 @@
expect(json["data"][2]["attributes"]["appealIds"].length).to eq(1)
expect(json["data"][2]["attributes"]["appealIds"].first).to include("A")
expect(json["data"][2]["attributes"]["updated"]).to eq("2018-11-27T19:00:00-05:00")
- expect(json["data"][2]["attributes"]["type"]).to eq("original")
+ expect(json["data"][2]["attributes"]["type"]).to eq(Constants.AMA_STREAM_TYPES.original.titleize)
expect(json["data"][2]["attributes"]["active"]).to eq(true)
expect(json["data"][2]["attributes"]["incompleteHistory"]).to eq(false)
expect(json["data"][2]["attributes"]["description"]).to eq("2 issues")
diff --git a/spec/feature/hearings/build_schedule/build_hearsched_spec.rb b/spec/feature/hearings/build_schedule/build_hearsched_spec.rb
index a90f15171f2..ea81dc55fec 100644
--- a/spec/feature/hearings/build_schedule/build_hearsched_spec.rb
+++ b/spec/feature/hearings/build_schedule/build_hearsched_spec.rb
@@ -28,7 +28,7 @@
video_hearing_days = HearingDay.where(request_type: "V")
expect(video_hearing_days.count).to eq(allocation_count)
co_hearing_days = HearingDay.where(request_type: "C")
- expect(co_hearing_days.count). to eq(84)
+ expect(co_hearing_days.count). to eq(22)
end
end
diff --git a/spec/feature/hearings/convert_travel_board_hearing/edit_hearsched_spec.rb b/spec/feature/hearings/convert_travel_board_hearing/edit_hearsched_spec.rb
index fa7ff742034..9eba92bada5 100644
--- a/spec/feature/hearings/convert_travel_board_hearing/edit_hearsched_spec.rb
+++ b/spec/feature/hearings/convert_travel_board_hearing/edit_hearsched_spec.rb
@@ -5,6 +5,7 @@
let!(:vacols_case) do
create(
:case,
+ :type_original,
bfhr: "2" # Travel Board
)
end
diff --git a/spec/feature/intake/edit_ep_claim_labels_spec.rb b/spec/feature/intake/edit_ep_claim_labels_spec.rb
new file mode 100644
index 00000000000..35b303eb6ec
--- /dev/null
+++ b/spec/feature/intake/edit_ep_claim_labels_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+feature "Intake Edit EP Claim Labels", :all_dbs do
+ include IntakeHelpers
+
+ before do
+ setup_intake_flags
+ FeatureToggle.enable!(:edit_ep_claim_labels)
+ end
+
+ after do
+ FeatureToggle.disable!(:edit_ep_claim_labels)
+ end
+
+ let!(:current_user) { User.authenticate!(roles: ["Mail Intake"]) }
+ let(:veteran_file_number) { "123412345" }
+ let(:veteran) { create(:veteran) }
+ let(:receipt_date) { Time.zone.today - 20 }
+ let(:profile_date) { 10.days.ago }
+ let(:promulgation_date) { 9.days.ago.to_date }
+ let!(:rating) { generate_rating_with_defined_contention(veteran, promulgation_date, profile_date) }
+ let(:benefit_type) { "compensation" }
+
+ let!(:higher_level_review) do
+ create(
+ :higher_level_review,
+ veteran_file_number: veteran.file_number,
+ receipt_date: receipt_date,
+ benefit_type: benefit_type,
+ legacy_opt_in_approved: false
+ )
+ end
+
+ # create associated intake
+ let!(:intake) do
+ create(
+ :intake,
+ user: current_user,
+ detail: higher_level_review,
+ veteran_file_number: veteran.file_number,
+ started_at: Time.zone.now,
+ completed_at: Time.zone.now,
+ completion_status: "success",
+ type: "HigherLevelReviewIntake"
+ )
+ end
+
+ let(:rating_request_issue) do
+ create(
+ :request_issue,
+ contested_rating_issue_reference_id: "def456",
+ contested_rating_issue_profile_date: rating.profile_date,
+ decision_review: higher_level_review,
+ benefit_type: benefit_type,
+ contested_issue_description: "PTSD denied"
+ )
+ end
+
+ let(:nonrating_request_issue) do
+ create(
+ :request_issue,
+ :nonrating,
+ decision_review: higher_level_review,
+ benefit_type: benefit_type,
+ contested_issue_description: "Apportionment"
+ )
+ end
+
+ let(:ineligible_request_issue) do
+ create(
+ :request_issue,
+ :nonrating,
+ :ineligible,
+ decision_review: higher_level_review,
+ benefit_type: benefit_type,
+ contested_issue_description: "Ineligible issue"
+ )
+ end
+
+ let(:withdrawn_request_issue) do
+ create(
+ :request_issue,
+ :nonrating,
+ :withdrawn,
+ decision_review: higher_level_review,
+ contested_issue_description: "Issue that's been withdrawn"
+ )
+ end
+
+ context "When editing a decision review with end products" do
+ before do
+ higher_level_review.create_issues!(
+ [
+ rating_request_issue,
+ nonrating_request_issue,
+ ineligible_request_issue,
+ withdrawn_request_issue
+ ]
+ )
+ higher_level_review.establish!
+ end
+
+ it "shows each established end product label" do
+ visit "higher_level_reviews/#{higher_level_review.uuid}/edit"
+
+ # First shows issues on end products, in ascending order by EP code
+ # Note for these, there's a row for the EP, and another for the issues
+ row = find("#table-row-8")
+ label = Constants::EP_CLAIM_TYPES[nonrating_request_issue.end_product_establishment.code]["official_label"]
+ expect(row).to have_content(label)
+ expect(row).to have_button("Edit claim label")
+ expect(find("#table-row-9")).to have_content(/Requested issues\n1. #{nonrating_request_issue.description}/i)
+
+ label = Constants::EP_CLAIM_TYPES[rating_request_issue.end_product_establishment.code]["official_label"]
+ row = find("#table-row-10")
+ expect(row).to have_content(label)
+ expect(row).to have_button("Edit claim label")
+ expect(find("#table-row-11")).to have_content(/Requested issues\n2. #{rating_request_issue.description}/i)
+
+ # Shows issues not on end products (single row)
+ row = find("#table-row-12")
+ expect(row).to have_content(/Requested issues\n3. #{ineligible_request_issue.description}/i)
+
+ # Shows withdrawn issues last (single row)
+ row = find("#table-row-13")
+ expect(row).to have_content(
+ /Withdrawn issues\n4. #{withdrawn_request_issue.description}/i
+ )
+ end
+ end
+end
diff --git a/spec/feature/intake/higher_level_review/edit_spec.rb b/spec/feature/intake/higher_level_review/edit_spec.rb
index da5384ae869..79ce1025d55 100644
--- a/spec/feature/intake/higher_level_review/edit_spec.rb
+++ b/spec/feature/intake/higher_level_review/edit_spec.rb
@@ -334,19 +334,23 @@
).reference_id
end
+ let!(:starting_request_issues) do
+ [
+ eligible_request_issue,
+ untimely_request_issue,
+ ri_with_active_previous_review,
+ ri_with_previous_hlr,
+ ri_before_ama,
+ eligible_ri_before_ama,
+ ri_legacy_issue_not_withdrawn,
+ ri_legacy_issue_ineligible
+ ]
+ end
+
before do
setup_legacy_opt_in_appeals(veteran.file_number)
another_higher_level_review.create_issues!([ri_in_review])
- higher_level_review.create_issues!([
- eligible_request_issue,
- untimely_request_issue,
- ri_with_active_previous_review,
- ri_with_previous_hlr,
- ri_before_ama,
- eligible_ri_before_ama,
- ri_legacy_issue_not_withdrawn,
- ri_legacy_issue_ineligible
- ])
+ higher_level_review.create_issues!(starting_request_issues)
higher_level_review.establish!
end
@@ -364,11 +368,24 @@
vacols_sequence_id: "2"
)
end
-
+ let!(:starting_request_issues) do
+ [
+ eligible_request_issue,
+ untimely_request_issue,
+ ri_with_active_previous_review,
+ ri_with_previous_hlr,
+ ri_before_ama,
+ eligible_ri_before_ama,
+ ri_legacy_issue_not_withdrawn,
+ ri_legacy_issue_ineligible,
+ ri_legacy_issue_eligible
+ ]
+ end
let(:legacy_opt_in_approved) { true }
it "shows the Higher-Level Review Edit page with ineligibility messages" do
visit "higher_level_reviews/#{ep_claim_id}/edit"
+
expect(page).to have_content(
"#{ri_with_previous_hlr.contention_text} #{ineligible.higher_level_review_to_higher_level_review}"
)
@@ -537,12 +554,11 @@
click_edit_submit_and_confirm
- expect(page).to have_current_path(
- "/higher_level_reviews/#{higher_level_review.uuid}/edit/confirmation"
- )
+ expect(page).to have_current_path("/higher_level_reviews/#{higher_level_review.uuid}/edit/confirmation")
- visit "higher_level_reviews/#{higher_level_review.uuid}/edit"
+ click_on "correct the issues"
+ expect(page).to have_current_path("/higher_level_reviews/#{higher_level_review.uuid}/edit")
expect(page).to have_content(COPY::VACOLS_OPTIN_ISSUE_CLOSED_EDIT)
end
end
diff --git a/spec/feature/intake/supplemental_claim/edit_spec.rb b/spec/feature/intake/supplemental_claim/edit_spec.rb
index 28528d5c4f4..4d7773f8643 100644
--- a/spec/feature/intake/supplemental_claim/edit_spec.rb
+++ b/spec/feature/intake/supplemental_claim/edit_spec.rb
@@ -505,12 +505,14 @@
)
# reload to verify that the new issues populate the form
- visit "supplemental_claims/#{rating_ep_claim_id}/edit"
+ click_on "correct the issues"
+ supplemental_claim.reload
+ expect(page).to have_current_path("/supplemental_claims/#{supplemental_claim.uuid}/edit")
expect(page).to have_content("Left knee granted")
expect(page).to_not have_content("PTSD denied")
# assert server has updated data
- new_request_issue = supplemental_claim.reload.request_issues.active.first
+ new_request_issue = supplemental_claim.request_issues.active.first
expect(new_request_issue.description).to eq("Left knee granted")
expect(request_issue.reload.decision_review).to_not be_nil
expect(request_issue.contention_removed_at).to eq(Time.zone.now)
diff --git a/spec/feature/queue/cavc_task_queue_spec.rb b/spec/feature/queue/cavc_task_queue_spec.rb
new file mode 100644
index 00000000000..4ddef4d5566
--- /dev/null
+++ b/spec/feature/queue/cavc_task_queue_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+RSpec.feature "CAVC-related tasks queue", :all_dbs do
+ let!(:org_admin) do
+ create(:user, full_name: "Adminy CacvRemandy") do |u|
+ OrganizationsUser.make_user_admin(u, CavcLitigationSupport.singleton)
+ end
+ end
+ let!(:org_nonadmin) { create(:user, full_name: "Woney Remandy") { |u| CavcLitigationSupport.singleton.add_user(u) } }
+ let!(:org_nonadmin2) { create(:user, full_name: "Tooey Remandy") { |u| CavcLitigationSupport.singleton.add_user(u) } }
+ let!(:other_user) { create(:user, full_name: "Othery Usery") }
+
+ context "when CAVC Lit Support is assigned SendCavcRemandProcessedLetterTask" do
+ let!(:send_task) { create(:send_cavc_remand_processed_letter_task) }
+
+ it "allows admin to assign SendCavcRemandProcessedLetterTask to user" do
+ # Logged in as CAVC Lit Support admin
+ User.authenticate!(user: org_admin)
+ visit "queue/appeals/#{send_task.appeal.external_id}"
+
+ find(".cf-select__control", text: "Select an action").click
+ find("div", class: "cf-select__option", text: Constants.TASK_ACTIONS.ASSIGN_TO_PERSON.label).click
+
+ find(".cf-select__control", text: org_admin.full_name).click
+ find("div", class: "cf-select__option", text: org_nonadmin.full_name).click
+ fill_in "taskInstructions", with: "Confirm info and send letter to Veteran."
+ click_on "Submit"
+ expect(page).to have_content COPY::ASSIGN_TASK_SUCCESS_MESSAGE % org_nonadmin.full_name
+
+ # Logged in as first user assignee
+ User.authenticate!(user: org_nonadmin)
+ visit "queue/appeals/#{send_task.appeal.external_id}"
+
+ find(".cf-select__control", text: "Select an action").click
+ expect(page).to have_content Constants.TASK_ACTIONS.MARK_COMPLETE.label
+ expect(page).to have_content Constants.TASK_ACTIONS.REASSIGN_TO_PERSON.label
+
+ find("div", class: "cf-select__option", text: Constants.TASK_ACTIONS.REASSIGN_TO_PERSON.label).click
+ find(".cf-select__control", text: COPY::ASSIGN_WIDGET_DROPDOWN_PLACEHOLDER).click
+ find("div", class: "cf-select__option", text: org_nonadmin2.full_name).click
+ fill_in "taskInstructions", with: "Going fishing. Handing off to you."
+ click_on "Submit"
+ expect(page).to have_content COPY::REASSIGN_TASK_SUCCESS_MESSAGE % org_nonadmin2.full_name
+
+ # Logged in as second user assignee (due to reassignment)
+ User.authenticate!(user: org_nonadmin2)
+ visit "queue/appeals/#{send_task.appeal.external_id}"
+
+ find(".cf-select__control", text: "Select an action").click
+ find("div", class: "cf-select__option", text: Constants.TASK_ACTIONS.MARK_COMPLETE.label).click
+ fill_in "completeTaskInstructions", with: "Letter sent."
+ click_on COPY::MARK_TASK_COMPLETE_BUTTON
+ expect(page).to have_content COPY::MARK_TASK_COMPLETE_CONFIRMATION % send_task.appeal.veteran_full_name
+ end
+ end
+end
diff --git a/spec/feature/queue/docket_change_spec.rb b/spec/feature/queue/docket_change_spec.rb
index ad43ea67fa9..50db760b5b5 100644
--- a/spec/feature/queue/docket_change_spec.rb
+++ b/spec/feature/queue/docket_change_spec.rb
@@ -72,6 +72,7 @@
it "allows Clerk of the Board attorney to send docket switch recommendation to judge" do
User.authenticate!(user: cotb_user)
visit "/queue/appeals/#{appeal.uuid}"
+
find(".cf-select__control", text: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL).click
find("div", class: "cf-select__option", text: Constants.TASK_ACTIONS.DOCKET_SWITCH_SEND_TO_JUDGE.label).click
@@ -86,11 +87,13 @@
# The previously assigned judge should be selected
expect(page).to have_content(judge_assign_task.assigned_to.display_name)
-
click_button(text: "Submit")
# Return back to user's queue
expect(page).to have_current_path("/queue")
+ # Return back to successs banner
+ expect(page).to have_content(appeal.appellant_name, judge_assign_task.assigned_to.display_name)
+ expect(page).to have_content(COPY::DOCKET_SWITCH_REQUEST_MESSAGE)
judge_task = DocketSwitchRulingTask.find_by(assigned_to: judge)
expect(judge_task).to_not be_nil
diff --git a/spec/feature/queue/motion_to_vacate_spec.rb b/spec/feature/queue/motion_to_vacate_spec.rb
index 2200116a575..8228fce4f26 100644
--- a/spec/feature/queue/motion_to_vacate_spec.rb
+++ b/spec/feature/queue/motion_to_vacate_spec.rb
@@ -579,7 +579,9 @@ def return_to_lit_support(disposition:)
PostDecisionMotionUpdater.new(judge_address_motion_to_vacate_task, post_decision_motion_params)
end
let!(:post_decision_motion) { post_decision_motion_updater.process }
- let(:vacate_stream) { Appeal.find_by(stream_docket_number: appeal.docket_number, stream_type: "vacate") }
+ let(:vacate_stream) do
+ Appeal.find_by(stream_docket_number: appeal.docket_number, stream_type: Constants.AMA_STREAM_TYPES.vacate)
+ end
let(:attorney_task) { AttorneyTask.find_by(assigned_to: drafting_attorney) }
let(:review_decisions_path) do
@@ -1094,7 +1096,9 @@ def add_decision_to_issue(idx, disposition, description)
end
def visit_vacate_stream
- vacate_stream = Appeal.find_by(stream_docket_number: appeal.docket_number, stream_type: "vacate")
+ vacate_stream = Appeal.find_by(
+ stream_docket_number: appeal.docket_number, stream_type: Constants.AMA_STREAM_TYPES.vacate
+ )
visit "/queue/appeals/#{vacate_stream.uuid}"
expect(page).to have_content("Vacate")
find("span", text: "View all cases").click
diff --git a/spec/jobs/set_appeal_age_aod_job_spec.rb b/spec/jobs/set_appeal_age_aod_job_spec.rb
index 8adfa1081ce..62963e7a110 100644
--- a/spec/jobs/set_appeal_age_aod_job_spec.rb
+++ b/spec/jobs/set_appeal_age_aod_job_spec.rb
@@ -3,12 +3,6 @@
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) }
@@ -22,7 +16,10 @@
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 }
+ allow_any_instance_of(SlackService).to receive(:send_notification) do |_, msg, title|
+ @slack_msg = msg
+ @slack_title = title
+ end
age_aod_appeal_wrong_dob.update(aod_based_on_age: true)
# simulate date-of-birth being corrected
@@ -39,7 +36,7 @@
expect(age_aod_appeal_wrong_dob.aod_based_on_age).to eq(true)
described_class.perform_now
- expect(@slack_msg).to include(success_msg)
+ expect(@slack_title).to include("[INFO] SetAppealAgeAodJob completed after running for less than a minute.")
# `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)
@@ -56,14 +53,15 @@
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(SlackService).to receive(:send_notification) do |_, msg, title|
+ @slack_msg = msg
+ @slack_title = title
+ end
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}/)
+ expect(@slack_title).to match(/#{described_class.name} failed after running for .*. Fatal error: #{error_msg}/)
end
end
end
diff --git a/spec/jobs/update_cached_appeals_attributes_job_spec.rb b/spec/jobs/update_cached_appeals_attributes_job_spec.rb
index c3e5af3fb19..c5fe6f601a6 100644
--- a/spec/jobs/update_cached_appeals_attributes_job_spec.rb
+++ b/spec/jobs/update_cached_appeals_attributes_job_spec.rb
@@ -90,14 +90,15 @@
end
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(SlackService).to receive(:send_notification) do |_, msg, title|
+ @slack_msg = msg
+ @slack_title = title
+ end
subject
- expected_msg = "UpdateCachedAppealsAttributesJob failed after running for .*. See Sentry event .*"
-
- expect(slack_msg).to match(/#{expected_msg}/)
+ expect(@slack_title).to match(/\[ERROR\] UpdateCachedAppealsAttributesJob failed after running for .*/)
+ expect(@slack_msg).to match(/See Sentry event .*/)
end
end
@@ -153,16 +154,17 @@
context "when BGS fails" do
shared_examples "rescues error" do
it "completes and sends warning to Slack" do
- slack_msg = ""
- allow_any_instance_of(SlackService).to receive(:send_notification) { |_, first_arg| slack_msg = first_arg }
+ allow_any_instance_of(SlackService).to receive(:send_notification) do |_, msg, title|
+ @slack_msg = msg
+ @slack_title = title
+ end
job = described_class.new
job.perform_now
expect(job.warning_msgs.count).to eq 3
- expect(slack_msg.lines.count).to eq 4
- expected_msg = "\\[WARN\\] UpdateCachedAppealsAttributesJob .*"
- expect(slack_msg).to match(/#{expected_msg}/)
+ expect(@slack_msg.lines.count).to eq 3
+ expect(@slack_title).to match(/\[WARN\] UpdateCachedAppealsAttributesJob: .*/)
end
end
diff --git a/spec/models/appeal_shared_examples.rb b/spec/models/appeal_shared_examples.rb
index 02052147dfc..6446d302575 100644
--- a/spec/models/appeal_shared_examples.rb
+++ b/spec/models/appeal_shared_examples.rb
@@ -17,3 +17,91 @@
expect(appeal.overtime?).to be(false)
end
end
+
+shared_examples "latest informal hearing presentation task" do
+ shared_examples "the appeal has an ihp task" do
+ it "returns the ihp task" do
+ expect(subject).to eq(ihp_task)
+ end
+
+ context "when the task is completed" do
+ before { ihp_task.completed! }
+
+ it "returns the ihp task" do
+ expect(subject).to eq(ihp_task)
+ end
+ end
+
+ context "when the task is cancelled" do
+ before { ihp_task.cancelled! }
+
+ it { expect(subject).to eq(nil) }
+ end
+ end
+
+ before { allow_any_instance_of(Colocated).to receive(:next_assignee).and_return(nil) }
+
+ let!(:root_task) { create(:root_task, appeal: appeal) }
+
+ subject { appeal.latest_informal_hearing_presentation_task }
+
+ context "when the appeal has no informal hearing presentation tasks" do
+ it { expect(subject).to eq(nil) }
+ end
+
+ context "when the appeal has an InformalHearingPresentationTask" do
+ let!(:ihp_task) { create(:informal_hearing_presentation_task, appeal: appeal) }
+
+ it_behaves_like "the appeal has an ihp task"
+ end
+
+ context "when the appeal has an InformalHearingPresentationTask" do
+ let!(:ihp_task) { create(:ama_colocated_task, :ihp, appeal: appeal) }
+
+ it_behaves_like "the appeal has an ihp task"
+ end
+
+ context "when there are multiple ihp tasks on the appeal" do
+ shared_examples "multiple ihp tasks" do
+ it "returns the more recent ihp task" do
+ expect(subject).to eq most_recent_task
+ end
+ end
+
+ context "when one task was completed more recently than the other" do
+ let!(:most_recent_task) { create(:ama_colocated_task, :ihp, appeal: appeal, closed_at: 1.day.ago) }
+ let!(:older_task) { create(:ama_colocated_task, :ihp, appeal: appeal, closed_at: 2.days.ago) }
+
+ it_behaves_like "multiple ihp tasks"
+
+ context "when the recently completed one was assigned before the less recently completed task" do
+ before do
+ most_recent_task.update!(assigned_at: 2.days.ago)
+ older_task.update!(assigned_at: 1.day.ago)
+ end
+
+ it_behaves_like "multiple ihp tasks"
+ end
+
+ context "when both were cancelled" do
+ before { [most_recent_task, older_task].each(&:cancelled!) }
+
+ it { expect(subject).to eq(nil) }
+ end
+ end
+
+ context "when one task was assigned more recently than the other" do
+ let!(:most_recent_task) { create(:ama_colocated_task, :ihp, appeal: appeal, assigned_at: 1.day.ago) }
+ let!(:older_task) { create(:ama_colocated_task, :ihp, appeal: appeal, assigned_at: 2.days.ago) }
+
+ it_behaves_like "multiple ihp tasks"
+ end
+
+ context "when one task was assigned more recently than the other was closed" do
+ let!(:most_recent_task) { create(:ama_colocated_task, :ihp, appeal: appeal, assigned_at: 1.day.ago) }
+ let!(:older_task) { create(:ama_colocated_task, :ihp, appeal: appeal, closed_at: 2.days.ago) }
+
+ it_behaves_like "multiple ihp tasks"
+ end
+ end
+end
diff --git a/spec/models/appeal_spec.rb b/spec/models/appeal_spec.rb
index 82b6b044747..bd2cb0e7fc7 100644
--- a/spec/models/appeal_spec.rb
+++ b/spec/models/appeal_spec.rb
@@ -12,7 +12,7 @@
let!(:appeal) { create(:appeal) } # must be *after* Timecop.freeze
context "#create_stream" do
- let(:stream_type) { "vacate" }
+ let(:stream_type) { Constants.AMA_STREAM_TYPES.vacate }
let!(:appeal) { create(:appeal, number_of_claimants: 1) }
subject { appeal.create_stream(stream_type) }
@@ -34,7 +34,7 @@
end
context "for de_novo appeal stream" do
- let(:stream_type) { "de_novo" }
+ let(:stream_type) { Constants.AMA_STREAM_TYPES.de_novo }
it "creates a de_novo appeal stream with data from the original appeal" do
expect(subject).to have_attributes(
@@ -973,7 +973,7 @@
end
context "for cavc stream" do
- let(:appeal) { create(:appeal, stream_type: "court_remand") }
+ let(:appeal) { create(:appeal, stream_type: Constants.AMA_STREAM_TYPES.court_remand) }
it "returns true" do
expect(subject).to eq(true)
@@ -1230,4 +1230,10 @@
end
end
end
+
+ describe "#latest_informal_hearing_presentation_task" do
+ let(:appeal) { create(:appeal) }
+
+ it_behaves_like "latest informal hearing presentation task"
+ end
end
diff --git a/spec/models/end_product_update_spec.rb b/spec/models/end_product_update_spec.rb
index d2d951a3a15..eab338ea759 100644
--- a/spec/models/end_product_update_spec.rb
+++ b/spec/models/end_product_update_spec.rb
@@ -25,11 +25,14 @@
let(:old_code) { "030HLRNR" }
let(:new_code) { "030HLRR" }
- it "updates issue type on request issues" do
+ it "updates type and attributes on request issues" do
subject
expect(epu.request_issues).not_to be_empty
- expect(epu.request_issues).to all have_attributes(type: "RatingRequestIssue")
+ expect(epu.request_issues).to all have_attributes(
+ type: "RatingRequestIssue",
+ description: "nonrating issue description"
+ )
end
end
diff --git a/spec/models/hearing_day_spec.rb b/spec/models/hearing_day_spec.rb
index c963453fd0e..7a41afa273f 100644
--- a/spec/models/hearing_day_spec.rb
+++ b/spec/models/hearing_day_spec.rb
@@ -344,7 +344,7 @@
subject { HearingDayRange.new(schedule_period.start_date, schedule_period.end_date).load_days }
it do
- expect(subject.size).to eql(427)
+ expect(subject.size).to eql(365)
end
end
end
diff --git a/spec/models/legacy_appeal_spec.rb b/spec/models/legacy_appeal_spec.rb
index 02eaf275adc..8d96c8ce87c 100644
--- a/spec/models/legacy_appeal_spec.rb
+++ b/spec/models/legacy_appeal_spec.rb
@@ -2987,4 +2987,10 @@
end
end
end
+
+ describe "#latest_informal_hearing_presentation_task" do
+ let(:appeal) { create(:legacy_appeal) }
+
+ it_behaves_like "latest informal hearing presentation task"
+ end
end
diff --git a/spec/models/task_pager_spec.rb b/spec/models/task_pager_spec.rb
index b06bdb3f5b9..0cbfa74317f 100644
--- a/spec/models/task_pager_spec.rb
+++ b/spec/models/task_pager_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "faker"
+require_relative "tasks/task_shared_examples.rb"
describe TaskPager, :all_dbs do
let(:assignee) { create(:organization) }
@@ -435,41 +436,11 @@
end
context "when sorting by Appeal Type column" do
+ let(:tab_name) { Constants.QUEUE_CONFIG.UNASSIGNED_TASKS_TAB_NAME }
let(:sort_by) { Constants.QUEUE_CONFIG.COLUMNS.APPEAL_TYPE.name }
let!(:created_tasks) { [] }
- let(:legacy_appeal_1) { create(:legacy_appeal, vacols_case: create(:case, :type_original)) }
- let(:legacy_appeal_2) { create(:legacy_appeal, vacols_case: create(:case, :type_post_remand)) }
- let(:legacy_appeal_3) { create(:legacy_appeal, vacols_case: create(:case, :type_cavc_remand)) }
- let(:appeal_1) { create(:appeal, :advanced_on_docket_due_to_motion) }
- let(:appeal_2) { create(:appeal) }
-
- before do
- legacy_appeals = [legacy_appeal_1, legacy_appeal_2, legacy_appeal_3]
- legacy_appeals.map do |appeal|
- create(:colocated_task, assigned_to: assignee, appeal: appeal)
- create(:cached_appeal,
- appeal_id: appeal.id,
- appeal_type: LegacyAppeal.name,
- case_type: appeal.type)
- end
- appeals = [appeal_1, appeal_2]
- appeals.map do |appeal|
- create(:ama_colocated_task, assigned_to: assignee, appeal: appeal)
- create(:cached_appeal,
- appeal_id: appeal.id,
- appeal_type: Appeal.name,
- case_type: appeal.type,
- is_aod: appeal.aod)
- end
- end
-
- it "sorts by AOD status, case type, and docket number" do
- # postgres ascending sort sorts booleans [true, false] as [false, true]. We want is_aod appeals to show up first
- # so we sort descending on is_aod
- expected_order = CachedAppeal.order(is_aod: :desc, case_type: :asc, docket_number: :asc)
- expect(subject.map(&:appeal_id)).to eq(expected_order.pluck(:appeal_id))
- end
+ it_behaves_like "sort by Appeal Type column"
end
end
diff --git a/spec/models/task_sorter_spec.rb b/spec/models/task_sorter_spec.rb
index 7ead889ce5f..182c97c9e85 100644
--- a/spec/models/task_sorter_spec.rb
+++ b/spec/models/task_sorter_spec.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require_relative "tasks/task_shared_examples.rb"
+
describe TaskSorter, :all_dbs do
describe ".new" do
subject { TaskSorter.new(args) }
@@ -286,41 +288,9 @@
context "when sorting by Appeal Type column" do
let(:column_name) { Constants.QUEUE_CONFIG.COLUMNS.APPEAL_TYPE.name }
- let(:tasks) { Task.where(assigned_to: org) }
-
- let(:org) { create(:organization) }
-
- before do
- Colocated.singleton.add_user(create(:user))
+ let(:tasks) { Task.where(assigned_to: assignee) }
- vacols_case_types = [:type_original, :type_post_remand, :type_cavc_remand]
- vacols_case_types.each do |case_type|
- appeal = create(:legacy_appeal, vacols_case: create(:case, case_type))
- create(:colocated_task, appeal: appeal, assigned_to: org)
- create(:cached_appeal,
- appeal_id: appeal.id,
- appeal_type: LegacyAppeal.name,
- case_type: appeal.type)
- end
-
- appeals = [create(:appeal, :advanced_on_docket_due_to_motion), create(:appeal)]
- appeals.each do |appeal|
- create(:ama_colocated_task, appeal: appeal, assigned_to: org)
- create(:cached_appeal,
- appeal_id: appeal.id,
- appeal_type: Appeal.name,
- case_type: appeal.type,
- is_aod: appeal.aod)
- end
- end
-
- it "sorts by AOD status, case type, and docket number" do
- # postgres ascending sort sorts booleans [true, false] as [false, true]. We want is_aod appeals to show up
- # first so we sort descending on is_aod
- expected_order = CachedAppeal.order(is_aod: :desc, case_type: :asc, docket_number: :asc)
- expect(expected_order.first.is_aod).to eq true
- expect(subject.map(&:appeal_id)).to eq(expected_order.pluck(:appeal_id))
- end
+ it_behaves_like "sort by Appeal Type column"
end
end
end
diff --git a/spec/models/tasks/bva_dispatch_task_spec.rb b/spec/models/tasks/bva_dispatch_task_spec.rb
index 1e44ca3fdb7..b7a4a635194 100644
--- a/spec/models/tasks/bva_dispatch_task_spec.rb
+++ b/spec/models/tasks/bva_dispatch_task_spec.rb
@@ -46,7 +46,7 @@
let(:user) { create(:user) }
let(:root_task) { create(:root_task, appeal: appeal) }
let(:appeal) { create(:appeal, stream_type: stream_type) }
- let(:stream_type) { "original" }
+ let(:stream_type) { Constants.AMA_STREAM_TYPES.original }
let(:the_case) { create(:case) }
let!(:legacy_appeal) { create(:legacy_appeal, vacols_case: the_case) }
let(:citation_number) { "A18123456" }
@@ -127,7 +127,7 @@
end
context "when de_novo appeal stream" do
- let(:stream_type) { "vacate" }
+ let(:stream_type) { Constants.AMA_STREAM_TYPES.vacate }
let!(:task) { create(:ama_judge_decision_review_task, appeal: appeal, assigned_to: judge) }
let!(:attorney_task) { create(:ama_attorney_task, parent: task, assigned_to: attorney) }
let!(:post_decision_motion) do
@@ -149,7 +149,9 @@
tasks = BvaDispatchTask.where(appeal: appeal, assigned_to: user)
expect(tasks.length).to eq(1)
- de_novo_stream = Appeal.find_by(stream_docket_number: appeal.docket_number, stream_type: "de_novo")
+ de_novo_stream = Appeal.find_by(
+ stream_docket_number: appeal.docket_number, stream_type: Constants.AMA_STREAM_TYPES.de_novo
+ )
expect(de_novo_stream).to_not be_nil
request_issues = de_novo_stream.request_issues
diff --git a/spec/models/tasks/cavc_task_spec.rb b/spec/models/tasks/cavc_task_spec.rb
index 4fbd8e3bed2..bb585447371 100644
--- a/spec/models/tasks/cavc_task_spec.rb
+++ b/spec/models/tasks/cavc_task_spec.rb
@@ -1,42 +1,21 @@
# frozen_string_literal: true
describe CavcTask, :postgres do
+ require_relative "task_shared_examples.rb"
+
describe ".create" do
subject { described_class.create(appeal: appeal, parent: parent_task) }
let(:appeal) { create(:appeal) }
let!(:parent_task) { create(:distribution_task, appeal: appeal) }
+ let(:parent_task_class) { DistributionTask }
- context "parent is DistributionTask" do
- it "creates task" do
- new_task = subject
- expect(new_task.valid?)
- expect(new_task.errors.messages[:parent]).to be_empty
-
- expect(appeal.tasks).to include new_task
- expect(parent_task.children).to include new_task
-
- expect(new_task.assigned_to).to eq Bva.singleton
- expect(new_task.label).to eq "All CAVC-related tasks"
- expect(new_task.default_instructions).to eq [COPY::CAVC_TASK_DEFAULT_INSTRUCTIONS]
- end
- end
+ it_behaves_like "task requiring specific parent"
- context "parent is not a DistributionTask" do
- let(:parent_task) { create(:root_task) }
- it "fails to create task" do
- new_task = subject
- expect(new_task.invalid?)
- expect(new_task.errors.messages[:parent]).to include("should be a DistributionTask")
- end
- end
-
- context "parent is nil" do
- let(:parent_task) { nil }
- it "fails to create task" do
- new_task = subject
- expect(new_task.invalid?)
- expect(new_task.errors.messages[:parent]).to include("can't be blank")
- end
+ it "has expected defaults" do
+ new_task = subject
+ expect(new_task.assigned_to).to eq Bva.singleton
+ expect(new_task.label).to eq COPY::CAVC_TASK_LABEL
+ expect(new_task.default_instructions).to eq [COPY::CAVC_TASK_DEFAULT_INSTRUCTIONS]
end
end
@@ -50,6 +29,7 @@
expect(RootTask.count).to eq 1
expect(DistributionTask.count).to eq 1
expect(CavcTask.count).to eq 1
+ expect(cavc_task.parent).to eq parent_task
end
end
context "parent task is provided" do
@@ -60,6 +40,7 @@
expect(RootTask.count).to eq 1
expect(DistributionTask.count).to eq 1
expect(CavcTask.count).to eq 1
+ expect(cavc_task.parent).to eq parent_task
end
end
context "nothing is provided" do
@@ -69,6 +50,7 @@
expect(RootTask.count).to eq 1
expect(DistributionTask.count).to eq 1
expect(CavcTask.count).to eq 1
+ expect(cavc_task.parent).to eq DistributionTask.first
end
end
end
diff --git a/spec/models/tasks/colocated_task_spec.rb b/spec/models/tasks/colocated_task_spec.rb
index 49a63db1578..38245eb6ba4 100644
--- a/spec/models/tasks/colocated_task_spec.rb
+++ b/spec/models/tasks/colocated_task_spec.rb
@@ -625,7 +625,9 @@
let(:post_decision_motion_updater) do
PostDecisionMotionUpdater.new(judge_address_motion_to_vacate_task, post_decision_motion_params)
end
- let(:vacate_stream) { Appeal.find_by(stream_docket_number: appeal.docket_number, stream_type: "vacate") }
+ let(:vacate_stream) do
+ Appeal.find_by(stream_docket_number: appeal.docket_number, stream_type: Constants.AMA_STREAM_TYPES.vacate)
+ end
let(:attorney_task) { AttorneyTask.find_by(assigned_to: attorney) }
let(:parent) { create(:ama_judge_decision_review_task, assigned_to: judge, appeal: vacate_stream ) }
diff --git a/spec/models/tasks/judge_task_spec.rb b/spec/models/tasks/judge_task_spec.rb
index a3e74bbd805..ec3b20fa871 100644
--- a/spec/models/tasks/judge_task_spec.rb
+++ b/spec/models/tasks/judge_task_spec.rb
@@ -14,7 +14,7 @@
describe ".available_actions" do
let(:user) { judge }
let(:appeal) { create(:appeal, stream_type: stream_type) }
- let(:stream_type) { "original" }
+ let(:stream_type) { Constants.AMA_STREAM_TYPES.original }
let(:subject_task) do
create(:ama_judge_assign_task, assigned_to: judge, appeal: appeal)
end
@@ -138,7 +138,7 @@
context "when it is a vacate type appeal" do
let(:judge3) { create(:user) }
let!(:user) { judge3 }
- let(:stream_type) { "vacate" }
+ let(:stream_type) { Constants.AMA_STREAM_TYPES.vacate }
let!(:task) do
create(:ama_judge_decision_review_task,
appeal: appeal,
diff --git a/spec/models/tasks/send_cavc_remand_processed_letter_task_spec.rb b/spec/models/tasks/send_cavc_remand_processed_letter_task_spec.rb
new file mode 100644
index 00000000000..55661a6fb1f
--- /dev/null
+++ b/spec/models/tasks/send_cavc_remand_processed_letter_task_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+describe SendCavcRemandProcessedLetterTask, :postgres do
+ require_relative "task_shared_examples.rb"
+
+ describe ".create" do
+ subject { described_class.create(appeal: appeal, parent: parent_task) }
+ let(:appeal) { create(:appeal) }
+ let!(:parent_task) { create(:cavc_task, appeal: appeal) }
+ let(:parent_task_class) { CavcTask }
+
+ it_behaves_like "task requiring specific parent"
+
+ it "has expected defaults" do
+ new_task = subject
+ expect(new_task.assigned_to).to eq CavcLitigationSupport.singleton
+ expect(new_task.label).to eq COPY::SEND_CAVC_REMAND_PROCESSED_LETTER_TASK_LABEL
+ expect(new_task.default_instructions).to be_empty
+ end
+
+ context "create child task assigned to user" do
+ let!(:parent_task) { create(:send_cavc_remand_processed_letter_task, appeal: appeal) }
+ it "returns non-admin actions" do
+ new_task = subject
+ expect(new_task.valid?)
+ expect(new_task.errors.messages[:parent]).to be_empty
+
+ expect(appeal.tasks).to include new_task
+ expect(parent_task.children).to include new_task
+
+ expect(new_task.label).to eq COPY::SEND_CAVC_REMAND_PROCESSED_LETTER_TASK_LABEL
+ expect(new_task.default_instructions).to be_empty
+ end
+ end
+ end
+
+ describe "FactoryBot.create(:send_cavc_remand_processed_letter_task) with different arguments" do
+ context "appeal is provided" do
+ let(:appeal) { create(:appeal) }
+ let!(:cavc_task) { create(:cavc_task, appeal: appeal) }
+ let!(:send_task) { create(:send_cavc_remand_processed_letter_task, appeal: appeal) }
+ it "finds existing parent_task to use as parent" do
+ expect(Appeal.count).to eq 1
+ expect(RootTask.count).to eq 1
+ expect(DistributionTask.count).to eq 1
+ expect(CavcTask.count).to eq 1
+ expect(SendCavcRemandProcessedLetterTask.count).to eq 1
+ expect(send_task.parent).to eq cavc_task
+ end
+ end
+ context "parent task is provided" do
+ let!(:parent_task) { create(:cavc_task) }
+ let!(:send_task) { create(:send_cavc_remand_processed_letter_task, parent: parent_task) }
+ it "uses existing parent_task" do
+ expect(Appeal.count).to eq 1
+ expect(RootTask.count).to eq 1
+ expect(DistributionTask.count).to eq 1
+ expect(CavcTask.count).to eq 1
+ expect(SendCavcRemandProcessedLetterTask.count).to eq 1
+ expect(send_task.parent).to eq parent_task
+ end
+ end
+ context "nothing is provided" do
+ let!(:send_task) { create(:send_cavc_remand_processed_letter_task) }
+ it "creates realistic task tree" do
+ expect(Appeal.count).to eq 1
+ expect(RootTask.count).to eq 1
+ expect(DistributionTask.count).to eq 1
+ expect(CavcTask.count).to eq 1
+ expect(SendCavcRemandProcessedLetterTask.count).to eq 1
+ expect(send_task.parent).to eq CavcTask.first
+ end
+ end
+ end
+
+ SendCRPLetterTask = SendCavcRemandProcessedLetterTask
+ describe "#available_actions" do
+ let(:org_admin) do
+ create(:user) do |u|
+ OrganizationsUser.make_user_admin(u, CavcLitigationSupport.singleton)
+ end
+ end
+ let(:org_nonadmin) { create(:user) { |u| CavcLitigationSupport.singleton.add_user(u) } }
+ let(:other_user) { create(:user) }
+ let(:send_task) { create(:send_cavc_remand_processed_letter_task) }
+ context "task assigned to CavcLitigationSupport admin" do
+ it "returns admin actions" do
+ expect(send_task.available_actions(org_admin)).to match_array SendCRPLetterTask::ADMIN_ACTIONS
+ expect(send_task.available_actions(other_user)).to be_empty
+ end
+ end
+ context "task assigned to CavcLitigationSupport non-admin" do
+ let(:child_task) { create(:send_cavc_remand_processed_letter_task, parent: send_task, assigned_to: org_nonadmin) }
+ it "returns non-admin actions" do
+ expect(child_task.available_actions(org_nonadmin)).to match_array SendCRPLetterTask::USER_ACTIONS
+ expect(send_task.available_actions(other_user)).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/models/tasks/task_shared_examples.rb b/spec/models/tasks/task_shared_examples.rb
new file mode 100644
index 00000000000..ec361598db6
--- /dev/null
+++ b/spec/models/tasks/task_shared_examples.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+shared_examples_for "task requiring specific parent" do
+ context "parent is the expected type" do
+ it "creates task" do
+ new_task = subject
+ expect(new_task.valid?)
+ expect(new_task.errors.messages[:parent]).to be_empty
+
+ expect(appeal.tasks).to include new_task
+ expect(parent_task.children).to include new_task
+ end
+ end
+
+ context "parent task is not the expected type" do
+ let(:parent_task) { create(:root_task) }
+ it "fails to create task" do
+ new_task = subject
+ expect(new_task.invalid?)
+ expect(new_task.errors.messages[:parent]).to include(/should be .*/)
+ end
+ end
+
+ context "parent is nil" do
+ let(:parent_task) { nil }
+ it "fails to create task" do
+ new_task = subject
+ expect(new_task.invalid?)
+ expect(new_task.errors.messages[:parent]).to include("can't be blank")
+ end
+ end
+end
+
+shared_examples_for "sort by Appeal Type column" do
+ let(:assignee) { create(:organization) }
+
+ before do
+ Colocated.singleton.add_user(create(:user))
+
+ vacols_case_types = [:type_original, :type_post_remand, :type_cavc_remand]
+ vacols_case_types.each_with_index do |case_type, index|
+ appeal = create(:legacy_appeal, vacols_case: create(:case, case_type))
+ create(:colocated_task, appeal: appeal, assigned_to: assignee)
+ create(:cached_appeal,
+ appeal_id: appeal.id,
+ docket_number: index,
+ appeal_type: LegacyAppeal.name,
+ case_type: appeal.type)
+ end
+
+ appeals = [
+ create(:appeal, :advanced_on_docket_due_to_motion, :type_cavc_remand),
+ create(:appeal, :advanced_on_docket_due_to_motion),
+ create(:appeal, :type_cavc_remand),
+ create(:appeal)
+ ]
+ appeals.each_with_index do |appeal, index|
+ create(:ama_colocated_task, appeal: appeal, assigned_to: assignee)
+ create(:cached_appeal,
+ appeal_id: appeal.id,
+ docket_number: index + vacols_case_types.count,
+ appeal_type: Appeal.name,
+ case_type: appeal.type,
+ is_aod: appeal.aod)
+ end
+ end
+
+ it "sorts by AOD status, case type, and docket number" do
+ # postgres ascending sort sorts booleans [true, false] as [false, true]. We want is_aod appeals to show up
+ # first so we sort descending on is_aod
+ expected_order = CachedAppeal.order(
+ "is_aod desc, CASE WHEN case_type = 'Court Remand' THEN 0 ELSE 1 END, docket_number asc"
+ )
+ expect(expected_order.first.is_aod).to eq true
+ expect(expected_order.first.case_type).to eq Constants.AMA_STREAM_TYPES.court_remand.titlecase
+ expect(subject.map { |task| [task.appeal_id, task.appeal_type] }).to eq(
+ expected_order.pluck(:appeal_id, :appeal_type)
+ )
+ end
+end
diff --git a/spec/queries/appeals_with_no_tasks_or_all_tasks_on_hold_query_spec.rb b/spec/queries/appeals_with_no_tasks_or_all_tasks_on_hold_query_spec.rb
index 5a4dd69972c..2c1a051b7be 100644
--- a/spec/queries/appeals_with_no_tasks_or_all_tasks_on_hold_query_spec.rb
+++ b/spec/queries/appeals_with_no_tasks_or_all_tasks_on_hold_query_spec.rb
@@ -18,6 +18,20 @@
schedule_hearing_task.completed!
appeal
end
+ let!(:appeal_with_fully_on_hold_subtree) do
+ appeal = create(:appeal, :with_post_intake_tasks)
+ task = create(:privacy_act_task, appeal: appeal, parent: appeal.root_task)
+ task.descendants.each(&:on_hold!)
+ appeal
+ end
+ let!(:appeal_with_failed_reactivated_task) do
+ appeal = create(:appeal, :with_post_intake_tasks)
+ task1 = create(:privacy_act_task, appeal: appeal, parent: appeal.root_task)
+ task1.descendants.each(&:on_hold!)
+ task2 = create(:privacy_act_task, appeal: appeal, parent: task1)
+ task2.descendants.each(&:completed!)
+ appeal
+ end
let!(:appeal_with_decision_documents) do
appeal = create(:appeal, :with_post_intake_tasks)
create(:decision_document, appeal: appeal)
@@ -37,6 +51,8 @@
appeal_with_zero_tasks,
appeal_with_one_task,
appeal_with_all_tasks_on_hold,
+ appeal_with_fully_on_hold_subtree,
+ appeal_with_failed_reactivated_task,
appeal_with_two_tasks_not_distribution,
dispatched_appeal_on_hold
]
@@ -68,6 +84,18 @@
it { is_expected.to eq(true) }
end
+ context "appeal_with_fully_on_hold_subtree" do
+ let(:appeal) { appeal_with_fully_on_hold_subtree }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context "appeal_with_failed_reactivated_task" do
+ let(:appeal) { appeal_with_failed_reactivated_task }
+
+ it { is_expected.to eq(true) }
+ end
+
context "appeal_with_decision_documents" do
let(:appeal) { appeal_with_decision_documents }
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 7ad91990566..5375efd6012 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -8,6 +8,23 @@
require "fake_date_helper"
require "react_on_rails"
require "timeout"
+require "knapsack_pro"
+
+TMP_RSPEC_XML_REPORT = "tmp/rspec.xml"
+FINAL_RSPEC_XML_REPORT = "#{Dir.home}/test-results/rspec/rspec.xml"
+
+KnapsackPro::Adapters::RSpecAdapter.bind
+KnapsackPro::Hooks::Queue.after_subset_queue do |_queue_id, _subset_queue_id|
+ if File.exist?(TMP_RSPEC_XML_REPORT)
+ FileUtils.mv(TMP_RSPEC_XML_REPORT, FINAL_RSPEC_XML_REPORT)
+ end
+end
+
+KnapsackPro::Hooks::Queue.after_queue do |_queue_id|
+ if File.exist?(FINAL_RSPEC_XML_REPORT) && ENV["CIRCLE_TEST_REPORTS"]
+ FileUtils.cp(FINAL_RSPEC_XML_REPORT, "#{ENV['CIRCLE_TEST_REPORTS']}/rspec.xml")
+ end
+end
# Add additional requires below this line. Rails is not loaded until this point!
diff --git a/spec/services/hearing_schedule/generate_hearing_days_schedule_spec.rb b/spec/services/hearing_schedule/generate_hearing_days_schedule_spec.rb
index f1e87b6994c..b2546d113f6 100644
--- a/spec/services/hearing_schedule/generate_hearing_days_schedule_spec.rb
+++ b/spec/services/hearing_schedule/generate_hearing_days_schedule_spec.rb
@@ -265,5 +265,15 @@
it { expect { subject }.to raise_error(HearingSchedule::GenerateHearingDaysSchedule::NoDaysAvailableForRO) }
end
+
+ context "allocated days to Central Office" do
+ subject { generate_hearing_days_schedule.generate_co_hearing_days_schedule }
+
+ it "only allocates 1 docket per week" do
+ # 26 wednesdays between 2018-04-01 and 2018-09-30; since Wed July 4 is a holiday,
+ # it picks another day
+ expect(subject.count).to eq(26)
+ end
+ end
end
end
diff --git a/spec/services/slack_service_spec.rb b/spec/services/slack_service_spec.rb
index 15fc946466b..bdcc6af10ad 100644
--- a/spec/services/slack_service_spec.rb
+++ b/spec/services/slack_service_spec.rb
@@ -38,6 +38,13 @@
end
end
+ context "message contains error but title contains warning" do
+ it "picks yellow color" do
+ slack_service.send_notification("there was an error", "Really just a warning")
+ expect(@http_params[:body]).to match(/"#ffff00"/)
+ end
+ end
+
context "message contains error" do
it "picks red color" do
slack_service.send_notification("there was an error")
diff --git a/spec/services/stuck_appeals_checker_spec.rb b/spec/services/stuck_appeals_checker_spec.rb
index 3ed38de146e..9cb144a5952 100644
--- a/spec/services/stuck_appeals_checker_spec.rb
+++ b/spec/services/stuck_appeals_checker_spec.rb
@@ -20,6 +20,12 @@
create(:bva_dispatch_task, :completed, appeal: appeal)
appeal
end
+ let!(:appeal_with_fully_on_hold_subtree) do
+ appeal = create(:appeal, :with_post_intake_tasks)
+ task = create(:privacy_act_task, appeal: appeal, parent: appeal.root_task)
+ task.descendants.each(&:on_hold!)
+ appeal
+ end
let!(:appeal_with_closed_root_open_child) do
appeal = create(:appeal, :with_post_intake_tasks)
appeal.root_task.completed!
@@ -27,11 +33,11 @@
end
describe "#call" do
- it "reports 3 appeals stuck" do
+ it "reports 5 appeals stuck" do
subject.call
expect(subject.report?).to eq(true)
- expect(subject.report).to match(/AppealsWithNoTasksOrAllTasksOnHoldQuery: 3/)
+ expect(subject.report).to match(/AppealsWithNoTasksOrAllTasksOnHoldQuery: 4/)
expect(subject.report).to match(/AppealsWithClosedRootTaskOpenChildrenQuery: 1/)
end
end
diff --git a/spec/workflows/bulk_task_assignment_spec.rb b/spec/workflows/bulk_task_assignment_spec.rb
index 323452acf20..3b38ba2b7d0 100644
--- a/spec/workflows/bulk_task_assignment_spec.rb
+++ b/spec/workflows/bulk_task_assignment_spec.rb
@@ -135,36 +135,64 @@
end
end
- def create_no_show_hearing_task_for_appeal(appeal, creation_time = 1.day.ago)
- create(:no_show_hearing_task, appeal: appeal, assigned_to: organization, created_at: creation_time)
+ def create_no_show_hearing_task_for_appeal(appeal)
+ create(:no_show_hearing_task, appeal: appeal, assigned_to: organization)
end
context "when there are priority appeals" do
let(:regional_office) { nil }
let(:task_count) { 20 }
+ let(:ama_receipt_date) { 4.days.ago }
+ let(:legacy_docket_number) { 3.days.ago.strftime("%y%m%d") }
- let(:aod_appeal) { create(:appeal, :advanced_on_docket_due_to_motion) }
- # Since cavc is not currently supported, ignore CAVC AMA appeals for now; use CAVC legacy appeals for now
- let(:aod_legacy_appeal) { create(:legacy_appeal, vacols_case: create(:case, :aod)) }
- let(:cavc_legacy_appeal) { create(:legacy_appeal, vacols_case: create(:case, :type_cavc_remand)) }
- let(:cavc_aod_legacy_appeal) { create(:legacy_appeal, vacols_case: create(:case, :aod, :type_cavc_remand)) }
+ let(:cavc_appeal) { create(:appeal, :type_cavc_remand, receipt_date: ama_receipt_date) }
+ let(:aod_appeal) { create(:appeal, :advanced_on_docket_due_to_motion, receipt_date: ama_receipt_date) }
+ let(:cavc_aod_appeal) do
+ create(:appeal, :advanced_on_docket_due_to_motion, :type_cavc_remand, receipt_date: ama_receipt_date)
+ end
+ let(:aod_legacy_appeal) do
+ create(:legacy_appeal, vacols_case: create(:case, :aod, folder: build(:folder, tinum: legacy_docket_number)))
+ end
+ let(:cavc_legacy_appeal) do
+ create(
+ :legacy_appeal,
+ vacols_case: create(:case, :type_cavc_remand, folder: build(:folder, tinum: legacy_docket_number))
+ )
+ end
+ let(:cavc_aod_legacy_appeal) do
+ create(
+ :legacy_appeal,
+ vacols_case: create(:case, :aod, :type_cavc_remand, folder: build(:folder, tinum: legacy_docket_number))
+ )
+ end
before do
- create_no_show_hearing_task_for_appeal(aod_appeal, 3.days.ago)
- create_no_show_hearing_task_for_appeal(aod_legacy_appeal, 2.days.ago)
+ create_no_show_hearing_task_for_appeal(aod_appeal)
+ create_no_show_hearing_task_for_appeal(cavc_appeal)
+ create_no_show_hearing_task_for_appeal(cavc_aod_appeal)
+ create_no_show_hearing_task_for_appeal(aod_legacy_appeal)
create_no_show_hearing_task_for_appeal(cavc_legacy_appeal)
create_no_show_hearing_task_for_appeal(cavc_aod_legacy_appeal)
UpdateCachedAppealsAttributesJob.perform_now
end
- let(:expected_appeal_ordering) { [cavc_aod_legacy_appeal, aod_appeal, aod_legacy_appeal, cavc_legacy_appeal] }
+ let(:expected_appeal_ordering) do
+ [
+ cavc_aod_appeal,
+ cavc_aod_legacy_appeal,
+ aod_appeal,
+ aod_legacy_appeal,
+ cavc_appeal,
+ cavc_legacy_appeal
+ ]
+ end
subject { BulkTaskAssignment.new(params).process }
it "sorts priority appeals first" do
prioritized_assigned_tasks = subject
- expect(prioritized_assigned_tasks.first(4).map(&:appeal)).to eq(expected_appeal_ordering)
+ expect(prioritized_assigned_tasks.first(6).map(&:appeal)).to eq(expected_appeal_ordering)
appeals_of_returned_tasks = prioritized_assigned_tasks.map(&:appeal)
expect(appeals_of_returned_tasks).to include(no_show_hearing_task1.appeal)
diff --git a/spec/workflows/initial_tasks_factory_spec.rb b/spec/workflows/initial_tasks_factory_spec.rb
index 9afdc950ca2..82417fea841 100644
--- a/spec/workflows/initial_tasks_factory_spec.rb
+++ b/spec/workflows/initial_tasks_factory_spec.rb
@@ -214,7 +214,7 @@
let(:appeal) do
create(:appeal,
- stream_type: "court_remand",
+ stream_type: Constants.AMA_STREAM_TYPES.court_remand,
docket_type: Constants.AMA_DOCKETS.direct_review,
claimants: [
create(:claimant, participant_id: participant_id_with_pva),
@@ -226,6 +226,8 @@
subject
expect(DistributionTask.find_by(appeal: appeal).status).to eq("on_hold")
expect(CavcTask.find_by(appeal: appeal).parent.class.name).to eq("DistributionTask")
+ expect(CavcTask.find_by(appeal: appeal).status).to eq("on_hold")
+ expect(SendCavcRemandProcessedLetterTask.find_by(appeal: appeal).status).to eq("assigned")
expect(appeal.tasks.count { |t| t.is_a?(TrackVeteranTask) }).to eq(1)
end
end
diff --git a/spec/workflows/post_decision_motion_updater_spec.rb b/spec/workflows/post_decision_motion_updater_spec.rb
index 2f41b2f9e85..3032c289b93 100644
--- a/spec/workflows/post_decision_motion_updater_spec.rb
+++ b/spec/workflows/post_decision_motion_updater_spec.rb
@@ -227,7 +227,7 @@
end
def vacate_stream
- Appeal.find_by(stream_docket_number: appeal.docket_number, stream_type: "vacate")
+ Appeal.find_by(stream_docket_number: appeal.docket_number, stream_type: Constants.AMA_STREAM_TYPES.vacate)
end
def verify_vacate_stream
diff --git a/spec/workflows/tasks_for_appeal_spec.rb b/spec/workflows/tasks_for_appeal_spec.rb
index e8e32eedccc..ebfca79770c 100644
--- a/spec/workflows/tasks_for_appeal_spec.rb
+++ b/spec/workflows/tasks_for_appeal_spec.rb
@@ -5,7 +5,14 @@
context "for a legacy appeal with a travel board hearing request" do
let(:user_roles) { ["Build HearSched"] }
let!(:user) { create(:user, roles: user_roles) }
- let(:vacols_case) { create(:case, :travel_board_hearing) }
+ let(:appeal_type) { "1" } # Original
+ let(:vacols_case) do
+ create(
+ :case,
+ :travel_board_hearing,
+ bfac: appeal_type
+ )
+ end
let!(:appeal) { create(:legacy_appeal, vacols_case: vacols_case) }
before { FeatureToggle.enable!(:convert_travel_board_to_video_or_virtual) }
@@ -78,6 +85,36 @@
subject
end
end
+
+ VACOLS::Case::TYPES.drop(1).each do |code, readable|
+ context "appeal is #{readable}" do
+ let(:appeal_type) { code }
+
+ it "doesn't call the hearing task tree initializer" do
+ expect(HearingTaskTreeInitializer).to_not receive(:for_appeal_with_pending_travel_board_hearing)
+
+ subject
+ end
+ end
+ end
+
+ context "the appeal has a hearing" do
+ let!(:hearing) { create(:legacy_hearing, appeal: appeal, disposition: disposition) }
+
+ before do
+ hearing.vacols_record.update!(folder_nr: vacols_case.bfkey)
+ end
+
+ context "hearing was held" do
+ let(:disposition) { "H" }
+
+ it "doesn't call the hearing task tree intitializer" do
+ expect(HearingTaskTreeInitializer).to_not receive(:for_appeal_with_pending_travel_board_hearing)
+
+ subject
+ end
+ end
+ end
end
end
end