diff --git a/.codeclimate.yml b/.codeclimate.yml index 4f885118872..998bef4649b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -122,3 +122,4 @@ exclude_patterns: - 'tmp/**/*' - 'app/assets/**/*' - 'client/test/data/camoQueueConfigData.js' + - 'client/app/intake/components/mockData/issueListProps.js' diff --git a/app/controllers/appeals_controller.rb b/app/controllers/appeals_controller.rb index f660eeeeb80..5fdbafc08bf 100644 --- a/app/controllers/appeals_controller.rb +++ b/app/controllers/appeals_controller.rb @@ -238,7 +238,7 @@ def review_removed_message end def review_withdrawn_message - "You have successfully withdrawn a review." + COPY::CLAIM_REVIEW_WITHDRAWN_MESSAGE end def withdrawn_issues diff --git a/app/controllers/claim_review_controller.rb b/app/controllers/claim_review_controller.rb index c2755e1ad6a..d4dc84d5a56 100644 --- a/app/controllers/claim_review_controller.rb +++ b/app/controllers/claim_review_controller.rb @@ -94,7 +94,7 @@ def render_success if claim_review.processed_in_caseflow? set_flash_success_message - render json: { redirect_to: claim_review.business_line.tasks_url, + render json: { redirect_to: claim_review.redirect_url, beforeIssues: request_issues_update.before_issues.map(&:serialize), afterIssues: request_issues_update.after_issues.map(&:serialize), withdrawnIssues: request_issues_update.withdrawn_issues.map(&:serialize) } @@ -136,24 +136,56 @@ def review_edited_message "You have successfully " + [added_issues, removed_issues, withdrawn_issues].compact.to_sentence + "." end + def vha_edited_decision_date_message + COPY::VHA_ADD_DECISION_DATE_TO_ISSUE_SUCCESS_MESSAGE + end + + def vha_established_message + "You have successfully established #{claimant_name}'s #{claim_review.class.review_title}" + end + + def claimant_name + if claim_review.veteran_is_not_claimant + claim_review.claimant.try(:name) + else + claim_review.veteran_full_name + end + end + + def vha_flash_message + issues_without_decision_date = (request_issues_update.after_issues - + request_issues_update.edited_issues - + request_issues_update.removed_or_withdrawn_issues) + .select { |issue| issue.decision_date.blank? && !issue.withdrawn? } + + if issues_without_decision_date.empty? + vha_established_message + elsif request_issues_update.edited_issues.any? + vha_edited_decision_date_message + else + review_edited_message + end + end + def set_flash_success_message flash[:edited] = if request_issues_update.after_issues.empty? decisions_removed_message elsif (request_issues_update.after_issues - request_issues_update.withdrawn_issues).empty? review_withdrawn_message + elsif claim_review.benefit_type == "vha" + vha_flash_message else review_edited_message end end def decisions_removed_message - claimant_name = claim_review.veteran_full_name "You have successfully removed #{claim_review.class.review_title} for #{claimant_name} (ID: #{claim_review.veteran.ssn})." end def review_withdrawn_message - "You have successfully withdrawn a review." + COPY::CLAIM_REVIEW_WITHDRAWN_MESSAGE end def claim_label_edit_params diff --git a/app/controllers/decision_reviews_controller.rb b/app/controllers/decision_reviews_controller.rb index 2c338eb48a0..687c6066c52 100644 --- a/app/controllers/decision_reviews_controller.rb +++ b/app/controllers/decision_reviews_controller.rb @@ -7,12 +7,16 @@ class DecisionReviewsController < ApplicationController before_action :verify_access, :react_routed, :set_application before_action :verify_veteran_record_access, only: [:show] - delegate :in_progress_tasks, + delegate :incomplete_tasks, + :incomplete_tasks_type_counts, + :incomplete_tasks_issue_type_counts, + :in_progress_tasks, :in_progress_tasks_type_counts, :in_progress_tasks_issue_type_counts, :completed_tasks, :completed_tasks_type_counts, :completed_tasks_issue_type_counts, + :included_tabs, to: :business_line SORT_COLUMN_MAPPINGS = { @@ -82,11 +86,28 @@ def business_line end def task_filter_details + task_filter_hash = {} + included_tabs.each do |tab_name| + case tab_name + when :incomplete + task_filter_hash[:incomplete] = incomplete_tasks_type_counts + task_filter_hash[:incomplete_issue_types] = incomplete_tasks_issue_type_counts + when :in_progress + task_filter_hash[:in_progress] = in_progress_tasks_type_counts + task_filter_hash[:in_progress_issue_types] = in_progress_tasks_issue_type_counts + when :completed + task_filter_hash[:completed] = completed_tasks_type_counts + task_filter_hash[:completed_issue_types] = completed_tasks_issue_type_counts + else + fail NotImplementedError "Tab name type not implemented for this business line: #{business_line}" + end + end + task_filter_hash + end + + def business_line_config_options { - in_progress: in_progress_tasks_type_counts, - completed: completed_tasks_type_counts, - in_progress_issue_types: in_progress_tasks_issue_type_counts, - completed_issue_types: completed_tasks_issue_type_counts + tabs: included_tabs } end @@ -101,7 +122,7 @@ def update_power_of_attorney render_error(error) end - helper_method :task_filter_details, :business_line, :task + helper_method :task_filter_details, :business_line, :task, :business_line_config_options private @@ -122,13 +143,14 @@ def decision_issue_params def queue_tasks tab_name = allowed_params[Constants.QUEUE_CONFIG.TAB_NAME_REQUEST_PARAM.to_sym] - return missing_tab_parameter_error unless tab_name - sort_by_column = SORT_COLUMN_MAPPINGS[allowed_params[Constants.QUEUE_CONFIG.SORT_COLUMN_REQUEST_PARAM.to_sym]] tasks = case tab_name + when "incomplete" then incomplete_tasks(pagination_query_params(sort_by_column)) when "in_progress" then in_progress_tasks(pagination_query_params(sort_by_column)) when "completed" then completed_tasks(pagination_query_params(sort_by_column)) + when nil + return missing_tab_parameter_error else return unrecognized_tab_name_error end diff --git a/app/controllers/intakes_controller.rb b/app/controllers/intakes_controller.rb index dc0ae020f41..f95fb63317c 100644 --- a/app/controllers/intakes_controller.rb +++ b/app/controllers/intakes_controller.rb @@ -56,9 +56,10 @@ def review def complete intake.complete!(params) + if !detail.is_a?(Appeal) && detail.try(:processed_in_caseflow?) - flash[:success] = success_message - render json: { serverIntake: { redirect_to: detail.business_line.tasks_url } } + flash[:success] = (detail.benefit_type == "vha") ? vha_success_message : success_message + render json: { serverIntake: { redirect_to: detail.try(:redirect_url) || business_line.tasks_url } } else render json: intake.ui_hash end @@ -193,9 +194,23 @@ def detail @detail ||= intake&.detail end + def claimant_name + if detail.veteran_is_not_claimant + detail.claimant.try(:name) + else + detail.veteran_full_name + end + end + def success_message - claimant_name = detail.veteran_full_name - claimant_name = detail.claimant.try(:name) if detail.veteran_is_not_claimant "#{claimant_name} (Veteran SSN: #{detail.veteran.ssn}) #{detail.class.review_title} has been processed." end + + def vha_success_message + if detail.request_issues_without_decision_dates? + "You have successfully saved #{claimant_name}'s #{detail.class.review_title}" + else + "You have successfully established #{claimant_name}'s #{detail.class.review_title}" + end + end end diff --git a/app/jobs/decision_review_process_job.rb b/app/jobs/decision_review_process_job.rb index 0e9ad7bfd07..fc9bae7c789 100644 --- a/app/jobs/decision_review_process_job.rb +++ b/app/jobs/decision_review_process_job.rb @@ -7,9 +7,6 @@ class DecisionReviewProcessJob < CaseflowJob application_attr :intake def perform(thing_to_establish) - # Temporarily stop establishing claims due to VBMS bug - return if FeatureToggle.enabled?(:disable_claim_establishment, user: RequestStore.store[:current_user]) - @decision_review = thing_to_establish # If establishment is for a RequestIssuesUpdate, use the user on the update diff --git a/app/jobs/populate_end_product_sync_queue_job.rb b/app/jobs/populate_end_product_sync_queue_job.rb index 079c1410ec2..13d537fab01 100644 --- a/app/jobs/populate_end_product_sync_queue_job.rb +++ b/app/jobs/populate_end_product_sync_queue_job.rb @@ -43,27 +43,40 @@ def perform attr_accessor :job_expected_end_time, :should_stop_job + # rubocop:disable Metrics/MethodLength def find_priority_end_product_establishments_to_sync - get_batch = <<-SQL - select id - from end_product_establishments - inner join vbms_ext_claim - on end_product_establishments.reference_id = vbms_ext_claim."CLAIM_ID"::varchar - where (end_product_establishments.synced_status <> vbms_ext_claim."LEVEL_STATUS_CODE" or end_product_establishments.synced_status is null) - and vbms_ext_claim."LEVEL_STATUS_CODE" in ('CLR','CAN') - and end_product_establishments.id not in (select end_product_establishment_id from priority_end_product_sync_queue) - limit #{BATCH_LIMIT}; + get_sql = <<-SQL + WITH priority_eps AS ( + SELECT vec."CLAIM_ID"::varchar, vec."LEVEL_STATUS_CODE" + FROM vbms_ext_claim vec + WHERE vec."LEVEL_STATUS_CODE" in ('CLR', 'CAN') + AND (vec."EP_CODE" LIKE '04%' OR vec."EP_CODE" LIKE '03%' OR vec."EP_CODE" LIKE '93%' OR vec."EP_CODE" LIKE '68%') + ), + priority_queued_epe_ids AS ( + SELECT end_product_establishment_id + FROM priority_end_product_sync_queue) + SELECT id + FROM end_product_establishments epe + INNER JOIN priority_eps + ON epe.reference_id = priority_eps."CLAIM_ID" + WHERE (epe.synced_status is null or epe.synced_status <> priority_eps."LEVEL_STATUS_CODE") + AND NOT EXISTS (SELECT end_product_establishment_id + FROM priority_queued_epe_ids + WHERE priority_queued_epe_ids.end_product_establishment_id = epe.id) + LIMIT #{BATCH_LIMIT}; SQL - ActiveRecord::Base.connection.exec_query(ActiveRecord::Base.sanitize_sql(get_batch)).rows.flatten + ActiveRecord::Base.connection.exec_query(ActiveRecord::Base.sanitize_sql(get_sql)).rows.flatten end + # rubocop:enable Metrics/MethodLength def insert_into_priority_sync_queue(batch) - batch.each do |ep_id| - PriorityEndProductSyncQueue.create!( - end_product_establishment_id: ep_id - ) + priority_end_product_sync_queue_records = batch.map do |ep_id| + PriorityEndProductSyncQueue.new(end_product_establishment_id: ep_id) end + + # Bulk insert PriorityEndProductSyncQueue records in a single SQL statement + PriorityEndProductSyncQueue.import(priority_end_product_sync_queue_records) Rails.logger.info("PopulateEndProductSyncQueueJob EPEs processed: #{batch} - Time: #{Time.zone.now}") end diff --git a/app/models/batch_processes/priority_ep_sync_batch_process.rb b/app/models/batch_processes/priority_ep_sync_batch_process.rb index 70fff6a681f..3b5b41196d4 100644 --- a/app/models/batch_processes/priority_ep_sync_batch_process.rb +++ b/app/models/batch_processes/priority_ep_sync_batch_process.rb @@ -67,6 +67,7 @@ def process_batch! end batch_complete! + destroy_synced_records_from_queue! end # rubocop:enable Metrics/MethodLength @@ -82,4 +83,16 @@ def assign_batch_to_queued_records!(records) last_batched_at: Time.zone.now) end end + + private + + # Purpose: Destroys "SYNCED" PEPSQ records to limit the growing number of table records. + # This functionality is needed for the PopulateEndProductSyncQueueJob query to be performant. + # + # Params: None + # + # Response: Log message stating newly destroyed PEPSQ records + def destroy_synced_records_from_queue! + PriorityEndProductSyncQueue.destroy_batch_process_pepsq_records!(self) + end end diff --git a/app/models/claim_review.rb b/app/models/claim_review.rb index b86e6143c7a..b56e7cd4a56 100644 --- a/app/models/claim_review.rb +++ b/app/models/claim_review.rb @@ -98,8 +98,37 @@ def add_user_to_business_line! business_line.add_user(RequestStore.store[:current_user]) end + def handle_issues_with_no_decision_date! + # Guard clause to only perform this update for VHA claim reviews for now + return nil if benefit_type != "vha" + + if request_issues_without_decision_dates? + review_task = tasks.find { |task| task.is_a?(DecisionReviewTask) } + review_task&.on_hold! + elsif !request_issues_without_decision_dates? + review_task = tasks.find { |task| task.is_a?(DecisionReviewTask) } + review_task&.assigned! + end + end + + def request_issues_without_decision_dates? + request_issues.active.any? { |issue| issue.decision_date.blank? } + end + def create_business_line_tasks! create_decision_review_task! if processed_in_caseflow? + + tasks.reload + + handle_issues_with_no_decision_date! + end + + def redirect_url + if benefit_type == "vha" && request_issues_without_decision_dates? + "#{business_line.tasks_url}?tab=incomplete" + else + business_line.tasks_url + end end # Idempotent method to create all the artifacts for this claim. diff --git a/app/models/concerns/has_business_line.rb b/app/models/concerns/has_business_line.rb index 7679cc047df..cd179323fe7 100644 --- a/app/models/concerns/has_business_line.rb +++ b/app/models/concerns/has_business_line.rb @@ -5,7 +5,11 @@ module HasBusinessLine def business_line business_line_name = Constants::BENEFIT_TYPES[benefit_type] - @business_line ||= BusinessLine.find_or_create_by(name: business_line_name) { |org| org.url = benefit_type } + @business_line ||= if benefit_type == "vha" + VhaBusinessLine.singleton + else + BusinessLine.find_or_create_by(name: business_line_name) { |org| org.url = benefit_type } + end end def processed_in_vbms? diff --git a/app/models/membership_request.rb b/app/models/membership_request.rb index eacafc14aa2..7f0b91f7de8 100644 --- a/app/models/membership_request.rb +++ b/app/models/membership_request.rb @@ -75,9 +75,9 @@ def requesting_vha_predocket_access? def check_request_for_automatic_addition_to_vha_businessline(deciding_user) if requesting_vha_predocket_access? - vha_business_line = BusinessLine.find_by(url: "vha") + vha_business_line = VhaBusinessLine.singleton - # If the requestor also has an outstanding membership request to the vha_businessline approve it + # If the requestor also has an outstanding membership request to the vha_business_line approve it # Also send an approval email vha_business_line_request = requestor.membership_requests.assigned.find_by(organization: vha_business_line) vha_business_line_request&.update_status_and_send_email("approved", deciding_user, "VHA") diff --git a/app/models/organizations/business_line.rb b/app/models/organizations/business_line.rb index 78c5020fdb6..cc3a4f66ff0 100644 --- a/app/models/organizations/business_line.rb +++ b/app/models/organizations/business_line.rb @@ -5,11 +5,30 @@ def tasks_url "/decision_reviews/#{url}" end + def included_tabs + [:in_progress, :completed] + end + + def tasks_query_type + { + in_progress: "open", + completed: "recently_completed" + } + end + # Example Params: # sort_order: 'desc', # sort_by: 'assigned_at', # filters: [], # search_query: 'Bob' + def incomplete_tasks(pagination_params = {}) + QueryBuilder.new( + query_type: :incomplete, + query_params: pagination_params, + parent: self + ).build_query + end + def in_progress_tasks(pagination_params = {}) QueryBuilder.new( query_type: :in_progress, @@ -26,6 +45,14 @@ def completed_tasks(pagination_params = {}) ).build_query end + def incomplete_tasks_type_counts + QueryBuilder.new(query_type: :incomplete, parent: self).task_type_count + end + + def incomplete_tasks_issue_type_counts + QueryBuilder.new(query_type: :incomplete, parent: self).issue_type_count + end + def in_progress_tasks_type_counts QueryBuilder.new(query_type: :in_progress, parent: self).task_type_count end @@ -56,12 +83,10 @@ class QueryBuilder .and(Task.arel_table[:type].eq(DecisionReviewTask.name)) }.freeze - TASKS_QUERY_TYPE = { - in_progress: "open", - completed: "recently_completed" - }.freeze - DEFAULT_ORDERING_HASH = { + incomplete: { + sort_by: :assigned_at + }, in_progress: { sort_by: :assigned_at }, @@ -96,56 +121,38 @@ def task_type_count end # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize def issue_type_count - query_type_predicate = if query_type == :in_progress - "AND tasks.status IN ('assigned', 'in_progress', 'on_hold') - AND request_issues.closed_at IS NULL - AND request_issues.ineligible_reason IS NULL" - else - "AND tasks.status = 'completed' - AND #{Task.arel_table[:closed_at].between(7.days.ago..Time.zone.now).to_sql}" - end + shared_select_statement = "tasks.id as tasks_id, request_issues.nonrating_issue_category as issue_category" + appeals_query = Task.send(parent.tasks_query_type[query_type]) + .select(shared_select_statement) + .joins(ama_appeal: :request_issues) + .where(query_constraints) + hlr_query = Task.send(parent.tasks_query_type[query_type]) + .select(shared_select_statement) + .joins(supplemental_claim: :request_issues) + .where(query_constraints) + sc_query = Task.send(parent.tasks_query_type[query_type]) + .select(shared_select_statement) + .joins(higher_level_review: :request_issues) + .where(query_constraints) nonrating_issue_count = ActiveRecord::Base.connection.execute <<-SQL WITH task_review_issues AS ( - SELECT tasks.id as task_id, request_issues.nonrating_issue_category as issue_category - FROM tasks - INNER JOIN higher_level_reviews ON tasks.appeal_id = higher_level_reviews.id - AND tasks.appeal_type = 'HigherLevelReview' - INNER JOIN request_issues ON higher_level_reviews.id = request_issues.decision_review_id - AND request_issues.decision_review_type = 'HigherLevelReview' - WHERE request_issues.nonrating_issue_category IS NOT NULL - AND tasks.assigned_to_id = #{business_line_id} - AND tasks.assigned_to_type = 'Organization' - #{query_type_predicate} - UNION ALL - SELECT tasks.id as task_id, request_issues.nonrating_issue_category as issue_category - FROM tasks - INNER JOIN supplemental_claims ON tasks.appeal_id = supplemental_claims.id - AND tasks.appeal_type = 'SupplementalClaim' - INNER JOIN request_issues ON supplemental_claims.id = request_issues.decision_review_id - AND request_issues.decision_review_type = 'SupplementalClaim' - WHERE tasks.assigned_to_id = #{business_line_id} - AND tasks.assigned_to_type = 'Organization' - #{query_type_predicate} - UNION ALL - SELECT tasks.id as task_id, request_issues.nonrating_issue_category as issue_category - FROM tasks - INNER JOIN appeals ON tasks.appeal_id = appeals.id - AND tasks.appeal_type = 'Appeal' - INNER JOIN request_issues ON appeals.id = request_issues.decision_review_id - AND request_issues.decision_review_type = 'Appeal' - WHERE tasks.assigned_to_id = #{business_line_id} - AND tasks.assigned_to_type = 'Organization' - #{query_type_predicate} + #{hlr_query.to_sql} + UNION ALL + #{sc_query.to_sql} + UNION ALL + #{appeals_query.to_sql} ) - SELECT issue_category, COUNT(issue_category) AS nonrating_issue_count + SELECT issue_category, COUNT(1) AS nonrating_issue_count FROM task_review_issues GROUP BY issue_category; SQL issue_count_options = nonrating_issue_count.reduce({}) do |acc, hash| - acc.merge(hash["issue_category"] => hash["nonrating_issue_count"]) + key = hash["issue_category"] || "None" + acc.merge(key => hash["nonrating_issue_count"]) end # Merge in all of the possible issue types for businessline. Guess that the key is the snakecase url @@ -158,6 +165,7 @@ def issue_type_count issue_count_options end # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize private @@ -177,7 +185,8 @@ def union_select_statements participant_id_alias, veteran_ssn_alias, issue_types, - issue_types_lower + issue_types_lower, + appeal_unique_id_alias ] end @@ -226,6 +235,10 @@ def participant_id_alias "veterans.participant_id as veteran_participant_id" end + def appeal_unique_id_alias + "uuid as external_appeal_id" + end + # All join clauses # NOTE: .left_joins(ama_appeal: :request_issues) @@ -262,6 +275,17 @@ def bgs_attorneys_join "LEFT JOIN bgs_attorneys ON claimants.participant_id = bgs_attorneys.participant_id" end + def union_query_join_clauses + [ + veterans_join, + claimants_join, + people_join, + unrecognized_appellants_join, + party_details_join, + bgs_attorneys_join + ] + end + # These values reflect the number of searchable fields in search_all_clause for where interpolation later def number_of_search_fields FeatureToggle.enabled?(:decision_review_queue_ssn_column, user: RequestStore[:current_user]) ? 4 : 2 @@ -286,7 +310,7 @@ def search_all_clause end def group_by_columns - "tasks.id, veterans.participant_id, veterans.ssn, veterans.first_name, veterans.last_name, "\ + "tasks.id, uuid, veterans.participant_id, veterans.ssn, veterans.first_name, veterans.last_name, "\ "unrecognized_party_details.name, unrecognized_party_details.last_name, people.first_name, people.last_name, "\ "veteran_is_not_claimant, bgs_attorneys.name" end @@ -329,14 +353,9 @@ def ama_appeals_query def decision_reviews_on_request_issues(join_constraint, where_constraints = query_constraints) Task.select(union_select_statements) - .send(TASKS_QUERY_TYPE[query_type]) + .send(parent.tasks_query_type[query_type]) .joins(join_constraint) - .joins(veterans_join) - .joins(claimants_join) - .joins(people_join) - .joins(unrecognized_appellants_join) - .joins(party_details_join) - .joins(bgs_attorneys_join) + .joins(*union_query_join_clauses) .where(where_constraints) .where(search_all_clause, *search_values) .where(issue_type_filter_predicate(query_params[:filters])) @@ -358,21 +377,27 @@ def combined_decision_review_tasks_query def query_constraints { + incomplete: { + # Don't retrieve any tasks with closed issues or issues with ineligible reasons for incomplete + assigned_to: parent, + "request_issues.closed_at": nil, + "request_issues.ineligible_reason": nil + }, in_progress: { # Don't retrieve any tasks with closed issues or issues with ineligible reasons for in progress - assigned_to: business_line_id, + assigned_to: parent, "request_issues.closed_at": nil, "request_issues.ineligible_reason": nil }, completed: { - assigned_to: business_line_id + assigned_to: parent } }[query_type] end def board_grant_effectuation_task_constraints { - assigned_to: business_line_id, + assigned_to: parent, 'tasks.type': BoardGrantEffectuationTask.name } end @@ -443,9 +468,12 @@ def issue_type_filter_predicate(filters) def build_issue_type_filter_predicates(tasks_to_include) first_task_name, *remaining_task_names = tasks_to_include + first_task_name = nil if first_task_name == "None" + filter = RequestIssue.arel_table[:nonrating_issue_category].eq(first_task_name) remaining_task_names.each do |task_name| + task_name = nil if task_name == "None" filter = filter.or(RequestIssue.arel_table[:nonrating_issue_category].eq(task_name)) end @@ -455,7 +483,7 @@ def build_issue_type_filter_predicates(tasks_to_include) end def decision_review_requests_union_subquery(filter) - base_query = Task.select("tasks.id").send(TASKS_QUERY_TYPE[query_type]) + base_query = Task.select("tasks.id").send(parent.tasks_query_type[query_type]) union_query = Arel::Nodes::UnionAll.new( Arel::Nodes::UnionAll.new( base_query @@ -482,3 +510,5 @@ def locate_issue_type_filter(filters) end end end + +require_dependency "vha_business_line" diff --git a/app/models/organizations/vha_business_line.rb b/app/models/organizations/vha_business_line.rb new file mode 100644 index 00000000000..af4b9a98a2f --- /dev/null +++ b/app/models/organizations/vha_business_line.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class VhaBusinessLine < BusinessLine + def self.singleton + VhaBusinessLine.first || VhaBusinessLine.find_or_create_by(name: Constants::BENEFIT_TYPES["vha"], url: "vha") + end + + def included_tabs + [:incomplete, :in_progress, :completed] + end + + def tasks_query_type + { + incomplete: "on_hold", + in_progress: "active", + completed: "recently_completed" + } + end +end diff --git a/app/models/priority_queues/priority_end_product_sync_queue.rb b/app/models/priority_queues/priority_end_product_sync_queue.rb index d8cd0f73ba0..d8f68cd1adb 100644 --- a/app/models/priority_queues/priority_end_product_sync_queue.rb +++ b/app/models/priority_queues/priority_end_product_sync_queue.rb @@ -46,16 +46,22 @@ def status_error!(errors) # for later manual review. def declare_record_stuck! update!(status: Constants.PRIORITY_EP_SYNC.stuck) - stuck_record = CaseflowStuckRecord.create!(stuck_record: self, - error_messages: error_messages, - determined_stuck_at: Time.zone.now) - msg = "StuckRecordAlert::SyncFailed End Product Establishment ID: #{end_product_establishment_id}." - Raven.capture_message(msg, level: "error", extra: { caseflow_stuck_record_id: stuck_record.id, - batch_process_type: batch_process.class.name, - batch_id: batch_id, - queue_type: self.class.name, - queue_id: id, - end_product_establishment_id: end_product_establishment_id, - determined_stuck_at: stuck_record.determined_stuck_at }) + CaseflowStuckRecord.create!(stuck_record: self, + error_messages: error_messages, + determined_stuck_at: Time.zone.now) + end + + # Purpose: Destroys "SYNCED" PEPSQ records to limit the growing number of table records. + # This functionality is needed for the PopulateEndProductSyncQueueJob query to be performant. + # + # Params: The batch process the synced records belong to + # + # Response: Log message stating newly destroyed PEPSQ records + def self.destroy_batch_process_pepsq_records!(batch_process) + synced_records = batch_process.priority_end_product_sync_queue.where(status: Constants.PRIORITY_EP_SYNC.synced) + log_text = "PriorityEpSyncBatchProcessJob #{synced_records.size} synced records deleted:"\ + " #{synced_records.map(&:id)} Time: #{Time.zone.now}" + synced_records.delete_all + Rails.logger.info(log_text) end end diff --git a/app/models/request_issue.rb b/app/models/request_issue.rb index 3d8ff5bdbaf..0576c915365 100644 --- a/app/models/request_issue.rb +++ b/app/models/request_issue.rb @@ -74,6 +74,13 @@ class RequestIssue < CaseflowRecord exclude_association :decision_review_id exclude_association :request_decision_issues end + + class DecisionDateInFutureError < StandardError + def initialize(request_issue_id) + super("Request Issue #{request_issue_id} cannot edit issue decision date " \ + "due to decision date being in the future") + end + end class ErrorCreatingDecisionIssue < StandardError def initialize(request_issue_id) super("Request Issue #{request_issue_id} cannot create decision issue " \ @@ -493,6 +500,10 @@ def close!(status:, closed_at_value: Time.zone.now) transaction do update!(closed_at: closed_at_value, closed_status: status) + + # Special handling for claim reviews that contain issues without a decision date + decision_review.try(:handle_issues_with_no_decision_date!) + yield if block_given? end end @@ -523,12 +534,22 @@ def save_edited_contention_text!(new_description) update!(edited_description: new_description, contention_updated_at: nil) end + def save_decision_date!(new_decision_date) + fail DecisionDateInFutureError, id if new_decision_date.to_date > Time.zone.today + + update!(decision_date: new_decision_date) + + # Special handling for claim reviews that contain issues without a decision date + decision_review.try(:handle_issues_with_no_decision_date!) + end + def remove! close!(status: :removed) do legacy_issue_optin&.flag_for_rollback! # If the decision issue is not associated with any other request issue, also delete decision_issues.each(&:soft_delete_on_removed_request_issue) + # Removing a request issue also deletes the associated request_decision_issue request_decision_issues.update_all(deleted_at: Time.zone.now) canceled! if submitted_not_processed? diff --git a/app/models/request_issues_update.rb b/app/models/request_issues_update.rb index 2993a921bbb..4d9dc5267b6 100644 --- a/app/models/request_issues_update.rb +++ b/app/models/request_issues_update.rb @@ -147,7 +147,14 @@ def calculate_pact_edited_issues def edited_issue_data return [] unless @request_issues_data - @request_issues_data.select { |ri| ri[:edited_description].present? && ri[:request_issue_id] } + @request_issues_data.select do |ri| + edited_issue?(ri) + end + end + + def edited_issue?(request_issue) + (request_issue[:edited_description].present? || request_issue[:edited_decision_date].present?) && + request_issue[:request_issue_id] end def mst_edited_issue_data @@ -245,9 +252,21 @@ def process_edited_issues! return if edited_issues.empty? edited_issue_data.each do |edited_issue| - RequestIssue.find( - edited_issue[:request_issue_id].to_s - ).save_edited_contention_text!(edited_issue[:edited_description]) + request_issue = RequestIssue.find(edited_issue[:request_issue_id].to_s) + edit_contention_text(edited_issue, request_issue) + edit_decision_date(edited_issue, request_issue) + end + end + + def edit_contention_text(edited_issue_params, request_issue) + if edited_issue_params[:edited_description] + request_issue.save_edited_contention_text!(edited_issue_params[:edited_description]) + end + end + + def edit_decision_date(edited_issue_params, request_issue) + if edited_issue_params[:edited_decision_date] + request_issue.save_decision_date!(edited_issue_params[:edited_decision_date]) end end diff --git a/app/models/serializers/work_queue/decision_review_task_serializer.rb b/app/models/serializers/work_queue/decision_review_task_serializer.rb index 088226e19ff..7455091447b 100644 --- a/app/models/serializers/work_queue/decision_review_task_serializer.rb +++ b/app/models/serializers/work_queue/decision_review_task_serializer.rb @@ -129,6 +129,12 @@ def self.representative_tz(object) decision_review(object).is_a?(Appeal) ? "Board Grant" : decision_review(object).class.review_title end + attribute :external_appeal_id do |object| + object[:external_appeal_id] || decision_review(object).uuid + end + + attribute :appeal_type + attribute :business_line do |object| assignee = object.assigned_to diff --git a/app/models/user.rb b/app/models/user.rb index aa36efb6e0e..2a5adc16d66 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -275,7 +275,7 @@ def camo_employee? end def vha_employee? - member_of_organization?(BusinessLine.find_by(url: "vha")) + member_of_organization?(VhaBusinessLine.singleton) end def organization_queue_user? diff --git a/app/models/vha_membership_request_mail_builder.rb b/app/models/vha_membership_request_mail_builder.rb index ba3a916a227..7b29401d227 100644 --- a/app/models/vha_membership_request_mail_builder.rb +++ b/app/models/vha_membership_request_mail_builder.rb @@ -136,12 +136,13 @@ def requestor_vha_pending_organization_request_names end def organization_vha?(organization) - vha_organization_types = [VhaCamo, VhaCaregiverSupport, VhaProgramOffice, VhaRegionalOffice] - organization.url == "vha" || vha_organization_types.any? { |vha_org| organization.is_a?(vha_org) } + vha_organization_types = [VhaBusinessLine, VhaCamo, VhaCaregiverSupport, VhaProgramOffice, VhaRegionalOffice] + vha_organization_types.any? { |vha_org| organization.is_a?(vha_org) } end def belongs_to_vha_org? - requestor.organizations.any? { |org| org.url == "vha" } + # requestor.organizations.any? { |org| org.url == "vha" } + requestor.member_of_organization?(VhaBusinessLine.singleton) end def single_request diff --git a/app/serializers/intake/request_issue_serializer.rb b/app/serializers/intake/request_issue_serializer.rb index 8f09f2d3087..263bea4454e 100644 --- a/app/serializers/intake/request_issue_serializer.rb +++ b/app/serializers/intake/request_issue_serializer.rb @@ -8,6 +8,7 @@ class Intake::RequestIssueSerializer attribute :rating_issue_profile_date, &:contested_rating_issue_profile_date attribute :rating_decision_reference_id, &:contested_rating_decision_reference_id attribute :description + attribute :nonrating_issue_description attribute :contention_text attribute :approx_decision_date, &:approx_decision_date_of_issue_being_contested attribute :category, &:nonrating_issue_category diff --git a/app/views/decision_reviews/index.html.erb b/app/views/decision_reviews/index.html.erb index b66c621dbd7..4b6ed191c42 100644 --- a/app/views/decision_reviews/index.html.erb +++ b/app/views/decision_reviews/index.html.erb @@ -14,6 +14,7 @@ }, poaAlert: {}, baseTasksUrl: business_line.tasks_url, + businessLineConfig: business_line_config_options, taskFilterDetails: task_filter_details }, ui: { diff --git a/app/views/decision_reviews/show.html.erb b/app/views/decision_reviews/show.html.erb index 46a529f330a..9e269261b89 100644 --- a/app/views/decision_reviews/show.html.erb +++ b/app/views/decision_reviews/show.html.erb @@ -8,6 +8,7 @@ serverNonComp: { businessLine: business_line.name, businessLineUrl: business_line.url, + businessLineConfig: business_line_config_options, baseTasksUrl: business_line.tasks_url, taskFilterDetails: task_filter_details, task: task.ui_hash, diff --git a/client/COPY.json b/client/COPY.json index 1ffac901d24..fc32c40cd20 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -888,6 +888,7 @@ "CORRECT_REQUEST_ISSUES_LINK": "Correct issues", "CORRECT_REQUEST_ISSUES_WITHDRAW": "Withdraw", "CORRECT_REQUEST_ISSUES_SAVE": "Save", + "CORRECT_REQUEST_ISSUES_ESTABLISH": "Establish", "CORRECT_REQUEST_ISSUES_SPLIT_APPEAL": "Split appeal", "CORRECT_REQUEST_ISSUES_REMOVE_VBMS_TITLE": "Remove review?", "CORRECT_REQUEST_ISSUES_REMOVE_VBMS_TEXT": "This will remove the review and cancel all the End Products associated with it.", @@ -931,6 +932,7 @@ "UPDATE_POA_PAGE_DESCRIPTION": "Add the appellant’s POA information based on the VA Form 21-22, so they can be notified of any correspondence sent to the claimant. If you are unable to find their name in the list of options, please select \"Name not listed\" and add their information accordingly.", "INTAKE_EDIT_WITHDRAW_DATE": "Please include the date the withdrawal was requested", "INTAKE_WITHDRAWN_BANNER": "This review will be withdrawn. You can intake these issues as a different type of decision review, if that was requested.", + "CLAIM_REVIEW_WITHDRAWN_MESSAGE": "You have successfully withdrawn a review.", "INTAKE_RATING_MAY_BE_PROCESS": "Rating may be in progress", "INTAKE_VETERAN_PAY_GRADE_INVALID": "Please check the Veteran's pay grade data in VBMS or SHARE to ensure all values are valid and try again.", "INTAKE_CONTENTION_HAS_EXAM_REQUESTED": "A medical exam is requested. Issue cannot be removed.", @@ -973,6 +975,7 @@ "VHA_CAMO_PRE_DOCKET_INTAKE_SUCCESS_TITLE": "Appeal recorded and sent to VHA CAMO for document assessment", "VHA_CAREGIVER_SUPPORT_PRE_DOCKET_INTAKE_SUCCESS_TITLE": "Appeal recorded and sent to VHA Caregiver for document assessment", + "VHA_NO_DECISION_DATE_BANNER": "This claim will be saved, but cannot be worked on until a decision date is added to this issue.", "EDUCATION_PRE_DOCKET_INTAKE_SUCCESS_TITLE": "Appeal recorded and sent to Education Service for document assessment", "PRE_DOCKET_INTAKE_SUCCESS_TITLE": "Appeal recorded in pre-docket queue", "INTAKE_SUCCESS_TITLE": "Intake completed", @@ -1219,6 +1222,7 @@ "VHA_CAMO_ASSIGN_TO_REGIONAL_OFFICE_DROPDOWN_LABEL_VAMC": "VA Medical Center", "VHA_CAMO_ASSIGN_TO_REGIONAL_OFFICE_DROPDOWN_LABEL_VISN": "VISN", "VHA_CAREGIVER_LABEL": "CSP", + "VHA_INCOMPLETE_TAB_DESCRIPTION": "Cases that have been only saved and not yet established. Select the claimant name if you need to edit issues.", "EDUCATION_LABEL": "Education Service", "PRE_DOCKET_TASK_LABEL": "Pre-Docket", "DOCKET_APPEAL_MODAL_TITLE": "Docket appeal", @@ -1401,6 +1405,8 @@ "VHA_ACTION_PLACE_CUSTOM_HOLD_COPY": "Enter a custom number of days for the hold (Value must be between 1 and 45 for VHA users)", "VHA_CANCEL_TASK_INSTRUCTIONS_LABEL": "Why are you returning? Provide any important context", "DISPOSITION_DECISION_DATE_LABEL": "Thank you for completing your decision in Caseflow. Please indicate the decision date.", + "VHA_ADD_DECISION_DATE_TO_ISSUE_SUCCESS_MESSAGE": "You have successfully updated an issue's decision date", + "NO_DATE_ENTERED": "No date entered", "REFRESH_POA": "Refresh POA", "POA_SUCCESSFULLY_REFRESH_MESSAGE": "Successfully refreshed. No power of attorney information was found at this time.", "POA_UPDATED_SUCCESSFULLY": "POA Updated Successfully" diff --git a/client/app/components/Alert.jsx b/client/app/components/Alert.jsx index 2d851edb3a8..0fcc4da8983 100644 --- a/client/app/components/Alert.jsx +++ b/client/app/components/Alert.jsx @@ -13,7 +13,7 @@ export default class Alert extends React.Component { messageDiv() { const message = this.props.children || this.props.message; - return
{message}
; + return
{message}
; } render() { @@ -56,6 +56,7 @@ Alert.propTypes = { */ lowerMargin: PropTypes.bool, message: PropTypes.node, + messageStyling: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), /** * If empty, a "slim" alert is displayed diff --git a/client/app/intake/IntakeFrame.jsx b/client/app/intake/IntakeFrame.jsx index 2fc66ff2829..56b57338bfb 100644 --- a/client/app/intake/IntakeFrame.jsx +++ b/client/app/intake/IntakeFrame.jsx @@ -14,7 +14,7 @@ import SelectFormPage, { SelectFormButton } from './pages/selectForm'; import SearchPage from './pages/search'; import ReviewPage from './pages/review'; import FinishPage, { FinishButtons } from './pages/finish'; -import { IntakeAddIssuesPage } from './pages/addIssues'; +import { IntakeAddIssuesPage } from './pages/addIssues/addIssues'; import CompletedPage, { CompletedNextButton } from './pages/completed'; import { PAGE_PATHS } from './constants'; import { toggleCancelModal, submitCancel } from './actions/intake'; diff --git a/client/app/intake/actions/addIssues.js b/client/app/intake/actions/addIssues.js index ef4c01e2ad3..481ad930a5e 100644 --- a/client/app/intake/actions/addIssues.js +++ b/client/app/intake/actions/addIssues.js @@ -3,6 +3,10 @@ import { issueByIndex } from '../util/issues'; const analytics = true; +export const toggleAddDecisionDateModal = () => ({ + type: ACTIONS.TOGGLE_ADD_DECISION_DATE_MODAL, +}); + export const toggleAddingIssue = () => ({ type: ACTIONS.TOGGLE_ADDING_ISSUE, meta: { analytics } @@ -58,6 +62,11 @@ export const setMstPactIssue = (mstPactValues) => ({ payload: { mstPactValues } }); +export const addDecisionDate = ({ decisionDate, index }) => ({ + type: ACTIONS.ADD_DECISION_DATE, + payload: { decisionDate, index } +}); + export const removeIssue = (index) => ({ type: ACTIONS.REMOVE_ISSUE, payload: { index } diff --git a/client/app/intake/components/AddDecisionDateModal/AddDecisionDateModal.jsx b/client/app/intake/components/AddDecisionDateModal/AddDecisionDateModal.jsx new file mode 100644 index 00000000000..cd419d79e01 --- /dev/null +++ b/client/app/intake/components/AddDecisionDateModal/AddDecisionDateModal.jsx @@ -0,0 +1,99 @@ +import React, { useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { css } from 'glamor'; +import PropTypes from 'prop-types'; +import DateSelector from 'app/components/DateSelector'; +import Modal from 'app/components/Modal'; +import { addDecisionDate } from 'app/intake/actions/addIssues'; +import { validateDateNotInFuture } from 'app/intake/util/issues'; +import BENEFIT_TYPES from 'constants/BENEFIT_TYPES'; + +const dateInputStyling = css({ + paddingTop: '24px' +}); + +const labelStyling = css({ + marginRight: '4px', +}); + +const AddDecisionDateModal = ({ closeHandler, currentIssue, index }) => { + const [decisionDate, setDecisionDate] = useState(currentIssue.editedDecisionDate); + const dispatch = useDispatch(); + + // We should disable the save button if there has been no date selected + // or if the date is in the future + const isSaveDisabled = useMemo(() => { + if (!decisionDate) { + return true; + } + + return !validateDateNotInFuture(decisionDate); + }, [decisionDate]); + + const handleOnSubmit = () => { + dispatch(addDecisionDate({ decisionDate, index })); + }; + + return ( +
+ { + closeHandler(); + handleOnSubmit(); + } + } + ]} + visible + closeHandler={closeHandler} + title={currentIssue.editedDecisionDate ? 'Edit Decision Date' : 'Add Decision Date'} + > +
+ + Issue: + + {currentIssue.category} +
+
+ + Benefit type: + + {BENEFIT_TYPES[currentIssue.benefitType]} +
+
+ + Issue description: + + {currentIssue.nonRatingIssueDescription || currentIssue.description} +
+
+ setDecisionDate(value)} + type="date" + value={decisionDate} + /> +
+
+
+ ); +}; + +AddDecisionDateModal.propTypes = { + closeHandler: PropTypes.func, + currentIssue: PropTypes.object, + index: PropTypes.number +}; + +export default AddDecisionDateModal; diff --git a/client/app/intake/components/AddDecisionDateModal/AddDecisionDateModal.stories.js b/client/app/intake/components/AddDecisionDateModal/AddDecisionDateModal.stories.js new file mode 100644 index 00000000000..20532dfdd63 --- /dev/null +++ b/client/app/intake/components/AddDecisionDateModal/AddDecisionDateModal.stories.js @@ -0,0 +1,25 @@ +import React from 'react'; +import ReduxBase from 'app/components/ReduxBase'; +import { reducer, generateInitialState } from 'app/intake'; +import AddDecisionDateModal from './AddDecisionDateModal'; +import mockData from './mockData'; + +const ReduxDecorator = (Story) => ( + + + +); + +export default { + component: AddDecisionDateModal, + decorators: [ReduxDecorator], + title: 'Intake/Edit Issues/Add Decision Date Modal', +}; + +export const Basic = () => { + const { closeHandler, currentIssue, index } = mockData; + + return ( + + ); +}; diff --git a/client/app/intake/components/AddDecisionDateModal/AddDecisionDateModal.test.js b/client/app/intake/components/AddDecisionDateModal/AddDecisionDateModal.test.js new file mode 100644 index 00000000000..cbb13486fa4 --- /dev/null +++ b/client/app/intake/components/AddDecisionDateModal/AddDecisionDateModal.test.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import AddDecisionDateModal from './AddDecisionDateModal'; +import mockData from './mockData'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => jest.fn().mockImplementation(() => Promise.resolve(true)), +})); + +// Ensures the snapshot always matches the same date. +const fakeDate = new Date(2023, 7, 10, 0, 0, 0, 0); + +describe('AddDecisionDateModal', () => { + + beforeAll(() => { + // Ensure consistent handling of dates across tests + jest.useFakeTimers('modern'); + jest.setSystemTime(fakeDate); + }); + + afterAll(() => { + jest.clearAllMocks(); + jest.useRealTimers(); + }); + + const setup = (testProps) => + render( + + ); + + it('renders', () => { + const modal = setup(mockData); + + expect(modal).toMatchSnapshot(); + expect(screen.getByText('Add Decision Date')).toBeInTheDocument(); + }); + + it('displays Edit Decision Date if the issue has an editedDecisionDate', () => { + setup({ ...mockData, currentIssue: { ...mockData.currentIssue, editedDecisionDate: '12/7/2017' } }); + + expect(screen.getByText('Edit Decision Date')).toBeInTheDocument(); + }); + + it('disables save button if no date is present', () => { + setup(mockData); + const save = screen.getByText('Save'); + + expect(save).toHaveAttribute('disabled'); + }); +}); diff --git a/client/app/intake/components/AddDecisionDateModal/__snapshots__/AddDecisionDateModal.test.js.snap b/client/app/intake/components/AddDecisionDateModal/__snapshots__/AddDecisionDateModal.test.js.snap new file mode 100644 index 00000000000..55e9675cdf8 --- /dev/null +++ b/client/app/intake/components/AddDecisionDateModal/__snapshots__/AddDecisionDateModal.test.js.snap @@ -0,0 +1,334 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AddDecisionDateModal renders 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+
+ +
+
+ , + "container":
+
+ +
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/client/app/intake/components/AddDecisionDateModal/mockData.js b/client/app/intake/components/AddDecisionDateModal/mockData.js new file mode 100644 index 00000000000..b04c118a7f6 --- /dev/null +++ b/client/app/intake/components/AddDecisionDateModal/mockData.js @@ -0,0 +1,22 @@ +const closeHandler = () => { + // eslint-disable-next-line no-console + console.log('Close'); +}; + +const currentIssue = { + id: '4310', + benefitType: 'vha', + description: 'Beneficiary Travel - Issue Description', + decisionDate: null, + decisionReviewTitle: 'Higher-Level Review', + contentionText: 'Beneficiary Travel - Issue Description', + category: 'Beneficiary Travel', +}; + +const index = 0; + +export default { + closeHandler, + currentIssue, + index +}; diff --git a/client/app/intake/components/AddIssuesModal.jsx b/client/app/intake/components/AddIssuesModal.jsx index bdc3c5622fa..1d187eeb85d 100644 --- a/client/app/intake/components/AddIssuesModal.jsx +++ b/client/app/intake/components/AddIssuesModal.jsx @@ -8,6 +8,7 @@ import Modal from '../../components/Modal'; import IntakeRadioField from './IntakeRadioField'; import TextField from '../../components/TextField'; import { issueByIndex } from '../util/issues'; +import { generateSkipButton } from '../util/buttonUtils'; class AddIssuesModal extends React.Component { constructor(props) { @@ -242,12 +243,8 @@ class AddIssuesModal extends React.Component { } ]; - if (this.props.onSkip && !this.props.intakeData.isDtaError) { - btns.push({ - classNames: ['usa-button', 'usa-button-secondary', 'no-matching-issues'], - name: this.props.skipText, - onClick: this.props.onSkip - }); + if (!this.props.intakeData.isDtaError) { + generateSkipButton(btns, this.props); } return btns; diff --git a/client/app/intake/components/AddedIssue.jsx b/client/app/intake/components/AddedIssue.jsx index 54419eee87e..f0c7b7c2b20 100644 --- a/client/app/intake/components/AddedIssue.jsx +++ b/client/app/intake/components/AddedIssue.jsx @@ -132,7 +132,9 @@ class AddedIssue extends React.PureComponent { )} {issue.benefitType && Benefit type: {BENEFIT_TYPES[issue.benefitType]}} - {issue.date && Decision date: {formatDateStr(issue.date)}} + + Decision date: {issue.date ? formatDateStr(issue.date) : COPY.NO_DATE_ENTERED } + {issue.notes && Notes: {issue.notes}} { (mstFeatureToggle || pactFeatureToggle || legacyMstPactFeatureToggle) && // eslint-disable-next-line max-len diff --git a/client/app/intake/components/CorrectionTypeModal.jsx b/client/app/intake/components/CorrectionTypeModal.jsx index 5538dbf3caf..1dc4bbfd555 100644 --- a/client/app/intake/components/CorrectionTypeModal.jsx +++ b/client/app/intake/components/CorrectionTypeModal.jsx @@ -5,6 +5,7 @@ import Modal from '../../components/Modal'; import RadioField from '../../components/RadioField'; import { INTAKE_CORRECTION_TYPE_MODAL_TITLE, INTAKE_CORRECTION_TYPE_MODAL_COPY } from '../../../COPY'; import { CORRECTION_TYPE_OPTIONS } from '../constants'; +import { generateSkipButton } from '../util/buttonUtils'; class CorrectionTypeModal extends React.Component { constructor(props) { @@ -39,13 +40,7 @@ class CorrectionTypeModal extends React.Component { } ]; - if (this.props.onSkip) { - btns.push({ - classNames: ['usa-button', 'usa-button-secondary', 'no-matching-issues'], - name: this.props.skipText, - onClick: this.props.onSkip - }); - } + generateSkipButton(btns, this.props); return btns; } diff --git a/client/app/intake/components/IssueList.jsx b/client/app/intake/components/IssueList.jsx index 9c8e05de127..44764729022 100644 --- a/client/app/intake/components/IssueList.jsx +++ b/client/app/intake/components/IssueList.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import COPY from '../../../COPY'; import { FORM_TYPES } from '../constants'; import AddedIssue from './AddedIssue'; +import Alert from 'app/components/Alert'; import Button from '../../components/Button'; import Dropdown from '../../components/Dropdown'; import EditContentionTitle from '../components/EditContentionTitle'; @@ -10,6 +11,16 @@ import { css } from 'glamor'; import { COLORS } from '../../constants/AppConstants'; import _ from 'lodash'; +const alertStyling = css({ + marginTop: 0, + marginBottom: '20px' +}); + +const messageStyling = css({ + color: COLORS.GREY, + fontSize: '17px !important', +}); + const nonEditableIssueStyling = css({ color: COLORS.GREY, fontStyle: 'Italic' @@ -57,6 +68,18 @@ export default class IssuesList extends React.Component { } } + const isIssueWithdrawn = issue.withdrawalDate || issue.withdrawalPending; + + // Do not show the Add Decision Date action if the issue is pending or is fully withdrawn + if ((!issue.date || issue.editedDecisionDate) && !isIssueWithdrawn) { + options.push( + { + displayText: issue.editedDecisionDate ? 'Edit decision date' : 'Add decision date', + value: 'add_decision_date' + } + ); + } + return options; } @@ -89,6 +112,9 @@ export default class IssuesList extends React.Component { issue, userCanWithdrawIssues, userCanEditIntakeIssues, intakeData.isDtaError, intakeData.docketType ); + const isIssueWithdrawn = issue.withdrawalDate || issue.withdrawalPending; + const showNoDecisionDateBanner = !issue.date && !isIssueWithdrawn; + return
+ {showNoDecisionDateBanner ? + : null} {editableContentionText && } diff --git a/client/app/intake/components/IssueList.test.js b/client/app/intake/components/IssueList.test.js new file mode 100644 index 00000000000..9e0617bd204 --- /dev/null +++ b/client/app/intake/components/IssueList.test.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import COPY from '../../../COPY'; +import userEvent from '@testing-library/user-event'; +import IssuesList from 'app/intake/components/IssueList'; +import { mockedIssueListProps } from './mockData/issueListProps'; + +describe('IssuesList', () => { + const mockOnClickIssueAction = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const setup = (testProps) => + render( + + ); + + it('renders the "Add Decision Date" list action if an issue has no decision date', () => { + setup(mockedIssueListProps); + + expect(screen.getByText('Add decision date')).toBeInTheDocument(); + + }); + + it('clicking "Add Decision Date" list action will open the Add Decision Date Modal', async () => { + setup(mockedIssueListProps); + const select = screen.getAllByText('Select action')[0].parentElement; + + await userEvent.selectOptions(select, ['Add decision date']); + expect(mockOnClickIssueAction).toHaveBeenCalledWith(0, 'add_decision_date'); + + }); + + it('renders the no decision date banner if an issue has no decision date', () => { + setup(mockedIssueListProps); + + expect(screen.getByText(COPY.VHA_NO_DECISION_DATE_BANNER)).toBeInTheDocument(); + expect(screen.getByText('Decision date: No date entered')).toBeInTheDocument(); + + }); + + it('does not render the no decision date banner if an issue has a decision date', () => { + const propsWithDecisionDates = { + ...mockedIssueListProps, + }; + + // Alter the first issue to have a decision date. + propsWithDecisionDates.issues[0].date = '2023-07-20'; + + setup(mockedIssueListProps); + + expect(screen.queryByText(COPY.VHA_NO_DECISION_DATE_BANNER)).not.toBeInTheDocument(); + + }); + + it('renders the "Edit decision date" list action if an issue originally has an editedDecisionDate', () => { + const propsWithEditedDecisionDate = { + ...mockedIssueListProps, + }; + + propsWithEditedDecisionDate.issues[0].editedDecisionDate = '2023-07-20'; + + setup(propsWithEditedDecisionDate); + + expect(screen.getByText('Edit decision date')).toBeInTheDocument(); + }); +}); diff --git a/client/app/intake/components/LegacyOptInModal.jsx b/client/app/intake/components/LegacyOptInModal.jsx index e2b1ee79593..8a19b767ac8 100644 --- a/client/app/intake/components/LegacyOptInModal.jsx +++ b/client/app/intake/components/LegacyOptInModal.jsx @@ -13,6 +13,7 @@ import { } from '../actions/addIssues'; import Modal from '../../components/Modal'; import RadioField from '../../components/RadioField'; +import { generateSkipButton } from '../util/buttonUtils'; const NO_MATCH_TEXT = 'None of these match'; const noneMatchOpt = (issue) => ({ @@ -133,13 +134,7 @@ class LegacyOptInModal extends React.Component { } ]; - if (this.props.onSkip) { - btns.push({ - classNames: ['usa-button', 'usa-button-secondary', 'no-matching-issues'], - name: this.props.skipText, - onClick: this.props.onSkip - }); - } + generateSkipButton(btns, this.props); return btns; } diff --git a/client/app/intake/components/NonratingRequestIssueModal.jsx b/client/app/intake/components/NonratingRequestIssueModal.jsx index 4d337bf2ab8..7cfd20f3416 100644 --- a/client/app/intake/components/NonratingRequestIssueModal.jsx +++ b/client/app/intake/components/NonratingRequestIssueModal.jsx @@ -17,6 +17,7 @@ import { validateDateNotInFuture, isTimely } from '../util/issues'; import { formatDateStr } from 'app/util/DateUtil'; import { VHA_PRE_DOCKET_ISSUE_BANNER } from 'app/../COPY'; import Checkbox from '../../components/Checkbox'; +import { generateSkipButton } from '../util/buttonUtils'; const NO_MATCH_TEXT = 'None of these match'; @@ -193,12 +194,19 @@ class NonratingRequestIssueModal extends React.Component { return ( !description || !category || - !decisionDate || + (!this.vhaHlrOrSC() && !decisionDate) || (formType === 'appeal' && !benefitType) || enforcePreDocketRequirement ); } + vhaHlrOrSC() { + const { benefitType } = this.state; + const { formType } = this.props; + + return ((formType === 'higher_level_review' || formType === 'supplemental_claim') && benefitType === 'vha'); + } + getModalButtons() { const btns = [ { @@ -210,17 +218,11 @@ class NonratingRequestIssueModal extends React.Component { classNames: ['usa-button', 'add-issue'], name: this.props.submitText, onClick: this.onAddIssue, - disabled: this.requiredFieldsMissing() || this.state.decisionDate.length < 10 || Boolean(this.state.dateError) + disabled: this.requiredFieldsMissing() || Boolean(this.state.dateError) } ]; - if (this.props.onSkip) { - btns.push({ - classNames: ['usa-button', 'usa-button-secondary', 'no-matching-issues'], - name: this.props.skipText, - onClick: this.props.onSkip - }); - } + generateSkipButton(btns, this.props); return btns; } @@ -285,6 +287,7 @@ class NonratingRequestIssueModal extends React.Component { errorMessage={this.state.dateError} onChange={this.decisionDateOnChange} type="date" + optional={this.vhaHlrOrSC()} />
diff --git a/client/app/intake/components/NonratingRequestIssueModal.stories.js b/client/app/intake/components/NonratingRequestIssueModal.stories.js index f09aae9ce6c..fc73930a942 100644 --- a/client/app/intake/components/NonratingRequestIssueModal.stories.js +++ b/client/app/intake/components/NonratingRequestIssueModal.stories.js @@ -42,3 +42,12 @@ export const basic = Template.bind({}); export const WithSkipButton = Template.bind({}); WithSkipButton.args = { ...defaultArgs, onSkip: () => true }; + +export const VhaBenefitType = Template.bind({}); +VhaBenefitType.args = { + ...defaultArgs, + intakeData: { + activeNonratingRequestIssues: [], + benefitType: 'vha' + } +}; diff --git a/client/app/intake/components/UnidentifiedIssuesModal.jsx b/client/app/intake/components/UnidentifiedIssuesModal.jsx index 50c2cb137e3..00a7561cc36 100644 --- a/client/app/intake/components/UnidentifiedIssuesModal.jsx +++ b/client/app/intake/components/UnidentifiedIssuesModal.jsx @@ -6,6 +6,7 @@ import TextField from '../../components/TextField'; import DateSelector from '../../components/DateSelector'; import { validateDateNotInFuture, isTimely } from '../util/issues'; import Checkbox from '../../components/Checkbox'; +import { generateSkipButton } from '../util/buttonUtils'; class UnidentifiedIssuesModal extends React.Component { constructor(props) { @@ -98,13 +99,7 @@ class UnidentifiedIssuesModal extends React.Component { } ]; - if (this.props.onSkip) { - btns.push({ - classNames: ['usa-button', 'usa-button-secondary', 'no-matching-issues'], - name: this.props.skipText, - onClick: this.props.onSkip - }); - } + generateSkipButton(btns, this.props); return btns; } diff --git a/client/app/intake/components/mockData/issueListProps.js b/client/app/intake/components/mockData/issueListProps.js new file mode 100644 index 00000000000..abb4a608635 --- /dev/null +++ b/client/app/intake/components/mockData/issueListProps.js @@ -0,0 +1,277 @@ +export const mockedIssueListProps = { + editPage: true, + intakeData: { + claimant: '358808523', + claimantType: 'veteran', + claimantName: 'Bob Smithkeebler', + veteranIsNotClaimant: false, + processedInCaseflow: true, + legacyOptInApproved: false, + legacyAppeals: [], + ratings: null, + editIssuesUrl: '/higher_level_reviews/6545833b-1a6c-4966-823f-7d0037aa5f6a/edit', + processedAt: '2023-07-28T14:34:36.571-04:00', + veteranInvalidFields: { + veteranMissingFields: '', + veteranAddressTooLong: false, + veteranZipCodeInvalid: false, + veteranPayGradeInvalid: false + }, + requestIssues: [ + { + id: 6292, + rating_issue_reference_id: null, + rating_issue_profile_date: null, + rating_decision_reference_id: null, + description: 'Other - stuff and things', + contention_text: 'Other - stuff and things', + approx_decision_date: null, + category: 'Other', + notes: null, + is_unidentified: null, + ramp_claim_id: null, + vacols_id: null, + vacols_sequence_id: null, + ineligible_reason: null, + ineligible_due_to_id: null, + decision_review_title: 'Higher-Level Review', + title_of_active_review: null, + contested_decision_issue_id: null, + withdrawal_date: null, + contested_issue_description: null, + end_product_code: null, + end_product_establishment_code: null, + verified_unidentified_issue: null, + editable: true, + exam_requested: null, + vacols_issue: null, + end_product_cleared: null, + benefit_type: 'vha', + is_predocket_needed: null + } + ], + decisionIssues: [], + activeNonratingRequestIssues: [ + { + id: '6291', + benefitType: 'vha', + decisionIssueId: null, + description: 'Caregiver | Eligibility - pact act testing', + decisionDate: '2023-07-19', + ineligibleReason: null, + ineligibleDueToId: null, + decisionReviewTitle: 'Higher-Level Review', + contentionText: 'Caregiver | Eligibility - pact act testing', + vacolsId: null, + vacolsSequenceId: null, + vacolsIssue: null, + endProductCleared: null, + endProductCode: null, + withdrawalDate: null, + editable: true, + examRequested: null, + isUnidentified: null, + notes: null, + category: 'Caregiver | Eligibility', + index: null, + isRating: false, + ratingIssueReferenceId: null, + ratingDecisionReferenceId: null, + ratingIssueProfileDate: null, + approxDecisionDate: '2023-07-19', + titleOfActiveReview: null, + rampClaimId: null, + verifiedUnidentifiedIssue: null, + isPreDocketNeeded: null + } + ], + contestableIssuesByDate: [], + intakeUser: 'SUPERUSER', + relationships: [ + { + value: 'CLAIMANT_WITH_PVA_AS_VSO', + fullName: 'Bob Vance', + relationshipType: 'Spouse', + displayText: 'Bob Vance, Spouse', + defaultPayeeCode: '10' + }, + { + value: '1129318238', + fullName: 'Cathy Smith', + relationshipType: 'Child', + displayText: 'Cathy Smith, Child', + defaultPayeeCode: '11' + }, + { + value: 'no-such-pid', + fullName: 'Tom Brady', + relationshipType: 'Child', + displayText: 'Tom Brady, Child', + defaultPayeeCode: '11' + } + ], + veteranValid: true, + receiptDate: '2023/07/14', + veteran: { + name: 'Bob Smithkeebler', + fileNumber: '000100009', + formName: 'Smithkeebler, Bob', + ssn: '303940217' + }, + powerOfAttorneyName: 'Clarence Darrow', + claimantRelationship: 'Veteran', + asyncJobUrl: '/asyncable_jobs/HigherLevelReview/jobs/385', + benefitType: 'vha', + payeeCode: null, + hasClearedRatingEp: false, + hasClearedNonratingEp: false, + informalConference: false, + sameOffice: null, + formType: 'higher_level_review', + contestableIssues: {}, + claimId: '6545833b-1a6c-4966-823f-7d0037aa5f6a', + featureToggles: { + useAmaActivationDate: true, + correctClaimReviews: true, + covidTimelinessExemption: true + }, + userCanWithdrawIssues: false, + addIssuesModalVisible: false, + nonRatingRequestIssueModalVisible: false, + unidentifiedIssuesModalVisible: false, + addedIssues: [ + { + id: '6291', + benefitType: 'vha', + decisionIssueId: null, + description: 'Other - Other description', + decisionDate: null, + ineligibleReason: null, + ineligibleDueToId: null, + decisionReviewTitle: 'Higher-Level Review', + contentionText: 'Other - Other description', + vacolsId: null, + vacolsSequenceId: null, + vacolsIssue: null, + endProductCleared: null, + endProductCode: null, + withdrawalDate: null, + editable: true, + examRequested: null, + isUnidentified: null, + notes: null, + category: 'Other', + index: null, + isRating: false, + ratingIssueReferenceId: null, + ratingDecisionReferenceId: null, + ratingIssueProfileDate: null, + approxDecisionDate: null, + titleOfActiveReview: null, + rampClaimId: null, + verifiedUnidentifiedIssue: null, + isPreDocketNeeded: null + }, + { + benefitType: 'vha', + category: 'Beneficiary Travel', + description: 'vha camo testing', + decisionDate: '2023-07-19', + ineligibleDueToId: null, + ineligibleReason: null, + decisionReviewTitle: null, + isRating: false, + isPreDocketNeeded: null, + timely: true, + editable: true + } + ], + originalIssues: [ + { + id: '6292', + benefitType: 'vha', + decisionIssueId: null, + description: 'Other - stuff and things', + decisionDate: null, + ineligibleReason: null, + ineligibleDueToId: null, + decisionReviewTitle: 'Higher-Level Review', + contentionText: 'Other - stuff and things', + vacolsId: null, + vacolsSequenceId: null, + vacolsIssue: null, + endProductCleared: null, + endProductCode: null, + withdrawalDate: null, + editable: true, + examRequested: null, + isUnidentified: null, + notes: null, + category: 'Other', + index: null, + isRating: false, + ratingIssueReferenceId: null, + ratingDecisionReferenceId: null, + ratingIssueProfileDate: null, + approxDecisionDate: null, + titleOfActiveReview: null, + rampClaimId: null, + verifiedUnidentifiedIssue: null, + isPreDocketNeeded: null + } + ], + requestStatus: { + requestIssuesUpdate: 'NOT_STARTED' + }, + requestIssuesUpdateErrorCode: null, + afterIssues: null, + beforeIssues: null, + updatedIssues: null, + editEpUpdateError: null, + issueCount: 2 + }, + issues: [ + { + index: 0, + id: '6292', + text: 'Other - stuff and things', + benefitType: 'vha', + date: null, + beforeAma: true, + ineligibleReason: null, + vacolsId: null, + vacolsSequenceId: null, + vacolsIssue: null, + decisionReviewTitle: 'Higher-Level Review', + withdrawalDate: null, + endProductCleared: null, + endProductCode: null, + category: 'Other', + editable: true, + examRequested: null, + decisionIssueId: null, + isPreDocketNeeded: null + }, + { + index: 1, + text: 'Beneficiary Travel - vha camo testing', + benefitType: 'vha', + date: '2023-07-19', + timely: true, + beforeAma: false, + ineligibleReason: null, + decisionReviewTitle: null, + category: 'Beneficiary Travel', + editable: true, + isPreDocketNeeded: null + } + ], + featureToggles: { + useAmaActivationDate: true, + correctClaimReviews: true, + covidTimelinessExemption: true + }, + formType: 'higher_level_review', + userCanWithdrawIssues: false, + withdrawReview: false +}; diff --git a/client/app/intake/constants.js b/client/app/intake/constants.js index 0cef7c94df3..b920fa673c4 100644 --- a/client/app/intake/constants.js +++ b/client/app/intake/constants.js @@ -120,6 +120,7 @@ export const ACTIONS = { SET_DOCKET_TYPE: 'SET_DOCKET_TYPE', SET_ORIGINAL_HEARING_REQUEST_TYPE: 'SET_ORIGINAL_HEARING_REQUEST_TYPE', TOGGLE_CANCEL_MODAL: 'TOGGLE_CANCEL_MODAL', + TOGGLE_ADD_DECISION_DATE_MODAL: 'TOGGLE_ADD_DECISION_DATE_MODAL', TOGGLE_ADDING_ISSUE: 'TOGGLE_ADDING_ISSUE', TOGGLE_ADD_ISSUES_MODAL: 'TOGGLE_ADD_ISSUES_MODAL', TOGGLE_NONRATING_REQUEST_ISSUE_MODAL: 'TOGGLE_NONRATING_REQUEST_ISSUE_MODAL', @@ -142,6 +143,7 @@ export const ACTIONS = { CONFIRM_FINISH_INTAKE: 'CONFIRM_FINISH_INTAKE', COMPLETE_INTAKE_NOT_CONFIRMED: 'COMPLETE_INTAKE_NOT_CONFIRMED', SET_ISSUE_SELECTED: 'SET_ISSUE_SELECTED', + ADD_DECISION_DATE: 'ADD_DECISION_DATE', ADD_ISSUE: 'ADD_ISSUE', REMOVE_ISSUE: 'REMOVE_ISSUE', WITHDRAW_ISSUE: 'WITHDRAW_ISSUE', diff --git a/client/app/intake/pages/addIssues.jsx b/client/app/intake/pages/addIssues/addIssues.jsx similarity index 85% rename from client/app/intake/pages/addIssues.jsx rename to client/app/intake/pages/addIssues/addIssues.jsx index c40dc6bb117..ae0b5ea8e4f 100644 --- a/client/app/intake/pages/addIssues.jsx +++ b/client/app/intake/pages/addIssues/addIssues.jsx @@ -10,23 +10,27 @@ import { bindActionCreators } from 'redux'; import { Redirect } from 'react-router-dom'; import Link from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/Link'; -import RemoveIssueModal from '../components/RemoveIssueModal'; -import CorrectionTypeModal from '../components/CorrectionTypeModal'; -import AddIssueManager from '../components/AddIssueManager'; - -import Button from '../../components/Button'; -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 EP_CLAIM_TYPES from '../../../constants/EP_CLAIM_TYPES'; -// eslint-disable-next-line max-len -import { formatAddedIssues, formatLegacyAddedIssues, formatRequestIssues, getAddIssuesFields, formatIssuesBySection } from '../util/issues'; -import Table from '../../components/Table'; -import IssueList from '../components/IssueList'; -import Alert from 'app/components/Alert'; +import AddDecisionDateModal from 'app/intake/components/AddDecisionDateModal/AddDecisionDateModal'; +import RemoveIssueModal from '../../components/RemoveIssueModal'; +import CorrectionTypeModal from '../../components/CorrectionTypeModal'; +import AddIssueManager from '../../components/AddIssueManager'; + +import Button from '../../../components/Button'; +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 EP_CLAIM_TYPES from '../../../../constants/EP_CLAIM_TYPES'; +import { formatAddedIssues, + formatRequestIssues, + getAddIssuesFields, + formatIssuesBySection, + formatLegacyAddedIssues } from '../../util/issues'; +import Table from '../../../components/Table'; +import issueSectionRow from './issueSectionRow/issueSectionRow'; import { + toggleAddDecisionDateModal, toggleAddingIssue, toggleAddIssuesModal, toggleUntimelyExemptionModal, @@ -42,12 +46,12 @@ import { toggleLegacyOptInModal, toggleCorrectionTypeModal, toggleEditIntakeIssueModal -} from '../actions/addIssues'; -import { editEpClaimLabel } from '../../intakeEdit/actions/edit'; -import COPY from '../../../COPY'; -import { EditClaimLabelModal } from '../../intakeEdit/components/EditClaimLabelModal'; -import { ConfirmClaimLabelModal } from '../../intakeEdit/components/ConfirmClaimLabelModal'; -import { EditIntakeIssueModal } from '../../intakeEdit/components/EditIntakeIssueModal'; +} from '../../actions/addIssues'; +import { editEpClaimLabel } from '../../../intakeEdit/actions/edit'; +import COPY from '../../../../COPY'; +import { EditClaimLabelModal } from '../../../intakeEdit/components/EditClaimLabelModal'; +import { ConfirmClaimLabelModal } from '../../../intakeEdit/components/ConfirmClaimLabelModal'; +import { EditIntakeIssueModal } from '../../../intakeEdit/components/EditIntakeIssueModal'; class AddIssuesPage extends React.Component { constructor(props) { @@ -61,6 +65,7 @@ class AddIssuesPage extends React.Component { this.state = { originalIssueLength, + issueAddDecisionDateIndex: 0, issueRemoveIndex: 0, issueIndex: 0, addingIssue: false, @@ -74,6 +79,10 @@ class AddIssuesPage extends React.Component { onClickIssueAction = (index, option = 'remove') => { switch (option) { + case 'add_decision_date': + this.props.toggleAddDecisionDateModal(); + this.setState({ issueAddDecisionDateIndex: index }); + break; case 'remove': if (this.props.toggleIssueRemoveModal) { // on the edit page, so show the remove modal @@ -234,14 +243,13 @@ class AddIssuesPage extends React.Component { return this.redirect(intakeData, hasClearedEp); } - if (intakeData && this.requestIssuesWithoutDecisionDates(intakeData)) { + if (intakeData && intakeData.benefitType !== 'vha' && this.requestIssuesWithoutDecisionDates(intakeData)) { return ; } const requestStatus = intakeData.requestStatus; const requestState = requestStatus.completeIntake || requestStatus.requestIssuesUpdate || requestStatus.editClaimLabelUpdate; - const endProductWithError = intakeData.editEpUpdateError; const requestErrorCode = intakeData.requestStatus.completeIntakeErrorCode || intakeData.requestIssuesUpdateErrorCode; @@ -272,7 +280,8 @@ class AddIssuesPage extends React.Component { // if an new issue was added or an issue was edited const newOrChangedIssue = - issues.filter((issue) => !issue.id || issue.editedDescription || issue.correctionType).length > 0; + issues.filter((issue) => !issue.id || issue.editedDescription || + issue.editedDecisionDate || issue.correctionType).length > 0; if (issueCountChanged || partialWithdrawal || newOrChangedIssue) { return true; @@ -381,7 +390,12 @@ class AddIssuesPage extends React.Component { if (editPage && haveIssuesChanged()) { // flash a save message if user is on the edit page & issues have changed - const issuesChangedBanner =

When you finish making changes, click "Save" to continue.

; + const isAllIssuesReadyToBeEstablished = _.every(intakeData.addedIssues, (issue) => ( + issue.withdrawalDate || issue.withdrawalPending) || issue.decisionDate + ); + + const establishText = intakeData.benefitType === 'vha' && isAllIssuesReadyToBeEstablished ? 'Establish' : 'Save'; + const issuesChangedBanner =

{`When you finish making changes, click "${establishText}" to continue.`}

; fieldsForFormType = fieldsForFormType.concat({ field: '', @@ -390,42 +404,6 @@ class AddIssuesPage extends React.Component { additionalRowClasses = (rowObj) => (rowObj.field === '' ? 'intake-issue-flash' : ''); } - let rowObjects = fieldsForFormType; - - const issueSectionRow = (sectionIssues, fieldTitle) => { - const reviewHasPredocketVhaIssues = sectionIssues.some( - (issue) => issue.benefitType === 'vha' && issue.isPreDocketNeeded === 'true' - ); - const showPreDocketBanner = !editPage && formType === 'appeal' && reviewHasPredocketVhaIssues; - - return { - field: fieldTitle, - content: ( -
- {endProductWithError && ( - - )} - { !fieldTitle.includes('issues') && Requested issues } - - {showPreDocketBanner && } -
- ) - }; - }; - const endProductLabelRow = (endProductCode, editDisabled) => { return { field: 'EP Claim Label', @@ -448,18 +426,46 @@ class AddIssuesPage extends React.Component { }; }; + let rowObjects = fieldsForFormType; + Object.keys(issuesBySection).sort(). map((key) => { const sectionIssues = issuesBySection[key]; const endProductCleared = sectionIssues[0]?.endProductCleared; + const issueSectionRowProps = { + editPage, + featureToggles, + formType, + intakeData, + onClickIssueAction: this.onClickIssueAction, + sectionIssues, + userCanWithdrawIssues, + userCanEditIntakeIssues, + withdrawReview, + }; if (key === 'requestedIssues') { - rowObjects = rowObjects.concat(issueSectionRow(sectionIssues, 'Requested issues')); + rowObjects = rowObjects.concat( + issueSectionRow({ + ...issueSectionRowProps, + fieldTitle: 'Requested issues', + }), + ); } else if (key === 'withdrawnIssues') { - rowObjects = rowObjects.concat(issueSectionRow(sectionIssues, 'Withdrawn issues')); + rowObjects = rowObjects.concat( + issueSectionRow({ + ...issueSectionRowProps, + fieldTitle: 'Withdrawn issues', + }), + ); } else { rowObjects = rowObjects.concat(endProductLabelRow(key, endProductCleared || issuesChanged)); - rowObjects = rowObjects.concat(issueSectionRow(sectionIssues, ' ', key)); + rowObjects = rowObjects.concat( + issueSectionRow({ + ...issueSectionRowProps, + fieldTitle: ' ', + }), + ); } return rowObjects; @@ -492,6 +498,13 @@ class AddIssuesPage extends React.Component { /> )} + {intakeData.addDecisionDateModalVisible && ( + + )} {intakeData.removeIssueModalVisible && ( bindActionCreators( { + toggleAddDecisionDateModal, toggleAddingIssue, toggleIssueRemoveModal, toggleCorrectionTypeModal, diff --git a/client/app/intake/pages/addIssues/issueSectionRow/IssueSectionRow.stories.js b/client/app/intake/pages/addIssues/issueSectionRow/IssueSectionRow.stories.js new file mode 100644 index 00000000000..fe57a903866 --- /dev/null +++ b/client/app/intake/pages/addIssues/issueSectionRow/IssueSectionRow.stories.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import issueSectionRow from './issueSectionRow'; +import Table from 'app/components/Table'; +import { issueSectionRowProps } from './mockData'; + +export default { + title: 'Intake/Edit Issues/Issue Section Row', + decorators: [], + parameters: {}, +}; + +const BaseComponent = ({ content, field }) => ( +
+ + +); + +export const Basic = () => { + const Component = issueSectionRow({ + ...issueSectionRowProps, + fieldTitle: 'Withdrawn issues', + }); + + return ( + + ); +}; + +export const WithNoDecisionDate = () => { + issueSectionRowProps.sectionIssues[0].date = null; + + const Component = issueSectionRow({ + ...issueSectionRowProps, + fieldTitle: 'Withdrawn issues', + }); + + return ( + + ); +}; + +BaseComponent.propTypes = { + content: PropTypes.element, + field: PropTypes.string +}; diff --git a/client/app/intake/pages/addIssues/issueSectionRow/issueSectionRow.jsx b/client/app/intake/pages/addIssues/issueSectionRow/issueSectionRow.jsx new file mode 100644 index 00000000000..199fc3282ab --- /dev/null +++ b/client/app/intake/pages/addIssues/issueSectionRow/issueSectionRow.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import COPY from '../../../../../COPY'; +import { FORM_TYPES } from 'app/intake/constants'; +import Alert from 'app/components/Alert'; +import ErrorAlert from 'app/intake/components/ErrorAlert'; +import IssueList from 'app/intake/components/IssueList'; + +const issueSectionRow = ( + { + editPage, + featureToggles, + fieldTitle, + formType, + intakeData, + onClickIssueAction, + sectionIssues, + userCanWithdrawIssues, + userCanEditIntakeIssues, + withdrawReview + }) => { + const reviewHasPredocketVhaIssues = sectionIssues.some( + (issue) => issue.benefitType === 'vha' && issue.isPreDocketNeeded === 'true' + ); + const showPreDocketBanner = !editPage && formType === 'appeal' && reviewHasPredocketVhaIssues; + + return { + content: ( +
+ {intakeData.editEpUpdateError && ( + + )} + { !fieldTitle.includes('issues') && Requested issues } + + {showPreDocketBanner && } +
+ ), + field: fieldTitle, + }; +}; + +export default issueSectionRow; + +issueSectionRow.propTypes = { + editPage: PropTypes.bool, + featureToggles: PropTypes.object, + fieldTitle: PropTypes.string, + formType: PropTypes.oneOf(_.map(FORM_TYPES, 'key')), + intakeData: PropTypes.object, + onClickIssueAction: PropTypes.func, + sectionIssues: PropTypes.arrayOf(PropTypes.object), + userCanWithdrawIssues: PropTypes.bool, + withdrawIssue: PropTypes.func, + userCanEditIntakeIssues: PropTypes.bool +}; diff --git a/client/app/intake/pages/addIssues/issueSectionRow/mockData.js b/client/app/intake/pages/addIssues/issueSectionRow/mockData.js new file mode 100644 index 00000000000..d8c13e416ec --- /dev/null +++ b/client/app/intake/pages/addIssues/issueSectionRow/mockData.js @@ -0,0 +1,24 @@ +/* eslint-disable no-empty-function */ +const issueSectionRowProps = { + editPage: true, + featureToggles: {}, + formType: 'test', + intakeData: {}, + onClickIssueAction: () => {}, + sectionIssues: [ + { + index: 0, + id: '4277', + text: 'Medical and Dental Care Reimbursement - Issue', + benefitType: 'vha', + date: '2023-07-11', + decisionReviewTitle: 'Supplemental Claim', + category: 'Medical and Dental Care Reimbursement', + editable: true, + }, + ], + userCanWithdrawIssues: true, + withdrawReview: false +}; + +export { issueSectionRowProps }; diff --git a/client/app/intake/pages/higherLevelReview/finish.jsx b/client/app/intake/pages/higherLevelReview/finish.jsx index e46bd81d2d5..d8b5f759888 100644 --- a/client/app/intake/pages/higherLevelReview/finish.jsx +++ b/client/app/intake/pages/higherLevelReview/finish.jsx @@ -7,6 +7,8 @@ import IssueCounter from '../../components/IssueCounter'; import { completeIntake } from '../../actions/decisionReview'; import { REQUEST_STATE, FORM_TYPES } from '../../constants'; import { issueCountSelector } from '../../selectors'; +import { some } from 'lodash'; +import PropTypes from 'prop-types'; class FinishNextButton extends React.PureComponent { handleClick = () => { @@ -20,7 +22,13 @@ class FinishNextButton extends React.PureComponent { } buttonText = () => { - if (this.props.higherLevelReview.processedInCaseflow) { + const { benefitType, addedIssues, processedInCaseflow } = this.props.higherLevelReview; + + if (benefitType === 'vha' && some(addedIssues, (obj) => !obj.decisionDate)) { + return `Save ${FORM_TYPES.HIGHER_LEVEL_REVIEW.shortName}`; + } + + if (processedInCaseflow) { return `Establish ${FORM_TYPES.HIGHER_LEVEL_REVIEW.shortName}`; } @@ -38,6 +46,16 @@ class FinishNextButton extends React.PureComponent { ; } +FinishNextButton.propTypes = { + issueCount: PropTypes.number, + requestState: PropTypes.string, + addedIssues: PropTypes.shape([PropTypes.object]), + higherLevelReview: PropTypes.object, + completeIntake: PropTypes.func, + history: PropTypes.object, + intakeId: PropTypes.number, +}; + const FinishNextButtonConnected = connect( ({ higherLevelReview, intake }) => ({ requestState: higherLevelReview.requestStatus.completeIntake, @@ -67,3 +85,7 @@ export class FinishButtons extends React.PureComponent { } +FinishButtons.propTypes = { + history: PropTypes.object +}; + diff --git a/client/app/intake/pages/supplementalClaim/finish.jsx b/client/app/intake/pages/supplementalClaim/finish.jsx index 76f26bacb14..468200bf601 100644 --- a/client/app/intake/pages/supplementalClaim/finish.jsx +++ b/client/app/intake/pages/supplementalClaim/finish.jsx @@ -7,6 +7,8 @@ import IssueCounter from '../../components/IssueCounter'; import { completeIntake } from '../../actions/decisionReview'; import { REQUEST_STATE, FORM_TYPES } from '../../constants'; import { issueCountSelector } from '../../selectors'; +import { some } from 'lodash'; +import PropTypes from 'prop-types'; class FinishNextButton extends React.PureComponent { handleClick = () => { @@ -20,7 +22,13 @@ class FinishNextButton extends React.PureComponent { } buttonText = () => { - if (this.props.supplementalClaim.processedInCaseflow) { + const { benefitType, addedIssues, processedInCaseflow } = this.props.supplementalClaim; + + if (benefitType === 'vha' && some(addedIssues, (obj) => !obj.decisionDate)) { + return `Save ${FORM_TYPES.SUPPLEMENTAL_CLAIM.shortName}`; + } + + if (processedInCaseflow) { return `Establish ${FORM_TYPES.SUPPLEMENTAL_CLAIM.shortName}`; } @@ -38,6 +46,16 @@ class FinishNextButton extends React.PureComponent { ; } +FinishNextButton.propTypes = { + issueCount: PropTypes.number, + requestState: PropTypes.string, + addedIssues: PropTypes.shape([PropTypes.object]), + supplementalClaim: PropTypes.object, + completeIntake: PropTypes.func, + history: PropTypes.object, + intakeId: PropTypes.number, +}; + const FinishNextButtonConnected = connect( ({ supplementalClaim, intake }) => ({ requestState: supplementalClaim.requestStatus.completeIntake, @@ -67,3 +85,7 @@ export class FinishButtons extends React.PureComponent { } +FinishButtons.propTypes = { + history: PropTypes.object +}; + diff --git a/client/app/intake/reducers/common.js b/client/app/intake/reducers/common.js index 8ef8ccfbe40..cab696f26ec 100644 --- a/client/app/intake/reducers/common.js +++ b/client/app/intake/reducers/common.js @@ -8,6 +8,12 @@ export const commonReducers = (state, action) => { let actionsMap = {}; let listOfIssues = state.addedIssues ? state.addedIssues : []; + actionsMap[ACTIONS.TOGGLE_ADD_DECISION_DATE_MODAL] = () => { + return update(state, { + $toggle: ['addDecisionDateModalVisible'] + }); + }; + actionsMap[ACTIONS.TOGGLE_ADDING_ISSUE] = () => { return update(state, { $toggle: ['addingIssue'] @@ -110,6 +116,18 @@ export const commonReducers = (state, action) => { }); }; + actionsMap[ACTIONS.ADD_DECISION_DATE] = () => { + const { decisionDate, index } = action.payload; + + listOfIssues[index].decisionDate = decisionDate; + listOfIssues[index].editedDecisionDate = decisionDate; + + return { + ...state, + editedIssues: listOfIssues + }; + }; + actionsMap[ACTIONS.ADD_ISSUE] = () => { let addedIssues = [...listOfIssues, action.payload]; diff --git a/client/app/intake/util/buttonUtils.js b/client/app/intake/util/buttonUtils.js new file mode 100644 index 00000000000..1d2548a7f54 --- /dev/null +++ b/client/app/intake/util/buttonUtils.js @@ -0,0 +1,12 @@ +// ButtonUtils.js +export const generateSkipButton = (btns, props) => { + if (props.onSkip) { + btns.push({ + classNames: ['usa-button', 'usa-button-secondary', 'no-matching-issues'], + name: props.skipText, + onClick: props.onSkip + }); + } + + return btns; +}; diff --git a/client/app/intake/util/issues.js b/client/app/intake/util/issues.js index 6fd59f33ebe..c2afd8fa163 100644 --- a/client/app/intake/util/issues.js +++ b/client/app/intake/util/issues.js @@ -1,13 +1,13 @@ +/* eslint-disable max-lines */ import _ from 'lodash'; import { formatDateStr } from '../../util/DateUtil'; import DATES from '../../../constants/DATES'; import { FORM_TYPES } from '../constants'; -const getClaimantField = (veteran, intakeData) => { +const getClaimantField = (intakeData) => { const { claimantName, claimantRelationship, - claimantType, payeeCode } = intakeData; @@ -118,6 +118,7 @@ export const formatRequestIssues = (requestIssues, contestableIssues) => { benefitType: issue.benefit_type, decisionIssueId: issue.contested_decision_issue_id, description: issue.description, + nonRatingIssueDescription: issue.nonrating_issue_description, decisionDate: issue.approx_decision_date, ineligibleReason: issue.ineligible_reason, ineligibleDueToId: issue.ineligible_due_to_id, @@ -257,6 +258,7 @@ const formatNonratingRequestIssues = (state) => { nonrating_issue_category: issue.category, decision_text: issue.description, decision_date: issue.decisionDate, + edited_decision_date: issue.editedDecisionDate, untimely_exemption: issue.untimelyExemption, untimely_exemption_notes: issue.untimelyExemptionNotes, untimely_exemption_covid: issue.untimelyExemptionCovid, @@ -351,7 +353,7 @@ export const getAddIssuesFields = (formType, veteran, intakeData) => { // If a field is to be conditionally rendered, set field = null to have it not show. fields = fields.filter((field) => field !== null); - let claimantField = getClaimantField(veteran, intakeData); + let claimantField = getClaimantField(intakeData); return fields.concat(claimantField); }; @@ -499,6 +501,7 @@ export const formatAddedIssues = (issues = [], useAmaActivationDate = false) => text: issue.id ? issue.description : `${issue.category} - ${issue.description}`, benefitType: issue.benefitType, date: issue.decisionDate, + editedDecisionDate: issue.editedDecisionDate, timely: issue.timely, beforeAma: decisionDate < amaActivationDate, untimelyExemption: issue.untimelyExemption, diff --git a/client/app/intakeEdit/IntakeEditFrame.jsx b/client/app/intakeEdit/IntakeEditFrame.jsx index 709d41931b3..0913db01846 100644 --- a/client/app/intakeEdit/IntakeEditFrame.jsx +++ b/client/app/intakeEdit/IntakeEditFrame.jsx @@ -8,7 +8,7 @@ import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolki import Link from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/Link'; import { LOGO_COLORS } from '../constants/AppConstants'; import { PAGE_PATHS } from '../intake/constants'; -import { EditAddIssuesPage } from '../intake/pages/addIssues'; +import { EditAddIssuesPage } from '../intake/pages/addIssues/addIssues'; import SplitAppealView from '../intake/pages/SplitAppealView'; import DecisionReviewEditCompletedPage from '../intake/pages/decisionReviewEditCompleted'; import Message from './pages/message'; diff --git a/client/app/intakeEdit/components/EditButtons.jsx b/client/app/intakeEdit/components/EditButtons.jsx index 68becb6c194..aba68ad9013 100644 --- a/client/app/intakeEdit/components/EditButtons.jsx +++ b/client/app/intakeEdit/components/EditButtons.jsx @@ -110,7 +110,8 @@ class SaveButtonUnconnected extends React.Component { veteranValid, processedInCaseflow, withdrawalDate, - receiptDate + receiptDate, + benefitType } = this.props; const invalidVeteran = !veteranValid && (_.some( @@ -133,7 +134,15 @@ class SaveButtonUnconnected extends React.Component { addedIssues, (issue) => issue.withdrawalPending || issue.withdrawalDate ); - const saveButtonText = withdrawReview ? COPY.CORRECT_REQUEST_ISSUES_WITHDRAW : COPY.CORRECT_REQUEST_ISSUES_SAVE; + let saveButtonText; + + if (benefitType === 'vha' && _.every(addedIssues, (issue) => ( + issue.withdrawalDate || issue.withdrawalPending) || issue.decisionDate + )) { + saveButtonText = COPY.CORRECT_REQUEST_ISSUES_ESTABLISH; + } else { + saveButtonText = withdrawReview ? COPY.CORRECT_REQUEST_ISSUES_WITHDRAW : COPY.CORRECT_REQUEST_ISSUES_SAVE; + } const originalIssueNumberCopy = sprintf(COPY.CORRECT_REQUEST_ISSUES_ORIGINAL_NUMBER, this.state.originalIssueNumber, pluralize('issue', this.state.originalIssueNumber), this.props.state.addedIssues.length); @@ -211,6 +220,7 @@ SaveButtonUnconnected.propTypes = { receiptDate: PropTypes.string, requestIssuesUpdate: PropTypes.func, formType: PropTypes.string, + benefitType: PropTypes.string, claimId: PropTypes.string, history: PropTypes.object, state: PropTypes.shape({ @@ -222,6 +232,7 @@ const SaveButton = connect( (state) => ({ claimId: state.claimId, formType: state.formType, + benefitType: state.benefitType, addedIssues: state.addedIssues, originalIssues: state.originalIssues, requestStatus: state.requestStatus, diff --git a/client/app/intakeEdit/reducers/index.js b/client/app/intakeEdit/reducers/index.js index 2a3b055f9fb..fb8c64e89a4 100644 --- a/client/app/intakeEdit/reducers/index.js +++ b/client/app/intakeEdit/reducers/index.js @@ -38,6 +38,7 @@ export const mapDataToInitialState = function(props = {}) { userCanEditIntakeIssues, userCanSplitAppeal, isLegacy, + addDecisionDateModalVisible: false, addIssuesModalVisible: false, nonRatingRequestIssueModalVisible: false, unidentifiedIssuesModalVisible: false, diff --git a/client/app/nonComp/components/NonCompTabs.jsx b/client/app/nonComp/components/NonCompTabs.jsx index c3fe18f6043..00878b8c83a 100644 --- a/client/app/nonComp/components/NonCompTabs.jsx +++ b/client/app/nonComp/components/NonCompTabs.jsx @@ -24,7 +24,7 @@ const NonCompTabsUnconnected = (props) => { const queryParams = new URLSearchParams(window.location.search); const currentTabName = queryParams.get(QUEUE_CONFIG.TAB_NAME_REQUEST_PARAM) || 'in_progress'; - const defaultSortColumn = currentTabName === 'in_progress' ? 'daysWaitingColumn' : 'completedDateColumn'; + const defaultSortColumn = currentTabName === 'completed' ? 'completedDateColumn' : 'daysWaitingColumn'; const getParamsFilter = queryParams.getAll(`${QUEUE_CONFIG.FILTER_COLUMN_REQUEST_PARAM}[]`); // Read from the url get params and the local filter. The get params should override the local filter. const filter = getParamsFilter.length > 0 ? getParamsFilter : localFilter; @@ -36,40 +36,56 @@ const NonCompTabsUnconnected = (props) => { defaultSortColumn, [`${QUEUE_CONFIG.FILTER_COLUMN_REQUEST_PARAM}[]`]: filter, }; - const tabArray = ['in_progress', 'completed']; + const tabArray = props.businessLineConfig.tabs; // If additional tabs need to be added, include them in the array above // to be able to locate them by their index const findTab = tabArray.findIndex((tabName) => tabName === currentTabName); const getTabByIndex = findTab === -1 ? 0 : findTab; - const tabs = [{ - label: 'In progress tasks', - page: - }, { - label: 'Completed tasks', - page: - }]; + const ALL_TABS = { + incomplete: { + label: 'Incomplete tasks', + page: + }, + in_progress: { + label: 'In progress tasks', + page: + }, + completed: { + label: 'Completed tasks', + page: + } + }; + + const tabs = Object.keys(ALL_TABS). + filter((key) => props.businessLineConfig.tabs.includes(key)). + map((key) => ALL_TABS[key]); return ( { + + const tabs = (typeValue) => { + if (typeValue === 'vha') { + return ['incomplete', 'in_progress', 'completed']; + } + + return ['in_progress', 'completed']; + + }; + const props = { serverNonComp: { featureToggles: { decisionReviewQueueSsnColumn: options.args.decisionReviewQueueSsnColumn }, - businessLineUrl: 'vha', + businessLineUrl: options.args.businessLineType || 'vha', baseTasksUrl: '/decision_reviews/vha', + businessLineConfig: { + tabs: tabs(options.args.businessLineType) + }, taskFilterDetails: { + incomplete: {}, in_progress: { '["BoardGrantEffectuationTask", "Appeal"]': 1, '["DecisionReviewTask", "HigherLevelReview"]': 10, @@ -39,6 +53,7 @@ const ReduxDecorator = (Story, options) => { 'Prosthetics | Other (not clothing allowance)': 12, 'Spina Bifida Treatment (Non-Compensation)': 10 }, + incomplete_issue_types: {}, completed_issue_types: {} } } @@ -60,8 +75,14 @@ export default { parameters: {}, args: defaultArgs, argTypes: { - decisionReviewQueueSsnColumn: { control: 'boolean' } - }, + decisionReviewQueueSsnColumn: { control: 'boolean' }, + businessLineType: { + control: { + type: 'select', + options: ['vha', 'generic'], + }, + } + } }; const Template = (args) => { diff --git a/client/app/nonComp/components/TaskTableTab.jsx b/client/app/nonComp/components/TaskTableTab.jsx index c76c6180645..14cb9764d69 100644 --- a/client/app/nonComp/components/TaskTableTab.jsx +++ b/client/app/nonComp/components/TaskTableTab.jsx @@ -20,6 +20,8 @@ import { extractEnabledTaskFilters, parseFilterOptions, } from '../util/index'; +import pluralize from 'pluralize'; +import { snakeCase } from 'lodash'; class TaskTableTabUnconnected extends React.PureComponent { constructor(props) { @@ -35,6 +37,7 @@ class TaskTableTabUnconnected extends React.PureComponent { predefinedColumns: this.props.predefinedColumns, searchText, searchValue: searchText, + tabName: this.props.tabName }; } @@ -55,8 +58,23 @@ class TaskTableTabUnconnected extends React.PureComponent { this.setState({ searchText: '', searchValue: '' }); }; + claimantColumnHelper = () => { + const { tabName } = this.state; + const claimantColumnObject = claimantColumn(); + + if (tabName === 'incomplete') { + claimantColumnObject.valueFunction = (task) => { + const claimType = pluralize(snakeCase(task.appeal.type)); + + return {task.claimant.name}; + }; + } + + return claimantColumnObject; + }; + getTableColumns = () => [ - claimantColumn(), + this.claimantColumnHelper(), { ...decisionReviewTypeColumn(), ...buildDecisionReviewFilterInformation( @@ -133,6 +151,7 @@ TaskTableTabUnconnected.propTypes = { filterableTaskTypes: PropTypes.object, filterableTaskIssueTypes: PropTypes.object, onHistoryUpdate: PropTypes.func, + tabName: PropTypes.string }; const TaskTableTab = connect( diff --git a/client/app/nonComp/pages/TaskPage.jsx b/client/app/nonComp/pages/TaskPage.jsx index 628f3d5e93d..e8d4081c986 100644 --- a/client/app/nonComp/pages/TaskPage.jsx +++ b/client/app/nonComp/pages/TaskPage.jsx @@ -16,7 +16,9 @@ class TaskPageUnconnected extends React.PureComponent { handleSave = (data) => { const successHandler = () => { // update to the completed tab - this.props.taskUpdateDefaultPage(1); + const completedTabIndex = this.props.businessLineConfig?.tabs?.indexOf('completed') || 1; + + this.props.taskUpdateDefaultPage(completedTabIndex); this.props.history.push(`/${this.props.businessLineUrl}`); }; @@ -113,7 +115,8 @@ TaskPageUnconnected.propTypes = { history: PropTypes.shape({ push: PropTypes.func }), - businessLineUrl: PropTypes.string + businessLineUrl: PropTypes.string, + businessLineConfig: PropTypes.shape({ tabs: PropTypes.array }), }; const TaskPage = connect( @@ -121,6 +124,7 @@ const TaskPage = connect( appeal: state.appeal, businessLine: state.businessLine, businessLineUrl: state.businessLineUrl, + businessLineConfig: state.businessLineConfig, task: state.task, decisionIssuesStatus: state.decisionIssuesStatus }), diff --git a/client/package.json b/client/package.json index cc2ba8fbd77..02832d2f8f0 100644 --- a/client/package.json +++ b/client/package.json @@ -57,7 +57,7 @@ "@babel/preset-react": "^7.12.7", "@babel/register": "^7.12.1", "@babel/runtime-corejs2": "^7.12.5", - "@department-of-veterans-affairs/caseflow-frontend-toolkit": "https://github.com/department-of-veterans-affairs/caseflow-frontend-toolkit#dfe37c9", + "@department-of-veterans-affairs/caseflow-frontend-toolkit": "https://github.com/department-of-veterans-affairs/caseflow-frontend-toolkit#f4669ed", "@fortawesome/fontawesome-free": "^5.3.1", "@hookform/resolvers": "^2.0.0-beta.5", "@reduxjs/toolkit": "^1.4.0", diff --git a/client/test/app/intake/NonratingRequestIssueModal-test.js b/client/test/app/intake/NonratingRequestIssueModal-test.js index a44a644e814..b9637888dbc 100644 --- a/client/test/app/intake/NonratingRequestIssueModal-test.js +++ b/client/test/app/intake/NonratingRequestIssueModal-test.js @@ -7,22 +7,39 @@ import { sample1 } from './testData'; describe('NonratingRequestIssueModal', () => { const formType = 'higher_level_review'; const intakeData = sample1.intakeData; + const featureTogglesEMOPreDocket = { eduPreDocketAppeals: true }; + + const wrapper = mount( + null} + featureToggles={{}} + /> + ); + + const wrapperNoSkip = mount( + + ); + + const wrapperEMOPreDocket = mount( + + ); describe('renders', () => { - it('renders button text', () => { - const wrapper = mount( - null} - featureToggles={{}} - /> - ); - - const cancelBtn = wrapper.find('.cf-modal-controls .close-modal'); - const skipBtn = wrapper.find('.cf-modal-controls .no-matching-issues'); - const submitBtn = wrapper.find('.cf-modal-controls .add-issue'); + const cancelBtn = wrapper.find('.cf-modal-controls .close-modal'); + const skipBtn = wrapper.find('.cf-modal-controls .no-matching-issues'); + const submitBtn = wrapper.find('.cf-modal-controls .add-issue'); + it('renders button text', () => { expect(cancelBtn.text()).toBe('Cancel adding this issue'); expect(skipBtn.text()).toBe('None of these match, see more options'); expect(submitBtn.text()).toBe('Add this issue'); @@ -39,36 +56,23 @@ describe('NonratingRequestIssueModal', () => { }); it('skip button only with onSkip prop', () => { - const wrapper = mount( - ); + expect(wrapperNoSkip.find('.cf-modal-controls .no-matching-issues').exists()).toBe(false); - expect(wrapper.find('.cf-modal-controls .no-matching-issues').exists()).toBe(false); + wrapperNoSkip.setProps({ onSkip: () => null }); - wrapper.setProps({ onSkip: () => null }); - expect(wrapper.find('.cf-modal-controls .no-matching-issues').exists()).toBe(true); + expect(wrapperNoSkip.find('.cf-modal-controls .no-matching-issues').exists()).toBe(true); }); it('disables button when nothing selected', () => { - const wrapper = mount( - - ); - - const submitBtn = wrapper.find('.cf-modal-controls .add-issue'); - expect(submitBtn.prop('disabled')).toBe(true); // Lots of things required for button to be enabled... wrapper.setState({ benefitType: 'compensation', - category: { label: 'Apportionment', - value: 'Apportionment' }, + category: { + label: 'Apportionment', + value: 'Apportionment' + }, decisionDate: '06/01/2019', dateError: false, description: 'thing' @@ -79,59 +83,76 @@ describe('NonratingRequestIssueModal', () => { }); describe('on appeal, with EMO Pre-Docket', () => { - const featureTogglesEMOPreDocket = {eduPreDocketAppeals: true }; + const preDocketRadioField = wrapperEMOPreDocket.find('.cf-is-predocket-needed'); + + wrapperEMOPreDocket.setState({ + benefitType: 'education', + category: { + label: 'accrued', + value: 'accrued' + }, + decisionDate: '03/30/2022', + dateError: false, + description: 'thing', + isPreDocketNeeded: null + }); it(' enabled selecting benefit type of "education" renders PreDocketRadioField', () => { - const wrapper = mount( - - ); - // Benefit type isn't education, so it should not be rendered - expect(wrapper.find('.cf-is-predocket-needed')).toHaveLength(0); + expect(preDocketRadioField).toHaveLength(0); - wrapper.setState({ + wrapperEMOPreDocket.setState({ benefitType: 'education' }); // Benefit type is now education, so it should be rendered - expect(wrapper.find('.cf-is-predocket-needed')).toHaveLength(1); + expect(wrapperEMOPreDocket.find('.cf-is-predocket-needed')).toHaveLength(1); }); - it('submit button is disabled with Education benefit_type if pre-docket selection is empty', () => { - const wrapper = mount( - - ); + it('Decision date does not have an optional label ', () => { + // Since this is a non vha benifit type the decision date is required, so the optional label should not be visible + const optionalLabel = wrapperEMOPreDocket.find('.decision-date .cf-optional'); + const submitButton = wrapperEMOPreDocket.find('.cf-modal-controls .add-issue'); - // Switch to an Education issue, but don't fill in pre-docket field - wrapper.setState({ - benefitType: 'education', - category: { - label: 'accrued', - value: 'accrued' - }, - decisionDate: '03/30/2022', - dateError: false, - description: 'thing', - isPreDocketNeeded: null - }); + expect(optionalLabel).not.toBe(); + expect(submitButton.prop('disabled')).toBe(true); + }); - const submitBtn = wrapper.find('.cf-modal-controls .add-issue'); + it('submit button is disabled with Education benefit_type if pre-docket selection is empty', () => { + // Switch to an Education issue, but don't fill in pre-docket field + const submitBtn = wrapperEMOPreDocket.find('.cf-modal-controls .add-issue'); expect(submitBtn.prop('disabled')).toBe(true); // Fill in pre-docket field to make sure the submit button gets enabled // Note that the radio field values are strings. - wrapper.setState({ + wrapperEMOPreDocket.setState({ isPreDocketNeeded: 'false' }); - expect(wrapper.find('.cf-modal-controls .add-issue').prop('disabled')).toBe(false); + expect(wrapperEMOPreDocket.find('.cf-modal-controls .add-issue').prop('disabled')).toBe(false); + }); + }); + + describe('on higher level review, with VHA benefit type', () => { + wrapperNoSkip.setState({ + benefitType: 'vha', + category: { + label: 'Beneficiary Travel', + value: 'Beneficiary Travel' + }, + description: 'test' + }); + + const optionalLabel = wrapperNoSkip.find('.decision-date .cf-optional'); + const submitButton = wrapperNoSkip.find('.cf-modal-controls .add-issue'); + + it('renders modal with decision date field being optional', () => { + expect(optionalLabel.text()).toBe('Optional'); + }); + + it('submit button is enabled without a decision date entered', () => { + expect(submitButton.prop('disabled')).toBe(false); }); }); }); diff --git a/client/test/app/intake/testData.js b/client/test/app/intake/testData.js index 2982541058f..c101b1690c5 100644 --- a/client/test/app/intake/testData.js +++ b/client/test/app/intake/testData.js @@ -875,6 +875,7 @@ export const sample1 = { useAmaActivationDate: true, correctClaimReviews: true, }, + addDecisionDateModalVisible: false, addIssuesModalVisible: false, nonRatingRequestIssueModalVisible: false, unidentifiedIssuesModalVisible: false, 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 d2445065ab6..808c450cfb9 100644 --- a/client/test/app/intake/util/__snapshots__/issues.test.js.snap +++ b/client/test/app/intake/util/__snapshots__/issues.test.js.snap @@ -50,6 +50,7 @@ Array [ "decisionIssueId": null, "decisionReviewTitle": "Appeal", "editable": true, + "editedDecisionDate": undefined, "editedDescription": undefined, "eligibleForSocOptIn": undefined, "eligibleForSocOptInWithExemption": undefined, @@ -130,6 +131,7 @@ Array [ "decisionIssueId": null, "decisionReviewTitle": "Appeal", "editable": true, + "editedDecisionDate": undefined, "editedDescription": undefined, "eligibleForSocOptIn": undefined, "eligibleForSocOptInWithExemption": undefined, diff --git a/client/test/app/nonComp/NonCompTabs.test.js b/client/test/app/nonComp/NonCompTabs.test.js index 1b6919641bd..0d656b6d076 100644 --- a/client/test/app/nonComp/NonCompTabs.test.js +++ b/client/test/app/nonComp/NonCompTabs.test.js @@ -4,24 +4,43 @@ import { Provider } from 'react-redux'; import { createStore } from 'redux'; import '@testing-library/jest-dom'; -import { taskFilterDetails } from '../../data/taskFilterDetails'; +import { vhaTaskFilterDetails, genericTaskFilterDetails } from '../../data/taskFilterDetails'; import NonCompTabsUnconnected from 'app/nonComp/components/NonCompTabs'; import ApiUtil from '../../../app/util/ApiUtil'; +import { VHA_INCOMPLETE_TAB_DESCRIPTION } from '../../../COPY'; -const basicProps = { +const basicVhaProps = { businessLine: 'Veterans Health Administration', businessLineUrl: 'vha', baseTasksUrl: '/decision_reviews/vha', selectedTask: null, decisionIssuesStatus: {}, - taskFilterDetails, + taskFilterDetails: vhaTaskFilterDetails, featureToggles: { decisionReviewQueueSsnColumn: true }, + businessLineConfig: { + tabs: ['incomplete', 'in_progress', 'completed'] + }, +}; + +const basicGenericProps = { + businessLine: 'Generic', + businessLineUrl: 'generic', + baseTasksUrl: '/decision_reviews/generic', + selectedTask: null, + decisionIssuesStatus: {}, + taskFilterDetails: genericTaskFilterDetails, + featureToggles: { + decisionReviewQueueSsnColumn: true + }, + businessLineConfig: { + tabs: ['in_progress', 'completed'] + }, }; beforeEach(() => { - jest.clearAllMocks(); + // jest.clearAllMocks(); // Mock ApiUtil get so the tasks will appear in the queues. ApiUtil.get = jest.fn().mockResolvedValue({ @@ -63,9 +82,9 @@ const checkFilterableHeaders = (expectedHeaders) => { }); }; -const renderNonCompTabs = () => { +const renderNonCompTabs = (props) => { - const nonCompTabsReducer = createReducer(basicProps); + const nonCompTabsReducer = createReducer(props); const store = createStore(nonCompTabsReducer); @@ -80,15 +99,87 @@ afterEach(() => { jest.clearAllMocks(); }); -describe('NonCompTabs', () => { +describe('NonCompTabsVha', () => { beforeEach(() => { - renderNonCompTabs(basicProps); + renderNonCompTabs(basicVhaProps); }); it('renders a tab titled "In progress tasks"', () => { + expect(screen.getAllByText('In progress tasks')).toBeTruthy(); + // Check for the correct in progress tasks header values + const expectedHeaders = ['Claimant', 'Veteran SSN', 'Issues', 'Issue Type', 'Days Waiting', 'Type']; + const sortableHeaders = expectedHeaders.filter((header) => header !== 'Type'); + const filterableHeaders = ['type', 'issue type']; + + checkTableHeaders(expectedHeaders); + checkSortableHeaders(sortableHeaders); + checkFilterableHeaders(filterableHeaders); + + }); + + it('renders a tab titled "Incomplete tasks"', async () => { + expect(screen.getAllByText('Incomplete tasks')).toBeTruthy(); + + const tabs = screen.getAllByRole('tab'); + + await tabs[0].click(); + + await waitFor(() => { + expect(screen.getByText(VHA_INCOMPLETE_TAB_DESCRIPTION)).toBeInTheDocument(); + }); + + // Check for the correct completed tasks header values + const expectedHeaders = ['Claimant', 'Veteran SSN', 'Issues', 'Issue Type', 'Days Waiting', 'Type']; + const sortableHeaders = expectedHeaders.filter((header) => header !== 'Type'); + const filterableHeaders = ['type', 'issue type']; + + checkTableHeaders(expectedHeaders); + checkSortableHeaders(sortableHeaders); + checkFilterableHeaders(filterableHeaders); + + }); + + it('renders a tab titled "Completed tasks"', async () => { + + expect(screen.getAllByText('Completed tasks')).toBeTruthy(); + + const tabs = screen.getAllByRole('tab'); + + await tabs[2].click(); + + await waitFor(() => { + expect(screen.getByText('Cases completed (last 7 days):')).toBeInTheDocument(); + }); + + // Check for the correct completed tasks header values + const expectedHeaders = ['Claimant', 'Veteran SSN', 'Issues', 'Issue Type', 'Date Completed', 'Type']; + const sortableHeaders = expectedHeaders.filter((header) => header !== 'Type'); + const filterableHeaders = ['type', 'issue type']; + + checkTableHeaders(expectedHeaders); + checkSortableHeaders(sortableHeaders); + checkFilterableHeaders(filterableHeaders); + + }); +}); + +describe('NonCompTabsGeneric', () => { + beforeEach(() => { + renderNonCompTabs(basicGenericProps); + }); + + it('renders a tab titled "In progress tasks"', async () => { expect(screen.getAllByText('In progress tasks')).toBeTruthy(); + const tabs = screen.getAllByRole('tab'); + + // The async from the first describe block is interferring with this test so wait for the tab to reload apparently. + await tabs[0].click(); + await waitFor(() => { + expect(screen.getByText('Days Waiting')).toBeInTheDocument(); + }); + // Check for the correct in progress tasks header values const expectedHeaders = ['Claimant', 'Veteran SSN', 'Issues', 'Issue Type', 'Days Waiting', 'Type']; const sortableHeaders = expectedHeaders.filter((header) => header !== 'Type'); @@ -100,6 +191,10 @@ describe('NonCompTabs', () => { }); + it('does not render a tab titled "Incomplete tasks"', () => { + expect(screen.queryByText('Incomplete tasks')).toBeNull(); + }); + it('renders a tab titled "Completed tasks"', async () => { expect(screen.getAllByText('Completed tasks')).toBeTruthy(); diff --git a/client/test/app/nonComp/util/index.test.js b/client/test/app/nonComp/util/index.test.js index 25fb553931f..6cbc04d7942 100644 --- a/client/test/app/nonComp/util/index.test.js +++ b/client/test/app/nonComp/util/index.test.js @@ -1,11 +1,11 @@ -import { taskFilterDetails } from '../../../data/taskFilterDetails'; +import { vhaTaskFilterDetails } from '../../../data/taskFilterDetails'; import { buildDecisionReviewFilterInformation } from 'app/nonComp/util/index'; const subject = (filterData) => buildDecisionReviewFilterInformation(filterData); describe('Parsing filter data', () => { it('From in progress tasks', () => { - const results = subject(taskFilterDetails.in_progress); + const results = subject(vhaTaskFilterDetails.in_progress); expect(results.filterOptions).toEqual([ { @@ -32,7 +32,7 @@ describe('Parsing filter data', () => { }); it('From completed tasks', () => { - const results = subject(taskFilterDetails.completed); + const results = subject(vhaTaskFilterDetails.completed); expect(results.filterOptions).toEqual([ { diff --git a/client/test/data/taskFilterDetails.js b/client/test/data/taskFilterDetails.js index af3313a3ff5..d4a196a4171 100644 --- a/client/test/data/taskFilterDetails.js +++ b/client/test/data/taskFilterDetails.js @@ -1,4 +1,4 @@ -export const taskFilterDetails = { +export const vhaTaskFilterDetails = { in_progress: { '["BoardGrantEffectuationTask", "Appeal"]': 6, '["DecisionReviewTask", "HigherLevelReview"]': 330, @@ -10,6 +10,7 @@ export const taskFilterDetails = { '["DecisionReviewTask", "SupplementalClaim"]': 15, '["VeteranRecordRequest", "Appeal"]': 3 }, + incomplete: {}, in_progress_issue_types: { CHAMPVA: 12, 'Caregiver | Tier Level': 20, @@ -45,5 +46,13 @@ export const taskFilterDetails = { 'Caregiver | Other': 9, 'Foreign Medical Program': 13, 'Camp Lejune Family Member': 8 - } + }, + incomplete_issue_types: {}, +}; + +export const genericTaskFilterDetails = { + in_progress: {}, + in_progress_issue_types: {}, + completed: {}, + completed_issue_types: {}, }; diff --git a/client/yarn.lock b/client/yarn.lock index 494f936b028..2990f524e21 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2529,9 +2529,9 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@department-of-veterans-affairs/caseflow-frontend-toolkit@https://github.com/department-of-veterans-affairs/caseflow-frontend-toolkit#dfe37c9": +"@department-of-veterans-affairs/caseflow-frontend-toolkit@https://github.com/department-of-veterans-affairs/caseflow-frontend-toolkit#f4669ed": version "2.6.1" - resolved "https://github.com/department-of-veterans-affairs/caseflow-frontend-toolkit#dfe37c98bd79c1befdf4ca306d537611a33b846d" + resolved "https://github.com/department-of-veterans-affairs/caseflow-frontend-toolkit#f4669edf624409da3e88b23fa7b64a1f716b7876" dependencies: classnames "^2.2.5" glamor "^2.20.40" diff --git a/db/migrate/20230725182732_update_vha_business_line_type.rb b/db/migrate/20230725182732_update_vha_business_line_type.rb new file mode 100644 index 00000000000..4eb40d27059 --- /dev/null +++ b/db/migrate/20230725182732_update_vha_business_line_type.rb @@ -0,0 +1,11 @@ +class UpdateVhaBusinessLineType < Caseflow::Migration + def up + # Update the business line record with url = 'vha' to set their type to 'VhaBusinessLine' + BusinessLine.where(url: 'vha').update_all(type: 'VhaBusinessLine') + end + + def down + # Revert the business line record with url = 'vha' to set their type back to 'BusinessLine' + BusinessLine.where(url: 'vha').update_all(type: 'BusinessLine') + end +end diff --git a/db/seeds/sanitized_business_line_json/business_line.json b/db/seeds/sanitized_business_line_json/business_line.json index 8f9e1c18848..fc398175ab9 100644 --- a/db/seeds/sanitized_business_line_json/business_line.json +++ b/db/seeds/sanitized_business_line_json/business_line.json @@ -47,7 +47,7 @@ }, { "id": 222, - "type": "BusinessLine", + "type": "VhaBusinessLine", "name": "Veterans Health Administration", "role": null, "url": "vha", diff --git a/db/seeds/sanitized_json/appeal-119577.json b/db/seeds/sanitized_json/appeal-119577.json index fbb9493809e..d0e708ba01b 100644 --- a/db/seeds/sanitized_json/appeal-119577.json +++ b/db/seeds/sanitized_json/appeal-119577.json @@ -6096,7 +6096,7 @@ }, { "id": 222, - "type": "BusinessLine", + "type": "VhaBusinessLine", "name": "Veterans Health Administration", "role": null, "url": "vha", diff --git a/db/seeds/sanitized_json/appeal-126187.json b/db/seeds/sanitized_json/appeal-126187.json index ced57887593..5749397ed0e 100644 --- a/db/seeds/sanitized_json/appeal-126187.json +++ b/db/seeds/sanitized_json/appeal-126187.json @@ -6378,7 +6378,7 @@ }, { "id": 222, - "type": "BusinessLine", + "type": "VhaBusinessLine", "name": "Veterans Health Administration", "role": null, "url": "vha", diff --git a/db/seeds/veterans_health_administration.rb b/db/seeds/veterans_health_administration.rb index 100ff0baabb..719086cdc19 100644 --- a/db/seeds/veterans_health_administration.rb +++ b/db/seeds/veterans_health_administration.rb @@ -209,7 +209,8 @@ def add_vha_user_to_be_vha_business_line_member .where("o.type like ?", "Vha%") .distinct # organization = BusinessLine.where(name:) - organization = Organization.find_by_name_or_url("Veterans Health Administration") + # organization = Organization.find_by_name_or_url("Veterans Health Administration") + organization = VhaBusinessLine.singleton user_list.each do |user| organization.add_user(user) end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 96d365ab2f7..44d392ed3cf 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -63,7 +63,7 @@ def index describe "#application_urls" do let(:user) { build(:user, roles: ["Mail Intake", "Case Details"]) } - let(:vha_org) { build(:business_line, url: "vha", name: "Veterans Health Administration") } + let(:vha_org) { build(:business_line, url: "vha", name: "Veterans Health Administration", type: "VhaBusinessLine") } before { vha_org.add_user(user) } it "should not return queue link if user is a part of VHA org and has role 'Case Details' " do expect(subject.send(:application_urls)).not_to include(subject.send(:queue_application_url)) diff --git a/spec/controllers/decision_reviews_controller_spec.rb b/spec/controllers/decision_reviews_controller_spec.rb index 34a0fe1717b..1c13f1adb3f 100644 --- a/spec/controllers/decision_reviews_controller_spec.rb +++ b/spec/controllers/decision_reviews_controller_spec.rb @@ -263,6 +263,25 @@ end end + # Throw in some on hold tasks as well to make sure generic businessline in progress includes on_hold tasks + let!(:on_hold_hlr_tasks) do + (0...20).map do |task_num| + task = create( + :higher_level_review_task, + assigned_to: non_comp_org, + assigned_at: task_num.minutes.ago + ) + task.on_hold! + task.appeal.update!(veteran_file_number: veteran.file_number) + create(:request_issue, :nonrating, decision_review: task.appeal, benefit_type: non_comp_org.url) + + # Generate some random request issues for testing issue type filters + generate_request_issues(task, non_comp_org) + + task + end + end + let!(:in_progress_sc_tasks) do (0...32).map do |task_num| task = create( @@ -412,7 +431,7 @@ } end - let(:in_progress_tasks) { in_progress_hlr_tasks + in_progress_sc_tasks } + let(:in_progress_tasks) { in_progress_hlr_tasks + on_hold_hlr_tasks + in_progress_sc_tasks } include_examples "task query filtering" include_examples "issue type query filtering" @@ -425,30 +444,30 @@ expect(response.status).to eq(200) response_body = JSON.parse(response.body) - expect(response_body["total_task_count"]).to eq 64 + expect(response_body["total_task_count"]).to eq 84 expect(response_body["tasks_per_page"]).to eq 15 - expect(response_body["task_page_count"]).to eq 5 + expect(response_body["task_page_count"]).to eq 6 expect( task_ids_from_response_body(response_body) ).to match_array task_ids_from_seed(in_progress_tasks, (0...15), :assigned_at) end - it "page 5 displays last 4 tasks" do - query_params[:page] = 5 + it "page 6 displays last 9 tasks" do + query_params[:page] = 6 subject expect(response.status).to eq(200) response_body = JSON.parse(response.body) - expect(response_body["total_task_count"]).to eq 64 + expect(response_body["total_task_count"]).to eq 84 expect(response_body["tasks_per_page"]).to eq 15 - expect(response_body["task_page_count"]).to eq 5 + expect(response_body["task_page_count"]).to eq 6 expect( task_ids_from_response_body(response_body) - ).to match_array task_ids_from_seed(in_progress_tasks, (-4..in_progress_tasks.size), :assigned_at) + ).to match_array task_ids_from_seed(in_progress_tasks, (-9..in_progress_tasks.size), :assigned_at) end end @@ -500,6 +519,105 @@ end end + context "vha org incomplete_tasks" do + let(:non_comp_org) { VhaBusinessLine.singleton } + + context "incomplete_tasks" do + let(:query_params) do + { + business_line_slug: non_comp_org.url, + tab: "incomplete" + } + end + + let!(:on_hold_sc_tasks) do + (0...20).map do |task_num| + task = create( + :supplemental_claim_task, + assigned_to: non_comp_org, + assigned_at: task_num.hours.ago + ) + task.on_hold! + task.appeal.update!(veteran_file_number: veteran.file_number) + create(:request_issue, :nonrating, decision_review: task.appeal, benefit_type: non_comp_org.url) + + # Generate some random request issues for testing issue type filters + generate_request_issues(task, non_comp_org) + + task + end + end + + let(:incomplete_tasks) { on_hold_hlr_tasks + on_hold_sc_tasks } + + include_examples "task query filtering" + include_examples "issue type query filtering" + + it "page 1 displays first 15 tasks" do + query_params[:page] = 1 + + subject + + expect(response.status).to eq(200) + response_body = JSON.parse(response.body) + + expect(response_body["total_task_count"]).to eq 40 + expect(response_body["tasks_per_page"]).to eq 15 + expect(response_body["task_page_count"]).to eq 3 + + expect( + task_ids_from_response_body(response_body) + ).to match_array task_ids_from_seed(incomplete_tasks, (0...15), :assigned_at) + end + + it "page 3 displays last 10 tasks" do + query_params[:page] = 3 + + subject + + expect(response.status).to eq(200) + response_body = JSON.parse(response.body) + + expect(response_body["total_task_count"]).to eq 40 + expect(response_body["tasks_per_page"]).to eq 15 + expect(response_body["task_page_count"]).to eq 3 + + expect( + task_ids_from_response_body(response_body) + ).to match_array task_ids_from_seed(incomplete_tasks, (-10..incomplete_tasks.size), :assigned_at) + end + end + + context "in_progress_tasks" do + let(:query_params) do + { + business_line_slug: non_comp_org.url, + tab: "in_progress" + } + end + + # The Vha Businessline in_progress should not include on_hold since it uses active for the tasks query + let(:in_progress_tasks) { in_progress_hlr_tasks + in_progress_sc_tasks } + + it "page 1 displays first 15 tasks" do + query_params[:page] = 1 + + subject + + expect(response.status).to eq(200) + response_body = JSON.parse(response.body) + + expect(response_body["total_task_count"]).to eq 64 + expect(response_body["tasks_per_page"]).to eq 15 + expect(response_body["task_page_count"]).to eq 5 + + expect( + task_ids_from_response_body(response_body) + ).to match_array task_ids_from_seed(in_progress_tasks, (0...15), :assigned_at) + end + end + end + it "throws 404 error if unrecognized tab name is provided" do get :index, params: { diff --git a/spec/controllers/intakes_controller_spec.rb b/spec/controllers/intakes_controller_spec.rb index c53ce434273..a02cd6b6151 100644 --- a/spec/controllers/intakes_controller_spec.rb +++ b/spec/controllers/intakes_controller_spec.rb @@ -258,6 +258,39 @@ expect(flash[:success]).to be_present end end + + context "when intaking a vha processed_in_caseflow AMA HLR/SC with a missing decision date" do + let(:veteran) { create(:veteran) } + let(:request_issue_params) do + [ + { + "benefit_type" => "vha", + "nonrating_issue_category" => "Beneficiary Travel", + "decision_text" => "Beneficiary testing", + "decision_date" => "", + "ineligible_due_to_id" => nil, + "ineligible_reason" => nil, + "withdrawal_date" => nil, + "is_predocket_needed" => nil + } + ] + end + + it "should return a JSON payload with a redirect_to path to the incomplete tab and the task should be on hold" do + intake = create(:intake, + user: current_user, + detail: create(:higher_level_review, + benefit_type: "vha", + veteran_file_number: veteran.file_number)) + + post :complete, params: { id: intake.id, request_issues: request_issue_params } + resp = JSON.parse(response.body, symbolize_names: true) + + expect(resp[:serverIntake]).to eq(redirect_to: "/decision_reviews/vha?tab=incomplete") + expect(flash[:success]).to be_present + expect(intake.reload.detail.reload.tasks.first.status).to eq("on_hold") + end + end end describe "#attorneys" do diff --git a/spec/controllers/membership_requests_controller_spec.rb b/spec/controllers/membership_requests_controller_spec.rb index 5d82a00716a..6984a36bd51 100644 --- a/spec/controllers/membership_requests_controller_spec.rb +++ b/spec/controllers/membership_requests_controller_spec.rb @@ -6,7 +6,7 @@ let(:requestor) { create(:user, css_id: "REQUESTOR1", email: "requestoremail@test.com", full_name: "Gaius Baelsar") } let(:camo_admin) { create(:user, css_id: "CAMO ADMIN", email: "camoemail@test.com", full_name: "CAMO ADMIN") } let(:camo_org) { VhaCamo.singleton } - let(:vha_business_line) { BusinessLine.find_by(url: "vha") } + let(:vha_business_line) { VhaBusinessLine.singleton } let(:existing_org) { create(:organization, name: "Testing Adverse Affects", url: "adverse-1") } let(:camo_membership_request) { create(:membership_request, organization: camo_org, requestor: requestor) } let(:vha_membership_request) { create(:membership_request, organization: vha_business_line, requestor: requestor) } @@ -229,7 +229,7 @@ def create_vha_orgs VhaCamo.singleton - create(:business_line, name: "Veterans Health Administration", url: "vha") + VhaBusinessLine.singleton VhaCaregiverSupport.singleton create(:vha_program_office, name: "Community Care - Veteran and Family Members Program", diff --git a/spec/factories/task.rb b/spec/factories/task.rb index f6e58563316..79f888a9b16 100644 --- a/spec/factories/task.rb +++ b/spec/factories/task.rb @@ -324,15 +324,15 @@ def self.find_first_task_or_create(appeal, task_type, **kwargs) end factory :higher_level_review_vha_task, class: DecisionReviewTask do - appeal { create(:higher_level_review, :with_vha_issue, benefit_type: "vha") } + appeal { create(:higher_level_review, :with_vha_issue, benefit_type: "vha", claimant_type: :veteran_claimant) } assigned_by { nil } - assigned_to { BusinessLine.where(name: "Veterans Health Administration").first } + assigned_to { VhaBusinessLine.singleton } end factory :supplemental_claim_vha_task, class: DecisionReviewTask do - appeal { create(:supplemental_claim, :with_vha_issue, benefit_type: "vha") } + appeal { create(:supplemental_claim, :with_vha_issue, benefit_type: "vha", claimant_type: :veteran_claimant) } assigned_by { nil } - assigned_to { BusinessLine.where(name: "Veterans Health Administration").first } + assigned_to { VhaBusinessLine.singleton } end factory :distribution_task, class: DistributionTask do diff --git a/spec/feature/help/vha_membership_request_spec.rb b/spec/feature/help/vha_membership_request_spec.rb index be989737c31..a5aa6047587 100644 --- a/spec/feature/help/vha_membership_request_spec.rb +++ b/spec/feature/help/vha_membership_request_spec.rb @@ -21,9 +21,7 @@ let(:camo_org) { VhaCamo.singleton } let(:caregiver_org) { VhaCaregiverSupport.singleton } let(:vha_org) do - org = BusinessLine.find_or_create_by(name: "Veterans Health Administration", url: "vha") - org.save - org + VhaBusinessLine.singleton end let(:prosthetics_org) do org = VhaProgramOffice.find_or_create_by(name: "Prosthetics", url: "prosthetics-url") diff --git a/spec/feature/help/vha_team_management_spec.rb b/spec/feature/help/vha_team_management_spec.rb index cbb5e747866..dda7bfee930 100644 --- a/spec/feature/help/vha_team_management_spec.rb +++ b/spec/feature/help/vha_team_management_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.feature "VhaTeamManagement" do - let(:vha_business_line) { create(:business_line, name: "Veterans Health Administration", url: "vha") } + let(:vha_business_line) { VhaBusinessLine.singleton } let(:camo_org) { VhaCamo.singleton } let(:vha_admin) { create(:user, full_name: "VHA ADMIN", css_id: "VHA_ADMIN") } diff --git a/spec/feature/intake/higher_level_review_spec.rb b/spec/feature/intake/higher_level_review_spec.rb index 677f3bbcbfa..392b946ad2d 100644 --- a/spec/feature/intake/higher_level_review_spec.rb +++ b/spec/feature/intake/higher_level_review_spec.rb @@ -474,30 +474,6 @@ end end - context "when disabling claim establishment is enabled" do - before { FeatureToggle.enable!(:disable_claim_establishment) } - after { FeatureToggle.disable!(:disable_claim_establishment) } - - it "completes intake and prevents edit" do - start_higher_level_review(veteran_no_ratings) - visit "/intake" - click_intake_continue - click_intake_add_issue - add_intake_nonrating_issue( - category: "Active Duty Adjustments", - description: "Description for Active Duty Adjustments", - date: profile_date.mdY - ) - click_intake_finish - - expect(page).to have_content("#{Constants.INTAKE_FORM_NAMES.higher_level_review} has been submitted.") - - click_on "correct the issues" - - expect(page).to have_content("Review not editable") - end - end - it "Shows a review error when something goes wrong" do start_higher_level_review(veteran_no_ratings) visit "/intake" diff --git a/spec/feature/intake/review_page_spec.rb b/spec/feature/intake/review_page_spec.rb index 6e632ba07a0..808f5873195 100644 --- a/spec/feature/intake/review_page_spec.rb +++ b/spec/feature/intake/review_page_spec.rb @@ -491,7 +491,7 @@ def select_claimant(index = 0) end context "Current user is a member of the VHA business line" do - let(:vha_business_line) { create(:business_line, name: benefit_type_label, url: "vha") } + let(:vha_business_line) { VhaBusinessLine.singleton } let(:current_user) { create(:user, roles: ["Admin Intake"]) } before do diff --git a/spec/feature/intake/vha_hlr_sc_enter_no_decision_date_spec.rb b/spec/feature/intake/vha_hlr_sc_enter_no_decision_date_spec.rb new file mode 100644 index 00000000000..abd7d1491e0 --- /dev/null +++ b/spec/feature/intake/vha_hlr_sc_enter_no_decision_date_spec.rb @@ -0,0 +1,396 @@ +# frozen_string_literal: true + +feature "Vha Higher-Level Review and Supplemental Claims Enter No Decision Date", :all_dbs do + include IntakeHelpers + + let!(:current_user) do + create(:user, roles: ["Mail Intake"]) + end + + let(:veteran_file_number) { "123412345" } + + let(:veteran) do + Generators::Veteran.build(file_number: veteran_file_number, + first_name: "Ed", + last_name: "Merica") + end + + let(:changed_issue_banner_save_text) do + "When you finish making changes, click \"Save\" to continue." + end + + let(:changed_issue_banner_establish_text) do + "When you finish making changes, click \"Establish\" to continue." + end + + before do + VhaBusinessLine.singleton.add_user(current_user) + CaseReview.singleton.add_user(current_user) + current_user.save + User.authenticate!(user: current_user) + end + + shared_examples "Vha HLR/SC Issue without decision date" do + it "Allows Vha to intake, edit, and establish a claim review with an issue without a decision date" do + intake_type + + visit "/intake" + + click_intake_continue + click_intake_add_issue + add_intake_nonrating_issue( + category: "Beneficiary Travel", + description: "Travel for VA meeting", + date: nil + ) + + expect(page).to have_content("1 issue") + expect(page).to have_content("Decision date: No date entered") + expect(page).to have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + expect(page).to have_content(intake_button_text) + + click_intake_finish + + # On hold tasks should land on the incomplete tab + expect(page).to have_content(COPY::VHA_INCOMPLETE_TAB_DESCRIPTION) + expect(page).to have_content(success_message_text) + + # Verify that the task has a status of on_hold + task = DecisionReviewTask.last + expect(task.status).to eq("on_hold") + + # Click the link and check to make sure that we are now on the edit issues page + click_link veteran.name.to_s + + expect(page).to have_content("Edit Issues") + expect(page).to have_content("Decision date: No date entered") + expect(page).to have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + + expect(page).to have_button("Save", disabled: true) + + issue_id = RequestIssue.last.id + + # Click the first issue actions button and select Add a decision date + within "#issue-#{issue_id}" do + first("select").select("Add decision date") + end + + # Check modal text + expect(page).to have_content("Add Decision Date") + expect(page).to have_content("Issue:Beneficiary Travel") + expect(page).to have_content("Benefit type:Veterans Health Administration") + expect(page).to have_content("Issue description:Travel for VA meeting") + + future_date = (Time.zone.now + 1.week).strftime("%m/%d/%Y") + past_date = (Time.zone.now - 1.week).strftime("%m/%d/%Y") + another_past_date = (Time.zone.now - 2.weeks).strftime("%m/%d/%Y") + + fill_in "decision-date", with: future_date + + expect(page).to have_content("Dates cannot be in the future") + + # The button should be disabled since the date is in the future + within ".cf-modal-controls" do + expect(page).to have_button("Save", disabled: true) + end + + # Test the modal cancel button + within ".cf-modal-controls" do + click_on "Cancel" + end + + expect(page).to_not have_content("Add Decision Date") + + # Open the modal again + # Click the first issue actions button and select Add a decision date + within "#issue-#{issue_id}" do + first("select").select("Add decision date") + end + + expect(page).to have_content("Add Decision Date") + + fill_in "decision-date", with: past_date + + within ".cf-modal-controls" do + expect(page).to have_button("Save", disabled: false) + click_on("Save") + end + + # Test functionality for editing a decision date once one has been selected + # Click the first issue actions button and select Edit decision date + within "#issue-#{issue_id}" do + select("Edit decision date", from: "issue-action-0") + end + + formatted_past_date = (Time.zone.now - 1.week).strftime("%Y-%m-%d") + within ".cf-modal-body" do + expect(page).to have_content("Edit Decision Date") + expect(page).to have_field(type: "date", with: formatted_past_date) + end + + fill_in "decision-date", with: another_past_date + + within ".cf-modal-controls" do + expect(page).to have_button("Save", disabled: false) + click_on("Save") + end + + expect(page).to have_content(changed_issue_banner_establish_text) + + # Check that the Edit Issues save button is now Establish, the decision date is added, and the banner is gone + expect(page).to_not have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + expect(page).to have_content("Decision date: #{another_past_date}") + expect(page).to have_button("Establish", disabled: false) + + click_on("Establish") + + # the task should now be assigned and on the in progress tab + expect(page).to_not have_content(COPY::VHA_INCOMPLETE_TAB_DESCRIPTION) + expect(page).to have_content(edit_establish_success_message_text) + expect(current_url).to include("/decision_reviews/vha?tab=in_progress") + + expect(task.reload.status).to eq("assigned") + + # Test adding a new issue without decision date then adding one + # Click the links and get to the edit issues page + click_link veteran.name.to_s + click_link "Edit Issues" + expect(page).to have_content("Edit Issues") + + # Open Add Issues modal and add issue + click_on("Add issue") + + fill_in "Issue category", with: "Beneficiary Travel" + find("#issue-category").send_keys :enter + fill_in "Issue description", with: "Test description" + + expect(page).to have_button("Add this issue", disabled: false) + click_on("Add this issue") + + # Test that the banner and text is present for added issues with no decision dates + expect(page).to have_content("Decision date: No date entered") + expect(page).to have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + + # Edit the decision date for added issue + # this is issue-undefined because the issue has not yet been created and does not have an id + within "#issue-undefined" do + select("Add decision date", from: "issue-action-1") + end + + fill_in "decision-date", with: past_date + + within ".cf-modal-controls" do + expect(page).to have_button("Save", disabled: false) + click_on("Save") + end + + # Check that the date gets saved and shows establish for added issue + expect(page).to_not have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + expect(page).to have_content("Decision date: #{past_date}") + expect(page).to have_button("Establish", disabled: false) + + click_on("Establish") + expect(page).to have_content("Number of issues has changed") + click_on("Yes, save") + + expect(page).to have_content(edit_establish_success_message_text) + expect(current_url).to include("/decision_reviews/vha?tab=in_progress") + + expect(task.reload.status).to eq("assigned") + end + end + + shared_examples "Vha HLR/SC adding issue without decision date to existing claim review" do + it "Allows Vha to add an issue without a decision date to an existing claim review and remove the issue" do + visit edit_url + + expect(task.status).to eq("assigned") + expect(page).to have_button("Establish", disabled: true) + + click_intake_add_issue + add_intake_nonrating_issue( + category: "Beneficiary Travel", + description: "Travel for VA meeting", + date: nil + ) + + click_intake_add_issue + add_intake_nonrating_issue( + category: "CHAMPVA", + description: "CHAMPVA issue", + date: nil + ) + + click_intake_add_issue + add_intake_nonrating_issue( + category: "Clothing Allowance", + description: "Clothes for dependent", + date: nil + ) + + expect(page).to have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + + click_button "Save" + + expect(page).to have_content(COPY::CORRECT_REQUEST_ISSUES_CHANGED_MODAL_TITLE) + + click_button "Yes, save" + + expect(page).to have_content(COPY::VHA_INCOMPLETE_TAB_DESCRIPTION) + expect(current_url).to include("/decision_reviews/vha?tab=incomplete") + expect(page).to have_content(edit_save_success_message_text) + expect(task.reload.status).to eq("on_hold") + + # Go back to the Edit issues page + click_link task.appeal.veteran.name.to_s + + expect(page).to have_button("Save", disabled: true) + + expect(page).to have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + + # Add a decision date, remove an issue, and withdraw an issue + new_issues = task.appeal.request_issues.reload.select { |issue| issue.decision_date.blank? } + request_issue_id = new_issues.first.id + second_issue_id = new_issues.second.id + third_issue_id = new_issues.third.id + + within "#issue-#{request_issue_id}" do + first("select").select("Add decision date") + end + + fill_in "decision-date", with: (Time.zone.now - 1.week).strftime("%m/%d/%Y") + + within ".cf-modal-controls" do + expect(page).to have_button("Save", disabled: false) + click_on("Save") + end + + expect(page).to have_content(changed_issue_banner_save_text) + + click_button "Save" + + expect(page).to have_content(edit_decision_date_success_message_text) + expect(current_url).to include("/decision_reviews/vha?tab=incomplete") + expect(task.reload.status).to eq("on_hold") + + # Go back to the Edit issues page + click_link task.appeal.veteran.name.to_s + + expect(page).to have_button("Save", disabled: true) + expect(page).to have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + + within "#issue-#{second_issue_id}" do + first("select").select("Remove issue") + end + + click_on("Yes, remove issue") + + expect(page).to have_content(changed_issue_banner_save_text) + expect(page).to have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + + within "#issue-#{third_issue_id}" do + first("select").select("Withdraw issue") + end + + expect(page).to have_content(changed_issue_banner_establish_text) + expect(page).to have_button("Establish", disabled: true) + + fill_in "withdraw-date", with: (Time.zone.now - 1.week).strftime("%m/%d/%Y") + + expect(page).to have_button("Establish", disabled: false) + expect(page).to_not have_content(COPY::VHA_NO_DECISION_DATE_BANNER) + + within "#issue-#{third_issue_id}" do + expect(page).to_not have_content("Select action") + end + + click_button "Establish" + + expect(page).to have_content(COPY::CORRECT_REQUEST_ISSUES_CHANGED_MODAL_TITLE) + + click_button "Yes, save" + + expect(page).to have_content(edit_establish_success_message_text) + expect(current_url).to include("/decision_reviews/vha?tab=in_progress") + expect(task.reload.status).to eq("assigned") + end + end + + context "creating Supplemental Claims with no decision date" do + let(:intake_type) do + start_supplemental_claim(veteran, benefit_type: "vha") + end + + let(:intake_button_text) { "Save Supplemental Claim" } + let(:success_message_text) { "You have successfully saved #{veteran.name}'s #{SupplementalClaim.review_title}" } + let(:edit_establish_success_message_text) do + "You have successfully established #{veteran.name}'s #{SupplementalClaim.review_title}" + end + + it_behaves_like "Vha HLR/SC Issue without decision date" + end + + context "creating Higher Level Reviews with no decision date" do + let(:intake_type) do + start_higher_level_review(veteran, benefit_type: "vha") + end + + let(:intake_button_text) { "Save Higher-Level Review" } + let(:success_message_text) { "You have successfully saved #{veteran.name}'s #{HigherLevelReview.review_title}" } + let(:edit_establish_success_message_text) do + "You have successfully established #{veteran.name}'s #{HigherLevelReview.review_title}" + end + + it_behaves_like "Vha HLR/SC Issue without decision date" + end + + context "adding an issue without a decision date to an existing HLR/SC" do + before do + task.appeal.establish! + end + + let(:claim_review) do + task.appeal + end + + let(:edit_decision_date_success_message_text) do + "You have successfully updated an issue's decision date" + end + + let(:edit_save_success_message_text) do + "You have successfully added 3 issues." + end + + context "an existing Higher-Level Review" do + let(:task) do + FactoryBot.create(:higher_level_review_vha_task, assigned_to: VhaBusinessLine.singleton) + end + + let(:edit_url) do + "/higher_level_reviews/#{claim_review.uuid}/edit" + end + + let(:edit_establish_success_message_text) do + "You have successfully established #{claim_review.veteran.name}'s #{HigherLevelReview.review_title}" + end + + it_behaves_like "Vha HLR/SC adding issue without decision date to existing claim review" + end + + context "an existing Supplmental Claim" do + let(:task) do + FactoryBot.create(:supplemental_claim_vha_task, assigned_to: VhaBusinessLine.singleton) + end + + let(:edit_url) do + "/supplemental_claims/#{claim_review.uuid}/edit" + end + + let(:edit_establish_success_message_text) do + "You have successfully established #{claim_review.veteran.name}'s #{SupplementalClaim.review_title}" + end + + it_behaves_like "Vha HLR/SC adding issue without decision date to existing claim review" + end + end +end diff --git a/spec/feature/non_comp/board_grants_spec.rb b/spec/feature/non_comp/board_grants_spec.rb index 81ec300cd75..71c16bcefb7 100644 --- a/spec/feature/non_comp/board_grants_spec.rb +++ b/spec/feature/non_comp/board_grants_spec.rb @@ -107,7 +107,7 @@ def submit_form vha_org.add_user(user) end - let!(:vha_org) { create(:business_line, name: "veterans health admin", url: "vha") } + let!(:vha_org) { VhaBusinessLine.singleton } let!(:vha_request_issues) do 3.times do |index| @@ -136,7 +136,7 @@ def submit_form # when this test executes, the nca business line with request issues already exists visit vha_dispositions_url - expect(page).to have_content("veterans health admin") + expect(page).to have_content("Veterans Health Administration") expect(page).to have_content("Decision") expect(page).to have_content(veteran.name) expect(page).to have_content(Constants.INTAKE_FORM_NAMES.appeal) diff --git a/spec/feature/non_comp/dispositions_spec.rb b/spec/feature/non_comp/dispositions_spec.rb index 1b507e192e6..80bce4ad502 100644 --- a/spec/feature/non_comp/dispositions_spec.rb +++ b/spec/feature/non_comp/dispositions_spec.rb @@ -272,7 +272,7 @@ def find_disabled_disposition(disposition, description = nil) FeatureToggle.disable!(:poa_button_refresh) end - let!(:vha_org) { create(:business_line, name: "Veterans Health Administration", url: "vha") } + let!(:vha_org) { VhaBusinessLine.singleton } let(:user) { create(:default_user) } let(:veteran) { create(:veteran) } let(:decision_date) { Time.zone.now + 10.days } diff --git a/spec/feature/non_comp/reviews_spec.rb b/spec/feature/non_comp/reviews_spec.rb index ce2a81942e0..4619012d520 100644 --- a/spec/feature/non_comp/reviews_spec.rb +++ b/spec/feature/non_comp/reviews_spec.rb @@ -1,21 +1,24 @@ # frozen_string_literal: true feature "NonComp Reviews Queue", :postgres do - let!(:non_comp_org) { create(:business_line, name: "Non-Comp Org", url: "vha") } + let(:non_comp_org) { VhaBusinessLine.singleton } let(:user) { create(:default_user) } let(:veteran_a) { create(:veteran, first_name: "Aaa", participant_id: "12345", ssn: "140261454") } let(:veteran_b) { create(:veteran, first_name: "Bbb", participant_id: "601111772", ssn: "191097395") } + let(:veteran_a_on_hold) { create(:veteran, first_name: "Douglas", participant_id: "87474", ssn: "999393976") } + let(:veteran_b_on_hold) { create(:veteran, first_name: "Gaius", participant_id: "601172", ssn: "191039395") } let(:veteran_c) { create(:veteran, first_name: "Ccc", participant_id: "1002345", ssn: "128455943") } - let(:hlr_a) do - create(:higher_level_review, claimant_type: :veteran_claimant, veteran_file_number: veteran_a.file_number) + let(:claimant_type) { :veteran_claimant } + let(:hlr_a_on_hold) do + create(:higher_level_review, veteran_file_number: veteran_a_on_hold.file_number, claimant_type: claimant_type) end - let(:hlr_b) do - create(:higher_level_review, claimant_type: :veteran_claimant, veteran_file_number: veteran_b.file_number) - end - let(:hlr_c) do - create(:higher_level_review, claimant_type: :veteran_claimant, veteran_file_number: veteran_c.file_number) + let(:hlr_b_on_hold) do + create(:higher_level_review, veteran_file_number: veteran_b_on_hold.file_number, claimant_type: claimant_type) end + let(:hlr_a) { create(:higher_level_review, veteran_file_number: veteran_a.file_number, claimant_type: claimant_type) } + let(:hlr_b) { create(:higher_level_review, veteran_file_number: veteran_b.file_number, claimant_type: claimant_type) } + let(:hlr_c) { create(:higher_level_review, veteran_file_number: veteran_c.file_number, claimant_type: claimant_type) } let(:appeal) { create(:appeal, veteran: veteran_c) } let!(:request_issue_a) do @@ -38,6 +41,14 @@ closed_at: 1.day.ago) end + let!(:request_issue_a_on_hold) do + create(:request_issue, :nonrating, nonrating_issue_category: "Clothing Allowance", decision_review: hlr_a_on_hold) + end + + let!(:request_issue_b_on_hold) do + create(:request_issue, :nonrating, nonrating_issue_category: "Other", decision_review: hlr_b_on_hold) + end + let(:today) { Time.zone.now } let(:last_week) { Time.zone.now - 7.days } @@ -88,6 +99,23 @@ ] end + let!(:on_hold_tasks) do + tasks = [ + create(:higher_level_review_task, + :in_progress, + appeal: hlr_a_on_hold, + assigned_to: non_comp_org, + assigned_at: last_week), + create(:higher_level_review_task, + :in_progress, + appeal: hlr_b_on_hold, + assigned_to: non_comp_org, + assigned_at: last_week) + ] + tasks.each(&:on_hold!) + tasks + end + let(:search_box_label) { "Search by Claimant Name, Veteran Participant ID, File Number or SSN" } let(:vet_id_column_header) do @@ -137,11 +165,12 @@ def current_table_rows scenario "displays tasks page with decision_review_queue_ssn_column feature toggle disabled" do visit BASE_URL - expect(page).to have_content("Non-Comp Org") + expect(page).to have_content("Veterans Health Administration") + expect(page).to have_content("Incomplete tasks") expect(page).to have_content("In progress tasks") expect(page).to have_content("Completed tasks") - # default is the in progress page + # default is the in progress page if no tab is specified in the url expect(page).to have_content("Days Waiting") expect(page).to have_content("Issues") expect(page).to have_content("Issue Type") @@ -150,6 +179,8 @@ def current_table_rows expect(page).to have_content(veteran_a.name) expect(page).to have_content(veteran_b.name) expect(page).to have_content(veteran_c.name) + expect(page).to_not have_content(veteran_a_on_hold.name) + expect(page).to_not have_content(veteran_b_on_hold.name) expect(page).to have_content(vet_id_column_header) expect(page).to have_content(vet_a_id_column_value) expect(page).to have_content(vet_b_id_column_value) @@ -157,11 +188,20 @@ def current_table_rows expect(page).to have_no_content(search_box_label) # ordered by assigned_at descending - expect(page).to have_content( /#{veteran_b.name}.+\s#{veteran_c.name}.+\s#{veteran_a.name}/ ) + click_on "Incomplete tasks" + expect(page).to have_content(COPY::VHA_INCOMPLETE_TAB_DESCRIPTION) + expect(page).to have_content("Higher-Level Review", count: 2) + expect(page).to have_content("Days Waiting") + + # ordered by assigned_at descending + expect(page).to have_content( + /#{veteran_a_on_hold.name}.+\s#{veteran_b_on_hold.name}/ + ) + click_on "Completed tasks" expect(page).to have_content("Higher-Level Review", count: 2) expect(page).to have_content("Date Completed") @@ -178,11 +218,12 @@ def current_table_rows context "with user enabled for intake" do scenario "displays tasks page" do visit BASE_URL - expect(page).to have_content("Non-Comp Org") + expect(page).to have_content("Veterans Health Administration") + expect(page).to have_content("Incomplete tasks") expect(page).to have_content("In progress tasks") expect(page).to have_content("Completed tasks") - # default is the in progress page + # default is the in progress page if no tab is specified in the url expect(page).to have_content("Days Waiting") expect(page).to have_content("Issues") expect(page).to have_content("Issue Type") @@ -191,6 +232,8 @@ def current_table_rows expect(page).to have_content(veteran_a.name) expect(page).to have_content(veteran_b.name) expect(page).to have_content(veteran_c.name) + expect(page).to_not have_content(veteran_a_on_hold.name) + expect(page).to_not have_content(veteran_b_on_hold.name) expect(page).to have_content(vet_id_column_header) expect(page).to have_content(vet_a_id_column_value) expect(page).to have_content(vet_b_id_column_value) @@ -322,7 +365,7 @@ def current_table_rows # Date Completed asc # Currently swapping tabs does not correctly populate get params. # These statements will need to updated when that is fixed - click_button("tasks-organization-queue-tab-1") + click_button("tasks-organization-queue-tab-2") later_date = Time.zone.now.strftime("%m/%d/%y") earlier_date = 2.days.ago.strftime("%m/%d/%y") @@ -454,6 +497,18 @@ def current_table_rows expect(page).to_not have_content("Camp Lejune Family Member") find(".cf-clear-filters-link").click expect(page).to have_content("Camp Lejune Family Member") + + # Verify the filter counts for the incomplete tab + click_on "Incomplete tasks" + find("[aria-label='Filter by issue type']").click + expect(page).to have_content("Clothing Allowance (1)") + expect(page).to have_content("Other (1)") + + # Verify the filter counts for the completed tab + click_on "Completed tasks" + find("[aria-label='Filter by issue type']").click + expect(page).to have_content("Apportionment (1)") + expect(page).to have_content("Camp Lejune Family Member (1)") end scenario "searching reviews by name" do @@ -779,15 +834,20 @@ def current_table_rows expect(page).to have_content("Filtering by: Issue Type (1)") # Swap to the completed tab - click_button("tasks-organization-queue-tab-1") + click_button("tasks-organization-queue-tab-2") expect(page).to have_content(pipe_issue_category) expect(page).to have_content("Filtering by: Issue Type (1)") # Swap back to the in progress tab - click_button("tasks-organization-queue-tab-0") + click_button("tasks-organization-queue-tab-1") expect(page).to have_content(pipe_issue_category) expect(page).to_not have_content("Foreign Medical Program") expect(page).to have_content("Filtering by: Issue Type (1)") + + # Swap to the incomplete tab with no results + click_button("tasks-organization-queue-tab-0") + expect(page).to_not have_content("Foreign Medical Program") + expect(page).to have_content("Filtering by: Issue Type (1)") end # Simulate this by setting a filter, visiting the task page, and coming back @@ -825,4 +885,52 @@ def current_table_rows end end end + + context "For a non comp org that is not VHA" do + after { FeatureToggle.disable!(:board_grant_effectuation_task) } + let(:non_comp_org) { create(:business_line, name: "Non-Comp Org", url: "nco") } + + scenario "displays tasks page for non VHA" do + visit "/decision_reviews/nco" + expect(page).to have_content("Non-Comp Org") + expect(page).to_not have_content("Incomplete tasks") + expect(page).to have_content("In progress tasks") + expect(page).to have_content("Completed tasks") + + # default is the in progress page if no tab is specified in the url + # in progress for non vha should still include on hold tasks + expect(page).to have_content("Days Waiting") + expect(page).to have_content("Issues") + expect(page).to have_content("Issue Type") + expect(page).to have_content("Higher-Level Review", count: 4) + expect(page).to have_content("Board Grant") + expect(page).to have_content(veteran_a.name) + expect(page).to have_content(veteran_b.name) + expect(page).to have_content(veteran_c.name) + expect(page).to have_content(veteran_a_on_hold.name) + expect(page).to have_content(veteran_b_on_hold.name) + expect(page).to have_content(vet_id_column_header) + expect(page).to have_content(vet_a_id_column_value) + expect(page).to have_content(vet_b_id_column_value) + expect(page).to have_content(vet_c_id_column_value) + expect(page).to have_no_content(search_box_label) + + # ordered by assigned_at descending + expect(page).to have_content( + /#{veteran_b.name}.+\s#{veteran_c.name}.+\s#{veteran_a.name}/ + ) + + click_on "Completed tasks" + expect(page).to have_content("Higher-Level Review", count: 2) + expect(page).to have_content("Date Completed") + + # ordered by closed_at descending + expect(page).to have_content( + Regexp.new( + /#{veteran_b.name} #{vet_b_id_column_value} 1/, + /#{request_issue_b.decision_date.strftime("%m\/%d\/%y")} Higher-Level Review/ + ) + ) + end + end end diff --git a/spec/feature/queue/appeal_notifications_page_spec.rb b/spec/feature/queue/appeal_notifications_page_spec.rb index dcf342796eb..ecfef5030f9 100644 --- a/spec/feature/queue/appeal_notifications_page_spec.rb +++ b/spec/feature/queue/appeal_notifications_page_spec.rb @@ -89,161 +89,170 @@ visit appeal_case_details_page click_link("View notifications sent to appellant") # notifications page opens in new browser window so go to that window - page.switch_to_window(page.windows.last) - expect(page).to have_current_path(appeal_notifications_page) - - # table is filled with notifications - table = page.find("tbody") - expect(table).to have_selector("tr", count: 15) - - # correct event type - event_type_cell = page.find("td", match: :first) - expect(event_type_cell).to have_content("Appeal docketed") - - # correct notification date - date_cell = page.all("td", minimum: 1)[1] - expect(date_cell).to have_content("11/01/2022") - - # correct notification type - notification_type_cell = page.all("td", minimum: 1)[2] - expect(notification_type_cell).to have_content("Email") - - # correct recipient information - recipient_info_cell = page.all("td", minimum: 1)[3] - expect(recipient_info_cell).to have_content("example@example.com") - - # correct status - status_cell = page.all("td", minimum: 1)[4] - expect(status_cell).to have_content("Delivered") - - # sort by notification date - sort = page.all("svg", class: "table-icon", minimum: 1)[1] - sort.click - cell = page.all("td", minimum: 1)[1] - expect(cell).to have_content("11/08/2022") + # page.switch_to_window(page.windows.last) + notification_window = page.windows.last + page.within_window(notification_window) do + expect(page).to have_current_path(appeal_notifications_page) + + # table is filled with notifications + table = page.find("tbody") + expect(table).to have_selector("tr", count: 15) + + # correct event type + event_type_cell = page.find("td", match: :first) + expect(event_type_cell).to have_content("Appeal docketed") + + # correct notification date + date_cell = page.all("td", minimum: 1)[1] + expect(date_cell).to have_content("11/01/2022") + + # correct notification type + notification_type_cell = page.all("td", minimum: 1)[2] + expect(notification_type_cell).to have_content("Email") + + # correct recipient information + recipient_info_cell = page.all("td", minimum: 1)[3] + expect(recipient_info_cell).to have_content("example@example.com") + + # correct status + status_cell = page.all("td", minimum: 1)[4] + expect(status_cell).to have_content("Delivered") + + # sort by notification date + sort = page.all("svg", class: "table-icon", minimum: 1)[1] + sort.click + cell = page.all("td", minimum: 1)[1] + expect(cell).to have_content("11/08/2022") + end end it "table can filter by each column, and filter by multiple columns at once" do visit appeal_case_details_page click_link("View notifications sent to appellant") # notifications page opens in new browser window so go to that window - page.switch_to_window(page.windows.last) - expect(page).to have_current_path(appeal_notifications_page) - - # by event type - filter = page.find("path", class: "unselected-filter-icon-inner-1", match: :first) - filter.click - filter_option = page.find("li", class: "cf-filter-option-row", text: "Appeal docketed") - filter_option.click - table = page.find("tbody") - cells = table.all("td", minimum: 1) - expect(table).to have_selector("tr", count: 2) - expect(cells[0]).to have_content("Appeal docketed") - expect(cells[5]).to have_content("Appeal docketed") - - # clear filter - filter.click - page.find("button", text: "Clear Event filter").click - - # by notification type - filter = page.all("path", class: "unselected-filter-icon-inner-1", minimum: 1)[1] - filter.click - filter_option = page.find("li", class: "cf-filter-option-row", text: "Email") - filter_option.click - table = page.find("tbody") - cells = table.all("td", minimum: 1) - expect(table).to have_selector("tr", count: 8) - expect(cells[2]).to have_content("Email") - expect(cells[37]).to have_content("Email") - - # clear filter - filter.click - page.find("button", text: "Clear Notification Type filter").click - - # by recipient information - filter = page.all("path", class: "unselected-filter-icon-inner-1", minimum: 1)[2] - filter.click - filter_option = page.find("li", class: "cf-filter-option-row", text: "Example@example.com") - filter_option.click - table = page.find("tbody") - cells = table.all("td", minimum: 1) - expect(table).to have_selector("tr", count: 4) - expect(cells[3]).to have_content("example@example.com") - expect(cells[18]).to have_content("example@example.com") - - # clear filter - filter.click - page.find("button", text: "Clear Recipient Information filter").click - - # by status - filter = page.all("path", class: "unselected-filter-icon-inner-1", minimum: 1)[3] - filter.click - filter_option = page.find("li", class: "cf-filter-option-row", text: "Delivered") - filter_option.click - table = page.find("tbody") - cells = table.all("td", minimum: 1) - expect(table).to have_selector("tr", count: 5) - expect(cells[4]).to have_content("Delivered") - expect(cells[24]).to have_content("Delivered") - - # clear filter - filter.click - page.find("button", text: "Clear Status filter").click - - # by multiple columns at once - filters = page.all("path", class: "unselected-filter-icon-inner-1", minimum: 1) - filters[0].click - page.find("li", class: "cf-filter-option-row", text: "Hearing scheduled").click - filters[1].click - page.find("li", class: "cf-filter-option-row", text: "Text").click - table = page.find("tbody") - cells = table.all("td", minimum: 1) - expect(table).to have_selector("tr", count: 1) - expect(cells[0]).to have_content("Hearing scheduled") - expect(cells[2]).to have_content("Text") + # page.switch_to_window(page.windows.last) + notification_window = page.windows.last + page.within_window(notification_window) do + expect(page).to have_current_path(appeal_notifications_page) + + # by event type + filter = page.find("path", class: "unselected-filter-icon-inner-1", match: :first) + filter.click + filter_option = page.find("li", class: "cf-filter-option-row", text: "Appeal docketed") + filter_option.click + table = page.find("tbody") + cells = table.all("td", minimum: 1) + expect(table).to have_selector("tr", count: 2) + expect(cells[0]).to have_content("Appeal docketed") + expect(cells[5]).to have_content("Appeal docketed") + + # clear filter + filter.click + page.find("button", text: "Clear Event filter").click + + # by notification type + filter = page.all("path", class: "unselected-filter-icon-inner-1", minimum: 1)[1] + filter.click + filter_option = page.find("li", class: "cf-filter-option-row", text: "Email") + filter_option.click + table = page.find("tbody") + cells = table.all("td", minimum: 1) + expect(table).to have_selector("tr", count: 8) + expect(cells[2]).to have_content("Email") + expect(cells[37]).to have_content("Email") + + # clear filter + filter.click + page.find("button", text: "Clear Notification Type filter").click + + # by recipient information + filter = page.all("path", class: "unselected-filter-icon-inner-1", minimum: 1)[2] + filter.click + filter_option = page.find("li", class: "cf-filter-option-row", text: "Example@example.com") + filter_option.click + table = page.find("tbody") + cells = table.all("td", minimum: 1) + expect(table).to have_selector("tr", count: 4) + expect(cells[3]).to have_content("example@example.com") + expect(cells[18]).to have_content("example@example.com") + + # clear filter + filter.click + page.find("button", text: "Clear Recipient Information filter").click + + # by status + filter = page.all("path", class: "unselected-filter-icon-inner-1", minimum: 1)[3] + filter.click + filter_option = page.find("li", class: "cf-filter-option-row", text: "Delivered") + filter_option.click + table = page.find("tbody") + cells = table.all("td", minimum: 1) + expect(table).to have_selector("tr", count: 5) + expect(cells[4]).to have_content("Delivered") + expect(cells[24]).to have_content("Delivered") + + # clear filter + filter.click + page.find("button", text: "Clear Status filter").click + + # by multiple columns at once + filters = page.all("path", class: "unselected-filter-icon-inner-1", minimum: 1) + filters[0].click + page.find("li", class: "cf-filter-option-row", text: "Hearing scheduled").click + filters[1].click + page.find("li", class: "cf-filter-option-row", text: "Text").click + table = page.find("tbody") + cells = table.all("td", minimum: 1) + expect(table).to have_selector("tr", count: 1) + expect(cells[0]).to have_content("Hearing scheduled") + expect(cells[2]).to have_content("Text") + end end it "notification page can properly navigate pages and event modal behaves properly" do visit appeal_case_details_page click_link("View notifications sent to appellant") # notifications page opens in new browser window so go to that window - page.switch_to_window(page.windows.last) - expect(page).to have_current_path(appeal_notifications_page) - - # next button moves to next page - click_on("Next", match: :first) - table = page.find("tbody") - expect(table).to have_selector("tr", count: 1) - - # next button disabled while on last page - expect(page).to have_button("Next", disabled: true) - - # prev button moves to previous page - click_on("Prev", match: :first) - event_type_cell = page.find("td", match: :first) - expect(event_type_cell).to have_content("Appeal docketed") - - # prev button disabled on the first page - expect(page).to have_button("Prev", disabled: true) - - # clicking numbered page button renders correct page - pagination = page.find(class: "cf-pagination-pages", match: :first) - pagination.find("Button", text: "2", match: :first).click - table = page.find("tbody") - expect(table).to have_selector("tr", count: 1) - - # modal appears when clicking on an event type - event_type_cell = page.find("td", match: :first).find("a") - event_type_cell.click - expect(page).to have_selector("div", class: "cf-modal-body") - - # background darkens and disables clicking when modal is open - expect(page).to have_selector("section", id: "modal_id") - - # clicking close button on modal removes dark background and closes modal - click_on("Close") - expect(page).not_to have_selector("div", class: "cf-modal-body") - expect(page).not_to have_selector("section", id: "modal_id") + # page.switch_to_window(page.windows.last) + notification_window = page.windows.last + page.within_window(notification_window) do + expect(page).to have_current_path(appeal_notifications_page) + + # next button moves to next page + click_on("Next", match: :first) + table = page.find("tbody") + expect(table).to have_selector("tr", count: 1) + + # next button disabled while on last page + expect(page).to have_button("Next", disabled: true) + + # prev button moves to previous page + click_on("Prev", match: :first) + event_type_cell = page.find("td", match: :first) + expect(event_type_cell).to have_content("Appeal docketed") + + # prev button disabled on the first page + expect(page).to have_button("Prev", disabled: true) + + # clicking numbered page button renders correct page + pagination = page.find(class: "cf-pagination-pages", match: :first) + pagination.find("Button", text: "2", match: :first).click + table = page.find("tbody") + expect(table).to have_selector("tr", count: 1) + + # modal appears when clicking on an event type + event_type_cell = page.find("td", match: :first).find("a") + event_type_cell.click + expect(page).to have_selector("div", class: "cf-modal-body") + + # background darkens and disables clicking when modal is open + expect(page).to have_selector("section", id: "modal_id") + + # clicking close button on modal removes dark background and closes modal + click_on("Close") + expect(page).not_to have_selector("div", class: "cf-modal-body") + expect(page).not_to have_selector("section", id: "modal_id") + end end end diff --git a/spec/feature/switch_apps_spec.rb b/spec/feature/switch_apps_spec.rb index fcf04dfc034..ca43455c721 100644 --- a/spec/feature/switch_apps_spec.rb +++ b/spec/feature/switch_apps_spec.rb @@ -60,7 +60,7 @@ end let!(:vha_business_line) do - create(:business_line, url: "vha", name: "Veterans Health Administration") + VhaBusinessLine.singleton end let!(:list_order) do diff --git a/spec/jobs/batch_processes/priority_ep_sync_batch_process_job_spec.rb b/spec/jobs/batch_processes/priority_ep_sync_batch_process_job_spec.rb index 1b2716e946a..be010324dfe 100644 --- a/spec/jobs/batch_processes/priority_ep_sync_batch_process_job_spec.rb +++ b/spec/jobs/batch_processes/priority_ep_sync_batch_process_job_spec.rb @@ -161,9 +161,9 @@ expect(BatchProcess.count).to eq(1) end - it "the batch process includes only 1 of the 2 available PriorityEndProductSyncQueue records" do - expect(PriorityEndProductSyncQueue.count).to eq(2) - expect(BatchProcess.first.priority_end_product_sync_queue.count).to eq(1) + it "the batch process syncs & deletes only 1 of the 2 available PriorityEndProductSyncQueue records" do + expect(PriorityEndProductSyncQueue.count).to eq(1) + expect(BatchProcess.first.priority_end_product_sync_queue.count).to eq(0) end it "the batch process has a state of 'COMPLETED'" do @@ -186,6 +186,10 @@ expect(BatchProcess.first.records_failed).to eq(0) end + it "the batch process has 1 records_completed" do + expect(BatchProcess.first.records_completed).to eq(1) + end + it "slack will NOT be notified when job runs successfully" do expect(slack_service).to_not have_received(:send_notification) end diff --git a/spec/jobs/decision_review_process_job_spec.rb b/spec/jobs/decision_review_process_job_spec.rb index 29fb75d430b..682b74f8ee5 100644 --- a/spec/jobs/decision_review_process_job_spec.rb +++ b/spec/jobs/decision_review_process_job_spec.rb @@ -49,15 +49,6 @@ def sort_by_last_submitted_at; end expect(establishment_subject.error).to be_nil end - context "when disable_claim_establishment feature toggle is enabled" do - before { FeatureToggle.enable!(:disable_claim_establishment) } - after { FeatureToggle.disable!(:disable_claim_establishment) } - - it "does not attempt establishment" do - expect(subject).to eq(nil) - end - end - context "transient VBMS error" do let(:vbms_error) do VBMS::HTTPError.new("500", "FAILED FOR UNKNOWN REASONS") diff --git a/spec/models/batch_processes/priority_ep_sync_batch_process_spec.rb b/spec/models/batch_processes/priority_ep_sync_batch_process_spec.rb index 1ebb9ebd8a8..23268ca59ad 100644 --- a/spec/models/batch_processes/priority_ep_sync_batch_process_spec.rb +++ b/spec/models/batch_processes/priority_ep_sync_batch_process_spec.rb @@ -149,6 +149,8 @@ PriorityEndProductSyncQueue.all end + let!(:original_pepsq_record_size) { pepsq_records.size } + let!(:batch_process) { PriorityEpSyncBatchProcess.create_batch!(pepsq_records) } subject { batch_process.process_batch! } @@ -156,23 +158,22 @@ context "when all batched records in the queue are able to sync successfully" do before do subject - pepsq_records.each(&:reload) + pepsq_records.reload end - it "each batched record in the queue will have a status of 'SYNCED' \n + + it "no batched record in the queue will have a status of 'SYNCED' since these are deleted in #process_batch! \n and the batch process will have a state of 'COMPLETED'" do - all_pepsq_statuses = pepsq_records.pluck(:status) - expect(all_pepsq_statuses).to all(eq(Constants.PRIORITY_EP_SYNC.synced)) + expect(pepsq_records.empty?).to eq true expect(batch_process.state).to eq(Constants.BATCH_PROCESS.completed) end - it "the number of records_attempted for the batch process will match the number of PEPSQ records batched, \n + it "the number of records_attempted for the batch process will \ + match the number of original PEPSQ records batched, \n the number of records_completed for the batch process will match the number of PEPSQ records synced, \n and the number of records_failed for the batch process will match the number of PEPSQ records not synced" do - expect(batch_process.records_attempted).to eq(pepsq_records.count) - all_synced_pepsq_records = pepsq_records.select { |record| record.status == Constants.PRIORITY_EP_SYNC.synced } - expect(batch_process.records_completed).to eq(all_synced_pepsq_records.count) - all_synced_pepsq_records = pepsq_records.reject { |record| record.status == Constants.PRIORITY_EP_SYNC.synced } - expect(batch_process.records_failed).to eq(all_synced_pepsq_records.count) + expect(batch_process.records_attempted).to eq(original_pepsq_record_size) + expect(batch_process.records_completed).to eq(original_pepsq_record_size - pepsq_records.size) + expect(batch_process.records_failed).to eq(0) end end @@ -181,14 +182,14 @@ active_hlr_epe_w_cleared_vbms_ext_claim.vbms_ext_claim.update!(level_status_code: "CAN") allow(Rails.logger).to receive(:error) subject - pepsq_records.each(&:reload) + pepsq_records.reload end - it "all but ONE of the batched records will have a status of 'SYNCED'" do + it "all but ONE of the batched records will have synced (therefore removed from the table)" do synced_status_pepsq_records = pepsq_records.select { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } not_synced_status_pepsq_records = pepsq_records.reject { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } - expect(synced_status_pepsq_records.count).to eq(pepsq_records.count - not_synced_status_pepsq_records.count) - expect(not_synced_status_pepsq_records.count).to eq(pepsq_records.count - synced_status_pepsq_records.count) + expect(synced_status_pepsq_records.count).to eq(0) + expect(not_synced_status_pepsq_records.count).to eq(1) end it "the failed batched record will have a status of 'ERROR' \n @@ -204,15 +205,15 @@ end it "the batch process will have a state of 'COMPLETED', \n - the number of records_attempted for the batch process will match the number of PEPSQ records batched, \n + the number of records_attempted for the batch process will match \ + the number of PEPSQ records that were batched, \n the number of records_completed for the batch process will match the number of successfully synced records \n the number of records_failed for the batch process will match the number of errored records" do expect(batch_process.state).to eq(Constants.BATCH_PROCESS.completed) - expect(batch_process.records_attempted).to eq(pepsq_records.count) - synced_pepsq_records = pepsq_records.select { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } - expect(batch_process.records_completed).to eq(synced_pepsq_records.count) + expect(batch_process.records_attempted).to eq(original_pepsq_record_size) + expect(batch_process.records_completed).to eq(original_pepsq_record_size - pepsq_records.size) failed_sync_pepsq_records = pepsq_records.reject { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } - expect(batch_process.records_failed).to eq(failed_sync_pepsq_records.count) + expect(batch_process.records_failed).to eq(failed_sync_pepsq_records.size) end end @@ -221,14 +222,14 @@ active_hlr_epe_w_cleared_vbms_ext_claim.vbms_ext_claim.destroy! allow(Rails.logger).to receive(:error) subject - pepsq_records.each(&:reload) + pepsq_records.reload end it "all but ONE of the batched records will have a status of 'SYNCED'" do synced_pepsq_records = pepsq_records.select { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } not_synced_pepsq_records = pepsq_records.reject { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } - expect(synced_pepsq_records.count).to eq(pepsq_records.count - not_synced_pepsq_records.count) - expect(not_synced_pepsq_records.count).to eq(pepsq_records.count - synced_pepsq_records.count) + expect(synced_pepsq_records.count).to eq(0) + expect(not_synced_pepsq_records.count).to eq(1) end it "the failed batched record will have a status of 'ERROR' \n @@ -247,15 +248,15 @@ end it "the batch process will have a state of 'COMPLETED', \n - the number of records_attempted for the batch process will match the number of PEPSQ records batched, \n + the number of records_attempted for the batch process will match \ + the number of PEPSQ records that were batched, \n the number of records_completed for the batch process will match the number of successfully synced records, \n and the number of records_failed for the batch process will match the number of errored records" do expect(batch_process.state).to eq(Constants.BATCH_PROCESS.completed) - expect(batch_process.records_attempted).to eq(pepsq_records.count) - synced_pepsq_records = pepsq_records.select { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } - expect(batch_process.records_completed).to eq(synced_pepsq_records.count) + expect(batch_process.records_attempted).to eq(original_pepsq_record_size) + expect(batch_process.records_completed).to eq(original_pepsq_record_size - pepsq_records.size) failed_sync_pepsq_records = pepsq_records.reject { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } - expect(batch_process.records_failed).to eq(failed_sync_pepsq_records.count) + expect(batch_process.records_failed).to eq(failed_sync_pepsq_records.size) end end @@ -265,7 +266,7 @@ Fakes::EndProductStore.cache_store.redis.del("end_product_records_test:#{epe.veteran_file_number}") allow(Rails.logger).to receive(:error) subject - pepsq_records.each(&:reload) + pepsq_records.reload end it "all but ONE of the batched records will have a status of 'SYNCED'" do @@ -289,15 +290,32 @@ it "the batch process will have a state of 'COMPLETED' \n and the number of records_attempted for the batch process will match the number of PEPSQ records batched" do expect(batch_process.state).to eq(Constants.BATCH_PROCESS.completed) - expect(batch_process.records_attempted).to eq(pepsq_records.count) + expect(batch_process.records_attempted).to eq(original_pepsq_record_size) end - it "the number of records_completed for the batch process will match the number of successfully synced records \n + it "the number of records_attempted for the batch process will match\ + the number of original PEPSQ records batched \n + the number of records_completed for the batch process will match the number of successfully synced records \n and the number of records_failed for the batch process will match the number of errored records" do - synced_pepsq_records = pepsq_records.select { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } - expect(batch_process.records_completed).to eq(synced_pepsq_records.count) - failed_sync_pepsq_records = pepsq_records.reject { |r| r.status == Constants.PRIORITY_EP_SYNC.synced } - expect(batch_process.records_failed).to eq(failed_sync_pepsq_records.count) + expect(batch_process.records_attempted).to eq(original_pepsq_record_size) + expect(batch_process.records_completed).to eq(original_pepsq_record_size - pepsq_records.size) + expect(batch_process.records_failed).to eq(1) + end + end + + context "when priority_ep_sync_batch_process destroys synced pepsq records" do + before do + allow(Rails.logger).to receive(:info) + subject + end + + it "should delete the synced_pepsq records from the pepsq table and log it" do + expect(batch_process.priority_end_product_sync_queue.count).to eq(0) + expect(Rails.logger).to have_received(:info).with( + "PriorityEpSyncBatchProcessJob #{pepsq_records.size} synced records deleted:"\ + " [#{pepsq_records[0].id}, #{pepsq_records[1].id}, #{pepsq_records[2].id}, #{pepsq_records[3].id}]"\ + " Time: 2022-01-01 07:00:00 -0500" + ) end end end diff --git a/spec/models/business_line_spec.rb b/spec/models/business_line_spec.rb index 8c34b157e4a..d6c0998f2cb 100644 --- a/spec/models/business_line_spec.rb +++ b/spec/models/business_line_spec.rb @@ -4,10 +4,6 @@ include_context :business_line, "VHA", "vha" let(:veteran) { create(:veteran) } - describe ".tasks_url" do - it { expect(business_line.tasks_url).to eq "/decision_reviews/vha" } - end - shared_examples "task filtration" do context "Higher-Level Review tasks" do let!(:task_filters) { ["col=decisionReviewType&val=HigherLevelReview"] } @@ -197,6 +193,43 @@ end end + describe ".incomplete_tasks" do + let!(:hlr_tasks_on_active_decision_reviews) do + tasks = create_list(:higher_level_review_vha_task, 5, assigned_to: business_line) + tasks.each(&:on_hold!) + tasks + end + + let!(:sc_tasks_on_active_decision_reviews) do + tasks = create_list(:supplemental_claim_vha_task, 5, assigned_to: business_line) + tasks.each(&:on_hold!) + tasks + end + + let!(:decision_review_tasks_on_inactive_decision_reviews) do + tasks = create_list(:higher_level_review_task, 5, assigned_to: business_line) + tasks.each(&:on_hold!) + tasks + end + + subject { business_line.incomplete_tasks(filters: task_filters) } + + include_examples "task filtration" + + context "with no filters" do + let!(:task_filters) { nil } + + it "All tasks associated with active decision reviews and BoardGrantEffectuationTasks are included" do + expect(subject.size).to eq 10 + expect(subject.map(&:id)).to match_array( + (hlr_tasks_on_active_decision_reviews + + sc_tasks_on_active_decision_reviews + ).pluck(:id) + ) + end + end + end + describe ".completed_tasks" do let!(:open_hlr_tasks) do add_veteran_and_request_issues_to_decision_reviews( @@ -274,6 +307,107 @@ end end + describe "Generic Non Comp Org Businessline" do + include_context :business_line, "NONCOMPORG", "nco" + + describe ".tasks_url" do + it { expect(business_line.tasks_url).to eq "/decision_reviews/nco" } + end + + describe ".included_tabs" do + it { expect(business_line.included_tabs).to match_array [:in_progress, :completed] } + end + + describe ".in_progress_tasks" do + let(:current_time) { Time.zone.now } + let!(:hlr_tasks_on_active_decision_reviews) do + create_list(:higher_level_review_vha_task, 5, assigned_to: business_line) + end + + let!(:sc_tasks_on_active_decision_reviews) do + create_list(:supplemental_claim_vha_task, 5, assigned_to: business_line) + end + + # Set some on hold tasks as well + let!(:on_hold_sc_tasks_on_active_decision_reviews) do + tasks = create_list(:supplemental_claim_vha_task, 5, assigned_to: business_line) + tasks.each(&:on_hold!) + tasks + end + + let!(:decision_review_tasks_on_inactive_decision_reviews) do + create_list(:higher_level_review_task, 5, assigned_to: business_line) + end + + let!(:board_grant_effectuation_tasks) do + tasks = create_list(:board_grant_effectuation_task, 5, assigned_to: business_line) + + tasks.each do |task| + create( + :request_issue, + :nonrating, + decision_review: task.appeal, + benefit_type: business_line.url, + closed_at: current_time, + closed_status: "decided" + ) + end + + tasks + end + + let!(:veteran_record_request_on_active_appeals) do + add_veteran_and_request_issues_to_decision_reviews( + create_list(:veteran_record_request_task, 5, assigned_to: business_line) + ) + end + + let!(:veteran_record_request_on_inactive_appeals) do + create_list(:veteran_record_request_task, 5, assigned_to: business_line) + end + + subject { business_line.in_progress_tasks(filters: task_filters) } + + include_examples "task filtration" + + context "With the :board_grant_effectuation_task FeatureToggle enabled" do + let!(:task_filters) { nil } + + before { FeatureToggle.enable!(:board_grant_effectuation_task) } + after { FeatureToggle.disable!(:board_grant_effectuation_task) } + + it "All tasks associated with active decision reviews and BoardGrantEffectuationTasks are included" do + expect(subject.size).to eq 25 + expect(subject.map(&:id)).to match_array( + (veteran_record_request_on_active_appeals + + board_grant_effectuation_tasks + + hlr_tasks_on_active_decision_reviews + + sc_tasks_on_active_decision_reviews + + on_hold_sc_tasks_on_active_decision_reviews + ).pluck(:id) + ) + end + end + + context "With the :board_grant_effectuation_task FeatureToggle disabled" do + let!(:task_filters) { nil } + + before { FeatureToggle.disable!(:board_grant_effectuation_task) } + + it "All tasks associated with active decision reviews are included, but not BoardGrantEffectuationTasks" do + expect(subject.size).to eq 20 + expect(subject.map(&:id)).to match_array( + (veteran_record_request_on_active_appeals + + hlr_tasks_on_active_decision_reviews + + sc_tasks_on_active_decision_reviews + + on_hold_sc_tasks_on_active_decision_reviews + ).pluck(:id) + ) + end + end + end + end + def add_veteran_and_request_issues_to_decision_reviews(tasks) tasks.each do |task| task.appeal.update!(veteran_file_number: veteran.file_number) diff --git a/spec/models/claim_review_spec.rb b/spec/models/claim_review_spec.rb index cd661b30f5c..7cee3d07920 100644 --- a/spec/models/claim_review_spec.rb +++ b/spec/models/claim_review_spec.rb @@ -401,18 +401,19 @@ def random_ref_id context "#create_business_line_tasks!" do subject { claim_review.create_business_line_tasks! } - let!(:request_issue) { create(:request_issue, decision_review: claim_review) } + let!(:request_issue) { create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now) } context "when processed in caseflow" do let(:benefit_type) { "vha" } - it "creates a decision review task" do + it "creates a decision review task with a status of assigned" do expect { subject }.to change(DecisionReviewTask, :count).by(1) expect(DecisionReviewTask.last).to have_attributes( appeal: claim_review, assigned_at: Time.zone.now, - assigned_to: BusinessLine.find_by(url: "vha") + assigned_to: VhaBusinessLine.singleton, + status: "assigned" ) end @@ -434,6 +435,21 @@ def random_ref_id expect { subject }.to_not change(DecisionReviewTask, :count) end end + + context "when one of a vha review's issues has no decision date" do + let!(:request_issue) { create(:request_issue, decision_review: claim_review) } + + it "creates a decision review task with a status of on_hold" do + expect { subject }.to change(DecisionReviewTask, :count).by(1) + + expect(DecisionReviewTask.last).to have_attributes( + appeal: claim_review, + assigned_at: Time.zone.now, + assigned_to: VhaBusinessLine.singleton, + status: "on_hold" + ) + end + end end context "when processed in VBMS" do @@ -445,6 +461,142 @@ def random_ref_id end end + describe "#request_issues_without_decision_dates?" do + let(:claim_review) { create(:higher_level_review, benefit_type: benefit_type) } + + subject { claim_review.request_issues_without_decision_dates? } + + context "it should return true if there are any issues without a decision date" do + let!(:request_issues) do + [ + create(:request_issue, decision_review: claim_review), + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now) + ] + end + + it "should return true" do + expect(subject).to be_truthy + end + end + + context "it should return false if there are not any issues without a decision date" do + let!(:request_issues) do + [ + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now), + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now) + ] + end + + it "should return false" do + expect(subject).to be_falsey + end + end + end + + describe "handle_issues_with_no_decision_date!" do + let(:benefit_type) { "vha" } + let(:claim_review) { create(:higher_level_review, benefit_type: benefit_type) } + let!(:decision_review_task) do + create(:higher_level_review_vha_task, appeal: claim_review, assigned_to: VhaBusinessLine.singleton) + end + + subject { claim_review.handle_issues_with_no_decision_date! } + + context "while it has any request issues without a decision date" do + let!(:request_issues) do + [ + create(:request_issue, decision_review: claim_review), + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now) + ] + end + + it "the task should have a status of on_hold" do + subject + expect(decision_review_task.reload.status).to eq "on_hold" + end + end + + context "while all request issues have a decision date" do + let!(:request_issues) do + [ + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now - 1.day), + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now) + ] + end + + it "the task should have a status of assigned" do + subject + expect(decision_review_task.reload.status).to eq "assigned" + end + end + + context "while it has any request issues without a decision date and is not vha benefit type" do + let(:benefit_type) { "compensation" } + let(:comp_org) { BusinessLine.find_or_create_by(name: Constants::BENEFIT_TYPES[benefit_type], url: benefit_type) } + let!(:decision_review_task) do + create(:higher_level_review_vha_task, appeal: claim_review, assigned_to: comp_org) + end + let!(:request_issues) do + [ + create(:request_issue, decision_review: claim_review), + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now) + ] + end + + it "the task should have a status of assigned" do + subject + expect(decision_review_task.reload.status).to eq "assigned" + end + end + end + + describe "redirect_url" do + let(:benefit_type) { "vha" } + let(:claim_review) { create(:higher_level_review, benefit_type: benefit_type) } + + subject { claim_review.redirect_url } + + context "it should return the incomplete tab url if there are any issues without a decision date" do + let!(:request_issues) do + [ + create(:request_issue, decision_review: claim_review), + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now) + ] + end + + it "should return the incomplete tasks tab route" do + expect(subject).to eq "/decision_reviews/vha?tab=incomplete" + end + end + + context "it should return the decision review url if the benefit type is not vha" do + let(:benefit_type) { "compensation" } + let!(:request_issues) do + [ + create(:request_issue, decision_review: claim_review), + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now) + ] + end + + it "should return the normal decision tasks url" do + expect(subject).to eq "/decision_reviews/compensation" + end + end + + context "it should return the decision review url if there are not any issues without a decision date" do + let!(:request_issues) do + [ + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now - 1.week), + create(:request_issue, decision_review: claim_review, decision_date: Time.zone.now) + ] + end + + it "should return the normal decision tasks url" do + expect(subject).to eq "/decision_reviews/vha" + end + end + end + describe "#create_issues!" do before { claim_review.save! } subject { claim_review.create_issues!(issues) } diff --git a/spec/models/higher_level_review_intake_spec.rb b/spec/models/higher_level_review_intake_spec.rb index 543b573c50d..e3c51095f3d 100644 --- a/spec/models/higher_level_review_intake_spec.rb +++ b/spec/models/higher_level_review_intake_spec.rb @@ -188,30 +188,6 @@ ) end - context "when disable_claim_establishment is enabled" do - before { FeatureToggle.enable!(:disable_claim_establishment) } - after { FeatureToggle.disable!(:disable_claim_establishment) } - - it "does not submit claims to VBMS" do - subject - - expect(intake).to be_success - expect(intake.detail.establishment_submitted_at).to eq(Time.zone.now) - expect(ratings_end_product_establishment).to_not be_nil - expect(ratings_end_product_establishment.established_at).to eq(nil) - expect(Fakes::VBMSService).not_to have_received(:establish_claim!) - expect(Fakes::VBMSService).not_to have_received(:create_contentions!) - expect(Fakes::VBMSService).not_to have_received(:associate_rating_request_issues!) - expect(intake.detail.request_issues.count).to eq 1 - expect(intake.detail.request_issues.first).to have_attributes( - contested_rating_issue_reference_id: "reference-id", - contested_issue_description: "decision text", - rating_issue_associated_at: nil - ) - expect(HigherLevelReview.processable.count).to eq 1 - end - end - context "when benefit type is pension" do let(:benefit_type) { "pension" } let(:pension_rating_ep_establishment) do diff --git a/spec/models/membership_request_spec.rb b/spec/models/membership_request_spec.rb index f567e88ed96..c0c26ac48fe 100644 --- a/spec/models/membership_request_spec.rb +++ b/spec/models/membership_request_spec.rb @@ -4,7 +4,7 @@ before do ActiveJob::Base.queue_adapter.enqueued_jobs.clear end - let(:vha_business_line) { create(:business_line, name: "Veterans Health Administration", url: "vha") } + let(:vha_business_line) { VhaBusinessLine.singleton } describe "#save" do let(:requestor) { create(:user) } diff --git a/spec/models/priority_queues/priority_end_product_sync_queue_spec.rb b/spec/models/priority_queues/priority_end_product_sync_queue_spec.rb index 984dcbb550d..e9754381704 100644 --- a/spec/models/priority_queues/priority_end_product_sync_queue_spec.rb +++ b/spec/models/priority_queues/priority_end_product_sync_queue_spec.rb @@ -171,19 +171,36 @@ found_record = CaseflowStuckRecord.find_by(stuck_record: record) expect(record.caseflow_stuck_records).to include(found_record) end + end + end + + describe "#self.destroy_batch_process_pepsq_records!(batch_process)" do + let!(:bp) { PriorityEpSyncBatchProcess.create } + + let!(:synced_records) { create_list(:priority_end_product_sync_queue, 2, :synced, batch_id: bp.batch_id) } + let!(:error_record) { create(:priority_end_product_sync_queue, :error, batch_id: bp.batch_id) } + + subject { PriorityEndProductSyncQueue.destroy_batch_process_pepsq_records!(bp) } + + context "when priority_ep_sync_batch_process destroys synced pepsq records" do + before do + allow(Rails.logger).to receive(:info) + subject + end + + it "should delete the synced PEPSQ records from the pepsq table" do + expect(PriorityEndProductSyncQueue.all.include?(synced_records)).to be false + end + + it "should NOT delete errored PEPSQ records from the pepsq table" do + expect(PriorityEndProductSyncQueue.all.include?(error_record)).to be true + end - it "a message will be sent to Sentry" do - expect(Raven).to have_received(:capture_message) - .with("StuckRecordAlert::SyncFailed End Product Establishment ID: #{record.end_product_establishment_id}.", - extra: { - batch_id: record.batch_id, - batch_process_type: record.batch_process.class.name, - caseflow_stuck_record_id: record.caseflow_stuck_records.first.id, - determined_stuck_at: anything, - end_product_establishment_id: record.end_product_establishment_id, - queue_type: record.class.name, - queue_id: record.id - }, level: "error") + it "should log a message with the number of deleted records and the deleted record's ID" do + expect(Rails.logger).to have_received(:info).with( + "PriorityEpSyncBatchProcessJob #{synced_records.size} synced records deleted:"\ + " [#{synced_records[0].id}, #{synced_records[1].id}] Time: #{Time.zone.now}" + ) end end end diff --git a/spec/models/request_issue_spec.rb b/spec/models/request_issue_spec.rb index 32d12ece7ce..e5eac551cc0 100644 --- a/spec/models/request_issue_spec.rb +++ b/spec/models/request_issue_spec.rb @@ -2446,4 +2446,53 @@ end end end + + context "#save_decision_date!" do + let(:new_decision_date) { Time.zone.now } + let(:benefit_type) { "vha" } + + subject { nonrating_request_issue.save_decision_date!(new_decision_date) } + + it "should update the decision date and call the review's handle_issues_with_no_decision_date! method" do + expect(review).to receive(:handle_issues_with_no_decision_date!).once + subject + expect(nonrating_request_issue.decision_date).to eq(new_decision_date.to_date) + end + + context "when the decision date is in the future" do + let(:future_date) { 2.days.from_now.to_date } + + subject { nonrating_request_issue } + + it "throws DecisionDateInFutureError" do + allow(subject).to receive(:update!) + + expect { subject.save_decision_date!(future_date) }.to raise_error(RequestIssue::DecisionDateInFutureError) + expect(subject).to_not have_received(:update!) + end + end + end + + context "vha handle issues with no decision date" do + let(:new_decision_date) { Time.zone.now } + let(:benefit_type) { "vha" } + + context("#remove!") do + subject { nonrating_request_issue.remove! } + + it "should call the review's handle_issues_with_no_decision_date! method for removal" do + expect(review).to receive(:handle_issues_with_no_decision_date!).once + subject + end + end + + context("#withdraw!") do + subject { nonrating_request_issue.withdraw!(Time.zone.now) } + + it "should call the review's handle_issues_with_no_decision_date! method for removal" do + expect(review).to receive(:handle_issues_with_no_decision_date!).once + subject + end + end + end end diff --git a/spec/models/request_issues_update_spec.rb b/spec/models/request_issues_update_spec.rb index e33019277b2..f5afe3a04f0 100644 --- a/spec/models/request_issues_update_spec.rb +++ b/spec/models/request_issues_update_spec.rb @@ -194,6 +194,53 @@ def allow_update_contention it { is_expected.to contain_exactly(existing_request_issue) } end + + context "when issue descision dates were edited as part of the update" do + let(:edited_decision_date) { Time.zone.now } + let(:request_issues_data) do + [{ request_issue_id: existing_legacy_opt_in_request_issue.id }, + { request_issue_id: existing_request_issue.id, + edited_decision_date: edited_decision_date }] + end + + it { is_expected.to contain_exactly(existing_request_issue) } + end + + context "when decision_date was edited as part of the update" do + new_decisision_date = Time.zone.today - 1000.years + + context "when benefit type is vha" do + let(:request_issues_data) do + existing_request_issue.decision_date = nil + [ + { + benefit_type: "vha", + edited_decision_date: new_decisision_date, + request_issue_id: existing_request_issue.id + } + ] + end + + it "updates the decision date" do + expect(existing_request_issue.reload.decision_date).to eq(new_decisision_date) + end + end + + context "when edited_decision_date is not present" do + let(:request_issues_data) do + existing_request_issue.decision_date = nil + [ + { + request_issue_id: existing_request_issue.id + } + ] + end + + it "does not update the decision date" do + expect(existing_request_issue.reload.decision_date).to eq(nil) + end + end + end end context "#corrected_issues" do @@ -297,6 +344,20 @@ def allow_update_contention end end + context "when an issue's decision date is edited" do + let(:edited_decision_date) { Time.zone.now } + let(:request_issues_data) do + [{ request_issue_id: existing_legacy_opt_in_request_issue.id }, + { request_issue_id: existing_request_issue.id, + edited_decision_date: edited_decision_date }] + end + + it "updates the request issue's decision date" do + expect(subject).to be_truthy + expect(existing_request_issue.reload.decision_date).to eq(edited_decision_date.to_date) + end + end + context "when issues contain new issues not in existing issues" do let(:request_issues_data) { request_issues_data_with_new_issue } diff --git a/spec/models/serializers/work_queue/board_grant_effectuation_task_serializer_spec.rb b/spec/models/serializers/work_queue/board_grant_effectuation_task_serializer_spec.rb index de96b67ba9b..298888ba62c 100644 --- a/spec/models/serializers/work_queue/board_grant_effectuation_task_serializer_spec.rb +++ b/spec/models/serializers/work_queue/board_grant_effectuation_task_serializer_spec.rb @@ -46,6 +46,8 @@ issue_count: 0, issue_types: "", type: "Board Grant", + external_appeal_id: task.appeal.uuid, + appeal_type: "Appeal", business_line: non_comp_org.url } } @@ -98,6 +100,8 @@ issue_count: 0, issue_types: "", type: "Board Grant", + external_appeal_id: task.appeal.uuid, + appeal_type: "Appeal", business_line: non_comp_org.url } } @@ -155,6 +159,8 @@ issue_count: 0, issue_types: "", type: "Board Grant", + external_appeal_id: task.appeal.uuid, + appeal_type: "Appeal", business_line: non_comp_org.url } } diff --git a/spec/models/serializers/work_queue/decision_review_task_serializer_spec.rb b/spec/models/serializers/work_queue/decision_review_task_serializer_spec.rb index 4f74831e6b6..950ed5d22bd 100644 --- a/spec/models/serializers/work_queue/decision_review_task_serializer_spec.rb +++ b/spec/models/serializers/work_queue/decision_review_task_serializer_spec.rb @@ -52,6 +52,8 @@ issue_count: 0, issue_types: "", type: "Higher-Level Review", + external_appeal_id: task.appeal.uuid, + appeal_type: "HigherLevelReview", business_line: non_comp_org.url } } @@ -92,6 +94,8 @@ issue_count: 0, issue_types: "", type: "Higher-Level Review", + external_appeal_id: task.appeal.uuid, + appeal_type: "HigherLevelReview", business_line: non_comp_org.url } } @@ -149,6 +153,8 @@ issue_count: 0, issue_types: "", type: "Higher-Level Review", + external_appeal_id: task.appeal.uuid, + appeal_type: "HigherLevelReview", business_line: non_comp_org.url } } @@ -157,9 +163,12 @@ end context "decision review with multiple issues with multiple issue categories" do + let!(:vha_org) { VhaBusinessLine.singleton } + let(:hlr) do + create(:higher_level_review_vha_task).appeal + end let(:claimant_type) { :veteran_claimant } let(:benefit_type) { "vha" } - let!(:vha_org) { create(:business_line, name: "Veterans Health Administration", url: "vha") } let(:request_issues) do [ create(:request_issue, benefit_type: "vha", nonrating_issue_category: "Beneficiary Travel"), @@ -207,11 +216,15 @@ issue_count: 2, issue_types: hlr.request_issues.active.pluck(:nonrating_issue_category).join(","), type: "Higher-Level Review", + external_appeal_id: task.appeal.uuid, + appeal_type: "HigherLevelReview", business_line: non_comp_org.url, appellant_type: "VeteranClaimant" } } - expect(subject.serializable_hash[:data]).to eq(serializable_hash) + # The request issues serializer is non-deterministic due to multiple request issues + # This just deletes the appeal data from the hash until that is fixed + expect(subject.serializable_hash[:data].delete(:appeal)).to eq(serializable_hash.delete(:appeal)) end end end diff --git a/spec/models/serializers/work_queue/veteran_record_request_serializer_spec.rb b/spec/models/serializers/work_queue/veteran_record_request_serializer_spec.rb index 990caf7f5c2..22a975f7090 100644 --- a/spec/models/serializers/work_queue/veteran_record_request_serializer_spec.rb +++ b/spec/models/serializers/work_queue/veteran_record_request_serializer_spec.rb @@ -38,7 +38,9 @@ issue_count: 0, issue_types: "", type: "Record Request", - business_line: non_comp_org.url + business_line: non_comp_org.url, + external_appeal_id: appeal.uuid, + appeal_type: "Appeal" } } diff --git a/spec/models/tasks/decision_review_task_spec.rb b/spec/models/tasks/decision_review_task_spec.rb index af24b2c94b8..a7a25ff472f 100644 --- a/spec/models/tasks/decision_review_task_spec.rb +++ b/spec/models/tasks/decision_review_task_spec.rb @@ -143,6 +143,8 @@ type: "Higher-Level Review", claimant: { name: hlr.veteran_full_name, relationship: "self" }, business_line: business_line.url, + external_appeal_id: decision_review_task.appeal.uuid, + appeal_type: "HigherLevelReview", has_poa: true } expect(subject).to eq serialized_hash @@ -184,6 +186,8 @@ issue_types: "", type: "Higher-Level Review", business_line: business_line.url, + external_appeal_id: decision_review_task.appeal.uuid, + appeal_type: "HigherLevelReview", has_poa: true } } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7b25a645c36..3e8075cbe7c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -765,7 +765,7 @@ class AnotherFakeTask < Dispatch::Task; end describe "vha_employee?" do let(:user) { create(:user) } - let(:org) { BusinessLine.create!(name: "Veterans Health Administration", url: "vha") } + let(:org) { VhaBusinessLine.singleton } subject { user.vha_employee? } diff --git a/spec/models/vha_business_line_spec.rb b/spec/models/vha_business_line_spec.rb new file mode 100644 index 00000000000..d64a730fe6f --- /dev/null +++ b/spec/models/vha_business_line_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +describe BusinessLine do + subject { VhaBusinessLine.singleton } + + describe ".tasks_url" do + it { expect(subject.tasks_url).to eq "/decision_reviews/vha" } + end + + describe ".included_tabs" do + it { expect(subject.included_tabs).to match_array [:incomplete, :in_progress, :completed] } + end + + describe ".singleton" do + it "is named correctly and has vha url" do + expect(subject).to have_attributes(name: "Veterans Health Administration", url: "vha") + end + end + + describe ".tasks_query_type" do + it "returns the correct task query types" do + expect(subject.tasks_query_type).to eq( + incomplete: "on_hold", + in_progress: "active", + completed: "recently_completed" + ) + end + end +end diff --git a/spec/models/vha_membership_request_mail_builder_spec.rb b/spec/models/vha_membership_request_mail_builder_spec.rb index 302e8335f44..b44805a7fc8 100644 --- a/spec/models/vha_membership_request_mail_builder_spec.rb +++ b/spec/models/vha_membership_request_mail_builder_spec.rb @@ -9,7 +9,7 @@ end let(:camo_org) { VhaCamo.singleton } - let(:vha_business_line) { BusinessLine.find_by(url: "vha") } + let(:vha_business_line) { VhaBusinessLine.singleton } let(:requestor) { create(:user, full_name: "Alice", email: "alice@test.com", css_id: "ALICEREQUEST") } let(:membership_requests) do [ @@ -199,7 +199,7 @@ private def create_vha_orgs - create(:business_line, name: "Veterans Health Administration", url: "vha") + VhaBusinessLine.singleton VhaCamo.singleton VhaCaregiverSupport.singleton create(:vha_program_office, diff --git a/spec/support/shared_context/decision_review/vha/shared_context_business_line.rb b/spec/support/shared_context/decision_review/vha/shared_context_business_line.rb index 7be6a656c55..7db495418c9 100644 --- a/spec/support/shared_context/decision_review/vha/shared_context_business_line.rb +++ b/spec/support/shared_context/decision_review/vha/shared_context_business_line.rb @@ -6,7 +6,11 @@ end RSpec.shared_context :business_line do |name, url| - let(:business_line) { create(:business_line, name: name, url: url) } + if url == "vha" + let(:business_line) { VhaBusinessLine.singleton } + else + let(:business_line) { create(:business_line, name: name, url: url) } + end end RSpec.shared_context :organization do |name, type|