From 055b0ba244c8d354df9ea94f3ac3bcae4d11e11c Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Wed, 23 Nov 2022 11:20:12 +0000 Subject: [PATCH 01/19] set env var APP_HOST for review apps (#1021) * set env var APP_HOST for review apps * rename incorrectly named config file --- .github/workflows/review_pipeline.yml | 1 + config/environments/{review_app.rb => review.rb} | 0 2 files changed, 1 insertion(+) rename config/environments/{review_app.rb => review.rb} (100%) diff --git a/.github/workflows/review_pipeline.yml b/.github/workflows/review_pipeline.yml index 1ccd2717c..90d7b3b44 100644 --- a/.github/workflows/review_pipeline.yml +++ b/.github/workflows/review_pipeline.yml @@ -120,6 +120,7 @@ jobs: cf set-env $APP_NAME S3_CONFIG $S3_CONFIG cf set-env $APP_NAME CSV_DOWNLOAD_PAAS_INSTANCE $CSV_DOWNLOAD_PAAS_INSTANCE cf set-env $APP_NAME SENTRY_DSN $SENTRY_DSN + cf set-env $APP_NAME APP_HOST "https://dluhc-core-review-${{ github.event.pull_request.number }}.london.cloudapps.digital" - name: Bind postgres service env: diff --git a/config/environments/review_app.rb b/config/environments/review.rb similarity index 100% rename from config/environments/review_app.rb rename to config/environments/review.rb From c0541b054b442ef02b323b7285b52f866d082333 Mon Sep 17 00:00:00 2001 From: Jack S <113976590+bibblobcode@users.noreply.github.com> Date: Wed, 23 Nov 2022 13:13:40 +0000 Subject: [PATCH 02/19] [CLDC-20] Address more ACs (#1020) * Bugfix provider_type on nil * Do not infer managing org * Always show 'your org' to non support users * Never show created_by_id in CYA * Show HousingProvider in CYA when not inferred * Always show ManagingOrganisation * Spec fix * Show ManagingOrganisation if managing agents >=1 * typo --- .../lettings_log_variables.rb | 4 +- .../lettings/pages/managing_organisation.rb | 9 +-- .../form/lettings/questions/created_by_id.rb | 4 +- .../lettings/questions/housing_provider.rb | 11 +++- .../questions/managing_organisation.rb | 8 +-- spec/features/lettings_log_spec.rb | 6 +- .../pages/managing_organisation_spec.rb | 48 ++-------------- .../lettings/questions/created_by_id_spec.rb | 16 +----- .../questions/housing_provider_spec.rb | 56 ++++++++++++++----- .../questions/managing_organisation_spec.rb | 50 +++++++++-------- 10 files changed, 94 insertions(+), 118 deletions(-) diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index c76d5c2d9..1b1817ec3 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -49,8 +49,8 @@ module DerivedVariables::LettingsLogVariables self.waityear = 2 if is_general_needs? # fixed term - self.prevten = 32 if managing_organisation.provider_type == "PRP" - self.prevten = 30 if managing_organisation.provider_type == "LA" + self.prevten = 32 if managing_organisation&.provider_type == "PRP" + self.prevten = 30 if managing_organisation&.provider_type == "LA" end end diff --git a/app/models/form/lettings/pages/managing_organisation.rb b/app/models/form/lettings/pages/managing_organisation.rb index fc2ef5e36..bc8534999 100644 --- a/app/models/form/lettings/pages/managing_organisation.rb +++ b/app/models/form/lettings/pages/managing_organisation.rb @@ -25,13 +25,6 @@ class Form::Lettings::Pages::ManagingOrganisation < ::Form::Page return false unless organisation return true unless organisation.holds_own_stock? - managing_agents = organisation.managing_agents - - return false if managing_agents.count.zero? - return true if managing_agents.count > 1 - - log.update!(managing_organisation: managing_agents.first) - - false + organisation.managing_agents.count >= 1 end end diff --git a/app/models/form/lettings/questions/created_by_id.rb b/app/models/form/lettings/questions/created_by_id.rb index a120e72a8..d5921e935 100644 --- a/app/models/form/lettings/questions/created_by_id.rb +++ b/app/models/form/lettings/questions/created_by_id.rb @@ -35,8 +35,8 @@ class Form::Lettings::Questions::CreatedById < ::Form::Question answer_options[value] end - def hidden_in_check_answers?(_log, current_user) - !current_user.support? + def hidden_in_check_answers?(_log, _current_user) + true end def derived? diff --git a/app/models/form/lettings/questions/housing_provider.rb b/app/models/form/lettings/questions/housing_provider.rb index 8d8db24b6..f46ea1632 100644 --- a/app/models/form/lettings/questions/housing_provider.rb +++ b/app/models/form/lettings/questions/housing_provider.rb @@ -42,10 +42,15 @@ class Form::Lettings::Questions::HousingProvider < ::Form::Question def hidden_in_check_answers?(_log, user = nil) @current_user = user - return false unless @current_user - return false if @current_user.support? + return false if current_user.support? - housing_providers_answer_options.count < 2 + housing_providers = current_user.organisation.housing_providers + + if current_user.organisation.holds_own_stock? + housing_providers.count.zero? + else + housing_providers.count <= 1 + end end def enabled diff --git a/app/models/form/lettings/questions/managing_organisation.rb b/app/models/form/lettings/questions/managing_organisation.rb index 42662202c..82ae2aabd 100644 --- a/app/models/form/lettings/questions/managing_organisation.rb +++ b/app/models/form/lettings/questions/managing_organisation.rb @@ -21,7 +21,7 @@ class Form::Lettings::Questions::ManagingOrganisation < ::Form::Question if log.owning_organisation.holds_own_stock? opts[log.owning_organisation.id] = "#{log.owning_organisation.name} (Owning organisation)" end - elsif current_user.organisation.holds_own_stock? + else opts[current_user.organisation.id] = "#{current_user.organisation.name} (Your organisation)" end @@ -47,11 +47,7 @@ class Form::Lettings::Questions::ManagingOrganisation < ::Form::Question def hidden_in_check_answers?(_log, user = nil) @current_user = user - - return false unless @current_user - return false if @current_user.support? - - managing_organisations_answer_options.count < 2 + @current_user.nil? end def enabled diff --git a/spec/features/lettings_log_spec.rb b/spec/features/lettings_log_spec.rb index 7836713e0..6d09b205a 100644 --- a/spec/features/lettings_log_spec.rb +++ b/spec/features/lettings_log_spec.rb @@ -89,8 +89,7 @@ RSpec.describe "Lettings Log Features" do log_id = page.current_path.scan(/\d/).join visit("lettings-logs/#{log_id}/setup/check-answers") expect(page).to have_content("Housing provider #{support_user.organisation.name}") - expect(page).to have_content("Log owner #{support_user.name}") - expect(page).to have_content("You have answered 3 of 9 questions") + expect(page).to have_content("You have answered 2 of 8 questions") end end end @@ -119,8 +118,7 @@ RSpec.describe "Lettings Log Features" do expect(page).to have_current_path("/lettings-logs/#{log_id}/needs-type") visit("lettings-logs/#{log_id}/setup/check-answers") expect(page).not_to have_content("Owning organisation #{user.organisation.name}") - expect(page).not_to have_content("User #{user.name}") - expect(page).to have_content("You have answered 0 of 6 questions") + expect(page).not_to have_content("Log owner #{user.name}") end end diff --git a/spec/models/form/lettings/pages/managing_organisation_spec.rb b/spec/models/form/lettings/pages/managing_organisation_spec.rb index 4a44aa25d..65c2589c6 100644 --- a/spec/models/form/lettings/pages/managing_organisation_spec.rb +++ b/spec/models/form/lettings/pages/managing_organisation_spec.rb @@ -40,10 +40,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do it "is not shown" do expect(page.routed_to?(log, nil)).to eq(false) end - - it "does not update managing_organisation_id" do - expect { page.routed_to?(log, nil) }.not_to change(log.reload, :managing_organisation) - end end context "when support" do @@ -56,10 +52,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do it "is shown" do expect(page.routed_to?(log, user)).to eq(true) end - - it "does not update managing_organisation_id" do - expect { page.routed_to?(log, user) }.not_to change(log.reload, :managing_organisation) - end end context "when owning_organisation not set" do @@ -69,10 +61,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do it "is not shown" do expect(page.routed_to?(log, user)).to eq(false) end - - it "does not update managing_organisation_id" do - expect { page.routed_to?(log, user) }.not_to change(log.reload, :managing_organisation) - end end context "when holds own stock" do @@ -84,10 +72,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do it "is not shown" do expect(page.routed_to?(log, user)).to eq(false) end - - it "does not update managing_organisation_id" do - expect { page.routed_to?(log, user) }.not_to change(log.reload, :managing_organisation) - end end context "with >1 managing_agents" do @@ -99,10 +83,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do it "is shown" do expect(page.routed_to?(log, user)).to eq(true) end - - it "does not update managing_organisation_id" do - expect { page.routed_to?(log, user) }.not_to change(log.reload, :managing_organisation) - end end context "with 1 managing_agents" do @@ -117,12 +97,8 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do ) end - it "is not shown" do - expect(page.routed_to?(log, user)).to eq(false) - end - - it "updates managing_organisation_id" do - expect { page.routed_to?(log, user) }.to change(log.reload, :managing_organisation).to(managing_agent) + it "is shown" do + expect(page.routed_to?(log, user)).to eq(true) end end end @@ -137,10 +113,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do it "is shown" do expect(page.routed_to?(log, user)).to eq(true) end - - it "does not update managing_organisation_id" do - expect { page.routed_to?(log, user) }.not_to change(log.reload, :managing_organisation) - end end context "when holds own stock" do @@ -152,10 +124,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do it "is not shown" do expect(page.routed_to?(log, user)).to eq(false) end - - it "does not update managing_organisation_id" do - expect { page.routed_to?(log, user) }.not_to change(log.reload, :managing_organisation) - end end context "with >1 managing_agents" do @@ -167,10 +135,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do it "is shown" do expect(page.routed_to?(log, user)).to eq(true) end - - it "does not update managing_organisation_id" do - expect { page.routed_to?(log, user) }.not_to change(log.reload, :managing_organisation) - end end context "with 1 managing_agents" do @@ -185,12 +149,8 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do ) end - it "is not shown" do - expect(page.routed_to?(log, user)).to eq(false) - end - - it "updates managing_organisation_id" do - expect { page.routed_to?(log, user) }.to change(log.reload, :managing_organisation).to(managing_agent) + it "is shown" do + expect(page.routed_to?(log, user)).to eq(true) end end end diff --git a/spec/models/form/lettings/questions/created_by_id_spec.rb b/spec/models/form/lettings/questions/created_by_id_spec.rb index 04fac20fb..a5e60ad2b 100644 --- a/spec/models/form/lettings/questions/created_by_id_spec.rb +++ b/spec/models/form/lettings/questions/created_by_id_spec.rb @@ -51,20 +51,8 @@ RSpec.describe Form::Lettings::Questions::CreatedById, type: :model do expect(question.derived?).to be true end - context "when the current user is support" do - let(:support_user) { build(:user, :support) } - - it "is shown in check answers" do - expect(question.hidden_in_check_answers?(nil, support_user)).to be false - end - end - - context "when the current user is not support" do - let(:user) { build(:user) } - - it "is not shown in check answers" do - expect(question.hidden_in_check_answers?(nil, user)).to be true - end + it "is not shown in check answers" do + expect(question.hidden_in_check_answers?(nil, user_1)).to be true end context "when the owning organisation is already set" do diff --git a/spec/models/form/lettings/questions/housing_provider_spec.rb b/spec/models/form/lettings/questions/housing_provider_spec.rb index 073c59e37..5363a0227 100644 --- a/spec/models/form/lettings/questions/housing_provider_spec.rb +++ b/spec/models/form/lettings/questions/housing_provider_spec.rb @@ -83,34 +83,64 @@ RSpec.describe Form::Lettings::Questions::HousingProvider, type: :model do end describe "#hidden_in_check_answers?" do - context "when housing providers < 2" do - context "when not support user" do - let(:user) { create(:user) } + context "when support" do + let(:user) { create(:user, :support) } + + it "is not hidden in check answers" do + expect(question.hidden_in_check_answers?(nil, user)).to be false + end + end + + context "when org holds own stock", :aggregate_failures do + let(:user) { create(:user, :data_coordinator, organisation: create(:organisation, holds_own_stock: true)) } + + context "when housing providers == 0" do + before do + user.organisation.housing_providers.delete_all + end it "is hidden in check answers" do + expect(user.organisation.housing_providers.count).to eq(0) expect(question.hidden_in_check_answers?(nil, user)).to be true end end - context "when support" do - let(:user) { create(:user, :support) } + context "when housing providers != 0" do + before do + create(:organisation_relationship, :owning, child_organisation: user.organisation) + end - it "is not hiddes in check answers" do + it "is visible in check answers" do + expect(user.organisation.housing_providers.count).to eq(1) expect(question.hidden_in_check_answers?(nil, user)).to be false end end end - context "when housing providers >= 2" do - let(:user) { create(:user) } + context "when org does not hold own stock", :aggregate_failures do + let(:user) { create(:user, :data_coordinator, organisation: create(:organisation, holds_own_stock: false)) } - before do - create(:organisation_relationship, :owning, child_organisation: user.organisation) - create(:organisation_relationship, :owning, child_organisation: user.organisation) + context "when housing providers <= 1" do + before do + create(:organisation_relationship, :owning, child_organisation: user.organisation) + end + + it "is hidden in check answers" do + expect(user.organisation.housing_providers.count).to eq(1) + expect(question.hidden_in_check_answers?(nil, user)).to be true + end end - it "is not hidden in check answers" do - expect(question.hidden_in_check_answers?(nil, user)).to be false + context "when housing providers >= 2" do + before do + create(:organisation_relationship, :owning, child_organisation: user.organisation) + create(:organisation_relationship, :owning, child_organisation: user.organisation) + end + + it "is visible in check answers" do + expect(user.organisation.housing_providers.count).to eq(2) + expect(question.hidden_in_check_answers?(nil, user)).to be false + end end end end diff --git a/spec/models/form/lettings/questions/managing_organisation_spec.rb b/spec/models/form/lettings/questions/managing_organisation_spec.rb index f80099d67..e045601bf 100644 --- a/spec/models/form/lettings/questions/managing_organisation_spec.rb +++ b/spec/models/form/lettings/questions/managing_organisation_spec.rb @@ -73,6 +73,27 @@ RSpec.describe Form::Lettings::Questions::ManagingOrganisation, type: :model do end end + context "when user not support and does not own stock" do + let(:user) { create(:user, :data_coordinator, organisation: create(:organisation, holds_own_stock: false)) } + + let(:log) { create(:lettings_log) } + let!(:org_rel1) { create(:organisation_relationship, :managing, parent_organisation: user.organisation) } + let!(:org_rel2) { create(:organisation_relationship, :managing, parent_organisation: user.organisation) } + + let(:options) do + { + "" => "Select an option", + user.organisation.id => "#{user.organisation.name} (Your organisation)", + org_rel1.child_organisation.id => org_rel1.child_organisation.name, + org_rel2.child_organisation.id => org_rel2.child_organisation.name, + } + end + + it "shows managing agents with own org at the top" do + expect(question.displayed_answer_options(log, user)).to eq(options) + end + end + context "when support user and org does not own own stock" do let(:user) { create(:user, :support) } let(:log_owning_org) { create(:organisation, holds_own_stock: false) } @@ -121,34 +142,19 @@ RSpec.describe Form::Lettings::Questions::ManagingOrganisation, type: :model do end describe "#hidden_in_check_answers?" do - context "when housing providers < 2" do - context "when not support user" do - let(:user) { create(:user) } - - it "is hidden in check answers" do - expect(question.hidden_in_check_answers?(nil, user)).to be true - end - end - - context "when support" do - let(:user) { create(:user, :support) } + context "when user present" do + let(:user) { create(:user) } - it "is not hiddes in check answers" do - expect(question.hidden_in_check_answers?(nil, user)).to be false - end + it "is hidden in check answers" do + expect(question.hidden_in_check_answers?(nil, user)).to be false end end - context "when managing agents >= 2" do - let(:user) { create(:user) } - - before do - create(:organisation_relationship, :managing, parent_organisation: user.organisation) - create(:organisation_relationship, :managing, parent_organisation: user.organisation) - end + context "when user not provided" do + let(:user) { create(:user, :support) } it "is not hidden in check answers" do - expect(question.hidden_in_check_answers?(nil, user)).to be false + expect(question.hidden_in_check_answers?(nil)).to be true end end end From 879cdea47fcf2ebc51cb6f2255cbcb5da619e193 Mon Sep 17 00:00:00 2001 From: natdeanlewissoftwire <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Thu, 24 Nov 2022 09:49:59 +0000 Subject: [PATCH 03/19] Cldc 1672 scheme reactivation (#1023) * feat: add scheme reactivation behaviour * test: add tests * refactor: linting * fix: find deactivation periods by scheme/location ids rather than just the first * feat: add activating_soon status to location (not to schemes as they have no startdate field) * feat: fix logic and add tests fo activating soon * fix: check for startdate presence --- app/controllers/locations_controller.rb | 4 +- app/controllers/schemes_controller.rb | 47 +++-- app/helpers/locations_helper.rb | 28 +-- app/helpers/schemes_helper.rb | 42 +++- app/helpers/tag_helper.rb | 2 + app/helpers/toggle_active_scheme_helper.rb | 17 ++ app/models/location.rb | 1 + app/models/scheme.rb | 17 +- app/models/scheme_deactivation_period.rb | 35 +++- app/views/schemes/deactivate_confirm.html.erb | 4 +- app/views/schemes/show.html.erb | 4 +- app/views/schemes/toggle_active.html.erb | 18 +- config/locales/en.yml | 22 ++- config/routes.rb | 2 + spec/factories/scheme_deactivation_period.rb | 1 + spec/helpers/locations_helper_spec.rb | 48 ++--- spec/helpers/schemes_helper_spec.rb | 182 +++++++++++++++++- spec/models/location_spec.rb | 14 +- spec/models/scheme_spec.rb | 20 ++ spec/requests/schemes_controller_spec.rb | 17 +- 20 files changed, 422 insertions(+), 103 deletions(-) create mode 100644 app/helpers/toggle_active_scheme_helper.rb diff --git a/app/controllers/locations_controller.rb b/app/controllers/locations_controller.rb index 766501260..b3f3ab203 100644 --- a/app/controllers/locations_controller.rb +++ b/app/controllers/locations_controller.rb @@ -50,12 +50,12 @@ class LocationsController < ApplicationController end def new_reactivation - @location_deactivation_period = LocationDeactivationPeriod.deactivations_without_reactivation.first + @location_deactivation_period = @location.location_deactivation_periods.deactivations_without_reactivation.first render "toggle_active", locals: { action: "reactivate" } end def reactivate - @location_deactivation_period = LocationDeactivationPeriod.deactivations_without_reactivation.first + @location_deactivation_period = @location.location_deactivation_periods.deactivations_without_reactivation.first @location_deactivation_period.reactivation_date = toggle_date("reactivation_date") @location_deactivation_period.reactivation_date_type = params[:location_deactivation_period][:reactivation_date_type] diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index 1a06d9d41..415db5c7a 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -27,10 +27,10 @@ class SchemesController < ApplicationController if params[:scheme_deactivation_period].blank? render "toggle_active", locals: { action: "deactivate" } else - @scheme_deactivation_period.deactivation_date = deactivation_date + @scheme_deactivation_period.deactivation_date = toggle_date("deactivation_date") @scheme_deactivation_period.deactivation_date_type = params[:scheme_deactivation_period][:deactivation_date_type] @scheme_deactivation_period.scheme = @scheme - if @scheme_deactivation_period.validate + if @scheme_deactivation_period.valid? redirect_to scheme_deactivate_confirm_path(@scheme, deactivation_date: @scheme_deactivation_period.deactivation_date, deactivation_date_type: @scheme_deactivation_period.deactivation_date_type) else render "toggle_active", locals: { action: "deactivate" }, status: :unprocessable_entity @@ -50,10 +50,25 @@ class SchemesController < ApplicationController redirect_to scheme_details_path(@scheme) end - def reactivate + def new_reactivation + @scheme_deactivation_period = @scheme.scheme_deactivation_periods.deactivations_without_reactivation.first render "toggle_active", locals: { action: "reactivate" } end + def reactivate + @scheme_deactivation_period = @scheme.scheme_deactivation_periods.deactivations_without_reactivation.first + + @scheme_deactivation_period.reactivation_date = toggle_date("reactivation_date") + @scheme_deactivation_period.reactivation_date_type = params[:scheme_deactivation_period][:reactivation_date_type] + + if @scheme_deactivation_period.update(reactivation_date: toggle_date("reactivation_date")) + flash[:notice] = reactivate_success_notice + redirect_to scheme_details_path(@scheme) + else + render "toggle_active", locals: { action: "reactivate" }, status: :unprocessable_entity + end + end + def new @scheme = Scheme.new end @@ -239,8 +254,7 @@ private :support_type, :arrangement_type, :intended_stay, - :confirmed, - :deactivation_date) + :confirmed) if arrangement_type_changed_to_different_org?(required_params) required_params[:managing_organisation_id] = nil @@ -303,18 +317,27 @@ private end end - def deactivation_date + def reactivate_success_notice + case @scheme.status + when :active + "#{@scheme.service_name} has been reactivated" + when :reactivating_soon + "#{@scheme.service_name} will reactivate on #{toggle_date('reactivation_date').to_time.to_formatted_s(:govuk_date)}" + end + end + + def toggle_date(key) if params[:scheme_deactivation_period].blank? return - elsif params[:scheme_deactivation_period][:deactivation_date_type] == "default" + elsif params[:scheme_deactivation_period]["#{key}_type".to_sym] == "default" return FormHandler.instance.current_collection_start_date - elsif params[:scheme_deactivation_period][:deactivation_date].present? - return params[:scheme_deactivation_period][:deactivation_date] + elsif params[:scheme_deactivation_period][key.to_sym].present? + return params[:scheme_deactivation_period][key.to_sym] end - day = params[:scheme_deactivation_period]["deactivation_date(3i)"] - month = params[:scheme_deactivation_period]["deactivation_date(2i)"] - year = params[:scheme_deactivation_period]["deactivation_date(1i)"] + day = params[:scheme_deactivation_period]["#{key}(3i)"] + month = params[:scheme_deactivation_period]["#{key}(2i)"] + year = params[:scheme_deactivation_period]["#{key}(1i)"] return nil if [day, month, year].any?(&:blank?) Time.zone.local(year.to_i, month.to_i, day.to_i) if Date.valid_date?(year.to_i, month.to_i, day.to_i) diff --git a/app/helpers/locations_helper.rb b/app/helpers/locations_helper.rb index ca04826ba..8d56226a9 100644 --- a/app/helpers/locations_helper.rb +++ b/app/helpers/locations_helper.rb @@ -42,22 +42,9 @@ module LocationsHelper base_attributes end - ActivePeriod = Struct.new(:from, :to) - def active_periods(location) - periods = [ActivePeriod.new(location.available_from, nil)] - - sorted_deactivation_periods = remove_nested_periods(location.location_deactivation_periods.sort_by(&:deactivation_date)) - sorted_deactivation_periods.each do |deactivation| - periods.last.to = deactivation.deactivation_date - periods << ActivePeriod.new(deactivation.reactivation_date, nil) - end - - remove_overlapping_and_empty_periods(periods) - end - def location_availability(location) availability = "" - active_periods(location).each do |period| + location_active_periods(location).each do |period| if period.from.present? availability << "\nActive from #{period.from.to_formatted_s(:govuk_date)}" availability << " to #{(period.to - 1.day).to_formatted_s(:govuk_date)}\nDeactivated on #{period.to.to_formatted_s(:govuk_date)}" if period.to.present? @@ -68,6 +55,19 @@ module LocationsHelper private + ActivePeriod = Struct.new(:from, :to) + def location_active_periods(location) + periods = [ActivePeriod.new(location.available_from, nil)] + + sorted_deactivation_periods = remove_nested_periods(location.location_deactivation_periods.sort_by(&:deactivation_date)) + sorted_deactivation_periods.each do |deactivation| + periods.last.to = deactivation.deactivation_date + periods << ActivePeriod.new(deactivation.reactivation_date, nil) + end + + remove_overlapping_and_empty_periods(periods) + end + def remove_overlapping_and_empty_periods(periods) periods.select { |period| period.from.present? && (period.to.nil? || period.from < period.to) } end diff --git a/app/helpers/schemes_helper.rb b/app/helpers/schemes_helper.rb index 6d12f6675..10a33cab6 100644 --- a/app/helpers/schemes_helper.rb +++ b/app/helpers/schemes_helper.rb @@ -28,11 +28,43 @@ module SchemesHelper end def scheme_availability(scheme) - availability = "Active from #{scheme.available_from.to_formatted_s(:govuk_date)}" - scheme.scheme_deactivation_periods.each do |deactivation| - availability << " to #{(deactivation.deactivation_date - 1.day).to_formatted_s(:govuk_date)}\nDeactivated on #{deactivation.deactivation_date.to_formatted_s(:govuk_date)}" - availability << "\nActive from #{deactivation.reactivation_date.to_formatted_s(:govuk_date)}" if deactivation.reactivation_date.present? + availability = "" + scheme_active_periods(scheme).each do |period| + if period.from.present? + availability << "\nActive from #{period.from.to_formatted_s(:govuk_date)}" + availability << " to #{(period.to - 1.day).to_formatted_s(:govuk_date)}\nDeactivated on #{period.to.to_formatted_s(:govuk_date)}" if period.to.present? + end end - availability + availability.strip + end + +private + + ActivePeriod = Struct.new(:from, :to) + def scheme_active_periods(scheme) + periods = [ActivePeriod.new(scheme.available_from, nil)] + + sorted_deactivation_periods = remove_nested_periods(scheme.scheme_deactivation_periods.sort_by(&:deactivation_date)) + sorted_deactivation_periods.each do |deactivation| + periods.last.to = deactivation.deactivation_date + periods << ActivePeriod.new(deactivation.reactivation_date, nil) + end + + remove_overlapping_and_empty_periods(periods) + end + + def remove_overlapping_and_empty_periods(periods) + periods.select { |period| period.from.present? && (period.to.nil? || period.from < period.to) } + end + + def remove_nested_periods(periods) + periods.select { |inner_period| periods.none? { |outer_period| is_nested?(inner_period, outer_period) } } + end + + def is_nested?(inner, outer) + return false if inner == outer + return false if [inner.deactivation_date, inner.reactivation_date, outer.deactivation_date, outer.reactivation_date].any?(&:blank?) + + [inner.deactivation_date, inner.reactivation_date].all? { |date| date.between?(outer.deactivation_date, outer.reactivation_date) } end end diff --git a/app/helpers/tag_helper.rb b/app/helpers/tag_helper.rb index 6682f97fd..f8b19dd83 100644 --- a/app/helpers/tag_helper.rb +++ b/app/helpers/tag_helper.rb @@ -9,6 +9,7 @@ module TagHelper active: "Active", incomplete: "Incomplete", deactivating_soon: "Deactivating soon", + activating_soon: "Activating soon", reactivating_soon: "Reactivating soon", deactivated: "Deactivated", }.freeze @@ -21,6 +22,7 @@ module TagHelper active: "green", incomplete: "red", deactivating_soon: "yellow", + activating_soon: "blue", reactivating_soon: "blue", deactivated: "grey", }.freeze diff --git a/app/helpers/toggle_active_scheme_helper.rb b/app/helpers/toggle_active_scheme_helper.rb new file mode 100644 index 000000000..9f93939fd --- /dev/null +++ b/app/helpers/toggle_active_scheme_helper.rb @@ -0,0 +1,17 @@ +module ToggleActiveSchemeHelper + def toggle_scheme_form_path(action, scheme) + if action == "deactivate" + scheme_new_deactivation_path(scheme) + else + scheme_reactivate_path(scheme) + end + end + + def date_type_question(action) + action == "deactivate" ? :deactivation_date_type : :reactivation_date_type + end + + def date_question(action) + action == "deactivate" ? :deactivation_date : :reactivation_date + end +end diff --git a/app/models/location.rb b/app/models/location.rb index c05a2351b..46cb1da6c 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -380,6 +380,7 @@ class Location < ApplicationRecord return :deactivated if open_deactivation&.deactivation_date.present? && Time.zone.now >= open_deactivation.deactivation_date return :deactivating_soon if open_deactivation&.deactivation_date.present? && Time.zone.now < open_deactivation.deactivation_date return :reactivating_soon if recent_deactivation&.reactivation_date.present? && Time.zone.now < recent_deactivation.reactivation_date + return :activating_soon if startdate.present? && Time.zone.now < startdate :active end diff --git a/app/models/scheme.rb b/app/models/scheme.rb index 8197a147c..a56a29602 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -210,18 +210,25 @@ class Scheme < ApplicationRecord end def available_from - created_at + [created_at, FormHandler.instance.current_collection_start_date].min end def status - recent_deactivation = scheme_deactivation_periods.deactivations_without_reactivation.first - return :active if recent_deactivation.blank? - return :deactivating_soon if Time.zone.now < recent_deactivation.deactivation_date + open_deactivation = scheme_deactivation_periods.deactivations_without_reactivation.first + recent_deactivation = scheme_deactivation_periods.order("created_at").last - :deactivated + return :deactivated if open_deactivation&.deactivation_date.present? && Time.zone.now >= open_deactivation.deactivation_date + return :deactivating_soon if open_deactivation&.deactivation_date.present? && Time.zone.now < open_deactivation.deactivation_date + return :reactivating_soon if recent_deactivation&.reactivation_date.present? && Time.zone.now < recent_deactivation.reactivation_date + + :active end def active? status == :active end + + def reactivating_soon? + status == :reactivating_soon + end end diff --git a/app/models/scheme_deactivation_period.rb b/app/models/scheme_deactivation_period.rb index f83a68e62..f716cbc32 100644 --- a/app/models/scheme_deactivation_period.rb +++ b/app/models/scheme_deactivation_period.rb @@ -1,15 +1,40 @@ class SchemeDeactivationPeriodValidator < ActiveModel::Validator def validate(record) + scheme = record.scheme + recent_deactivation = scheme.scheme_deactivation_periods.deactivations_without_reactivation.first + if recent_deactivation.present? + validate_reactivation(record, recent_deactivation, scheme) + else + validate_deactivation(record, scheme) + end + end + + def validate_reactivation(record, recent_deactivation, scheme) + if record.reactivation_date.blank? + if record.reactivation_date_type.blank? + record.errors.add(:reactivation_date_type, message: I18n.t("validations.scheme.toggle_date.not_selected")) + elsif record.reactivation_date_type == "other" + record.errors.add(:reactivation_date, message: I18n.t("validations.scheme.toggle_date.invalid")) + end + elsif !record.reactivation_date.between?(scheme.available_from, Time.zone.local(2200, 1, 1)) + record.errors.add(:reactivation_date, message: I18n.t("validations.scheme.toggle_date.out_of_range", date: scheme.available_from.to_formatted_s(:govuk_date))) + elsif record.reactivation_date < recent_deactivation.deactivation_date + record.errors.add(:reactivation_date, message: I18n.t("validations.scheme.reactivation.before_deactivation", date: recent_deactivation.deactivation_date.to_formatted_s(:govuk_date))) + end + end + + def validate_deactivation(record, scheme) if record.deactivation_date.blank? if record.deactivation_date_type.blank? - record.errors.add(:deactivation_date_type, message: I18n.t("validations.scheme.deactivation_date.not_selected")) + record.errors.add(:deactivation_date_type, message: I18n.t("validations.scheme.toggle_date.not_selected")) elsif record.deactivation_date_type == "other" - record.errors.add(:deactivation_date, message: I18n.t("validations.scheme.deactivation_date.invalid")) + record.errors.add(:deactivation_date, message: I18n.t("validations.scheme.toggle_date.invalid")) end + elsif scheme.scheme_deactivation_periods.any? { |period| period.reactivation_date.present? && record.deactivation_date.between?(period.deactivation_date, period.reactivation_date - 1.day) } + record.errors.add(:deactivation_date, message: I18n.t("validations.scheme.deactivation.during_deactivated_period")) else - collection_start_date = FormHandler.instance.current_collection_start_date - unless record.deactivation_date.between?(collection_start_date, Time.zone.local(2200, 1, 1)) - record.errors.add(:deactivation_date, message: I18n.t("validations.scheme.deactivation_date.out_of_range", date: collection_start_date.to_formatted_s(:govuk_date))) + unless record.deactivation_date.between?(scheme.available_from, Time.zone.local(2200, 1, 1)) + record.errors.add(:deactivation_date, message: I18n.t("validations.scheme.toggle_date.out_of_range", date: scheme.available_from.to_formatted_s(:govuk_date))) end end end diff --git a/app/views/schemes/deactivate_confirm.html.erb b/app/views/schemes/deactivate_confirm.html.erb index 0cfd454c8..92479d93b 100644 --- a/app/views/schemes/deactivate_confirm.html.erb +++ b/app/views/schemes/deactivate_confirm.html.erb @@ -1,5 +1,3 @@ -<% title = "Deactivate #{@scheme.service_name}" %> -<% content_for :title, title %> <%= form_with model: @scheme_deactivation_period, url: scheme_deactivate_path(@scheme), method: "patch", local: true do |f| %> <% content_for :before_content do %> <%= govuk_back_link(href: :back) %> @@ -8,7 +6,7 @@ <%= @scheme.service_name %> This change will affect <%= @scheme.lettings_logs.count %> logs - <%= govuk_warning_text text: I18n.t("warnings.scheme.deactivation.review_logs") %> + <%= govuk_warning_text text: I18n.t("warnings.scheme.deactivate.review_logs") %> <%= f.hidden_field :confirm, value: true %> <%= f.hidden_field :deactivation_date, value: @deactivation_date %> <%= f.hidden_field :deactivation_date_type, value: @deactivation_date_type %> diff --git a/app/views/schemes/show.html.erb b/app/views/schemes/show.html.erb index 2c01c06f1..e0b229af4 100644 --- a/app/views/schemes/show.html.erb +++ b/app/views/schemes/show.html.erb @@ -26,9 +26,9 @@ <% end %> <% if FeatureToggle.scheme_toggle_enabled? %> - <% if @scheme.active? %> + <% if @scheme.active? || @scheme.reactivating_soon? %> <%= govuk_button_link_to "Deactivate this scheme", scheme_new_deactivation_path(@scheme), warning: true %> <% else %> - <%= govuk_button_link_to "Reactivate this scheme", scheme_reactivate_path(@scheme) %> + <%= govuk_button_link_to "Reactivate this scheme", scheme_new_reactivation_path(@scheme) %> <% end %> <% end %> diff --git a/app/views/schemes/toggle_active.html.erb b/app/views/schemes/toggle_active.html.erb index fbb3ebc5f..1b7507375 100644 --- a/app/views/schemes/toggle_active.html.erb +++ b/app/views/schemes/toggle_active.html.erb @@ -1,29 +1,31 @@ <% title = "#{action.humanize} #{@scheme.service_name}" %> <% content_for :title, title %> + <% content_for :before_content do %> <%= govuk_back_link( text: "Back", href: scheme_details_path(@scheme), ) %> <% end %> -<%= form_with model: @scheme_deactivation_period, url: scheme_new_deactivation_path(@scheme), method: "patch", local: true do |f| %> + +<%= form_with model: @scheme_deactivation_period, url: toggle_scheme_form_path(action, @scheme), method: "patch", local: true do |f| %>
<% collection_start_date = FormHandler.instance.current_collection_start_date %> <%= f.govuk_error_summary %> - <%= f.govuk_radio_buttons_fieldset :deactivation_date_type, - legend: { text: I18n.t("questions.scheme.deactivation.apply_from") }, + <%= f.govuk_radio_buttons_fieldset date_type_question(action), + legend: { text: I18n.t("questions.scheme.toggle_active.apply_from") }, caption: { text: title }, - hint: { text: I18n.t("hints.scheme.deactivation", date: collection_start_date.to_formatted_s(:govuk_date)) } do %> - <%= govuk_warning_text text: I18n.t("warnings.scheme.deactivation.existing_logs") %> - <%= f.govuk_radio_button :deactivation_date_type, + hint: { text: I18n.t("hints.scheme.toggle_active", date: collection_start_date.to_formatted_s(:govuk_date)) } do %> + <%= govuk_warning_text text: I18n.t("warnings.scheme.#{action}.existing_logs") %> + <%= f.govuk_radio_button date_type_question(action), "default", label: { text: "From the start of the current collection period (#{collection_start_date.to_formatted_s(:govuk_date)})" } %> - <%= f.govuk_radio_button :deactivation_date_type, + <%= f.govuk_radio_button date_type_question(action), "other", label: { text: "For tenancies starting after a certain date" }, **basic_conditional_html_attributes({ "deactivation_date" => %w[other] }, "scheme") do %> - <%= f.govuk_date_field :deactivation_date, + <%= f.govuk_date_field date_question(action), legend: { text: "Date", size: "m" }, hint: { text: "For example, 27 3 2022" }, width: 20 %> diff --git a/config/locales/en.yml b/config/locales/en.yml index f839ccf7e..5a8843e46 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -313,10 +313,16 @@ en: missing: "You must show the DLUHC privacy notice to the tenant before you can submit this log." scheme: - deactivation_date: + toggle_date: not_selected: "Select one of the options" invalid: "Enter a valid day, month and year" out_of_range: "The date must be on or after the %{date}" + reactivation: + before_deactivation: "This scheme was deactivated on %{date}. The reactivation date must be on or after deactivation date" + deactivation: + during_deactivated_period: "The scheme is already deactivated during this date, please enter a different date" + + location: toggle_date: @@ -324,8 +330,8 @@ en: invalid: "Enter a valid day, month and year" out_of_range: "The date must be on or after the %{date}" reactivation: - before_deactivation: "This location was deactivated on %{date}\nThe reactivation date must be on or after deactivation date" - deactivation: + before_deactivation: "This location was deactivated on %{date}. The reactivation date must be on or after deactivation date" + deactivation: during_deactivated_period: "The location is already deactivated during this date, please enter a different date" soft_validations: @@ -381,7 +387,7 @@ en: toggle_active: apply_from: "When should this change apply?" scheme: - deactivation: + toggle_active: apply_from: "When should this change apply?" descriptions: location: @@ -397,19 +403,21 @@ en: units: "A unit can be a bedroom in a shared house or flat, or a house with 4 bedrooms. Do not include bedrooms used for wardens, managers, volunteers or sleep-in staff." toggle_active: "If the date is before %{date}, select ‘From the start of the current collection period’ because the previous period has now closed." scheme: - deactivation: "If the date is before %{date}, select ‘From the start of the current collection period’ because the previous period has now closed." + toggle_active: "If the date is before %{date}, select ‘From the start of the current collection period’ because the previous period has now closed." warnings: location: deactivate: existing_logs: "It will not be possible to add logs with this location if their tenancy start date is on or after the date you enter. Any existing logs may be affected." review_logs: "Your data providers will need to review these logs and answer a few questions again. We’ll email each log creator with a list of logs that need updating." - reactivate: + reactivate: existing_logs: "You’ll be able to add logs with this location if their tenancy start date is on or after the date you enter." scheme: - deactivation: + deactivate: existing_logs: "It will not be possible to add logs with this scheme if their tenancy start date is on or after the date you enter. Any existing logs may be affected." review_logs: "Your data providers will need to review these logs and answer a few questions again. We’ll email each log creator with a list of logs that need updating." + reactivate: + existing_logs: "You’ll be able to add logs with this scheme if their tenancy start date is on or after the date you enter." test: one_argument: "This is based on the tenant’s work situation: %{ecstat1}" diff --git a/config/routes.rb b/config/routes.rb index 98d7151b1..ce005902f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,8 +52,10 @@ Rails.application.routes.draw do get "new-deactivation", to: "schemes#new_deactivation" get "deactivate-confirm", to: "schemes#deactivate_confirm" get "reactivate", to: "schemes#reactivate" + get "new-reactivation", to: "schemes#new_reactivation" patch "new-deactivation", to: "schemes#new_deactivation" patch "deactivate", to: "schemes#deactivate" + patch "reactivate", to: "schemes#reactivate" resources :locations do get "edit-name", to: "locations#edit_name" diff --git a/spec/factories/scheme_deactivation_period.rb b/spec/factories/scheme_deactivation_period.rb index c073bc68a..95e33d9ce 100644 --- a/spec/factories/scheme_deactivation_period.rb +++ b/spec/factories/scheme_deactivation_period.rb @@ -1,5 +1,6 @@ FactoryBot.define do factory :scheme_deactivation_period do + deactivation_date { Time.zone.local(2022, 4, 1) } reactivation_date { nil } end end diff --git a/spec/helpers/locations_helper_spec.rb b/spec/helpers/locations_helper_spec.rb index bccacba3c..4d9340542 100644 --- a/spec/helpers/locations_helper_spec.rb +++ b/spec/helpers/locations_helper_spec.rb @@ -59,8 +59,8 @@ RSpec.describe LocationsHelper do end it "returns one active period without to date" do - expect(active_periods(location).count).to eq(1) - expect(active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: nil) + expect(location_active_periods(location).count).to eq(1) + expect(location_active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: nil) end it "ignores reactivations that were deactivated on the same day" do @@ -68,8 +68,8 @@ RSpec.describe LocationsHelper do FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), location:) location.reload - expect(active_periods(location).count).to eq(1) - expect(active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + expect(location_active_periods(location).count).to eq(1) + expect(location_active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) end it "returns sequential non reactivated active periods" do @@ -77,19 +77,19 @@ RSpec.describe LocationsHelper do FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 7, 6), location:) location.reload - expect(active_periods(location).count).to eq(2) - expect(active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) - expect(active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 6, 4), to: Time.zone.local(2022, 7, 6)) + expect(location_active_periods(location).count).to eq(2) + expect(location_active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + expect(location_active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 6, 4), to: Time.zone.local(2022, 7, 6)) end it "returns sequential reactivated active periods" do FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 6, 4), location:) FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 7, 6), reactivation_date: Time.zone.local(2022, 8, 5), location:) location.reload - expect(active_periods(location).count).to eq(3) - expect(active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) - expect(active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 6, 4), to: Time.zone.local(2022, 7, 6)) - expect(active_periods(location).third).to have_attributes(from: Time.zone.local(2022, 8, 5), to: nil) + expect(location_active_periods(location).count).to eq(3) + expect(location_active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + expect(location_active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 6, 4), to: Time.zone.local(2022, 7, 6)) + expect(location_active_periods(location).third).to have_attributes(from: Time.zone.local(2022, 8, 5), to: nil) end it "returns non sequential non reactivated active periods" do @@ -97,19 +97,19 @@ RSpec.describe LocationsHelper do FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: nil, location:) location.reload - expect(active_periods(location).count).to eq(2) - expect(active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) - expect(active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 8, 5), to: nil) + expect(location_active_periods(location).count).to eq(2) + expect(location_active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + expect(location_active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 8, 5), to: nil) end it "returns non sequential reactivated active periods" do FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 7, 6), reactivation_date: Time.zone.local(2022, 8, 5), location:) FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 6, 4), location:) location.reload - expect(active_periods(location).count).to eq(3) - expect(active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) - expect(active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 6, 4), to: Time.zone.local(2022, 7, 6)) - expect(active_periods(location).third).to have_attributes(from: Time.zone.local(2022, 8, 5), to: nil) + expect(location_active_periods(location).count).to eq(3) + expect(location_active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + expect(location_active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 6, 4), to: Time.zone.local(2022, 7, 6)) + expect(location_active_periods(location).third).to have_attributes(from: Time.zone.local(2022, 8, 5), to: nil) end it "returns correct active periods when reactivation happends during a deactivated period" do @@ -117,9 +117,9 @@ RSpec.describe LocationsHelper do FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 4, 6), reactivation_date: Time.zone.local(2022, 7, 7), location:) location.reload - expect(active_periods(location).count).to eq(2) - expect(active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 4, 6)) - expect(active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 11, 11), to: nil) + expect(location_active_periods(location).count).to eq(2) + expect(location_active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 4, 6)) + expect(location_active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 11, 11), to: nil) end it "returns correct active periods when a full deactivation period happens during another deactivation period" do @@ -127,9 +127,9 @@ RSpec.describe LocationsHelper do FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 4, 6), reactivation_date: Time.zone.local(2022, 7, 7), location:) location.reload - expect(active_periods(location).count).to eq(2) - expect(active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 4, 6)) - expect(active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 7, 7), to: nil) + expect(location_active_periods(location).count).to eq(2) + expect(location_active_periods(location).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 4, 6)) + expect(location_active_periods(location).second).to have_attributes(from: Time.zone.local(2022, 7, 7), to: nil) end end diff --git a/spec/helpers/schemes_helper_spec.rb b/spec/helpers/schemes_helper_spec.rb index 9db3639d9..96f472457 100644 --- a/spec/helpers/schemes_helper_spec.rb +++ b/spec/helpers/schemes_helper_spec.rb @@ -1,8 +1,94 @@ require "rails_helper" RSpec.describe SchemesHelper do + describe "Active periods" do + let(:scheme) { FactoryBot.create(:scheme) } + + before do + Timecop.freeze(2022, 10, 10) + end + + after do + Timecop.unfreeze + end + + it "returns one active period without to date" do + expect(scheme_active_periods(scheme).count).to eq(1) + expect(scheme_active_periods(scheme).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: nil) + end + + it "ignores reactivations that were deactivated on the same day" do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 6, 4), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), scheme:) + scheme.reload + + expect(scheme_active_periods(scheme).count).to eq(1) + expect(scheme_active_periods(scheme).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + end + + it "returns sequential non reactivated active periods" do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 6, 4), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 7, 6), scheme:) + scheme.reload + + expect(scheme_active_periods(scheme).count).to eq(2) + expect(scheme_active_periods(scheme).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + expect(scheme_active_periods(scheme).second).to have_attributes(from: Time.zone.local(2022, 6, 4), to: Time.zone.local(2022, 7, 6)) + end + + it "returns sequential reactivated active periods" do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 6, 4), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 7, 6), reactivation_date: Time.zone.local(2022, 8, 5), scheme:) + scheme.reload + expect(scheme_active_periods(scheme).count).to eq(3) + expect(scheme_active_periods(scheme).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + expect(scheme_active_periods(scheme).second).to have_attributes(from: Time.zone.local(2022, 6, 4), to: Time.zone.local(2022, 7, 6)) + expect(scheme_active_periods(scheme).third).to have_attributes(from: Time.zone.local(2022, 8, 5), to: nil) + end + + it "returns non sequential non reactivated active periods" do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 7, 6), reactivation_date: Time.zone.local(2022, 8, 5), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: nil, scheme:) + scheme.reload + + expect(scheme_active_periods(scheme).count).to eq(2) + expect(scheme_active_periods(scheme).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + expect(scheme_active_periods(scheme).second).to have_attributes(from: Time.zone.local(2022, 8, 5), to: nil) + end + + it "returns non sequential reactivated active periods" do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 7, 6), reactivation_date: Time.zone.local(2022, 8, 5), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 6, 4), scheme:) + scheme.reload + expect(scheme_active_periods(scheme).count).to eq(3) + expect(scheme_active_periods(scheme).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 5, 5)) + expect(scheme_active_periods(scheme).second).to have_attributes(from: Time.zone.local(2022, 6, 4), to: Time.zone.local(2022, 7, 6)) + expect(scheme_active_periods(scheme).third).to have_attributes(from: Time.zone.local(2022, 8, 5), to: nil) + end + + it "returns correct active periods when reactivation happends during a deactivated period" do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 11, 11), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 4, 6), reactivation_date: Time.zone.local(2022, 7, 7), scheme:) + scheme.reload + + expect(scheme_active_periods(scheme).count).to eq(2) + expect(scheme_active_periods(scheme).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 4, 6)) + expect(scheme_active_periods(scheme).second).to have_attributes(from: Time.zone.local(2022, 11, 11), to: nil) + end + + it "returns correct active periods when a full deactivation period happens during another deactivation period" do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 6, 11), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 4, 6), reactivation_date: Time.zone.local(2022, 7, 7), scheme:) + scheme.reload + + expect(scheme_active_periods(scheme).count).to eq(2) + expect(scheme_active_periods(scheme).first).to have_attributes(from: Time.zone.local(2022, 4, 1), to: Time.zone.local(2022, 4, 6)) + expect(scheme_active_periods(scheme).second).to have_attributes(from: Time.zone.local(2022, 7, 7), to: nil) + end + end + describe "display_scheme_attributes" do - let!(:scheme) { FactoryBot.create(:scheme, created_at: Time.zone.local(2022, 8, 8)) } + let!(:scheme) { FactoryBot.create(:scheme, created_at: Time.zone.local(2022, 4, 1)) } it "returns correct display attributes" do attributes = [ @@ -18,32 +104,116 @@ RSpec.describe SchemesHelper do { name: "Secondary client group", value: scheme.secondary_client_group }, { name: "Level of support given", value: scheme.support_type }, { name: "Intended length of stay", value: scheme.intended_stay }, - { name: "Availability", value: "Active from 8 August 2022" }, + { name: "Availability", value: "Active from 1 April 2022" }, { name: "Status", value: :active }, ] expect(display_scheme_attributes(scheme)).to eq(attributes) end context "when viewing availability" do - context "with are no deactivations" do + context "with no deactivations" do it "displays created_at as availability date" do availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] expect(availability_attribute).to eq("Active from #{scheme.created_at.to_formatted_s(:govuk_date)}") end + + it "displays current collection start date as availability date if created_at is later than collection start date" do + scheme.update!(created_at: Time.zone.local(2022, 4, 16)) + availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + + expect(availability_attribute).to eq("Active from 1 April 2022") + end end context "with previous deactivations" do + context "and all reactivated deactivations" do + before do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 8, 10), reactivation_date: Time.zone.local(2022, 9, 1), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 9, 15), reactivation_date: Time.zone.local(2022, 9, 28), scheme:) + scheme.reload + end + + it "displays the timeline of availability" do + availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + + expect(availability_attribute).to eq("Active from 1 April 2022 to 9 August 2022\nDeactivated on 10 August 2022\nActive from 1 September 2022 to 14 September 2022\nDeactivated on 15 September 2022\nActive from 28 September 2022") + end + end + + context "and non reactivated deactivation" do + before do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 8, 10), reactivation_date: Time.zone.local(2022, 9, 1), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 9, 15), reactivation_date: nil, scheme:) + scheme.reload + end + + it "displays the timeline of availability" do + availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + + expect(availability_attribute).to eq("Active from 1 April 2022 to 9 August 2022\nDeactivated on 10 August 2022\nActive from 1 September 2022 to 14 September 2022\nDeactivated on 15 September 2022") + end + end + end + + context "with out of order deactivations" do + context "and all reactivated deactivations" do + before do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 9, 24), reactivation_date: Time.zone.local(2022, 9, 28), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 15), reactivation_date: Time.zone.local(2022, 6, 18), scheme:) + scheme.reload + end + + it "displays the timeline of availability" do + availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + + expect(availability_attribute).to eq("Active from 1 April 2022 to 14 June 2022\nDeactivated on 15 June 2022\nActive from 18 June 2022 to 23 September 2022\nDeactivated on 24 September 2022\nActive from 28 September 2022") + end + end + + context "and one non reactivated deactivation" do + before do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 9, 24), reactivation_date: Time.zone.local(2022, 9, 28), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 15), reactivation_date: nil, scheme:) + scheme.reload + end + + it "displays the timeline of availability" do + availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + + expect(availability_attribute).to eq("Active from 1 April 2022 to 14 June 2022\nDeactivated on 15 June 2022\nActive from 28 September 2022") + end + end + end + + context "with multiple out of order deactivations" do + context "and one non reactivated deactivation" do + before do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 9, 24), reactivation_date: Time.zone.local(2022, 9, 28), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 10, 24), reactivation_date: Time.zone.local(2022, 10, 28), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 15), reactivation_date: nil, scheme:) + scheme.reload + end + + it "displays the timeline of availability" do + availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + + expect(availability_attribute).to eq("Active from 1 April 2022 to 14 June 2022\nDeactivated on 15 June 2022\nActive from 28 September 2022 to 23 October 2022\nDeactivated on 24 October 2022\nActive from 28 October 2022") + end + end + end + + context "with intersecting deactivations" do before do - FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 8, 10), reactivation_date: Time.zone.local(2022, 9, 1), scheme:) - FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 9, 15), reactivation_date: nil, scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 10, 10), reactivation_date: Time.zone.local(2022, 12, 1), scheme:) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 11, 11), reactivation_date: Time.zone.local(2022, 12, 11), scheme:) scheme.reload end it "displays the timeline of availability" do availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] - expect(availability_attribute).to eq("Active from 8 August 2022 to 9 August 2022\nDeactivated on 10 August 2022\nActive from 1 September 2022 to 14 September 2022\nDeactivated on 15 September 2022") + expect(availability_attribute).to eq("Active from 1 April 2022 to 9 October 2022\nDeactivated on 10 October 2022\nActive from 11 December 2022") end end end diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb index 64a4855da..dd5d697e9 100644 --- a/spec/models/location_spec.rb +++ b/spec/models/location_spec.rb @@ -151,6 +151,12 @@ RSpec.describe Location, type: :model do location.save! expect(location.status).to eq(:reactivating_soon) end + + it "returns activating soon if the location has a future startdate" do + location.startdate = Time.zone.local(2022, 7, 7) + location.save! + expect(location.status).to eq(:activating_soon) + end end context "when there have been previous deactivations" do @@ -188,12 +194,18 @@ RSpec.describe Location, type: :model do expect(location.status).to eq(:reactivating_soon) end - it "returns if the location had a deactivation during another deactivation" do + it "returns reactivating soon if the location had a deactivation during another deactivation" do Timecop.freeze(2022, 6, 4) FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 6, 2), location:) location.save! expect(location.status).to eq(:reactivating_soon) end + + it "returns activating soon if the location has a future startdate" do + location.startdate = Time.zone.local(2022, 7, 7) + location.save! + expect(location.status).to eq(:activating_soon) + end end end end diff --git a/spec/models/scheme_spec.rb b/spec/models/scheme_spec.rb index b19e8c992..58d0a8da8 100644 --- a/spec/models/scheme_spec.rb +++ b/spec/models/scheme_spec.rb @@ -125,6 +125,12 @@ RSpec.describe Scheme, type: :model do scheme.reload expect(scheme.status).to eq(:deactivated) end + + it "returns reactivating soon if the location has a future reactivation date" do + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 7), reactivation_date: Time.zone.local(2022, 6, 8), scheme:) + scheme.save! + expect(scheme.status).to eq(:reactivating_soon) + end end context "when there have been previous deactivations" do @@ -153,6 +159,20 @@ RSpec.describe Scheme, type: :model do scheme.reload expect(scheme.status).to eq(:deactivated) end + + it "returns reactivating soon if the scheme has a future reactivation date" do + Timecop.freeze(2022, 6, 8) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 7), reactivation_date: Time.zone.local(2022, 6, 9), scheme:) + scheme.save! + expect(scheme.status).to eq(:reactivating_soon) + end + + it "returns if the scheme had a deactivation during another deactivation" do + Timecop.freeze(2022, 6, 4) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 5, 5), reactivation_date: Time.zone.local(2022, 6, 2), scheme:) + scheme.save! + expect(scheme.status).to eq(:reactivating_soon) + end end end end diff --git a/spec/requests/schemes_controller_spec.rb b/spec/requests/schemes_controller_spec.rb index 0c3b47f1a..ee9cca533 100644 --- a/spec/requests/schemes_controller_spec.rb +++ b/spec/requests/schemes_controller_spec.rb @@ -274,7 +274,7 @@ RSpec.describe SchemesController, type: :request do it "renders reactivate this scheme" do expect(response).to have_http_status(:ok) - expect(page).to have_link("Reactivate this scheme", href: "/schemes/#{scheme.id}/reactivate") + expect(page).to have_link("Reactivate this scheme", href: "/schemes/#{scheme.id}/new-reactivation") end end @@ -283,7 +283,7 @@ RSpec.describe SchemesController, type: :request do it "renders reactivate this scheme" do expect(response).to have_http_status(:ok) - expect(page).to have_link("Reactivate this scheme", href: "/schemes/#{scheme.id}/reactivate") + expect(page).to have_link("Reactivate this scheme", href: "/schemes/#{scheme.id}/new-reactivation") end end end @@ -915,7 +915,6 @@ RSpec.describe SchemesController, type: :request do context "when signed in as a support" do let(:user) { FactoryBot.create(:user, :support) } let(:scheme_to_update) { FactoryBot.create(:scheme, owning_organisation: user.organisation, confirmed: nil) } - # let!(:location) { FactoryBot.create(:location, scheme: scheme_to_update) } before do FactoryBot.create(:location, scheme: scheme_to_update) @@ -1855,7 +1854,7 @@ RSpec.describe SchemesController, type: :request do it "displays the new page with an error message" do expect(response).to have_http_status(:unprocessable_entity) - expect(page).to have_content(I18n.t("validations.scheme.deactivation_date.not_selected")) + expect(page).to have_content(I18n.t("validations.scheme.toggle_date.not_selected")) end end @@ -1864,7 +1863,7 @@ RSpec.describe SchemesController, type: :request do it "displays the new page with an error message" do expect(response).to have_http_status(:unprocessable_entity) - expect(page).to have_content(I18n.t("validations.scheme.deactivation_date.invalid")) + expect(page).to have_content(I18n.t("validations.scheme.toggle_date.invalid")) end end @@ -1873,7 +1872,7 @@ RSpec.describe SchemesController, type: :request do it "displays the new page with an error message" do expect(response).to have_http_status(:unprocessable_entity) - expect(page).to have_content(I18n.t("validations.scheme.deactivation_date.out_of_range", date: "1 April 2022")) + expect(page).to have_content(I18n.t("validations.scheme.toggle_date.out_of_range", date: "1 April 2022")) end end @@ -1882,7 +1881,7 @@ RSpec.describe SchemesController, type: :request do it "displays page with an error message" do expect(response).to have_http_status(:unprocessable_entity) - expect(page).to have_content(I18n.t("validations.scheme.deactivation_date.invalid")) + expect(page).to have_content(I18n.t("validations.scheme.toggle_date.invalid")) end end @@ -1891,7 +1890,7 @@ RSpec.describe SchemesController, type: :request do it "displays page with an error message" do expect(response).to have_http_status(:unprocessable_entity) - expect(page).to have_content(I18n.t("validations.scheme.deactivation_date.invalid")) + expect(page).to have_content(I18n.t("validations.scheme.toggle_date.invalid")) end end @@ -1900,7 +1899,7 @@ RSpec.describe SchemesController, type: :request do it "displays page with an error message" do expect(response).to have_http_status(:unprocessable_entity) - expect(page).to have_content(I18n.t("validations.scheme.deactivation_date.invalid")) + expect(page).to have_content(I18n.t("validations.scheme.toggle_date.invalid")) end end end From 9c5d52c78bc2918661abdaf0904be6f8f4d60038 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 24 Nov 2022 12:57:40 +0000 Subject: [PATCH 04/19] CLDC-1618 Add cookie banner (#973) * CLDC-1618 Add cookie banner * CLDC-1618 Add analytics script inclusion tests * CLDC-1618 Include cookie banner JS * CLDC-1618 Appease JS Standard --- app/controllers/cookies_controller.rb | 6 ++ app/frontend/application.js | 7 ++- app/frontend/cookie-banner.js | 57 +++++++++++++++++++ app/views/cookies/_banner.html.erb | 48 ++++++++++++++++ app/views/layouts/application.html.erb | 24 ++++---- spec/views/layouts/application_layout_spec.rb | 57 +++++++++++++++++++ 6 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 app/frontend/cookie-banner.js create mode 100644 app/views/cookies/_banner.html.erb create mode 100644 spec/views/layouts/application_layout_spec.rb diff --git a/app/controllers/cookies_controller.rb b/app/controllers/cookies_controller.rb index 50d874e92..0e78fa1bc 100644 --- a/app/controllers/cookies_controller.rb +++ b/app/controllers/cookies_controller.rb @@ -18,6 +18,12 @@ class CookiesController < ApplicationController redirect_to cookies_path end + format.json do + render json: { + status: "ok", + message: %(You’ve #{analytics_consent == 'on' ? 'accepted' : 'rejected'} analytics cookies.), + } + end end end diff --git a/app/frontend/application.js b/app/frontend/application.js index ff4bae81a..36b730098 100644 --- a/app/frontend/application.js +++ b/app/frontend/application.js @@ -4,18 +4,19 @@ // files to reference that code so it'll be compiled. // Polyfills for IE +import '@stimulus/polyfills' import '@webcomponents/webcomponentsjs' import 'core-js/stable' -import 'regenerator-runtime/runtime' -import '@stimulus/polyfills' import 'custom-event-polyfill' import 'intersection-observer' +import 'regenerator-runtime/runtime' // import { initAll as GOVUKFrontend } from 'govuk-frontend' import { initAll as GOVUKPrototypeComponents } from 'govuk-prototype-components' -import './styles/application.scss' import './controllers' +import './cookie-banner' +import './styles/application.scss' require.context('govuk-frontend/govuk/assets') diff --git a/app/frontend/cookie-banner.js b/app/frontend/cookie-banner.js new file mode 100644 index 000000000..e39414f72 --- /dev/null +++ b/app/frontend/cookie-banner.js @@ -0,0 +1,57 @@ +const cookieBannerEl = document.querySelector('.js-cookie-banner') + +if (cookieBannerEl) { + const cookieFormEl = document.querySelector('.js-cookie-form') + + cookieFormEl.addEventListener('click', (e) => { + if (e.target.tagName !== 'BUTTON') { + return + } + + const body = new window.FormData(cookieFormEl) + body.append('cookies_form[accept_analytics_cookies]', e.target.value) + + fetch(cookieFormEl.action, { + method: 'PUT', + headers: { + Accept: 'application/json' + }, + body + }) + .then((res) => { + if (res.status >= 200 && res.status < 300) { + return res + } + + throw new Error(res) + }) + .then((res) => res.json()) + .then(({ message }) => { + const messageEl = cookieBannerEl.querySelector('.js-cookie-message') + messageEl.textContent = message + + cookieBannerEl + .querySelector('.js-cookie-banner__form') + .setAttribute('hidden', '') + cookieBannerEl + .querySelector('.js-cookie-banner__success') + .removeAttribute('hidden') + }) + + const gaSrc = window.analyticsScript + if (e.target.value === 'on' && gaSrc) { + const scriptEl = document.createElement('script') + scriptEl.src = gaSrc + document.body.appendChild(scriptEl) + } + + e.preventDefault() + }) + + const hideBannerEl = document.querySelector('.js-hide-cookie-banner') + hideBannerEl.addEventListener('click', (e) => { + e.preventDefault() + + cookieBannerEl.setAttribute('hidden', '') + }) +} diff --git a/app/views/cookies/_banner.html.erb b/app/views/cookies/_banner.html.erb new file mode 100644 index 000000000..7885bad47 --- /dev/null +++ b/app/views/cookies/_banner.html.erb @@ -0,0 +1,48 @@ + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 2e5f66da1..c52b587f3 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -25,15 +25,15 @@ <% gtm_container = get_gtm_container %> <% gtm_id = get_gtm_id %> - - - - <% if cookies[:accept_analytics_cookies] == "on" %> + + + + <% else %> - - <% if cookies[:accept_analytics_cookies] %> + <% if cookies[:accept_analytics_cookies] == "on" %> + <% end %> + <% unless cookies[:accept_analytics_cookies] || current_page?(cookies_path) %> + <%= render "cookies/banner" %> + <% end %> + <%= govuk_skip_link %> <%= govuk_header( diff --git a/spec/views/layouts/application_layout_spec.rb b/spec/views/layouts/application_layout_spec.rb new file mode 100644 index 000000000..2ee571e09 --- /dev/null +++ b/spec/views/layouts/application_layout_spec.rb @@ -0,0 +1,57 @@ +require "rails_helper" + +RSpec.describe "layouts/application" do + shared_examples "analytics cookie elements" do |banner:, scripts:| + define_negated_matcher :not_match, :match + + it "#{banner ? 'includes' : 'omits'} the cookie banner" do + banner_text = "We’d like to use analytics cookies so we can understand how you use the service and make improvements." + if banner + expect(rendered).to match(banner_text) + else + expect(rendered).not_to match(banner_text) + end + end + + it "#{scripts ? 'includes' : 'omits'} the analytics scripts" do + gtm_script_tag = / Date: Thu, 24 Nov 2022 15:33:23 +0000 Subject: [PATCH 05/19] [1711] Orgs parent-child relationship now atomic (#1019) * remove Organisation#relationship_type * add indexes and fk constraints to org relations * remove relationship_type from seeds - as these have now been removed * seeding is now idempotent --- .../organisation_relationships_controller.rb | 16 +++----- app/controllers/users_controller.rb | 1 + app/models/organisation.rb | 5 ++- app/models/organisation_relationship.rb | 12 ------ ...20221122122311_delete_relationship_type.rb | 5 +++ ...20221122130928_add_org_relation_indexes.rb | 10 +++++ db/schema.rb | 8 +++- db/seeds.rb | 12 ++---- spec/factories/organisation_relationship.rb | 8 ---- .../lettings/pages/housing_provider_spec.rb | 7 +--- .../pages/managing_organisation_spec.rb | 10 ++--- .../questions/housing_provider_spec.rb | 8 ++-- .../questions/managing_organisation_spec.rb | 16 ++++---- spec/models/organisation_relationship_spec.rb | 25 ++++++++++++ spec/models/organisation_spec.rb | 20 ---------- ...anisation_relationships_controller_spec.rb | 38 +++++++++---------- 16 files changed, 96 insertions(+), 105 deletions(-) create mode 100644 db/migrate/20221122122311_delete_relationship_type.rb create mode 100644 db/migrate/20221122130928_add_org_relation_indexes.rb create mode 100644 spec/models/organisation_relationship_spec.rb diff --git a/app/controllers/organisation_relationships_controller.rb b/app/controllers/organisation_relationships_controller.rb index f50349d20..e7a6a7856 100644 --- a/app/controllers/organisation_relationships_controller.rb +++ b/app/controllers/organisation_relationships_controller.rb @@ -39,7 +39,6 @@ class OrganisationRelationshipsController < ApplicationController def create_housing_provider child_organisation = @organisation - relationship_type = OrganisationRelationship::OWNING if params[:organisation][:related_organisation_id].empty? @organisation.errors.add :related_organisation_id, "You must choose a housing provider" @organisations = Organisation.where.not(id: child_organisation.id).pluck(:id, :name) @@ -47,21 +46,20 @@ class OrganisationRelationshipsController < ApplicationController return else parent_organisation = related_organisation - if OrganisationRelationship.exists?(child_organisation:, parent_organisation:, relationship_type:) + if OrganisationRelationship.exists?(child_organisation:, parent_organisation:) @organisation.errors.add :related_organisation_id, "You have already added this housing provider" @organisations = Organisation.where.not(id: child_organisation.id).pluck(:id, :name) render "organisation_relationships/add_housing_provider" return end end - create!(child_organisation:, parent_organisation:, relationship_type:) + create!(child_organisation:, parent_organisation:) flash[:notice] = "#{related_organisation.name} is now one of #{current_user.data_coordinator? ? 'your' : "this organisation's"} housing providers" redirect_to housing_providers_organisation_path end def create_managing_agent parent_organisation = @organisation - relationship_type = OrganisationRelationship::MANAGING if params[:organisation][:related_organisation_id].empty? @organisation.errors.add :related_organisation_id, "You must choose a managing agent" @organisations = Organisation.where.not(id: parent_organisation.id).pluck(:id, :name) @@ -69,14 +67,14 @@ class OrganisationRelationshipsController < ApplicationController return else child_organisation = related_organisation - if OrganisationRelationship.exists?(child_organisation:, parent_organisation:, relationship_type:) + if OrganisationRelationship.exists?(child_organisation:, parent_organisation:) @organisation.errors.add :related_organisation_id, "You have already added this managing agent" @organisations = Organisation.where.not(id: parent_organisation.id).pluck(:id, :name) render "organisation_relationships/add_managing_agent" return end end - create!(child_organisation:, parent_organisation:, relationship_type:) + create!(child_organisation:, parent_organisation:) flash[:notice] = "#{related_organisation.name} is now one of #{current_user.data_coordinator? ? 'your' : "this organisation's"} managing agents" redirect_to managing_agents_organisation_path end @@ -89,7 +87,6 @@ class OrganisationRelationshipsController < ApplicationController relationship = OrganisationRelationship.find_by!( child_organisation: @organisation, parent_organisation: target_organisation, - relationship_type: OrganisationRelationship::OWNING, ) relationship.destroy! flash[:notice] = "#{target_organisation.name} is no longer one of #{current_user.data_coordinator? ? 'your' : "this organisation's"} housing providers" @@ -104,7 +101,6 @@ class OrganisationRelationshipsController < ApplicationController relationship = OrganisationRelationship.find_by!( parent_organisation: @organisation, child_organisation: target_organisation, - relationship_type: OrganisationRelationship::MANAGING, ) relationship.destroy! flash[:notice] = "#{target_organisation.name} is no longer one of #{current_user.data_coordinator? ? 'your' : "this organisation's"} managing agents" @@ -113,8 +109,8 @@ class OrganisationRelationshipsController < ApplicationController private - def create!(child_organisation:, parent_organisation:, relationship_type:) - @resource = OrganisationRelationship.new(child_organisation:, parent_organisation:, relationship_type:) + def create!(child_organisation:, parent_organisation:) + @resource = OrganisationRelationship.new(child_organisation:, parent_organisation:) @resource.save! end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 9ab691a53..4ba32a442 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,6 +3,7 @@ class UsersController < ApplicationController include Devise::Controllers::SignInOut include Helpers::Email include Modules::SearchFilter + before_action :authenticate_user! before_action :find_resource, except: %i[new create] before_action :authenticate_scope!, except: %i[new] diff --git a/app/models/organisation.rb b/app/models/organisation.rb index cc0df4ef6..bb636624c 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -13,9 +13,10 @@ class Organisation < ApplicationRecord has_many :child_organisation_relationships, foreign_key: :parent_organisation_id, class_name: "OrganisationRelationship" has_many :child_organisations, through: :child_organisation_relationships - has_many :housing_provider_relationships, -> { where(relationship_type: OrganisationRelationship::OWNING) }, foreign_key: :child_organisation_id, class_name: "OrganisationRelationship" + has_many :housing_provider_relationships, foreign_key: :child_organisation_id, class_name: "OrganisationRelationship" has_many :housing_providers, through: :housing_provider_relationships, source: :parent_organisation - has_many :managing_agent_relationships, -> { where(relationship_type: OrganisationRelationship::MANAGING) }, foreign_key: :parent_organisation_id, class_name: "OrganisationRelationship" + + has_many :managing_agent_relationships, foreign_key: :parent_organisation_id, class_name: "OrganisationRelationship" has_many :managing_agents, through: :managing_agent_relationships, source: :child_organisation scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") } diff --git a/app/models/organisation_relationship.rb b/app/models/organisation_relationship.rb index d1f073737..034fc5d0e 100644 --- a/app/models/organisation_relationship.rb +++ b/app/models/organisation_relationship.rb @@ -1,16 +1,4 @@ class OrganisationRelationship < ApplicationRecord belongs_to :child_organisation, class_name: "Organisation" belongs_to :parent_organisation, class_name: "Organisation" - - scope :owning, -> { where(relationship_type: OWNING) } - scope :managing, -> { where(relationship_type: MANAGING) } - - OWNING = "owning".freeze - MANAGING = "managing".freeze - RELATIONSHIP_TYPE = { - OWNING => 0, - MANAGING => 1, - }.freeze - - enum relationship_type: RELATIONSHIP_TYPE end diff --git a/db/migrate/20221122122311_delete_relationship_type.rb b/db/migrate/20221122122311_delete_relationship_type.rb new file mode 100644 index 000000000..81982a25c --- /dev/null +++ b/db/migrate/20221122122311_delete_relationship_type.rb @@ -0,0 +1,5 @@ +class DeleteRelationshipType < ActiveRecord::Migration[7.0] + def change + remove_column :organisation_relationships, :relationship_type, :integer, null: false + end +end diff --git a/db/migrate/20221122130928_add_org_relation_indexes.rb b/db/migrate/20221122130928_add_org_relation_indexes.rb new file mode 100644 index 000000000..770e97fde --- /dev/null +++ b/db/migrate/20221122130928_add_org_relation_indexes.rb @@ -0,0 +1,10 @@ +class AddOrgRelationIndexes < ActiveRecord::Migration[7.0] + def change + add_index :organisation_relationships, :child_organisation_id + add_index :organisation_relationships, :parent_organisation_id + add_index :organisation_relationships, %i[parent_organisation_id child_organisation_id], unique: true, name: "index_org_rel_parent_child_uniq" + + add_foreign_key :organisation_relationships, :organisations, column: :parent_organisation_id + add_foreign_key :organisation_relationships, :organisations, column: :child_organisation_id + end +end diff --git a/db/schema.rb b/db/schema.rb index f2760b505..ff2619f43 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: 2022_11_17_103855) do +ActiveRecord::Schema[7.0].define(version: 2022_11_22_130928) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -286,7 +286,9 @@ ActiveRecord::Schema[7.0].define(version: 2022_11_17_103855) do t.integer "parent_organisation_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.integer "relationship_type", null: false + t.index ["child_organisation_id"], name: "index_organisation_relationships_on_child_organisation_id" + t.index ["parent_organisation_id", "child_organisation_id"], name: "index_org_rel_parent_child_uniq", unique: true + t.index ["parent_organisation_id"], name: "index_organisation_relationships_on_parent_organisation_id" end create_table "organisation_rent_periods", force: :cascade do |t| @@ -477,6 +479,8 @@ ActiveRecord::Schema[7.0].define(version: 2022_11_17_103855) do add_foreign_key "lettings_logs", "organisations", column: "owning_organisation_id", on_delete: :cascade add_foreign_key "lettings_logs", "schemes" add_foreign_key "locations", "schemes" + add_foreign_key "organisation_relationships", "organisations", column: "child_organisation_id" + add_foreign_key "organisation_relationships", "organisations", column: "parent_organisation_id" add_foreign_key "sales_logs", "organisations", column: "owning_organisation_id", on_delete: :cascade add_foreign_key "schemes", "organisations", column: "managing_organisation_id" add_foreign_key "schemes", "organisations", column: "owning_organisation_id", on_delete: :cascade diff --git a/db/seeds.rb b/db/seeds.rb index 7bcc90abc..bf40a59d1 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -67,25 +67,21 @@ unless Rails.env.test? end end - OrganisationRelationship.create!( + OrganisationRelationship.find_or_create_by!( child_organisation: org, parent_organisation: housing_provider1, - relationship_type: OrganisationRelationship::OWNING, ) - OrganisationRelationship.create!( + OrganisationRelationship.find_or_create_by!( child_organisation: org, parent_organisation: housing_provider2, - relationship_type: OrganisationRelationship::OWNING, ) - OrganisationRelationship.create!( + OrganisationRelationship.find_or_create_by!( child_organisation: managing_agent1, parent_organisation: org, - relationship_type: OrganisationRelationship::MANAGING, ) - OrganisationRelationship.create!( + OrganisationRelationship.find_or_create_by!( child_organisation: managing_agent2, parent_organisation: org, - relationship_type: OrganisationRelationship::MANAGING, ) if (Rails.env.development? || Rails.env.review?) && User.count.zero? diff --git a/spec/factories/organisation_relationship.rb b/spec/factories/organisation_relationship.rb index 418599902..de2e2901b 100644 --- a/spec/factories/organisation_relationship.rb +++ b/spec/factories/organisation_relationship.rb @@ -2,13 +2,5 @@ FactoryBot.define do factory :organisation_relationship do child_organisation { FactoryBot.create(:organisation) } parent_organisation { FactoryBot.create(:organisation) } - - trait :owning do - relationship_type { OrganisationRelationship::OWNING } - end - - trait :managing do - relationship_type { OrganisationRelationship::MANAGING } - end end end diff --git a/spec/models/form/lettings/pages/housing_provider_spec.rb b/spec/models/form/lettings/pages/housing_provider_spec.rb index 014ffbd66..2b2be7bfc 100644 --- a/spec/models/form/lettings/pages/housing_provider_spec.rb +++ b/spec/models/form/lettings/pages/housing_provider_spec.rb @@ -79,7 +79,6 @@ RSpec.describe Form::Lettings::Pages::HousingProvider, type: :model do before do create( :organisation_relationship, - :owning, child_organisation: user.organisation, parent_organisation: housing_provider, ) @@ -101,13 +100,11 @@ RSpec.describe Form::Lettings::Pages::HousingProvider, type: :model do before do create( :organisation_relationship, - :owning, child_organisation: user.organisation, parent_organisation: housing_provider1, ) create( :organisation_relationship, - :owning, child_organisation: user.organisation, parent_organisation: housing_provider2, ) @@ -140,8 +137,8 @@ RSpec.describe Form::Lettings::Pages::HousingProvider, type: :model do context "with >0 housing_providers" do before do - create(:organisation_relationship, :owning, child_organisation: user.organisation) - create(:organisation_relationship, :owning, child_organisation: user.organisation) + create(:organisation_relationship, child_organisation: user.organisation) + create(:organisation_relationship, child_organisation: user.organisation) end it "is shown" do diff --git a/spec/models/form/lettings/pages/managing_organisation_spec.rb b/spec/models/form/lettings/pages/managing_organisation_spec.rb index 65c2589c6..01d6ba9b2 100644 --- a/spec/models/form/lettings/pages/managing_organisation_spec.rb +++ b/spec/models/form/lettings/pages/managing_organisation_spec.rb @@ -76,8 +76,8 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do context "with >1 managing_agents" do before do - create(:organisation_relationship, :managing, parent_organisation: log.owning_organisation) - create(:organisation_relationship, :managing, parent_organisation: log.owning_organisation) + create(:organisation_relationship, parent_organisation: log.owning_organisation) + create(:organisation_relationship, parent_organisation: log.owning_organisation) end it "is shown" do @@ -91,7 +91,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do before do create( :organisation_relationship, - :managing, child_organisation: managing_agent, parent_organisation: log.owning_organisation, ) @@ -128,8 +127,8 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do context "with >1 managing_agents" do before do - create(:organisation_relationship, :managing, parent_organisation: user.organisation) - create(:organisation_relationship, :managing, parent_organisation: user.organisation) + create(:organisation_relationship, parent_organisation: user.organisation) + create(:organisation_relationship, parent_organisation: user.organisation) end it "is shown" do @@ -143,7 +142,6 @@ RSpec.describe Form::Lettings::Pages::ManagingOrganisation, type: :model do before do create( :organisation_relationship, - :managing, child_organisation: managing_agent, parent_organisation: user.organisation, ) diff --git a/spec/models/form/lettings/questions/housing_provider_spec.rb b/spec/models/form/lettings/questions/housing_provider_spec.rb index 5363a0227..c65706d63 100644 --- a/spec/models/form/lettings/questions/housing_provider_spec.rb +++ b/spec/models/form/lettings/questions/housing_provider_spec.rb @@ -107,7 +107,7 @@ RSpec.describe Form::Lettings::Questions::HousingProvider, type: :model do context "when housing providers != 0" do before do - create(:organisation_relationship, :owning, child_organisation: user.organisation) + create(:organisation_relationship, child_organisation: user.organisation) end it "is visible in check answers" do @@ -122,7 +122,7 @@ RSpec.describe Form::Lettings::Questions::HousingProvider, type: :model do context "when housing providers <= 1" do before do - create(:organisation_relationship, :owning, child_organisation: user.organisation) + create(:organisation_relationship, child_organisation: user.organisation) end it "is hidden in check answers" do @@ -133,8 +133,8 @@ RSpec.describe Form::Lettings::Questions::HousingProvider, type: :model do context "when housing providers >= 2" do before do - create(:organisation_relationship, :owning, child_organisation: user.organisation) - create(:organisation_relationship, :owning, child_organisation: user.organisation) + create(:organisation_relationship, child_organisation: user.organisation) + create(:organisation_relationship, child_organisation: user.organisation) end it "is visible in check answers" do diff --git a/spec/models/form/lettings/questions/managing_organisation_spec.rb b/spec/models/form/lettings/questions/managing_organisation_spec.rb index e045601bf..17cc621f0 100644 --- a/spec/models/form/lettings/questions/managing_organisation_spec.rb +++ b/spec/models/form/lettings/questions/managing_organisation_spec.rb @@ -56,8 +56,8 @@ RSpec.describe Form::Lettings::Questions::ManagingOrganisation, type: :model do let(:user) { create(:user, :data_coordinator, organisation: create(:organisation, holds_own_stock: true)) } let(:log) { create(:lettings_log) } - let!(:org_rel1) { create(:organisation_relationship, :managing, parent_organisation: user.organisation) } - let!(:org_rel2) { create(:organisation_relationship, :managing, parent_organisation: user.organisation) } + let!(:org_rel1) { create(:organisation_relationship, parent_organisation: user.organisation) } + let!(:org_rel2) { create(:organisation_relationship, parent_organisation: user.organisation) } let(:options) do { @@ -77,8 +77,8 @@ RSpec.describe Form::Lettings::Questions::ManagingOrganisation, type: :model do let(:user) { create(:user, :data_coordinator, organisation: create(:organisation, holds_own_stock: false)) } let(:log) { create(:lettings_log) } - let!(:org_rel1) { create(:organisation_relationship, :managing, parent_organisation: user.organisation) } - let!(:org_rel2) { create(:organisation_relationship, :managing, parent_organisation: user.organisation) } + let!(:org_rel1) { create(:organisation_relationship, parent_organisation: user.organisation) } + let!(:org_rel2) { create(:organisation_relationship, parent_organisation: user.organisation) } let(:options) do { @@ -98,8 +98,8 @@ RSpec.describe Form::Lettings::Questions::ManagingOrganisation, type: :model do let(:user) { create(:user, :support) } let(:log_owning_org) { create(:organisation, holds_own_stock: false) } let(:log) { create(:lettings_log, owning_organisation: log_owning_org) } - let!(:org_rel1) { create(:organisation_relationship, :managing, parent_organisation: log_owning_org) } - let!(:org_rel2) { create(:organisation_relationship, :managing, parent_organisation: log_owning_org) } + let!(:org_rel1) { create(:organisation_relationship, parent_organisation: log_owning_org) } + let!(:org_rel2) { create(:organisation_relationship, parent_organisation: log_owning_org) } let(:options) do { @@ -118,8 +118,8 @@ RSpec.describe Form::Lettings::Questions::ManagingOrganisation, type: :model do let(:user) { create(:user, :support) } let(:log_owning_org) { create(:organisation, holds_own_stock: true) } let(:log) { create(:lettings_log, owning_organisation: log_owning_org) } - let!(:org_rel1) { create(:organisation_relationship, :managing, parent_organisation: log_owning_org) } - let!(:org_rel2) { create(:organisation_relationship, :managing, parent_organisation: log_owning_org) } + let!(:org_rel1) { create(:organisation_relationship, parent_organisation: log_owning_org) } + let!(:org_rel2) { create(:organisation_relationship, parent_organisation: log_owning_org) } let(:options) do { diff --git a/spec/models/organisation_relationship_spec.rb b/spec/models/organisation_relationship_spec.rb new file mode 100644 index 000000000..8857f29d6 --- /dev/null +++ b/spec/models/organisation_relationship_spec.rb @@ -0,0 +1,25 @@ +require "rails_helper" + +RSpec.describe OrganisationRelationship do + let(:parent_organisation) { create(:organisation) } + let(:child_organisation) { create(:organisation) } + + context "when a relationship exists" do + subject!(:relationship) do + described_class.create!(parent_organisation:, + child_organisation:) + end + + describe "parent#managing_agents" do + it "includes child" do + expect(parent_organisation.managing_agents).to include(child_organisation) + end + end + + describe "child#housing_providers" do + it "includes parent" do + expect(child_organisation.housing_providers).to include(parent_organisation) + end + end + end +end diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index d4d63b6a0..cf9afe170 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -34,14 +34,12 @@ RSpec.describe Organisation, type: :model do before do FactoryBot.create( :organisation_relationship, - :owning, child_organisation:, parent_organisation: organisation, ) FactoryBot.create( :organisation_relationship, - :owning, child_organisation: grandchild_organisation, parent_organisation: child_organisation, ) @@ -65,21 +63,12 @@ RSpec.describe Organisation, type: :model do before do FactoryBot.create( :organisation_relationship, - :managing, child_organisation:, parent_organisation: organisation, ) FactoryBot.create( :organisation_relationship, - :owning, - child_organisation:, - parent_organisation: organisation, - ) - - FactoryBot.create( - :organisation_relationship, - :owning, child_organisation: grandchild_organisation, parent_organisation: child_organisation, ) @@ -98,21 +87,12 @@ RSpec.describe Organisation, type: :model do before do FactoryBot.create( :organisation_relationship, - :managing, - child_organisation:, - parent_organisation: organisation, - ) - - FactoryBot.create( - :organisation_relationship, - :owning, child_organisation:, parent_organisation: organisation, ) FactoryBot.create( :organisation_relationship, - :managing, child_organisation: grandchild_organisation, parent_organisation: child_organisation, ) diff --git a/spec/requests/organisation_relationships_controller_spec.rb b/spec/requests/organisation_relationships_controller_spec.rb index 83655156c..99c57a01a 100644 --- a/spec/requests/organisation_relationships_controller_spec.rb +++ b/spec/requests/organisation_relationships_controller_spec.rb @@ -21,8 +21,8 @@ RSpec.describe OrganisationRelationshipsController, type: :request do let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD 2") } before do - FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: housing_provider, relationship_type: OrganisationRelationship.relationship_types[:owning]) - FactoryBot.create(:organisation_relationship, child_organisation: other_organisation, parent_organisation: other_org_housing_provider, relationship_type: OrganisationRelationship.relationship_types[:owning]) + FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: housing_provider) + FactoryBot.create(:organisation_relationship, child_organisation: other_organisation, parent_organisation: other_org_housing_provider) get "/organisations/#{organisation.id}/housing-providers", headers:, params: {} end @@ -83,8 +83,8 @@ RSpec.describe OrganisationRelationshipsController, type: :request do let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD") } before do - FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent, relationship_type: OrganisationRelationship.relationship_types[:managing]) - FactoryBot.create(:organisation_relationship, parent_organisation: other_organisation, child_organisation: other_org_managing_agent, relationship_type: OrganisationRelationship.relationship_types[:managing]) + FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent) + FactoryBot.create(:organisation_relationship, parent_organisation: other_organisation, child_organisation: other_org_managing_agent) get "/organisations/#{organisation.id}/managing-agents", headers:, params: {} end @@ -153,7 +153,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do it "sets the organisation relationship attributes correctly" do request - expect(OrganisationRelationship).to exist(child_organisation_id: organisation.id, parent_organisation_id: housing_provider.id, relationship_type: OrganisationRelationship::OWNING) + expect(OrganisationRelationship).to exist(child_organisation_id: organisation.id, parent_organisation_id: housing_provider.id) end it "redirects to the organisation list" do @@ -181,7 +181,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do it "sets the organisation relationship attributes correctly" do request - expect(OrganisationRelationship).to exist(parent_organisation_id: organisation.id, child_organisation_id: managing_agent.id, relationship_type: OrganisationRelationship::MANAGING) + expect(OrganisationRelationship).to exist(parent_organisation_id: organisation.id, child_organisation_id: managing_agent.id) end it "redirects to the organisation list" do @@ -200,7 +200,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do let(:request) { delete "/organisations/#{organisation.id}/housing-providers", headers:, params: } before do - FactoryBot.create(:organisation_relationship, :owning, child_organisation: organisation, parent_organisation: housing_provider) + FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: housing_provider) end it "deletes the new organisation relationship" do @@ -225,7 +225,6 @@ RSpec.describe OrganisationRelationshipsController, type: :request do before do FactoryBot.create( :organisation_relationship, - :managing, parent_organisation: organisation, child_organisation: managing_agent, ) @@ -256,8 +255,8 @@ RSpec.describe OrganisationRelationshipsController, type: :request do let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD") } before do - FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: housing_provider, relationship_type: OrganisationRelationship.relationship_types[:owning]) - FactoryBot.create(:organisation_relationship, child_organisation: other_organisation, parent_organisation: other_org_housing_provider, relationship_type: OrganisationRelationship.relationship_types[:owning]) + FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: housing_provider) + FactoryBot.create(:organisation_relationship, child_organisation: other_organisation, parent_organisation: other_org_housing_provider) get "/organisations/#{organisation.id}/housing-providers", headers:, params: {} end @@ -304,8 +303,8 @@ RSpec.describe OrganisationRelationshipsController, type: :request do let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD") } before do - FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent, relationship_type: OrganisationRelationship.relationship_types[:managing]) - FactoryBot.create(:organisation_relationship, parent_organisation: other_organisation, child_organisation: other_org_managing_agent, relationship_type: OrganisationRelationship.relationship_types[:managing]) + FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent) + FactoryBot.create(:organisation_relationship, parent_organisation: other_organisation, child_organisation: other_org_managing_agent) get "/organisations/#{organisation.id}/managing-agents", headers:, params: {} end @@ -383,7 +382,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do it "sets the organisation relationship attributes correctly" do request - expect(OrganisationRelationship).to exist(child_organisation_id: organisation.id, parent_organisation_id: housing_provider.id, relationship_type: OrganisationRelationship::OWNING) + expect(OrganisationRelationship).to exist(child_organisation_id: organisation.id, parent_organisation_id: housing_provider.id) end it "redirects to the organisation list" do @@ -411,7 +410,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do it "sets the organisation relationship attributes correctly" do request - expect(OrganisationRelationship).to exist(parent_organisation_id: organisation.id, child_organisation_id: managing_agent.id, relationship_type: OrganisationRelationship::MANAGING) + expect(OrganisationRelationship).to exist(parent_organisation_id: organisation.id, child_organisation_id: managing_agent.id) end it "redirects to the organisation list" do @@ -430,7 +429,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do let(:request) { delete "/organisations/#{organisation.id}/housing-providers", headers:, params: } before do - FactoryBot.create(:organisation_relationship, :owning, child_organisation: organisation, parent_organisation: housing_provider) + FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: housing_provider) end it "deletes the new organisation relationship" do @@ -455,7 +454,6 @@ RSpec.describe OrganisationRelationshipsController, type: :request do before do FactoryBot.create( :organisation_relationship, - :managing, parent_organisation: organisation, child_organisation: managing_agent, ) @@ -477,8 +475,8 @@ RSpec.describe OrganisationRelationshipsController, type: :request do let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD 2") } before do - FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: housing_provider, relationship_type: OrganisationRelationship.relationship_types[:owning]) - FactoryBot.create(:organisation_relationship, child_organisation: other_organisation, parent_organisation: other_org_housing_provider, relationship_type: OrganisationRelationship.relationship_types[:owning]) + FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: housing_provider) + FactoryBot.create(:organisation_relationship, child_organisation: other_organisation, parent_organisation: other_org_housing_provider) get "/organisations/#{organisation.id}/housing-providers", headers:, params: {} end @@ -531,8 +529,8 @@ RSpec.describe OrganisationRelationshipsController, type: :request do let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD 2") } before do - FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent, relationship_type: OrganisationRelationship.relationship_types[:managing]) - FactoryBot.create(:organisation_relationship, parent_organisation: other_organisation, child_organisation: other_org_managing_agent, relationship_type: OrganisationRelationship.relationship_types[:managing]) + FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent) + FactoryBot.create(:organisation_relationship, parent_organisation: other_organisation, child_organisation: other_org_managing_agent) get "/organisations/#{organisation.id}/managing-agents", headers:, params: {} end From e0a6a719b1a02fbf9312a151023b462a5dd0057f Mon Sep 17 00:00:00 2001 From: natdeanlewissoftwire <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Thu, 24 Nov 2022 17:14:07 +0000 Subject: [PATCH 06/19] feat: add new design to scheme page (#1003) * feat: add new design to scheme page * feat: add incomplete status * feat: use confirmed behaviour --- app/models/scheme.rb | 2 ++ app/views/schemes/_scheme_list.html.erb | 22 +++++++++++----------- db/schema.rb | 6 +++--- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/models/scheme.rb b/app/models/scheme.rb index a56a29602..fce696bb6 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -214,6 +214,8 @@ class Scheme < ApplicationRecord end def status + return :incomplete unless confirmed + open_deactivation = scheme_deactivation_periods.deactivations_without_reactivation.first recent_deactivation = scheme_deactivation_periods.order("created_at").last diff --git a/app/views/schemes/_scheme_list.html.erb b/app/views/schemes/_scheme_list.html.erb index 976622ce0..5fa712d1e 100644 --- a/app/views/schemes/_scheme_list.html.erb +++ b/app/views/schemes/_scheme_list.html.erb @@ -5,31 +5,31 @@ <% end %> <%= table.head do |head| %> <%= head.row do |row| %> - <% row.cell(header: true, text: "Code", html_attributes: { - scope: "col", - }) %> <% row.cell(header: true, text: "Scheme", html_attributes: { scope: "col", }) %> - <% row.cell(header: true, text: "Locations", html_attributes: { - scope: "col", - }) %> - <% row.cell(header: true, text: "Support provided by", html_attributes: { + <% row.cell(header: true, text: "Code", html_attributes: { scope: "col", }) %> - <% row.cell(header: true, text: "Created", html_attributes: { + <% row.cell(header: true, text: "Locations", html_attributes: { scope: "col", }) %> + <% if FeatureToggle.scheme_toggle_enabled? %> + <% row.cell(header: true, text: "Status", html_attributes: { + scope: "col", + }) %> + <% end %> <% end %> <% end %> <% @schemes.each do |scheme| %> <%= table.body do |body| %> <%= body.row do |row| %> - <% row.cell(text: scheme.id_to_display) %> <% row.cell(text: simple_format(scheme_cell(scheme), { class: "govuk-!-font-weight-bold" }, wrapper_tag: "div")) %> + <% row.cell(text: scheme.id_to_display) %> <% row.cell(text: scheme.locations&.count) %> - <% row.cell(text: scheme.managing_organisation&.name) %> - <% row.cell(text: scheme.confirmed? ? scheme.created_at.to_formatted_s(:govuk_date) : govuk_tag(colour: "grey", text: "Incomplete")) %> + <% if FeatureToggle.scheme_toggle_enabled? %> + <% row.cell(text: status_tag(scheme.status)) %> + <% end %> <% end %> <% end %> <% end %> diff --git a/db/schema.rb b/db/schema.rb index ff2619f43..9df577686 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -374,15 +374,15 @@ ActiveRecord::Schema[7.0].define(version: 2022_11_22_130928) do t.integer "la_known" t.integer "income1" t.integer "income1nk" + t.integer "details_known_2" + t.integer "details_known_3" + t.integer "details_known_4" t.integer "age4" t.integer "age4_known" t.integer "age5" t.integer "age5_known" t.integer "age6" t.integer "age6_known" - t.integer "details_known_2" - t.integer "details_known_3" - t.integer "details_known_4" t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" t.index ["managing_organisation_id"], name: "index_sales_logs_on_managing_organisation_id" t.index ["owning_organisation_id"], name: "index_sales_logs_on_owning_organisation_id" From 8de3570e88e6f0688edbcc0ea9ad009763f0ac77 Mon Sep 17 00:00:00 2001 From: Jack S <113976590+bibblobcode@users.noreply.github.com> Date: Fri, 25 Nov 2022 11:20:32 +0000 Subject: [PATCH 07/19] [CLDC-1513] Add buyer 1 mortgage question (#1025) * Rename section from outgoings to savings * Fix specs * Add inc1mort column --- .../form/sales/pages/buyer1_mortgage.rb | 15 +++++++ .../form/sales/questions/buyer1_mortgage.rb | 16 ++++++++ app/models/form/sales/sections/finances.rb | 2 +- ...ings.rb => income_benefits_and_savings.rb} | 7 ++-- .../20221124102329_add_mortgage1_to_sales.rb | 7 ++++ db/schema.rb | 3 +- spec/factories/sales_log.rb | 1 + .../form/sales/pages/buyer1_mortgage_spec.rb | 33 +++++++++++++++ .../sales/questions/buyer1_mortgage_spec.rb | 40 +++++++++++++++++++ .../form/sales/sections/finances_spec.rb | 2 +- ...rb => income_benefits_and_savings_spec.rb} | 7 ++-- spec/models/form_handler_spec.rb | 4 +- 12 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 app/models/form/sales/pages/buyer1_mortgage.rb create mode 100644 app/models/form/sales/questions/buyer1_mortgage.rb rename app/models/form/sales/subsections/{income_benefits_and_outgoings.rb => income_benefits_and_savings.rb} (50%) create mode 100644 db/migrate/20221124102329_add_mortgage1_to_sales.rb create mode 100644 spec/models/form/sales/pages/buyer1_mortgage_spec.rb create mode 100644 spec/models/form/sales/questions/buyer1_mortgage_spec.rb rename spec/models/form/sales/subsections/{income_benefits_and_outgoings_spec.rb => income_benefits_and_savings_spec.rb} (73%) diff --git a/app/models/form/sales/pages/buyer1_mortgage.rb b/app/models/form/sales/pages/buyer1_mortgage.rb new file mode 100644 index 000000000..e17e9124b --- /dev/null +++ b/app/models/form/sales/pages/buyer1_mortgage.rb @@ -0,0 +1,15 @@ +class Form::Sales::Pages::Buyer1Mortgage < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_1_mortgage" + @header = "" + @description = "" + @subsection = subsection + end + + def questions + @questions ||= [ + Form::Sales::Questions::Buyer1Mortgage.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/questions/buyer1_mortgage.rb b/app/models/form/sales/questions/buyer1_mortgage.rb new file mode 100644 index 000000000..c524c3495 --- /dev/null +++ b/app/models/form/sales/questions/buyer1_mortgage.rb @@ -0,0 +1,16 @@ +class Form::Sales::Questions::Buyer1Mortgage < ::Form::Question + def initialize(id, hsh, page) + super + @id = "inc1mort" + @check_answer_label = "Buyer 1's income used for mortgage application" + @header = "Was buyer 1's income used for a mortgage application" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @page = page + end + + ANSWER_OPTIONS = { + "1" => { "value" => "Yes" }, + "2" => { "value" => "No" }, + }.freeze +end diff --git a/app/models/form/sales/sections/finances.rb b/app/models/form/sales/sections/finances.rb index c2c4082fd..eb5c1a7f5 100644 --- a/app/models/form/sales/sections/finances.rb +++ b/app/models/form/sales/sections/finances.rb @@ -6,7 +6,7 @@ class Form::Sales::Sections::Finances < ::Form::Section @description = "" @form = form @subsections = [ - Form::Sales::Subsections::IncomeBenefitsAndOutgoings.new(nil, nil, self), + Form::Sales::Subsections::IncomeBenefitsAndSavings.new(nil, nil, self), ] end end diff --git a/app/models/form/sales/subsections/income_benefits_and_outgoings.rb b/app/models/form/sales/subsections/income_benefits_and_savings.rb similarity index 50% rename from app/models/form/sales/subsections/income_benefits_and_outgoings.rb rename to app/models/form/sales/subsections/income_benefits_and_savings.rb index 6ec5c6488..a9e7080db 100644 --- a/app/models/form/sales/subsections/income_benefits_and_outgoings.rb +++ b/app/models/form/sales/subsections/income_benefits_and_savings.rb @@ -1,8 +1,8 @@ -class Form::Sales::Subsections::IncomeBenefitsAndOutgoings < ::Form::Subsection +class Form::Sales::Subsections::IncomeBenefitsAndSavings < ::Form::Subsection def initialize(id, hsh, section) super - @id = "income_benefits_and_outgoings" - @label = "Income, benefits and outgoings" + @id = "income_benefits_and_savings" + @label = "Income, benefits and savings" @section = section @depends_on = [{ "setup_completed?" => true }] end @@ -10,6 +10,7 @@ class Form::Sales::Subsections::IncomeBenefitsAndOutgoings < ::Form::Subsection def pages @pages ||= [ Form::Sales::Pages::Buyer1Income.new(nil, nil, self), + Form::Sales::Pages::Buyer1Mortgage.new(nil, nil, self), ] end end diff --git a/db/migrate/20221124102329_add_mortgage1_to_sales.rb b/db/migrate/20221124102329_add_mortgage1_to_sales.rb new file mode 100644 index 000000000..472ac28b7 --- /dev/null +++ b/db/migrate/20221124102329_add_mortgage1_to_sales.rb @@ -0,0 +1,7 @@ +class AddMortgage1ToSales < ActiveRecord::Migration[7.0] + def change + change_table :sales_logs, bulk: true do |t| + t.column :inc1mort, :int + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9df577686..4ceb57dbc 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: 2022_11_22_130928) do +ActiveRecord::Schema[7.0].define(version: 2022_11_24_102329) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -383,6 +383,7 @@ ActiveRecord::Schema[7.0].define(version: 2022_11_22_130928) do t.integer "age5_known" t.integer "age6" t.integer "age6_known" + t.integer "inc1mort" t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" t.index ["managing_organisation_id"], name: "index_sales_logs_on_managing_organisation_id" t.index ["owning_organisation_id"], name: "index_sales_logs_on_owning_organisation_id" diff --git a/spec/factories/sales_log.rb b/spec/factories/sales_log.rb index 8e37581c5..e6c45a009 100644 --- a/spec/factories/sales_log.rb +++ b/spec/factories/sales_log.rb @@ -52,6 +52,7 @@ FactoryBot.define do age6 { 40 } income1nk { 0 } income1 { 10_000 } + inc1mort { 1 } la_known { "1" } la { "E09000003" } end diff --git a/spec/models/form/sales/pages/buyer1_mortgage_spec.rb b/spec/models/form/sales/pages/buyer1_mortgage_spec.rb new file mode 100644 index 000000000..d3aa2e1bb --- /dev/null +++ b/spec/models/form/sales/pages/buyer1_mortgage_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer1Mortgage, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[inc1mort]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_1_mortgage") + end + + it "has the correct header" do + expect(page.header).to eq("") + end + + it "has the correct description" do + expect(page.description).to eq("") + end + + it "has correct depends_on" do + expect(page.depends_on).to be_nil + end +end diff --git a/spec/models/form/sales/questions/buyer1_mortgage_spec.rb b/spec/models/form/sales/questions/buyer1_mortgage_spec.rb new file mode 100644 index 000000000..0e5f49117 --- /dev/null +++ b/spec/models/form/sales/questions/buyer1_mortgage_spec.rb @@ -0,0 +1,40 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer1Mortgage, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("inc1mort") + end + + it "has the correct header" do + expect(question.header).to eq("Was buyer 1's income used for a mortgage application") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 1's income used for mortgage application") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "1" => { "value" => "Yes" }, + "2" => { "value" => "No" }, + }) + end +end diff --git a/spec/models/form/sales/sections/finances_spec.rb b/spec/models/form/sales/sections/finances_spec.rb index 4797f6b4e..6c2f63ded 100644 --- a/spec/models/form/sales/sections/finances_spec.rb +++ b/spec/models/form/sales/sections/finances_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Form::Sales::Sections::Finances, type: :model do it "has correct subsections" do expect(section.subsections.map(&:id)).to eq( %w[ - income_benefits_and_outgoings + income_benefits_and_savings ], ) end diff --git a/spec/models/form/sales/subsections/income_benefits_and_outgoings_spec.rb b/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb similarity index 73% rename from spec/models/form/sales/subsections/income_benefits_and_outgoings_spec.rb rename to spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb index 01028b0a8..0ebbac6ab 100644 --- a/spec/models/form/sales/subsections/income_benefits_and_outgoings_spec.rb +++ b/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndOutgoings, type: :model do +RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model do subject(:subsection) { described_class.new(subsection_id, subsection_definition, section) } let(:subsection_id) { nil } @@ -15,16 +15,17 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndOutgoings, type: :mode expect(subsection.pages.map(&:id)).to eq( %w[ buyer_1_income + buyer_1_mortgage ], ) end it "has the correct id" do - expect(subsection.id).to eq("income_benefits_and_outgoings") + expect(subsection.id).to eq("income_benefits_and_savings") end it "has the correct label" do - expect(subsection.label).to eq("Income, benefits and outgoings") + expect(subsection.label).to eq("Income, benefits and savings") end it "has correct depends on" do diff --git a/spec/models/form_handler_spec.rb b/spec/models/form_handler_spec.rb index e0ef853ae..7db258b53 100644 --- a/spec/models/form_handler_spec.rb +++ b/spec/models/form_handler_spec.rb @@ -61,14 +61,14 @@ RSpec.describe FormHandler do it "is able to load a current sales form" do form = form_handler.get_form("current_sales") expect(form).to be_a(Form) - expect(form.pages.count).to eq(44) + expect(form.pages.count).to eq(45) expect(form.name).to eq("2022_2023_sales") end it "is able to load a previous sales form" do form = form_handler.get_form("previous_sales") expect(form).to be_a(Form) - expect(form.pages.count).to eq(44) + expect(form.pages.count).to eq(45) expect(form.name).to eq("2021_2022_sales") end end From 41c726ce9c8bc0564e3c71c59e38dab09e8459a3 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Fri, 25 Nov 2022 14:22:21 +0000 Subject: [PATCH 08/19] Add a default ordering for schemes (#1030) * Add a default ordering for schemes * Move ordering back to the controller * Update app/controllers/schemes_controller.rb Co-authored-by: James Rose Co-authored-by: James Rose --- app/controllers/schemes_controller.rb | 2 +- spec/models/scheme_spec.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index 415db5c7a..a26471ca1 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -9,7 +9,7 @@ class SchemesController < ApplicationController def index redirect_to schemes_organisation_path(current_user.organisation) unless current_user.support? - all_schemes = Scheme.all.order("service_name ASC") + all_schemes = Scheme.order(confirmed: :asc, service_name: :asc) @pagy, @schemes = pagy(filtered_collection(all_schemes, search_term)) @searched = search_term.presence diff --git a/spec/models/scheme_spec.rb b/spec/models/scheme_spec.rb index 58d0a8da8..43a4112d4 100644 --- a/spec/models/scheme_spec.rb +++ b/spec/models/scheme_spec.rb @@ -175,4 +175,19 @@ RSpec.describe Scheme, type: :model do end end end + + describe "all schemes" do + before do + FactoryBot.create_list(:scheme, 4) + FactoryBot.create_list(:scheme, 3, confirmed: false) + end + + it "can sort the schemes by status" do + all_schemes = described_class.all.order(confirmed: :asc, service_name: :asc) + expect(all_schemes.count).to eq(7) + expect(all_schemes[0].status).to eq(:incomplete) + expect(all_schemes[1].status).to eq(:incomplete) + expect(all_schemes[2].status).to eq(:incomplete) + end + end end From 3ea0c80c2402a284533d9bd58dee09293cdfc433 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Fri, 25 Nov 2022 14:38:01 +0000 Subject: [PATCH 09/19] limit review pipeline to single concurrency (#1033) --- .github/workflows/review_pipeline.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/review_pipeline.yml b/.github/workflows/review_pipeline.yml index 90d7b3b44..22f99afde 100644 --- a/.github/workflows/review_pipeline.yml +++ b/.github/workflows/review_pipeline.yml @@ -1,5 +1,7 @@ name: Review app pipeline +concurrency: ${{ github.workflow }}-${{ github.event.pull_request.number }} + on: pull_request: types: From 464e5f96278c6a9eb744be3cb473bc9627d4a9ef Mon Sep 17 00:00:00 2001 From: Jack S <113976590+bibblobcode@users.noreply.github.com> Date: Fri, 25 Nov 2022 17:11:03 +0000 Subject: [PATCH 10/19] [CLDC-1514] Add buyer 2 income question (#1036) --- app/models/form/sales/pages/buyer2_income.rb | 19 +++++++ .../form/sales/questions/buyer2_income.rb | 14 +++++ .../sales/questions/buyer2_income_known.rb | 21 +++++++ .../income_benefits_and_savings.rb | 1 + .../20221125142847_add_buyer2_to_sales.rb | 6 ++ db/schema.rb | 4 +- spec/factories/sales_log.rb | 2 + .../form/sales/pages/buyer2_income_spec.rb | 33 +++++++++++ .../questions/buyer2_income_known_spec.rb | 55 +++++++++++++++++++ .../sales/questions/buyer2_income_spec.rb | 53 ++++++++++++++++++ .../income_benefits_and_savings_spec.rb | 1 + spec/models/form_handler_spec.rb | 4 +- 12 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 app/models/form/sales/pages/buyer2_income.rb create mode 100644 app/models/form/sales/questions/buyer2_income.rb create mode 100644 app/models/form/sales/questions/buyer2_income_known.rb create mode 100644 db/migrate/20221125142847_add_buyer2_to_sales.rb create mode 100644 spec/models/form/sales/pages/buyer2_income_spec.rb create mode 100644 spec/models/form/sales/questions/buyer2_income_known_spec.rb create mode 100644 spec/models/form/sales/questions/buyer2_income_spec.rb diff --git a/app/models/form/sales/pages/buyer2_income.rb b/app/models/form/sales/pages/buyer2_income.rb new file mode 100644 index 000000000..207b6ec46 --- /dev/null +++ b/app/models/form/sales/pages/buyer2_income.rb @@ -0,0 +1,19 @@ +class Form::Sales::Pages::Buyer2Income < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_income" + @header = "" + @description = "" + @subsection = subsection + @depends_on = [{ + "jointpur" => 1, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::Buyer2IncomeKnown.new(nil, nil, self), + Form::Sales::Questions::Buyer2Income.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/questions/buyer2_income.rb b/app/models/form/sales/questions/buyer2_income.rb new file mode 100644 index 000000000..4abb306a0 --- /dev/null +++ b/app/models/form/sales/questions/buyer2_income.rb @@ -0,0 +1,14 @@ +class Form::Sales::Questions::Buyer2Income < ::Form::Question + def initialize(id, hsh, page) + super + @id = "income2" + @check_answer_label = "Buyer 2’s gross annual income" + @header = "Buyer 2’s gross annual income" + @type = "numeric" + @page = page + @min = 0 + @step = 1 + @width = 5 + @prefix = "£" + end +end diff --git a/app/models/form/sales/questions/buyer2_income_known.rb b/app/models/form/sales/questions/buyer2_income_known.rb new file mode 100644 index 000000000..f0897f6be --- /dev/null +++ b/app/models/form/sales/questions/buyer2_income_known.rb @@ -0,0 +1,21 @@ +class Form::Sales::Questions::Buyer2IncomeKnown < ::Form::Question + def initialize(id, hsh, page) + super + @id = "income2nk" + @check_answer_label = "Buyer 2’s gross annual income" + @header = "Do you know buyer 2’s annual income?" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @page = page + @guidance_position = GuidancePosition::BOTTOM + @guidance_partial = "what_counts_as_income_sales" + @conditional_for = { + "income2" => [0], + } + end + + ANSWER_OPTIONS = { + "0" => { "value" => "Yes" }, + "1" => { "value" => "No" }, + }.freeze +end diff --git a/app/models/form/sales/subsections/income_benefits_and_savings.rb b/app/models/form/sales/subsections/income_benefits_and_savings.rb index a9e7080db..26ab3310b 100644 --- a/app/models/form/sales/subsections/income_benefits_and_savings.rb +++ b/app/models/form/sales/subsections/income_benefits_and_savings.rb @@ -11,6 +11,7 @@ class Form::Sales::Subsections::IncomeBenefitsAndSavings < ::Form::Subsection @pages ||= [ Form::Sales::Pages::Buyer1Income.new(nil, nil, self), Form::Sales::Pages::Buyer1Mortgage.new(nil, nil, self), + Form::Sales::Pages::Buyer2Income.new(nil, nil, self), ] end end diff --git a/db/migrate/20221125142847_add_buyer2_to_sales.rb b/db/migrate/20221125142847_add_buyer2_to_sales.rb new file mode 100644 index 000000000..883ad44dd --- /dev/null +++ b/db/migrate/20221125142847_add_buyer2_to_sales.rb @@ -0,0 +1,6 @@ +class AddBuyer2ToSales < ActiveRecord::Migration[7.0] + change_table :sales_logs, bulk: true do |t| + t.column :income2, :int + t.column :income2nk, :int + end +end diff --git a/db/schema.rb b/db/schema.rb index 4ceb57dbc..e5a4d27e4 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: 2022_11_24_102329) do +ActiveRecord::Schema[7.0].define(version: 2022_11_25_142847) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -384,6 +384,8 @@ ActiveRecord::Schema[7.0].define(version: 2022_11_24_102329) do t.integer "age6" t.integer "age6_known" t.integer "inc1mort" + t.integer "income2" + t.integer "income2nk" t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" t.index ["managing_organisation_id"], name: "index_sales_logs_on_managing_organisation_id" t.index ["owning_organisation_id"], name: "index_sales_logs_on_owning_organisation_id" diff --git a/spec/factories/sales_log.rb b/spec/factories/sales_log.rb index e6c45a009..e55287d67 100644 --- a/spec/factories/sales_log.rb +++ b/spec/factories/sales_log.rb @@ -53,6 +53,8 @@ FactoryBot.define do income1nk { 0 } income1 { 10_000 } inc1mort { 1 } + income2nk { 0 } + income2 { 10_000 } la_known { "1" } la { "E09000003" } end diff --git a/spec/models/form/sales/pages/buyer2_income_spec.rb b/spec/models/form/sales/pages/buyer2_income_spec.rb new file mode 100644 index 000000000..0450ceed1 --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_income_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2Income, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[income2nk income2]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_2_income") + end + + it "has the correct header" do + expect(page.header).to eq("") + end + + it "has the correct description" do + expect(page.description).to eq("") + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([{ "jointpur" => 1 }]) + end +end diff --git a/spec/models/form/sales/questions/buyer2_income_known_spec.rb b/spec/models/form/sales/questions/buyer2_income_known_spec.rb new file mode 100644 index 000000000..06e7afc3e --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_income_known_spec.rb @@ -0,0 +1,55 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2IncomeKnown, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("income2nk") + end + + it "has the correct header" do + expect(question.header).to eq("Do you know buyer 2’s annual income?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 2’s gross annual income") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "0" => { "value" => "Yes" }, + "1" => { "value" => "No" }, + }) + end + + it "has correct conditional for" do + expect(question.conditional_for).to eq({ + "income2" => [0], + }) + end + + it "has the correct guidance_partial" do + expect(question.guidance_partial).to eq("what_counts_as_income_sales") + end + + it "has the correct guidance position", :aggregate_failures do + expect(question.bottom_guidance?).to eq(true) + expect(question.top_guidance?).to eq(false) + end +end diff --git a/spec/models/form/sales/questions/buyer2_income_spec.rb b/spec/models/form/sales/questions/buyer2_income_spec.rb new file mode 100644 index 000000000..b7828918a --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_income_spec.rb @@ -0,0 +1,53 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2Income, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("income2") + end + + it "has the correct header" do + expect(question.header).to eq("Buyer 2’s gross annual income") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 2’s gross annual income") + end + + it "has the correct type" do + expect(question.type).to eq("numeric") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint" do + expect(question.hint_text).to be_nil + end + + it "has correct width" do + expect(question.width).to eq(5) + end + + it "has correct step" do + expect(question.step).to eq(1) + end + + it "has correct prefix" do + expect(question.prefix).to eq("£") + end + + it "has correct min" do + expect(question.min).to eq(0) + end +end diff --git a/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb b/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb index 0ebbac6ab..bea0fe88d 100644 --- a/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb +++ b/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb @@ -16,6 +16,7 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model %w[ buyer_1_income buyer_1_mortgage + buyer_2_income ], ) end diff --git a/spec/models/form_handler_spec.rb b/spec/models/form_handler_spec.rb index 7db258b53..6a335e232 100644 --- a/spec/models/form_handler_spec.rb +++ b/spec/models/form_handler_spec.rb @@ -61,14 +61,14 @@ RSpec.describe FormHandler do it "is able to load a current sales form" do form = form_handler.get_form("current_sales") expect(form).to be_a(Form) - expect(form.pages.count).to eq(45) + expect(form.pages.count).to eq(46) expect(form.name).to eq("2022_2023_sales") end it "is able to load a previous sales form" do form = form_handler.get_form("previous_sales") expect(form).to be_a(Form) - expect(form.pages.count).to eq(45) + expect(form.pages.count).to eq(46) expect(form.name).to eq("2021_2022_sales") end end From 3a60c4cd02f6dc0e7cc0848be93b5b8fa925aa0c Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Mon, 28 Nov 2022 10:46:12 +0000 Subject: [PATCH 11/19] Deactivation does not affect logs (#1018) * Update affected logs count and skip the confirmation page if no logs are affected for locations * Update affected logs count and skip the confirmation page if no logs are affected for schemes * refactor --- app/controllers/locations_controller.rb | 9 +++- app/controllers/schemes_controller.rb | 9 +++- .../locations/deactivate_confirm.html.erb | 2 +- app/views/schemes/deactivate_confirm.html.erb | 2 +- spec/requests/locations_controller_spec.rb | 50 +++++++++++++++--- spec/requests/schemes_controller_spec.rb | 52 ++++++++++++++++--- 6 files changed, 102 insertions(+), 22 deletions(-) diff --git a/app/controllers/locations_controller.rb b/app/controllers/locations_controller.rb index b3f3ab203..1da3bf5c4 100644 --- a/app/controllers/locations_controller.rb +++ b/app/controllers/locations_controller.rb @@ -38,8 +38,13 @@ class LocationsController < ApplicationController end def deactivate_confirm - @deactivation_date = params[:deactivation_date] - @deactivation_date_type = params[:deactivation_date_type] + @affected_logs = @location.lettings_logs.filter_by_before_startdate(params[:deactivation_date]) + if @affected_logs.count.zero? + deactivate + else + @deactivation_date = params[:deactivation_date] + @deactivation_date_type = params[:deactivation_date_type] + end end def deactivate diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index a26471ca1..389446b7f 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -39,8 +39,13 @@ class SchemesController < ApplicationController end def deactivate_confirm - @deactivation_date = params[:deactivation_date] - @deactivation_date_type = params[:deactivation_date_type] + @affected_logs = @scheme.lettings_logs.filter_by_before_startdate(params[:deactivation_date]) + if @affected_logs.count.zero? + deactivate + else + @deactivation_date = params[:deactivation_date] + @deactivation_date_type = params[:deactivation_date_type] + end end def deactivate diff --git a/app/views/locations/deactivate_confirm.html.erb b/app/views/locations/deactivate_confirm.html.erb index 47b7bbf60..6748af918 100644 --- a/app/views/locations/deactivate_confirm.html.erb +++ b/app/views/locations/deactivate_confirm.html.erb @@ -4,7 +4,7 @@ <% end %>

