diff --git a/app/aggregates/deciding.rb b/app/aggregates/deciding.rb new file mode 100644 index 000000000..144bb8102 --- /dev/null +++ b/app/aggregates/deciding.rb @@ -0,0 +1,26 @@ +module Deciding + class DecisionNotFound < StandardError; end + class ApplicationNotAssignedToUser < StandardError; end + + class DraftCreated < RailsEventStore::Event; end + class InterestsOfJusticeSet < RailsEventStore::Event; end + class FundingDecisionSet < RailsEventStore::Event; end + + class << self + def stream_name(decision_id) + "Deciding$#{decision_id}" + end + end + + class Configuration + def call(event_store) + event_store.subscribe( + DecisionHandler, to: [Reviewing::AddDecision] + ) + end + end + + class DecisionHandler + def call(event); end + end +end diff --git a/app/aggregates/deciding/command.rb b/app/aggregates/deciding/command.rb new file mode 100644 index 000000000..198bfd8ce --- /dev/null +++ b/app/aggregates/deciding/command.rb @@ -0,0 +1,31 @@ +module Deciding + class Command < Dry::Struct + attribute :decision_id, Types::Uuid + + def with_decision(&block) + repository.with_aggregate( + Decision.new(decision_id), + stream_name, + &block + ) + end + + private + + def repository + @repository ||= AggregateRoot::Repository.new( + Rails.configuration.event_store + ) + end + + def stream_name + Deciding.stream_name(decision_id) + end + + class << self + def call(args) + new(args).call + end + end + end +end diff --git a/app/aggregates/deciding/commands/create_draft.rb b/app/aggregates/deciding/commands/create_draft.rb new file mode 100644 index 000000000..fb1a1afd2 --- /dev/null +++ b/app/aggregates/deciding/commands/create_draft.rb @@ -0,0 +1,12 @@ +module Deciding + class CreateDraft < Command + attribute :application_id, Types::Uuid + attribute :user_id, Types::Uuid + + def call + with_decision do |decision| + decision.create_draft(user_id:, application_id:) + end + end + end +end diff --git a/app/aggregates/deciding/commands/load_decision.rb b/app/aggregates/deciding/commands/load_decision.rb new file mode 100644 index 000000000..df336909e --- /dev/null +++ b/app/aggregates/deciding/commands/load_decision.rb @@ -0,0 +1,7 @@ +module Deciding + class LoadDecision < Command + def call + repository.load(Decision.new(decision_id), stream_name) + end + end +end diff --git a/app/aggregates/deciding/commands/set_funding_decision.rb b/app/aggregates/deciding/commands/set_funding_decision.rb new file mode 100644 index 000000000..190d8f5c3 --- /dev/null +++ b/app/aggregates/deciding/commands/set_funding_decision.rb @@ -0,0 +1,13 @@ +module Deciding + class SetFundingDecision < Command + attribute :user_id, Types::Uuid + attribute :result, Types::FundingDecisionResult + attribute :details, Types::String + + def call + with_decision do |decision| + decision.set_funding_decision(user_id:, result:, details:) + end + end + end +end diff --git a/app/aggregates/deciding/commands/set_interests_of_justice.rb b/app/aggregates/deciding/commands/set_interests_of_justice.rb new file mode 100644 index 000000000..1709b71bb --- /dev/null +++ b/app/aggregates/deciding/commands/set_interests_of_justice.rb @@ -0,0 +1,12 @@ +module Deciding + class SetInterestsOfJustice < Command + attribute :user_id, Types::Uuid + attribute :interests_of_justice, Types::InterestsOfJusticeDecision + + def call + with_decision do |decision| + decision.set_interests_of_justice(user_id:, interests_of_justice:) + end + end + end +end diff --git a/app/aggregates/deciding/decision.rb b/app/aggregates/deciding/decision.rb new file mode 100644 index 000000000..537e082c6 --- /dev/null +++ b/app/aggregates/deciding/decision.rb @@ -0,0 +1,54 @@ +module Deciding + class Decision + include AggregateRoot + + def initialize(decision_id) + @decision_id = decision_id + @interests_of_justice = nil + end + + attr_accessor :application_id, :decision_id, :result, :details, :state + + def create_draft(user_id:, application_id:) + apply DraftCreated.new( + data: { decision_id:, application_id:, user_id: } + ) + end + + def set_interests_of_justice(user_id:, interests_of_justice:) + apply InterestsOfJusticeSet.new( + data: { decision_id:, application_id:, user_id:, interests_of_justice: } + ) + end + + def set_funding_decision(user_id:, result:, details:) + apply FundingDecisionSet.new( + data: { decision_id:, application_id:, user_id:, result:, details: } + ) + end + + on DraftCreated do |event| + @application_id = event.data.fetch(:application_id) + @state = Types::DecisionState[:draft] + end + + on InterestsOfJusticeSet do |event| + @interests_of_justice = event.data.fetch(:interests_of_justice) + end + + on FundingDecisionSet do |event| + @result = event.data.fetch(:result) + @details = event.data.fetch(:details) + end + + def interests_of_justice + return if @interests_of_justice.nil? + + Types::InterestsOfJusticeDecision[@interests_of_justice] + end + + def to_param + decision_id + end + end +end diff --git a/app/aggregates/reviewing.rb b/app/aggregates/reviewing.rb index e75eb17a4..8692bebea 100644 --- a/app/aggregates/reviewing.rb +++ b/app/aggregates/reviewing.rb @@ -15,6 +15,9 @@ class CannotMarkAsReadyWhenSentBack < Error; end class CannotSendBackWhenCompleted < Error; end class NotReceived < Error; end + class DecisionAdded < Event; end + class DecisionRemoved < Event; end + class << self def stream_name(application_id) "Reviewing$#{application_id}" diff --git a/app/aggregates/reviewing/available_reviewer_actions.rb b/app/aggregates/reviewing/available_reviewer_actions.rb index c08ca749b..e4cbd48e1 100644 --- a/app/aggregates/reviewing/available_reviewer_actions.rb +++ b/app/aggregates/reviewing/available_reviewer_actions.rb @@ -6,8 +6,7 @@ class AvailableReviewerActions marked_as_ready: [:complete, :send_back], }, non_means: { - open: [:complete, :send_back], - marked_as_ready: [:complete, :send_back] # TODO: remove once all non-means in this state processed + open: FeatureFlags.adding_decisions.enabled? ? [:add_funding_decision, :send_back] : [:complete, :send_back], }, pse: { open: [:complete] diff --git a/app/aggregates/reviewing/command.rb b/app/aggregates/reviewing/command.rb index 829e1583f..c00e6bbe4 100644 --- a/app/aggregates/reviewing/command.rb +++ b/app/aggregates/reviewing/command.rb @@ -17,7 +17,7 @@ def repository end def stream_name - Reviewing.stream_name(application_id) + "Reviewing$#{application_id}" end class << self diff --git a/app/aggregates/reviewing/commands/add_decision.rb b/app/aggregates/reviewing/commands/add_decision.rb new file mode 100644 index 000000000..83cb234dd --- /dev/null +++ b/app/aggregates/reviewing/commands/add_decision.rb @@ -0,0 +1,13 @@ +module Reviewing + class AddDecision < Command + attribute :application_id, Types::Uuid + attribute :user_id, Types::Uuid + attribute :decision_id, Types::Uuid + + def call + with_review do |review| + review.add_decision(user_id:, decision_id:) + end + end + end +end diff --git a/app/aggregates/reviewing/commands/remove_decision.rb b/app/aggregates/reviewing/commands/remove_decision.rb new file mode 100644 index 000000000..3b3ef061d --- /dev/null +++ b/app/aggregates/reviewing/commands/remove_decision.rb @@ -0,0 +1,13 @@ +module Reviewing + class RemoveDecision < Command + attribute :application_id, Types::Uuid + attribute :user_id, Types::Uuid + attribute :decision_id, Types::Uuid + + def call + with_review do |review| + review.remove_decision(user_id:, decision_id:) + end + end + end +end diff --git a/app/aggregates/reviewing/review.rb b/app/aggregates/reviewing/review.rb index e4f3fae03..64184458f 100644 --- a/app/aggregates/reviewing/review.rb +++ b/app/aggregates/reviewing/review.rb @@ -4,22 +4,13 @@ class Review def initialize(id) @id = id - @application_type = nil - @state = nil - @return_reason = nil - @reviewer_id = nil - @reviewed_at = nil - @received_at = nil - @submitted_at = nil - @superseded_at = nil - @superseded_by = nil - @parent_id = nil - @work_stream = nil - end - - attr_reader :id, :state, :return_reason, :reviewed_at, :reviewer_id, - :submitted_at, :superseded_by, :superseded_at, :parent_id, - :work_stream, :application_type + @decision_ids = [] + end + + attr_accessor :state, :return_reason, :reviewed_at, :reviewer_id, :submitted_at, :superseded_by, + :superseded_at, :parent_id, :work_stream, :application_type + + attr_reader :id, :decision_ids alias application_id id @@ -68,6 +59,18 @@ def mark_as_ready(user_id:) ) end + def add_decision(user_id:, decision_id:) + apply DecisionAdded.new( + data: { application_id:, user_id:, decision_id: } + ) + end + + def remove_decision(user_id:, decision_id:) + apply DecisionRemoved.new( + data: { application_id:, user_id:, decision_id: } + ) + end + on ApplicationReceived do |event| @state = Types::ReviewState[:open] @received_at = event.timestamp @@ -77,6 +80,14 @@ def mark_as_ready(user_id:) @work_stream = event.data.fetch(:work_stream, Types::WorkStreamType['criminal_applications_team']) end + on DecisionAdded do |event| + @decision_ids << event.data.fetch(:decision_id) + end + + on DecisionRemoved do |event| + @decision_ids -= [event.data.fetch(:decision_id)] + end + on SentBack do |event| @state = Types::ReviewState[:sent_back] @return_reason = event.data.fetch(:reason, nil) @@ -108,15 +119,11 @@ def received? end def business_day - return nil unless @submitted_at - - BusinessDay.new(day_zero: @submitted_at) + BusinessDay.new(day_zero: @submitted_at) if @submitted_at.present? end def reviewed_on - return nil unless @reviewed_at - - @reviewed_at.in_time_zone('London').to_date + @reviewed_at.in_time_zone('London').to_date if @reviewed_at.present? end def available_reviewer_actions diff --git a/app/components/decision_component.rb b/app/components/decision_component.rb new file mode 100644 index 000000000..9363b116f --- /dev/null +++ b/app/components/decision_component.rb @@ -0,0 +1,82 @@ +class DecisionComponent < ViewComponent::Base + include ActionView::Helpers + include AppTextHelper + + def initialize(decision:, decision_iteration:) + @decision = decision + @decision_iteration = decision_iteration + + super + end + + def call + govuk_summary_card(title:, actions:) do |_card| + govuk_summary_list do |list| + if interests_of_justice.present? + list.with_row do |row| + row.with_key { label_text(:result, scope: [:decision, :interests_of_justice]) } + row.with_value { ioj_result } + end + + list.with_row do |row| + row.with_key { label_text(:details, scope: [:decision, :interests_of_justice]) } + row.with_value { simple_format(interests_of_justice[:details]) } + end + + list.with_row do |row| + row.with_key { label_text(:assessed_by, scope: [:decision, :interests_of_justice]) } + row.with_value { interests_of_justice[:assessed_by] } + end + + list.with_row do |row| + row.with_key { label_text(:assessed_on, scope: [:decision, :interests_of_justice]) } + row.with_value { l interests_of_justice[:assessed_on], format: :compact } + end + end + + list.with_row do |row| + row.with_key { label_text(:result, scope: [:decision]) } + row.with_value { render DecisionResultComponent.new(result: decision.result) } + end + + list.with_row do |row| + row.with_key { label_text(:details, scope: [:decision]) } + row.with_value { decision.details } + end + end + end + end + + private + + def ioj_result + t(interests_of_justice[:result], scope: [:values, :decision_result]) + end + + attr_reader :decision, :decision_iteration + + delegate :means, :interests_of_justice, to: :decision + + def actions + [change_link, remove_link].compact + end + + def remove_link + button_to('Remove', { action: :destroy, id: decision.decision_id }, method: :delete) + end + + def change_link + govuk_link_to('Change', { action: :edit, id: decision.decision_id }) + end + + def title + safe_join(['Case', count].compact, ' ') + end + + def count + return unless decision_iteration + return unless decision_iteration.size > 1 + + decision_iteration.index + 1 + end +end diff --git a/app/components/decision_result_component.rb b/app/components/decision_result_component.rb new file mode 100644 index 000000000..d3447e6ef --- /dev/null +++ b/app/components/decision_result_component.rb @@ -0,0 +1,29 @@ +class DecisionResultComponent < ViewComponent::Base + def initialize(result: nil) + @result = result + + super + end + + def call + return if result.nil? + + govuk_tag( + text: t(result, scope: [:values, :decision_result]), + colour: colour + ) + end + + private + + attr_reader :result + + def colour + case result + when /fail/ + 'red' + else + 'green' + end + end +end diff --git a/app/components/funding_decision_component.rb b/app/components/funding_decision_component.rb new file mode 100644 index 000000000..9dcc2a6c5 --- /dev/null +++ b/app/components/funding_decision_component.rb @@ -0,0 +1,29 @@ +class FundingDecisionComponent < ViewComponent::Base + # Wraps the Govuk Summary Card component so that when used with + # .with_collection the item number is added to the card title. + + def initialize(decision:, decision_iteration:) + @item = item + @item_title = title + @item_iteration = item_iteration + + super + end + + def call + app_card_list + end + + def title + safe_join([@item_title, count].compact, ' ') + end + + private + + def count + return unless item_iteration + return unless item_iteration.size > 1 + + item_iteration.index + 1 + end +end diff --git a/app/components/review_action_component.rb b/app/components/review_action_component.rb index ed08301f1..48cddfa40 100644 --- a/app/components/review_action_component.rb +++ b/app/components/review_action_component.rb @@ -24,15 +24,28 @@ def target complete_crime_application_path(application) when :send_back new_crime_application_return_path(application) + when :add_funding_decision + crime_application_decisions_path(application) when :mark_as_ready ready_crime_application_path(application) end end def method - return :get if action == :send_back + case action + when :send_back + :get + when :add_funding_decision + add_funding_decision_method + else + :put + end + end + + def add_funding_decision_method + return :get unless application.review.decision_ids.empty? - :put + :post end def warning diff --git a/app/controllers/casework/decisions/funding_decisions_controller.rb b/app/controllers/casework/decisions/funding_decisions_controller.rb new file mode 100644 index 000000000..634ad9f49 --- /dev/null +++ b/app/controllers/casework/decisions/funding_decisions_controller.rb @@ -0,0 +1,26 @@ +module Casework + module Decisions + # rename overall result? + class FundingDecisionsController < Casework::BaseController + include EditableDecision + + before_action :set_form + + private + + def form_class + ::Decisions::FundingDecisionForm + end + + def permitted_params + params[:decisions_funding_decision_form].permit( + :result, :details + ) + end + + def next_step + crime_application_decisions_path + end + end + end +end diff --git a/app/controllers/casework/decisions/interests_of_justices_controller.rb b/app/controllers/casework/decisions/interests_of_justices_controller.rb new file mode 100644 index 000000000..45992f3a4 --- /dev/null +++ b/app/controllers/casework/decisions/interests_of_justices_controller.rb @@ -0,0 +1,25 @@ +module Casework + module Decisions + class InterestsOfJusticesController < Casework::BaseController + include EditableDecision + + before_action :set_form + + private + + def form_class + ::Decisions::InterestsOfJusticeForm + end + + def permitted_params + params[:decisions_interests_of_justice_form].permit( + :result, :details, :assessed_by, :assessed_on + ) + end + + def next_step + edit_crime_application_decision_funding_decision_path + end + end + end +end diff --git a/app/controllers/casework/decisions/means_controller.rb b/app/controllers/casework/decisions/means_controller.rb new file mode 100644 index 000000000..9991a69ca --- /dev/null +++ b/app/controllers/casework/decisions/means_controller.rb @@ -0,0 +1,57 @@ +module Casework + module Decisions + class InterestsOfJusticesController < Casework::BaseController + before_action :set_crime_application + + def edit + @interests_of_justice = ::Decisions::InterestsOfJustice.new + end + + def update + @interests_of_justice = ::Decisions::InterestsOfJustice.new( + interests_of_justice_params + ) + + @interests_of_justice.validate! + + Deciding::SetInterestsOfJustice.new( + decision_id: params[:decision_id], + user_id: current_user_id, + details: @interests_of_justice.attributes.deep_symbolize_keys + ).call + + flash_and_redirect :success, :sent_back + rescue ActiveModel::ValidationError + render :edit + end + + private + + def draft_decision + @crime_application.decision + end + + def interests_of_justice_params + params[:decisions_interests_of_justice].permit( + :result, :reason, :assessed_by, :assessed_on + ) + end + + def create_draft_decision + Reviewing::CreateDraftDecision.new( + application_id: params[:crime_application_id], + user_id: current_user_id + ).call + end + + def set_ioj_decision + Reviewing::CreateDraftDecision.new( + application_id: params[:crime_application_id], + user_id: current_user_id + ).call + end + + def set_means_decision; end + end + end +end diff --git a/app/controllers/casework/decisions_controller.rb b/app/controllers/casework/decisions_controller.rb new file mode 100644 index 000000000..6ded05759 --- /dev/null +++ b/app/controllers/casework/decisions_controller.rb @@ -0,0 +1,58 @@ +module Casework + class DecisionsController < Casework::BaseController + include EditableDecision + + before_action :set_decision, except: [:index, :create] + + def index + @decisions = current_crime_application.review.decision_ids.map do |decision_id| + Deciding::LoadDecision.call( + application_id: params[:crime_application_id], + decision_id: decision_id + ) + end + end + + def edit + redirect_to edit_crime_application_decision_interests_of_justice_path( + crime_application_id: params[:crime_application_id], decision_id: + params[:id] + ) + end + + def confirm_destroy; end + + def create + decision_id = SecureRandom.uuid + + args = { + application_id: @crime_application.id, + user_id: current_user_id, + decision_id: decision_id + } + + ActiveRecord::Base.transaction do + Reviewing::AddDecision.call(**args) + Deciding::CreateDraft.call(**args) + end + + redirect_to edit_crime_application_decision_interests_of_justice_path( + crime_application_id: params[:crime_application_id], decision_id: decision_id + ) + end + + def destroy + args = { + application_id: @crime_application.id, + user_id: current_user_id, + decision_id: @decision.decision_id + } + + ActiveRecord::Base.transaction do + Reviewing::RemoveDecision.new(**args).call + end + + redirect_to action: :index + end + end +end diff --git a/app/controllers/concerns/editable_decision.rb b/app/controllers/concerns/editable_decision.rb new file mode 100644 index 000000000..743ed598d --- /dev/null +++ b/app/controllers/concerns/editable_decision.rb @@ -0,0 +1,41 @@ +module EditableDecision + extend ActiveSupport::Concern + + included do + before_action :set_crime_application + before_action :set_decision + before_action :confirm_assigned + end + + def edit; end + + def update + @form_object.update_with_user!( + permitted_params, current_user_id + ) + + redirect_to next_step + rescue ActiveModel::ValidationError + render :edit + end + + private + + def set_decision + decision = Deciding::LoadDecision.call( + decision_id: params[:decision_id] || params[:id] + ) + + raise Deciding::DecisionNotFound unless decision.application_id == current_crime_application.id + + @decision = decision + end + + def set_form + @form_object = form_class.build(@decision) + end + + def confirm_assigned + raise Deciding::DecisionNotFound unless @crime_application.reviewable_by?(current_user_id) + end +end diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 712e8a332..ff84f1f48 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -11,6 +11,10 @@ def offence_dates(offence) render OffenceDatesComponent.new(offence:) end + def decision_result(result) + render DecisionResultComponent.new(result:) + end + def conflict_of_interest(codefendant) render ConflictOfInterestComponent.new(codefendant:) end diff --git a/app/lib/types.rb b/app/lib/types.rb index 48efbcfb7..ee3005bb5 100644 --- a/app/lib/types.rb +++ b/app/lib/types.rb @@ -34,6 +34,8 @@ module Types ReviewState = Symbol.default(:open).enum(*%i[open sent_back completed marked_as_ready]) + DecisionState = Symbol.enum(*%i[draft]) + ReviewType = Symbol.enum(*%i[means non_means pse]) CASEWORKER_ROLE = 'caseworker'.freeze @@ -57,6 +59,18 @@ module Types details: String ) + # MAAT also returns other result values which we may also need to handle. + MeansResult = String.enum('pass', 'fail') + InterestsOfJusticeResult = String.enum('pass', 'fail') + FundingDecisionResult = String.enum(*%w[granted_on_ioj fail_on_ioj]) + + InterestsOfJusticeDecision = Hash.schema( + result: InterestsOfJusticeResult, + details: String, + assessed_by: String, + assessed_on: Date + ) + Report = String.enum(*%w[ caseworker_report processed_report diff --git a/app/models/crime_application.rb b/app/models/crime_application.rb index f55b1cf2e..a396312a5 100644 --- a/app/models/crime_application.rb +++ b/app/models/crime_application.rb @@ -43,6 +43,10 @@ def history @history ||= ApplicationHistory.new(application: self) end + def funding_decisions + @funding_decisions ||= [Mock::FundingDecision.draft_maat] + end + def to_param id end diff --git a/app/models/decision.rb b/app/models/decision.rb new file mode 100644 index 000000000..2520d8317 --- /dev/null +++ b/app/models/decision.rb @@ -0,0 +1,6 @@ +class Decision < ApplicationStruct + attribute? :interests_of_justice, Decisions::InterestsOfJustice + attribute? :result, Types::String + attribute? :details, Types::String + attribute? :decision_id, Types::Uuid +end diff --git a/app/models/decisions/funding_decision_form.rb b/app/models/decisions/funding_decision_form.rb new file mode 100644 index 000000000..bc5c7ef3d --- /dev/null +++ b/app/models/decisions/funding_decision_form.rb @@ -0,0 +1,41 @@ +module Decisions + class FundingDecisionForm + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveRecord::AttributeAssignment + include ActiveModel::Dirty + + attribute :application_id, :immutable_string + attribute :decision_id, :immutable_string + + attribute :result, :string + attribute :details, :string + + validates :result, inclusion: { in: :possible_results } + + def possible_results + Types::FundingDecisionResult.values + end + + def update_with_user!(attributes, user_id) + assign_attributes(attributes) + validate! + return unless changed? + + Deciding::SetFundingDecision.new( + decision_id:, user_id:, result:, details: + ).call + end + + class << self + def build(decision) + new( + application_id: decision.application_id, + decision_id: decision.decision_id, + result: decision.result, + details: decision.details + ) + end + end + end +end diff --git a/app/models/decisions/interests_of_justice.rb b/app/models/decisions/interests_of_justice.rb new file mode 100644 index 000000000..2aa237334 --- /dev/null +++ b/app/models/decisions/interests_of_justice.rb @@ -0,0 +1,8 @@ +module Decisions + class InterestsOfJustice < ApplicationStruct + attribute :result, Types::InterestsOfJusticeResult + attribute :details, Types::String + attribute :assessed_by, Types::String + attribute :assessed_on, Types::Date + end +end diff --git a/app/models/decisions/interests_of_justice_form.rb b/app/models/decisions/interests_of_justice_form.rb new file mode 100644 index 000000000..5bddd89ec --- /dev/null +++ b/app/models/decisions/interests_of_justice_form.rb @@ -0,0 +1,46 @@ +module Decisions + class InterestsOfJusticeForm + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveRecord::AttributeAssignment + include ActiveModel::Dirty + + attribute :application_id, :immutable_string + attribute :decision_id, :immutable_string + + attribute :result, :string + attribute :details, :string + attribute :assessed_by, :string + attribute :assessed_on, :date + + validates :result, inclusion: { in: :possible_results } + validates :details, :assessed_by, presence: true + validates :assessed_on, presence: true + + def possible_results + Types::InterestsOfJusticeResult.values + end + + def update_with_user!(attributes, user_id) + assign_attributes(attributes) + validate! + return unless changed? + + Deciding::SetInterestsOfJustice.new( + decision_id: decision_id, + user_id: user_id, + interests_of_justice: self.attributes.deep_symbolize_keys + ).call + end + + class << self + def build(decision) + new( + **(decision.interests_of_justice || {}), + application_id: decision.application_id, + decision_id: decision.decision_id + ) + end + end + end +end diff --git a/app/models/decisions/outcome.rb b/app/models/decisions/outcome.rb new file mode 100644 index 000000000..c9807bb3d --- /dev/null +++ b/app/models/decisions/outcome.rb @@ -0,0 +1,10 @@ +module Decisions + class Outcome < ApplicationStruct + OUTCOMES = [:failed_interests_of_justice, :granted].freeze + + attribute? :outcome, Types::Nil | Types::String + attribute? :reason, Types::Nil | Types::String + + validates :outcome, presence: true + end +end diff --git a/app/models/interests_of_justice_decision.rb b/app/models/interests_of_justice_decision.rb new file mode 100644 index 000000000..1cc300315 --- /dev/null +++ b/app/models/interests_of_justice_decision.rb @@ -0,0 +1,18 @@ +class InterestsOfJusticeDecision < ApplicationStruct + attribute :interests_of_justice, InterestsOfJusticeDecision +end + +class InterestsOfJusticeDecision + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveRecord::AttributeAssignment + + attribute :result, :string + attribute :details, :string + attribute :assessed_by, :string + attribute :assessed_on, :date + + validates :result, inclusion: { in: Types::InterestsOfJusticeResult.values } + validates :reason, :assessed_by, presence: true + validates :assessed_on, presence: true +end diff --git a/app/views/casework/decisions/funding_decisions/edit.html.erb b/app/views/casework/decisions/funding_decisions/edit.html.erb new file mode 100644 index 000000000..e2e0a67ae --- /dev/null +++ b/app/views/casework/decisions/funding_decisions/edit.html.erb @@ -0,0 +1,25 @@ +<% title t '.title' %> + +
+
+

+ <%= @crime_application.reference %> +

+ +

+ <%= @crime_application.applicant.full_name %> +

+ +

+ <%= t '.title' %> +

+ + <%= form_with model: @form_object, + url: crime_application_decision_funding_decision_path(), method: :put do |f| %> + <%= f.govuk_error_summary %> + <%= f.govuk_collection_radio_buttons :result, @form_object.possible_results, :to_s %> + <%= f.govuk_text_area :details %> + <%= f.govuk_submit %> + <% end %> +
+
diff --git a/app/views/casework/decisions/index.html.erb b/app/views/casework/decisions/index.html.erb new file mode 100644 index 000000000..5db967c53 --- /dev/null +++ b/app/views/casework/decisions/index.html.erb @@ -0,0 +1,21 @@ +<% title t '.title', count: @decisions.size %> + +
+
+

+ <%= @crime_application.reference %> +

+ +

+ <%= @crime_application.applicant.full_name %> +

+ +

+ <%= t '.title', count: @decisions.size %> +

+
+
+ +<%= render(DecisionComponent.with_collection(@decisions)) %> + +<%= govuk_button_to(t(:add_funding_decision, scope: 'calls_to_action')) %> diff --git a/app/views/casework/decisions/interests_of_justices/edit.html.erb b/app/views/casework/decisions/interests_of_justices/edit.html.erb new file mode 100644 index 000000000..73f0698c5 --- /dev/null +++ b/app/views/casework/decisions/interests_of_justices/edit.html.erb @@ -0,0 +1,32 @@ +<% title t '.title' %> + +
+
+

+ <%= @crime_application.reference %> +

+ +

+ <%= @crime_application.applicant.full_name %> +

+ +

+ <%= t '.title' %> +

+ + <%= form_with model: @form_object, + url: crime_application_decision_interests_of_justice_path(), method: :put do |f| %> + + <%= f.govuk_error_summary %> + + <%= f.govuk_collection_radio_buttons :result, + @form_object.possible_results, + :to_s + %> + <%= f.govuk_text_area :details %> + <%= f.govuk_text_field :assessed_by %> + <%= f.govuk_date_field :assessed_on, maxlength_enabled: true %> + <%= f.govuk_submit %> + <% end %> +
+
diff --git a/app/views/casework/decisions/interests_of_justices/new.html.erb b/app/views/casework/decisions/interests_of_justices/new.html.erb new file mode 100644 index 000000000..f449f6464 --- /dev/null +++ b/app/views/casework/decisions/interests_of_justices/new.html.erb @@ -0,0 +1,23 @@ +<% title 'TESING' %> + +
+
+

+ <%= @crime_application.reference %> +

+ + <%= form_with model: @interests_of_justice, url: crime_application_add_ioj_decision_path(@crime_application), method: :post do |f| %> + + <%= f.govuk_error_summary %> + + <%= f.govuk_collection_radio_buttons :result, + ::Types::InterestsOfJusticeResult.values, + :to_s + %> + <%= f.govuk_text_area :reason %> + <%= f.govuk_text_field :assessed_by %> + <%= f.govuk_date_field :assessed_on, maxlength_enabled: true %> + <%= f.govuk_submit %> + <% end %> +
+
diff --git a/app/views/casework/funding_decisions/new.html.erb b/app/views/casework/funding_decisions/new.html.erb new file mode 100644 index 000000000..6a606254f --- /dev/null +++ b/app/views/casework/funding_decisions/new.html.erb @@ -0,0 +1,18 @@ +<% title 'TESING' %> + +
+
+

+ <%= @crime_application.reference %> +

+ + <%= form_with model: @form_object, url: crime_application_funding_decisions_path(@crime_application.id) do |f| %> + <%= f.text_field :reason %> + + <%= f.govuk_error_summary %> + + <%= f.govuk_submit %> + <% end %> +
+
+ diff --git a/config/application.rb b/config/application.rb index 37dcaf4df..84017ab61 100644 --- a/config/application.rb +++ b/config/application.rb @@ -79,6 +79,8 @@ class Application < Rails::Application config.action_dispatch.rescue_responses.merge!( 'Reporting::ReportNotFound' => :not_found, 'Allocating::WorkStreamNotFound' => :not_found, + 'Deciding::DecisionNotFound' => :not_found, + 'Deciding::ApplicationNotAssignedToUser' => :forbidden, 'ApplicationController::ForbiddenError' => :forbidden ) end diff --git a/config/initializers/event_store.rb b/config/initializers/event_store.rb index 42f4ee4b6..4554db7a2 100644 --- a/config/initializers/event_store.rb +++ b/config/initializers/event_store.rb @@ -1,6 +1,8 @@ Rails.configuration.to_prepare do event_store = Rails.configuration.event_store = RailsEventStore::Client.new + Deciding::Configuration.new.call(event_store) + #Notifying NotifierConfiguration.new.call(event_store) @@ -9,4 +11,5 @@ ReceivedOnReports::Configuration.new.call(event_store) Reviews::Configuration.new.call(event_store) CaseworkerReports::Configuration.new.call(event_store) + end diff --git a/config/locales/en/casework.yml b/config/locales/en/casework.yml index 143661235..e65641067 100644 --- a/config/locales/en/casework.yml +++ b/config/locales/en/casework.yml @@ -180,6 +180,16 @@ en: income_more_than: "%{prefix} more than £12,475?" income_details: Income income_details_partner: Partner's income + + decision: + card_title: Case + interests_of_justice: + result: Interests of justice test results + details: Interests of justice reason + assessed_by: Interests of justice test caseworker name + assessed_on: Date of interests of justice test + result: Overall result + details: Further information about the decision employments_title: Jobs employment_title: Job employments: @@ -415,6 +425,13 @@ en: change_in_financial_circumstances: Change in financial circumstances committal: Committal for sentence deleted_user_name: '[deleted]' + decision_result: + pass: Passed + fail: Failed + granted_on_ioj: Granted + fail_on_ioj: Rejected + + either_way: Either way esa: Income-related Employment and Support Allowance (ESA) guarantee_pension: Guarantee Credit element of Pension Credit @@ -575,6 +592,7 @@ en: calls_to_action: abandon_reassign_to_self: No, do not reassign + add_funding_decision: Add a funding decision assign_to_self: Assign to your list clear_search: Clear confirm_reassign_to_self: Yes, reassign @@ -645,7 +663,6 @@ en: casework: - assigned_applications: index: page_title: Your list @@ -725,3 +742,15 @@ en: <<: *LABELS values: <<: *VALUES + decisions: + index: + title: + zero: Add a funding decision + one: Check the funding decision + other: You have added %{count} case + interests_of_justices: + edit: + title: Interests of justice test + funding_decisions: + edit: + title: Overall result diff --git a/config/locales/en/helpers.yml b/config/locales/en/helpers.yml index ee147de7f..a6417188b 100644 --- a/config/locales/en/helpers.yml +++ b/config/locales/en/helpers.yml @@ -2,10 +2,35 @@ en: helpers: back_link: Back + + hint: + decisions_interests_of_justice_form: + assessed_on: For example, DD/MM/YYY + + legend: + decisions_interests_of_justice_form: + result: What is the interests of justice test result? + assessed_on: Enter the date of assessment + decisions_funding_decision_form: + result: What is the funding decision for this application? + + label: + decisions_interests_of_justice_form: + result_options: + pass: Passed + fail: Refused + details: What is the reason for this? + assessed_by: What is the name of the caseworker who assessed this? + + decisions_funding_decision_form: + result_options: + granted_on_ioj: Granted + fail_on_ioj: Failed interests of justice test + details: Additional comments for the provider (optional) + submit: save_and_continue: Save and continue save_and_come_back_later: Save and come back later subjects: applicant: client applicant_and_partner: client %{conjunction} partner - diff --git a/config/routes.rb b/config/routes.rb index e41b20c05..634437d84 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ Rails.application.routes.draw do mount DatastoreApi::HealthEngine::Engine => '/datastore' + mount RailsEventStore::Browser => "/res" if Rails.env.development? get :health, to: 'healthcheck#show' get :ping, to: 'healthcheck#ping' @@ -36,6 +37,16 @@ put :ready, on: :member resource :reassign, only: [:new, :create] resource :return, only: [:new, :create] + post 'add_a_funding_decision', to: 'decisions#create' + post 'add_a_funding_decision', to: 'decisions#create' + + resources :decisions do + get :confirm_destroy, on: :member + scope module: :decisions do + resource :interests_of_justice, only: [:edit, :update] + resource :funding_decision, only: [:edit, :update] + end + end end get 'applications/open/:work_stream', to: 'crime_applications#open', as: :open_work_stream diff --git a/config/settings.yml b/config/settings.yml index 1c4b83ee0..6504eed26 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -23,6 +23,11 @@ feature_flags: local: true staging: true production: true + adding_decisions: + local: true + staging: false + production: false + # For settings that vary by HostEnv name host_env_settings: phase_banner_tag: diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb new file mode 100644 index 000000000..0eb2de564 --- /dev/null +++ b/spec/support/capybara_helpers.rb @@ -0,0 +1,39 @@ +module CapybaraHelpers + def fill_in_date(date) + fill_in('Day', with: date.mday) + fill_in('Month', with: date.month) + fill_in('Year', with: date.year) + end + + def fill_date(question, with: nil) + date = with || Time.zone.today + + within(find('legend', text: question).sibling('div.govuk-date-input')) do + fill_in_date(date) + end + end + + # radio buttons + def choose_answer(question, choice) + q = find('legend', text: question).sibling('div.govuk-radios') + + within q do + choose(choice) + end + end + + # check boxes + def choose_answers(question, choices = []) + q = find('legend', text: question).sibling('div.govuk-checkboxes') + + within q do + choices.each do |choice| + check(choice) + end + end + end + + def save_and_continue + click_button('Save and continue') + end +end diff --git a/spec/system/casework/reviewing/a_non_means_application_spec.rb b/spec/system/casework/reviewing/a_non_means_application_spec.rb index 5548e6cee..5abda5034 100644 --- a/spec/system/casework/reviewing/a_non_means_application_spec.rb +++ b/spec/system/casework/reviewing/a_non_means_application_spec.rb @@ -2,6 +2,8 @@ RSpec.describe 'Reviewing a Non-means application' do include_context 'with stubbed application' do + let(:feature_flag_enabled?) { false } + let(:application_data) do JSON.parse(LaaCrimeSchemas.fixture(1.0).read).merge( 'is_means_tested' => 'no', @@ -10,6 +12,10 @@ end before do + allow(FeatureFlags).to receive(:adding_decisions) { + instance_double(FeatureFlags::EnabledFeature, enabled?: feature_flag_enabled?) + } + allow(DatastoreApi::Requests::UpdateApplication).to receive(:new).and_return( instance_double(DatastoreApi::Requests::UpdateApplication, call: {}) ) @@ -20,10 +26,24 @@ it 'can be completed by the caseworker' do expect(page).to have_button('Send back to provider') - click_button 'Mark as completed' - expect(page).to have_content('You marked the application as complete') end + + context 'when adding decisions feature flag enabled' do + let(:feature_flag_enabled?) { true } + + before do + click_button 'Start' + choose_answer('What is the interests of justice test result?', 'Passed') + fill_in('What is the reason for this?', 'Test result reason details') + fill_in('Enter the name of the caseworker who assessed this', 'Zoe Blogs') + fill_date('Enter the date of assessment', Date.yesterday) + save_and_continue + end + + it 'can be completed by the caseworker' do + end + end end end diff --git a/spec/system/casework/reviewing/viewing_a_draft_maat_funding_decision_spec.rb b/spec/system/casework/reviewing/viewing_a_draft_maat_funding_decision_spec.rb new file mode 100644 index 000000000..e69de29bb