diff --git a/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb b/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb index eea9f98edc2..15e417c84db 100644 --- a/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb +++ b/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb @@ -9,6 +9,8 @@ # - A child task of the same name is created and assigned to the HearingAdmin organization ## class HearingPostponementRequestMailTask < HearingRequestMailTask + prepend HearingPostponed + include RunAsyncable class << self def label COPY::HEARING_POSTPONEMENT_REQUEST_MAIL_TASK_LABEL @@ -27,10 +29,13 @@ def allow_creation?(*) Constants.TASK_ACTIONS.CANCEL_TASK.to_h ].freeze + # Purpose: Determines the actions a user can take depending on their permissions and the state of the appeal + # Params: user - The current user object + # Return: The task actions array of objects def available_actions(user) return [] unless user.in_hearing_admin_team? - if active_schedule_hearing_task? || open_assign_hearing_disposition_task? + if active_schedule_hearing_task || hearing_scheduled_and_awaiting_disposition? TASK_ACTIONS else [ @@ -40,20 +45,243 @@ def available_actions(user) end end + # Purpose: Updates the current state of the appeal + # Params: params - The update params object + # user - The current user object + # Return: The current hpr task and newly created tasks + def update_from_params(params, user) + payload_values = params.delete(:business_payloads)&.dig(:values) || params + + # If the request is to mark HPR mail task complete + if payload_values[:granted]&.to_s.present? + # If request to postpone hearing is granted + if payload_values[:granted] + created_tasks = update_hearing_and_create_tasks(payload_values[:after_disposition_update]) + end + update_self_and_parent_mail_task(user: user, payload_values: payload_values) + + [self] + (created_tasks || []) + else + super(params, user) + end + end + + # Purpose: Only show HPR mail task assigned to "HearingAdmin" on the Case Timeline + # Params: None + # Return: boolean if task is assigned to MailTeam + def hide_from_case_timeline + assigned_to.is_a?(MailTeam) + end + + # Purpose: Determines if there is an open hearing + # Params: None + # Return: The hearing if one exists + def open_hearing + @open_hearing ||= open_assign_hearing_disposition_task&.hearing + end + + # Purpose: Gives the latest hearing task + # Params: None + # Return: The hearing task + def hearing_task + @hearing_task ||= open_hearing&.hearing_task || active_schedule_hearing_task.parent + end + private - def active_schedule_hearing_task? - appeal.tasks.where(type: ScheduleHearingTask.name).active.any? + # Purpose: Gives the latest active hearing task + # Params: None + # Return: The latest active hearing task + def active_schedule_hearing_task + appeal.tasks.of_type(ScheduleHearingTask.name).active.first + end + + # ChangeHearingDispositionTask is a subclass of AssignHearingDispositionTask + ASSIGN_HEARING_DISPOSITION_TASKS = [ + AssignHearingDispositionTask.name, + ChangeHearingDispositionTask.name + ].freeze + + # Purpose: Gives the latest active assign hearing disposition task + # Params: None + # Return: The latest active assign hearing disposition task + def open_assign_hearing_disposition_task + @open_assign_hearing_disposition_task ||= appeal.tasks.of_type(ASSIGN_HEARING_DISPOSITION_TASKS).open&.first + end + + # Purpose: Associated appeal has an upcoming hearing with an open status + # Params: None + # Return: Returns a boolean if the appeal has an upcoming hearing + def hearing_scheduled_and_awaiting_disposition? + return false unless open_hearing + + # Ensure associated hearing is not scheduled for the past + !open_hearing.scheduled_for_past? + end + + # Purpose: Sets the previous hearing's disposition to postponed + # Params: None + # Return: Returns a boolean for if the hearing has been updated + def postpone_previous_hearing + update_hearing(disposition: Constants.HEARING_DISPOSITION_TYPES.postponed) + end + + # Purpose: Wrapper for updating hearing and creating new hearing tasks + # Params: Params object for additional tasks or updates after updating the hearing + # Return: Returns the newly created tasks + def update_hearing_and_create_tasks(after_disposition_update) + multi_transaction do + # If hearing exists, postpone previous hearing and handle conference links + + if open_hearing + postpone_previous_hearing + clean_up_virtual_hearing + end + # Schedule hearing or create new ScheduleHearingTask depending on after disposition action + reschedule_or_schedule_later(after_disposition_update) + end + end + + # Purpose: Sets the previous hearing's disposition + # Params: None + # Return: Returns a boolean for if the hearing has been updated + def update_hearing(hearing_hash) + if open_hearing.is_a?(LegacyHearing) + open_hearing.update_caseflow_and_vacols(hearing_hash) + else + open_hearing.update(hearing_hash) + end + end + + # Purpose: Deletes the old scheduled virtual hearings + # Params: None + # Return: Returns nil + def clean_up_virtual_hearing + if open_hearing.virtual? + perform_later_or_now(VirtualHearings::DeleteConferencesJob) + end + end + + # Purpose: Either reschedule or send to schedule veteran list + # Params: None + # Return: Returns newly created tasks + def reschedule_or_schedule_later(after_disposition_update) + case after_disposition_update[:action] + when "reschedule" + new_hearing_attrs = after_disposition_update[:new_hearing_attrs] + reschedule( + hearing_day_id: new_hearing_attrs[:hearing_day_id], + scheduled_time_string: new_hearing_attrs[:scheduled_time_string], + hearing_location: new_hearing_attrs[:hearing_location], + virtual_hearing_attributes: new_hearing_attrs[:virtual_hearing_attributes], + notes: new_hearing_attrs[:notes], + email_recipients_attributes: new_hearing_attrs[:email_recipients] + ) + when "schedule_later" + schedule_later + else + fail ArgumentError, "unknown disposition action" + end end - def open_assign_hearing_disposition_task? - # ChangeHearingDispositionTask is a subclass of AssignHearingDispositionTask - disposition_task_names = [AssignHearingDispositionTask.name, ChangeHearingDispositionTask.name] - open_task = appeal.tasks.where(type: disposition_task_names).open.first + # rubocop:disable Metrics/ParameterLists + # Purpose: Reschedules the hearings + # Params: hearing_day_id - The ID of the hearing day that its going to be scheduled + # scheduled_time_string - The string for the scheduled time + # hearing_location - The hearing location string + # virtual_hearing_attributes - object for virtual hearing attributes + # notes - additional notes for the hearing string + # email_recipients_attributes - the object for the email recipients + # Return: Returns new hearing and assign disposition task + def reschedule( + hearing_day_id:, + scheduled_time_string:, + hearing_location: nil, + virtual_hearing_attributes: nil, + notes: nil, + email_recipients_attributes: nil + ) + multi_transaction do + new_hearing_task = hearing_task.cancel_and_recreate + + new_hearing = HearingRepository.slot_new_hearing(hearing_day_id: hearing_day_id, + appeal: appeal, + hearing_location_attrs: hearing_location&.to_hash, + scheduled_time_string: scheduled_time_string, + notes: notes) + if virtual_hearing_attributes.present? + @alerts = VirtualHearings::ConvertToVirtualHearingService + .convert_hearing_to_virtual(new_hearing, virtual_hearing_attributes) + elsif email_recipients_attributes.present? + create_or_update_email_recipients(new_hearing, email_recipients_attributes) + end + + disposition_task = AssignHearingDispositionTask + .create_assign_hearing_disposition_task!(appeal, new_hearing_task, new_hearing) + + AppellantNotification.notify_appellant(appeal, "Hearing scheduled") + + [new_hearing_task, disposition_task] + end + end + # rubocop:enable Metrics/ParameterLists + + # Purpose: Sends the appeal back to the scheduling list + # Params: None + # Return: Returns the new hearing task and schedule task + def schedule_later + new_hearing_task = hearing_task.cancel_and_recreate + schedule_task = ScheduleHearingTask.create!(appeal: appeal, parent: new_hearing_task) + + [new_hearing_task, schedule_task].compact + end + + # Purpose: Completes the Mail task assigned to the MailTeam and the one for HearingAdmin + # Params: user - The current user object + # payload_values - The attributes needed for the update + # Return: Boolean for if the tasks have been updated + def update_self_and_parent_mail_task(user:, payload_values:) + # Append instructions/context provided by HearingAdmin to original details from MailTeam + updated_instructions = format_instructions_on_completion( + admin_context: payload_values[:instructions], + ruling: payload_values[:granted] ? "GRANTED" : "DENIED", + date_of_ruling: payload_values[:date_of_ruling] + ) + + # Complete HPR mail task assigned to HearingAdmin + update!( + completed_by: user, + status: Constants.TASK_STATUSES.completed, + instructions: updated_instructions + ) + # Complete parent HPR mail task assigned to MailTeam + update_parent_status + end + + # Purpose: Appends instructions on to the instructions provided in the mail task + # Params: admin_context - String for instructions + # ruling - string for granted or denied + # date_of_ruling - string for the date of ruling + # Return: instructions string + def format_instructions_on_completion(admin_context:, ruling:, date_of_ruling:) + formatted_date = date_of_ruling.to_date&.strftime("%m/%d/%Y") + + markdown_to_append = <<~EOS + + *** + + ###### Marked as complete: + + **DECISION** + Motion to postpone #{ruling} + + **DATE OF RULING** + #{formatted_date} - return false unless open_task&.hearing + **DETAILS** + #{admin_context} + EOS - # Ensure hearing associated with AssignHearingDispositionTask is not scheduled in the past - !open_task.hearing.scheduled_for_past? + [instructions[0] + markdown_to_append] end end diff --git a/app/models/tasks/hearing_mail_tasks/hearing_request_mail_task.rb b/app/models/tasks/hearing_mail_tasks/hearing_request_mail_task.rb index ed409c91747..40f0ee7503a 100644 --- a/app/models/tasks/hearing_mail_tasks/hearing_request_mail_task.rb +++ b/app/models/tasks/hearing_mail_tasks/hearing_request_mail_task.rb @@ -7,10 +7,17 @@ # HearingRequestMailTask is itself not an assignable task type ## class HearingRequestMailTask < MailTask + include RunAsyncable validates :parent, presence: true, on: :create before_validation :verify_request_type_designated + class HearingAssociationMissing < StandardError + def initialize + super(format(COPY::HEARING_TASK_ASSOCIATION_MISSING_MESSAGE, hearing_task_id)) + end + end + class << self def allow_creation?(*) false @@ -26,10 +33,6 @@ def available_actions(_user) [] end - def update_from_params(params, current_user) - super(params, current_user) - end - private # Ensure create is called on a descendant mail task and not directly on the HearingRequestMailTask class diff --git a/client/app/components/RadioField.jsx b/client/app/components/RadioField.jsx index 203f83e9342..5786e43f906 100644 --- a/client/app/components/RadioField.jsx +++ b/client/app/components/RadioField.jsx @@ -42,7 +42,8 @@ export const RadioField = (props) => { strongLabel, hideLabel, styling, - vertical + vertical, + optionsStyling } = props; const isVertical = useMemo(() => props.vertical || props.options.length > 2, [ @@ -99,7 +100,7 @@ export const RadioField = (props) => { {errorMessage} )} -
+
{options.map((option, i) => { const optionDisabled = isDisabled(option); @@ -213,7 +214,8 @@ RadioField.propTypes = { errorMessage: PropTypes.string, strongLabel: PropTypes.bool, hideLabel: PropTypes.bool, - styling: PropTypes.object + styling: PropTypes.object, + optionsStyling: PropTypes.object }; export default RadioField; diff --git a/client/app/hearings/components/ScheduleVeteran.jsx b/client/app/hearings/components/ScheduleVeteran.jsx index 66512f47510..ab3365a5ec7 100644 --- a/client/app/hearings/components/ScheduleVeteran.jsx +++ b/client/app/hearings/components/ScheduleVeteran.jsx @@ -259,6 +259,11 @@ export const ScheduleVeteran = ({ }, ...(prevHearingDisposition === HEARING_DISPOSITION_TYPES.scheduled_in_error && { hearing_notes: scheduledHearing?.notes + }), + ...('rulingDate' in scheduledHearing && { + date_of_ruling: scheduledHearing?.rulingDate.value, + instructions: scheduledHearing?.instructions, + granted: scheduledHearing?.granted }) } } diff --git a/client/app/queue/CreateMailTaskDialog.jsx b/client/app/queue/CreateMailTaskDialog.jsx index b67ae663995..b1a8618dd9e 100644 --- a/client/app/queue/CreateMailTaskDialog.jsx +++ b/client/app/queue/CreateMailTaskDialog.jsx @@ -53,7 +53,7 @@ export class CreateMailTaskDialog extends React.Component { prependUrlToInstructions = () => { if (this.isHearingRequestMailTask()) { - return (`**LINK TO DOCUMENT:** \n ${this.state.eFolderUrl} \n **DETAILS:** \n ${this.state.instructions}`); + return (`**LINK TO DOCUMENT:** \n ${this.state.eFolderUrl} \n\n **DETAILS:** \n ${this.state.instructions}`); } return this.state.instructions; diff --git a/client/app/queue/components/hearingMailRequestModals/CompleteHearingPostponementRequestModal.jsx b/client/app/queue/components/hearingMailRequestModals/CompleteHearingPostponementRequestModal.jsx index d741e2d4b45..dfff3ed306d 100644 --- a/client/app/queue/components/hearingMailRequestModals/CompleteHearingPostponementRequestModal.jsx +++ b/client/app/queue/components/hearingMailRequestModals/CompleteHearingPostponementRequestModal.jsx @@ -1,31 +1,40 @@ import React, { useReducer } from 'react'; import PropTypes from 'prop-types'; - +import { withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { taskById, appealWithDetailSelector } from '../../selectors'; +import { requestPatch, showErrorMessage } from '../../uiReducer/uiActions'; +import { onReceiveAmaTasks } from '../../QueueActions'; import COPY from '../../../../COPY'; +import TASK_STATUSES from '../../../../constants/TASK_STATUSES'; +import HEARING_DISPOSITION_TYPES from '../../../../constants/HEARING_DISPOSITION_TYPES'; import QueueFlowModal from '../QueueFlowModal'; import RadioField from '../../../components/RadioField'; import Alert from '../../../components/Alert'; import DateSelector from '../../../components/DateSelector'; import TextareaField from '../../../components/TextareaField'; - import { marginTop, marginBottom } from '../../constants'; - -const RULING_OPTIONS = [ - { displayText: 'Granted', value: true }, - { displayText: 'Denied', value: false } -]; +import { setScheduledHearing } from 'common/actions'; const ACTIONS = { RESCHEDULE: 'reschedule', SCHEDULE_LATER: 'schedule_later' }; -const POSTPONEMENT_ACTIONS = [ +const RULING_OPTIONS = [ + { displayText: 'Granted', value: true }, + { displayText: 'Denied', value: false } +]; + +const POSTPONEMENT_OPTIONS = [ { displayText: 'Reschedule immediately', value: ACTIONS.RESCHEDULE }, { displayText: 'Send to Schedule Veteran list', value: ACTIONS.SCHEDULE_LATER } ]; const CompleteHearingPostponementRequestModal = (props) => { + const { appealId, appeal, taskId, task } = props; + const formReducer = (state, action) => { switch (action.type) { case 'granted': @@ -54,6 +63,11 @@ const CompleteHearingPostponementRequestModal = (props) => { ...state, scheduledOption: action.payload }; + case 'isPosting': + return { + ...state, + isPosting: action.payload + }; default: throw new Error('Unknown action type'); } @@ -65,7 +79,8 @@ const CompleteHearingPostponementRequestModal = (props) => { granted: null, rulingDate: { value: '', valid: false }, instructions: '', - scheduledOption: null + scheduledOption: null, + isPosting: false } ); @@ -79,7 +94,84 @@ const CompleteHearingPostponementRequestModal = (props) => { return granted !== null && rulingDate.valid && instructions !== ''; }; - const submit = () => console.log(props); + const getPayload = () => { + const { granted, rulingDate, instructions } = state; + + return { + data: { + task: { + status: TASK_STATUSES.completed, + business_payloads: { + values: { + // If request is denied, do not assign new disposition to hearing + disposition: granted ? HEARING_DISPOSITION_TYPES.postponed : null, + after_disposition_update: granted ? { action: ACTIONS.SCHEDULE_LATER } : null, + date_of_ruling: rulingDate.value, + instructions, + granted + }, + }, + }, + }, + }; + }; + + const getSuccessMsg = () => { + const { granted } = state; + + const message = granted ? + `${appeal.veteranFullName} was successfully added back to the schedule veteran list.` : + 'You have successfully marked hearing postponement request task as complete.'; + + return { + title: message + }; + }; + + const submit = () => { + const { isPosting, granted, scheduledOption } = state; + + if (granted && scheduledOption === ACTIONS.RESCHEDULE) { + props.setScheduledHearing({ + action: ACTIONS.RESCHEDULE, + taskId, + disposition: HEARING_DISPOSITION_TYPES.postponed, + ...state + }); + + props.history.push( + `/queue/appeals/${appealId}/tasks/${taskId}/schedule_veteran` + ); + + return Promise.reject(); + } + + if (isPosting) { + return; + } + + const payload = getPayload(); + + dispatch({ type: 'isPosting', payload: true }); + + return props. + requestPatch(`/tasks/${task.taskId}`, payload, getSuccessMsg()). + then( + (resp) => { + dispatch({ type: 'isPosting', payload: false }); + props.onReceiveAmaTasks(resp.body.tasks.data); + }, + () => { + dispatch({ type: 'isPosting', payload: false }); + + props.showErrorMessage({ + title: 'Unable to postpone hearing.', + detail: + 'Please retry submitting again and contact support if errors persist.', + }); + } + ); + }; return ( { submitDisabled={!validateForm()} validateForm={validateForm} submit={submit} - pathAfterSubmit="/organizations/hearing-admin" + pathAfterSubmit={`/queue/appeals/${appealId}`} > <> { value={state.granted} options={RULING_OPTIONS} styling={marginBottom(1)} + optionsStyling={{ marginTop: 0 }} /> {state.granted && { inputRef={props.register} onChange={(value) => dispatch({ type: 'scheduledOption', payload: value })} value={state.scheduledOption} - options={POSTPONEMENT_ACTIONS} + options={POSTPONEMENT_OPTIONS} vertical styling={marginBottom(1.5)} />} @@ -145,8 +238,43 @@ const CompleteHearingPostponementRequestModal = (props) => { ); }; +const mapStateToProps = (state, ownProps) => ({ + task: taskById(state, { taskId: ownProps.taskId }), + appeal: appealWithDetailSelector(state, ownProps) +}); + +const mapDispatchToProps = (dispatch) => + bindActionCreators( + { + setScheduledHearing, + requestPatch, + onReceiveAmaTasks, + showErrorMessage + }, + dispatch + ); + CompleteHearingPostponementRequestModal.propTypes = { - register: PropTypes.func + register: PropTypes.func, + appealId: PropTypes.string.isRequired, + taskId: PropTypes.string.isRequired, + history: PropTypes.object, + setScheduledHearing: PropTypes.func, + appeal: PropTypes.shape({ + externalId: PropTypes.string, + veteranFullName: PropTypes.string + }), + task: PropTypes.shape({ + taskId: PropTypes.string, + }), + requestPatch: PropTypes.func, + onReceiveAmaTasks: PropTypes.func, + showErrorMessage: PropTypes.func, }; -export default CompleteHearingPostponementRequestModal; +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(CompleteHearingPostponementRequestModal) +); diff --git a/client/app/styles/queue/_timeline.scss b/client/app/styles/queue/_timeline.scss index 34da8acebf7..13fdf45cac7 100644 --- a/client/app/styles/queue/_timeline.scss +++ b/client/app/styles/queue/_timeline.scss @@ -91,12 +91,26 @@ margin: 1em 0 0; font-size: 15px; font-weight: 900; + margin-bottom: 2.4rem; +} + +.task-instructions > h6 { + margin: 1em 0 0; + font-size: 17px; + font-weight: 900; + margin-bottom: 2.4rem; + text-transform: none; } .task-instructions > p { margin-top: 0; + word-wrap: break-word; strong { font-size: 15px; } } + +.task-instructions > hr { + margin-bottom: 2.4rem; +} diff --git a/client/jest.config.js b/client/jest.config.js index 0088e38a94a..2592d7e7d0e 100644 --- a/client/jest.config.js +++ b/client/jest.config.js @@ -5,7 +5,8 @@ module.exports = { '^constants/(.*)$': '/constants/$1', '^test/(.*)$': '/test/$1', '^COPY': '/COPY', - '\\.(css|less|scss|sss|styl)$': '/node_modules/jest-css-modules' + '\\.(css|less|scss|sss|styl)$': '/node_modules/jest-css-modules', + '^common/(.*)$': '/app/components/common/$1' }, // Runs before the environment is configured globalSetup: './test/global-setup.js', diff --git a/client/webpack.config.js b/client/webpack.config.js index ab5ea042c0f..d0cd3058cf5 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -50,6 +50,7 @@ const config = { components: path.resolve('app/2.0/components'), utils: path.resolve('app/2.0/utils'), styles: path.resolve('app/2.0/styles'), + common: path.resolve('app/components/common'), test: path.resolve('test'), }, }, diff --git a/db/schema.rb b/db/schema.rb index 71f378159e5..8d245f1c78e 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 + t.datetime "created_at", null: false, comment: "Date and Time the record was inserted into the table" 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" + t.datetime "updated_at", comment: "Date and time the record was last updated" 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" @@ -1293,7 +1293,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.string "email_notification_content", comment: "Full Email Text Content of Notification" + t.text "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" @@ -1304,8 +1304,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.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.text "sms_notification_content", comment: "Full SMS Text Content of Notification" + t.string "sms_notification_external_id" 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" diff --git a/spec/factories/task.rb b/spec/factories/task.rb index 20157d3086c..7eefe5051ca 100644 --- a/spec/factories/task.rb +++ b/spec/factories/task.rb @@ -95,8 +95,10 @@ def self.find_first_task_or_create(appeal, task_type, **kwargs) distro_task = task.parent task.update!(parent: root_task) ScheduleHearingTask.create!(appeal: appeal, parent: distro_task, assigned_to: Bva.singleton) - HearingPostponementRequestMailTask.create!(appeal: appeal, parent: task, - assigned_to: HearingAdmin.singleton) + HearingPostponementRequestMailTask.create!(appeal: appeal, + parent: task, + assigned_to: HearingAdmin.singleton, + instructions: task.instructions) end end @@ -109,12 +111,36 @@ def self.find_first_task_or_create(appeal, task_type, **kwargs) schedule_hearing_task = ScheduleHearingTask.create!(appeal: appeal, parent: distro_task, assigned_to: Bva.singleton) schedule_hearing_task.update(status: "completed", closed_at: Time.zone.now) - hearing = create(:hearing, disposition: nil, judge: nil, appeal: appeal) + scheduled_time = Time.zone.today + 1.month + if appeal.is_a?(Appeal) + hearing = create(:hearing, + disposition: nil, + judge: nil, + appeal: appeal, + scheduled_time: scheduled_time) + else + case_hearing = create(:case_hearing, folder_nr: appeal.vacols_id, hearing_date: scheduled_time) + hearing_day = create(:hearing_day, + request_type: HearingDay::REQUEST_TYPES[:video], + regional_office: "RO19", + scheduled_for: scheduled_time) + hearing = create(:legacy_hearing, + disposition: nil, + case_hearing: case_hearing, + appeal_id: appeal.id, + appeal: appeal, + hearing_day: hearing_day) + appeal.update!(hearings: [hearing]) + end + HearingTaskAssociation.create!(hearing: hearing, hearing_task: schedule_hearing_task.parent) distro_task.update!(status: "on_hold") - AssignHearingDispositionTask.create!(appeal: appeal, parent: schedule_hearing_task.parent, + AssignHearingDispositionTask.create!(appeal: appeal, + parent: schedule_hearing_task.parent, assigned_to: Bva.singleton) - HearingTaskAssociation.create!(hearing: hearing, hearing_task: schedule_hearing_task.parent) - HearingPostponementRequestMailTask.create!(appeal: appeal, parent: task, assigned_to: HearingAdmin.singleton) + HearingPostponementRequestMailTask.create!(appeal: appeal, + parent: task, + assigned_to: HearingAdmin.singleton, + instructions: task.instructions) end end @@ -662,6 +688,9 @@ def self.find_first_task_or_create(appeal, task_type, **kwargs) factory :hearing_postponement_request_mail_task, class: HearingPostponementRequestMailTask do parent { create(:distribution_task, appeal: appeal) } assigned_to { MailTeam.singleton } + instructions do + ["**LINK TO DOCUMENT:** \n https://www.caseflowreader.com/doc \n\n **DETAILS:** \n Context on task creation"] + end end end end diff --git a/spec/feature/queue/mail_task_spec.rb b/spec/feature/queue/mail_task_spec.rb index 0c6b6e6a92e..8c8e716f26f 100644 --- a/spec/feature/queue/mail_task_spec.rb +++ b/spec/feature/queue/mail_task_spec.rb @@ -1,10 +1,17 @@ # frozen_string_literal: true RSpec.feature "MailTasks", :postgres do + include ActiveJob::TestHelper + + def clean_up_after_threads + DatabaseCleaner.clean_with(:truncation, except: %w[notification_events vftypes issref]) + end + let(:user) { create(:user) } before do User.authenticate!(user: user) + Seeds::NotificationEvents.new.seed! end describe "Assigning a mail team task to a team member" do @@ -146,22 +153,99 @@ before do HearingAdmin.singleton.add_user(User.current_user) end - let(:hpr_task) { create(:hearing_postponement_request_mail_task, :with_unscheduled_hearing) } + let(:appeal) { hpr_task.appeal } + let(:hpr_task) do + create(:hearing_postponement_request_mail_task, + :with_unscheduled_hearing, assigned_by_id: User.system_user.id) + end + let(:scheduled_hpr_task) do + create(:hearing_postponement_request_mail_task, + :with_scheduled_hearing, assigned_by_id: User.system_user.id) + end + let(:scheduled_appeal) { scheduled_hpr_task.appeal } + let(:legacy_appeal) do + create(:legacy_appeal, :with_veteran, vacols_case: create(:case)) + end + let!(:legacy_hpr_task) do + create(:hearing_postponement_request_mail_task, + :with_unscheduled_hearing, + assigned_by_id: User.system_user.id, appeal: legacy_appeal) + end + let(:scheduled_legacy_appeal) do + create(:legacy_appeal, :with_veteran, vacols_case: create(:case)) + end + let!(:scheduled_legacy_hpr_task) do + create(:hearing_postponement_request_mail_task, + :with_scheduled_hearing, + assigned_by_id: User.system_user.id, appeal: scheduled_legacy_appeal) + end + let(:email) { "test@caseflow.com" } + + shared_examples_for "scheduling a hearing" do + before do + perform_enqueued_jobs do + FeatureToggle.enable!(:schedule_veteran_virtual_hearing) + page = appeal.is_a?(Appeal) ? "queue/appeals/#{appeal.uuid}" : "queue/appeals/#{appeal.vacols_id}" + visit(page) + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.COMPLETE_AND_POSTPONE.label, + match: :first) + end + find(".cf-form-radio-option", text: ruling).click + fill_in("rulingDateSelector", with: ruling_date) + find(:css, ".cf-form-radio-option label", text: "Reschedule immediately").click + fill_in("instructionsField", with: instructions) + click_button("Mark as complete") + + within(:css, ".dropdown-hearingType") { click_dropdown(text: "Virtual") } + within(:css, ".dropdown-regionalOffice") { click_dropdown(text: "Denver, CO") } + within(:css, ".dropdown-hearingDate") { click_dropdown(index: 0) } + find("label", text: "12:30 PM Mountain Time (US & Canada) / 2:30 PM Eastern Time (US & Canada)").click + if has_css?("[id='Appellant Email (for these notifications only)']") + fill_in("Appellant Email (for these notifications only)", with: email) + else + fill_in("Veteran Email (for these notifications only)", with: email) + end + click_button("Schedule") + end + end + + it "gets scheduled" do + expect(page).to have_content("You have successfully") + end + + it "sends proper notifications" do + scheduled_payload = AppellantNotification.create_payload(appeal, "Hearing scheduled").to_json + if appeal.hearings.any? + postpone_payload = AppellantNotification.create_payload(appeal, "Postponement of hearing") + .to_json + expect(SendNotificationJob).to receive(:perform_later).with(postpone_payload) + end + expect(SendNotificationJob).to receive(:perform_later).with(scheduled_payload) + end + end context "changing task type" do it "submit button starts out disabled" do - visit("queue/appeals/#{hpr_task.appeal.uuid}") - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, text: COPY::CHANGE_TASK_TYPE_SUBHEAD) + visit("queue/appeals/#{appeal.uuid}") + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: COPY::CHANGE_TASK_TYPE_SUBHEAD, + match: :first) + end modal = find(".cf-modal-body") expect(modal).to have_button("Change task type", disabled: true) end it "current tasks should have new task" do - appeal = hpr_task.appeal visit("queue/appeals/#{appeal.uuid}") - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, text: COPY::CHANGE_TASK_TYPE_SUBHEAD) - find(".cf-select__control", text: "Select an action type").click - find(".cf-select__option", text: "Change of address").click + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: COPY::CHANGE_TASK_TYPE_SUBHEAD, + match: :first) + end + click_dropdown(prompt: "Select an action type", text: "Change of address") fill_in("Provide instructions and context for this change:", with: "instructions") click_button("Change task type") new_task = appeal.tasks.last @@ -171,10 +255,13 @@ end it "case timeline should cancel old task" do - visit("queue/appeals/#{hpr_task.appeal.uuid}") - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, text: COPY::CHANGE_TASK_TYPE_SUBHEAD) - find(".cf-select__control", text: "Select an action type").click - find(".cf-select__option", text: "Change of address").click + visit("queue/appeals/#{appeal.uuid}") + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: COPY::CHANGE_TASK_TYPE_SUBHEAD, + match: :first) + end + click_dropdown(prompt: "Select an action type", text: "Change of address") fill_in("Provide instructions and context for this change:", with: "instructions") click_button("Change task type") first_task_item = find("#case-timeline-table tr:nth-child(2)") @@ -186,18 +273,25 @@ context "assigning to new team" do it "submit button starts out disabled" do - visit("queue/appeals/#{hpr_task.appeal.uuid}") - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, text: Constants.TASK_ACTIONS.ASSIGN_TO_TEAM.label) + visit("queue/appeals/#{appeal.uuid}") + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.ASSIGN_TO_TEAM.label, + match: :first) + end modal = find(".cf-modal-body") expect(modal).to have_button("Submit", disabled: true) end it "assigns to new team" do - appeal = hpr_task.appeal page = "queue/appeals/#{appeal.uuid}" visit(page) - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, text: Constants.TASK_ACTIONS.ASSIGN_TO_TEAM.label) - find(".cf-select__control", text: "Select a team").click + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.ASSIGN_TO_TEAM.label, + match: :first) + end + find(".cf-select__control", text: "Select a team", match: :first).click find(".cf-select__option", text: "BVA Intake").click fill_in("taskInstructions", with: "instructions") click_button("Submit") @@ -211,9 +305,12 @@ context "assigning to person" do it "submit button starts out disabled" do - visit("queue/appeals/#{hpr_task.appeal.uuid}") - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, - text: Constants.TASK_ACTIONS.ASSIGN_TO_PERSON.label) + visit("queue/appeals/#{appeal.uuid}") + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.ASSIGN_TO_PERSON.label, + match: :first) + end modal = find(".cf-modal-body") expect(modal).to have_button("Submit", disabled: true) end @@ -221,11 +318,13 @@ it "assigns to person" do new_user = User.create!(css_id: "NEW_USER", full_name: "John Smith", station_id: "101") HearingAdmin.singleton.add_user(new_user) - appeal = hpr_task.appeal page = "queue/appeals/#{appeal.uuid}" visit(page) - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, - text: Constants.TASK_ACTIONS.ASSIGN_TO_PERSON.label) + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.ASSIGN_TO_PERSON.label, + match: :first) + end find(".cf-select__control", text: User.current_user.full_name).click find(".cf-select__option", text: new_user.full_name).click fill_in("taskInstructions", with: "instructions") @@ -241,15 +340,23 @@ context "cancelling task" do it "submit button starts out disabled" do visit("queue/appeals/#{hpr_task.appeal.uuid}") - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, text: Constants.TASK_ACTIONS.CANCEL_TASK.label) + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.CANCEL_TASK.label, + match: :first) + end modal = find(".cf-modal-body") expect(modal).to have_button("Submit", disabled: true) end it "should remove HearingPostponementRequestTask from current tasks" do - page = "queue/appeals/#{hpr_task.appeal.uuid}" + page = "queue/appeals/#{appeal.uuid}" visit(page) - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, text: Constants.TASK_ACTIONS.CANCEL_TASK.label) + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.CANCEL_TASK.label, + match: :first) + end fill_in("taskInstructions", with: "instructions") click_button("Submit") visit(page) @@ -258,9 +365,13 @@ end it "case timeline should cancel task" do - page = "queue/appeals/#{hpr_task.appeal.uuid}" + page = "queue/appeals/#{appeal.uuid}" visit(page) - click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, text: Constants.TASK_ACTIONS.CANCEL_TASK.label) + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.CANCEL_TASK.label, + match: :first) + end fill_in("taskInstructions", with: "instructions") click_button("Submit") visit(page) @@ -270,5 +381,168 @@ expect(first_task_item).to have_content("CANCELLED BY\n#{User.current_user.css_id}") end end + + context "mark as complete" do + let(:ruling_date) { "08/15/2023" } + let(:instructions) { "instructions" } + + shared_examples "whether granted or denied" do + it "completes HearingPostponementRequestMailTask on Case Timeline" do + mail_task = find("#case-timeline-table tr:nth-child(2)") + expect(mail_task).to have_content("COMPLETED ON\n#{hpr_task.updated_at.strftime('%m/%d/%Y')}") + expect(mail_task).to have_content("HearingPostponementRequestMailTask completed") + end + + it "updates instructions of HearingPostponementRequestMailTask on Case Timeline" do + find(:css, "#case-timeline-table .cf-btn-link", text: "View task instructions", match: :first).click + instructions_div = find("div", class: "task-instructions") + expect(instructions_div).to have_content("Motion to postpone #{ruling.upcase}") + expect(instructions_div).to have_content("DATE OF RULING\n#{ruling_date}") + expect(instructions_div).to have_content("DETAILS\n#{instructions}") + end + end + + shared_examples "postponement granted" do + it "previous hearing disposition is postponed" do + visit "queue/appeals/#{appeal.uuid}" + within(:css, "#hearing-details") do + hearing = find(:css, ".cf-bare-list ul:nth-child(2)") + expect(hearing).to have_content("Disposition: Postponed") + end + end + end + + context "ruling is granted" do + let(:ruling) { "Granted" } + context "schedule immediately" do + let!(:virtual_hearing_day) do + create( + :hearing_day, + request_type: HearingDay::REQUEST_TYPES[:virtual], + scheduled_for: Time.zone.today + 160.days, + regional_office: "RO39" + ) + end + + before do + HearingsManagement.singleton.add_user(User.current_user) + User.current_user.update!(roles: ["Build HearSched"]) + appeal.update!(closest_regional_office: "RO39") + end + + context "AMA appeal" do + context "unscheduled hearing" do + include_examples "scheduling a hearing" + include_examples "whether granted or denied" + end + + context "scheduled hearing" do + let(:appeal) { scheduled_appeal } + include_examples "scheduling a hearing" + include_examples "whether granted or denied" + end + end + + context "Legacy appeal" do + let(:appeal) { legacy_appeal } + context "unscheduled hearing" do + include_examples "scheduling a hearing" + include_examples "whether granted or denied" + end + + context "scheduled hearing" do + let(:appeal) { scheduled_legacy_appeal } + include_examples "scheduling a hearing" + include_examples "whether granted or denied" + end + end + end + + context "send to schedule veteran list" do + before :each do + FeatureToggle.enable!(:schedule_veteran_virtual_hearing) + page = "queue/appeals/#{appeal.uuid}" + visit(page) + within("tr", text: "TASK", match: :first) do + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.COMPLETE_AND_POSTPONE.label, + match: :first) + end + find(".cf-form-radio-option", text: ruling).click + fill_in("rulingDateSelector", with: ruling_date) + find(:css, ".cf-form-radio-option label", text: "Send to Schedule Veteran list").click + fill_in("instructionsField", with: instructions) + click_button("Mark as complete") + end + + shared_examples "whether hearing is scheduled or unscheduled" do + it "creates new ScheduleHearing task under Task Actions" do + appeal + new_task = appeal.tasks.last + most_recent_task = find("tr", text: "TASK", match: :first) + expect(most_recent_task).to have_content("ASSIGNED ON\n#{new_task.assigned_at.strftime('%m/%d/%Y')}") + expect(most_recent_task).to have_content("TASK\nSchedule hearing") + end + + it "cancels Hearing task on Case Timeline" do + hearing_task = find("#case-timeline-table tr:nth-child(3)") + + expect(hearing_task).to have_content("CANCELLED ON\n#{hpr_task.updated_at.strftime('%m/%d/%Y')}") + expect(hearing_task).to have_content("HearingTask cancelled") + expect(hearing_task).to have_content("CANCELLED BY\n#{User.current_user.css_id}") + end + end + + context "appeal has unscheduled hearing" do + include_examples "whether granted or denied" + include_examples "whether hearing is scheduled or unscheduled" + + it "cancels ScheduleHearing task on Case Timeline" do + schedule_task = find("#case-timeline-table tr:nth-child(4)") + + expect(schedule_task).to have_content("CANCELLED ON\n#{hpr_task.updated_at.strftime('%m/%d/%Y')}") + expect(schedule_task).to have_content("ScheduleHearingTask cancelled") + expect(schedule_task).to have_content("CANCELLED BY\n#{User.current_user.css_id}") + end + end + + context "appeal has scheduled hearing" do + let(:hpr_task) do + create(:hearing_postponement_request_mail_task, + :with_scheduled_hearing, + assigned_by_id: User.system_user.id) + end + + include_examples "whether granted or denied" + include_examples "whether hearing is scheduled or unscheduled" + include_examples "postponement granted" + + it "cancels AssignHearingDisposition task on Case Timeline" do + disposition_task = find("#case-timeline-table tr:nth-child(4)") + + expect(disposition_task).to have_content("CANCELLED ON\n#{hpr_task.updated_at.strftime('%m/%d/%Y')}") + expect(disposition_task).to have_content("AssignHearingDispositionTask cancelled") + expect(disposition_task).to have_content("CANCELLED BY\n#{User.current_user.css_id}") + end + end + end + end + + context "ruling is denied" do + let(:ruling) { "Denied" } + before do + FeatureToggle.enable!(:schedule_veteran_virtual_hearing) + page = "queue/appeals/#{appeal.uuid}" + visit(page) + click_dropdown(prompt: COPY::TASK_ACTION_DROPDOWN_BOX_LABEL, + text: Constants.TASK_ACTIONS.COMPLETE_AND_POSTPONE.label) + find(".cf-form-radio-option", text: ruling).click + fill_in("rulingDateSelector", with: ruling_date) + fill_in("instructionsField", with: instructions) + click_button("Mark as complete") + end + include_examples "whether granted or denied" + end + end end end diff --git a/spec/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task_spec.rb b/spec/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task_spec.rb index 638cf2c6c88..4a471c99fa5 100644 --- a/spec/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task_spec.rb +++ b/spec/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task_spec.rb @@ -87,10 +87,10 @@ context "when there is neither an active ScheduleHearingTask " \ "nor an open AssignHearingDispositionTask in the appeal's task tree" do let(:hpr) { create(:hearing_postponement_request_mail_task, :with_unscheduled_hearing) } - let(:schedul_hearing_task) { hpr.appeal.tasks.find_by(type: ScheduleHearingTask.name) } + let(:schedule_hearing_task) { hpr.appeal.tasks.find_by(type: ScheduleHearingTask.name) } before do - schedul_hearing_task.cancel_task_and_child_subtasks + schedule_hearing_task.cancel_task_and_child_subtasks end include_examples "returns appropriate reduced task actions"