<%= @location.postcode %> - This change will affect <%= @location.lettings_logs.count %> logs + This change will affect <%= @affected_logs.count %> logs

<%= govuk_warning_text text: I18n.t("warnings.location.deactivate.review_logs") %> <%= f.hidden_field :confirm, value: true %> diff --git a/app/views/schemes/deactivate_confirm.html.erb b/app/views/schemes/deactivate_confirm.html.erb index 92479d93b..b5dda160e 100644 --- a/app/views/schemes/deactivate_confirm.html.erb +++ b/app/views/schemes/deactivate_confirm.html.erb @@ -4,7 +4,7 @@ <% end %>

<%= @scheme.service_name %> - This change will affect <%= @scheme.lettings_logs.count %> logs + This change will affect <%= @affected_logs.count %> logs

<%= govuk_warning_text text: I18n.t("warnings.scheme.deactivate.review_logs") %> <%= f.hidden_field :confirm, value: true %> diff --git a/spec/requests/locations_controller_spec.rb b/spec/requests/locations_controller_spec.rb index 237149f7d..b1cffd3bf 100644 --- a/spec/requests/locations_controller_spec.rb +++ b/spec/requests/locations_controller_spec.rb @@ -1243,11 +1243,13 @@ RSpec.describe LocationsController, type: :request do let!(:lettings_log) { FactoryBot.create(:lettings_log, :sh, location:, scheme:, startdate:, owning_organisation: user.organisation) } let(:startdate) { Time.utc(2022, 10, 11) } let(:add_deactivations) { nil } + let(:setup_locations) { nil } before do Timecop.freeze(Time.utc(2022, 10, 10)) sign_in user add_deactivations + setup_locations location.save! patch "/schemes/#{scheme.id}/locations/#{location.id}/new-deactivation", params: end @@ -1259,20 +1261,52 @@ RSpec.describe LocationsController, type: :request do context "with default date" do let(:params) { { location_deactivation_period: { deactivation_date_type: "default", deactivation_date: } } } - it "redirects to the confirmation page" do - follow_redirect! - expect(response).to have_http_status(:ok) - expect(page).to have_content("This change will affect #{location.lettings_logs.count} logs") + context "and affected logs" do + it "redirects to the confirmation page" do + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_content("This change will affect 1 logs") + end + end + + context "and no affected logs" do + let(:setup_locations) { location.lettings_logs.update(location: nil) } + + it "redirects to the location page and updates the deactivation period" do + follow_redirect! + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") + location.reload + expect(location.location_deactivation_periods.count).to eq(1) + expect(location.location_deactivation_periods.first.deactivation_date).to eq(Time.zone.local(2022, 4, 1)) + end end end context "with other date" do let(:params) { { location_deactivation_period: { deactivation_date_type: "other", "deactivation_date(3i)": "10", "deactivation_date(2i)": "10", "deactivation_date(1i)": "2022" } } } - it "redirects to the confirmation page" do - follow_redirect! - expect(response).to have_http_status(:ok) - expect(page).to have_content("This change will affect #{location.lettings_logs.count} logs") + context "and afected logs" do + it "redirects to the confirmation page" do + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_content("This change will affect #{location.lettings_logs.count} logs") + end + end + + context "and no affected logs" do + let(:setup_locations) { location.lettings_logs.update(location: nil) } + + it "redirects to the location page and updates the deactivation period" do + follow_redirect! + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") + location.reload + expect(location.location_deactivation_periods.count).to eq(1) + expect(location.location_deactivation_periods.first.deactivation_date).to eq(Time.zone.local(2022, 10, 10)) + end end end diff --git a/spec/requests/schemes_controller_spec.rb b/spec/requests/schemes_controller_spec.rb index ee9cca533..4de927f8f 100644 --- a/spec/requests/schemes_controller_spec.rb +++ b/spec/requests/schemes_controller_spec.rb @@ -1772,10 +1772,12 @@ RSpec.describe SchemesController, type: :request do let(:deactivation_date) { Time.utc(2022, 10, 10) } let!(:lettings_log) { FactoryBot.create(:lettings_log, :sh, location:, scheme:, startdate:, owning_organisation: user.organisation) } let(:startdate) { Time.utc(2022, 10, 11) } + let(:setup_schemes) { nil } before do Timecop.freeze(Time.utc(2022, 10, 10)) sign_in user + setup_schemes patch "/schemes/#{scheme.id}/new-deactivation", params: end @@ -1786,20 +1788,54 @@ RSpec.describe SchemesController, type: :request do context "with default date" do let(:params) { { scheme_deactivation_period: { deactivation_date_type: "default", deactivation_date: } } } - it "redirects to the confirmation page" do - follow_redirect! - expect(response).to have_http_status(:ok) - expect(page).to have_content("This change will affect #{scheme.lettings_logs.count} logs") + context "and affected logs" do + it "redirects to the confirmation page" do + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_content("This change will affect #{scheme.lettings_logs.count} logs") + end + end + + context "and no affected logs" do + let(:setup_schemes) { scheme.lettings_logs.update(scheme: nil) } + + it "redirects to the location page and updates the deactivation period" do + follow_redirect! + follow_redirect! + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") + scheme.reload + expect(scheme.scheme_deactivation_periods.count).to eq(1) + expect(scheme.scheme_deactivation_periods.first.deactivation_date).to eq(Time.zone.local(2022, 4, 1)) + end end end context "with other date" do let(:params) { { scheme_deactivation_period: { deactivation_date_type: "other", "deactivation_date(3i)": "10", "deactivation_date(2i)": "10", "deactivation_date(1i)": "2022" } } } - it "redirects to the confirmation page" do - follow_redirect! - expect(response).to have_http_status(:ok) - expect(page).to have_content("This change will affect #{scheme.lettings_logs.count} logs") + context "and affected logs" do + it "redirects to the confirmation page" do + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_content("This change will affect #{scheme.lettings_logs.count} logs") + end + end + + context "and no affected logs" do + let(:setup_schemes) { scheme.lettings_logs.update(scheme: nil) } + + it "redirects to the location page and updates the deactivation period" do + follow_redirect! + follow_redirect! + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") + scheme.reload + expect(scheme.scheme_deactivation_periods.count).to eq(1) + expect(scheme.scheme_deactivation_periods.first.deactivation_date).to eq(Time.zone.local(2022, 10, 10)) + end end end From 5bd41fae995ba322f0cd781e09f69943bcac7093 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Tue, 29 Nov 2022 13:39:32 +0000 Subject: [PATCH 12/19] bind S3 buckets for review apps (#1037) --- .github/workflows/review_pipeline.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/review_pipeline.yml b/.github/workflows/review_pipeline.yml index 22f99afde..603a8315c 100644 --- a/.github/workflows/review_pipeline.yml +++ b/.github/workflows/review_pipeline.yml @@ -145,6 +145,14 @@ jobs: run: | cf bind-service $APP_NAME $SERVICE_NAME --wait + - name: Bind S3 buckets services + env: + APP_NAME: dluhc-core-review-${{ github.event.pull_request.number }} + run: | + cf bind-service $APP_NAME dluhc-core-review-csv-bucket --wait + cf bind-service $APP_NAME dluhc-core-review-export-bucket --wait + cf bind-service $APP_NAME dluhc-core-review-import-bucket --wait + - name: Start review app env: APP_NAME: dluhc-core-review-${{ github.event.pull_request.number }} From e125a698dd5136e5fa1702295056d0b995b17fc0 Mon Sep 17 00:00:00 2001 From: SamSeed-Softwire <63662292+SamSeed-Softwire@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:02:00 +0000 Subject: [PATCH 13/19] Add troubleshooting review apps section to docs (#1041) * docs: add troubleshooting review apps section * docs: rename overall section title Co-authored-by: James Rose * docs: fix typos Co-authored-by: James Rose Co-authored-by: James Rose --- docs/infrastructure.md | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/infrastructure.md b/docs/infrastructure.md index 63bbf2e42..81dc3f165 100644 --- a/docs/infrastructure.md +++ b/docs/infrastructure.md @@ -122,6 +122,57 @@ After a sucessful deployment a comment will be added to the pull request with th Once a pull request has been closed the review app infrastructure will be tore down to save on any costs. Should you wish to re-open a closed pull request the review app will be spun up again. +### How to fix review app deployment failures + +One reason a review app deployment might fail is that it is attempting to run migrations which conflict with data in the database. For example you might have introduced a unique constraint, but the database associated with the review app has duplicate data in it that would violate this constraint, and so the migration cannot be run. There are two main ways to remedy this: + +**Method 1 - Edit database via console** +1. Log in to Cloud Foundry + ```bash + cf login -a api.london.cloud.service.gov.uk -u + ``` + * Your username should be the email address you signed up to GOVUK PaaS with. + * Choose the dev environment whilst logging in. +2. If you were already logged in then Cloud Foundry, then instead just target the dev environment + ```bash + cf target -o dluhc-core -s dev + ``` +3. Find the name of your app + ```bash + cf apps + ``` + * The app name will be in this format: `dluhc-core-review-`. +4. Open a console for your app + ```bash + cf ssh -t -c "/tmp/lifecycle/launcher /home/vcap/app 'rails console' ''" + ``` +5. Edit the database as appropriate, e.g. delete dodgy data and recreate correctly + +**Method 2 - Nuke and restart** + +1. Find the name of your app + ```bash + cf apps + ``` + * The app name will be in this format: `dluhc-core-review-`. +2. Delete the app + ```bash + cf delete + ``` +3. Find the name of the matching Postgres service + ```bash + cf services + ``` + * The service name will be in this format: `dluhc-core-review--postgres`. +4. Delete the service + ```bash + cf delete-service + ``` + * Use `cf services` or `cf service ` to check the operation status. + * There's no need to delete the Redis service. +5. Re-run the whole review app pipeline in GitHub + * If it fails it's likely that the deletion from the previous step hadn't completed yet. So just wait a few minutes and re-run the pipeline again. + ## Setting up Infrastructure for a new environment ### Staging From a91c8da62ee9de5ce6be6f21b1cd02c617e78a57 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Wed, 30 Nov 2022 11:07:24 +0000 Subject: [PATCH 14/19] Update available_from methods (#1026) * Update available_from methods * Move collection start date logic to form handler --- app/models/form_handler.rb | 5 +++ app/models/location.rb | 4 ++- app/models/scheme.rb | 2 +- spec/helpers/locations_helper_spec.rb | 4 +-- spec/models/location_spec.rb | 44 +++++++++++++++++++++++++++ spec/models/scheme_spec.rb | 34 +++++++++++++++++++++ 6 files changed, 89 insertions(+), 4 deletions(-) diff --git a/app/models/form_handler.rb b/app/models/form_handler.rb index 61b981436..03de5e290 100644 --- a/app/models/form_handler.rb +++ b/app/models/form_handler.rb @@ -49,6 +49,11 @@ class FormHandler today < window_end_date ? today.year - 1 : today.year end + def collection_start_date(date) + window_end_date = Time.zone.local(date.year, 4, 1) + date < window_end_date ? Time.zone.local(date.year - 1, 4, 1) : Time.zone.local(date.year, 4, 1) + end + def current_collection_start_date Time.zone.local(current_collection_start_year, 4, 1) end diff --git a/app/models/location.rb b/app/models/location.rb index 46cb1da6c..4fa161e0b 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -370,7 +370,9 @@ class Location < ApplicationRecord end def available_from - startdate || [created_at, FormHandler.instance.current_collection_start_date].min + return startdate if startdate.present? + + FormHandler.instance.collection_start_date(created_at) end def status diff --git a/app/models/scheme.rb b/app/models/scheme.rb index fce696bb6..73aecd6ac 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -210,7 +210,7 @@ class Scheme < ApplicationRecord end def available_from - [created_at, FormHandler.instance.current_collection_start_date].min + FormHandler.instance.collection_start_date(created_at) end def status diff --git a/spec/helpers/locations_helper_spec.rb b/spec/helpers/locations_helper_spec.rb index 4d9340542..c80449ce6 100644 --- a/spec/helpers/locations_helper_spec.rb +++ b/spec/helpers/locations_helper_spec.rb @@ -154,11 +154,11 @@ RSpec.describe LocationsHelper do context "when viewing availability" do context "with no deactivations" do - it "displays created_at as availability date if startdate is not present" do + it "displays previous collection start date as availability date if created_at is earlier than collection start date" do location.update!(startdate: nil) availability_attribute = display_location_attributes(location).find { |x| x[:name] == "Availability" }[:value] - expect(availability_attribute).to eq("Active from #{location.created_at.to_formatted_s(:govuk_date)}") + expect(availability_attribute).to eq("Active from 1 April 2021") end it "displays current collection start date as availability date if created_at is later than collection start date" do diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb index dd5d697e9..58bd3872e 100644 --- a/spec/models/location_spec.rb +++ b/spec/models/location_spec.rb @@ -208,4 +208,48 @@ RSpec.describe Location, type: :model do end end end + + describe "available_from" do + context "when there is a startdate" do + let(:location) { FactoryBot.build(:location, startdate: Time.zone.local(2022, 4, 6)) } + + it "returns the startdate" do + expect(location.available_from).to eq(Time.zone.local(2022, 4, 6)) + end + end + + context "when there is no start date" do + context "and the location was created at the start of the 2022/23 collection window" do + let(:location) { FactoryBot.build(:location, created_at: Time.zone.local(2022, 4, 6), startdate: nil) } + + it "returns the beginning of 22/23 collection window" do + expect(location.available_from).to eq(Time.zone.local(2022, 4, 1)) + end + end + + context "and the location was created at the end of the 2022/23 collection window" do + let(:location) { FactoryBot.build(:location, created_at: Time.zone.local(2023, 2, 6), startdate: nil) } + + it "returns the beginning of 22/23 collection window" do + expect(location.available_from).to eq(Time.zone.local(2022, 4, 1)) + end + end + + context "and the location was created at the start of the 2021/22 collection window" do + let(:location) { FactoryBot.build(:location, created_at: Time.zone.local(2021, 4, 6), startdate: nil) } + + it "returns the beginning of 21/22 collection window" do + expect(location.available_from).to eq(Time.zone.local(2021, 4, 1)) + end + end + + context "and the location was created at the end of the 2021/22 collection window" do + let(:location) { FactoryBot.build(:location, created_at: Time.zone.local(2022, 2, 6), startdate: nil) } + + it "returns the beginning of 21/22 collection window" do + expect(location.available_from).to eq(Time.zone.local(2021, 4, 1)) + end + end + end + end end diff --git a/spec/models/scheme_spec.rb b/spec/models/scheme_spec.rb index 43a4112d4..d6bacb11a 100644 --- a/spec/models/scheme_spec.rb +++ b/spec/models/scheme_spec.rb @@ -190,4 +190,38 @@ RSpec.describe Scheme, type: :model do expect(all_schemes[2].status).to eq(:incomplete) end end + + describe "available_from" do + context "when the scheme was created at the start of the 2022/23 collection window" do + let(:scheme) { FactoryBot.build(:scheme, created_at: Time.zone.local(2022, 4, 6)) } + + it "returns the beginning of 22/23 collection window" do + expect(scheme.available_from).to eq(Time.zone.local(2022, 4, 1)) + end + end + + context "when the scheme was created at the end of the 2022/23 collection window" do + let(:scheme) { FactoryBot.build(:scheme, created_at: Time.zone.local(2023, 2, 6)) } + + it "returns the beginning of 22/23 collection window" do + expect(scheme.available_from).to eq(Time.zone.local(2022, 4, 1)) + end + end + + context "when the scheme was created at the start of the 2021/22 collection window" do + let(:scheme) { FactoryBot.build(:scheme, created_at: Time.zone.local(2021, 4, 6)) } + + it "returns the beginning of 21/22 collection window" do + expect(scheme.available_from).to eq(Time.zone.local(2021, 4, 1)) + end + end + + context "when the scheme was created at the end of the 2021/22 collection window" do + let(:scheme) { FactoryBot.build(:scheme, created_at: Time.zone.local(2022, 2, 6)) } + + it "returns the beginning of 21/22 collection window" do + expect(scheme.available_from).to eq(Time.zone.local(2021, 4, 1)) + end + end + end end From 409a24b3336cd92c4e8ad13143146aa88fb40c0d Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Wed, 30 Nov 2022 11:46:08 +0000 Subject: [PATCH 15/19] Cldc 1609 schemes and location validations (#1022) * Add base errors for start date * Add validation to location * add validation to scheme id * update error messages * wip * return dates for errors * choose newest reactivation date * Add validation to scheme_id * Fix validations in tests * add diferent error message * fix some edge dates and add activating_soon schemes error * fix error message * move status_during_startdate into validations files * rebase * refactor * rename method * remove reverse and update ordering * Extract scheme validation method * Refactor validations * Refactor status validations --- app/models/location.rb | 20 +- app/models/scheme.rb | 19 +- app/models/validations/date_validations.rb | 5 + app/models/validations/setup_validations.rb | 11 + app/models/validations/shared_validations.rb | 35 ++++ config/locales/en.yml | 8 + ...nswers_summary_list_card_component_spec.rb | 2 +- spec/factories/lettings_log.rb | 4 +- spec/factories/location.rb | 2 +- spec/factories/scheme.rb | 2 +- spec/features/form/check_answers_page_spec.rb | 3 +- spec/features/form/validations_spec.rb | 1 + .../fixtures/files/lettings_logs_download.csv | 2 +- .../lettings_logs_download_non_support.csv | 2 +- spec/helpers/schemes_helper_spec.rb | 2 +- spec/jobs/email_csv_job_spec.rb | 3 +- spec/models/lettings_log_spec.rb | 4 +- .../validations/date_validations_spec.rb | 147 +++++++++++++ .../validations/setup_validations_spec.rb | 194 ++++++++++++++++++ spec/requests/form_controller_spec.rb | 1 + spec/requests/locations_controller_spec.rb | 10 +- spec/requests/schemes_controller_spec.rb | 2 +- .../lettings_log_export_service_spec.rb | 8 +- .../lettings_logs_import_service_spec.rb | 6 +- 24 files changed, 454 insertions(+), 39 deletions(-) diff --git a/app/models/location.rb b/app/models/location.rb index 4fa161e0b..5ba6bf3e4 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -375,17 +375,23 @@ class Location < ApplicationRecord FormHandler.instance.collection_start_date(created_at) end - def status - open_deactivation = location_deactivation_periods.deactivations_without_reactivation.first - recent_deactivation = location_deactivation_periods.order("created_at").last + def open_deactivation + location_deactivation_periods.deactivations_without_reactivation.first + end + + def recent_deactivation + location_deactivation_periods.order("created_at").last + end - return :deactivated if open_deactivation&.deactivation_date.present? && Time.zone.now >= open_deactivation.deactivation_date - return :deactivating_soon if open_deactivation&.deactivation_date.present? && Time.zone.now < open_deactivation.deactivation_date - return :reactivating_soon if recent_deactivation&.reactivation_date.present? && Time.zone.now < recent_deactivation.reactivation_date - return :activating_soon if startdate.present? && Time.zone.now < startdate + def status(date = Time.zone.now) + return :deactivated if open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date + return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date + return :reactivating_soon if recent_deactivation&.reactivation_date.present? && date < recent_deactivation.reactivation_date + return :activating_soon if startdate.present? && date < startdate :active end + alias_method :status_at, :status def active? status == :active diff --git a/app/models/scheme.rb b/app/models/scheme.rb index 73aecd6ac..bc243d449 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -213,18 +213,23 @@ class Scheme < ApplicationRecord FormHandler.instance.collection_start_date(created_at) end - def status - return :incomplete unless confirmed + def open_deactivation + scheme_deactivation_periods.deactivations_without_reactivation.first + end - open_deactivation = scheme_deactivation_periods.deactivations_without_reactivation.first - recent_deactivation = scheme_deactivation_periods.order("created_at").last + def recent_deactivation + scheme_deactivation_periods.order("created_at").last + end - return :deactivated if open_deactivation&.deactivation_date.present? && Time.zone.now >= open_deactivation.deactivation_date - return :deactivating_soon if open_deactivation&.deactivation_date.present? && Time.zone.now < open_deactivation.deactivation_date - return :reactivating_soon if recent_deactivation&.reactivation_date.present? && Time.zone.now < recent_deactivation.reactivation_date + def status(date = Time.zone.now) + return :incomplete unless confirmed + return :deactivated if open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date + return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date + return :reactivating_soon if recent_deactivation&.reactivation_date.present? && date < recent_deactivation.reactivation_date :active end + alias_method :status_at, :status def active? status == :active diff --git a/app/models/validations/date_validations.rb b/app/models/validations/date_validations.rb index 0353abd26..ceec8ed9a 100644 --- a/app/models/validations/date_validations.rb +++ b/app/models/validations/date_validations.rb @@ -1,4 +1,6 @@ module Validations::DateValidations + include Validations::SharedValidations + def validate_property_major_repairs(record) date_valid?("mrcdate", record) if record["startdate"].present? && record["mrcdate"].present? && record["startdate"] < record["mrcdate"] @@ -59,6 +61,9 @@ module Validations::DateValidations if record["mrcdate"].present? && record.startdate < record["mrcdate"] record.errors.add :startdate, I18n.t("validations.setup.startdate.after_major_repair_date") end + + location_during_startdate_validation(record, :startdate) + scheme_during_startdate_validation(record, :startdate) end private diff --git a/app/models/validations/setup_validations.rb b/app/models/validations/setup_validations.rb index 44770c947..c1bdc3a71 100644 --- a/app/models/validations/setup_validations.rb +++ b/app/models/validations/setup_validations.rb @@ -1,10 +1,21 @@ module Validations::SetupValidations + include Validations::SharedValidations + def validate_irproduct_other(record) if intermediate_product_rent_type?(record) && record.irproduct_other.blank? record.errors.add :irproduct_other, I18n.t("validations.setup.intermediate_rent_product_name.blank") end end + def validate_location(record) + location_during_startdate_validation(record, :location_id) + end + + def validate_scheme(record) + location_during_startdate_validation(record, :scheme_id) + scheme_during_startdate_validation(record, :scheme_id) + end + private def intermediate_product_rent_type?(record) diff --git a/app/models/validations/shared_validations.rb b/app/models/validations/shared_validations.rb index 9b4f8a3ff..2694fd743 100644 --- a/app/models/validations/shared_validations.rb +++ b/app/models/validations/shared_validations.rb @@ -33,4 +33,39 @@ module Validations::SharedValidations end end end + + def location_during_startdate_validation(record, field) + location_inactive_status = inactive_status(record.startdate, record.location) + + if location_inactive_status.present? + date, scope, deactivation_date = location_inactive_status.values_at(:date, :scope, :deactivation_date) + record.errors.add field, I18n.t("validations.setup.startdate.location.#{scope}", postcode: record.location.postcode, date:, deactivation_date:) + end + end + + def scheme_during_startdate_validation(record, field) + scheme_inactive_status = inactive_status(record.startdate, record.scheme) + if scheme_inactive_status.present? + date, scope, deactivation_date = scheme_inactive_status.values_at(:date, :scope, :deactivation_date) + record.errors.add field, I18n.t("validations.setup.startdate.scheme.#{scope}", name: record.scheme.service_name, date:, deactivation_date:) + end + end + + def inactive_status(date, resource) + return if date.blank? || resource.blank? + + status = resource.status_at(date) + return unless %i[reactivating_soon activating_soon deactivated].include?(status) + + closest_reactivation = resource.recent_deactivation + open_deactivation = resource.open_deactivation + + date = case status + when :reactivating_soon then closest_reactivation.reactivation_date + when :activating_soon then resource&.available_from + when :deactivated then open_deactivation.deactivation_date + end + + { scope: status, date: date&.to_formatted_s(:govuk_date), deactivation_date: closest_reactivation&.deactivation_date&.to_formatted_s(:govuk_date) } + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 5a8843e46..43e03c814 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -120,6 +120,14 @@ en: before_scheme_end_date: "The tenancy start date must be before the end date for this supported housing scheme" after_void_date: "Enter a tenancy start date that is after the void date" after_major_repair_date: "Enter a tenancy start date that is after the major repair date" + location: + deactivated: "The location %{postcode} was deactivated on %{date} and was not available on the day you entered." + reactivating_soon: "The location %{postcode} was deactivated on %{deactivation_date} and is not available on the date you entered. It reactivates on %{date}" + activating_soon: "The location %{postcode} is not available until %{date}. Enter a tenancy start date after %{date}" + scheme: + deactivated: "%{name} was deactivated on %{date} and was not available on the day you entered" + reactivating_soon: "%{name} was deactivated on %{deactivation_date} and is not available on the date you entered. It reactivates on %{date}" + activating_soon: "%{name} is not available until %{date}. Enter a tenancy start date after %{date}" property: mrcdate: diff --git a/spec/components/check_answers_summary_list_card_component_spec.rb b/spec/components/check_answers_summary_list_card_component_spec.rb index 6273da69d..6e63c90cd 100644 --- a/spec/components/check_answers_summary_list_card_component_spec.rb +++ b/spec/components/check_answers_summary_list_card_component_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe CheckAnswersSummaryListCardComponent, type: :component do context "when given a set of questions" do let(:user) { FactoryBot.build(:user) } - let(:log) { FactoryBot.build(:lettings_log, :completed, age2: 99) } + let(:log) { FactoryBot.build(:lettings_log, :completed, age2: 99, startdate: Time.zone.local(2021, 5, 1)) } let(:subsection_id) { "household_characteristics" } let(:subsection) { log.form.get_subsection(subsection_id) } let(:questions) { subsection.applicable_questions(log) } diff --git a/spec/factories/lettings_log.rb b/spec/factories/lettings_log.rb index d337bc592..217e8a605 100644 --- a/spec/factories/lettings_log.rb +++ b/spec/factories/lettings_log.rb @@ -60,7 +60,7 @@ FactoryBot.define do illness { 1 } preg_occ { 2 } startertenancy { 1 } - tenancylength { 5 } + tenancylength { nil } tenancy { 1 } ppostcode_full { Faker::Address.postcode } rsnvac { 6 } @@ -134,7 +134,7 @@ FactoryBot.define do property_relet { 0 } mrcdate { Time.zone.local(2020, 5, 5, 10, 36, 49) } incref { 0 } - startdate { Time.utc(2022, 2, 2, 10, 36, 49) } + startdate { Time.zone.today } armedforces { 1 } builtype { 1 } unitletas { 2 } diff --git a/spec/factories/location.rb b/spec/factories/location.rb index 3359f64cd..870140cd5 100644 --- a/spec/factories/location.rb +++ b/spec/factories/location.rb @@ -7,7 +7,7 @@ FactoryBot.define do mobility_type { %w[A M N W X].sample } location_code { "E09000033" } location_admin_district { "Westminster" } - startdate { Faker::Date.between(from: 6.months.ago, to: Time.zone.today) } + startdate { nil } confirmed { true } scheme trait :export do diff --git a/spec/factories/scheme.rb b/spec/factories/scheme.rb index 4a85f2036..031d9b8c1 100644 --- a/spec/factories/scheme.rb +++ b/spec/factories/scheme.rb @@ -12,7 +12,7 @@ FactoryBot.define do owning_organisation { FactoryBot.create(:organisation) } managing_organisation { FactoryBot.create(:organisation) } confirmed { true } - created_at { Time.zone.now } + created_at { Time.zone.local(2021, 4, 1) } trait :export do sensitive { 1 } registered_under_care_act { 1 } diff --git a/spec/features/form/check_answers_page_spec.rb b/spec/features/form/check_answers_page_spec.rb index 03d237dd2..5f4665725 100644 --- a/spec/features/form/check_answers_page_spec.rb +++ b/spec/features/form/check_answers_page_spec.rb @@ -7,7 +7,7 @@ RSpec.describe "Form Check Answers Page" do let(:subsection) { "household-characteristics" } let(:conditional_subsection) { "conditional-question" } let(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) } - let(:location) { FactoryBot.create(:location, scheme:, mobility_type: "N") } + let(:location) { FactoryBot.create(:location, scheme:, mobility_type: "N", startdate: Time.zone.local(2021, 4, 1)) } let(:lettings_log) do FactoryBot.create( @@ -36,6 +36,7 @@ RSpec.describe "Form Check Answers Page" do :completed, owning_organisation: user.organisation, managing_organisation: user.organisation, + startdate: Time.zone.local(2021, 5, 1), ) end let(:id) { lettings_log.id } diff --git a/spec/features/form/validations_spec.rb b/spec/features/form/validations_spec.rb index 54d4f1a48..0790096e2 100644 --- a/spec/features/form/validations_spec.rb +++ b/spec/features/form/validations_spec.rb @@ -28,6 +28,7 @@ RSpec.describe "validations" do managing_organisation: user.organisation, status: 1, declaration: nil, + startdate: Time.zone.local(2021, 5, 1), ) end let(:id) { lettings_log.id } diff --git a/spec/fixtures/files/lettings_logs_download.csv b/spec/fixtures/files/lettings_logs_download.csv index f16b5f8ad..18c679926 100644 --- a/spec/fixtures/files/lettings_logs_download.csv +++ b/spec/fixtures/files/lettings_logs_download.csv @@ -1,2 +1,2 @@ id,status,created_at,updated_at,created_by_name,is_dpo,owning_organisation_name,managing_organisation_name,collection_start_year,needstype,renewal,startdate,rent_type_detail,irproduct_other,tenancycode,propcode,age1,sex1,ecstat1,hhmemb,relat2,age2,sex2,retirement_value_check,ecstat2,armedforces,leftreg,illness,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_h,is_previous_la_inferred,prevloc_label,prevloc,illness_type_1,illness_type_2,is_la_inferred,la_label,la,postcode_known,postcode_full,previous_la_known,wchair,preg_occ,cbl,earnings,incfreq,net_income_value_check,benefits,hb,period,brent,scharge,pscharge,supcharg,tcharge,offered,layear,ppostcode_full,mrcdate,declaration,ethnic,national,prevten,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,reservist,startertenancy,tenancylength,tenancy,rsnvac,unittype_gn,beds,waityear,reasonpref,chr,cap,reasonother,housingneeds_f,housingneeds_g,illness_type_3,illness_type_4,illness_type_8,illness_type_5,illness_type_6,illness_type_7,illness_type_9,illness_type_10,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,tenancyother,property_owner_organisation,property_manager_organisation,purchaser_code,reason,majorrepairs,hbrentshortfall,property_relet,incref,first_time_property_let_as_social_housing,unitletas,builtype,voiddate,renttype,lettype,totchild,totelder,totadult,net_income_known,nocharge,is_carehome,household_charge,referral,tshortfall,chcharge,ppcodenk,age1_known,age2_known,age3_known,age4_known,age5_known,age6_known,age7_known,age8_known,ethnic_group,letting_allocation_unknown,details_known_2,details_known_3,details_known_4,details_known_5,details_known_6,details_known_7,details_known_8,has_benefits,wrent,wscharge,wpschrge,wsupchrg,wtcharge,wtshortfall,refused,housingneeds,wchchrg,newprop,relat3,relat4,relat5,relat6,relat7,relat8,rent_value_check,old_form_id,lar,irproduct,old_id,joint,illness_type_0,tshortfall_known,sheltered,pregnancy_value_check,hhtype,new_old,vacdays,major_repairs_date_value_check,void_date_value_check,housingneeds_type,housingneeds_other,unittype_sh,scheme_code,scheme_service_name,scheme_sensitive,scheme_type,scheme_registered_under_care_act,scheme_owning_organisation_name,scheme_managing_organisation_name,scheme_primary_client_group,scheme_has_other_client_group,scheme_secondary_client_group,scheme_support_type,scheme_intended_stay,scheme_created_at,location_code,location_postcode,location_name,location_units,location_type_of_unit,location_mobility_type,location_admin_district,location_startdate -{id},in_progress,2022-02-08 16:52:15 +0000,2022-02-08 16:52:15 +0000,Danny Rojas,No,DLUHC,DLUHC,2021,Supported housing,,2 October 2021,London Affordable Rent,,,,,,,,,,,,,,,,,,,,No,,,,,No,Westminster,E09000033,,SE1 1TE,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,2,8,0,0,0,,0,,,,,,,,,,,,,,,,,,,,,,,,0,,,,,,,0,,,,,,,,,,,,,,,,,,,,9,1,,,,,,6,{scheme_code},{scheme_service_name},{scheme_sensitive},Missing,No,DLUHC,DLUHC,{scheme_primary_client_group},,{scheme_secondary_client_group},{scheme_support_type},{scheme_intended_stay},2022-06-05 01:00:00 +0100,{location_code},SE1 1TE,Downing Street,20,Bungalow,Fitted with equipment and adaptations,Westminster,{location_startdate} +{id},in_progress,2022-02-08 16:52:15 +0000,2022-02-08 16:52:15 +0000,Danny Rojas,No,DLUHC,DLUHC,2021,Supported housing,,2 October 2021,London Affordable Rent,,,,,,,,,,,,,,,,,,,,No,,,,,No,Westminster,E09000033,,SE1 1TE,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,2,8,0,0,0,,0,,,,,,,,,,,,,,,,,,,,,,,,0,,,,,,,0,,,,,,,,,,,,,,,,,,,,9,1,,,,,,6,{scheme_code},{scheme_service_name},{scheme_sensitive},Missing,No,DLUHC,DLUHC,{scheme_primary_client_group},,{scheme_secondary_client_group},{scheme_support_type},{scheme_intended_stay},2021-04-01 00:00:00 +0100,{location_code},SE1 1TE,Downing Street,20,Bungalow,Fitted with equipment and adaptations,Westminster,{location_startdate} diff --git a/spec/fixtures/files/lettings_logs_download_non_support.csv b/spec/fixtures/files/lettings_logs_download_non_support.csv index ae0beeee0..0687c536e 100644 --- a/spec/fixtures/files/lettings_logs_download_non_support.csv +++ b/spec/fixtures/files/lettings_logs_download_non_support.csv @@ -1,2 +1,2 @@ id,status,created_at,updated_at,created_by_name,is_dpo,owning_organisation_name,managing_organisation_name,collection_start_year,renewal,startdate,irproduct_other,tenancycode,propcode,age1,sex1,ecstat1,relat2,age2,sex2,ecstat2,armedforces,leftreg,illness,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_h,prevloc_label,illness_type_1,illness_type_2,la_label,postcode_full,wchair,preg_occ,cbl,earnings,incfreq,benefits,hb,period,brent,scharge,pscharge,supcharg,tcharge,offered,layear,ppostcode_full,mrcdate,declaration,ethnic,national,prevten,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,reservist,startertenancy,tenancylength,tenancy,rsnvac,unittype_gn,beds,waityear,reasonpref,chr,cap,reasonother,housingneeds_f,housingneeds_g,illness_type_3,illness_type_4,illness_type_8,illness_type_5,illness_type_6,illness_type_7,illness_type_9,illness_type_10,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,tenancyother,property_owner_organisation,property_manager_organisation,purchaser_code,reason,majorrepairs,hbrentshortfall,property_relet,incref,unitletas,builtype,voiddate,lettype,nocharge,household_charge,referral,tshortfall,chcharge,ppcodenk,ethnic_group,has_benefits,refused,housingneeds,wchchrg,newprop,relat3,relat4,relat5,relat6,relat7,relat8,lar,irproduct,joint,illness_type_0,sheltered,major_repairs_date_value_check,void_date_value_check,housingneeds_type,housingneeds_other,unittype_sh,scheme_code,scheme_service_name,scheme_sensitive,scheme_type,scheme_registered_under_care_act,scheme_owning_organisation_name,scheme_managing_organisation_name,scheme_primary_client_group,scheme_has_other_client_group,scheme_secondary_client_group,scheme_support_type,scheme_intended_stay,scheme_created_at,location_code,location_postcode,location_name,location_units,location_type_of_unit,location_mobility_type,location_admin_district,location_startdate -{id},in_progress,2022-02-08 16:52:15 +0000,2022-02-08 16:52:15 +0000,Danny Rojas,No,DLUHC,DLUHC,2021,,2 October 2021,,,,,,,,,,,,,,,,,,,,,Westminster,SE1 1TE,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,8,0,,,,,,,0,0,,,,,,,,,,,,,,,,,,,6,{scheme_code},{scheme_service_name},{scheme_sensitive},Missing,No,DLUHC,DLUHC,{scheme_primary_client_group},,{scheme_secondary_client_group},{scheme_support_type},{scheme_intended_stay},2022-06-05 01:00:00 +0100,{location_code},SE1 1TE,Downing Street,20,Bungalow,Fitted with equipment and adaptations,Westminster,{location_startdate} +{id},in_progress,2022-02-08 16:52:15 +0000,2022-02-08 16:52:15 +0000,Danny Rojas,No,DLUHC,DLUHC,2021,,2 October 2021,,,,,,,,,,,,,,,,,,,,,Westminster,SE1 1TE,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,8,0,,,,,,,0,0,,,,,,,,,,,,,,,,,,,6,{scheme_code},{scheme_service_name},{scheme_sensitive},Missing,No,DLUHC,DLUHC,{scheme_primary_client_group},,{scheme_secondary_client_group},{scheme_support_type},{scheme_intended_stay},2021-04-01 00:00:00 +0100,{location_code},SE1 1TE,Downing Street,20,Bungalow,Fitted with equipment and adaptations,Westminster,{location_startdate} diff --git a/spec/helpers/schemes_helper_spec.rb b/spec/helpers/schemes_helper_spec.rb index 96f472457..b1e9f9c96 100644 --- a/spec/helpers/schemes_helper_spec.rb +++ b/spec/helpers/schemes_helper_spec.rb @@ -2,7 +2,7 @@ require "rails_helper" RSpec.describe SchemesHelper do describe "Active periods" do - let(:scheme) { FactoryBot.create(:scheme) } + let(:scheme) { FactoryBot.create(:scheme, created_at: Time.zone.today) } before do Timecop.freeze(2022, 10, 10) diff --git a/spec/jobs/email_csv_job_spec.rb b/spec/jobs/email_csv_job_spec.rb index aa27c421b..f4939b855 100644 --- a/spec/jobs/email_csv_job_spec.rb +++ b/spec/jobs/email_csv_job_spec.rb @@ -33,7 +33,8 @@ describe EmailCsvJob do :completed, owning_organisation: organisation, managing_organisation: organisation, - created_by: user) + created_by: user, + startdate: Time.zone.local(2021, 5, 1)) allow(Storage::S3Service).to receive(:new).and_return(storage_service) allow(storage_service).to receive(:write_file) diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index 45a24b964..15936b767 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -1672,7 +1672,7 @@ RSpec.describe LettingsLog do let(:scheme) { FactoryBot.create(:scheme) } let!(:location) { FactoryBot.create(:location, scheme:) } - before { lettings_log.update!(scheme:) } + before { lettings_log.update!(startdate: Time.zone.local(2022, 4, 2), scheme:) } it "derives the scheme location" do record_from_db = ActiveRecord::Base.connection.execute("select location_id from lettings_logs where id=#{lettings_log.id}").to_a[0] @@ -2375,7 +2375,7 @@ RSpec.describe LettingsLog do describe "csv download" do let(:scheme) { FactoryBot.create(:scheme) } - let(:location) { FactoryBot.create(:location, :export, scheme:, type_of_unit: 6, postcode: "SE11TE") } + let(:location) { FactoryBot.create(:location, :export, scheme:, type_of_unit: 6, postcode: "SE11TE", startdate: Time.zone.local(2021, 10, 1)) } let(:user) { FactoryBot.create(:user, organisation: location.scheme.owning_organisation) } let(:expected_content) { csv_export_file.read } diff --git a/spec/models/validations/date_validations_spec.rb b/spec/models/validations/date_validations_spec.rb index 0b20ab07a..87351b9dd 100644 --- a/spec/models/validations/date_validations_spec.rb +++ b/spec/models/validations/date_validations_spec.rb @@ -83,6 +83,153 @@ RSpec.describe Validations::DateValidations do date_validator.validate_startdate(record) expect(record.errors["startdate"]).to be_empty end + + context "with a deactivated location" do + let(:scheme) { create(:scheme) } + let(:location) { create(:location, scheme:, startdate: nil) } + + before do + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), location:) + location.reload + end + + it "produces error when tenancy start date is during deactivated location period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.location = location + date_validator.validate_startdate(record) + expect(record.errors["startdate"]) + .to include(match I18n.t("validations.setup.startdate.location.deactivated", postcode: location.postcode, date: "4 June 2022")) + end + + it "produces no error when tenancy start date is during an active location period" do + record.startdate = Time.zone.local(2022, 6, 1) + record.location = location + date_validator.validate_startdate(record) + expect(record.errors["startdate"]).to be_empty + end + end + + context "with a location that is reactivating soon" do + let(:scheme) { create(:scheme) } + let(:location) { create(:location, scheme:, startdate: nil) } + + before do + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), reactivation_date: Time.zone.local(2022, 8, 4), location:) + location.reload + end + + it "produces error when tenancy start date is during deactivated location period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.location = location + date_validator.validate_startdate(record) + expect(record.errors["startdate"]) + .to include(match I18n.t("validations.setup.startdate.location.reactivating_soon", postcode: location.postcode, date: "4 August 2022", deactivation_date: "4 June 2022")) + end + + it "produces no error when tenancy start date is during an active location period" do + record.startdate = Time.zone.local(2022, 9, 1) + record.location = location + date_validator.validate_startdate(record) + expect(record.errors["startdate"]).to be_empty + end + end + + context "with a location that has many reactivations soon" do + let(:scheme) { create(:scheme) } + let(:location) { create(:location, scheme:, startdate: nil) } + + before do + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), reactivation_date: Time.zone.local(2022, 8, 4), location:) + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 2), reactivation_date: Time.zone.local(2022, 8, 3), location:) + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 1), reactivation_date: Time.zone.local(2022, 9, 4), location:) + location.reload + end + + it "produces error when tenancy start date is during deactivated location period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.location = location + date_validator.validate_startdate(record) + expect(record.errors["startdate"]) + .to include(match I18n.t("validations.setup.startdate.location.reactivating_soon", postcode: location.postcode, date: "4 September 2022", deactivation_date: "1 June 2022")) + end + + it "produces no error when tenancy start date is during an active location period" do + record.startdate = Time.zone.local(2022, 10, 1) + record.location = location + date_validator.validate_startdate(record) + expect(record.errors["startdate"]).to be_empty + end + end + + context "with a location with no deactivation periods" do + let(:scheme) { create(:scheme) } + let(:location) { create(:location, scheme:, startdate: Time.zone.local(2022, 9, 15)) } + + it "produces no error" do + record.startdate = Time.zone.local(2022, 10, 15) + record.location = location + date_validator.validate_startdate(record) + expect(record.errors["startdate"]).to be_empty + end + + it "produces an error when the date is before available_from date" do + record.startdate = Time.zone.local(2022, 8, 15) + record.location = location + date_validator.validate_startdate(record) + expect(record.errors["startdate"]) + .to include(match I18n.t("validations.setup.startdate.location.activating_soon", postcode: location.postcode, date: "15 September 2022")) + end + end + + context "with a scheme that is reactivating soon" do + let(:scheme) { create(:scheme) } + + before do + create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), reactivation_date: Time.zone.local(2022, 8, 4), scheme:) + scheme.reload + end + + it "produces error when tenancy start date is during deactivated scheme period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.scheme = scheme + date_validator.validate_startdate(record) + expect(record.errors["startdate"]) + .to include(match I18n.t("validations.setup.startdate.scheme.reactivating_soon", name: scheme.service_name, date: "4 August 2022", deactivation_date: "4 June 2022")) + end + + it "produces no error when tenancy start date is during an active scheme period" do + record.startdate = Time.zone.local(2022, 9, 1) + record.scheme = scheme + date_validator.validate_startdate(record) + expect(record.errors["startdate"]).to be_empty + end + end + + context "with a scheme that has many reactivations soon" do + let(:scheme) { create(:scheme) } + + before do + create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), reactivation_date: Time.zone.local(2022, 8, 4), scheme:) + create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 2), reactivation_date: Time.zone.local(2022, 8, 3), scheme:) + create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 1), reactivation_date: Time.zone.local(2022, 9, 4), scheme:) + scheme.reload + end + + it "produces error when tenancy start date is during deactivated scheme period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.scheme = scheme + date_validator.validate_startdate(record) + expect(record.errors["startdate"]) + .to include(match I18n.t("validations.setup.startdate.scheme.reactivating_soon", name: scheme.service_name, date: "4 September 2022", deactivation_date: "1 June 2022")) + end + + it "produces no error when tenancy start date is during an active scheme period" do + record.startdate = Time.zone.local(2022, 10, 1) + record.scheme = scheme + date_validator.validate_startdate(record) + expect(record.errors["startdate"]).to be_empty + end + end end describe "major repairs date" do diff --git a/spec/models/validations/setup_validations_spec.rb b/spec/models/validations/setup_validations_spec.rb index 378aef904..fa95757c4 100644 --- a/spec/models/validations/setup_validations_spec.rb +++ b/spec/models/validations/setup_validations_spec.rb @@ -30,4 +30,198 @@ RSpec.describe Validations::SetupValidations do expect(record.errors["irproduct_other"]).to be_empty end end + + describe "#validate_scheme" do + context "with a deactivated location" do + let(:scheme) { create(:scheme) } + let(:location) { create(:location, scheme:, startdate: nil) } + + before do + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), location:) + location.reload + end + + it "produces error when tenancy start date is during deactivated location period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.location = location + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]) + .to include(match I18n.t("validations.setup.startdate.location.deactivated", postcode: location.postcode, date: "4 June 2022")) + end + + it "produces no error when tenancy start date is during an active location period" do + record.startdate = Time.zone.local(2022, 6, 1) + record.location = location + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]).to be_empty + end + end + + context "with a location that is reactivating soon" do + let(:scheme) { create(:scheme) } + let(:location) { create(:location, scheme:, startdate: nil) } + + before do + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), reactivation_date: Time.zone.local(2022, 8, 4), location:) + location.reload + end + + it "produces error when tenancy start date is during deactivated location period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.location = location + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]) + .to include(match I18n.t("validations.setup.startdate.location.reactivating_soon", postcode: location.postcode, date: "4 August 2022", deactivation_date: "4 June 2022")) + end + + it "produces no error when tenancy start date is during an active location period" do + record.startdate = Time.zone.local(2022, 9, 1) + record.location = location + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]).to be_empty + end + end + + context "with a location with no deactivation periods" do + let(:scheme) { create(:scheme, created_at: Time.zone.local(2022, 10, 3)) } + let(:location) { create(:location, scheme:, startdate: Time.zone.local(2022, 9, 15)) } + + it "produces no error" do + record.startdate = Time.zone.local(2022, 10, 15) + record.location = location + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]).to be_empty + end + + it "produces an error when the date is before available_from date" do + record.startdate = Time.zone.local(2022, 8, 15) + record.location = location + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]) + .to include(match I18n.t("validations.setup.startdate.location.activating_soon", postcode: location.postcode, date: "15 September 2022")) + end + end + + context "with a scheme that is reactivating soon" do + let(:scheme) { create(:scheme, created_at: Time.zone.local(2022, 4, 1)) } + + before do + create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), reactivation_date: Time.zone.local(2022, 8, 4), scheme:) + scheme.reload + end + + it "produces error when tenancy start date is during deactivated scheme period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.scheme = scheme + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]) + .to include(match I18n.t("validations.setup.startdate.scheme.reactivating_soon", name: scheme.service_name, date: "4 August 2022", deactivation_date: "4 June 2022")) + end + + it "produces no error when tenancy start date is during an active scheme period" do + record.startdate = Time.zone.local(2022, 9, 1) + record.scheme = scheme + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]).to be_empty + end + end + + context "with a scheme that has many reactivations soon" do + let(:scheme) { create(:scheme, created_at: Time.zone.local(2022, 4, 1)) } + + before do + create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), reactivation_date: Time.zone.local(2022, 8, 4), scheme:) + create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 2), reactivation_date: Time.zone.local(2022, 8, 3), scheme:) + create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 1), reactivation_date: Time.zone.local(2022, 9, 4), scheme:) + scheme.reload + end + + it "produces error when tenancy start date is during deactivated scheme period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.scheme = scheme + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]) + .to include(match I18n.t("validations.setup.startdate.scheme.reactivating_soon", name: scheme.service_name, date: "4 September 2022", deactivation_date: "1 June 2022")) + end + + it "produces no error when tenancy start date is during an active scheme period" do + record.startdate = Time.zone.local(2022, 10, 1) + record.scheme = scheme + setup_validator.validate_scheme(record) + expect(record.errors["scheme_id"]).to be_empty + end + end + end + + describe "#validate_location" do + context "with a deactivated location" do + let(:scheme) { create(:scheme) } + let(:location) { create(:location, scheme:, startdate: nil) } + + before do + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), location:) + location.reload + end + + it "produces error when tenancy start date is during deactivated location period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.location = location + setup_validator.validate_location(record) + expect(record.errors["location_id"]) + .to include(match I18n.t("validations.setup.startdate.location.deactivated", postcode: location.postcode, date: "4 June 2022")) + end + + it "produces no error when tenancy start date is during an active location period" do + record.startdate = Time.zone.local(2022, 6, 1) + record.location = location + setup_validator.validate_location(record) + expect(record.errors["location_id"]).to be_empty + end + end + + context "with a location that is reactivating soon" do + let(:scheme) { create(:scheme) } + let(:location) { create(:location, scheme:, startdate: nil) } + + before do + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 4), reactivation_date: Time.zone.local(2022, 8, 4), location:) + location.reload + end + + it "produces error when tenancy start date is during deactivated location period" do + record.startdate = Time.zone.local(2022, 7, 5) + record.location = location + setup_validator.validate_location(record) + expect(record.errors["location_id"]) + .to include(match I18n.t("validations.setup.startdate.location.reactivating_soon", postcode: location.postcode, date: "4 August 2022", deactivation_date: "4 June 2022")) + end + + it "produces no error when tenancy start date is during an active location period" do + record.startdate = Time.zone.local(2022, 9, 1) + record.location = location + setup_validator.validate_location(record) + expect(record.errors["location_id"]).to be_empty + end + end + + context "with a location with no deactivation periods" do + let(:scheme) { create(:scheme) } + let(:location) { create(:location, scheme:, startdate: Time.zone.local(2022, 9, 15)) } + + it "produces no error" do + record.startdate = Time.zone.local(2022, 10, 15) + record.location = location + setup_validator.validate_location(record) + expect(record.errors["location_id"]).to be_empty + end + + it "produces an error when the date is before available_from date" do + record.startdate = Time.zone.local(2022, 8, 15) + record.location = location + setup_validator.validate_location(record) + expect(record.errors["location_id"]) + .to include(match I18n.t("validations.setup.startdate.location.activating_soon", postcode: location.postcode, date: "15 September 2022")) + end + end + end end diff --git a/spec/requests/form_controller_spec.rb b/spec/requests/form_controller_spec.rb index 1bb73f399..a7f374f27 100644 --- a/spec/requests/form_controller_spec.rb +++ b/spec/requests/form_controller_spec.rb @@ -28,6 +28,7 @@ RSpec.describe FormController, type: :request do :completed, owning_organisation: organisation, managing_organisation: organisation, + startdate: Time.zone.local(2021, 5, 1), ) end let(:headers) { { "Accept" => "text/html" } } diff --git a/spec/requests/locations_controller_spec.rb b/spec/requests/locations_controller_spec.rb index b1cffd3bf..1c3c47219 100644 --- a/spec/requests/locations_controller_spec.rb +++ b/spec/requests/locations_controller_spec.rb @@ -847,7 +847,7 @@ RSpec.describe LocationsController, type: :request do context "when signed in as a data coordinator user" do let(:user) { FactoryBot.create(:user, :data_coordinator) } let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) } - let!(:locations) { FactoryBot.create_list(:location, 3, scheme:) } + let!(:locations) { FactoryBot.create_list(:location, 3, scheme:, startdate: Time.zone.local(2022, 4, 1)) } before do sign_in user @@ -858,7 +858,7 @@ RSpec.describe LocationsController, type: :request do let!(:another_scheme) { FactoryBot.create(:scheme) } before do - FactoryBot.create(:location, scheme:) + FactoryBot.create(:location, scheme:, startdate: Time.zone.local(2022, 4, 1)) end it "returns 404 not found" do @@ -874,7 +874,7 @@ RSpec.describe LocationsController, type: :request do expect(page).to have_content(location.type_of_unit) expect(page).to have_content(location.mobility_type) expect(page).to have_content(location.location_admin_district) - expect(page).to have_content(location.startdate&.to_formatted_s(:govuk_date)) + expect(page).to have_content(location.startdate.to_formatted_s(:govuk_date)) end end @@ -964,7 +964,7 @@ RSpec.describe LocationsController, type: :request do context "when signed in as a support user" do let(:user) { FactoryBot.create(:user, :support) } let!(:scheme) { FactoryBot.create(:scheme) } - let!(:locations) { FactoryBot.create_list(:location, 3, scheme:) } + let!(:locations) { FactoryBot.create_list(:location, 3, scheme:, startdate: Time.zone.local(2022, 4, 1)) } before do allow(user).to receive(:need_two_factor_authentication?).and_return(false) @@ -977,7 +977,7 @@ RSpec.describe LocationsController, type: :request do expect(page).to have_content(location.id) expect(page).to have_content(location.postcode) expect(page).to have_content(location.type_of_unit) - expect(page).to have_content(location.startdate&.to_formatted_s(:govuk_date)) + expect(page).to have_content(location.startdate.to_formatted_s(:govuk_date)) end end diff --git a/spec/requests/schemes_controller_spec.rb b/spec/requests/schemes_controller_spec.rb index 4de927f8f..0769e15c6 100644 --- a/spec/requests/schemes_controller_spec.rb +++ b/spec/requests/schemes_controller_spec.rb @@ -1767,7 +1767,7 @@ RSpec.describe SchemesController, type: :request do context "when signed in as a data coordinator" do let(:user) { FactoryBot.create(:user, :data_coordinator) } - let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) } + let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation, created_at: Time.zone.today) } let!(:location) { FactoryBot.create(:location, scheme:) } let(:deactivation_date) { Time.utc(2022, 10, 10) } let!(:lettings_log) { FactoryBot.create(:lettings_log, :sh, location:, scheme:, startdate:, owning_organisation: user.organisation) } diff --git a/spec/services/exports/lettings_log_export_service_spec.rb b/spec/services/exports/lettings_log_export_service_spec.rb index 044fe6c93..71ad30400 100644 --- a/spec/services/exports/lettings_log_export_service_spec.rb +++ b/spec/services/exports/lettings_log_export_service_spec.rb @@ -59,7 +59,7 @@ RSpec.describe Exports::LettingsLogExportService do end context "and one lettings log is available for export" do - let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737") } + let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737", startdate: Time.utc(2022, 2, 2, 10, 36, 49), tenancylength: 5) } it "generates a ZIP export file with the expected filename" do expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) @@ -237,7 +237,7 @@ RSpec.describe Exports::LettingsLogExportService do let(:csv_export_file) { File.open("spec/fixtures/exports/general_needs_log.csv", "r:UTF-8") } let(:expected_csv_filename) { "export_2022_05_01.csv" } - let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737") } + let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737", startdate: Time.utc(2022, 2, 2, 10, 36, 49), tenancylength: 5) } it "generates an CSV export file with the expected content" do expected_content = replace_entity_ids(lettings_log, csv_export_file.read) @@ -254,9 +254,9 @@ RSpec.describe Exports::LettingsLogExportService do let(:organisation) { FactoryBot.create(:organisation, provider_type: "LA") } let(:user) { FactoryBot.create(:user, organisation:) } let(:scheme) { FactoryBot.create(:scheme, :export, owning_organisation: organisation) } - let(:location) { FactoryBot.create(:location, :export, scheme:) } + let(:location) { FactoryBot.create(:location, :export, scheme:, startdate: Time.zone.local(2021, 4, 1)) } - let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, :export, :sh, scheme:, location:, created_by: user, owning_organisation: organisation) } + let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, :export, :sh, scheme:, location:, created_by: user, owning_organisation: organisation, startdate: Time.utc(2022, 2, 2, 10, 36, 49)) } it "generates an XML export file with the expected content" do expected_content = replace_entity_ids(lettings_log, export_file.read) diff --git a/spec/services/imports/lettings_logs_import_service_spec.rb b/spec/services/imports/lettings_logs_import_service_spec.rb index 453ee7e30..acb9d9dd4 100644 --- a/spec/services/imports/lettings_logs_import_service_spec.rb +++ b/spec/services/imports/lettings_logs_import_service_spec.rb @@ -30,9 +30,9 @@ RSpec.describe Imports::LettingsLogsImportService do FactoryBot.create(:user, old_user_id: "e29c492473446dca4d50224f2bb7cf965a261d6f", organisation:) # Location setup - FactoryBot.create(:location, old_visible_id: "10", postcode: "LS166FT", scheme_id: scheme1.id, mobility_type: "W") - FactoryBot.create(:location, scheme_id: scheme1.id) - FactoryBot.create(:location, old_visible_id: "10", postcode: "LS166FT", scheme_id: scheme2.id, mobility_type: "W") + FactoryBot.create(:location, old_visible_id: "10", postcode: "LS166FT", scheme_id: scheme1.id, mobility_type: "W", startdate: Time.zone.local(2021, 4, 1)) + FactoryBot.create(:location, scheme_id: scheme1.id, startdate: Time.zone.local(2021, 4, 1)) + FactoryBot.create(:location, old_visible_id: "10", postcode: "LS166FT", scheme_id: scheme2.id, mobility_type: "W", startdate: Time.zone.local(2021, 4, 1)) # Stub the form handler to use the real form allow(FormHandler.instance).to receive(:get_form).with("previous_lettings").and_return(real_2021_2022_form) From a62846c2129dc4969fe9039e761afa844632c4fa Mon Sep 17 00:00:00 2001 From: SamSeed-Softwire <63662292+SamSeed-Softwire@users.noreply.github.com> Date: Wed, 30 Nov 2022 15:31:56 +0000 Subject: [PATCH 16/19] CLDC-1666 Update locations table (#987) * feat: remove redundant cols from locations table * feat: add status col to locations table * test: start adding status tag tests * test: pick the simplest format for the tag helper tests * feat: make the locations table (plus related content above & below) 2/3 width * feat: add incomplete to location status method * test: all status tag helpers * test: update test text to match what's being tested * test: update locations list test in locations_controller_spec.rb * test: update locations list test in schemes feature tests * test: that status=incomplete when mandatory info is missing * feat: simplify logic for incomplete status Co-authored-by: James Rose * feat: hide new locations table layout behind feature flag * feat: use route helpers to get scheme_location path * feat: simplify "scheme_id: @scheme.id" to "@scheme" * feat: reference location, not @location, in scheme_location_path * feat: reorder postcode and code in locations table * feat: change ' ' to "" in location_spec * feat: update wording and ordering for 'view location details' page * test: re-add tests for locations page when new layout not enabled * test: check for mobility_type and location_admin_district in locations_controller as support user * test: update locations helper test to match new field naming and ordering * feat: always scale locations list by 2/3, not just when new layout used * feat: scale the scheme details page by 2/3 to match the locations list page * feat: only make scheme details and locations pages 2/3 scale if feature toggle on * test: mock new locations layout feature toggle in tests * feat: use the existing location_toggle_enabled instead of new_locations_table_layout_enabled * refactor: don't use html inside ruby in locations index page * refactor: use Rails routes in all places in locations index page * style: lint * feat: move logic from views/schemes/show into schemes_helper * refactor: make consistent the removal of fields from SchemeHelper base_attributes * test: improvements to tests * test: make 'returns correct display attributes' tests sensible * test: check scheme toggle off => no scheme status shown * style: correct indentation in spec/helpers/schemes_helper_spec.rb * test: that when owning = managing, "Organisation providing support" hidden * style: lint code * test: don't set scheme id explicitly in schemes_helper_spec * style: reorder tag helper spec to match tag helper * refactor: ensure display_scheme_attributes always takes user type * test: make location incomplete status depend on location.confirmed * test: add missing location validations for nil postcode & mobility type Co-authored-by: James Rose --- app/helpers/locations_helper.rb | 4 +- app/helpers/schemes_helper.rb | 10 +- app/models/location.rb | 1 + app/views/locations/index.html.erb | 135 ++++++++++++++------- app/views/schemes/show.html.erb | 30 +++-- spec/features/schemes_spec.rb | 18 ++- spec/helpers/locations_helper_spec.rb | 4 +- spec/helpers/schemes_helper_spec.rb | 105 ++++++++++++---- spec/helpers/tag_helper_spec.rb | 9 ++ spec/models/location_spec.rb | 27 ++++- spec/requests/locations_controller_spec.rb | 32 ++++- 11 files changed, 282 insertions(+), 93 deletions(-) diff --git a/app/helpers/locations_helper.rb b/app/helpers/locations_helper.rb index 8d56226a9..be5d3d4c0 100644 --- a/app/helpers/locations_helper.rb +++ b/app/helpers/locations_helper.rb @@ -26,12 +26,12 @@ module LocationsHelper def display_location_attributes(location) base_attributes = [ { name: "Postcode", value: location.postcode }, - { name: "Local authority", value: location.location_admin_district }, { name: "Location name", value: location.name, edit: true }, + { name: "Local authority", value: location.location_admin_district }, { name: "Total number of units at this location", value: location.units }, { name: "Common type of unit", value: location.type_of_unit }, { name: "Mobility type", value: location.mobility_type }, - { name: "Code", value: location.location_code }, + { name: "Location code", value: location.location_code }, { name: "Availability", value: location_availability(location) }, ] diff --git a/app/helpers/schemes_helper.rb b/app/helpers/schemes_helper.rb index 10a33cab6..3d143f8a2 100644 --- a/app/helpers/schemes_helper.rb +++ b/app/helpers/schemes_helper.rb @@ -1,5 +1,5 @@ module SchemesHelper - def display_scheme_attributes(scheme) + def display_scheme_attributes(scheme, user) base_attributes = [ { name: "Scheme code", value: scheme.id_to_display }, { name: "Name", value: scheme.service_name, edit: true }, @@ -18,11 +18,15 @@ module SchemesHelper ] if FeatureToggle.scheme_toggle_enabled? - base_attributes.append({ name: "Status", value: scheme.status }) + base_attributes.append({ name: "Status", value: status_tag(scheme.status) }) + end + + if user.data_coordinator? + base_attributes.delete_if { |item| item[:name] == "Housing stock owned by" } end if scheme.arrangement_type_same? - base_attributes.delete({ name: "Organisation providing support", value: scheme.managing_organisation&.name }) + base_attributes.delete_if { |item| item[:name] == "Organisation providing support" } end base_attributes end diff --git a/app/models/location.rb b/app/models/location.rb index 5ba6bf3e4..6eededcc0 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -384,6 +384,7 @@ class Location < ApplicationRecord end def status(date = Time.zone.now) + return :incomplete unless confirmed return :deactivated if open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date return :reactivating_soon if recent_deactivation&.reactivation_date.present? && date < recent_deactivation.reactivation_date diff --git a/app/views/locations/index.html.erb b/app/views/locations/index.html.erb index 5b2cbae47..812296be8 100644 --- a/app/views/locations/index.html.erb +++ b/app/views/locations/index.html.erb @@ -11,57 +11,104 @@ <%= render partial: "organisations/headings", locals: { main: @scheme.service_name, sub: nil } %> -<%= render SubNavigationComponent.new(items: scheme_items(request.path, @scheme.id, "Locations")) %> +<% if FeatureToggle.location_toggle_enabled? %> +
+
+<% end %> + <%= render SubNavigationComponent.new(items: scheme_items(request.path, @scheme.id, "Locations")) %> -

