diff --git a/Gemfile b/Gemfile index f3ffe697c16..f75dbc4f033 100644 --- a/Gemfile +++ b/Gemfile @@ -12,7 +12,7 @@ gem "acts_as_tree" gem "amoeba" # BGS -gem "bgs", git: "https://github.com/department-of-veterans-affairs/ruby-bgs.git", ref: "7d7c67f7bad5e5aa03e257f0d8e57a4aa1a6cbbf" +gem "bgs", git: "https://github.com/department-of-veterans-affairs/ruby-bgs.git", ref: "5f47e7b2656ef347d314ef43c93d38a9f20816ec" # Bootsnap speeds up app boot (and started to be a default gem in 5.2). gem "bootsnap", require: false gem "browser" diff --git a/Gemfile.lock b/Gemfile.lock index 3e82890113a..012a1b5e716 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -57,10 +57,10 @@ GIT GIT remote: https://github.com/department-of-veterans-affairs/ruby-bgs.git - revision: 7d7c67f7bad5e5aa03e257f0d8e57a4aa1a6cbbf - ref: 7d7c67f7bad5e5aa03e257f0d8e57a4aa1a6cbbf + revision: 5f47e7b2656ef347d314ef43c93d38a9f20816ec + ref: 5f47e7b2656ef347d314ef43c93d38a9f20816ec specs: - bgs (0.2) + bgs (0.3) httpclient nokogiri (>= 1.11.0.rc4) savon (~> 2.12) diff --git a/app/controllers/appeals_controller.rb b/app/controllers/appeals_controller.rb index 609ad735de4..73898b4f39c 100644 --- a/app/controllers/appeals_controller.rb +++ b/app/controllers/appeals_controller.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ClassLength class AppealsController < ApplicationController include UpdatePOAConcern before_action :react_routed @@ -49,7 +50,7 @@ def show_case_list end end - # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def fetch_notification_list appeals_id = params[:appeals_id] respond_to do |format| @@ -84,7 +85,7 @@ def fetch_notification_list end end end - # rubocop:enable Metrics/MethodLength, Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength def document_count doc_count = EFolderService.document_count(appeal.veteran_file_number, current_user) @@ -154,7 +155,9 @@ def show def edit # only AMA appeals may call /edit - return not_found if appeal.is_a?(LegacyAppeal) + # this was removed for MST/PACT initiative to edit MST/PACT for legacy issues + return not_found if appeal.is_a?(LegacyAppeal) && + !FeatureToggle.enabled?(:legacy_mst_pact_identification, user: RequestStore[:current_user]) end helper_method :appeal, :url_appeal_uuid @@ -168,18 +171,12 @@ def url_appeal_uuid end def update - if request_issues_update.perform! - # if cc appeal, create SendInitialNotificationLetterTask - if appeal.contested_claim? && FeatureToggle.enabled?(:cc_appeal_workflow) - # check if an existing letter task is open - existing_letter_task_open = appeal.tasks.any? do |task| - task.class == SendInitialNotificationLetterTask && task.status == "assigned" - end - # create SendInitialNotificationLetterTask unless one is open - send_initial_notification_letter unless existing_letter_task_open - end - + if appeal.is_a?(LegacyAppeal) && + FeatureToggle.enabled?(:legacy_mst_pact_identification, user: RequestStore[:current_user]) + legacy_mst_pact_updates + elsif request_issues_update.perform! set_flash_success_message + create_subtasks! render json: { beforeIssues: request_issues_update.before_issues.map(&:serialize), @@ -193,6 +190,18 @@ def update private + def create_subtasks! + # if cc appeal, create SendInitialNotificationLetterTask + if appeal.contested_claim? && FeatureToggle.enabled?(:cc_appeal_workflow) + # check if an existing letter task is open + existing_letter_task_open = appeal.tasks.any? do |task| + task.class == SendInitialNotificationLetterTask && task.status == "assigned" + end + # create SendInitialNotificationLetterTask unless one is open + send_initial_notification_letter unless existing_letter_task_open + end + end + # :reek:DuplicateMethodCall { allow_calls: ['result.extra'] } # :reek:FeatureEnvy def render_search_results_as_json(result) @@ -260,7 +269,238 @@ def review_edited_message "You have successfully " + [added_issues, removed_issues, withdrawn_issues].compact.to_sentence + "." end + # check if changes in params + def mst_pact_changes? + request_issues_update.mst_edited_issues.any? || request_issues_update.pact_edited_issues.any? + end + + # format MST/PACT edit success banner message + # rubocop:disable Layout/LineLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength + def mst_and_pact_edited_issues + # list of edit counts + mst_added = 0 + mst_removed = 0 + pact_added = 0 + pact_removed = 0 + # get edited issues from params and reject new issues without id + if !appeal.is_a?(LegacyAppeal) + existing_issues = params[:request_issues].reject { |iss| iss[:request_issue_id].nil? } + + # get added issues + new_issues = request_issues_update.after_issues - request_issues_update.before_issues + # get removed issues + removed_issues = request_issues_update.before_issues - request_issues_update.after_issues + + # calculate edits + existing_issues.each do |issue_edit| + # find the original issue and compare MST/PACT changes + before_issue = request_issues_update.before_issues.find { |b_issue| b_issue.id == issue_edit[:request_issue_id].to_i } + + # increment edit counts if they meet the criteria for added/removed + mst_added += 1 if issue_edit[:mst_status] != before_issue.mst_status && issue_edit[:mst_status] + mst_removed += 1 if issue_edit[:mst_status] != before_issue.mst_status && !issue_edit[:mst_status] + pact_added += 1 if issue_edit[:pact_status] != before_issue.pact_status && issue_edit[:pact_status] + pact_removed += 1 if issue_edit[:pact_status] != before_issue.pact_status && !issue_edit[:pact_status] + end + else + existing_issues = legacy_issue_params[:request_issues] + existing_issues.each do |issue_edit| + mst_added += 1 if legacy_issues_with_updated_mst_pact_status[:mst_edited].include?(issue_edit) && issue_edit[:mst_status] + mst_removed += 1 if legacy_issues_with_updated_mst_pact_status[:mst_edited].include?(issue_edit) && !issue_edit[:mst_status] + pact_added += 1 if legacy_issues_with_updated_mst_pact_status[:pact_edited].include?(issue_edit) && issue_edit[:pact_status] + pact_removed += 1 if legacy_issues_with_updated_mst_pact_status[:pact_edited].include?(issue_edit) && !issue_edit[:pact_status] + new_issues = [] + removed_issues = [] + end + end + + # return if no edits, removals, or additions + return if (mst_added + mst_removed + pact_added + pact_removed == 0) && removed_issues.empty? && new_issues.empty? + + message = [] + + message << "#{pact_removed} #{'issue'.pluralize(pact_removed)} unmarked as PACT" unless pact_removed == 0 + message << "#{mst_removed} #{'issue'.pluralize(mst_removed)} unmarked as MST" unless mst_removed == 0 + message << "#{mst_added} #{'issue'.pluralize(mst_added)} marked as MST" unless mst_added == 0 + message << "#{pact_added} #{'issue'.pluralize(pact_added)} marked as PACT" unless pact_added == 0 + + # add in removed message and added message, if any + message << create_mst_pact_message_for_new_and_removed_issues(new_issues, "added") unless new_issues.empty? + message << create_mst_pact_message_for_new_and_removed_issues(removed_issues, "removed") unless removed_issues.empty? + + message.flatten + end + # rubocop:enable Layout/LineLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength + + # create MST/PACT message for added/removed issues + # rubocop:disable Layout/LineLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def create_mst_pact_message_for_new_and_removed_issues(issues, type) + special_issue_message = [] + # check if any added/removed issues have MST/PACT and get the count + mst_count = issues.count { |issue| issue.mst_status && !issue.pact_status } + pact_count = issues.count { |issue| issue.pact_status && !issue.mst_status } + both_count = issues.count { |issue| issue.pact_status && issue.mst_status } + none_count = issues.count { |issue| !issue.pact_status && !issue.mst_status } + + special_issue_message << "#{mst_count} #{'issue'.pluralize(mst_count)} with MST #{type}" unless mst_count == 0 + special_issue_message << "#{pact_count} #{'issue'.pluralize(pact_count)} with PACT #{type}" unless pact_count == 0 + special_issue_message << "#{both_count} #{'issue'.pluralize(both_count)} with MST and PACT #{type}" unless both_count == 0 + special_issue_message << "#{none_count} #{'issue'.pluralize(none_count)} #{type}" unless none_count == 0 + + special_issue_message + end + # rubocop:enable Layout/LineLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + # check if there is a change in mst/pact on legacy issue + # if there is a change, creat an issue update task + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def legacy_mst_pact_updates + legacy_issue_params[:request_issues].each do |current_issue| + issue = appeal.issues.find { |iss| iss.vacols_sequence_id == current_issue[:vacols_sequence_id].to_i } + + # Check for changes in mst/pact status + next unless issue.mst_status != current_issue[:mst_status] || issue.pact_status != current_issue[:pact_status] + + # If there is a change : + # Create issue_update_task to populate casetimeline if there is a change + create_legacy_issue_update_task(issue, current_issue) + + # Grab record from Vacols database to issue. + # When updating an Issue, method in IssueMapper and IssueRepo requires the attrs show below in issue_attrs:{} + record = VACOLS::CaseIssue.find_by(isskey: appeal.vacols_id, issseq: current_issue[:vacols_sequence_id]) + Issue.update_in_vacols!( + vacols_id: appeal.vacols_id, + vacols_sequence_id: current_issue[:vacols_sequence_id], + issue_attrs: { + mst_status: current_issue[:mst_status] ? "Y" : "N", + pact_status: current_issue[:pact_status] ? "Y" : "N", + program: record[:issprog], + issue: record[:isscode], + level_1: record[:isslev1], + level_2: record[:isslev2], + level_3: record[:isslev3] + } + ) + end + set_flash_mst_edit_message + render json: { issues: json_issues }, status: :ok + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + def json_issues + appeal.issues.map do |issue| + ::WorkQueue::LegacyIssueSerializer.new(issue).serializable_hash[:data][:attributes] + end + end + + def legacy_issues_with_updated_mst_pact_status + mst_edited = legacy_issue_params[:request_issues].find_all do |current_issue| + issue = appeal.issues.find { |iss| iss.vacols_sequence_id == current_issue[:vacols_sequence_id].to_i } + issue.mst_status != current_issue[:mst_status] + end + pact_edited = legacy_issue_params[:request_issues].find_all do |current_issue| + issue = appeal.issues.find { |iss| iss.vacols_sequence_id == current_issue[:vacols_sequence_id].to_i } + issue.pact_status != current_issue[:pact_status] + end + { mst_edited: mst_edited, pact_edited: pact_edited } + end + + def legacy_issue_params + # Checks the keys for each object in request_issues array + request_issue_params = params.require("request_issues").each do |current_param| + current_param.permit(:request_issue_id, + :withdrawal_date, + :vacols_sequence_id, + :mst_status, + :pact_status, + :mst_status_update_reason_notes, + :pact_status_update_reason_notes).to_h + end + + # After check, recreate safe_params object and include vacols_uniq_id + safe_params = { + request_issues: request_issue_params, + vacols_user_id: current_user.vacols_uniq_id + } + safe_params + end + + def create_params + legacy_issue_params.merge(vacols_id: appeal.vacols_id) + end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # :reek:FeatureEnvy + def create_legacy_issue_update_task(before_issue, current_issue) + user = RequestStore[:current_user] + + # close out any tasks that might be open + open_issue_task = Task.where( + assigned_to: SpecialIssueEditTeam.singleton + ).where(status: "assigned").where(appeal: appeal) + open_issue_task[0].delete unless open_issue_task.empty? + + task = IssuesUpdateTask.create!( + appeal: appeal, + parent: appeal.root_task, + assigned_to: SpecialIssueEditTeam.singleton, + assigned_by: user, + completed_by: user + ) + # format the task instructions and close out + set = CaseTimelineInstructionSet.new( + change_type: "Edited Issue", + issue_category: [ + "Benefit Type: #{before_issue.labels[0]}\n", + "Issue: #{before_issue.labels[1..-2].join("\n")}\n", + "Code: #{[before_issue.codes[-1], before_issue.labels[-1]].join(' - ')}\n", + "Note: #{before_issue.note}\n", + "Disposition: #{before_issue.readable_disposition}\n" + ].compact.join("\r\n"), + benefit_type: "", + original_mst: before_issue.mst_status, + original_pact: before_issue.pact_status, + edit_mst: current_issue[:mst_status], + edit_pact: current_issue[:pact_status] + ) + task.format_instructions(set) + task.completed! + + # create SpecialIssueChange record to log the changes + SpecialIssueChange.create!( + issue_id: before_issue.id, + appeal_id: appeal.id, + appeal_type: "LegacyAppeal", + task_id: task.id, + created_at: Time.zone.now.utc, + created_by_id: RequestStore[:current_user].id, + created_by_css_id: RequestStore[:current_user].css_id, + original_mst_status: before_issue.mst_status, + original_pact_status: before_issue.pact_status, + updated_mst_status: current_issue[:mst_status], + updated_pact_status: current_issue[:pact_status], + change_category: "Edited Issue" + ) + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + # updated flash message to show mst/pact message if mst/pact changes (not to legacy) + # rubocop:disable Layout/LineLength def set_flash_success_message + return set_flash_mst_edit_message if mst_pact_changes? && + (FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) || + FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user])) + + set_flash_edit_message + end + # rubocop:enable Layout/LineLength + + # create success message with added and removed issues + def set_flash_mst_edit_message + flash[:mst_pact_edited] = mst_and_pact_edited_issues + end + + def set_flash_edit_message flash[:edited] = if request_issues_update.after_issues.empty? review_removed_message elsif (request_issues_update.after_issues - request_issues_update.withdrawn_issues).empty? @@ -355,3 +595,4 @@ def get_appeal_object(appeals_id) end end end +# rubocop:enable Metrics/ClassLength diff --git a/app/controllers/case_reviews_controller.rb b/app/controllers/case_reviews_controller.rb index 45b15d0df80..290a60811ba 100644 --- a/app/controllers/case_reviews_controller.rb +++ b/app/controllers/case_reviews_controller.rb @@ -10,8 +10,8 @@ def set_application end def complete - result = CompleteCaseReview.new(case_review_class: case_review_class, params: complete_params).call - + new_complete_case_review = CompleteCaseReview.new(case_review_class: case_review_class, params: complete_params) + result = new_complete_case_review.call if result.success? case_review = result.extra[:case_review] render json: { @@ -74,6 +74,7 @@ def judge_case_review_params def issues_params # This is a combined list of params for ama and legacy appeals + # Reprsents the information the front end is sending to create a decision issue object [ :id, :disposition, @@ -81,6 +82,8 @@ def issues_params :readjudication, :benefit_type, :diagnostic_code, + :mst_status, + :pact_status, request_issue_ids: [], remand_reasons: [ :code, diff --git a/app/controllers/intakes_controller.rb b/app/controllers/intakes_controller.rb index 2a2f7ded773..37e21cb3ba0 100644 --- a/app/controllers/intakes_controller.rb +++ b/app/controllers/intakes_controller.rb @@ -10,7 +10,6 @@ class IntakesController < ApplicationController def index no_cache - respond_to do |format| format.html { render(:index) } end @@ -43,6 +42,7 @@ def destroy def review if intake.review!(params) render json: intake.ui_hash + else render json: { error_codes: intake.review_errors }, status: :unprocessable_entity end @@ -98,6 +98,7 @@ def index_props { userDisplayName: current_user.display_name, userCanIntakeAppeals: current_user.can_intake_appeals?, + userCanEditIntakeIssues: current_user.can_edit_intake_issues?, serverIntake: intake_ui_hash, dropdownUrls: dropdown_urls, applicationUrls: application_urls, @@ -151,6 +152,10 @@ def feature_toggle_ui_hash filedByVaGovHlr: FeatureToggle.enabled?(:filed_by_va_gov_hlr, user: current_user), updatedIntakeForms: FeatureToggle.enabled?(:updated_intake_forms, user: current_user), eduPreDocketAppeals: FeatureToggle.enabled?(:edu_predocket_appeals, user: current_user), + mstIdentification: FeatureToggle.enabled?(:mst_identification, user: current_user), + pactIdentification: FeatureToggle.enabled?(:pact_identification, user: current_user), + legacyMstPactIdentification: FeatureToggle.enabled?(:legacy_mst_pact_identification, user: current_user), + justificationReason: FeatureToggle.enabled?(:justification_reason, user: current_user), updatedAppealForm: FeatureToggle.enabled?(:updated_appeal_form, user: current_user), hlrScUnrecognizedClaimants: FeatureToggle.enabled?(:hlr_sc_unrecognized_claimants, user: current_user), vhaClaimReviewEstablishment: FeatureToggle.enabled?(:vha_claim_review_establishment, user: current_user), diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 980f828e09f..fa6d8052b65 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# IssuesController for LegacyAppeals class IssuesController < ApplicationController before_action :validate_access_to_task @@ -16,24 +17,45 @@ class IssuesController < ApplicationController handle_non_critical_error("issues", e) end + # rubocop:disable Layout/LineLength def create return record_not_found unless appeal - Issue.create_in_vacols!(issue_attrs: create_params) + issue = Issue.create_in_vacols!(issue_attrs: create_params) + + # create MST/PACT task if issue was created + if convert_to_bool(create_params[:mst_status]) || + convert_to_bool(create_params[:pact_status]) + issue_in_caseflow = appeal.issues.find { |iss| iss.vacols_sequence_id == issue.issseq.to_i } + create_legacy_issue_update_task(issue_in_caseflow) if FeatureToggle.enabled?(:legacy_mst_pact_identification, user: RequestStore[:current_user]) + end render json: { issues: json_issues }, status: :created end + # rubocop:enable Layout/LineLength + # rubocop:disable Layout/LineLength, Metrics/AbcSize def update return record_not_found unless appeal + issue = appeal.issues.find { |iss| iss.vacols_sequence_id == params[:vacols_sequence_id].to_i } + if issue.mst_status != convert_to_bool(params[:issues][:mst_status]) || + issue.pact_status != convert_to_bool(params[:issues][:pact_status]) + create_legacy_issue_update_task(issue) if FeatureToggle.enabled?(:legacy_mst_pact_identification, user: RequestStore[:current_user]) + end + Issue.update_in_vacols!( vacols_id: appeal.vacols_id, vacols_sequence_id: params[:vacols_sequence_id], issue_attrs: issue_params ) + + # Set LegacyAppeal issues to nil in order to refresh and retrieve new update + appeal.issues = nil if appeal.is_legacy? + render json: { issues: json_issues }, status: :ok end + # rubocop:enable Layout/LineLength, Metrics/AbcSize def destroy return record_not_found unless appeal @@ -47,6 +69,82 @@ def destroy private + # rubocop:disable Layout/LineLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + def create_legacy_issue_update_task(issue) + user = current_user + + # close out any tasks that might be open + open_issue_task = Task.where( + assigned_to: SpecialIssueEditTeam.singleton + ).where(status: "assigned").where(appeal: appeal) + open_issue_task[0].delete unless open_issue_task.empty? + + task = IssuesUpdateTask.create!( + appeal: appeal, + parent: appeal.root_task, + assigned_to: SpecialIssueEditTeam.singleton, + assigned_by: user, + completed_by: user + ) + + # set up data for added or edited issue depending on the params action + disposition = issue.readable_disposition.nil? ? "N/A" : issue.readable_disposition + change_category = (params[:action] == "create") ? "Added Issue" : "Edited Issue" + updated_mst_status = convert_to_bool(params[:issues][:mst_status]) unless params[:action] == "create" + updated_pact_status = convert_to_bool(params[:issues][:pact_status]) unless params[:action] == "create" + + note = params[:issues][:note].nil? ? "N/A" : params[:issues][:note] + # use codes from params to get descriptions + # opting to use params vs issue model to capture in-flight issue changes + program_code = params[:issues][:program] + issue_code = params[:issues][:issue] + level_1_code = params[:issues][:level_1] + + # line up param codes to their descriptions + param_issue = Constants::ISSUE_INFO[program_code] + iss = param_issue["levels"][issue_code]["description"] unless issue_code.nil? + level_1_description = level_1_code.nil? ? "N/A" : param_issue["levels"][issue_code]["levels"][level_1_code]["description"] + + # format the task instructions and close out + set = CaseTimelineInstructionSet.new( + change_type: change_category, + issue_category: [ + "Benefit Type: #{param_issue['description']}\n", + "Issue: #{iss}\n", + "Code: #{[level_1_code, level_1_description].join(' - ')}\n", + "Note: #{note}\n", + "Disposition: #{disposition}\n" + ].compact.join("\r\n"), + benefit_type: "", + original_mst: issue.mst_status, + original_pact: issue.pact_status, + edit_mst: updated_mst_status, + edit_pact: updated_pact_status + ) + task.format_instructions(set) + task.completed! + # create SpecialIssueChange record to log the changes + SpecialIssueChange.create!( + issue_id: issue.id, + appeal_id: appeal.id, + appeal_type: "LegacyAppeal", + task_id: task.id, + created_at: Time.zone.now.utc, + created_by_id: user.id, + created_by_css_id: user.css_id, + original_mst_status: issue.mst_status, + original_pact_status: issue.pact_status, + updated_mst_status: updated_mst_status, + updated_pact_status: updated_pact_status, + change_category: change_category + ) + end + # rubocop:enable Layout/LineLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + + def convert_to_bool(status) + status == "Y" + end + def json_issues appeal.issues.map do |issue| ::WorkQueue::LegacyIssueSerializer.new(issue).serializable_hash[:data][:attributes] @@ -68,7 +166,9 @@ def issue_params :issue, :level_1, :level_2, - :level_3).to_h + :level_3, + :mst_status, + :pact_status).to_h safe_params[:vacols_user_id] = current_user.vacols_uniq_id safe_params end diff --git a/app/mappers/issue_mapper.rb b/app/mappers/issue_mapper.rb index f4492bbeadb..92e79a28384 100644 --- a/app/mappers/issue_mapper.rb +++ b/app/mappers/issue_mapper.rb @@ -10,7 +10,9 @@ module IssueMapper note: :issdesc, disposition: :issdc, disposition_date: :issdcls, - vacols_id: :isskey + vacols_id: :isskey, + mst_status: :issmst, + pact_status: :isspact }.freeze # For disposition descriptions, please see the VACOLS_DISPOSITIONS_BY_ID file @@ -39,10 +41,10 @@ def rename_and_validate_vacols_attrs(action:, issue_attrs:) private def validate!(issue_attrs) - return if (issue_attrs.keys & [:issprog, :isscode, :isslev1, :isslev2, :isslev3]).empty? + return if (issue_attrs.keys & [:issprog, :isscode, :isslev1, :isslev2, :isslev3, :issmst, :isspact]).empty? - if issue_attrs.slice(:issprog, :isscode, :isslev1, :isslev2, :isslev3).size != 5 - msg = "All keys must be present: program, issue, level_1, level_2, level_3" + if issue_attrs.slice(:issprog, :isscode, :isslev1, :isslev2, :isslev3, :issmst, :isspact).size != 7 + msg = "All keys must be present: program, issue, level_1, level_2, level_3, mst_status, pact_status" fail Caseflow::Error::IssueRepositoryError, message: msg end diff --git a/app/models/appeal.rb b/app/models/appeal.rb index ca8fff8310e..13e6ecc6a48 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -254,6 +254,28 @@ def contested_claim? end end + # :reek:RepeatedConditionals + # decision issue status overrules request issues/special issue list for both mst and pact + def mst? + return false unless FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) + + return decision_issues.any?(&:mst_status) unless decision_issues.empty? + + request_issues.active.any?(&:mst_status) || + (special_issue_list && + special_issue_list.created_at < "2023-06-01".to_date && + special_issue_list.military_sexual_trauma) + end + + # :reek:RepeatedConditionals + def pact? + return false unless FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) + + return decision_issues.any?(&:pact_status) unless decision_issues.empty? + + request_issues.active.any?(&:pact_status) + end + # Returns the most directly responsible party for an appeal when it is at the Board, # mirroring Legacy Appeals' location code in VACOLS def assigned_to_location @@ -280,6 +302,7 @@ def decorated_with_status AppealStatusApiDecorator.new(self) end + # :reek:RepeatedConditionals def active_request_issues_or_decision_issues decision_issues.empty? ? active_request_issues : fetch_all_decision_issues end @@ -351,6 +374,7 @@ def clone_cavc_remand(parent_appeal, user_css_id) dup_remand&.save end + # :reek:RepeatedConditionals # clone issues clones request_issues the user selected # and anydecision_issues/decision_request_issues tied to the request issue # rubocop:disable Metrics/AbcSize, Metrics/MethodLength @@ -470,7 +494,7 @@ def clone_hearings(parent_appeal) end end - # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def clone_task_tree(parent_appeal, user_css_id) # get the task tree from the parent parent_ordered_tasks = parent_appeal.tasks.order(:created_at) @@ -505,9 +529,9 @@ def clone_task_tree(parent_appeal, user_css_id) break if parent_appeal.tasks.count == tasks.count end end - # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity - # clone_task is used for splitting an appeal, tie to css_id for split + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # clone_task is used for splitting an appeal, tie to css_id for split def clone_task(original_task, user_css_id) # clone the task dup_task = original_task.amoeba_dup @@ -919,6 +943,10 @@ def can_redistribute_appeal? return true if relevant_tasks.all?(&:closed?) end + def is_legacy? + false + end + private def business_lines_needing_assignment diff --git a/app/models/case_timeline_instruction_set.rb b/app/models/case_timeline_instruction_set.rb new file mode 100644 index 00000000000..de4af8ff15b --- /dev/null +++ b/app/models/case_timeline_instruction_set.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# :reek:TooManyInstanceVariables +class CaseTimelineInstructionSet + attr_reader :change_type, + :issue_category, + :benefit_type, + :original_mst, + :original_pact, + :edit_mst, + :edit_pact, + :mst_edit_reason, + :pact_edit_reason + + # rubocop:disable Metrics/ParameterLists + # :reek:LongParameterList and :reek:TooManyInstanceVariables + def initialize( + change_type:, + issue_category:, + benefit_type:, + original_mst:, + original_pact:, + edit_mst: nil, + edit_pact: nil, + mst_edit_reason: nil, + pact_edit_reason: nil + ) + @change_type = change_type + @issue_category = issue_category + @benefit_type = benefit_type + @original_mst = original_mst + @original_pact = original_pact + @edit_mst = edit_mst + @edit_pact = edit_pact + @mst_edit_reason = mst_edit_reason + @pact_edit_reason = pact_edit_reason + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/app/models/concerns/issue_updater.rb b/app/models/concerns/issue_updater.rb index aba47fca7a9..3b5347083be 100644 --- a/app/models/concerns/issue_updater.rb +++ b/app/models/concerns/issue_updater.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ModuleLength module IssueUpdater extend ActiveSupport::Concern @@ -35,8 +36,11 @@ def update_issue_dispositions_in_vacols! private + # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize + # :reek:FeatureEnvy def create_decision_issues! - issues.each do |issue_attrs| + ordered_issues = issues.sort_by { |issue| issue[:request_issue_ids]&.first } + ordered_issues.each do |issue_attrs| request_issues = appeal.request_issues.active_or_withdrawn.where(id: issue_attrs[:request_issue_ids]) next if request_issues.empty? @@ -47,15 +51,27 @@ def create_decision_issues! diagnostic_code: issue_attrs[:diagnostic_code].presence, participant_id: appeal.veteran.participant_id, decision_review: appeal, - caseflow_decision_date: appeal.decision_document&.decision_date + caseflow_decision_date: appeal.decision_document&.decision_date, + mst_status: issue_attrs[:mst_status], + pact_status: issue_attrs[:pact_status] ) request_issues.each do |request_issue| RequestDecisionIssue.create!(decision_issue: decision_issue, request_issue: request_issue) + + # compare the MST/PACT status of the orignial issue and decision to create task and record + next unless (request_issue.mst_status != decision_issue.mst_status && + FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user])) || + (request_issue.pact_status != decision_issue.pact_status && + FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user])) + + create_issue_update_task(request_issue, decision_issue) end + create_remand_reasons(decision_issue, issue_attrs[:remand_reasons] || []) end end + # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize def fail_if_not_all_request_issues_have_decision! unless appeal.every_request_issue_has_decision? @@ -108,4 +124,77 @@ def create_remand_reasons(decision_issue, remand_reasons_attrs) end end end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # :reek:FeatureEnvy + def create_issue_update_task(original_issue, decision_issue) + root_task = RootTask.find_or_create_by!(appeal: appeal) + + # close out any tasks that might be open + open_issue_task = Task.where( + assigned_to: SpecialIssueEditTeam.singleton + ).where(status: "assigned").where(appeal: appeal) + open_issue_task[0].delete unless open_issue_task.empty? + + task = IssuesUpdateTask.create!( + appeal: appeal, + parent: root_task, + assigned_to: SpecialIssueEditTeam.singleton, + assigned_by: RequestStore[:current_user], + completed_by: RequestStore[:current_user] + ) + + set = CaseTimelineInstructionSet.new( + change_type: "Edited Issue", + issue_category: task_text_helper( + [ + original_issue.contested_issue_description, + original_issue.nonrating_issue_category, + original_issue.nonrating_issue_description + ] + ), + benefit_type: task_text_benefit_type(original_issue), + original_mst: original_issue.mst_status, + original_pact: original_issue.pact_status, + edit_mst: decision_issue.mst_status, + edit_pact: decision_issue.pact_status + ) + task.format_instructions(set) + + task.completed! + + SpecialIssueChange.create!( + issue_id: original_issue.id, + appeal_id: appeal.id, + appeal_type: "Appeal", + task_id: task.id, + created_at: Time.zone.now.utc, + created_by_id: RequestStore[:current_user].id, + created_by_css_id: RequestStore[:current_user].css_id, + original_mst_status: original_issue.mst_status, + original_pact_status: original_issue.pact_status, + updated_mst_status: decision_issue.mst_status, + updated_pact_status: decision_issue.pact_status, + mst_from_vbms: original_issue&.vbms_mst_status, + pact_from_vbms: original_issue&.vbms_pact_status, + change_category: "Edited Decision Issue", + decision_issue_id: decision_issue.id + ) + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + def task_text_benefit_type(issue) + issue.benefit_type ? issue.benefit_type.capitalize : "" + end + + def task_text_helper(text_array) + if text_array.compact.length > 1 + text_array.compact.join(" - ") + elsif text_array.compact.length == 1 + text_array.join + else + "Description unavailable" + end + end end +# rubocop:enable Metrics/ModuleLength diff --git a/app/models/contestable_issue.rb b/app/models/contestable_issue.rb index 32a44afec5f..cf456ad3703 100644 --- a/app/models/contestable_issue.rb +++ b/app/models/contestable_issue.rb @@ -11,7 +11,7 @@ class ContestableIssue :decision_issue, :rating_issue_profile_date, :source_request_issues, :rating_issue_diagnostic_code, :source_decision_review, :rating_decision_reference_id, :rating_issue_subject_text, - :rating_issue_percent_number + :rating_issue_percent_number, :special_issues class << self def from_rating_issue(rating_issue, contesting_decision_review) @@ -30,7 +30,8 @@ def from_rating_issue(rating_issue, contesting_decision_review) # TODO: These should never be set unless there is a decision issue. We should refactor this to # account for that. source_request_issues: rating_issue.source_request_issues, - source_decision_review: rating_issue.source_request_issues.first&.decision_review + source_decision_review: rating_issue.source_request_issues.first&.decision_review, + special_issues: SpecialIssuesComparator.new(rating_issue).special_issues ) end @@ -61,6 +62,7 @@ def from_rating_decision(rating_decision, contesting_decision_review) description: rating_decision.decision_text, contesting_decision_review: contesting_decision_review, rating_issue_diagnostic_code: rating_decision.diagnostic_code, + special_issues: SpecialIssuesComparator.new(rating_decision).special_issues, is_rating: true # true even if rating_reference_id is nil ) end @@ -80,7 +82,9 @@ def serialize sourceReviewType: source_review_type, timely: timely?, latestIssuesInChain: serialize_latest_decision_issues, - isRating: is_rating + isRating: is_rating, + mstAvailable: mst_available?, + pactAvailable: pact_available? } end @@ -112,6 +116,33 @@ def timely? approx_decision_date && contesting_decision_review.timely_issue?(approx_decision_date) end + # cycle the issues to see if the past decision had any mst codes on contentions + def mst_available? + return false unless FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) + + source_request_issues.try(:each) do |issue| + return true if issue.mst_contention_status? || issue.mst_status? + end + special_issues&.each do |special_issue| + return true if special_issue[:mst_available] + end + false + end + + # cycle the issues to see if the past decision had any pact codes on contentions + def pact_available? + return false unless FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) + + source_request_issues.try(:each) do |issue| + return true if issue.pact_contention_status? || issue.pact_status? + end + special_issues&.each do |special_issue| + return true if special_issue[:pact_available] + end + + false + end + private def contested_by_request_issue diff --git a/app/models/decision_issue.rb b/app/models/decision_issue.rb index b2a8d76dc19..7ce7f6f3d9d 100644 --- a/app/models/decision_issue.rb +++ b/app/models/decision_issue.rb @@ -187,7 +187,9 @@ def create_contesting_request_issue!(appeal) nonrating_issue_category: nonrating_issue_category, benefit_type: benefit_type, decision_date: caseflow_decision_date, - veteran_participant_id: decision_review.veteran.participant_id + veteran_participant_id: decision_review.veteran.participant_id, + mst_status: mst_status, + pact_status: pact_status ) end diff --git a/app/models/etl/decision_issue.rb b/app/models/etl/decision_issue.rb index 4d05446e259..d0a1d455b30 100644 --- a/app/models/etl/decision_issue.rb +++ b/app/models/etl/decision_issue.rb @@ -3,6 +3,8 @@ # copy of decision_issues class ETL::DecisionIssue < ETL::Record + attr_accessor :mst_status, :pact_status + class << self private diff --git a/app/models/hearing_issue_note.rb b/app/models/hearing_issue_note.rb index 406f69515e7..e9b31029a2d 100644 --- a/app/models/hearing_issue_note.rb +++ b/app/models/hearing_issue_note.rb @@ -9,12 +9,14 @@ class HearingIssueNote < CaseflowRecord delegate :description, to: :request_issue delegate :notes, to: :request_issue delegate :benefit_type, to: :request_issue + delegate :mst_status, to: :request_issue + delegate :pact_status, to: :request_issue alias program benefit_type def to_hash serializable_hash( - methods: [:docket_name, :diagnostic_code, :description, :notes, :program], + methods: [:docket_name, :diagnostic_code, :description, :notes, :program, :mst_status, :pact_status], include: [hearing: { methods: [:external_id] }] ) end diff --git a/app/models/issue.rb b/app/models/issue.rb index 79de40bd2c7..385fe61436b 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -8,7 +8,8 @@ class Issue include ActiveModel::Serialization attr_accessor :id, :vacols_sequence_id, :codes, :disposition, :disposition_date, - :disposition_id, :readable_disposition, :close_date, :note + :disposition_id, :readable_disposition, :close_date, :note, + :mst_status, :pact_status # Labels are only loaded if we run the joins to ISSREF and VFTYPES (see VACOLS::CaseIssue) attr_writer :labels @@ -334,7 +335,9 @@ def load_from_vacols(hash) disposition_date: hash["issdcls"], # readable disposition is a string, i.e. "Remanded" readable_disposition: Constants::VACOLS_DISPOSITIONS_BY_ID[hash["issdc"]], - close_date: AppealRepository.normalize_vacols_date(hash["issdcls"]) + close_date: AppealRepository.normalize_vacols_date(hash["issdcls"]), + mst_status: hash["issmst"]&.casecmp("y")&.zero? || false, + pact_status: hash["isspact"]&.casecmp("y")&.zero? || false ) end diff --git a/app/models/legacy_appeal.rb b/app/models/legacy_appeal.rb index 3ed9539fb55..823eaa563dd 100644 --- a/app/models/legacy_appeal.rb +++ b/app/models/legacy_appeal.rb @@ -632,6 +632,23 @@ def special_issues? end end + def mst? + return false unless FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) && + FeatureToggle.enabled?(:legacy_mst_pact_identification, user: RequestStore[:current_user]) + + issues.any?(&:mst_status) || + (special_issue_list && + special_issue_list.created_at < "2023-06-01".to_date && + special_issue_list.military_sexual_trauma) + end + + def pact? + return false unless FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) && + FeatureToggle.enabled?(:legacy_mst_pact_identification, user: RequestStore[:current_user]) + + issues.any?(&:pact_status) + end + def documents_with_type(*types) @documents_by_type ||= {} types.reduce([]) do |accumulator, type| @@ -918,6 +935,28 @@ def claimant_participant_id veteran_is_not_claimant ? person_for_appellant&.participant_id : veteran&.participant_id end + # :reek:FeatureEnvy + def hearing_day_if_schedueled + hearing_date = Hearing.find_by(appeal_id: id) + + if hearing_date.nil? + nil + + else + hearing_date.hearing_day.scheduled_for + end + end + + def ui_hash + Intake::LegacyAppealSerializer.new(self).serializable_hash[:data][:attributes] + end + + # rubocop:disable Naming/PredicateName + def is_legacy? + true + end + # rubocop:enable Naming/PredicateName + private def soc_eligible_for_opt_in?(receipt_date:, covid_flag: false) diff --git a/app/models/legacy_hearing.rb b/app/models/legacy_hearing.rb index 07bbbfb9acc..370ce6aa900 100644 --- a/app/models/legacy_hearing.rb +++ b/app/models/legacy_hearing.rb @@ -268,6 +268,18 @@ def original_request_type end end + # :reek:FeatureEnvy + def prepare_worksheet_issues + worksheet_issues = [] + appeal.worksheet_issues.each_with_index do |wi, idx| + worksheet_issues.push(wi.attributes) + issue = appeal.issues.find { |iss| iss.vacols_sequence_id.to_i == wi[:vacols_sequence_id].to_i } + worksheet_issues[idx][:mst_status] = issue&.mst_status + worksheet_issues[idx][:pact_status] = issue&.pact_status + end + worksheet_issues + end + def quick_to_hash(current_user_id) ::LegacyHearingSerializer.quick( self, @@ -324,7 +336,9 @@ def current_issue_count # we want to fetch it from BGS, save it to the DB, then return it def military_service super || begin - update(military_service: veteran.periods_of_service.join("\n")) if persisted? && veteran + if !HearingDay.find_by(id: hearing_day_vacols_id).nil? || !HearingDay.find_by(id: hearing_day_id).nil? + update(military_service: veteran.periods_of_service.join("\n")) if persisted? && veteran + end super end end diff --git a/app/models/organizations/special_issue_edit_team.rb b/app/models/organizations/special_issue_edit_team.rb new file mode 100644 index 00000000000..8b8643ef3dd --- /dev/null +++ b/app/models/organizations/special_issue_edit_team.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class SpecialIssueEditTeam < Organization + alias_attribute :full_name, :name + + def self.singleton + SpecialIssueEditTeam.first || SpecialIssueEditTeam.create( + name: "Special Issue Edit Team", + url: "special-issue-edit-team" + ) + end +end diff --git a/app/models/rating.rb b/app/models/rating.rb index 2b90da330dd..f743e2f99ad 100644 --- a/app/models/rating.rb +++ b/app/models/rating.rb @@ -65,12 +65,13 @@ def issues issues.map do |issue| most_recent_disability_hash_for_issue = map_of_dis_sn_to_most_recent_disability_hash[issue[:dis_sn]] most_recent_evaluation_for_issue = most_recent_disability_hash_for_issue&.most_recent_evaluation + special_issues = most_recent_disability_hash_for_issue&.special_issues if most_recent_evaluation_for_issue issue[:dgnstc_tc] = most_recent_evaluation_for_issue[:dgnstc_tc] issue[:prcnt_no] = most_recent_evaluation_for_issue[:prcnt_no] end - + issue[:special_issues] = special_issues if special_issues RatingIssue.from_bgs_hash(self, issue) end end @@ -81,6 +82,9 @@ def decisions disability_data = Array.wrap(rating_profile[:disabilities] || rating_profile.dig(:disability_list, :disability)) disability_data.map do |disability| + most_recent_disability_hash_for_issue = map_of_dis_sn_to_most_recent_disability_hash[disability[:dis_sn]] + special_issues = most_recent_disability_hash_for_issue&.special_issues + disability[:special_issues] = special_issues if special_issues RatingDecision.from_bgs_disability(self, disability) end end diff --git a/app/models/rating_decision.rb b/app/models/rating_decision.rb index 915a48369b4..2cce62f309c 100644 --- a/app/models/rating_decision.rb +++ b/app/models/rating_decision.rb @@ -27,9 +27,12 @@ class RatingDecision :promulgation_date, :rating_sequence_number, :rating_issue_reference_id, - :type_name + :type_name, + :special_issues, + :rba_contentions_data class << self + # rubocop:disable Metrics/MethodLength def from_bgs_disability(rating, disability) latest_evaluation = RatingProfileDisability.new(disability).most_recent_evaluation || {} new( @@ -49,9 +52,12 @@ def from_bgs_disability(rating, disability) profile_date: rating.profile_date, promulgation_date: rating.promulgation_date, participant_id: rating.participant_id, - benefit_type: rating.pension? ? :pension : :compensation + benefit_type: rating.pension? ? :pension : :compensation, + special_issues: disability[:special_issues], + rba_contentions_data: disability[:rba_contentions_data] ) end + # rubocop:enable Metrics/MethodLength def deserialize(hash) new(hash) diff --git a/app/models/rating_issue.rb b/app/models/rating_issue.rb index 0569fd13b98..382c4c37a1f 100644 --- a/app/models/rating_issue.rb +++ b/app/models/rating_issue.rb @@ -17,7 +17,8 @@ class RatingIssue :promulgation_date, :rba_contentions_data, :reference_id, - :subject_text + :subject_text, + :special_issues # adding another field? * ) @@ -48,11 +49,17 @@ def from_bgs_hash(rating, bgs_data) promulgation_date: rating.promulgation_date, rba_contentions_data: ensure_array_of_hashes(bgs_data.dig(:rba_issue_contentions)), reference_id: bgs_data[:rba_issue_id], - subject_text: bgs_data[:subjct_txt] + subject_text: bgs_data[:subjct_txt], + special_issues: bgs_data[:special_issues] ) end def deserialize(serialized_hash) + DataDogService.increment_counter( + metric_group: "mst_pact_group", + metric_name: "bgs_service.previous_service_call.rating_issue", + app_name: RequestStore[:application] + ) new( serialized_hash.slice( :benefit_type, @@ -64,7 +71,8 @@ def deserialize(serialized_hash) :promulgation_date, :rba_contentions_data, :reference_id, - :subject_text + :subject_text, + :special_issues ).merge(associated_end_products: deserialize_end_products(serialized_hash)) ) end diff --git a/app/models/request_issue.rb b/app/models/request_issue.rb index 99fdbc5ad4b..9cf778c8119 100644 --- a/app/models/request_issue.rb +++ b/app/models/request_issue.rb @@ -190,7 +190,7 @@ def from_intake_data(data, decision_review: nil) private - # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def attributes_from_intake_data(data) contested_issue_present = attributes_look_like_contested_issue?(data) issue_text = (data[:is_unidentified] || data[:verified_unidentified_issue]) ? data[:decision_text] : nil @@ -219,10 +219,16 @@ def attributes_from_intake_data(data) edited_description: data[:edited_description], correction_type: data[:correction_type], verified_unidentified_issue: data[:verified_unidentified_issue], - is_predocket_needed: data[:is_predocket_needed] + is_predocket_needed: data[:is_predocket_needed], + mst_status: data[:mst_status], + vbms_mst_status: data[:vbms_mst_status], + mst_status_update_reason_notes: data[:mst_status_update_reason_notes], + pact_status: data[:pact_status], + vbms_pact_status: data[:vbms_pact_status], + pact_status_update_reason_notes: data[:pact_status_update_reason_notes] } end - # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize def attributes_look_like_contested_issue?(data) data[:rating_issue_reference_id] || @@ -259,6 +265,34 @@ def status_active? end_product_establishment.status_active? end + def mst_contention_status? + return false if bgs_contention.nil? + + if bgs_contention.special_issues.is_a?(Hash) + return bgs_contention.special_issues[:spis_tc] == "MST" if bgs_contention&.special_issues + elsif bgs_contention.special_issues.is_a?(Array) + bgs_contention.special_issues.each do |issue| + return true if issue[:spis_tc] == "MST" + end + end + false + end + + def pact_contention_status? + return false if bgs_contention.nil? + + if bgs_contention.special_issues.is_a?(Hash) + if bgs_contention&.special_issues + return %w[PACT PACTDICRE PEES1].include?(bgs_contention.special_issues[:spis_tc]) + end + elsif bgs_contention.special_issues.is_a?(Array) + bgs_contention.special_issues.each do |issue| + return true if %w[PACT PACTDICRE PEES1].include?(issue[:spis_tc]) + end + end + false + end + def rating? !!associated_rating_issue? || !!previous_rating_issue? || @@ -644,7 +678,7 @@ def contention_missing? end def contention - end_product_establishment.contention_for_object(self) + end_product_establishment&.contention_for_object(self) end def bgs_contention diff --git a/app/models/request_issues_update.rb b/app/models/request_issues_update.rb index 8b8c29a4001..2e9cedeed31 100644 --- a/app/models/request_issues_update.rb +++ b/app/models/request_issues_update.rb @@ -16,6 +16,7 @@ class RequestIssuesUpdate < CaseflowRecord delegate :withdrawn_issues, to: :withdrawal delegate :corrected_issues, :correction_issues, to: :correction + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def perform! return false unless validate_before_perform return false if processed? @@ -28,8 +29,14 @@ def perform! after_request_issue_ids: after_issues.map(&:id), withdrawn_request_issue_ids: withdrawn_issues.map(&:id), edited_request_issue_ids: edited_issues.map(&:id), + mst_edited_request_issue_ids: mst_edited_issues.map(&:id), + pact_edited_request_issue_ids: pact_edited_issues.map(&:id), corrected_request_issue_ids: corrected_issues.map(&:id) ) + if FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) || + FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) + create_mst_pact_issue_update_tasks + end create_business_line_tasks! if added_issues.present? cancel_active_tasks submit_for_processing! @@ -39,6 +46,7 @@ def perform! true end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize def process_job if run_async? @@ -90,8 +98,17 @@ def edited_issues @edited_issues ||= edited_request_issue_ids ? fetch_edited_issues : calculate_edited_issues end + def mst_edited_issues + @mst_edited_issues ||= mst_edited_request_issue_ids ? fetch_mst_edited_issues : calculate_mst_edited_issues + end + + def pact_edited_issues + @pact_edited_issues ||= pact_edited_request_issue_ids ? fetch_pact_edited_issues : calculate_pact_edited_issues + end + def all_updated_issues - added_issues + removed_issues + withdrawn_issues + edited_issues + correction_issues + added_issues + removed_issues + withdrawn_issues + edited_issues + + correction_issues + mst_edited_issues + pact_edited_issues end private @@ -115,6 +132,18 @@ def calculate_edited_issues end end + def calculate_mst_edited_issues + mst_edited_issue_data.map do |mst_issue_data| + review.find_or_build_request_issue_from_intake_data(mst_issue_data) + end + end + + def calculate_pact_edited_issues + pact_edited_issue_data.map do |pact_issue_data| + review.find_or_build_request_issue_from_intake_data(pact_issue_data) + end + end + def edited_issue_data return [] unless @request_issues_data @@ -128,6 +157,33 @@ def edited_issue?(request_issue) request_issue[:request_issue_id] end + def mst_edited_issue_data + return [] unless @request_issues_data + + # cycle through the request issue change data for changes in before/after MST/PACT + @request_issues_data.select do |issue| + # skip if the issue is a new issue + next if issue[:request_issue_id].nil? + + # find the before issue + original_issue = before_issues.find { |bi| bi&.id == issue[:request_issue_id].to_i } + original_issue&.mst_status != !!issue[:mst_status] + end + end + + def pact_edited_issue_data + return [] unless @request_issues_data + + @request_issues_data.select do |issue| + # skip if the issue is a new issue + next if issue[:request_issue_id].nil? + + # find the before issue + original_issue = before_issues.find { |bi| bi.id == issue[:request_issue_id].to_i } + original_issue&.pact_status != !!issue[:pact_status] + end + end + def calculate_before_issues review.request_issues.active_or_ineligible.select(&:persisted?) end @@ -136,6 +192,9 @@ def validate_before_perform if !changes? @error_code = :no_changes elsif RequestIssuesUpdate.where(review: review).where.not(id: id).processable.exists? + if @error_code == :no_changes + RequestIssuesUpdate.where(review: review).where.not(id: id).processable.last.destroy + end @error_code = :previous_update_not_done_processing end @@ -154,6 +213,14 @@ def fetch_edited_issues RequestIssue.where(id: edited_request_issue_ids) end + def fetch_mst_edited_issues + RequestIssue.where(id: mst_edited_issue_data.map(&:id)) + end + + def fetch_pact_edited_issues + RequestIssue.where(id: pact_edited_issue_data.map(&:id)) + end + def process_issues! review.create_issues!(added_issues, self) process_removed_issues! @@ -161,6 +228,8 @@ def process_issues! process_withdrawn_issues! process_edited_issues! process_corrected_issues! + process_mst_edited_issues! if FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) + process_pact_edited_issues! if FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) end def process_legacy_issues! @@ -201,6 +270,39 @@ def edit_decision_date(edited_issue_params, request_issue) end end + # :reek:FeatureEnvy + def process_mst_edited_issues! + return if mst_edited_issues.empty? + + mst_edited_issue_data.each do |mst_edited_issue| + RequestIssue.find(mst_edited_issue[:request_issue_id].to_s) + .update!( + mst_status: mst_edited_issue[:mst_status], + mst_status_update_reason_notes: mst_edited_issue[:mst_status_update_reason_notes] + ) + end + end + + # :reek:FeatureEnvy + def process_pact_edited_issues! + return if pact_edited_issues.empty? + + pact_edited_issue_data.each do |pact_edited_issue| + RequestIssue.find( + pact_edited_issue[:request_issue_id].to_s + ).update!( + pact_status: pact_edited_issue[:pact_status], + pact_status_update_reason_notes: pact_edited_issue[:pact_status_update_reason_notes] + ) + end + end + + def create_mst_pact_issue_update_tasks + handle_mst_pact_edits_task + handle_mst_pact_removal_task + handle_added_mst_pact_edits_task + end + def process_removed_issues! removed_issues.each(&:remove!) end @@ -216,4 +318,125 @@ def correction def process_corrected_issues! correction.call end + + def handle_mst_pact_edits_task + # filter out added or removed issues + after_issues = fetch_after_issues + edited_issues = before_issues & after_issues + # cycle each edited issue (before) and compare MST/PACT with (fetch_after_issues) + # reverse_each to make the issues on the case timeline appear in UI in a similar sequence to the edit issues page + edited_issues.reverse_each do |before_issue| + after_issue = after_issues.find { |issue| issue.id == before_issue.id } + # if before/after has a change in MST/PACT, create issue update task + if (before_issue.mst_status != after_issue.mst_status) || (before_issue.pact_status != after_issue.pact_status) + create_issue_update_task("Edited Issue", before_issue, after_issue) + end + end + end + + def handle_added_mst_pact_edits_task + after_issues = fetch_after_issues + added_issues = after_issues - before_issues + added_issues.reverse_each do |issue| + if issue.mst_status || issue.pact_status + create_issue_update_task("Added Issue", issue) + end + end + end + + def handle_mst_pact_removal_task + # filter out added or removed issues + after_issues = fetch_after_issues + edited_issues = before_issues - after_issues + # cycle each edited issue (before) and compare MST/PACT with (fetch_after_issues) + edited_issues.reverse_each do |before_issue| + # lazily create a new RequestIssue since the mst/pact status would be removed if deleted? + if before_issue.mst_status || before_issue.pact_status + create_issue_update_task("Removed Issue", before_issue) + end + end + end + + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + # :reek:FeatureEnvy + def create_issue_update_task(change_type, before_issue, after_issue = nil) + transaction do + # close out any tasks that might be open + open_issue_task = Task.where( + assigned_to: SpecialIssueEditTeam.singleton + ).where(status: "assigned").where(appeal: before_issue.decision_review) + open_issue_task[0].delete unless open_issue_task.empty? + + task = IssuesUpdateTask.create!( + appeal: before_issue.decision_review, + parent: RootTask.find_by(appeal: before_issue.decision_review), + assigned_to: SpecialIssueEditTeam.singleton, + assigned_by: RequestStore[:current_user], + completed_by: RequestStore[:current_user] + ) + + # check if change from vbms mst/pact status + vbms_mst_edit = if before_issue.vbms_mst_status.nil? + false + else + !before_issue.vbms_mst_status && before_issue.mst_status + end + + vbms_pact_edit = if before_issue.vbms_pact_status.nil? + false + else + !before_issue.vbms_pact_status && before_issue.pact_status + end + + # if a new issue is added and VBMS was edited, reference the original status + if change_type == "Added Issue" && (vbms_mst_edit || vbms_pact_edit) + set = CaseTimelineInstructionSet.new( + change_type: change_type, + issue_category: before_issue.contested_issue_description, + benefit_type: before_issue.benefit_type&.capitalize, + original_mst: before_issue.vbms_mst_status, + original_pact: before_issue.vbms_pact_status, + edit_mst: before_issue.mst_status, + edit_pact: before_issue.pact_status + ) + else + # format the task instructions and close out + # use contested issue description if nonrating issue category is nil + # rubocop:disable Layout/LineLength + issue_description = "#{before_issue.nonrating_issue_category} - #{before_issue.nonrating_issue_description}" unless before_issue.nonrating_issue_category.nil? + issue_description = before_issue.contested_issue_description if issue_description.nil? + set = CaseTimelineInstructionSet.new( + change_type: change_type, + issue_category: issue_description, + benefit_type: before_issue.benefit_type&.capitalize, + original_mst: before_issue.mst_status, + original_pact: before_issue.pact_status, + edit_mst: after_issue&.mst_status, + edit_pact: after_issue&.pact_status + ) + end + task.format_instructions(set) + # rubocop:enable Layout/LineLength, Metrics/AbcSize + task.completed! + + # create SpecialIssueChange record to log the changes + SpecialIssueChange.create!( + issue_id: before_issue.id, + appeal_id: before_issue.decision_review.id, + appeal_type: "Appeal", + task_id: task.id, + created_at: Time.zone.now.utc, + created_by_id: RequestStore[:current_user].id, + created_by_css_id: RequestStore[:current_user].css_id, + original_mst_status: before_issue.mst_status, + original_pact_status: before_issue.pact_status, + updated_mst_status: after_issue&.mst_status, + updated_pact_status: after_issue&.pact_status, + mst_from_vbms: before_issue&.vbms_mst_status, + pact_from_vbms: before_issue&.vbms_pact_status, + change_category: change_type + ) + end + end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity end diff --git a/app/models/serializers/idt/v1/appeal_details_serializer.rb b/app/models/serializers/idt/v1/appeal_details_serializer.rb index 0f675ddfbbd..8119ef32fe3 100644 --- a/app/models/serializers/idt/v1/appeal_details_serializer.rb +++ b/app/models/serializers/idt/v1/appeal_details_serializer.rb @@ -67,13 +67,18 @@ class Idt::V1::AppealDetailsSerializer attribute :badges do |object| if object.is_a?(LegacyAppeal) - nil + { + mst: !!object.mst?, + pact: !!object.pact? + } else { contested_claim: object.contested_claim?, fnod: object.veteran_appellant_deceased?, hearing: object.hearings.any?(&:held?), - overtime: object.overtime? + overtime: object.overtime?, + mst: object.mst?, + pact: object.pact? } end end diff --git a/app/models/serializers/work_queue/appeal_serializer.rb b/app/models/serializers/work_queue/appeal_serializer.rb index 6a2f2cc69ca..99926d79078 100644 --- a/app/models/serializers/work_queue/appeal_serializer.rb +++ b/app/models/serializers/work_queue/appeal_serializer.rb @@ -30,6 +30,10 @@ class WorkQueue::AppealSerializer attribute :contested_claim, &:contested_claim? + attribute :mst, &:mst? + + attribute :pact, &:pact? + attribute :issues do |object| object.request_issues.active_or_decided_or_withdrawn.includes(:remand_reasons).map do |issue| { @@ -40,7 +44,11 @@ class WorkQueue::AppealSerializer diagnostic_code: issue.contested_rating_issue_diagnostic_code, remand_reasons: issue.remand_reasons, closed_status: issue.closed_status, - decision_date: issue.decision_date + decision_date: issue.decision_date, + mst_status: FeatureToggle.enabled?(:mst_identification) ? issue.mst_status : false, + pact_status: FeatureToggle.enabled?(:pact_identification) ? issue.pact_status : false, + mst_justification: issue&.mst_status_update_reason_notes, + pact_justification: issue&.pact_status_update_reason_notes } end end @@ -61,7 +69,9 @@ class WorkQueue::AppealSerializer benefit_type: issue.benefit_type, remand_reasons: issue.remand_reasons, diagnostic_code: issue.diagnostic_code, - request_issue_ids: issue.request_decision_issues.pluck(:request_issue_id) + request_issue_ids: issue.request_decision_issues.pluck(:request_issue_id), + mst_status: FeatureToggle.enabled?(:mst_identification) ? issue.mst_status : false, + pact_status: FeatureToggle.enabled?(:pact_identification) ? issue.pact_status : false } end end diff --git a/app/models/serializers/work_queue/legacy_appeal_serializer.rb b/app/models/serializers/work_queue/legacy_appeal_serializer.rb index 0f7fca12051..b5200ac1605 100644 --- a/app/models/serializers/work_queue/legacy_appeal_serializer.rb +++ b/app/models/serializers/work_queue/legacy_appeal_serializer.rb @@ -67,6 +67,10 @@ class WorkQueue::LegacyAppealSerializer attribute :closest_regional_office_label + attribute :mst, &:mst? + + attribute :pact, &:pact? + attribute(:available_hearing_locations) { |object| available_hearing_locations(object) } attribute :docket_name do @@ -92,6 +96,10 @@ class WorkQueue::LegacyAppealSerializer ).editable? end + attribute :can_edit_request_issues do |object, params| + AppealRequestIssuesPolicy.new(user: params[:user], appeal: object).legacy_issues_editable? + end + attribute :attorney_case_review_id do |object| latest_vacols_attorney_case_review(object)&.vacols_id end diff --git a/app/models/serializers/work_queue/legacy_issue_serializer.rb b/app/models/serializers/work_queue/legacy_issue_serializer.rb index 850389cb35c..00d43d12b8e 100644 --- a/app/models/serializers/work_queue/legacy_issue_serializer.rb +++ b/app/models/serializers/work_queue/legacy_issue_serializer.rb @@ -16,4 +16,6 @@ class WorkQueue::LegacyIssueSerializer attribute :labels attribute(:readjudication) { false } attribute :remand_reasons + attribute(:mst_status) { |object| FeatureToggle.enabled?(:mst_identification) ? object.mst_status : false } + attribute(:pact_status) { |object| FeatureToggle.enabled?(:pact_identification) ? object.pact_status : false } end diff --git a/app/models/serializers/work_queue/legacy_task_serializer.rb b/app/models/serializers/work_queue/legacy_task_serializer.rb index 5427ed4502a..39dc35d7338 100644 --- a/app/models/serializers/work_queue/legacy_task_serializer.rb +++ b/app/models/serializers/work_queue/legacy_task_serializer.rb @@ -64,6 +64,14 @@ class WorkQueue::LegacyTaskSerializer object.appeal.overtime? end + attribute :mst do |object| + object.appeal.mst? + end + + attribute :pact do |object| + object.appeal.pact? + end + attribute :veteran_appellant_deceased do |object| object.appeal.veteran_appellant_deceased? end diff --git a/app/models/serializers/work_queue/task_column_serializer.rb b/app/models/serializers/work_queue/task_column_serializer.rb index 9d20508bb3f..f2ed1f5655a 100644 --- a/app/models/serializers/work_queue/task_column_serializer.rb +++ b/app/models/serializers/work_queue/task_column_serializer.rb @@ -265,6 +265,22 @@ def self.serialize_attribute?(params, columns) end end + attribute :mst do |object, params| + columns = [Constants.QUEUE_CONFIG.COLUMNS.BADGES.name] + + if serialize_attribute?(params, columns) + object.appeal.try(:mst?) + end + end + + attribute :pact do |object, params| + columns = [Constants.QUEUE_CONFIG.COLUMNS.BADGES.name] + + if serialize_attribute?(params, columns) + object.appeal.try(:pact?) + end + end + attribute :veteran_appellant_deceased do |object, params| columns = [Constants.QUEUE_CONFIG.COLUMNS.BADGES.name] diff --git a/app/models/serializers/work_queue/task_serializer.rb b/app/models/serializers/work_queue/task_serializer.rb index dbdbbcfb0a1..8490d064bdf 100644 --- a/app/models/serializers/work_queue/task_serializer.rb +++ b/app/models/serializers/work_queue/task_serializer.rb @@ -35,7 +35,7 @@ class WorkQueue::TaskSerializer end attribute :completed_by do |object| - object.try(:completed_by).try(:css_id) unless object.appeal.is_a?(LegacyAppeal) + object.try(:completed_by).try(:css_id) end attribute :assigned_to do |object| @@ -129,6 +129,14 @@ class WorkQueue::TaskSerializer object.appeal.try(:contested_claim?) end + attribute :mst do |object| + object.appeal.try(:mst?) + end + + attribute :pact do |object| + object.appeal.try(:pact?) + end + attribute :veteran_appellant_deceased do |object| object.appeal.try(:veteran_appellant_deceased?) end diff --git a/app/models/special_issue_change.rb b/app/models/special_issue_change.rb new file mode 100644 index 00000000000..b82228e800b --- /dev/null +++ b/app/models/special_issue_change.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# class for keeping track of MST/PACT special issue changes on legacy and AMA issues +class SpecialIssueChange < CaseflowRecord +end diff --git a/app/models/special_issues_comparator.rb b/app/models/special_issues_comparator.rb new file mode 100644 index 00000000000..da96c274060 --- /dev/null +++ b/app/models/special_issues_comparator.rb @@ -0,0 +1,224 @@ +# frozen_string_literal: true + +# class to get special issues from ratings +# built for MST/PACT release + +class SpecialIssuesComparator + attr_accessor :issue, :rating_special_issues, :bgs_client, :veteran_contentions, :linked_contentions + + def initialize(issue) + @issue = issue + @rating_special_issues = issue&.special_issues + @bgs_client = BGSService.new + end + + MST_SPECIAL_ISSUES = ["sexual assault trauma", "sexual trauma/assault", "sexual harassment"].freeze + PACT_SPECIAL_ISSUES = [ + "agent orange - outside vietnam or unknown", + "agent orange - vietnam", + "amyotrophic lateral sclerosis (als)", + "burn pit exposure", + "environmental hazard in gulf war", + "gulf war presumptive", + "radiation" + ].freeze + CONTENTION_PACT_ISSUES = %w[ + pact + pactdicre + pees1 + ].freeze + CONTENTION_MST_ISSUES = [ + "mst" + ].freeze + + # returns a hash with mst_available and pact_available values + # values generated from ratings special issues and contentions + def special_issues + [{ + mst_available: mst_from_rating_or_contention, + pact_available: pact_from_rating_or_contention + }] + end + + # check rating for existing mst status; if none, search contentions + def mst_from_rating_or_contention + return false unless FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) + return true if mst_from_rating? + return true if mst_from_contention? + + false + end + + # check rating for existing pact status; if none, search contentions + def pact_from_rating_or_contention + return false unless FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) + return true if pact_from_rating? + return true if pact_from_contention? + + false + end + + # cycles rating special issues and returns if a special issue is MST + def mst_from_rating? + return false if rating_special_issues.blank? + + rating_special_issues.each do |special_issue| + return true if special_issue_has_mst?(special_issue) + end + + false + end + + # cycles rating special issues and returns if a special issue is PACT + def pact_from_rating? + return false if rating_special_issues.blank? + + rating_special_issues.each do |special_issue| + return true if special_issue_has_pact?(special_issue) + end + + false + end + + # :reek:UtilityFunction + # checks if rating special issue meets MST criteria + def special_issue_has_mst?(special_issue) + special_issue.transform_keys!(&:to_s) + if special_issue["spis_tn"]&.casecmp("ptsd - personal trauma")&.zero? + return MST_SPECIAL_ISSUES.include?(special_issue["spis_basis_tn"]&.downcase) + end + + if special_issue["spis_tn"]&.casecmp("non-ptsd personal trauma")&.zero? + MST_SPECIAL_ISSUES.include?(special_issue["spis_basis_tn"]&.downcase) + end + end + + # :reek:UtilityFunction + # checks if rating special issue meets PACT criteria + def special_issue_has_pact?(special_issue) + special_issue.transform_keys!(&:to_s) + if special_issue["spis_tn"]&.casecmp("gulf war presumptive 3.320")&.zero? + return special_issue.keys(&:to_s)["spis_basis_tn"]&.casecmp("particulate matter")&.zero? + end + + PACT_SPECIAL_ISSUES.include?(special_issue["spis_tn"]&.downcase) + end + + # cycle contentions tied to the rating issue/decision and return true if there is a match for mst + def mst_from_contention? + self.linked_contentions ||= contentions_tied_to_issue + return false if linked_contentions.blank? + + linked_contentions.each do |contention| + return true if mst_contention_status?(contention) + end + + false + end + + # cycle contentions tied to the rating issue/decision and return true if there is a match for pact + def pact_from_contention? + self.linked_contentions ||= contentions_tied_to_issue + return false if linked_contentions.blank? + + linked_contentions.each do |contention| + return true if pact_contention_status?(contention) + end + + false + end + + # checks single contention special issue status for MST + # :reek:UtilityFunction + def mst_contention_status?(bgs_contention) + bgs_contention.transform_keys!(&:to_s) + return false if bgs_contention.nil? || bgs_contention["special_issues"].blank? + + if bgs_contention["special_issues"].is_a?(Hash) + CONTENTION_MST_ISSUES.include?(bgs_contention["special_issues"][:spis_tc]&.downcase) + elsif bgs_contention["special_issues"].is_a?(Array) + bgs_contention["special_issues"].any? { |issue| CONTENTION_MST_ISSUES.include?(issue[:spis_tc]&.downcase) } + end + end + + # checks single contention special issue status for PACT + # :reek:UtilityFunction + def pact_contention_status?(bgs_contention) + bgs_contention.transform_keys!(&:to_s) + return false if bgs_contention.nil? || bgs_contention["special_issues"].blank? + + if bgs_contention["special_issues"].is_a?(Hash) + CONTENTION_PACT_ISSUES.include?(bgs_contention["special_issues"][:spis_tc]&.downcase) + elsif bgs_contention["special_issues"].is_a?(Array) + bgs_contention["special_issues"].any? { |issue| CONTENTION_PACT_ISSUES.include?(issue[:spis_tc]&.downcase) } + end + end + + # get the contentions for the veteran, find the contentions that are tied to the rating issue + def contentions_tied_to_issue + # establish veteran contentions + self.veteran_contentions ||= fetch_contentions_by_participant_id(issue.participant_id) + + return nil if veteran_contentions.blank? + + match_ratings_with_contentions + end + + def fetch_contentions_by_participant_id(participant_id) + bgs_client.find_contentions_by_participant_id(participant_id) + end + + # cycles list of rba_contentions on the rating issue and matches them with + # contentions tied to the veteran + def match_ratings_with_contentions + contention_matches = [] + + return [] if issue.rba_contentions_data.blank? + + # cycle contentions tied to rating issue + issue.rba_contentions_data.each do |rba| + # grab contention on the rating + rba_contention = rba.with_indifferent_access + # cycle through the list of contentions from the BGS call (all contentions tied to veteran) + veteran_contentions.each do |contention| + next unless contention.is_a?(Hash) + + # store any matches that are found + link_contention_to_rating(contention, rba_contention, contention_matches) + end + end + contention_matches&.compact + end + + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # :reek:UtilityFunction + + # takes the contention given and tries to match it to the current rating issue (issue) + def link_contention_to_rating(contention, rba_contention, contention_matches) + # if only one contention, check the contention info + if contention.dig(:contentions).is_a?(Hash) + # get the single contention from the response + single_contention_info = contention.dig(:contentions) + + return if single_contention_info.blank? + + # see if the contention ties to the rating. if it does, add it to the matches list + if single_contention_info.dig(:cntntn_id) == rba_contention.dig(:cntntn_id) + contention_matches << single_contention_info + end + + # if the response contains an array of contentions, unpack each one and compare + elsif contention.dig(:contentions).is_a?(Array) + + # cycle the contentions within the array to make the comparison to the rba_contention + contention.dig(:contentions).each do |contention_info| + next if contention_info.dig(:cntntn_id).blank? + + # see if the contention ties to the rating. if it does, add it to the matches list + contention_matches << contention_info if contention_info.dig(:cntntn_id) == rba_contention.dig(:cntntn_id) + end + end + contention_matches + end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity +end diff --git a/app/models/tasks/establishment_task.rb b/app/models/tasks/establishment_task.rb new file mode 100644 index 00000000000..8f1f61798e4 --- /dev/null +++ b/app/models/tasks/establishment_task.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +class EstablishmentTask < Task + validates :parent, presence: true + + def label + "Establishment Task" + end + + # :reek:FeatureEnvy + # :reek:DuplicateMethodCall { max_calls: 2 } + def format_instructions(request_issues) + # format the instructions by loading an array and adding it to the instructions + added_issue_format = [] + request_issues.each do |issue| + original_special_issue_status = "" + # ignore issues that don't have mst or pact status + next if !issue.mst_status && !issue.pact_status + + # Logic for checking if a prior decision from vbms with mst/pact designation was updated in intake process + if issue.contested_issue_description + if issue.vbms_mst_status != issue.mst_status || issue.vbms_pact_status != issue.pact_status + original_special_issue_status = format_special_issues_text(issue.vbms_mst_status, issue.vbms_pact_status).to_s + end + end + + special_issue_status = format_special_issues_text(issue.mst_status, issue.pact_status).to_s + added_issue_format << [ + format_description_text(issue), + issue.benefit_type.capitalize, + original_special_issue_status, + special_issue_status + ] + + # create record to log the special issues changes + create_special_issue_changes_record(issue) + end + # add edit_issue_format into the instructions array for the task + instructions << added_issue_format + + save! + end + + private + + def format_description_text(issue) + if issue.contested_issue_description || issue.nonrating_issue_category && issue.nonrating_issue_description + issue.contested_issue_description || issue.nonrating_issue_category + " - " + issue.nonrating_issue_description + else + # we should probably remove this before pushing to prod + "Description unavailable" + end + end + + # rubocop:disable Metrics/CyclomaticComplexity + def format_special_issues_text(mst_status, pact_status) + # same method as issues_update_task + # format the special issues comment to display the change in the special issues status(es) + special_issue_phrase = "Special Issues:" + + return special_issue_phrase + " None" if !mst_status && !pact_status + return special_issue_phrase + " MST, PACT" if mst_status && pact_status + return special_issue_phrase + " MST" if mst_status + return special_issue_phrase + " PACT" if pact_status + end + # rubocop:enable Metrics/CyclomaticComplexity + + # :reek:FeatureEnvy + def create_special_issue_changes_record(issue) + # create SpecialIssueChange record to log the changes + SpecialIssueChange.create!( + issue_id: issue.id, + appeal_id: appeal.id, + appeal_type: "Appeal", + task_id: id, + created_at: Time.zone.now.utc, + created_by_id: RequestStore[:current_user].id, + created_by_css_id: RequestStore[:current_user].css_id, + original_mst_status: issue.mst_status, + original_pact_status: issue.pact_status, + mst_from_vbms: issue&.vbms_mst_status, + pact_from_vbms: issue&.vbms_pact_status, + change_category: "Established Issue" + ) + end +end diff --git a/app/models/tasks/issues_update_task.rb b/app/models/tasks/issues_update_task.rb new file mode 100644 index 00000000000..5239d9294e1 --- /dev/null +++ b/app/models/tasks/issues_update_task.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class IssuesUpdateTask < Task + validates :parent, presence: true + + def label + "Issues Update Task" + end + + # :reek:FeatureEnvy + def format_instructions(set) + # format the instructions by loading an array and adding it to the instructions + edit_issue_format = [] + # add the change type + edit_issue_format << set.change_type + edit_issue_format << set.benefit_type + edit_issue_format << set.issue_category + original_comment = format_special_issues_text(set.original_mst, set.original_pact).to_s + edit_issue_format << original_comment + + # format edit if edit values are given + unless set.edit_mst.nil? || set.edit_pact.nil? + updated_comment = format_special_issues_text(set.edit_mst, set.edit_pact).to_s + edit_issue_format << updated_comment + end + + # add the MST and PACT edit reasons. Removed on release but kept incase we need it for the future + # edit_issue_format << mst_edit_reason + # edit_issue_format << pact_edit_reason + + # add edit_issue_format into the instructions array for the task + instructions << edit_issue_format + + save! + end + + private + + # rubocop:disable Metrics/CyclomaticComplexity + def format_special_issues_text(mst_status, pact_status) + # format the special issues comment to display the change in the special issues status(es) + special_issue_status = "Special Issues:" + + return special_issue_status + " None" if !mst_status && !pact_status + return special_issue_status + " MST, PACT" if mst_status && pact_status + return special_issue_status + " MST" if mst_status + return special_issue_status + " PACT" if pact_status + end + # rubocop:enable Metrics/CyclomaticComplexity +end diff --git a/app/models/tasks/judge_decision_review_task.rb b/app/models/tasks/judge_decision_review_task.rb index 2c59e5b54d6..e0ae4a8b86f 100644 --- a/app/models/tasks/judge_decision_review_task.rb +++ b/app/models/tasks/judge_decision_review_task.rb @@ -35,6 +35,10 @@ def self.label private def ama_judge_actions - Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT_SP_ISSUES.to_h + # bypass special issues page if mst/pact enabled + return Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT.to_h if + FeatureToggle.enabled?(:mst_identification) || FeatureToggle.enabled?(:pact_identification) + + Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT_SPECIAL_ISSUES.to_h end end diff --git a/app/models/tasks/judge_dispatch_return_task.rb b/app/models/tasks/judge_dispatch_return_task.rb index c8dee67b1f9..fe0a6209656 100644 --- a/app/models/tasks/judge_dispatch_return_task.rb +++ b/app/models/tasks/judge_dispatch_return_task.rb @@ -6,7 +6,7 @@ class JudgeDispatchReturnTask < JudgeTask def additional_available_actions(_user) [ - Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT.to_h, + ama_issue_checkout, Constants.TASK_ACTIONS.JUDGE_DISPATCH_RETURN_TO_ATTORNEY.to_h, Constants.TASK_ACTIONS.CANCEL_TASK.to_h ] @@ -15,4 +15,13 @@ def additional_available_actions(_user) def self.label COPY::JUDGE_DISPATCH_RETURN_TASK_LABEL end + + # :reek:UtilityFunction + def ama_issue_checkout + # bypass special issues page if mst/pact enabled + return Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT.to_h if + FeatureToggle.enabled?(:mst_identification) || FeatureToggle.enabled?(:pact_identification) + + Constants.TASK_ACTIONS.JUDGE_AMA_CHECKOUT_SPECIAL_ISSUES.to_h + end end diff --git a/app/models/user.rb b/app/models/user.rb index 000aec8502f..e663375c066 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -151,6 +151,15 @@ def administer_org_users? admin? || granted?("Admin Intake") || roles.include?("Admin Intake") || member_of_organization?(Bva.singleton) end + # editing logic for MST and PACT + def can_edit_intake_issues? + return false unless FeatureToggle.enabled?(:mst_identification) || + FeatureToggle.enabled?(:pact_identification) || + FeatureToggle.enabled?(:legacy_mst_pact_identification) + + BvaIntake.singleton.admins.include?(self) || member_of_organization?(ClerkOfTheBoard.singleton) + end + def can_view_overtime_status? (attorney_in_vacols? || judge_in_vacols?) && FeatureToggle.enabled?(:overtime_revamp, user: self) end diff --git a/app/models/vacols/case_issue.rb b/app/models/vacols/case_issue.rb index f4a54243233..2f3848f8b71 100644 --- a/app/models/vacols/case_issue.rb +++ b/app/models/vacols/case_issue.rb @@ -27,7 +27,9 @@ def attributes_for_readjudication level_2: isslev2, level_3: isslev3, vacols_id: isskey, - note: issdesc + note: issdesc, + mst_status: issmst, + pact_status: isspact } end @@ -55,6 +57,8 @@ def self.descriptions(vacols_ids) ISSUES.ISSLEV1, ISSUES.ISSLEV2, ISSUES.ISSLEV3, + ISSUES.ISSMST, + ISSUES.ISSPACT, ISSREF.PROG_DESC ISSPROG_LABEL, ISSREF.ISS_DESC ISSCODE_LABEL, case when ISSUES.ISSLEV1 is not null then diff --git a/app/policies/appeal_request_issues_policy.rb b/app/policies/appeal_request_issues_policy.rb index 8e5a7ade34b..9035112c95b 100644 --- a/app/policies/appeal_request_issues_policy.rb +++ b/app/policies/appeal_request_issues_policy.rb @@ -9,7 +9,11 @@ def initialize(user:, appeal:) def editable? editable_by_case_review_team_member? || case_is_in_active_review_by_current_user? || hearing_is_assigned_to_judge_user? || editable_by_cavc_team_member? || - editable_by_ssc_team_member? + editable_by_ssc_team_member? || editable_by_cob_team_member? + end + + def legacy_issues_editable? + FeatureToggle.enabled?(:legacy_mst_pact_identification, user: RequestStore[:current_user]) && editable? end private @@ -30,6 +34,19 @@ def editable_by_ssc_team_member? FeatureToggle.enabled?(:split_appeal_workflow) end + # editable option added for MST and PACT editing + def editable_by_cob_team_member? + ClerkOfTheBoard.singleton.users.include?(user) && + mst_pact_feature_toggles_enabled? + end + + # returns true if one feature toggle is enabled + def mst_pact_feature_toggles_enabled? + FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) || + FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) || + FeatureToggle.enabled?(:legacy_mst_pact_identification, user: RequestStore[:current_user]) + end + def current_user_can_edit_issues? user&.can_edit_issues? end diff --git a/app/repositories/task_action_repository.rb b/app/repositories/task_action_repository.rb index 0de87e8e529..cc6374acd72 100644 --- a/app/repositories/task_action_repository.rb +++ b/app/repositories/task_action_repository.rb @@ -890,6 +890,10 @@ def mark_task_complete_data(*) def select_ama_review_decision_action(task) return Constants.TASK_ACTIONS.REVIEW_VACATE_DECISION.to_h if task.appeal.vacate? + # route to decision if mst/pact toggles are enabled. + return Constants.TASK_ACTIONS.REVIEW_AMA_DECISION.to_h if + FeatureToggle.enabled?(:mst_identification) || FeatureToggle.enabled?(:pact_identification) + Constants.TASK_ACTIONS.REVIEW_AMA_DECISION_SP_ISSUES.to_h end diff --git a/app/serializers/hearings/hearing_serializer.rb b/app/serializers/hearings/hearing_serializer.rb index 11c6bdedac2..5cea1784a1a 100644 --- a/app/serializers/hearings/hearing_serializer.rb +++ b/app/serializers/hearings/hearing_serializer.rb @@ -46,6 +46,12 @@ class HearingSerializer attribute :contested_claim do |hearing| hearing.appeal.contested_claim? end + attribute :mst do |hearing| + hearing.appeal.mst? + end + attribute :pact do |hearing| + hearing.appeal.pact? + end attribute :current_issue_count attribute :disposition attribute :disposition_editable diff --git a/app/serializers/hearings/legacy_hearing_serializer.rb b/app/serializers/hearings/legacy_hearing_serializer.rb index 339bca6f189..b1b92417f82 100644 --- a/app/serializers/hearings/legacy_hearing_serializer.rb +++ b/app/serializers/hearings/legacy_hearing_serializer.rb @@ -122,4 +122,13 @@ class LegacyHearingSerializer attribute :current_user_timezone do |_, params| params[:user]&.timezone end + + attribute :worksheet_issues, &:prepare_worksheet_issues + attribute :mst do |object| + object.appeal.mst? + end + + attribute :pact do |object| + object.appeal.pact? + end end diff --git a/app/serializers/intake/decision_issue_serializer.rb b/app/serializers/intake/decision_issue_serializer.rb index 6876b94366c..c644a925adb 100644 --- a/app/serializers/intake/decision_issue_serializer.rb +++ b/app/serializers/intake/decision_issue_serializer.rb @@ -9,6 +9,8 @@ class Intake::DecisionIssueSerializer attribute :description attribute :disposition attribute :approx_decision_date + attribute :mst_status + attribute :pact_status attribute :request_issue_id do |object| object.request_issues&.first&.id end diff --git a/app/serializers/intake/legacy_appeal_serializer.rb b/app/serializers/intake/legacy_appeal_serializer.rb new file mode 100644 index 00000000000..d5dda33ca95 --- /dev/null +++ b/app/serializers/intake/legacy_appeal_serializer.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +class Intake::LegacyAppealSerializer + include FastJsonapi::ObjectSerializer + set_key_transform :camel_lower + + attribute :claimant, &:claimant_participant_id + attribute :claimant_type do |object| + object.claimant[:representative][:type] + end + attribute :claimant_name, &:veteran_full_name + attribute :veteran_is_not_claimant + attribute :request_issues, &:issues + + attribute :intake_user + + attribute :processed_in_caseflow do |_object| + true + end + + attribute :legacy_opt_in_approved do |_object| + true + end + + attribute :legacy_appeals do |_object| + [] + end + + attribute :ratings do |_object| + [] + end + + attribute :edit_issues_url do |object| + "/appeals/#{object.id}/edit" + end + + attribute :processed_at do |_object| + nil + end + + attribute :veteran_invalid_fields do |_object| + nil + end + + attribute :active_nonrating_request_issues do |_object| + [] + end + + attribute :contestable_issues_by_date do |_object| + [] + end + + attribute :intake_user do |_object| + nil + end + + attribute :receipt_date do |_object| + nil + end + + attribute :decision_issues do |object| + object.veteran.decision_issues.map(&:serialize) + end + + attribute :relationships do |object| + object.veteran&.relationships&.map(&:serialize) + end + + attribute :veteran_valid do |object| + object.veteran&.valid?(:bgs) + end + + attribute :veteran do |object| + { + name: object.veteran&.name&.formatted(:readable_short), + fileNumber: object.veteran_file_number, + formName: object.veteran&.name&.formatted(:form), + ssn: object.veteran&.ssn + } + end + + attribute :power_of_attorney_name do |_object| + nil + end + + attribute :claimant_relationship do |_object| + nil + end + + attribute :docket_type do + "Legacy" + end + + attribute :is_outcoded do + nil + end + + attribute :form_type do + "appeal" + end +end diff --git a/app/serializers/intake/rating_issue_serializer.rb b/app/serializers/intake/rating_issue_serializer.rb index 5cbabac34dd..41d0416293c 100644 --- a/app/serializers/intake/rating_issue_serializer.rb +++ b/app/serializers/intake/rating_issue_serializer.rb @@ -18,4 +18,5 @@ class Intake::RatingIssueSerializer attribute :rba_contentions_data attribute :reference_id attribute :subject_text + attribute :special_issues end diff --git a/app/serializers/intake/request_issue_serializer.rb b/app/serializers/intake/request_issue_serializer.rb index dc234fa65ac..263bea4454e 100644 --- a/app/serializers/intake/request_issue_serializer.rb +++ b/app/serializers/intake/request_issue_serializer.rb @@ -39,4 +39,10 @@ class Intake::RequestIssueSerializer end attribute :benefit_type attribute :is_predocket_needed + attribute :mst_status + attribute :vbms_mst_status + attribute :pact_status + attribute :vbms_pact_status + attribute :mst_status_update_reason_notes + attribute :pact_status_update_reason_notes end diff --git a/app/services/external_api/bgs_service.rb b/app/services/external_api/bgs_service.rb index 05db6b81545..0b155f89293 100644 --- a/app/services/external_api/bgs_service.rb +++ b/app/services/external_api/bgs_service.rb @@ -3,6 +3,7 @@ require "bgs" # Thin interface to all things BGS +# rubocop:disable Metrics/ClassLength class ExternalApi::BGSService include PowerOfAttorneyMapper include AddressMapper @@ -324,6 +325,11 @@ def bust_fetch_veteran_info_cache(vbms_id) def fetch_ratings_in_range(participant_id:, start_date:, end_date:) DBService.release_db_connections + DataDogService.increment_counter( + metric_group: "mst_pact_group", + metric_name: "bgs_service.fetch_ratings_in_range_called", + app_name: RequestStore[:application] + ) start_date, end_date = formatted_start_and_end_dates(start_date, end_date) MetricsService.record("BGS: fetch ratings in range: \ @@ -343,6 +349,12 @@ def fetch_ratings_in_range(participant_id:, start_date:, end_date:) def fetch_rating_profile(participant_id:, profile_date:) DBService.release_db_connections + DataDogService.increment_counter( + metric_group: "mst_pact_group", + metric_name: "bgs_service.fetch_rating_profile_called", + app_name: RequestStore[:application] + ) + MetricsService.record("BGS: fetch rating profile: \ participant_id = #{participant_id}, \ profile_date = #{profile_date}", @@ -451,6 +463,23 @@ def find_contentions_by_claim_id(claim_id) end end + def find_contentions_by_participant_id(participant_id) + return [] unless FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) || + FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) + + # find contention info in cache; if not there, call to BGS and cache it + Rails.cache.fetch("find_contentions_by_participant_id_#{participant_id}", expires_in: 24.hours) do + DBService.release_db_connections + MetricsService.record("BGS: find contentions for veteran by participant_id #{participant_id}", + service: :bgs, + name: "contention.find_contention_by_participant_id") do + client.contention.find_contention_by_participant_id(participant_id) + rescue BGS::ShareError + [] + end + end + end + def find_current_rating_profile_by_ptcpnt_id(participant_id) DBService.release_db_connections MetricsService.record("BGS: find current rating profile for veteran by participant_id #{participant_id}", @@ -515,3 +544,4 @@ def formatted_start_and_end_dates(start_date, end_date) end # :nocov: end +# rubocop:enable Metrics/ClassLength diff --git a/app/views/appeals/edit.html.erb b/app/views/appeals/edit.html.erb index f543d778340..052d84c36c8 100644 --- a/app/views/appeals/edit.html.erb +++ b/app/views/appeals/edit.html.erb @@ -3,6 +3,7 @@ userDisplayName: current_user.display_name, userCanWithdrawIssues: current_user.can_withdraw_issues?, userCanSplitAppeal: current_user.can_split_appeal?(appeal), + userCanEditIntakeIssues: current_user.can_edit_intake_issues?, appeal: appeal, hearings: appeal.hearings, hearingDayDate: appeal.hearing_day_if_schedueled, @@ -13,12 +14,17 @@ buildDate: build_date, serverIntake: appeal.ui_hash, claimId: url_appeal_uuid, + isLegacy: appeal.is_legacy?, featureToggles: { useAmaActivationDate: FeatureToggle.enabled?(:use_ama_activation_date, user: current_user), correctClaimReviews: FeatureToggle.enabled?(:correct_claim_reviews, user: current_user), covidTimelinessExemption: FeatureToggle.enabled?(:covid_timeliness_exemption, user: current_user), split_appeal_workflow: FeatureToggle.enabled?(:split_appeal_workflow, user: current_user), cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, user: current_user), + mstIdentification: FeatureToggle.enabled?(:mst_identification, user: current_user), + pactIdentification: FeatureToggle.enabled?(:pact_identification, user: current_user), + legacyMstPactIdentification: FeatureToggle.enabled?(:legacy_mst_pact_identification, user: current_user), + justificationReason: FeatureToggle.enabled?(:justification_reason, user: current_user), vhaPreDocketAppeals: false } }) %> diff --git a/app/views/hearings/index.html.erb b/app/views/hearings/index.html.erb index 5d605dd89d5..30f37fe1828 100644 --- a/app/views/hearings/index.html.erb +++ b/app/views/hearings/index.html.erb @@ -14,6 +14,9 @@ applicationUrls: application_urls, feedbackUrl: feedback_url, buildDate: build_date, + mstIdentification: FeatureToggle.enabled?(:mst_identification, user: current_user), + pactIdentification: FeatureToggle.enabled?(:pact_identification, user: current_user), + legacyMstPactIdentification: FeatureToggle.enabled?(:legacy_mst_pact_identification, user: current_user), userCanAddVirtualHearingDays: FeatureToggle.enabled?(:national_vh_queue, user: current_user), userCanAssignHearingSchedule: current_user.can_assign_hearing_schedule?, userCanBuildHearingSchedule: current_user.can?('Build HearSched'), diff --git a/app/views/higher_level_reviews/edit.html.erb b/app/views/higher_level_reviews/edit.html.erb index 6468416219d..c8e45f5e183 100644 --- a/app/views/higher_level_reviews/edit.html.erb +++ b/app/views/higher_level_reviews/edit.html.erb @@ -2,6 +2,7 @@ <%= react_component("IntakeEdit", props: { userDisplayName: current_user.display_name, userCanWithdrawIssues: current_user.can_withdraw_issues?, + userCanEditIntakeIssues: current_user.can_edit_intake_issues?, dropdownUrls: dropdown_urls, applicationUrls: application_urls, feedbackUrl: feedback_url, diff --git a/app/views/queue/index.html.erb b/app/views/queue/index.html.erb index b12500347a8..c63f943d613 100644 --- a/app/views/queue/index.html.erb +++ b/app/views/queue/index.html.erb @@ -52,6 +52,10 @@ split_appeal_workflow: FeatureToggle.enabled?(:split_appeal_workflow, user: current_user), cavc_remand_granted_substitute_appellant: FeatureToggle.enabled?(:cavc_remand_granted_substitute_appellant, user: current_user), cavc_dashboard_workflow: FeatureToggle.enabled?(:cavc_dashboard_workflow, user: current_user), + mstIdentification: FeatureToggle.enabled?(:mst_identification, user: current_user), + pactIdentification: FeatureToggle.enabled?(:pact_identification, user: current_user), + legacyMstPactIdentification: FeatureToggle.enabled?(:legacy_mst_pact_identification, user: current_user), + justificationReason: FeatureToggle.enabled?(:justification_reason, user: current_user), cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, user: current_user), metricsBrowserError: FeatureToggle.enabled?(:metrics_browser_error, user: current_user), cc_vacatur_visibility: FeatureToggle.enabled?(:cc_vacatur_visibility, user: current_user), diff --git a/app/views/supplemental_claims/edit.html.erb b/app/views/supplemental_claims/edit.html.erb index b3c8d18054e..8293ea763a2 100644 --- a/app/views/supplemental_claims/edit.html.erb +++ b/app/views/supplemental_claims/edit.html.erb @@ -2,6 +2,7 @@ <%= react_component("IntakeEdit", props: { userDisplayName: current_user.display_name, userCanWithdrawIssues: current_user.can_withdraw_issues?, + userCanEditIntakeIssues: current_user.can_edit_intake_issues?, dropdownUrls: dropdown_urls, applicationUrls: application_urls, feedbackUrl: feedback_url, diff --git a/app/workflows/initial_tasks_factory.rb b/app/workflows/initial_tasks_factory.rb index 9c9cfe798bd..6c642bee735 100644 --- a/app/workflows/initial_tasks_factory.rb +++ b/app/workflows/initial_tasks_factory.rb @@ -21,7 +21,13 @@ def initialize(appeal) STATE_CODES_REQUIRING_TRANSLATION_TASK = %w[VI VQ PR PH RP PI].freeze + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def create_root_and_sub_tasks! + # if changes to mst or pact, create IssueUpdateTask + if (@appeal.mst? && FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user])) || + (@appeal.pact? && FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user])) + create_establishment_task + end create_vso_tracking_tasks ActiveRecord::Base.transaction do create_subtasks! if @appeal.original? || @appeal.cavc? || @appeal.appellant_substitution? @@ -31,6 +37,7 @@ def create_root_and_sub_tasks! end maybe_create_translation_task end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity private @@ -45,7 +52,6 @@ def create_vso_tracking_tasks # rubocop:disable Metrics/CyclomaticComplexity def create_subtasks! distribution_task # ensure distribution_task exists - if @appeal.appellant_substitution? create_selected_tasks elsif @appeal.cavc? @@ -201,4 +207,16 @@ def maybe_create_translation_task ensure TranslationTask.create_from_parent(distribution_task) if STATE_CODES_REQUIRING_TRANSLATION_TASK.include?(state_code) end + + def create_establishment_task + task = EstablishmentTask.create!( + appeal: @appeal, + parent: @root_task, + assigned_by: RequestStore[:current_user], + assigned_to: SpecialIssueEditTeam.singleton, + completed_by: RequestStore[:current_user] + ) + task.format_instructions(@appeal.request_issues) + task.completed! + end end diff --git a/app/workflows/rating_profile_disability.rb b/app/workflows/rating_profile_disability.rb index 45dbf24e088..d3ba0d8d4df 100644 --- a/app/workflows/rating_profile_disability.rb +++ b/app/workflows/rating_profile_disability.rb @@ -38,4 +38,8 @@ def most_recent_evaluation evaluation[:conv_begin_dt] || evaluation[:begin_dt] || evaluation[:dis_dt] || Time.zone.local(0) end end + + def special_issues + @special_issues ||= Array.wrap(self[:disability_special_issues]) + end end diff --git a/client/COPY.json b/client/COPY.json index b92e0df6a5c..9664b5b65df 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -634,6 +634,8 @@ "SPECIAL_ISSUES_BENEFIT_TYPE_SECTION": "Benefit Types: ", "SPECIAL_ISSUES_ISSUES_ON_APPEAL_SECTION": "Issues on Appeal: ", "SPECIAL_ISSUES_DIC_OR_PENSION_SECTION": "Dependency and Indemnity Compensation (DIC) or Pension: ", + "SPECIAL_ISSUES_BANNER_TEXT": "Military Sexual Trauma and PACT Act will be identified at the issue level. If you need to add these issues, please add them on the next page by editing the applicable issue(s)", + "SPECIAL_ISSUES_NONE_CHOSEN_TITLE": "Choose at least one.", "SPECIAL_ISSUES_NONE_CHOSEN_DETAIL": "If no special issues apply to this case, please confirm by selecting \"No special issues\"", "ADVANCE_ON_DOCKET_MOTION_PAGE_TITLE": "Update Advanced on Docket (AOD) Status", @@ -943,6 +945,13 @@ "INTAKE_REQUEST_ISSUE_UNTIMELY": "Please note: The issue requested isn't usually eligible because its decision date is older than what's allowed.", "INTAKE_LEGACY_ISSUE_UNTIMELY": "Please note: The legacy issue isn't eligible for SOC/SSOC opt-in unless an exemption has been requested for reasons related to good cause.", "INTAKE_REQUEST_ISSUE_AND_LEGACY_ISSUE_UNTIMELY": "Please note: The issue isn't usually eligible because its decision date is older than what is allowed, and the legacy issue issue isn't eligible for SOC/SSOC opt-in unless an exemption has been requested for reasons related to good cause.", + "INTAKE_ADD_EDIT_SPECIAL_ISSUES_LABEL": "Special Issues: ", + "INTAKE_EDIT_ISSUE_TITLE": "Edit issue", + "INTAKE_EDIT_ISSUE_SELECT_SPECIAL_ISSUES": "Select any special issues that apply", + "INTAKE_EDIT_ISSUE_CHANGE_MESSAGE": "Why was this change made?", + "INTAKE_EDIT_ISSUE_LABEL": "Issue: ", + "INTAKE_EDIT_ISSUE_BENEFIT_TYPE": "Benefit type: ", + "INTAKE_EDIT_ISSUE_DECISION_DATE": "Decision date: ", "INTAKE_VHA_CLAIM_REVIEW_REQUIREMENT": "Only VHA team members can establish Higher Level Reviews and Supplemental Claims with VHA issues. If you have a VHA claim, please return the packet to the main “VHA” queue in the Centralized Mail Portal or send downloaded documents to %s.", "VHA_BENEFIT_EMAIL_ADDRESS": "VHABENEFITAPPEALS@va.gov", "VHA_CAREGIVER_SUPPORT_EMAIL_ADDRESS": "VHA.CSPAppeals@va.gov", @@ -957,6 +966,12 @@ "UNTIMELY_EXEMPTION_COPY_VHA": " OR is this issue related to a VHA Caregiver appeal", "VHA_PRE_DOCKET_ADD_ISSUES_NOTICE": "You have added VHA issues. Once you click \"Submit Appeal\", this appeal will be marked for pre-docketing.", "VHA_PRE_DOCKET_ISSUE_BANNER": "Based on the issue selected, this will go to pre-docket queue.", + + "MST_SHORT_LABEL": "MST", + "PACT_SHORT_LABEL": "PACT", + "MST_LABEL": "Military Sexual Trauma (MST)", + "PACT_LABEL": "PACT Act", + "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.", diff --git a/client/app/admin/reducers/featureToggle.js b/client/app/admin/reducers/featureToggle.js index 331e3653b7a..8f85fb53bf9 100644 --- a/client/app/admin/reducers/featureToggle.js +++ b/client/app/admin/reducers/featureToggle.js @@ -22,7 +22,10 @@ const updateFromServerFeatures = (state, featureToggles) => { }, updatedAppealForm: { $set: Boolean(featureToggles.updatedAppealForm) - } + }, + justificationReason: { + $set: Boolean(featureToggles.justificationReason) + }, }); }; @@ -34,7 +37,8 @@ export const mapDataToFeatureToggle = (data = { featureToggles: {} }) => filedByVaGovHlr: false, updatedIntakeForms: false, eduPreDocketAppeals: false, - updatedAppealForm: false + updatedAppealForm: false, + justificationReason: false }, data.featureToggles ); diff --git a/client/app/components/AmaIssueList.jsx b/client/app/components/AmaIssueList.jsx index e4bd42001ec..7a509e63f52 100644 --- a/client/app/components/AmaIssueList.jsx +++ b/client/app/components/AmaIssueList.jsx @@ -33,12 +33,30 @@ const issueErrorStyling = css({ borderLeft: '4px solid #cd2026' }); +// format special issues to display 'None', 'PACT', 'MST', or 'MST and PACT' +const specialIssuesFormatting = (mstStatus, pactStatus) => { + if (!mstStatus && !pactStatus) { + return 'None'; + } else if (mstStatus && pactStatus) { + return 'MST and PACT'; + } else if (mstStatus) { + return 'MST'; + } else if (pactStatus) { + return 'PACT'; + } +}; + export const AmaIssue = (props) => { return
  • -
    Benefit type: {BENEFIT_TYPES[props.issue.program]}
    -
    Issue: {props.issue.description}
    +
    Benefit type: {BENEFIT_TYPES[props.issue.program]}
    +
    Issue: {props.issue.description}
    { props.issue.diagnostic_code && -
    Diagnostic code: {props.issue.diagnostic_code}
    } +
    Diagnostic code: {props.issue.diagnostic_code}
    } + { (props.mstFeatureToggle || props.pactFeatureToggle) &&
    + Special Issues: { + specialIssuesFormatting(props.issue.mst_status, props.issue.pact_status) + } +
    } { props.issue.notes &&
    Note from NOD: {props.issue.notes}
    } { props.issue.closed_status && props.issue.closed_status === 'withdrawn' && @@ -55,7 +73,9 @@ export default class AmaIssueList extends React.PureComponent { const { requestIssues, children, - errorMessages + errorMessages, + mstFeatureToggle, + pactFeatureToggle, } = this.props; return
      @@ -71,6 +91,10 @@ export default class AmaIssueList extends React.PureComponent { {children} @@ -88,13 +112,19 @@ AmaIssue.propTypes = { description: PropTypes.string, diagnostic_code: PropTypes.string, notes: PropTypes.string, - closed_status: PropTypes.string + closed_status: PropTypes.string, + mst_status: PropTypes.bool, + pact_status: PropTypes.bool }), - children: PropTypes.node + children: PropTypes.node, + pactFeatureToggle: PropTypes.bool, + mstFeatureToggle: PropTypes.bool }; AmaIssueList.propTypes = { children: PropTypes.node, requestIssues: PropTypes.array, - errorMessages: PropTypes.object + errorMessages: PropTypes.object, + pactFeatureToggle: PropTypes.bool, + mstFeatureToggle: PropTypes.bool }; diff --git a/client/app/components/Checkbox.jsx b/client/app/components/Checkbox.jsx index e6c67ea3905..79199ea778a 100644 --- a/client/app/components/Checkbox.jsx +++ b/client/app/components/Checkbox.jsx @@ -22,7 +22,6 @@ export const Checkbox = (props) => { } = props; const handleChange = (event) => onChange?.(event.target.checked, event); - const wrapperClasses = classnames(`checkbox-wrapper-${name}`, { 'cf-form-checkboxes': !unpadded, 'usa-input-error': Boolean(errorMessage), diff --git a/client/app/components/CheckboxGroup.jsx b/client/app/components/CheckboxGroup.jsx index 22b1d527eeb..f69e59d826c 100644 --- a/client/app/components/CheckboxGroup.jsx +++ b/client/app/components/CheckboxGroup.jsx @@ -83,7 +83,8 @@ CheckboxGroup.propTypes = { label: PropTypes.oneOfType([ PropTypes.string, PropTypes.node - ]) + ]), + requiresJustification: PropTypes.bool }) ).isRequired, onChange: PropTypes.func.isRequired, @@ -95,5 +96,5 @@ CheckboxGroup.propTypes = { getCheckbox: PropTypes.func, styling: PropTypes.object, strongLabel: PropTypes.bool, - disableAll: PropTypes.bool + disableAll: PropTypes.bool, }; diff --git a/client/app/components/RadioField.jsx b/client/app/components/RadioField.jsx index 5786e43f906..1880ba7c00e 100644 --- a/client/app/components/RadioField.jsx +++ b/client/app/components/RadioField.jsx @@ -68,11 +68,11 @@ export const RadioField = (props) => { const maybeAddTooltip = (option, radioField) => { if (option.tooltipText) { - const idKey = `tooltip-${option.value}`; + const keyId = `tooltip-${option.value}`; return {
      {options.map((option, i) => { const optionDisabled = isDisabled(option); - const radioField = (
      + + ; } else { badges = @@ -46,6 +50,8 @@ class BadgeArea extends React.PureComponent { + + ; } diff --git a/client/app/components/badges/MstBadge/MstBadge.jsx b/client/app/components/badges/MstBadge/MstBadge.jsx new file mode 100644 index 00000000000..8726dd68f61 --- /dev/null +++ b/client/app/components/badges/MstBadge/MstBadge.jsx @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; + +import Badge from '../Badge'; +import { COLORS } from 'app/constants/AppConstants'; + +/** + * Component to display if the appeal is a mst. + */ + +const MstBadge = (props) => { + const { appeal } = props; + + // During decision review workflow, saved/staged changes made are updated to appeal.decisionIssues + // if legacy check issues for changes, if ama check decision for changes + const issues = (appeal.isLegacyAppeal || appeal.type === 'LegacyAppeal') ? appeal.issues : appeal.decisionIssues; + + // check the issues/decisions for mst/pact changes in flight + if (issues && issues?.length > 0) { + if (!issues.some((issue) => issue.mst_status === true)) { + return null; + } + } else if (!appeal?.mst) { + // if issues are empty/undefined, use appeal model mst check + return null; + } + + const tooltipText = 'Appeal has issue(s) related to Military Sexual Trauma'; + + return ; +}; + +MstBadge.propTypes = { + appeal: PropTypes.object, +}; + +export default MstBadge; diff --git a/client/app/components/badges/MstBadge/MstBadge.stories.js b/client/app/components/badges/MstBadge/MstBadge.stories.js new file mode 100644 index 00000000000..f35c330be32 --- /dev/null +++ b/client/app/components/badges/MstBadge/MstBadge.stories.js @@ -0,0 +1,20 @@ +import React from 'react'; + +import MstBadgeComponent from './MstBadge'; + +export default { + title: 'Commons/Components/Badges/MST Badge', + component: MstBadgeComponent, + parameters: { + layout: 'centered', + }, + args: { + appeal: { + mst: true, + } + } +}; + +const Template = (args) => ; + +export const MSTBadge = Template.bind({}); diff --git a/client/app/components/badges/MstBadge/MstBadge.test.js b/client/app/components/badges/MstBadge/MstBadge.test.js new file mode 100644 index 00000000000..0b4e1c377b0 --- /dev/null +++ b/client/app/components/badges/MstBadge/MstBadge.test.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; +import { createStore, applyMiddleware } from 'redux'; + +import rootReducer from 'app/queue/reducers'; +import MstBadge from './MstBadge'; + +describe('MstBadge', () => { + const defaultAppeal = { + mst: true, + }; + + const getStore = () => createStore(rootReducer, applyMiddleware(thunk)); + + const setupMstBadge = (store) => { + return mount( + + + + ); + }; + + it('renders correctly', () => { + const store = getStore(); + const component = setupMstBadge(store); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/client/app/components/badges/MstBadge/__snapshots__/MstBadge.test.js.snap b/client/app/components/badges/MstBadge/__snapshots__/MstBadge.test.js.snap new file mode 100644 index 00000000000..99038478f4f --- /dev/null +++ b/client/app/components/badges/MstBadge/__snapshots__/MstBadge.test.js.snap @@ -0,0 +1,163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MstBadge renders correctly 1`] = ` + + + +
      + + + MST + + + +