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 needed, you may correct the issues.
+
If needed, you may correct the issues.
]; 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 needed, you may correct the issues.
+
If needed, you may correct the issues.
); 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 + + + +
+
+ + This case has an IHP Request associated with it. + +
    +
  • + + IHP Requested: + + + + 06/16/20 + +
  • +
  • + + IHP Received: + + + + 06/26/20 + + + (10 days) +
  • +
  • + + On hold for IHP: + + + 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