From 8ea7eb9185a224cd44d57d4312998220ffb39970 Mon Sep 17 00:00:00 2001 From: AimanK Date: Mon, 12 Jun 2023 10:12:45 -0400 Subject: [PATCH 01/34] Add 508 ARIA label for screen readers in CF Help --- client/app/help/HelpRootView.jsx | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/client/app/help/HelpRootView.jsx b/client/app/help/HelpRootView.jsx index 115620d135d..264a0001444 100644 --- a/client/app/help/HelpRootView.jsx +++ b/client/app/help/HelpRootView.jsx @@ -6,29 +6,36 @@ const HelpRootView = () => { const pages = [ { name: 'Certification Help', - url: '/certification/help' }, + url: '/certification/help', + ariaLabel: 'Click for help resources/frequently asked questions on using Caseflow Certification' }, { name: 'Dispatch Help', - url: '/dispatch/help' }, + url: '/dispatch/help', + ariaLabel: 'Click for help resources/frequently asked questions on using Caseflow Dispatch' }, { name: 'Reader Help', - url: '/reader/help' }, + url: '/reader/help', + ariaLabel: 'Click for help resources/frequently asked questions on using Caseflow Reader' }, { name: 'Hearings Help', - url: '/hearing_prep/help' }, + url: '/hearing_prep/help', + ariaLabel: 'Click for help resources/frequently asked questions on using Caseflow Hearings' }, { name: 'Intake Help', - url: '/intake/help' }, + url: '/intake/help', + ariaLabel: 'Click for help resources/frequently asked questions on using Caseflow Intake' }, { name: 'Queue Help', - url: '/queue/help' }, + url: '/queue/help', + ariaLabel: 'Click for help resources/frequently asked questions on using Caseflow Queue' }, { name: 'VHA Help', - url: '/vha/help' }, + url: '/vha/help', + ariaLabel: 'Click for help resources/frequently asked questions on using Caseflow VHA' }, ]; return

Go Back

-

Caseflow Help

+

Caseflow Help