Locations

+

Locations

-<%= render SearchComponent.new(current_user:, search_label: "Search by location name or postcode", value: @searched) %> + <%= render SearchComponent.new(current_user:, search_label: "Search by location name or postcode", value: @searched) %> -<%= govuk_section_break(visible: true, size: "m") %> + <%= govuk_section_break(visible: true, size: "m") %> +<% if FeatureToggle.location_toggle_enabled? %> +
+
+<% end %> -<%= govuk_table do |table| %> - <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> - <%= render(SearchResultCaptionComponent.new(searched: @searched, count: @pagy.count, item_label:, total_count: @total_count, item: "locations", path: request.path)) %> - <% end %> - <%= table.head do |head| %> - <%= head.row do |row| %> - <% row.cell(header: true, text: "Code", html_attributes: { - scope: "col", - }) %> - <% row.cell(header: true, text: "Postcode", html_attributes: { - scope: "col", - }) %> - <% row.cell(header: true, text: "Units", html_attributes: { - scope: "col", - }) %> - <% row.cell(header: true, text: "Common unit type", html_attributes: { - scope: "col", - }) %> - <% row.cell(header: true, text: "Mobility type", html_attributes: { - scope: "col", - }) %> - <% row.cell(header: true, text: "Local authority", html_attributes: { - scope: "col", - }) %> - <% row.cell(header: true, text: "Available from", html_attributes: { - scope: "col", - }) %> - <% end %> - <% end %> - <% @locations.each do |location| %> - <%= table.body do |body| %> - <%= body.row do |row| %> - <% row.cell(text: location.id) %> - <% row.cell(text: simple_format(location_cell_postcode(location, "/schemes/#{@scheme.id}/locations/#{location.id}"), { class: "govuk-!-font-weight-bold" }, wrapper_tag: "div")) %> - <% row.cell(text: location.units) %> - <% row.cell(text: simple_format("#{location.type_of_unit}")) %> - <% row.cell(text: location.mobility_type) %> - <% row.cell(text: simple_format(location_cell_location_admin_district(location, "/schemes/#{@scheme.id}/locations/#{location.id}/edit-local-authority"), wrapper_tag: "div")) %> - <% row.cell(text: location.startdate&.to_formatted_s(:govuk_date)) %> +<% if FeatureToggle.location_toggle_enabled? %> +
+
+ <%= govuk_table do |table| %> + <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> + <%= render(SearchResultCaptionComponent.new(searched: @searched, count: @pagy.count, item_label:, total_count: @total_count, item: "locations", path: request.path)) %> + <% end %> + <%= table.head do |head| %> + <%= head.row do |row| %> + <% row.cell(header: true, text: "Postcode", html_attributes: { + scope: "col", + }) %> + <% row.cell(header: true, text: "Location code", html_attributes: { + scope: "col", + }) %> + <% row.cell(header: true, text: "Status", html_attributes: { + scope: "col", + }) %> + <% end %> <% end %> + <% @locations.each do |location| %> + <%= table.body do |body| %> + <%= body.row do |row| %> + <% row.cell(text: simple_format(location_cell_postcode(location, scheme_location_path(@scheme, location)), { class: "govuk-!-font-weight-bold" }, wrapper_tag: "div")) %> + <% row.cell(text: location.id) %> + <% row.cell(text: status_tag(location.status)) %> + <% end %> + <% end %> + <% end %> + <% end %> + <%= govuk_button_link_to "Add a location", new_scheme_location_path(@scheme), secondary: true %> +
+
+ +<% else %> + <%= govuk_table do |table| %> + <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> + <%= render(SearchResultCaptionComponent.new(searched: @searched, count: @pagy.count, item_label:, total_count: @total_count, item: "locations", path: request.path)) %> + <% end %> + <%= table.head do |head| %> + <%= head.row do |row| %> + <% row.cell(header: true, text: "Code", html_attributes: { + scope: "col", + }) %> + <% row.cell(header: true, text: "Postcode", html_attributes: { + scope: "col", + }) %> + <% row.cell(header: true, text: "Units", html_attributes: { + scope: "col", + }) %> + <% row.cell(header: true, text: "Common unit type", html_attributes: { + scope: "col", + }) %> + <% row.cell(header: true, text: "Mobility type", html_attributes: { + scope: "col", + }) %> + <% row.cell(header: true, text: "Local authority", html_attributes: { + scope: "col", + }) %> + <% row.cell(header: true, text: "Available from", html_attributes: { + scope: "col", + }) %> + <% end %> + <% end %> + <% @locations.each do |location| %> + <%= table.body do |body| %> + <%= body.row do |row| %> + <% row.cell(text: location.id) %> + <% row.cell(text: simple_format(location_cell_postcode(location, scheme_location_path(@scheme, location)), { class: "govuk-!-font-weight-bold" }, wrapper_tag: "div")) %> + <% row.cell(text: location.units) %> + <% row.cell do %> + <%= simple_format(location.type_of_unit) %> + <% end %> + <% row.cell(text: location.mobility_type) %> + <% row.cell(text: simple_format(location_cell_location_admin_district(location, scheme_location_edit_local_authority_path(@scheme, location)), wrapper_tag: "div")) %> + <% row.cell(text: location.startdate&.to_formatted_s(:govuk_date)) %> + <% end %> + <% end %> <% end %> <% end %> + <%= govuk_button_link_to "Add a location", new_scheme_location_path(@scheme), secondary: true %> + <% end %> -<%= govuk_button_link_to "Add a location", new_scheme_location_path(scheme_id: @scheme.id), secondary: true %> <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "locations" } %> diff --git a/app/views/schemes/show.html.erb b/app/views/schemes/show.html.erb index e0b229af4..5d93414d0 100644 --- a/app/views/schemes/show.html.erb +++ b/app/views/schemes/show.html.erb @@ -10,19 +10,27 @@ <%= render partial: "organisations/headings", locals: { main: @scheme.service_name, sub: nil } %> -<%= render SubNavigationComponent.new(items: scheme_items(request.path, @scheme.id, "Locations")) %> +<% if FeatureToggle.location_toggle_enabled? %> +
+
+<% end %> + <%= render SubNavigationComponent.new(items: scheme_items(request.path, @scheme.id, "Locations")) %> -

