diff --git a/app/models/tasks/cavc_task.rb b/app/models/tasks/cavc_task.rb new file mode 100644 index 00000000000..60444a55fa8 --- /dev/null +++ b/app/models/tasks/cavc_task.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +## +# This task is used to track all related CAVC subtasks for AMA Appeal Streams. +# If this task is still open, there is still more CAVC-specific work to be done of this appeal. +# This task should be a child of DistributionTask, and so it blocks distribution until all its children are closed. +# There are no actions available to any user for this task. + +class CavcTask < Task + validates :parent, presence: true, parentTask: { task_type: DistributionTask }, on: :create + + before_validation :set_assignee + + def self.label + "All CAVC-related tasks" + end + + def default_instructions + [COPY::CAVC_TASK_DEFAULT_INSTRUCTIONS] + end + + def available_actions(_user) + [] + end + + def verify_org_task_unique + true + end + + private + + def set_assignee + self.assigned_to = Bva.singleton + end + + def cascade_closure_from_child_task?(_child_task) + true + end +end diff --git a/app/models/validators/parent_task_validator.rb b/app/models/validators/parent_task_validator.rb new file mode 100644 index 00000000000..faf14ab2bde --- /dev/null +++ b/app/models/validators/parent_task_validator.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +## +# Validates the parent task +# +# Usage example: For the parent field, calls the built-in PresenceValidator and +# ParentTaskValidator with argument DistributionTask when the record is being created: +# validates :parent, presence: true, parentTask: { task_type: DistributionTask }, on: :create + +class ParentTaskValidator < ActiveModel::Validator + def validate(record) + record.errors.add(:parent, "parent should be a #{options[:task_type].name}") unless correct_parent_type?(record) + end + + private + + def correct_parent_type?(record) + return true if options[:task_type].nil? + + record.parent&.type == options[:task_type].name + end +end diff --git a/app/serializers/hearings/hearing_day_serializer.rb b/app/serializers/hearings/hearing_day_serializer.rb index 285a7d89842..974e6b90598 100644 --- a/app/serializers/hearings/hearing_day_serializer.rb +++ b/app/serializers/hearings/hearing_day_serializer.rb @@ -31,7 +31,7 @@ class HearingDaySerializer def self.get_readable_request_type(hearing_day, params) if params[:video_hearing_days_request_types].nil? - fail ArgumentError, "params must have video_hearing_days_request_tyeps" + fail ArgumentError, "params must have video_hearing_days_request_types" end # `video_hearing_days_request_types` should be constructed with diff --git a/client/COPY.json b/client/COPY.json index 68a787fa03e..e1ed64bfbe1 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -353,6 +353,8 @@ "HEARING_TASK_ASSOCIATION_MISSING_MESASAGE": "Hearing task (%s) is missing an associated hearing. This means that either the hearing was deleted in VACOLS or the hearing association has been deleted.", "HEARING_TASK_DEFAULT_INSTRUCTIONS": "This task will be auto-completed when all hearing-related tasks have been completed.", + "CAVC_TASK_DEFAULT_INSTRUCTIONS": "This task will be auto-completed when all CAVC-related tasks have been closed.", + "ASSIGN_HEARING_DISPOSITION_TASK_DEFAULT_INSTRUCTIONS": "Postpone or cancel a hearing prior to the hearing date. This task will be auto-completed after the hearing's scheduled date.", "CHANGE_HEARING_DISPOSITION_TASK_DEFAULT_INSTRUCTIONS": "Change hearing disposition (Held, Canceled, No Show, Postponed) if it was marked in error.", diff --git a/spec/factories/task.rb b/spec/factories/task.rb index 84181cc3e92..57412346452 100644 --- a/spec/factories/task.rb +++ b/spec/factories/task.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true FactoryBot.define do + module FactoryBotHelper + def self.find_first_task_or_create(appeal, task_type) + (appeal.tasks.open.where(type: task_type.name).first if appeal) || + FactoryBot.create(task_type.name.underscore.to_sym, appeal: appeal) + end + end + # By default, this task is created in a new Legacy appeal factory :task do assigned_at { rand(30..35).days.ago } @@ -290,6 +297,7 @@ end factory :distribution_task, class: DistributionTask do + parent { appeal.root_task || create(:root_task, appeal: appeal) } assigned_by { nil } assigned_to { Bva.singleton } @@ -347,6 +355,10 @@ factory :translation_task, class: TranslationTask do end + factory :cavc_task, class: CavcTask do + parent { FactoryBotHelper.find_first_task_or_create(appeal, DistributionTask) } + end + factory :hearing_task, class: HearingTask do assigned_to { Bva.singleton } parent { appeal.root_task || create(:root_task, appeal: appeal) } diff --git a/spec/models/tasks/cavc_task_spec.rb b/spec/models/tasks/cavc_task_spec.rb new file mode 100644 index 00000000000..8320d87e963 --- /dev/null +++ b/spec/models/tasks/cavc_task_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +describe CavcTask, :postgres do + describe ".create" do + subject { described_class.create(appeal: appeal, parent: parent_task) } + let(:appeal) { create(:appeal) } + let!(:parent_task) { create(:distribution_task, appeal: appeal) } + + context "parent is DistributionTask" do + it "creates task" do + new_task = subject + expect(new_task.valid?) + expect(new_task.errors.messages[:parent]).to be_empty + + expect(appeal.tasks).to include new_task + expect(parent_task.children).to include new_task + + expect(new_task.assigned_to).to eq Bva.singleton + expect(new_task.label).to eq "All CAVC-related tasks" + expect(new_task.default_instructions).to eq [COPY::CAVC_TASK_DEFAULT_INSTRUCTIONS] + end + end + + context "parent is not a DistributionTask" do + let(:parent_task) { create(:root_task) } + it "fails to create task" do + new_task = subject + expect(new_task.invalid?) + expect(new_task.errors.messages[:parent]).to include("parent should be a DistributionTask") + end + end + + context "parent is nil" do + let(:parent_task) { nil } + it "fails to create task" do + new_task = subject + expect(new_task.invalid?) + expect(new_task.errors.messages[:parent]).to include("can't be blank") + end + end + end + + describe "FactoryBot.create(:cavc_task) with different arguments" do + context "appeal is provided" do + let(:appeal) { create(:appeal) } + let!(:parent_task) { create(:distribution_task, appeal: appeal) } + let!(:cavc_task) { create(:cavc_task, appeal: appeal) } + it "finds existing distribution_task to use as parent" do + expect(Appeal.count).to eq 1 + expect(RootTask.count).to eq 1 + expect(DistributionTask.count).to eq 1 + expect(CavcTask.count).to eq 1 + end + end + context "parent task is provided" do + let(:parent_task) { create(:distribution_task) } + let!(:cavc_task) { create(:cavc_task, parent: parent_task) } + it "uses existing distribution_task" do + expect(Appeal.count).to eq 1 + expect(RootTask.count).to eq 1 + expect(DistributionTask.count).to eq 1 + expect(CavcTask.count).to eq 1 + end + end + context "nothing is provided" do + let!(:cavc_task) { create(:cavc_task) } + it "creates realistic task tree" do + expect(Appeal.count).to eq 1 + expect(RootTask.count).to eq 1 + expect(DistributionTask.count).to eq 1 + expect(CavcTask.count).to eq 1 + end + end + end + + describe "#available_actions" do + let(:user) { create(:user) } + let(:cavc_task) { create(:cavc_task) } + it "returns empty" do + expect(cavc_task.available_actions(user)).to be_empty + end + end + + context "closing child tasks" do + let(:user) { create(:user) } + let(:cavc_task) { create(:cavc_task) } + let!(:child_task) { create(:ama_task, parent: cavc_task) } + context "as complete" do + it "completes parent CavcTask" do + child_task.completed! + expect(cavc_task.closed?) + expect(cavc_task.status).to eq "completed" + end + end + context "as cancelled" do + it "cancels parent CavcTask" do + child_task.cancelled! + expect(cavc_task.closed?) + expect(cavc_task.status).to eq "cancelled" + end + end + context "has multiple children" do + let!(:child_task2) { create(:ama_task, parent: cavc_task) } + it "leaves parent CavcTask open when completing 1 child" do + child_task.completed! + expect(cavc_task.open?) + expect(cavc_task.status).to eq "on_hold" + end + it "leaves parent CavcTask open when cancelling 1 child" do + child_task.cancelled! + expect(cavc_task.open?) + expect(cavc_task.status).to eq "on_hold" + end + it "closes parent CavcTask when closing last open child" do + child_task.cancelled! + expect(cavc_task.open?) + expect(cavc_task.status).to eq "on_hold" + child_task2.completed! + expect(cavc_task.closed?) + expect(cavc_task.status).to eq "completed" + end + end + end +end