; From 609ca8132952996c4104ec88ba1ad00d1d5b2a1d Mon Sep 17 00:00:00 2001 From: AimanK Date: Tue, 27 Jun 2023 12:29:34 -0400 Subject: [PATCH 02/34] Schema check in --- db/schema.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index c43d1649e9d..a71a2b7b68b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -91,7 +91,7 @@ t.boolean "appeal_docketed", default: false, null: false, comment: "When true, appeal has been docketed" t.bigint "appeal_id", null: false, comment: "AMA or Legacy Appeal ID" t.string "appeal_type", null: false, comment: "Appeal Type (Appeal or LegacyAppeal)" - t.datetime "created_at", null: false, comment: "Date and Time the record was inserted into the table" + t.datetime "created_at", null: false t.bigint "created_by_id", null: false, comment: "User id of the user that inserted the record" t.boolean "decision_mailed", default: false, null: false, comment: "When true, appeal has decision mail request complete" t.boolean "hearing_postponed", default: false, null: false, comment: "When true, appeal has hearing postponed and no hearings scheduled" @@ -100,7 +100,7 @@ t.boolean "privacy_act_complete", default: false, null: false, comment: "When true, appeal has a privacy act request completed" t.boolean "privacy_act_pending", default: false, null: false, comment: "When true, appeal has a privacy act request still open" t.boolean "scheduled_in_error", default: false, null: false, comment: "When true, hearing was scheduled in error and none scheduled" - t.datetime "updated_at", comment: "Date and time the record was last updated" + t.datetime "updated_at" t.bigint "updated_by_id", comment: "User id of the last user that updated the record" t.boolean "vso_ihp_complete", default: false, null: false, comment: "When true, appeal has a VSO IHP request completed" t.boolean "vso_ihp_pending", default: false, null: false, comment: "When true, appeal has a VSO IHP request pending" @@ -1263,7 +1263,7 @@ t.string "appeals_type", null: false, comment: "Type of Appeal" t.datetime "created_at", comment: "Timestamp of when Noticiation was Created" t.boolean "email_enabled", default: true, null: false - t.text "email_notification_content", comment: "Full Email Text Content of Notification" + t.string "email_notification_content", comment: "Full Email Text Content of Notification" t.string "email_notification_external_id", comment: "VA Notify Notification Id for the email notification send through their API " t.string "email_notification_status", comment: "Status of the Email Notification" t.date "event_date", null: false, comment: "Date of Event" @@ -1274,8 +1274,8 @@ t.string "participant_id", comment: "ID of Participant" t.string "recipient_email", comment: "Participant's Email Address" t.string "recipient_phone_number", comment: "Participants Phone Number" - t.text "sms_notification_content", comment: "Full SMS Text Content of Notification" - t.string "sms_notification_external_id" + t.string "sms_notification_content", comment: "Full SMS Text Content of Notification" + t.string "sms_notification_external_id", comment: "VA Notify Notification Id for the sms notification send through their API " t.string "sms_notification_status", comment: "Status of SMS/Text Notification" t.datetime "updated_at", comment: "TImestamp of when Notification was Updated" t.index ["appeals_id", "appeals_type"], name: "index_appeals_notifications_on_appeals_id_and_appeals_type" From 488699ee26d236e91f1a6f703f738feed99a7be6 Mon Sep 17 00:00:00 2001 From: Lauren Berry Date: Tue, 25 Jul 2023 08:11:13 -0700 Subject: [PATCH 03/34] laurenoelle/APPEALS-19871 initial files --- client/app/reader/LastRetrievalAlert.jsx | 22 ++++++++++------------ client/app/reader/LastRetrievalInfo.jsx | 9 +-------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/client/app/reader/LastRetrievalAlert.jsx b/client/app/reader/LastRetrievalAlert.jsx index 880296536b5..da2f5c981cf 100644 --- a/client/app/reader/LastRetrievalAlert.jsx +++ b/client/app/reader/LastRetrievalAlert.jsx @@ -15,34 +15,32 @@ class LastRetrievalAlert extends React.PureComponent { render() { - // Check that document manifests have been recieved from VVA and VBMS - if (!this.props.manifestVbmsFetchedAt || !this.props.manifestVvaFetchedAt) { + // Check that document manifests have been recieved from VBMS + if (!this.props.manifestVbmsFetchedAt) { return
Some of {this.props.appeal.veteran_full_name}'s documents are not available at the moment due to - a loading error from VBMS or VVA. As a result, you may be viewing a partial list of claims folder documents. + a loading error from VBMS. As a result, you may be viewing a partial list of claims folder documents.

- Please refresh your browser at a later point to view a complete list of documents in the claims + Please visit at a later point to view a complete list of documents in the claims folder.
; } const staleCacheTime = moment().subtract(CACHE_TIMEOUT_HOURS, 'h'), - vbmsManifestTimestamp = moment(this.props.manifestVbmsFetchedAt, 'MM/DD/YY HH:mma Z'), - vvaManifestTimestamp = moment(this.props.manifestVvaFetchedAt, 'MM/DD/YY HH:mma Z'); + vbmsManifestTimestamp = moment(this.props.manifestVbmsFetchedAt, 'MM/DD/YY HH:mma Z'); // Check that manifest results are fresh - if (vbmsManifestTimestamp.isBefore(staleCacheTime) || vvaManifestTimestamp.isBefore(staleCacheTime)) { + if (vbmsManifestTimestamp.isBefore(staleCacheTime)) { const now = moment(), - vbmsDiff = now.diff(vbmsManifestTimestamp, 'hours'), - vvaDiff = now.diff(vvaManifestTimestamp, 'hours'); + vbmsDiff = now.diff(vbmsManifestTimestamp, 'hours'); return
- We last synced with VBMS and VVA {Math.max(vbmsDiff, vvaDiff)} hours ago. If you'd like to check for new - documents, refresh the page. + We last synced with VBMS {vbmsDiff} hours ago. If you'd like to check for new + documents, visit later.
; } @@ -52,5 +50,5 @@ class LastRetrievalAlert extends React.PureComponent { } export default connect( - (state) => _.pick(state.documentList, ['manifestVvaFetchedAt', 'manifestVbmsFetchedAt']) + (state) => _.pick(state.documentList, ['manifestVbmsFetchedAt']) )(LastRetrievalAlert); diff --git a/client/app/reader/LastRetrievalInfo.jsx b/client/app/reader/LastRetrievalInfo.jsx index 01e43efda38..bd88e37d3d4 100644 --- a/client/app/reader/LastRetrievalInfo.jsx +++ b/client/app/reader/LastRetrievalInfo.jsx @@ -11,18 +11,11 @@ class UnconnectedLastRetrievalInfo extends React.PureComponent { :
Unable to display VBMS documents at this time -
, - this.props.manifestVvaFetchedAt ? -
- Last VVA retrieval: {this.props.manifestVvaFetchedAt.slice(0, -5)} -
: -
- Unable to display VVA documents at this time
]; } } export default connect( - (state) => _.pick(state.documentList, ['manifestVvaFetchedAt', 'manifestVbmsFetchedAt']) + (state) => _.pick(state.documentList, ['manifestVbmsFetchedAt']) )(UnconnectedLastRetrievalInfo); From e899e79168b9e356448ca0813c5fa2e6e46524dc Mon Sep 17 00:00:00 2001 From: Lauren Berry Date: Wed, 26 Jul 2023 12:34:39 -0700 Subject: [PATCH 04/34] Fixing error with .pick --- client/app/reader/LastRetrievalAlert.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/reader/LastRetrievalAlert.jsx b/client/app/reader/LastRetrievalAlert.jsx index da2f5c981cf..c1253ae0c90 100644 --- a/client/app/reader/LastRetrievalAlert.jsx +++ b/client/app/reader/LastRetrievalAlert.jsx @@ -50,5 +50,5 @@ class LastRetrievalAlert extends React.PureComponent { } export default connect( - (state) => _.pick(state.documentList, ['manifestVbmsFetchedAt']) + (state) => _.pick(state.documentList, 'manifestVbmsFetchedAt') )(LastRetrievalAlert); From 3af682cc249e6648aad2fad4ba38e23d813ba8d0 Mon Sep 17 00:00:00 2001 From: Lauren Berry Date: Wed, 26 Jul 2023 12:47:36 -0700 Subject: [PATCH 05/34] Fixing .pick --- client/app/reader/LastRetrievalInfo.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/reader/LastRetrievalInfo.jsx b/client/app/reader/LastRetrievalInfo.jsx index bd88e37d3d4..804c926f9f5 100644 --- a/client/app/reader/LastRetrievalInfo.jsx +++ b/client/app/reader/LastRetrievalInfo.jsx @@ -17,5 +17,5 @@ class UnconnectedLastRetrievalInfo extends React.PureComponent { } export default connect( - (state) => _.pick(state.documentList, ['manifestVbmsFetchedAt']) + (state) => _.pick(state.documentList, 'manifestVbmsFetchedAt') )(UnconnectedLastRetrievalInfo); From 2e476e76c181fa96d65e2819757bc703115ad2c3 Mon Sep 17 00:00:00 2001 From: AnandEdara <131183324+AnandEdara@users.noreply.github.com> Date: Fri, 28 Jul 2023 00:19:09 -0500 Subject: [PATCH 06/34] Adding a rails initializer for monitoring deprecation warnings --- config/initializers/deprecation_alerts.rb | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 config/initializers/deprecation_alerts.rb diff --git a/config/initializers/deprecation_alerts.rb b/config/initializers/deprecation_alerts.rb new file mode 100644 index 00000000000..dadc9229281 --- /dev/null +++ b/config/initializers/deprecation_alerts.rb @@ -0,0 +1,25 @@ +require "#{Rails.root}/app/services/slack_service" + +if Rails.env.production? + ActiveSupport::Deprecation.behavior = ->(message, callstack) do + # Log the deprecation warning to Rails logger + Rails.logger.warn(message) + + # Log the deprecation warning to Sentry + Raven.capture_message( + 'Rails Deprecation Warning in Production', + level: 'warning', + extra: { + message: message, + callstack: callstack, + environment: Rails.env + } + ) + + # Send the deprecation warning to the Slack channel + slack_msg = "Deprecation Warning: #{message}" + SlackService.new(url: ENV['SLACK_DISPATCH_ALERT_URL']).send_notification(slack_msg, "Deprecation Warning", "#appeals-deprecation-alerts") + + ActiveSupport::Deprecation::DEFAULT_BEHAVIORS[:stderr].call(message, callstack) + end +end From 1e014051c8b52d16aaaab122937ea531774d8035 Mon Sep 17 00:00:00 2001 From: Lauren Berry Date: Mon, 31 Jul 2023 08:44:13 -0700 Subject: [PATCH 07/34] Modifying jest tests --- client/app/reader/DocumentList/DocumentListActions.js | 7 ++++--- client/app/reader/DocumentList/DocumentListReducer.js | 6 +----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/client/app/reader/DocumentList/DocumentListActions.js b/client/app/reader/DocumentList/DocumentListActions.js index 0d24e4fc024..a6dfcd72459 100644 --- a/client/app/reader/DocumentList/DocumentListActions.js +++ b/client/app/reader/DocumentList/DocumentListActions.js @@ -182,8 +182,9 @@ export const setViewingDocumentsOrComments = (documentsOrComments) => ({ } }); -export const onReceiveManifests = (manifestVbmsFetchedAt, manifestVvaFetchedAt) => ({ +export const onReceiveManifests = (manifestVbmsFetchedAt) => ({ type: Constants.RECEIVE_MANIFESTS, - payload: { manifestVbmsFetchedAt, - manifestVvaFetchedAt } + payload: { + manifestVbmsFetchedAt + } }); diff --git a/client/app/reader/DocumentList/DocumentListReducer.js b/client/app/reader/DocumentList/DocumentListReducer.js index dd96cc2539e..1107e7cea8b 100644 --- a/client/app/reader/DocumentList/DocumentListReducer.js +++ b/client/app/reader/DocumentList/DocumentListReducer.js @@ -54,8 +54,7 @@ const initialState = { category: false } }, - manifestVbmsFetchedAt: null, - manifestVvaFetchedAt: null + manifestVbmsFetchedAt: null }; const documentListReducer = (state = initialState, action = {}) => { @@ -181,9 +180,6 @@ const documentListReducer = (state = initialState, action = {}) => { return update(state, { manifestVbmsFetchedAt: { $set: action.payload.manifestVbmsFetchedAt - }, - manifestVvaFetchedAt: { - $set: action.payload.manifestVvaFetchedAt } }); case Constants.UPDATE_FILTERED_RESULTS: From 50210b0275755fe62b989d958aeddd3604ed72f9 Mon Sep 17 00:00:00 2001 From: Lauren Berry Date: Wed, 2 Aug 2023 10:03:42 -0700 Subject: [PATCH 08/34] Updated language following feedback from UX team --- client/app/reader/LastRetrievalAlert.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/app/reader/LastRetrievalAlert.jsx b/client/app/reader/LastRetrievalAlert.jsx index c1253ae0c90..1111163be2f 100644 --- a/client/app/reader/LastRetrievalAlert.jsx +++ b/client/app/reader/LastRetrievalAlert.jsx @@ -15,7 +15,7 @@ class LastRetrievalAlert extends React.PureComponent { render() { - // Check that document manifests have been recieved from VBMS + // Check that document manifests have been recieved from VBMS -- red banner if (!this.props.manifestVbmsFetchedAt) { return
@@ -24,7 +24,7 @@ class LastRetrievalAlert extends React.PureComponent {

Please visit at a later point to view a complete list of documents in the claims - folder. + folder or force a sync by visiting eFolder and downloading the documents.
; } @@ -32,7 +32,7 @@ class LastRetrievalAlert extends React.PureComponent { const staleCacheTime = moment().subtract(CACHE_TIMEOUT_HOURS, 'h'), vbmsManifestTimestamp = moment(this.props.manifestVbmsFetchedAt, 'MM/DD/YY HH:mma Z'); - // Check that manifest results are fresh + // Check that manifest results are fresh -- yellow banner if (vbmsManifestTimestamp.isBefore(staleCacheTime)) { const now = moment(), vbmsDiff = now.diff(vbmsManifestTimestamp, 'hours'); @@ -40,7 +40,7 @@ class LastRetrievalAlert extends React.PureComponent { return
We last synced with VBMS {vbmsDiff} hours ago. If you'd like to check for new - documents, visit later. + documents, visit later or force a sync by visiting eFolder and downloading the documents.
; } From 74f59d924db5d971b3e5005b03996908b98a5aae Mon Sep 17 00:00:00 2001 From: kristeja Date: Wed, 2 Aug 2023 16:33:36 -0700 Subject: [PATCH 09/34] APPEALS-17497 MST & PACT Special issues identification changes --- Gemfile | 2 +- Gemfile.lock | 6 +- app/controllers/appeals_controller.rb | 252 +- app/controllers/case_reviews_controller.rb | 7 +- app/controllers/intakes_controller.rb | 7 +- app/controllers/issues_controller.rb | 97 +- app/mappers/issue_mapper.rb | 10 +- app/models/appeal.rb | 24 + app/models/concerns/issue_updater.rb | 79 +- app/models/contestable_issue.rb | 42 +- app/models/decision_issue.rb | 4 +- app/models/etl/decision_issue.rb | 2 + app/models/hearing_issue_note.rb | 4 +- app/models/issue.rb | 7 +- app/models/legacy_appeal.rb | 36 + app/models/legacy_hearing.rb | 15 +- .../organizations/special_issue_edit_team.rb | 12 + app/models/rating.rb | 148 +- app/models/rating_decision.rb | 42 +- app/models/rating_issue.rb | 28 +- app/models/request_issue.rb | 34 +- app/models/request_issues_update.rb | 205 +- .../idt/v1/appeal_details_serializer.rb | 9 +- .../work_queue/appeal_serializer.rb | 14 +- .../work_queue/legacy_appeal_serializer.rb | 8 + .../work_queue/legacy_issue_serializer.rb | 2 + .../work_queue/legacy_task_serializer.rb | 8 + .../work_queue/task_column_serializer.rb | 16 + .../serializers/work_queue/task_serializer.rb | 10 +- app/models/special_issue_change.rb | 5 + app/models/tasks/establishment_task.rb | 75 + app/models/tasks/issues_update_task.rb | 48 + .../tasks/judge_decision_review_task.rb | 6 +- .../tasks/judge_dispatch_return_task.rb | 10 +- app/models/user.rb | 5 + app/models/vacols/case_issue.rb | 6 +- app/policies/appeal_request_issues_policy.rb | 19 +- app/repositories/task_action_repository.rb | 4 + .../hearings/hearing_serializer.rb | 6 + .../hearings/legacy_hearing_serializer.rb | 9 + .../intake/decision_issue_serializer.rb | 2 + .../intake/legacy_appeal_serializer.rb | 103 + .../intake/rating_issue_serializer.rb | 1 + .../intake/request_issue_serializer.rb | 6 + app/services/external_api/bgs_service.rb | 18 + app/views/appeals/edit.html.erb | 6 + app/views/hearings/index.html.erb | 3 + app/views/higher_level_reviews/edit.html.erb | 1 + app/views/queue/index.html.erb | 4 + app/views/supplemental_claims/edit.html.erb | 1 + app/workflows/initial_tasks_factory.rb | 22 +- app/workflows/rating_profile_disability.rb | 4 + client/COPY.json | 15 + client/app/admin/reducers/featureToggle.js | 8 +- client/app/components/AmaIssueList.jsx | 44 +- client/app/components/Checkbox.jsx | 1 - client/app/components/CheckboxGroup.jsx | 5 +- client/app/components/RadioField.jsx | 1 - client/app/components/badges/BadgeArea.jsx | 6 + .../components/badges/MstBadge/MstBadge.jsx | 44 + .../badges/MstBadge/MstBadge.stories.js | 20 + .../badges/MstBadge/MstBadge.test.js | 33 + .../__snapshots__/MstBadge.test.js.snap | 163 ++ .../components/badges/PactBadge/PactBadge.jsx | 44 + .../badges/PactBadge/PactBadge.stories.js | 20 + .../badges/PactBadge/PactBadge.test.js | 33 + .../__snapshots__/PactBadge.test.js.snap | 163 ++ client/app/constants/AppConstants.js | 2 + client/app/constants/SpecialIssueFilters.js | 13 +- client/app/hearings/HearingsApp.jsx | 15 +- client/app/hearings/components/Details.jsx | 1 + .../assignHearings/AssignHearings.jsx | 13 +- .../assignHearings/AssignHearingsList.jsx | 13 +- .../assignHearings/AssignHearingsTable.jsx | 41 +- .../assignHearings/AssignHearingsTabs.jsx | 21 +- .../assignHearings/TimeSlotCard.jsx | 15 +- .../assignHearings/UpcomingHearingsTable.jsx | 12 +- .../dailyDocket/DailyDocketPrinted.jsx | 4 + .../dailyDocket/DailyDocketRowDisplayText.jsx | 7 + .../hearings/components/dailyDocket/style.js | 5 + .../components/details/DetailsHeader.jsx | 12 +- .../HearingWorksheetPrinted.jsx | 16 + .../scheduleHearing/AppealInformation.jsx | 6 + .../scheduleHearing/TimeSlotDetail.jsx | 25 + .../containers/AssignHearingsContainer.jsx | 13 +- client/app/hearings/utils.js | 2 + client/app/intake/IntakeFrame.jsx | 5 +- client/app/intake/actions/addIssues.js | 15 + .../app/intake/components/AddIssueManager.jsx | 8 +- .../app/intake/components/AddIssuesModal.jsx | 158 +- client/app/intake/components/AddedIssue.jsx | 23 +- .../intake/components/IntakeRadioField.jsx | 325 +++ client/app/intake/components/IssueList.jsx | 39 +- .../components/NonratingRequestIssueModal.jsx | 53 + client/app/intake/constants.js | 2 + client/app/intake/pages/addIssues.jsx | 69 +- client/app/intake/reducers/common.js | 25 +- client/app/intake/reducers/featureToggles.js | 16 + client/app/intake/util/issues.js | 88 +- client/app/intakeEdit/IntakeEditFrame.jsx | 1 + .../components/EditIntakeIssueModal.jsx | 215 ++ .../EditIntakeIssueModal.stories.js | 36 + client/app/intakeEdit/reducers/index.js | 11 +- client/app/nonComp/components/Alerts.jsx | 10 + client/app/queue/AddEditIssueView.jsx | 77 +- client/app/queue/CaseDetailsView.jsx | 1 + client/app/queue/CaseListTable.jsx | 10 +- .../queue/LegacySelectDispositionsView.jsx | 7 +- client/app/queue/QueueApp.jsx | 8 + client/app/queue/SelectDispositionsView.jsx | 264 ++- client/app/queue/SelectSpecialIssuesView.jsx | 22 +- .../queue/components/CaseDetailsIssueList.jsx | 80 +- .../app/queue/components/ContestedIssues.jsx | 16 + .../app/queue/components/DecisionIssues.jsx | 29 +- .../components/IssueRemandReasonsOptions.jsx | 16 +- .../queue/components/LegacyIssueListItem.jsx | 24 +- .../queue/components/QueueCheckboxGroup.jsx | 133 ++ client/app/queue/components/QueueFlowPage.jsx | 6 +- client/app/queue/components/TaskRows.jsx | 157 +- client/app/queue/constants.js | 36 + client/app/queue/utils.js | 38 + client/app/styles/hearings/_hearings.scss | 5 + client/constants/TASK_ACTIONS.json | 2 +- .../__snapshots__/Details.test.js.snap | 1326 +++++++++++ .../ScheduleVeteran.test.js.snap | 2058 +++++++++++++++++ .../ScheduleVeteranForm.test.js.snap | 1488 ++++++++++++ .../assignHearings/TimeSlotCard.test.js | 2 +- .../AssignHearingsList.test.js.snap | 36 + .../__snapshots__/TimeSlotCard.test.js.snap | 12 + .../__snapshots__/DailyDocketRow.test.js.snap | 21 + .../AppealInformation.test.js.snap | 36 + .../__snapshots__/TimeSlotButton.test.js.snap | 12 + .../__snapshots__/TimeSlotDetail.test.js.snap | 24 + .../util/__snapshots__/issues.test.js.snap | 24 + ...12154615_add_mst_pact_to_request_issues.rb | 6 + ...est_issue_ids_to_request_issues_updates.rb | 6 + ..._add_mst_pact_reasons_to_request_issues.rb | 6 + ...add_vbms_mst_and_pact_to_request_issues.rb | 6 + ...0609124110_create_special_issue_changes.rb | 21 + ...hange_category_to_special_issue_changes.rb | 5 + ...0609161731_update_special_issue_changes.rb | 19 + ...3132146_add_mst_pact_to_decision_issues.rb | 6 + ...dd_decision_id_to_special_issue_changes.rb | 5 + db/schema.rb | 30 + db/seeds.rb | 1 + db/seeds/bgs_service_record_maker.rb | 11 + db/seeds/users.rb | 11 +- lib/fakes/bgs_service.rb | 4 + lib/fakes/bgs_service_record_maker.rb | 175 +- lib/generators/bgs_contention.rb | 50 + lib/generators/contention.rb | 106 + lib/generators/rating.rb | 8 +- lib/generators/vacols/case.rb | 12 +- lib/generators/vacols/case_issue.rb | 9 +- lib/generators/veteran.rb | 3 +- lib/tasks/seed_legacy_appeals.rake | 142 ++ local/vacols/bgs_setup.csv | 42 +- local/vacols/vacols_copy_2_tables_dev.sql | 4 +- local/vacols/vacols_copy_2_tables_test.sql | 4 +- scripts/enable_features_dev.rb | 7 +- spec/controllers/appeals_controller_spec.rb | 61 + .../case_reviews_controller_spec.rb | 18 + spec/controllers/intakes_controller_spec.rb | 68 + spec/controllers/issues_controller_spec.rb | 20 +- spec/factories/special_issue_list.rb | 8 + spec/feature/intake/add_issues_spec.rb | 182 +- spec/feature/intake/appeal/edit_spec.rb | 129 ++ spec/feature/intake/appeal_spec.rb | 118 +- .../nonrating_request_issue_modal_spec.rb | 43 +- .../feature/intake/supplemental_claim_spec.rb | 3 +- spec/feature/queue/ama_queue_spec.rb | 95 +- spec/feature/queue/ama_queue_workflow_spec.rb | 466 ++++ .../queue/attorney_checkout_flow_spec.rb | 499 +++- .../queue/bva_dispatch_return_flow_spec.rb | 7 - spec/feature/queue/case_details_spec.rb | 89 + .../queue/delete_request_issues_spec.rb | 7 +- .../feature/queue/judge_checkout_flow_spec.rb | 403 +++- .../feature/queue/quality_review_flow_spec.rb | 3 - spec/feature/withdrawn_request_issues_spec.rb | 4 - spec/mappers/issue_mapper_spec.rb | 13 +- spec/models/appeal_spec.rb | 110 + spec/models/contestable_issue_spec.rb | 20 +- spec/models/decision_review_spec.rb | 16 +- spec/models/end_product_establishment_spec.rb | 50 + spec/models/issues_update_task_spec.rb | 237 ++ spec/models/judge_case_review_spec.rb | 5 +- spec/models/request_issue_spec.rb | 330 ++- spec/models/request_issues_update_spec.rb | 42 + .../idt/appeal_details_serializer_spec.rb | 45 +- spec/models/tasks/judge_task_spec.rb | 4 +- spec/seeds/intake_spec.rb | 4 +- spec/support/intake_helpers.rb | 118 + spec/workflows/initial_tasks_factory_spec.rb | 66 + 193 files changed, 12992 insertions(+), 389 deletions(-) create mode 100644 app/models/organizations/special_issue_edit_team.rb create mode 100644 app/models/special_issue_change.rb create mode 100644 app/models/tasks/establishment_task.rb create mode 100644 app/models/tasks/issues_update_task.rb create mode 100644 app/serializers/intake/legacy_appeal_serializer.rb create mode 100644 client/app/components/badges/MstBadge/MstBadge.jsx create mode 100644 client/app/components/badges/MstBadge/MstBadge.stories.js create mode 100644 client/app/components/badges/MstBadge/MstBadge.test.js create mode 100644 client/app/components/badges/MstBadge/__snapshots__/MstBadge.test.js.snap create mode 100644 client/app/components/badges/PactBadge/PactBadge.jsx create mode 100644 client/app/components/badges/PactBadge/PactBadge.stories.js create mode 100644 client/app/components/badges/PactBadge/PactBadge.test.js create mode 100644 client/app/components/badges/PactBadge/__snapshots__/PactBadge.test.js.snap create mode 100644 client/app/intake/components/IntakeRadioField.jsx create mode 100644 client/app/intakeEdit/components/EditIntakeIssueModal.jsx create mode 100644 client/app/intakeEdit/components/EditIntakeIssueModal.stories.js create mode 100644 client/app/queue/components/QueueCheckboxGroup.jsx create mode 100644 db/migrate/20230412154615_add_mst_pact_to_request_issues.rb create mode 100644 db/migrate/20230508182827_add_mst_and_pact_edited_request_issue_ids_to_request_issues_updates.rb create mode 100644 db/migrate/20230508190230_add_mst_pact_reasons_to_request_issues.rb create mode 100644 db/migrate/20230531163349_add_vbms_mst_and_pact_to_request_issues.rb create mode 100644 db/migrate/20230609124110_create_special_issue_changes.rb create mode 100644 db/migrate/20230609153757_add_change_category_to_special_issue_changes.rb create mode 100644 db/migrate/20230609161731_update_special_issue_changes.rb create mode 100644 db/migrate/20230623132146_add_mst_pact_to_decision_issues.rb create mode 100644 db/migrate/20230629114954_add_decision_id_to_special_issue_changes.rb create mode 100644 db/seeds/bgs_service_record_maker.rb create mode 100644 lib/tasks/seed_legacy_appeals.rake create mode 100644 spec/factories/special_issue_list.rb create mode 100644 spec/feature/queue/ama_queue_workflow_spec.rb create mode 100644 spec/models/issues_update_task_spec.rb diff --git a/Gemfile b/Gemfile index d334b2634f8..df9a7c2e660 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 710c4378d8d..de2664ad49b 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 9813c971005..17327609881 100644 --- a/app/controllers/appeals_controller.rb +++ b/app/controllers/appeals_controller.rb @@ -164,7 +164,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 @@ -178,18 +180,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), @@ -203,6 +199,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) @@ -270,7 +278,226 @@ 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 + 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 { |i| i[: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 { |i| i.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 + + # create MST/PACT message for added/removed issues + 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 + + # check if there is a change in mst/pact on legacy issue + # if there is a change, creat an issue update task + def legacy_mst_pact_updates + legacy_issue_params[:request_issues].each do |current_issue| + issue = appeal.issues.find { |i| i.vacols_sequence_id == current_issue[:vacols_sequence_id].to_i } + + # Check for changes in mst/pact status + if 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 + end + set_flash_mst_edit_message + render json: { issues: json_issues }, status: :ok + end + + 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 { |i| i.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 { |i| i.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 + + 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 + task.format_instructions( + "Edited Issue", + [ + "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"), + "", + before_issue.mst_status, + before_issue.pact_status, + current_issue[:mst_status], + current_issue[:pact_status] + ) + 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 + + # updated flash message to show mst/pact message if mst/pact changes (not to legacy) 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 + + # 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? @@ -395,4 +622,3 @@ def get_appeal_object(appeals_id) end end end - 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 e7401c09488..dc0ae020f41 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 @@ -97,6 +97,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, @@ -150,6 +151,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..71b2b4b48b7 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 @@ -19,7 +20,14 @@ class IssuesController < ApplicationController 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 { |i| i.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 @@ -27,11 +35,21 @@ def create def update return record_not_found unless appeal + issue = appeal.issues.find { |i| i.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 @@ -47,6 +65,79 @@ def destroy private + 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 + task.format_instructions( + change_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"), + "", + issue.mst_status, + issue.pact_status, + updated_mst_status, + updated_pact_status + ) + 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 + + 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 +159,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 b8550e58709..4ea29b9773a 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -266,6 +266,26 @@ def contested_claim? end end + # 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 + + 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 @@ -931,6 +951,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/concerns/issue_updater.rb b/app/models/concerns/issue_updater.rb index aba47fca7a9..80c25510e0a 100644 --- a/app/models/concerns/issue_updater.rb +++ b/app/models/concerns/issue_updater.rb @@ -36,7 +36,8 @@ def update_issue_dispositions_in_vacols! private 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,12 +48,24 @@ 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 || + request_issue.pact_status != decision_issue.pact_status) && + FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) || + 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 @@ -108,4 +121,66 @@ def create_remand_reasons(decision_issue, remand_reasons_attrs) end end end + + 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] + ) + + task.format_instructions( + "Edited Issue", + task_text_helper([original_issue.contested_issue_description, original_issue.nonrating_issue_category, original_issue.nonrating_issue_description]), + task_text_benefit_type(original_issue), + original_issue.mst_status, + original_issue.pact_status, + decision_issue.mst_status, + decision_issue.pact_status + ) + + 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 + + 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 diff --git a/app/models/contestable_issue.rb b/app/models/contestable_issue.rb index 32a44afec5f..f3af9d212ed 100644 --- a/app/models/contestable_issue.rb +++ b/app/models/contestable_issue.rb @@ -11,10 +11,11 @@ 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) + # epe = EndProductEstablishment.find_by(reference_id: rating_issue.reference_id) new( rating_issue_reference_id: rating_issue.reference_id, rating_issue_profile_date: rating_issue.profile_date.to_date, @@ -30,7 +31,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: rating_issue.special_issues ) end @@ -48,7 +50,7 @@ def from_decision_issue(decision_issue, contesting_decision_review) source_request_issues: decision_issue.request_issues.active, source_decision_review: source, contesting_decision_review: contesting_decision_review, - is_rating: decision_issue.rating? + is_rating: decision_issue.rating?, ) end @@ -61,7 +63,8 @@ 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, - is_rating: true # true even if rating_reference_id is nil + special_issues: rating_decision.special_issues, + is_rating: true, # true even if rating_reference_id is nil ) end end @@ -80,7 +83,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 +117,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 8106a302538..96f195bbe4f 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 @@ -328,7 +329,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 6c83e717938..c2a36ba1cf8 100644 --- a/app/models/legacy_appeal.rb +++ b/app/models/legacy_appeal.rb @@ -630,6 +630,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| @@ -915,6 +932,25 @@ def claimant_participant_id veteran_is_not_claimant ? person_for_appellant&.participant_id : veteran&.participant_id end + def hearing_day_if_schedueled + hearing_date = Hearing.find_by(appeal_id: id) + + if hearing_date.nil? + return nil + + else + return hearing_date.hearing_day.scheduled_for + end + end + + def ui_hash + Intake::LegacyAppealSerializer.new(self).serializable_hash[:data][:attributes] + end + + def is_legacy? + true + end + 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..db640a7be0a 100644 --- a/app/models/legacy_hearing.rb +++ b/app/models/legacy_hearing.rb @@ -268,6 +268,17 @@ def original_request_type end end + def prepare_worksheet_issues + worksheet_issues = [] + appeal.worksheet_issues.each_with_index do |wi, idx| + worksheet_issues.push(wi.attributes) + issue = appeal.issues.find { |i| i.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 +335,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..e913a3b5a86 100644 --- a/app/models/rating.rb +++ b/app/models/rating.rb @@ -8,6 +8,21 @@ class Rating ONE_YEAR_PLUS_DAYS = 372.days TWO_LIFETIMES = 250.years + 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 = [ + "pact", + "pactdicre", + "pees1" + ].freeze class NilRatingProfileListError < StandardError def ignorable? @@ -32,6 +47,10 @@ def fetch_in_range(*) fail Caseflow::Error::MustImplementInSubclass end + def fetch_contentions_by_participant_id(participant_id) + BGSService.new.find_contentions_by_participant_id(participant_id) + end + def sorted_ratings_from_bgs_response(response:, start_date:) unsorted = ratings_from_bgs_response(response) unpromulgated = unsorted.select { |rating| rating.promulgation_date.nil? } @@ -49,6 +68,96 @@ def fetch_promulgated(participant_id) def from_bgs_hash(_data) fail Caseflow::Error::MustImplementInSubclass end + + def special_issue_has_mst?(special_issue) + 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 + + def special_issue_has_pact?(special_issue) + if special_issue[:spis_tn]&.casecmp("gulf war presumptive 3.320")&.zero? + return special_issue[:spis_basis_tn]&.casecmp("particulate matter")&.zero? + end + + PACT_SPECIAL_ISSUES.include?(special_issue[:spis_tn]&.downcase) + end + + def mst_from_contentions_for_rating?(contentions) + return false if contentions.blank? + + contentions.any? { |contention| mst_contention_status?(contention) } + end + + def pact_from_contentions_for_rating?(contentions) + return false if contentions.blank? + + contentions.any? { |contention| pact_contention_status?(contention) } + end + + def participant_contentions(serialized_hash) + # guard for MST/PACT feature toggle + # commented out for testing + # return [] unless FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) || + # FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) + + contentions_data = [] + response = fetch_contentions_by_participant_id(serialized_hash[:participant_id]) + + return if response.nil? + + serialized_hash[:rba_contentions_data].each do |rba| + rba_contention = rba.with_indifferent_access + response.each do |resp| + next unless resp.is_a?(Hash) + + # if only one contention, check the contention info + if resp.dig(:contentions).is_a?(Hash) + # get the single contention from the response + cntn = resp.dig(:contentions) + + next if cntn.blank? + + # see if the contetion ties to the rating + contentions_data << cntn if cntn.dig(:cntntn_id) == rba_contention.dig(:cntntn_id) + + # if the response contains an array of contentions, unpack each one and compare + elsif resp.dig(:contentions).is_a?(Array) + + resp.dig(:contentions).each do |contention| + next if contention.dig(:cntntn_id).blank? + + contentions_data << contention if contention.dig(:cntntn_id) == rba_contention.dig(:cntntn_id) + end + end + end + end + contentions_data.compact + end + + def mst_contention_status?(bgs_contention) + return false if bgs_contention.nil? || bgs_contention[:special_issues].blank? + + if bgs_contention[:special_issues].is_a?(Hash) + bgs_contention[:special_issues][:spis_tc] == "MST" + elsif bgs_contention[:special_issues].is_a?(Array) + bgs_contention[:special_issues].any? { |issue| issue[:spis_tc] == "MST" } + end + end + + def pact_contention_status?(bgs_contention) + 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 end # WARNING: profile_date is a misnomer adopted from BGS terminology. @@ -65,12 +174,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,10 +191,46 @@ 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 + disability[:rba_contentions_data] = rba_contentions_data(disability) + RatingDecision.from_bgs_disability(self, disability) end end + def rba_contentions_data(disability) + rating_issues.each do |issue| + next unless issue[:dis_sn] == disability[:dis_sn] + + return ensure_array_of_hashes(issue[:rba_issue_contentions]) + end + end + + def veteran + @veteran ||= Veteran.find_by(participant_id: participant_id) + end + + def rating_issues + return [] unless veteran + + veteran.ratings.map { |rating| Array.wrap(rating.rating_profile[:rating_issues]) }.compact.flatten + + # return empty list when there are no ratings + rescue PromulgatedRating::BackfilledRatingError + # Ignore PromulgatedRating::BackfilledRatingErrors since they are a regular occurrence and we don't need to take + # any action when we see them. + [] + rescue PromulgatedRating::LockedRatingError => error + Raven.capture_exception(error) + [] + end + + def ensure_array_of_hashes(array_or_hash_or_nil) + [array_or_hash_or_nil || {}].flatten.map(&:deep_symbolize_keys) + end + def associated_end_products associated_claims_data.map do |claim_data| EndProduct.new( diff --git a/app/models/rating_decision.rb b/app/models/rating_decision.rb index 915a48369b4..d5c4de2ff12 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,13 +52,46 @@ 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) + # reject unknown attributes to prevent UnknownAttribute errors + new(hash.merge(special_issues: deserialize_special_issues(hash)) + .reject { |k, _| !RatingDecision.attribute_method?(k) }) end + + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity + def deserialize_special_issues(serialized_hash) + data = [] + if serialized_hash[:special_issues].present? + filtered_special_issues = serialized_hash[:special_issues].map do |special_issue| + special_issue.with_indifferent_access if special_issue.with_indifferent_access[:dis_sn] == serialized_hash[:disability_id] # rubocop:disable Layout/LineLength + end.compact + + filtered_special_issues.each do |special_issue| + data << { mst_available: true } if Rating.special_issue_has_mst?(special_issue) + + data << { pact_available: true } if Rating.special_issue_has_pact?(special_issue) + end + end + + if serialized_hash[:rba_contentions_data] + # get the contentions from the rating by the participant id + contentions = Rating.participant_contentions(serialized_hash) + data << { mst_available: true } if Rating.mst_from_contentions_for_rating?(contentions) + + data << { pact_available: true } if Rating.pact_from_contentions_for_rating?(contentions) + end + data + end + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/CyclomaticComplexity end def decision_text diff --git a/app/models/rating_issue.rb b/app/models/rating_issue.rb index 0569fd13b98..0f1adc9f2b3 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,7 +49,8 @@ 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 @@ -66,6 +68,7 @@ def deserialize(serialized_hash) :reference_id, :subject_text ).merge(associated_end_products: deserialize_end_products(serialized_hash)) + .merge(special_issues: deserialize_special_issues(serialized_hash)) ) end @@ -82,6 +85,27 @@ def deserialize_end_products(serialized_hash) EndProduct.deserialize(end_product_hash) end end + + def deserialize_special_issues(serialized_hash) + # guard for MST/PACT feature toggle + return [] unless FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) || + FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) + + data = [] + serialized_hash[:special_issues]&.each do |special_issue| + data << { mst_available: true } if Rating.special_issue_has_mst?(special_issue) + + data << { pact_available: true } if Rating.special_issue_has_pact?(special_issue) + end + if serialized_hash[:rba_contentions_data] + # get the contentinons from the rating by the participant id + contentions = Rating.participant_contentions(serialized_hash) + data << { mst_available: true } if Rating.mst_from_contentions_for_rating?(contentions) + + data << { pact_available: true } if Rating.pact_from_contentions_for_rating?(contentions) + end + data + end end def serialize diff --git a/app/models/request_issue.rb b/app/models/request_issue.rb index 7b5bb191c87..a4fa8d29f58 100644 --- a/app/models/request_issue.rb +++ b/app/models/request_issue.rb @@ -206,7 +206,13 @@ 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 @@ -246,6 +252,30 @@ 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) + return ["PACT", "PACTDICRE", "PEES1"].include?(bgs_contention.special_issues[:spis_tc]) if bgs_contention&.special_issues + elsif bgs_contention.special_issues.is_a?(Array) + bgs_contention.special_issues.each do |issue| + return true if ["PACT", "PACTDICRE", "PEES1"].include?(issue[:spis_tc]) + end + end + false + end + def rating? !!associated_rating_issue? || !!previous_rating_issue? || @@ -609,7 +639,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 07781d18aef..527ec14b792 100644 --- a/app/models/request_issues_update.rb +++ b/app/models/request_issues_update.rb @@ -28,8 +28,12 @@ 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) ) + create_mst_pact_issue_update_tasks if FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) || + FeatureToggle.enabled?(:pact_identification, user: RequestStore[:current_user]) create_business_line_tasks! if added_issues.present? cancel_active_tasks submit_for_processing! @@ -90,8 +94,16 @@ 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,12 +127,51 @@ 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 @request_issues_data.select { |ri| ri[:edited_description].present? && ri[: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 @@ -129,6 +180,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 @@ -147,6 +201,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! @@ -154,6 +216,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! @@ -182,6 +246,37 @@ def process_edited_issues! end end + 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 + + 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 @@ -197,4 +292,112 @@ 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 similar sequence to what user sees the edit issues page + edited_issues.reverse_each do |before_issue| + after_issue = after_issues.find { |i| i.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 + + 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 = before_issue.vbms_mst_status.nil? ? false : !before_issue.vbms_mst_status && before_issue.mst_status + vbms_pact_edit = before_issue.vbms_pact_status.nil? ? false : !before_issue.vbms_pact_status && before_issue.pact_status + + # 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) + task.format_instructions( + change_type, + before_issue.contested_issue_description, + before_issue.benefit_type&.capitalize, + before_issue.vbms_mst_status, + before_issue.vbms_pact_status, + before_issue.mst_status, + 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? + task.format_instructions( + change_type, + issue_description, + before_issue.benefit_type&.capitalize, + before_issue.mst_status, + before_issue.pact_status, + after_issue&.mst_status, + after_issue&.pact_status + ) + end + # rubocop:enable Layout/LineLength + 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 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 bfa23c51fe4..9b8f575f5ae 100644 --- a/app/models/serializers/work_queue/appeal_serializer.rb +++ b/app/models/serializers/work_queue/appeal_serializer.rb @@ -28,6 +28,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| { @@ -38,7 +42,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: issue.mst_status, + pact_status: issue.pact_status, + mst_justification: issue&.mst_status_update_reason_notes, + pact_justification: issue&.pact_status_update_reason_notes } end end @@ -59,7 +67,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: issue.mst_status, + pact_status: issue.pact_status } 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 8dee592a766..0dd71a7ce38 100644 --- a/app/models/serializers/work_queue/legacy_appeal_serializer.rb +++ b/app/models/serializers/work_queue/legacy_appeal_serializer.rb @@ -65,6 +65,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 @@ -90,6 +94,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..a01595112f2 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 + attribute :pact_status 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/tasks/establishment_task.rb b/app/models/tasks/establishment_task.rb new file mode 100644 index 00000000000..d576f9fc603 --- /dev/null +++ b/app/models/tasks/establishment_task.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +class EstablishmentTask < Task + validates :parent, presence: true + + def label + "Establishment Task" + end + + 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 + + def format_special_issues_text(mst_status, pact_status) + # format the special issues comment to display the change in the special issues status(es) + s = "Special issues:" + + return s + " None" if !mst_status && !pact_status + return s + " MST, PACT" if mst_status && pact_status + return s + " MST" if mst_status + return s + " PACT" if pact_status + end + + 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..7a9aa53ba65 --- /dev/null +++ b/app/models/tasks/issues_update_task.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class IssuesUpdateTask < Task + validates :parent, presence: true + + def label + "Issues Update Task" + end + + def format_instructions(change_type, issue_category, benefit_type, original_mst, original_pact, edit_mst = nil, edit_pact = nil, + _mst_edit_reason = nil, _pact_edit_reason = nil) + # format the instructions by loading an array and adding it to the instructions + edit_issue_format = [] + # add the change type + edit_issue_format << change_type + edit_issue_format << benefit_type + edit_issue_format << issue_category + original_comment = format_special_issues_text(original_mst, original_pact).to_s + edit_issue_format << original_comment + + # format edit if edit values are given + unless edit_mst.nil? || edit_pact.nil? + updated_comment = format_special_issues_text(edit_mst, 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 + + def format_special_issues_text(mst_status, pact_status) + # format the special issues comment to display the change in the special issues status(es) + s = "Special Issues:" + + return s + " None" if !mst_status && !pact_status + return s + " MST, PACT" if mst_status && pact_status + return s + " MST" if mst_status + return s + " PACT" if pact_status + end +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..ad3ada10d61 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,12 @@ def additional_available_actions(_user) def self.label COPY::JUDGE_DISPATCH_RETURN_TASK_LABEL end + + 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 65076b555b2..aa36efb6e0e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -151,6 +151,11 @@ 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? + 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 1feaf5144e7..1a261a286d5 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 8701aa36a68..efa50b882db 100644 --- a/app/repositories/task_action_repository.rb +++ b/app/repositories/task_action_repository.rb @@ -877,6 +877,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..24d0e361765 --- /dev/null +++ b/app/serializers/intake/legacy_appeal_serializer.rb @@ -0,0 +1,103 @@ +# 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 do |object| + object.veteran_full_name + end + 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 af300de4a25..8f09f2d3087 100644 --- a/app/serializers/intake/request_issue_serializer.rb +++ b/app/serializers/intake/request_issue_serializer.rb @@ -38,4 +38,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..ebc37c01c1a 100644 --- a/app/services/external_api/bgs_service.rb +++ b/app/services/external_api/bgs_service.rb @@ -451,6 +451,24 @@ def find_contentions_by_claim_id(claim_id) end end + def find_contentions_by_participant_id(participant_id) + # commented out for testing + # 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.logger.info("fetching contention info for participant #{participant_id}") + Rails.cache.fetch("find_contentions_by_participant_id_#{participant_id}", expires_in: 24.hours) do + Rails.logger.info("calling BGS and caching contention info for participant #{participant_id}") + 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) + 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}", diff --git a/app/views/appeals/edit.html.erb b/app/views/appeals/edit.html.erb index f543d778340..eeecd2e8cd4 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), + mst_identification: FeatureToggle.enabled?(:mst_identification, user: current_user), + pact_identification: FeatureToggle.enabled?(:pact_identification, user: current_user), + legacy_mst_pact_identification: FeatureToggle.enabled?(:legacy_mst_pact_identification, user: current_user), + justification_reason: 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 db78283f635..b841733926f 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 e0ba7a1038a..a1eca8f69bb 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), + mst_identification: FeatureToggle.enabled?(:mst_identification, user: current_user), + pact_identification: FeatureToggle.enabled?(:pact_identification, user: current_user), + legacy_mst_pact_identification: FeatureToggle.enabled?(:legacy_mst_pact_identification, user: current_user), + justification_reason: FeatureToggle.enabled?(:justification_reason, user: current_user), cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, 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 c3649a4bf21..b1b7badeb02 100644 --- a/app/workflows/initial_tasks_factory.rb +++ b/app/workflows/initial_tasks_factory.rb @@ -22,6 +22,13 @@ def initialize(appeal) STATE_CODES_REQUIRING_TRANSLATION_TASK = %w[VI VQ PR PH RP PI].freeze def create_root_and_sub_tasks! + # if changes to mst or pact, create IssueUpdateTask + if @appeal.mst? || + @appeal.pact? && + FeatureToggle.enabled?(:mst_identification, user: RequestStore[:current_user]) || + 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? @@ -43,9 +50,9 @@ def create_vso_tracking_tasks end # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity def create_subtasks! distribution_task # ensure distribution_task exists - if @appeal.appellant_substitution? create_selected_tasks elsif @appeal.cavc? @@ -67,6 +74,7 @@ def create_subtasks! end end # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/PerceivedComplexity def distribution_task @distribution_task ||= @appeal.tasks.open.find_by(type: :DistributionTask) || @@ -199,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 9bd85705be1..32d38616fe3 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", @@ -938,6 +940,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", @@ -952,6 +961,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", "EDUCATION_PRE_DOCKET_INTAKE_SUCCESS_TITLE": "Appeal recorded and sent to Education Service for document assessment", 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 45a5fba8938..188e14c5c47 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 f566bcbb30a..eb6b5cf52f9 100644 --- a/client/app/components/RadioField.jsx +++ b/client/app/components/RadioField.jsx @@ -102,7 +102,6 @@ export const RadioField = (props) => {
      {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..2d1fdb4ab01 --- /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 + + + +