Scheme

+

Scheme

-<%= govuk_summary_list do |summary_list| %> - <% display_scheme_attributes(@scheme).each do |attr| %> - <% next if current_user.data_coordinator? && attr[:name] == ("Housing stock owned by") %> - <%= summary_list.row do |row| %> - <% row.key { attr[:name].eql?("Registered under Care Standards Act 2000") ? "Registered under Care Standards Act 2000" : attr[:name].to_s.humanize } %> - <% row.value { attr[:name].eql?("Status") ? status_tag(attr[:value]) : details_html(attr) } %> - <% row.action(text: "Change", href: scheme_edit_name_path(scheme_id: @scheme.id)) if attr[:edit] %> - <% end %> - <% end %> + <%= govuk_summary_list do |summary_list| %> + <% display_scheme_attributes(@scheme, current_user).each do |attr| %> + <%= summary_list.row do |row| %> + <% row.key { attr[:name] } %> + <% row.value { details_html(attr) } %> + <% row.action(text: "Change", href: scheme_edit_name_path(scheme_id: @scheme.id)) if attr[:edit] %> + <% end %> + <% end %> + <% end %> + +<% if FeatureToggle.location_toggle_enabled? %> +
+
<% end %> <% if FeatureToggle.scheme_toggle_enabled? %> diff --git a/spec/features/schemes_spec.rb b/spec/features/schemes_spec.rb index 797b718af..bee63350a 100644 --- a/spec/features/schemes_spec.rb +++ b/spec/features/schemes_spec.rb @@ -195,11 +195,27 @@ RSpec.describe "Schemes scheme Features" do expect(page).to have_link("Locations") end - context "when I click locations link" do + context "when I click locations link and the new locations layout feature toggle is enabled" do before do click_link("Locations") end + it "shows details of those locations" do + locations.each do |location| + expect(page).to have_content(location.id) + expect(page).to have_content(location.postcode) + expect(page).to have_content(location.name) + expect(page).to have_content("Active") + end + end + end + + context "when I click locations link and the new locations layout feature toggle is disabled" do + before do + allow(FeatureToggle).to receive(:location_toggle_enabled?).and_return(false) + click_link("Locations") + end + it "shows details of those locations" do locations.each do |location| expect(page).to have_content(location.id) diff --git a/spec/helpers/locations_helper_spec.rb b/spec/helpers/locations_helper_spec.rb index c80449ce6..96db265e3 100644 --- a/spec/helpers/locations_helper_spec.rb +++ b/spec/helpers/locations_helper_spec.rb @@ -139,12 +139,12 @@ RSpec.describe LocationsHelper do it "returns correct display attributes" do attributes = [ { name: "Postcode", value: location.postcode }, - { name: "Local authority", value: location.location_admin_district }, { name: "Location name", value: location.name, edit: true }, + { name: "Local authority", value: location.location_admin_district }, { name: "Total number of units at this location", value: location.units }, { name: "Common type of unit", value: location.type_of_unit }, { name: "Mobility type", value: location.mobility_type }, - { name: "Code", value: location.location_code }, + { name: "Location code", value: location.location_code }, { name: "Availability", value: "Active from 1 April 2022" }, { name: "Status", value: :active }, ] diff --git a/spec/helpers/schemes_helper_spec.rb b/spec/helpers/schemes_helper_spec.rb index b1e9f9c96..af9f133ed 100644 --- a/spec/helpers/schemes_helper_spec.rb +++ b/spec/helpers/schemes_helper_spec.rb @@ -87,40 +87,97 @@ RSpec.describe SchemesHelper do end end + include TagHelper describe "display_scheme_attributes" do - let!(:scheme) { FactoryBot.create(:scheme, created_at: Time.zone.local(2022, 4, 1)) } + let(:owning_organisation) { FactoryBot.create(:organisation, name: "Acme LTD Owning") } + let(:managing_organisation) { FactoryBot.create(:organisation, name: "Acme LTD Managing") } + let!(:scheme) do + FactoryBot.create(:scheme, + service_name: "Test service_name", + sensitive: 0, + scheme_type: 7, + registered_under_care_act: 3, + owning_organisation:, + managing_organisation:, + arrangement_type: "V", + primary_client_group: "S", + has_other_client_group: 1, + secondary_client_group: "I", + support_type: 4, + intended_stay: "P", + created_at: Time.zone.local(2022, 4, 1)) + end + let!(:scheme_where_managing_organisation_is_owning_organisation) { FactoryBot.create(:scheme, arrangement_type: "D") } + let(:support_user) { FactoryBot.create(:user, :support) } + let(:coordinator_user) { FactoryBot.create(:user, :data_coordinator) } + + it "returns correct display attributes for a support user" do + attributes = [ + { name: "Scheme code", value: "S#{scheme.id}" }, + { name: "Name", value: "Test service_name", edit: true }, + { name: "Confidential information", value: "No", edit: true }, + { name: "Type of scheme", value: "Housing for older people" }, + { name: "Registered under Care Standards Act 2000", value: "Yes – registered care home providing personal care" }, + { name: "Housing stock owned by", value: "Acme LTD Owning", edit: true }, + { name: "Support services provided by", value: "A registered charity or voluntary organisation" }, + { name: "Organisation providing support", value: "Acme LTD Managing" }, + { name: "Primary client group", value: "Rough sleepers" }, + { name: "Has another client group", value: "Yes" }, + { name: "Secondary client group", value: "Refugees (permanent)" }, + { name: "Level of support given", value: "High level" }, + { name: "Intended length of stay", value: "Permanent" }, + { name: "Availability", value: "Active from 1 April 2022" }, + { name: "Status", value: status_tag(:active) }, + ] + expect(display_scheme_attributes(scheme, support_user)).to eq(attributes) + end - it "returns correct display attributes" do + it "returns correct display attributes for a coordinator user" do attributes = [ - { name: "Scheme code", value: scheme.id_to_display }, - { name: "Name", value: scheme.service_name, edit: true }, - { name: "Confidential information", value: scheme.sensitive, edit: true }, - { name: "Type of scheme", value: scheme.scheme_type }, - { name: "Registered under Care Standards Act 2000", value: scheme.registered_under_care_act }, - { name: "Housing stock owned by", value: scheme.owning_organisation.name, edit: true }, - { name: "Support services provided by", value: scheme.arrangement_type }, - { name: "Primary client group", value: scheme.primary_client_group }, - { name: "Has another client group", value: scheme.has_other_client_group }, - { name: "Secondary client group", value: scheme.secondary_client_group }, - { name: "Level of support given", value: scheme.support_type }, - { name: "Intended length of stay", value: scheme.intended_stay }, + { name: "Scheme code", value: "S#{scheme.id}" }, + { name: "Name", value: "Test service_name", edit: true }, + { name: "Confidential information", value: "No", edit: true }, + { name: "Type of scheme", value: "Housing for older people" }, + { name: "Registered under Care Standards Act 2000", value: "Yes – registered care home providing personal care" }, + { name: "Support services provided by", value: "A registered charity or voluntary organisation" }, + { name: "Organisation providing support", value: "Acme LTD Managing" }, + { name: "Primary client group", value: "Rough sleepers" }, + { name: "Has another client group", value: "Yes" }, + { name: "Secondary client group", value: "Refugees (permanent)" }, + { name: "Level of support given", value: "High level" }, + { name: "Intended length of stay", value: "Permanent" }, { name: "Availability", value: "Active from 1 April 2022" }, - { name: "Status", value: :active }, + { name: "Status", value: status_tag(:active) }, ] - expect(display_scheme_attributes(scheme)).to eq(attributes) + expect(display_scheme_attributes(scheme, coordinator_user)).to eq(attributes) + end + + context "when the scheme toggle is disabled" do + it "doesn't show the scheme status" do + allow(FeatureToggle).to receive(:scheme_toggle_enabled?).and_return(false) + attributes = display_scheme_attributes(scheme, support_user).find { |x| x[:name] == "Status" } + expect(attributes).to be_nil + end + end + + context "when the managing organisation is the owning organisation" do + it "doesn't show the organisation providing support" do + attributes = display_scheme_attributes(scheme_where_managing_organisation_is_owning_organisation, support_user).find { |x| x[:name] == "Organisation providing support" } + expect(attributes).to be_nil + end end context "when viewing availability" do context "with no deactivations" do it "displays created_at as availability date" do - availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + availability_attribute = display_scheme_attributes(scheme, support_user).find { |x| x[:name] == "Availability" }[:value] expect(availability_attribute).to eq("Active from #{scheme.created_at.to_formatted_s(:govuk_date)}") end it "displays current collection start date as availability date if created_at is later than collection start date" do scheme.update!(created_at: Time.zone.local(2022, 4, 16)) - availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + availability_attribute = display_scheme_attributes(scheme, support_user).find { |x| x[:name] == "Availability" }[:value] expect(availability_attribute).to eq("Active from 1 April 2022") end @@ -135,7 +192,7 @@ RSpec.describe SchemesHelper do end it "displays the timeline of availability" do - availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + availability_attribute = display_scheme_attributes(scheme, support_user).find { |x| x[:name] == "Availability" }[:value] expect(availability_attribute).to eq("Active from 1 April 2022 to 9 August 2022\nDeactivated on 10 August 2022\nActive from 1 September 2022 to 14 September 2022\nDeactivated on 15 September 2022\nActive from 28 September 2022") end @@ -149,7 +206,7 @@ RSpec.describe SchemesHelper do end it "displays the timeline of availability" do - availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + availability_attribute = display_scheme_attributes(scheme, support_user).find { |x| x[:name] == "Availability" }[:value] expect(availability_attribute).to eq("Active from 1 April 2022 to 9 August 2022\nDeactivated on 10 August 2022\nActive from 1 September 2022 to 14 September 2022\nDeactivated on 15 September 2022") end @@ -165,7 +222,7 @@ RSpec.describe SchemesHelper do end it "displays the timeline of availability" do - availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + availability_attribute = display_scheme_attributes(scheme, support_user).find { |x| x[:name] == "Availability" }[:value] expect(availability_attribute).to eq("Active from 1 April 2022 to 14 June 2022\nDeactivated on 15 June 2022\nActive from 18 June 2022 to 23 September 2022\nDeactivated on 24 September 2022\nActive from 28 September 2022") end @@ -179,7 +236,7 @@ RSpec.describe SchemesHelper do end it "displays the timeline of availability" do - availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + availability_attribute = display_scheme_attributes(scheme, support_user).find { |x| x[:name] == "Availability" }[:value] expect(availability_attribute).to eq("Active from 1 April 2022 to 14 June 2022\nDeactivated on 15 June 2022\nActive from 28 September 2022") end @@ -196,7 +253,7 @@ RSpec.describe SchemesHelper do end it "displays the timeline of availability" do - availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + availability_attribute = display_scheme_attributes(scheme, support_user).find { |x| x[:name] == "Availability" }[:value] expect(availability_attribute).to eq("Active from 1 April 2022 to 14 June 2022\nDeactivated on 15 June 2022\nActive from 28 September 2022 to 23 October 2022\nDeactivated on 24 October 2022\nActive from 28 October 2022") end @@ -211,7 +268,7 @@ RSpec.describe SchemesHelper do end it "displays the timeline of availability" do - availability_attribute = display_scheme_attributes(scheme).find { |x| x[:name] == "Availability" }[:value] + availability_attribute = display_scheme_attributes(scheme, support_user).find { |x| x[:name] == "Availability" }[:value] expect(availability_attribute).to eq("Active from 1 April 2022 to 9 October 2022\nDeactivated on 10 October 2022\nActive from 11 December 2022") end diff --git a/spec/helpers/tag_helper_spec.rb b/spec/helpers/tag_helper_spec.rb index 3f32b8502..3373737de 100644 --- a/spec/helpers/tag_helper_spec.rb +++ b/spec/helpers/tag_helper_spec.rb @@ -11,6 +11,15 @@ RSpec.describe TagHelper do it "returns tag with correct status text and colour and custom class" do expect(status_tag("not_started", "app-tag--small")).to eq("Not started") + expect(status_tag("cannot_start_yet", "app-tag--small")).to eq("Cannot start yet") + expect(status_tag("in_progress", "app-tag--small")).to eq("In progress") + expect(status_tag("completed", "app-tag--small")).to eq("Completed") + expect(status_tag("active", "app-tag--small")).to eq("Active") + expect(status_tag("incomplete", "app-tag--small")).to eq("Incomplete") + expect(status_tag("deactivating_soon", "app-tag--small")).to eq("Deactivating soon") + expect(status_tag("activating_soon", "app-tag--small")).to eq("Activating soon") + expect(status_tag("reactivating_soon", "app-tag--small")).to eq("Reactivating soon") + expect(status_tag("deactivated", "app-tag--small")).to eq("Deactivated") end end end diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb index 58bd3872e..11c826d28 100644 --- a/spec/models/location_spec.rb +++ b/spec/models/location_spec.rb @@ -34,12 +34,18 @@ RSpec.describe Location, type: :model do expect { location.save! } .to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Postcode #{I18n.t('validations.postcode')}") end + + it "does add an error when the postcode is missing" do + location.postcode = nil + expect { location.save! } + .to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Postcode #{I18n.t('validations.postcode')}") + end end describe "#units" do let(:location) { FactoryBot.build(:location) } - it "does add an error when the postcode is invalid" do + it "does add an error when the number of units is invalid" do location.units = nil expect { location.save! } .to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Units #{I18n.t('activerecord.errors.models.location.attributes.units.blank')}") @@ -49,13 +55,23 @@ RSpec.describe Location, type: :model do describe "#type_of_unit" do let(:location) { FactoryBot.build(:location) } - it "does add an error when the postcode is invalid" do + it "does add an error when the type of unit is invalid" do location.type_of_unit = nil expect { location.save! } .to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Type of unit #{I18n.t('activerecord.errors.models.location.attributes.type_of_unit.blank')}") end end + describe "#mobility_type" do + let(:location) { FactoryBot.build(:location) } + + it "does add an error when the mobility type is invalid" do + location.mobility_type = nil + expect { location.save! } + .to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Mobility type #{I18n.t('activerecord.errors.models.location.attributes.mobility_type.blank')}") + end + end + describe "paper trail" do let(:location) { FactoryBot.create(:location) } let!(:name) { location.name } @@ -123,6 +139,13 @@ RSpec.describe Location, type: :model do Timecop.unfreeze end + context "when location is not confirmed" do + it "returns incomplete " do + location.confirmed = false + expect(location.status).to eq(:incomplete) + end + end + context "when there have not been any previous deactivations" do it "returns active if the location has no deactivation records" do expect(location.status).to eq(:active) diff --git a/spec/requests/locations_controller_spec.rb b/spec/requests/locations_controller_spec.rb index 1c3c47219..45463ceb1 100644 --- a/spec/requests/locations_controller_spec.rb +++ b/spec/requests/locations_controller_spec.rb @@ -867,14 +867,25 @@ RSpec.describe LocationsController, type: :request do end end - it "shows scheme" do + it "shows locations with correct data wben the new locations layout feature toggle is enabled" do + locations.each do |location| + expect(page).to have_content(location.id) + expect(page).to have_content(location.postcode) + expect(page).to have_content(location.name) + expect(page).to have_content(location.status) + end + end + + it "shows locations with correct data wben the new locations layout feature toggle is disabled" do + allow(FeatureToggle).to receive(:location_toggle_enabled?).and_return(false) + get "/schemes/#{scheme.id}/locations" locations.each do |location| expect(page).to have_content(location.id) expect(page).to have_content(location.postcode) expect(page).to have_content(location.type_of_unit) expect(page).to have_content(location.mobility_type) expect(page).to have_content(location.location_admin_district) - expect(page).to have_content(location.startdate.to_formatted_s(:govuk_date)) + expect(page).to have_content(location.startdate&.to_formatted_s(:govuk_date)) end end @@ -972,12 +983,25 @@ RSpec.describe LocationsController, type: :request do get "/schemes/#{scheme.id}/locations" end - it "shows scheme" do + it "shows locations with correct data wben the new locations layout feature toggle is enabled" do + locations.each do |location| + expect(page).to have_content(location.id) + expect(page).to have_content(location.postcode) + expect(page).to have_content(location.name) + expect(page).to have_content(location.status) + end + end + + it "shows locations with correct data wben the new locations layout feature toggle is disabled" do + allow(FeatureToggle).to receive(:location_toggle_enabled?).and_return(false) + get "/schemes/#{scheme.id}/locations" locations.each do |location| expect(page).to have_content(location.id) expect(page).to have_content(location.postcode) expect(page).to have_content(location.type_of_unit) - expect(page).to have_content(location.startdate.to_formatted_s(:govuk_date)) + expect(page).to have_content(location.mobility_type) + expect(page).to have_content(location.location_admin_district) + expect(page).to have_content(location.startdate&.to_formatted_s(:govuk_date)) end end From 3295e850059d0d78baf9f34c5e19124741a903d4 Mon Sep 17 00:00:00 2001 From: James Rose Date: Wed, 30 Nov 2022 15:46:42 +0000 Subject: [PATCH 17/19] Add hint text to household rent or charges question (#1042) --- config/forms/2022_2023.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/forms/2022_2023.json b/config/forms/2022_2023.json index 2c6d0763f..a40ad87f0 100644 --- a/config/forms/2022_2023.json +++ b/config/forms/2022_2023.json @@ -7470,7 +7470,7 @@ "household_charge": { "check_answer_label": "Does the household pay rent or charges?", "header": "Does the household pay rent or other charges for the accommodation?", - "hint_text": "", + "hint_text": "If rent is charged on the property then answer Yes to this question, even if the tenants do not pay it themselves.", "type": "radio", "answer_options": { "0": { From b52c27782c2828d6cd3624bd6f25cb18027055e6 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Thu, 1 Dec 2022 09:22:55 +0000 Subject: [PATCH 18/19] tweak copy for 2022/2023 lettings access needs Q (#1014) --- config/forms/2022_2023.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/forms/2022_2023.json b/config/forms/2022_2023.json index a40ad87f0..fe1511153 100644 --- a/config/forms/2022_2023.json +++ b/config/forms/2022_2023.json @@ -5929,7 +5929,7 @@ "description": "", "questions": { "housingneeds_type": { - "header": "What type of access need do they have?", + "header": "What type of access needs do they have?", "hint_text": "", "type": "radio", "check_answer_label": "Disabled access needs", @@ -5947,7 +5947,7 @@ "value": true }, "3": { - "value": "None of the above" + "value": "None of the listed options" } } }, From a305bb7d52920df01afeb8b493ea4bfc6c8de856 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Thu, 1 Dec 2022 09:55:08 +0000 Subject: [PATCH 19/19] [1714] Bulk upload part three (#1032) * add start of bulk upload logs journey * split upload controller into 2 * add year page to bulk upload journey * bulk upload years now dynamic * bulk upload journey: add copy for prepare file * add link to bulk upload template * add placeholder for upload your file page * handle bulk upload when not in crossover * fix tests around bulk upload redirect * fix typos in bulk upload jouney --- .../bulk_upload_lettings_logs_controller.rb | 50 +++++++++++++++++ .../bulk_upload_sales_logs_controller.rb | 50 +++++++++++++++++ app/helpers/logs_helper.rb | 21 ++++++++ app/helpers/navigation_items_helper.rb | 4 +- app/models/form.rb | 8 +++ .../bulk_upload_lettings/prepare_your_file.rb | 41 ++++++++++++++ .../bulk_upload_lettings/upload_your_file.rb | 19 +++++++ app/models/forms/bulk_upload_lettings/year.rb | 37 +++++++++++++ .../bulk_upload_sales/prepare_your_file.rb | 41 ++++++++++++++ .../bulk_upload_sales/upload_your_file.rb | 19 +++++++ app/models/forms/bulk_upload_sales/year.rb | 37 +++++++++++++ .../forms/prepare_your_file.html.erb | 33 ++++++++++++ .../forms/upload_your_file.html.erb | 17 ++++++ .../forms/year.html.erb | 16 ++++++ .../forms/prepare_your_file.html.erb | 33 ++++++++++++ .../forms/upload_your_file.html.erb | 17 ++++++ .../forms/year.html.erb | 16 ++++++ app/views/logs/index.html.erb | 12 +++-- config/initializers/feature_toggle.rb | 4 ++ config/locales/en.yml | 12 +++++ config/routes.rb | 14 +++++ .../bulk-upload-lettings-template-v1.xlsx | Bin 0 -> 35928 bytes .../files/bulk-upload-sales-template-v1.xlsx | Bin 0 -> 26703 bytes .../bulk_upload_lettings_logs_spec.rb | 51 ++++++++++++++++++ spec/features/bulk_upload_sales_logs_spec.rb | 51 ++++++++++++++++++ spec/models/form_spec.rb | 34 ++++++++++++ .../forms/bulk_upload_lettings/year_spec.rb | 12 +++++ .../forms/bulk_upload_sales/year_spec.rb | 12 +++++ spec/rails_helper.rb | 1 + ...lk_upload_lettings_logs_controller_spec.rb | 32 +++++++++++ .../bulk_upload_sales_logs_controller_spec.rb | 32 +++++++++++ 31 files changed, 720 insertions(+), 6 deletions(-) create mode 100644 app/controllers/bulk_upload_lettings_logs_controller.rb create mode 100644 app/controllers/bulk_upload_sales_logs_controller.rb create mode 100644 app/helpers/logs_helper.rb create mode 100644 app/models/forms/bulk_upload_lettings/prepare_your_file.rb create mode 100644 app/models/forms/bulk_upload_lettings/upload_your_file.rb create mode 100644 app/models/forms/bulk_upload_lettings/year.rb create mode 100644 app/models/forms/bulk_upload_sales/prepare_your_file.rb create mode 100644 app/models/forms/bulk_upload_sales/upload_your_file.rb create mode 100644 app/models/forms/bulk_upload_sales/year.rb create mode 100644 app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb create mode 100644 app/views/bulk_upload_lettings_logs/forms/upload_your_file.html.erb create mode 100644 app/views/bulk_upload_lettings_logs/forms/year.html.erb create mode 100644 app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb create mode 100644 app/views/bulk_upload_sales_logs/forms/upload_your_file.html.erb create mode 100644 app/views/bulk_upload_sales_logs/forms/year.html.erb create mode 100644 public/files/bulk-upload-lettings-template-v1.xlsx create mode 100644 public/files/bulk-upload-sales-template-v1.xlsx create mode 100644 spec/features/bulk_upload_lettings_logs_spec.rb create mode 100644 spec/features/bulk_upload_sales_logs_spec.rb create mode 100644 spec/models/forms/bulk_upload_lettings/year_spec.rb create mode 100644 spec/models/forms/bulk_upload_sales/year_spec.rb create mode 100644 spec/requests/bulk_upload_lettings_logs_controller_spec.rb create mode 100644 spec/requests/bulk_upload_sales_logs_controller_spec.rb diff --git a/app/controllers/bulk_upload_lettings_logs_controller.rb b/app/controllers/bulk_upload_lettings_logs_controller.rb new file mode 100644 index 000000000..108c25fef --- /dev/null +++ b/app/controllers/bulk_upload_lettings_logs_controller.rb @@ -0,0 +1,50 @@ +class BulkUploadLettingsLogsController < ApplicationController + before_action :authenticate_user! + + def start + if in_crossover_period? + redirect_to bulk_upload_lettings_log_path(id: "year") + else + redirect_to bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: current_year }) + end + end + + def show + render form.view_path + end + + def update + if form.valid? + redirect_to form.next_path + else + render form.view_path + end + end + +private + + def current_year + FormHandler.instance.forms["current_lettings"].start_date.year + end + + def in_crossover_period? + FormHandler.instance.forms.values.any?(&:in_crossover_period?) + end + + def form + @form ||= case params[:id] + when "year" + Forms::BulkUploadLettings::Year.new(form_params) + when "prepare-your-file" + Forms::BulkUploadLettings::PrepareYourFile.new(form_params) + when "upload-your-file" + Forms::BulkUploadLettings::UploadYourFile.new(form_params) + else + raise "Page not found for path #{params[:id]}" + end + end + + def form_params + params.fetch(:form, {}).permit(:year) + end +end diff --git a/app/controllers/bulk_upload_sales_logs_controller.rb b/app/controllers/bulk_upload_sales_logs_controller.rb new file mode 100644 index 000000000..81d018d4c --- /dev/null +++ b/app/controllers/bulk_upload_sales_logs_controller.rb @@ -0,0 +1,50 @@ +class BulkUploadSalesLogsController < ApplicationController + before_action :authenticate_user! + + def start + if in_crossover_period? + redirect_to bulk_upload_sales_log_path(id: "year") + else + redirect_to bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: current_year }) + end + end + + def show + render form.view_path + end + + def update + if form.valid? + redirect_to form.next_path + else + render form.view_path + end + end + +private + + def current_year + FormHandler.instance.forms["current_sales"].start_date.year + end + + def in_crossover_period? + FormHandler.instance.forms.values.any?(&:in_crossover_period?) + end + + def form + @form ||= case params[:id] + when "year" + Forms::BulkUploadSales::Year.new(form_params) + when "prepare-your-file" + Forms::BulkUploadSales::PrepareYourFile.new(form_params) + when "upload-your-file" + Forms::BulkUploadSales::UploadYourFile.new(form_params) + else + raise "Page not found for path #{params[:id]}" + end + end + + def form_params + params.fetch(:form, {}).permit(:year) + end +end diff --git a/app/helpers/logs_helper.rb b/app/helpers/logs_helper.rb new file mode 100644 index 000000000..6567f0a13 --- /dev/null +++ b/app/helpers/logs_helper.rb @@ -0,0 +1,21 @@ +module LogsHelper + def log_type_for_controller(controller) + case controller.class.to_s + when "LettingsLogsController" + "lettings" + when "SalesLogsController" + "sales" + else + raise "Log type not found for #{controller.class}" + end + end + + def bulk_upload_path_for_controller(controller, id:) + case log_type_for_controller(controller) + when "lettings" + bulk_upload_lettings_log_path(id:) + when "sales" + bulk_upload_sales_log_path(id:) + end + end +end diff --git a/app/helpers/navigation_items_helper.rb b/app/helpers/navigation_items_helper.rb index 07c125a8d..d996894ea 100644 --- a/app/helpers/navigation_items_helper.rb +++ b/app/helpers/navigation_items_helper.rb @@ -65,11 +65,11 @@ module NavigationItemsHelper private def lettings_logs_current?(path) - path == "/lettings-logs" + path.starts_with?("/lettings-logs") end def sales_logs_current?(path) - path == "/sales-logs" + path.starts_with?("/sales-logs") end def users_current?(path) diff --git a/app/models/form.rb b/app/models/form.rb index 22321f431..9d9acf2ea 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -223,4 +223,12 @@ class Form end end end + + def in_crossover_period?(now: Time.zone.now) + ((end_date - 3.months) < now) && (now < end_date) + end + + def inspect + "#<#{self.class} @type=#{type} @name=#{name}>" + end end diff --git a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb new file mode 100644 index 000000000..684ba1437 --- /dev/null +++ b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb @@ -0,0 +1,41 @@ +module Forms + module BulkUploadLettings + class PrepareYourFile + include ActiveModel::Model + include ActiveModel::Attributes + include Rails.application.routes.url_helpers + + attribute :year, :integer + + def view_path + "bulk_upload_lettings_logs/forms/prepare_your_file" + end + + def back_path + if in_crossover_period? + Rails.application.routes.url_helpers.bulk_upload_lettings_log_path(id: "year", form: { year: }) + else + Rails.application.routes.url_helpers.lettings_logs_path + end + end + + def next_path + bulk_upload_lettings_log_path(id: "upload-your-file", form: { year: }) + end + + def template_path + "/files/bulk-upload-lettings-template-v1.xlsx" + end + + def year_combo + "#{year}/#{year + 1 - 2000}" + end + + private + + def in_crossover_period? + FormHandler.instance.forms.values.any?(&:in_crossover_period?) + end + end + end +end diff --git a/app/models/forms/bulk_upload_lettings/upload_your_file.rb b/app/models/forms/bulk_upload_lettings/upload_your_file.rb new file mode 100644 index 000000000..1415ffe19 --- /dev/null +++ b/app/models/forms/bulk_upload_lettings/upload_your_file.rb @@ -0,0 +1,19 @@ +module Forms + module BulkUploadLettings + class UploadYourFile + include ActiveModel::Model + include ActiveModel::Attributes + include Rails.application.routes.url_helpers + + attribute :year, :integer + + def view_path + "bulk_upload_lettings_logs/forms/upload_your_file" + end + + def back_path + bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: }) + end + end + end +end diff --git a/app/models/forms/bulk_upload_lettings/year.rb b/app/models/forms/bulk_upload_lettings/year.rb new file mode 100644 index 000000000..9fa17b19e --- /dev/null +++ b/app/models/forms/bulk_upload_lettings/year.rb @@ -0,0 +1,37 @@ +module Forms + module BulkUploadLettings + class Year + include ActiveModel::Model + include ActiveModel::Attributes + include Rails.application.routes.url_helpers + + attribute :year, :integer + + validates :year, presence: true + + def view_path + "bulk_upload_lettings_logs/forms/year" + end + + def options + possible_years.map do |year| + OpenStruct.new(id: year, name: "#{year}/#{year + 1}") + end + end + + def back_path + lettings_logs_path + end + + def next_path + bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: }) + end + + private + + def possible_years + FormHandler.instance.lettings_forms.values.map { |form| form.start_date.year }.sort.reverse + end + end + end +end diff --git a/app/models/forms/bulk_upload_sales/prepare_your_file.rb b/app/models/forms/bulk_upload_sales/prepare_your_file.rb new file mode 100644 index 000000000..da017dbbd --- /dev/null +++ b/app/models/forms/bulk_upload_sales/prepare_your_file.rb @@ -0,0 +1,41 @@ +module Forms + module BulkUploadSales + class PrepareYourFile + include ActiveModel::Model + include ActiveModel::Attributes + include Rails.application.routes.url_helpers + + attribute :year, :integer + + def view_path + "bulk_upload_sales_logs/forms/prepare_your_file" + end + + def back_path + if in_crossover_period? + Rails.application.routes.url_helpers.bulk_upload_sales_log_path(id: "year", form: { year: }) + else + Rails.application.routes.url_helpers.sales_logs_path + end + end + + def next_path + bulk_upload_sales_log_path(id: "upload-your-file", form: { year: }) + end + + def template_path + "/files/bulk-upload-sales-template-v1.xlsx" + end + + def year_combo + "#{year}/#{year + 1 - 2000}" + end + + private + + def in_crossover_period? + FormHandler.instance.forms.values.any?(&:in_crossover_period?) + end + end + end +end diff --git a/app/models/forms/bulk_upload_sales/upload_your_file.rb b/app/models/forms/bulk_upload_sales/upload_your_file.rb new file mode 100644 index 000000000..3d421e9f1 --- /dev/null +++ b/app/models/forms/bulk_upload_sales/upload_your_file.rb @@ -0,0 +1,19 @@ +module Forms + module BulkUploadSales + class UploadYourFile + include ActiveModel::Model + include ActiveModel::Attributes + include Rails.application.routes.url_helpers + + attribute :year, :integer + + def view_path + "bulk_upload_sales_logs/forms/upload_your_file" + end + + def back_path + bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: }) + end + end + end +end diff --git a/app/models/forms/bulk_upload_sales/year.rb b/app/models/forms/bulk_upload_sales/year.rb new file mode 100644 index 000000000..361061990 --- /dev/null +++ b/app/models/forms/bulk_upload_sales/year.rb @@ -0,0 +1,37 @@ +module Forms + module BulkUploadSales + class Year + include ActiveModel::Model + include ActiveModel::Attributes + include Rails.application.routes.url_helpers + + attribute :year, :integer + + validates :year, presence: true + + def view_path + "bulk_upload_sales_logs/forms/year" + end + + def options + possible_years.map do |year| + OpenStruct.new(id: year, name: "#{year}/#{year + 1}") + end + end + + def back_path + sales_logs_path + end + + def next_path + bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: }) + end + + private + + def possible_years + FormHandler.instance.sales_forms.values.map { |form| form.start_date.year }.sort.reverse + end + end + end +end diff --git a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb new file mode 100644 index 000000000..d8cfedd08 --- /dev/null +++ b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb @@ -0,0 +1,33 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: @form.back_path %> +<% end %> + +
+
+ <%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "prepare-your-file"), method: :patch do |f| %> + <%= f.hidden_field :year %> + + Upload lettings logs in bulk (<%= @form.year_combo %>) +

