diff --git a/Gemfile b/Gemfile index b2d9ef3872..a5793e55d7 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ ruby "3.2.2" gem "aasm", "~> 5.5.0" gem "active_model_serializers", "~> 0.10.14" +gem "deep_cloneable", "~> 3.2.0" gem "discard", "~> 1.3" gem "geckoboard-ruby" gem "google-apis-sheets_v4" diff --git a/Gemfile.lock b/Gemfile.lock index cb10d7b889..97b4f072d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -213,6 +213,8 @@ GEM date (3.3.3) debug_inspector (1.1.0) declarative (0.0.20) + deep_cloneable (3.2.0) + activerecord (>= 3.1.0, < 8) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) devise (4.9.3) @@ -735,6 +737,7 @@ DEPENDENCIES cucumber cucumber-rails (>= 2.4.0) database_cleaner + deep_cloneable (~> 3.2.0) devise devise_saml_authenticatable (>= 1.7.0) dibber diff --git a/app/controllers/providers/copy_case_confirmations_controller.rb b/app/controllers/providers/copy_case_confirmations_controller.rb new file mode 100644 index 0000000000..1c4d5aadb4 --- /dev/null +++ b/app/controllers/providers/copy_case_confirmations_controller.rb @@ -0,0 +1,23 @@ +module Providers + class CopyCaseConfirmationsController < ProviderBaseController + def show + @form = CopyCase::ConfirmationForm.new(model: legal_aid_application) + @copiable_case = LegalAidApplication.find(session[:copy_case_id]) + end + + def update + @form = CopyCase::ConfirmationForm.new(form_params) + @copiable_case = LegalAidApplication.find(session[:copy_case_id]) + + render :show unless save_continue_or_draft(@form) + end + + private + + def form_params + merge_with_model(legal_aid_application) do + params.require(:legal_aid_application).permit(:copy_case_id, :copy_case_confirmation) + end + end + end +end diff --git a/app/controllers/providers/copy_case_invitations_controller.rb b/app/controllers/providers/copy_case_invitations_controller.rb new file mode 100644 index 0000000000..87d69cb537 --- /dev/null +++ b/app/controllers/providers/copy_case_invitations_controller.rb @@ -0,0 +1,23 @@ +module Providers + class CopyCaseInvitationsController < ProviderBaseController + def show + @form = CopyCase::InvitationForm.new(model: legal_aid_application) + end + + def update + @form = CopyCase::InvitationForm.new(form_params) + + render :show unless save_continue_or_draft(@form) + end + + private + + def form_params + merge_with_model(legal_aid_application) do + next {} unless params[:legal_aid_application] + + params.require(:legal_aid_application).permit(:copy_case) + end + end + end +end diff --git a/app/controllers/providers/copy_case_searches_controller.rb b/app/controllers/providers/copy_case_searches_controller.rb new file mode 100644 index 0000000000..7ab7096884 --- /dev/null +++ b/app/controllers/providers/copy_case_searches_controller.rb @@ -0,0 +1,27 @@ +module Providers + class CopyCaseSearchesController < ProviderBaseController + def show + @form = CopyCase::SearchForm.new(model: legal_aid_application) + end + + def update + @form = CopyCase::SearchForm.new(form_params) + + render :show unless save_continue_or_draft(@form) + end + + private + + def save_continue_or_draft(form, **) + draft_selected? ? form.save_as_draft : form.save! + return false if form.invalid? + + session[:copy_case_id] = form.copiable_case.id + continue_or_draft(**) + end + + def form_params + params.require(:legal_aid_application).permit(:search_ref) + end + end +end diff --git a/app/forms/copy_case/confirmation_form.rb b/app/forms/copy_case/confirmation_form.rb new file mode 100644 index 0000000000..2910e0559f --- /dev/null +++ b/app/forms/copy_case/confirmation_form.rb @@ -0,0 +1,30 @@ +module CopyCase + class ConfirmationForm < BaseForm + form_for LegalAidApplication + + attr_accessor :copy_case_id, :copy_case_confirmation + + validates :copy_case_id, presence: true, unless: proc { draft? } + validates :copy_case_confirmation, presence: true, unless: proc { draft? || copy_case_confirmation.present? } + + def save + return false unless valid? + + cloner = CopyCase::ClonerService.new(legal_aid_application, legal_aid_application_to_copy) + cloner.call + end + alias_method :save!, :save + + def legal_aid_application_to_copy + @legal_aid_application_to_copy ||= LegalAidApplication.find(copy_case_id) + end + + def legal_aid_application + @legal_aid_application ||= model + end + + def exclude_from_model + %i[copy_case_id copy_case_confirmation] + end + end +end diff --git a/app/forms/copy_case/invitation_form.rb b/app/forms/copy_case/invitation_form.rb new file mode 100644 index 0000000000..7ac10479c0 --- /dev/null +++ b/app/forms/copy_case/invitation_form.rb @@ -0,0 +1,9 @@ +module CopyCase + class InvitationForm < BaseForm + form_for LegalAidApplication + + attr_accessor :copy_case + + validates :copy_case, presence: true, unless: proc { draft? || copy_case.present? } + end +end diff --git a/app/forms/copy_case/search_form.rb b/app/forms/copy_case/search_form.rb new file mode 100644 index 0000000000..1159520767 --- /dev/null +++ b/app/forms/copy_case/search_form.rb @@ -0,0 +1,35 @@ +module CopyCase + class SearchForm < BaseForm + form_for LegalAidApplication + + APPLICATION_REF_REGEXP = /\AL-[0-9ABCDEFHIJKLMNPRTUVWXY]{3}-[0-9ABCDEFHIJKLMNPRTUVWXY]{3}\z/ + + attr_accessor :search_ref, :copiable_case + + # TODO: error message locales + validates :search_ref, + presence: true, + format: { with: APPLICATION_REF_REGEXP }, + unless: :draft? + + validate :case_exists, unless: :draft? + + def case_exists + errors.add(:search_ref, "does not exist") unless case_found? + end + + def save + return false unless valid? + + true + end + + def case_found? + @copiable_case = LegalAidApplication.find_by(application_ref: search_ref) + end + + def exclude_from_model + [:search_ref] + end + end +end diff --git a/app/services/copy_case/cloner_service.rb b/app/services/copy_case/cloner_service.rb new file mode 100644 index 0000000000..459d7c6972 --- /dev/null +++ b/app/services/copy_case/cloner_service.rb @@ -0,0 +1,32 @@ +module CopyCase + class ClonerService + attr_accessor :copy, :original + + def self.call(copy, original) + new(copy, original).call + end + + def initialize(copy, original) + @copy = copy + @original = original + end + + def call + clone_proceedings + end + + private + + def clone_proceedings + new_proceedings = original.proceedings.each_with_object([]) do |proceeding, memo| + memo << proceeding.deep_clone( + except: %i[legal_aid_application_id proceeding_case_id], + include: [:scope_limitations], + ) + end + + copy.proceedings = new_proceedings + copy.save! + end + end +end diff --git a/app/services/flow/flows/provider_start.rb b/app/services/flow/flows/provider_start.rb index 88f32108f1..376a5710e4 100644 --- a/app/services/flow/flows/provider_start.rb +++ b/app/services/flow/flows/provider_start.rb @@ -28,12 +28,47 @@ class ProviderStart < FlowSteps }, address_selections: { path: ->(application) { urls.providers_legal_aid_application_address_selection_path(application) }, - forward: ->(application) { application.proceedings.any? ? :has_other_proceedings : :proceedings_types }, + forward: lambda do |application| + if Setting.linked_applications? + :copy_case_invitations + else + application.proceedings.any? ? :has_other_proceedings : :proceedings_types + end + end, check_answers: :check_provider_answers, }, addresses: { path: ->(application) { urls.providers_legal_aid_application_address_path(application) }, - forward: ->(application) { application.proceedings.any? ? :has_other_proceedings : :proceedings_types }, + forward: lambda do |application| + if Setting.linked_applications? + :copy_case_invitations + else + application.proceedings.any? ? :has_other_proceedings : :proceedings_types + end + end, + check_answers: :check_provider_answers, + }, + copy_case_invitations: { + path: ->(application) { urls.providers_legal_aid_application_copy_case_invitation_path(application) }, + forward: lambda do |application| + if application.copy_case? + :copy_case_searches + else + application.proceedings.any? ? :has_other_proceedings : :proceedings_types + end + end, + check_answers: :check_provider_answers, + }, + copy_case_searches: { + path: ->(application) { urls.providers_legal_aid_application_copy_case_search_path(application) }, + forward: :copy_case_confirmations, + check_answers: :check_provider_answers, + }, + copy_case_confirmations: { + path: ->(application) { urls.providers_legal_aid_application_copy_case_confirmation_path(application) }, + forward: lambda do |application| + application.proceedings.any? ? :has_other_proceedings : :proceedings_types + end, check_answers: :check_provider_answers, }, about_financial_means: { diff --git a/app/views/providers/copy_case_confirmations/show.html.erb b/app/views/providers/copy_case_confirmations/show.html.erb new file mode 100644 index 0000000000..403f892bef --- /dev/null +++ b/app/views/providers/copy_case_confirmations/show.html.erb @@ -0,0 +1,44 @@ +<%= form_with(model: @form, + url: providers_legal_aid_application_copy_case_confirmation_path, + method: :patch, + local: true) do |form| %> + <%= page_template page_title: t(".heading"), template: :basic, form: do %> + + <%= govuk_table do |table| + table.with_caption(html_attributes: { class: "govuk-visually-hidden" }, text: t(".table_caption")) + + table.with_head do |head| + head.with_row do |row| + row.with_cell(text: "LAA Ref.") + row.with_cell(text: "Client") + row.with_cell(text: "Category") + row.with_cell(text: "Firm") + end + end + + table.with_body do |body| + body.with_row do |row| + row.with_cell(text: @copiable_case.application_ref) + row.with_cell(text: @copiable_case.applicant.full_name) + row.with_cell(text: @copiable_case.proceedings.map(&:category_of_law).join(",")) + row.with_cell(text: @copiable_case.provider.firm.name) + end + end + end %> + + <%= form.hidden_field :copy_case_id, value: @copiable_case.id %> + + <%= form.govuk_collection_radio_buttons( + :copy_case_confirmation, + yes_no_options, + :value, + :label, + legend: { size: "xl", tag: "h1", text: t(".heading") }, + ) %> + + <%= next_action_buttons( + show_draft: true, + form:, + ) %> + <% end %> +<% end %> diff --git a/app/views/providers/copy_case_invitations/show.html.erb b/app/views/providers/copy_case_invitations/show.html.erb new file mode 100644 index 0000000000..e8f389eda1 --- /dev/null +++ b/app/views/providers/copy_case_invitations/show.html.erb @@ -0,0 +1,20 @@ +<%= form_with(model: @form, + url: providers_legal_aid_application_copy_case_invitation_path, + method: :patch, + local: true) do |form| %> + <%= page_template page_title: t(".heading"), template: :basic, form: do %> + + <%= form.govuk_collection_radio_buttons( + :copy_case, + yes_no_options, + :value, + :label, + legend: { size: "xl", tag: "h1", text: t(".heading") }, + ) %> + + <%= next_action_buttons( + show_draft: true, + form:, + ) %> + <% end %> +<% end %> diff --git a/app/views/providers/copy_case_searches/show.html.erb b/app/views/providers/copy_case_searches/show.html.erb new file mode 100644 index 0000000000..b783deeed0 --- /dev/null +++ b/app/views/providers/copy_case_searches/show.html.erb @@ -0,0 +1,17 @@ +<%= form_with(model: @form, + url: providers_legal_aid_application_copy_case_search_path, + method: :patch, + local: true) do |form| %> + <%= page_template page_title: t(".heading"), template: :basic, form: do %> + <%= form.govuk_text_field :search_ref, + label: { text: t(".search_ref.label"), size: "xl", tag: "h1" }, + hint: { text: t(".search_ref.hint") }, + width: "three-quarters", + value: params["search_ref"] || "" %> + + <%= next_action_buttons( + show_draft: true, + form:, + ) %> + <% end %> +<% end %> diff --git a/config/locales/en/activemodel.yml b/config/locales/en/activemodel.yml index 2fabe35db5..b7b860804f 100644 --- a/config/locales/en/activemodel.yml +++ b/config/locales/en/activemodel.yml @@ -411,6 +411,8 @@ en: blank: Enter details of the incident legal_aid_application: attributes: + copy_case: + blank: Select yes if you want to copy an application confirm_delegated_functions_date: blank: Confirm the date you used delegated functions has_dependants: diff --git a/config/locales/en/providers.yml b/config/locales/en/providers.yml index 89584fb0a0..f9ac4ff96b 100644 --- a/config/locales/en/providers.yml +++ b/config/locales/en/providers.yml @@ -317,6 +317,21 @@ en: be prosecuted need to pay a financial penalty have their legal aid stopped and have to pay back the costs + + copy_case_confirmations: + show: + heading: Copy this application to your application? + table_caption: Application to copy to your application + copy_case_invitations: + show: + heading: Copy an application to your application? + copy_case_searches: + show: + heading: Find the application to copy + search_ref: + label: Find the application to copy + hint: Enter the LAA reference of the case you are searching for + check_passported_answers: show: h1-heading: Check your answers diff --git a/config/routes.rb b/config/routes.rb index a2bdb3038e..8251363ae4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -160,6 +160,9 @@ resources :remove_state_benefits, only: %i[show update] end get :search, on: :collection + resource :copy_case_invitation, only: %i[show update] + resource :copy_case_search, only: %i[show update] + resource :copy_case_confirmation, only: %i[show update] resource :delete, controller: :delete, only: %i[show destroy] resources :proceedings_types, only: %i[index create] resource :has_other_proceedings, only: %i[show update destroy] diff --git a/db/migrate/20231020085303_add_copy_case_to_legal_aid_application.rb b/db/migrate/20231020085303_add_copy_case_to_legal_aid_application.rb new file mode 100644 index 0000000000..0f1c297564 --- /dev/null +++ b/db/migrate/20231020085303_add_copy_case_to_legal_aid_application.rb @@ -0,0 +1,5 @@ +class AddCopyCaseToLegalAidApplication < ActiveRecord::Migration[7.0] + def change + add_column :legal_aid_applications, :copy_case, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 9e60db7c3a..274c713fcd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_09_145448) do +ActiveRecord::Schema[7.0].define(version: 2023_10_20_085303) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -602,6 +602,7 @@ t.decimal "substantive_cost_requested" t.string "substantive_cost_reasons" t.boolean "applicant_in_receipt_of_housing_benefit" + t.boolean "copy_case" t.index ["applicant_id"], name: "index_legal_aid_applications_on_applicant_id" t.index ["application_ref"], name: "index_legal_aid_applications_on_application_ref", unique: true t.index ["discarded_at"], name: "index_legal_aid_applications_on_discarded_at" diff --git a/spec/services/copy_case/cloner_service_spec.rb b/spec/services/copy_case/cloner_service_spec.rb new file mode 100644 index 0000000000..256e88e17a --- /dev/null +++ b/spec/services/copy_case/cloner_service_spec.rb @@ -0,0 +1,51 @@ +require "rails_helper" + +RSpec.describe CopyCase::ClonerService do + subject(:instance) { described_class.new(target, source) } + + describe "#call" do + subject(:call) { instance.call } + + let(:target) { create(:legal_aid_application, :with_applicant) } + let(:source) { create(:legal_aid_application, :with_proceedings) } + + it "copies proceedings" do + expect { call } + .to change { target.reload.proceedings.count } + .from(0) + .to(1) + + source_proceeding = source.proceedings.first + copy_of_proceeding = target.proceedings.first + expected_attributes = source_proceeding + .attributes + .except("id", + "legal_aid_application_id", + "proceeding_case_id", + "created_at", + "updated_at") + + expect(copy_of_proceeding) + .to have_attributes(**expected_attributes) + end + + context "when source has a proceeding with nested scope limitations" do + it "copies proceedings scope limitations" do + expect { call } + .to change { target.reload.proceedings.first&.scope_limitations&.count } + .from(nil) + .to(2) + + source_scope_limitations = source.proceedings.first.scope_limitations.map do |scl| + scl.attributes.except("id", "proceeding_id", "created_at", "updated_at") + end + + target_scope_limitations = target.proceedings.first.scope_limitations.map do |scl| + scl.attributes.except("id", "proceeding_id", "created_at", "updated_at") + end + + expect(target_scope_limitations).to match_array(source_scope_limitations) + end + end + end +end