Prepare your file

+ +

Create your file

+
    +
  • Download the <%= govuk_link_to "bulk lettings template", @form.template_path %>
  • +
  • Export the data from your housing management system, matching the template
  • +
  • If you cannot export it in this format, you may have to input it manually
  • +
+ +

Check your data

+
    +
  • Check data is complete and formatted correctly, using data specifications (opens in a new tab)
  • +
+ +

Save your file

+
    +
  • Save the file (CSV format only)
  • +
+ + <%= f.govuk_submit %> + <% end %> +
+
diff --git a/app/views/bulk_upload_lettings_logs/forms/upload_your_file.html.erb b/app/views/bulk_upload_lettings_logs/forms/upload_your_file.html.erb new file mode 100644 index 000000000..86dde8ae2 --- /dev/null +++ b/app/views/bulk_upload_lettings_logs/forms/upload_your_file.html.erb @@ -0,0 +1,17 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: @form.back_path %> +<% end %> + +<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "upload-your-file"), method: :patch do |f| %> + <%= f.govuk_error_summary %> + +
+ Upload your file goes here +
+ +
+ year selected <%= @form.year %> +
+ + <%= f.govuk_submit %> +<% end %> diff --git a/app/views/bulk_upload_lettings_logs/forms/year.html.erb b/app/views/bulk_upload_lettings_logs/forms/year.html.erb new file mode 100644 index 000000000..8ba1c280f --- /dev/null +++ b/app/views/bulk_upload_lettings_logs/forms/year.html.erb @@ -0,0 +1,16 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: @form.back_path %> +<% end %> + +<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "year"), method: :patch do |f| %> + <%= f.govuk_error_summary %> + + <%= f.govuk_collection_radio_buttons :year, + @form.options, + :id, + :name, + legend: { text: "Which year are you uploading data for?", size: "l" }, + caption: { text: "Upload lettings logs in bulk", size: "l" } %> + + <%= f.govuk_submit %> +<% end %> diff --git a/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb b/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb new file mode 100644 index 000000000..0157b66eb --- /dev/null +++ b/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb @@ -0,0 +1,33 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: @form.back_path %> +<% end %> + +
+
+ <%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "prepare-your-file"), method: :patch do |f| %> + <%= f.hidden_field :year %> + + Upload sales logs in bulk (<%= @form.year_combo %>) +

Prepare your file

+ +

Create your file

+
    +
  • Download the <%= govuk_link_to "bulk sales template", @form.template_path %>
  • +
  • Export the data from your housing management system, matching the template
  • +
  • If you cannot export it in this format, you may have to input it manually
  • +
+ +

Check your data

+
    +
  • Check data is complete and formatted correctly, using data specifications (opens in a new tab)
  • +
+ +

Save your file

+
    +
  • Save the file (CSV format only)
  • +
+ + <%= f.govuk_submit %> + <% end %> +
+
diff --git a/app/views/bulk_upload_sales_logs/forms/upload_your_file.html.erb b/app/views/bulk_upload_sales_logs/forms/upload_your_file.html.erb new file mode 100644 index 000000000..a178339e8 --- /dev/null +++ b/app/views/bulk_upload_sales_logs/forms/upload_your_file.html.erb @@ -0,0 +1,17 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: @form.back_path %> +<% end %> + +<%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "upload-your-file"), method: :patch do |f| %> + <%= f.govuk_error_summary %> + +
+ Upload your file goes here +
+ +
+ year selected <%= @form.year %> +
+ + <%= f.govuk_submit %> +<% end %> diff --git a/app/views/bulk_upload_sales_logs/forms/year.html.erb b/app/views/bulk_upload_sales_logs/forms/year.html.erb new file mode 100644 index 000000000..d8aa09172 --- /dev/null +++ b/app/views/bulk_upload_sales_logs/forms/year.html.erb @@ -0,0 +1,16 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: @form.back_path %> +<% end %> + +<%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "year"), method: :patch do |f| %> + <%= f.govuk_error_summary %> + + <%= f.govuk_collection_radio_buttons :year, + @form.options, + :id, + :name, + legend: { text: "Which year are you uploading data for?", size: "l" }, + caption: { text: "Upload sales logs in bulk", size: "l" } %> + + <%= f.govuk_submit %> +<% end %> diff --git a/app/views/logs/index.html.erb b/app/views/logs/index.html.erb index 431ca0f45..a340973cf 100644 --- a/app/views/logs/index.html.erb +++ b/app/views/logs/index.html.erb @@ -10,14 +10,18 @@ <% end %>
-
+
<% if current_page?(controller: 'lettings_logs', action: 'index') %> - <%= govuk_button_to "Create a new lettings log", lettings_logs_path %> + <%= govuk_button_to "Create a new lettings log", lettings_logs_path, class: "govuk-!-margin-right-6" %> <% end %> + <% if FeatureToggle.sales_log_enabled? && current_page?(controller: 'sales_logs', action: 'index') %> - <%= govuk_button_to "Create a new sales log", sales_logs_path %> + <%= govuk_button_to "Create a new sales log", sales_logs_path, class: "govuk-!-margin-right-6" %> + <% end %> + + <% if FeatureToggle.bulk_upload_logs? %> + <%= govuk_button_link_to "Upload #{log_type_for_controller(controller)} logs in bulk", bulk_upload_path_for_controller(controller, id: "start"), secondary: true %> <% end %> - <%#= govuk_link_to "Upload logs", bulk_upload_lettings_logs_path %>
<%= render partial: "log_filters" %> diff --git a/config/initializers/feature_toggle.rb b/config/initializers/feature_toggle.rb index 7cd75ddd3..8fc2cd7d4 100644 --- a/config/initializers/feature_toggle.rb +++ b/config/initializers/feature_toggle.rb @@ -22,4 +22,8 @@ class FeatureToggle def self.managing_for_other_user_enabled? !Rails.env.production? end + + def self.bulk_upload_logs? + !Rails.env.production? + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 43e03c814..2463041f9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -38,6 +38,18 @@ en: create_password: "Create a password to finish setting up your account" reset_password: "Reset your password" + activemodel: + errors: + models: + forms/bulk_upload_lettings/year: + attributes: + year: + blank: You must select a collection period to upload for + forms/bulk_upload_sales/year: + attributes: + year: + blank: You must select a collection period to upload for + activerecord: errors: models: diff --git a/config/routes.rb b/config/routes.rb index ce005902f..aebc664ab 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -111,6 +111,12 @@ Rails.application.routes.draw do get "csv-download", to: "lettings_logs#download_csv" post "email-csv", to: "lettings_logs#email_csv" get "csv-confirmation", to: "lettings_logs#csv_confirmation" + + resources :bulk_upload_lettings_logs, path: "bulk-upload-logs" do + collection do + get :start + end + end end member do @@ -130,6 +136,14 @@ Rails.application.routes.draw do end resources :sales_logs, path: "/sales-logs" do + collection do + resources :bulk_upload_sales_logs, path: "bulk-upload-logs" do + collection do + get :start + end + end + end + FormHandler.instance.sales_forms.each do |_key, form| form.pages.map do |page| get page.id.to_s.dasherize, to: "form#show_page" diff --git a/public/files/bulk-upload-lettings-template-v1.xlsx b/public/files/bulk-upload-lettings-template-v1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ca30388baf0f2e1933ef805f8e397d0bd9ca6c13 GIT binary patch literal 35928 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 literal 0 HcmV?d00001 diff --git a/public/files/bulk-upload-sales-template-v1.xlsx b/public/files/bulk-upload-sales-template-v1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..73c9f829c4e4ebe46b21db1539704ccb4b048927 GIT binary patch literal 26703 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 literal 0 HcmV?d00001 diff --git a/spec/features/bulk_upload_lettings_logs_spec.rb b/spec/features/bulk_upload_lettings_logs_spec.rb new file mode 100644 index 000000000..e9a05b07d --- /dev/null +++ b/spec/features/bulk_upload_lettings_logs_spec.rb @@ -0,0 +1,51 @@ +require "rails_helper" + +RSpec.describe "Bulk upload lettings log" do + let(:user) { create(:user) } + + before do + sign_in user + end + + context "when during crossover period" do + it "shows journey with year option" do + Timecop.freeze(2023, 6, 1) do + visit("/lettings-logs") + expect(page).to have_link("Upload lettings logs in bulk") + click_link("Upload lettings logs in bulk") + + expect(page).to have_content("Which year") + click_button("Continue") + + expect(page).to have_content("You must select a collection period to upload for") + choose("2022/2023") + click_button("Continue") + + click_link("Back") + + expect(page.find_field("form-year-2022-field")).to be_checked + click_button("Continue") + + expect(page).to have_content("Upload lettings logs in bulk (2022/23)") + click_button("Continue") + + expect(page).to have_content("Upload your file") + end + end + end + + context "when not it crossover period" do + it "shows journey with year option" do + Timecop.freeze(2023, 10, 1) do + visit("/lettings-logs") + expect(page).to have_link("Upload lettings logs in bulk") + click_link("Upload lettings logs in bulk") + + expect(page).to have_content("Upload lettings logs in bulk (2022/23)") + click_button("Continue") + + expect(page).to have_content("Upload your file") + end + end + end +end diff --git a/spec/features/bulk_upload_sales_logs_spec.rb b/spec/features/bulk_upload_sales_logs_spec.rb new file mode 100644 index 000000000..67187ff78 --- /dev/null +++ b/spec/features/bulk_upload_sales_logs_spec.rb @@ -0,0 +1,51 @@ +require "rails_helper" + +RSpec.describe "Bulk upload sales log" do + let(:user) { create(:user) } + + before do + sign_in user + end + + context "when during crossover period" do + it "shows journey with year option" do + Timecop.freeze(2023, 6, 1) do + visit("/sales-logs") + expect(page).to have_link("Upload sales logs in bulk") + click_link("Upload sales logs in bulk") + + expect(page).to have_content("Which year") + click_button("Continue") + + expect(page).to have_content("You must select a collection period to upload for") + choose("2022/2023") + click_button("Continue") + + click_link("Back") + + expect(page.find_field("form-year-2022-field")).to be_checked + click_button("Continue") + + expect(page).to have_content("Upload sales logs in bulk (2022/23)") + click_button("Continue") + + expect(page).to have_content("Upload your file") + end + end + end + + context "when not it crossover period" do + it "shows journey with year option" do + Timecop.freeze(2023, 10, 1) do + visit("/sales-logs") + expect(page).to have_link("Upload sales logs in bulk") + click_link("Upload sales logs in bulk") + + expect(page).to have_content("Upload sales logs in bulk (2022/23)") + click_button("Continue") + + expect(page).to have_content("Upload your file") + end + end + end +end diff --git a/spec/models/form_spec.rb b/spec/models/form_spec.rb index a062a4aa5..f8bcddcca 100644 --- a/spec/models/form_spec.rb +++ b/spec/models/form_spec.rb @@ -235,4 +235,38 @@ RSpec.describe Form, type: :model do expect(form.sections[1].class).to eq(Form::Sales::Sections::PropertyInformation) end end + + describe "#in_crossover_period?" do + context "when now not specified" do + context "when after end period" do + subject(:form) { described_class.new(nil, 2022, [], "sales") } + + it "returns false" do + Timecop.freeze(2023, 8, 1) do + expect(form).not_to be_in_crossover_period + end + end + end + + context "when during crossover" do + subject(:form) { described_class.new(nil, 2022, [], "sales") } + + it "returns true" do + Timecop.freeze(2023, 6, 1) do + expect(form).to be_in_crossover_period + end + end + end + + context "when before crossover" do + subject(:form) { described_class.new(nil, 2022, [], "sales") } + + it "returns false" do + Timecop.freeze(2023, 1, 1) do + expect(form).not_to be_in_crossover_period + end + end + end + end + end end diff --git a/spec/models/forms/bulk_upload_lettings/year_spec.rb b/spec/models/forms/bulk_upload_lettings/year_spec.rb new file mode 100644 index 000000000..0b0babb30 --- /dev/null +++ b/spec/models/forms/bulk_upload_lettings/year_spec.rb @@ -0,0 +1,12 @@ +require "rails_helper" + +RSpec.describe Forms::BulkUploadLettings::Year do + subject(:form) { described_class.new } + + describe "#options" do + it "returns correct years" do + expect(form.options.map(&:id)).to eql([2022, 2021]) + expect(form.options.map(&:name)).to eql(%w[2022/2023 2021/2022]) + end + end +end diff --git a/spec/models/forms/bulk_upload_sales/year_spec.rb b/spec/models/forms/bulk_upload_sales/year_spec.rb new file mode 100644 index 000000000..2276b1e4d --- /dev/null +++ b/spec/models/forms/bulk_upload_sales/year_spec.rb @@ -0,0 +1,12 @@ +require "rails_helper" + +RSpec.describe Forms::BulkUploadSales::Year do + subject(:form) { described_class.new } + + describe "#options" do + it "returns correct years" do + expect(form.options.map(&:id)).to eql([2022, 2021]) + expect(form.options.map(&:name)).to eql(%w[2022/2023 2021/2022]) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index cdeb71092..e25631df7 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -82,6 +82,7 @@ RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :view config.include Devise::Test::IntegrationHelpers, type: :request + config.include Devise::Test::IntegrationHelpers, type: :feature config.include ViewComponent::TestHelpers, type: :component config.include Capybara::RSpecMatchers, type: :component config.include ActiveJob::TestHelper diff --git a/spec/requests/bulk_upload_lettings_logs_controller_spec.rb b/spec/requests/bulk_upload_lettings_logs_controller_spec.rb new file mode 100644 index 000000000..788965e83 --- /dev/null +++ b/spec/requests/bulk_upload_lettings_logs_controller_spec.rb @@ -0,0 +1,32 @@ +require "rails_helper" + +RSpec.describe BulkUploadLettingsLogsController, type: :request do + let(:user) { FactoryBot.create(:user) } + let(:organisation) { user.organisation } + + before do + sign_in user + end + + describe "GET /lettings-logs/bulk-upload-logs/start" do + context "when not in crossover period" do + it "redirects to /prepare-your-file" do + Timecop.freeze(2022, 1, 1) do + get "/lettings-logs/bulk-upload-logs/start", params: {} + + expect(response).to redirect_to("/lettings-logs/bulk-upload-logs/prepare-your-file?form%5Byear%5D=2022") + end + end + end + + context "when in crossover period" do + it "redirects to /year" do + Timecop.freeze(2023, 6, 1) do + get "/lettings-logs/bulk-upload-logs/start", params: {} + + expect(response).to redirect_to("/lettings-logs/bulk-upload-logs/year") + end + end + end + end +end diff --git a/spec/requests/bulk_upload_sales_logs_controller_spec.rb b/spec/requests/bulk_upload_sales_logs_controller_spec.rb new file mode 100644 index 000000000..f668b0da1 --- /dev/null +++ b/spec/requests/bulk_upload_sales_logs_controller_spec.rb @@ -0,0 +1,32 @@ +require "rails_helper" + +RSpec.describe BulkUploadSalesLogsController, type: :request do + let(:user) { FactoryBot.create(:user) } + let(:organisation) { user.organisation } + + before do + sign_in user + end + + describe "GET /sales-logs/bulk-upload-logs/start" do + context "when not in crossover period" do + it "redirects to /prepare-your-file" do + Timecop.freeze(2022, 1, 1) do + get "/sales-logs/bulk-upload-logs/start", params: {} + + expect(response).to redirect_to("/sales-logs/bulk-upload-logs/prepare-your-file?form%5Byear%5D=2022") + end + end + end + + context "when in crossover period" do + it "redirects to /year" do + Timecop.freeze(2023, 6, 1) do + get "/sales-logs/bulk-upload-logs/start", params: {} + + expect(response).to redirect_to("/sales-logs/bulk-upload-logs/year") + end + end + end + end +end