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 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] [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 zcmeFYbCe*@voF}TZQHhc+O}=mwr#t6x~Fa1wl!_rn(g`CxBKqf-*4Z!_wToLPGv@Z zBC6_SR76HrW|X`XFbE0&7ytwS001EXOVz7j6(9fr;&&4n00Kx$*v{73#MW6)*~8w% zNr%qe#+slI1c)LZ0O(u(|3Cf@*1%+njNJwUOz1W68$OIql)8l)%m6}Jm@iHX(51hQ zN<>7+2{h3H>7VY#d9Zk9Hwb0hOX4PG=<5ZoBpd(jWy zR$L!+)I=!JHjI2kD**>i-d83Mb@JZGB4D;cQc3>eA6tQH>LVZlYNm`{A|L6;Oq5;n zPVvY4x9sIc1Q0BRvcl5jD-23fVoOtzdUn}7>jV|QrrdBV5UEJbnuyq_@&aGsnIao9 zmh=mCJyWY0P2|C}0Pd<~*O(Z0fDy?-%N~9Ok7&)6YKbjy!gZK z;Q?z@f6}6401Fg10pqk^h&&*)sbEg>Iy_H{O?thx88OEQi*adD@>dwd*uBAhHXXZbBfB4;TR8&mUj_`Txbsk#)OGfWEza=i7;) zzr9?~(Zt$`p6+k?|9kiU!7lxux?Y(mBL&Qe5PBW>hj6lsdxMH--kM(IM8^0NkTK&^ zpFJbKxc&24RM5Gg8P7Aj#x2();ciXuX6I;SXlG~jHz*dX>e#Jw zVRZA)e+kPfTc{B__@fZ%1I?SKjLR;$UXn6J5TeEA7;%HLT=rbM2vI~^7~%qPH}CNH z^5>42-dJQYpQnh6fa0(JJX}Nj9U)Gs2YXo~SGUkWTf!*Eh@1^`iL3BbHW%gxhE2Ev zO}LB(V9(mfTopCzqLP?^^DC7CA}pmOo^zQYnOY4saaY+S_$0fn`xw+Vma@evSY9`f zFjXouUYx{4wzXQ2#6UghovJ9ra`x^3sTF6!MS~+^6xv^1Ui7gVsHutv(oRiVyi*_H z3C*RdD%cKy|3Gk`ox5*-O_fuoSl+N)3*d5DHg7172fZK(#d^lc9#e57e(j!dbK z^~N55WyjH$1>nMh%>VZ?@M}+TqJOQw9s+eE2js2LmQjvIiLnb9syN*YQnTZoj+BXd zqL94qLbd-QL~KS!za#t1SS%tZINuRzStb&zUc`z=n=3YJc~RM^A3Qq&$WGq7jTKme zB$R+y^2qVq8}R)8;K3$uuonNHY}IhhP}sWbrpDb@w)UVszCASxRs^c8JdhUoiR}-q zm8{sHC+Ca76-Gxcyt^FV12Ng2EjW4XrXq@T-!KYe1TvsBu8oBeeYi_F`$KDs(3UBv zyZ*#dyLZ3)C;X8Ie%7gVH{^0oiag4-pX`Mh1x-E$Jg8X(mjuLdTc`F)wB57HX1&UP z+QG>lH_e39Fx{iM1oe$cefHO8`EDsHcq4!BrymUxKaZB2iG!_FZ11H+<1rGCTVm8w ztgX+J(C-$L+7qTTZx-Xg31Vi(9NI)yB(kX!5RFS%vOtcC74d#~Pq=TlrwJ^-VqAe1 z&4Vgm z4mFnL?1fyj^_(LcZCDocmdl(0f?I|?ba}-ZS<1e(T05fc{(G5#ZpS0`ar4Y3&;=5U z{jPerz+(UN!oX2O?gH7Zu>OXSso-MgE9W<{_DQt1IS#*`ImaiHSEi1+m)kvyUj#iX z-VE8CyWRB{6E__SLd90{oXuB!wC}(5;8S4L0s8Nt=LQM@fCT^n`0c^}5%>PHAOGJm z5b%43`z`x__R*OvV>R%1h8Of1w)?(Ea@h`l_E5`s;&QI9wol<;ZVj%rmFmZzG)ES) zrz=DUCh@W~Ks|9jRbyFbS6j`NnZP0|4x*V}xQt4i*U{1;hQxW$z8f>wD-IrOeYcVI z$jyTDNlJdcETSvIElOheE?tg{gyL3bA^0w7QMq>_tTmS!#arWVDp6nPS}AzPJY}xM zVAv9J-E*$F^V;4_IicD63lSU?i@q^`Z+G@5{gS(aEwzB?p8M!LmLRQRob-uF=1?*V zT*vT+htO-B@Fo5c8!_~pvC@3L9}|Q7Tv#Lj0)j9X=|Eha7dfE)E=LK@CADDs(&j$o z{HC1b0QN1BM-{l$AR}t#jN^Pr!luw|!15p2{VvQhVFo#;88eUK0<#^HFYtdCFo^n> zE%ZNCTt_M(#Ow%Cz4;|wCbA&BHMgcN@j|t_i`;+6|#0v z7|%%%^VWCD1NiV?Zx5_*$2HyMlMU^b6)xPM#~SGO=fBAtj8jdAPaj{z*248we_eCR z<~QBJK}!S=Pc)c*1(?pK8?>Lj3U_OA@Xw^76Gad^5CM7kB7sD3NOZ39YtYf}MI9)L z+O~eoaB?qYYcH9VZ9J&z{CYZns>rRpf0A+m zkBwUb2R0z~Cj?maQ;a)q`W}eL=4%NUBqHeju=z@g0pjiMYS;;)v$6*6M$JT&gx;qlq6TnJ>q4Z5JJ=8=riZIEhH2-jgSJYprJM_>Dr{QmN5FeW^HsoIy5fW8z%&xE9e0um6IRF*l6 zw*q%oL1I|MYB)h|o!i9?*7mwsmN1||pEL|!4C^os5G$Oxh&g5vW}HVxk3rqpA@jMI zQd<=FJvB+uqdDSeh0d9ukj68KI5KfqtOl1o=y80v;d?h|y9*#i1lVy}jolWZ&Dcp{xd=LuONeo!7MBBn}E|6Bt_3<}XUYPWpr6HX{-1?$W1{Nc0tE#f84QFHF{qy`-9~tj1lW+oOCC}% zs9$IxHuR?i*y7D|7NkN@V4(q@4ap!(XYLr)gBU&XT8JZ+JWlST1u6qimijSs$puS z_7R3vG{c)QMfQ*;RTq`_&Eox}{!R73MsjF~G@IxhB(|gwY5L2NI?(-1^yhH{7;$o3 z(YH2TRp3X!F$iD+%3)?0a;YX#NbH1@))D*D)@@z!`2-Q*!W~E{Nncv{yq8GM*wLqe(hovy`v{ z#$9Qi+IKPi6%rH%ea;DZfLSVtHGgvQBK@=ueQ`2P$IHfo9P>gZy7B%=ZGQ@%8@)qw zo58pIh5t8BeTSkyf3mgL9I-i(K2SmR*!xY{?ZT!YaF%^m6A#a2ekZKlGcaSPu5iCU zZ;%Ao5->ydzn4;|08WjMpHbm_-komj@5BGO**EySd)NEJ`SxYG7#*AY^>}lv^t=*P zniCh>{dw}0%`#(JR7C&B_w9BCzbvljj}tWx?&p>?es|}~WzW~`>BN3s&fd$KF`H)3 zM?L14b*Lw+rI^?je=gVQ%;U@9ecpN6Ob-Zb$HIx|6>FyUK%HoomHTq~0alHMQKl76 z!!XSXhe3=-wZk}8l=gl!Yiiptr&Xn64D@eC`?C^xz#3ghRQvNLdBC1+2vqwE>;j-% zA8O^^rWPSkZ!dD?-nQ#tDEH@F70sI~uL7X$PXguM4*ZaBC`lgh+82##R|i=;zwM?z_eD zwJLbJ{x@(`KT2vC9xs6vG1k#s0DI)s{mCDb@# zwSQTu->km*ZCDwONFTcBM$pytnF&6jP}dgS~%f5(T< zD#_o9oUQ!iM+;_S?!y5p#HhPSj33h$LBYWP(V=X7UnFY$0hQ9mAE_9y<+q zc{YB^CBYZJOCZTRFh(%V8=J&H(?H`y^FS*fEi?(?YfO|ca|uDPmBAxhoeuT43guuV z>}E=wwG-g? zEUKVgxa*<}O{g$XR_7O`|xlJ57dzdol?x~zf zrAmtftU}b~VN*@TczmtJcy#y+RiQk zKO)XijO=ljMTU)MVi3_F7kZ9#4y5L2=1mx)O0?%_>Py(~6EMp$t3kYmP_?0+BB_gu zTZJSesaq>9Ohm$`3ihC4igk5H_3%%a*KDVZjxjQlo5){i62vpg>eSR0`-dEBeu_v& zY8DrNS9J|zcsMKYh7C!S26>pjh6)vF(3nZJjl&O(<}xDvAI?32$Z|vqlU+V_N)PVw z?#CL2YVw{}VN&v*&CQVw%NC5q`0Ua9K}%i*ED}4(#%^Vqj2n|E#Jl`+JVT~4<^jpF zp>a1++T^(eI;2W~btY5h0q1qpKB}=<;qZ39(SR07@Hl%4T%LgfXMh?vN$}cMbgov1 zJKfV=PPmdFaL#17+yi+o_hjKy|GXFlN#}H7$~g(#Ao^FLgE;dm-J(z%{iukgdA0xT4}jn-NF-5V#;oVGiEzizEOOr zXFutD?KkjWwrr1xNJ$003jQ0b02wlIo}tI#zg&>Vwf@Z zys=p@NfHHPTB}>=q0n7f*w++8qs^l^nJ}EUvN%8kVG449HH6l3ieL*WtiXEwDB{^r z2x(Yhh|)p?dju(W8vXEA-(kYeL^blOw>1QZ(`dqOMFogbWt2cl)N=3WjUd%fbY4}M znt)oiV6|8xI_lt)aoO?T!-dF{twS*)=tKEzUmumm_cnStf{hJU<*0nY6PA-vMJ|W zT_G+HXN@ZcB~UFOE*f>vUQ;KPDTO+YiJr7JFM`>VVQz|%>gSziDJwc8XL$R&LgOQIiQZ%pj5Lt~#1|i(ak1}v_&?U{~d^NJ$BADy4k1^x9AeXX2hIZbmPAcJw-R0 zHFmiRGOy>({X_fw@N#c?4}Yrt!0WX|3!?Dfg2_pS*DossaHWF!-&X9urtBsqo2xAk z6K&>IRC?6wZH5C9VQ1#nbEYM2#@c@S>9x3AwuD{B^~A(>^ZWX+IyRs`c^2Jxxk0)$ zHaGY=e@?DU%LddCCF190Irc9bUrH$+g$TD_DF0<$E(iI#6d zOXi!ncU}#?LRL%P?N$zfv{h0`->p{pCJa^5XLD6UU~L>!R%c_?z6t+t!gtjGyZc(( zAZSE~8i{NttU4VWen6E(rkg%zvVqNP;8(bvj2aJOp+=KJ`KO-r7yPy-0ZkIDw5!U4&OI>RM8D zz8XrlGieu**F5TDWMNWurCM^1b4V9a+J8HAJS(mu)$!`NP}yqqLf-XR!KVQ>=eq7>odF)_x$mk7)-*!V zHHOXIrRHyA8<)al#XLx(@NXk`?w^KQs?-|m`YxF>Wrrv>SAri0+{vCFWk;yTi7-IP z1(wZCy3k)vXO8aobHLyS)9S4n`|fZ5vh1Hb{@!R<@UK90(BB7PfPXWrPksUab15}Q z2^H(QK92H%!keU^2wsUCQgX%Oi}knE|APnrgJb;_8T^ODYmNLNnI~Y6n$g%NN7Ndr zJf0{1bF}1RESM#ux!_CWr96d1zl7z37g4UEEFxb5Ah9@r}Ta$Y5duM6C8ad4pJ=)?1_r?CsY)@wAs z0?W=ywuJ)piO>mSfpI;8Hj*)5r1~%8@L@Xf2{rNRxZ=lm zz4xwm7Od=b*{T-i^_Zx1$fLG6EW=V6(+BKTQA{8AbFQ5M8QW(`c3n{JkK@Y*S#Bs{ z{u}PVdvOrlX`WwChm=RsKATX5%{M`|t8gAI6F$k_Ow$=@$hEepzDWlMkAHuE&*y*z50p< zysp|WVQiY$!+h+d{^x$*tM~u9i`SLk(V=pwN z?6BDpy3tSk2%d7)>q$qLh`>{6h9o`$tBOYxG!;4;+a^FI*5<0jV?m)%WTJGT9+rjY z*aUzc62GuoDA!R&^-kioC8VMvtA<7NT>ahgcVLdE)(Y&lJ(C%nrGtxks=yD=O8^pB z4tO&~A9sDrd~8Ss6n{5{VaiGHVjy8|YI^&;}4(r$V5;KX37hW?4!ZlKb*U!P@KaCfo5`d8cr9TrK`v8HqVn@HRlpS1zSqIYD z_&9Lp6udT{tE4Qva-VbTQ0%B=Qf*MdyJrT#3C%B0DH&~;*QI0^<$)&f z;+*Qxs7R%v>+bclkUY{Xn)|uzs6nFyT%VRg`5|f1R^NTH3p={^OX)?@sB2Bz^#;uN zc0%dpDQ4YqXb4+=(%h3JWEsw;#p%jOJg-59-Wzn6x&(($P>?0MNQ!>$xI7cp3X<=j z5EcN|SbYwq+(7h=BbdT=ub7$wPCitL?zf;t-69=r-=8ca6b6A6sJlpD*S35U;Frf0mACd_8V2meBG4yiF;=d+Y4lB5oHzCYXn1 z=lkUP+YU2S9~4s`6`c&GbD?m6ZR0J|qH_1<5SsF72el4+5#%?%__u(E#a4pXVNjfD zyy|9i^x8~o1VPsTUY#~UpO zV=e1sPE0|CHIknG){t%G*?Y41NsKcyb55z2G8dtj01=;X z?&a@{e&#`oz#*Dx?p+shX9K(Y)Oj5=Jk$Vg6i>cz^cn?M(i;j8H!HN2E)4FD)b6w| zcU-6uKlCxi(TM?%dnEJGO&0l8dh0AW(`le2|5N?QX758Zahzv4@rQ29Q83;1?yN>+ zb^-PS7EVZ+o#k5wC^(*lTjd%qyp8&tOLtS1)G214X`0QR+K#o}Am_N=P$r%B>ddTP zM`SNE?CcGWuN5V2mKN2TSUX~?dh_$r?HK0vWXO2T*Ak|&UT?{Dz^ul`QkfBP!SGqS z*m#)=$r)m?BYqk%X`1-zy?r7;*jOa3Z~hf81jCpxVdJLmm}A*4Bnrc%lhKn#%%Y@b z?pb{lAqnvr4yun26YFw@O;cV^w~7wqoTf&_k&`4YjkAfS_H#q2ww)V>Ce%yP4Q&-E zG3KR4_R845DrI=;ztmaXIhx758Hrz}!Kmp6YBeU{oLJ0jyw&jhR5)nVns8$4&j0BN|u5l-cI1V9FY7P8snX>G2+#8PICzvKT ztG3EeT+al^ir0IYnhOkc4+(n}nPu7T=vDhF(@Olyuymv&OHd&~dt{($i_SIYuao3a zb9CGdk50kj(-isSf8Y#uAupN$*TwaHaw!Yu;iWK0u%im@^cKW7H+vkS9rK(0a{P7MM58`C;gW%DZ+tRxi*zK|#Tiz6*~`n{5W+=C=8j3<-fVxS zYlitJ_9@wM-ummRac&E5`b9I(8 zmJJvy@nX@dC>XP5jqyxrvg~Asc~0b z4>zw<8a$luj>**lquTGeD(zJYoL8eTtXy+L5jJkL9*d=gYtOG5;ES4Lh8u3_t(? zc(DNhVE-fbIyrk-n>hX51-;PFvfJZ8@zt~VBYg18f{p|g&OlX?AexC=wOHL)L*~$n zMJ^h98mw{YmuEyCZ!BpcnGs;wLcdD>bnA9?b^9Wmz_gzbo_$9nFr5+ulPJPdx<(}6 z#~XYEfY~;v5sf(`?$1UdhELY>hi&>WG~u0;a2^0n124ZM`B#F;;ap-dN6#L0VqaQt z6pB=OWJpR4NS{MBTgjxOO^01cy6MjY2FW5iX+@guN;T8L1=Lh$Sok$J$m=?T&pjE5N48OAPBQoc=j%G#^ zPq#9Vq{FcsQ%Hs?DOR^4qA-g{+DopLlAP(Ja@F!Q_s!bc^$bN%gKkX-a7(#JOZGWi zE@ib{nJ{pa%hR9zFd7wPPp-?6;3@GTkRIOcNDQY5j!=lfXQQ6=0W(mg60Gqucof== z*?-$&Kgga_FHqmKjEWlK5h(a-Xxchj0?!;OCUEe7%RSl#sH9m0-HbC+*#@N)RXofu z^*Qt$`2E;a^i>T$@V$o53cvyTW*+f z2s+oGw6ofHejBn4eqP)E0BL@aP1096hO3aPV8Js+MEJB3hrkZeO;Zob&2vriG6&Uh zBOV>_aGqATjTJ=V2S$h5)rVVs6p9L5;fTO;8-x*<1DConbD{zgf^}R>VbOglXr|?o^V#S%h5m@%w znsQl!r|-ZCk(5N60aGZKs%#7?`U5tD;v;k5HApqOeMBOQrF?Q~xs4uL()hRpX*m_TXy?}3Pb5`% zQK%#w@-*uSw|Vp+H9vJ|yZVcDf;T)rAweHA0-Z5b(gGjOYrCH}XITQ&(&h&MSB5~! zU&`v z$`7J-9g#lFoHM#!T%>?95n^P;e$CXKt7B9&;pWd&F!y5l^6M#q<&5$2!tG+ByR7_R zmn`u#nf*-OqLjZiOx$Hng71g}tQhf&Pp?+G*|1#7dq|L)A`*CfNwQSo@>zlo`7_9W zII9(`XGCl6|J9bI3vcFDUC6Tl)@-%&wc&p5J|Gq5n;GyF zvLE9Jc1>7_)C}aOw*a)5o_ymngQ3q}2fx+rv(Md(`D8uv@aC=$_r|*e<1>=;*matN z{|0;Ul|WC_e8_RQ5eal1M z(e@sNaU4t6)6z6_hk>(vOcJ|$d* zpy)o}-jGF~C{uzy{OCBSMsFKV4ount&x0p9fdiihg4NyQ*7{nSa*6=E{63)O@vbEZ znDu@r@E5uxB~fQv+HIiK2i6SyA`{xCQa6GpB3^6J++1HE_?-WgG1BRMpZfufIufWK z*`CYWpi6e&PzDs;^urK8askV-n`=J(G~9{C+eg+6D>TYNxBdi4?yGhL1{LpZz0J=N z{Pa$C#mF!qpePdxbD$#!THSiWj&@6>+W3U0mh#p%3#D5E_fDCCt*z2fzn-^|!}py( zMwCaCU<(I;@9;#ldnyFW{YD(X!$pQ2Jc?F0tX}buTHpqwsdd_*x%#W^PXWdh$7s%J zPMtkwHPMb{PFG@fI-#$8hF~X7*rKL2BbPPgmo@MkdiXZoUmf8*fy2lNaE*OM2K+I^ zTF!UWedPrKHfmi>0H&#XOPwyt%n0WLy)u zHYLt}<}I>g`iWH-+;% zd;}CC(F)?t?_?sysisrwPlz?`Y(sIR7PH^-)8@|+PbS$R2Y2B@u`bjFF5#>%3?S4v zx9ZGc-l!brlCKYcx}kQPxT#wc$zput4!B4Oww4vFRGkA)*NqtAQSR8AyWj+vZpZ{A zKOYi49erM{{0xjA#2B6zd)JyEWGn^Rjc?+`pO+9ME(%Pb)fD3w`2BSfmyl8TXyjj* z9t5U{LW*%0_Ct_z7W>JFl2LI3ij9VX-Y4=1CoQP?qIH^$tCBHaWUn4Y4=E2J3=@+iA)y& ze7IO6FnR5dPQrE3S!$7frX%KmoNgN|o-TaA<9EgM&wZ*}Im^~3Ce=@T1&S=x zyLGGjWN*}v2sXek6rLVXj$dXF1?Agy161IfXy?&$@$1Su`O@UW(oFEG zfk=$vUz=umhtc|7V35Efr?A_})F?1y(*erEIyUR8;6Of>uAvmiT=K<77XY{3v7OR7 za8V%7kWXzoLGl~K0%h=d9AXZLaVtwZ(D$jNMf} zzdsH&?#xH2ebiMffBgsApcP@a5XEFg zeN9ipgnPiwj-;`A#oGK=eVier8!&5(QLtUdP;Z8&qK}wJAB{Tb=<3fyVremhIGgFf zKn}<`YQ1+PMg|?&DrlTO<49F8L8pxgh<;+bPV{E1%qRQnP1U88_uZLAjj~^M zET`?mWhYfyX2E&-QNbByK9vxbbwwX`VnC4l`0;eq?VXCJx@q?8aN-T_=BYW)qs zDP(&UnEZg(2w%mzS2gqd!;cP| z5eEs@hSPfwQCN|UjWgy^SF(MgOVEyZ2UN^IOUlZJ%!@qw8h0t^9oymDDI$!k5Ze=M z%wrF5#cbQL?2(;FE_FrKLQ|MW9ME|?L|3Iq@bEJ(rHXmacN0l%)IO_{?K>nokrrhu zNw}Kg^5a?GQ-${MTK5+hM{43vF8JCM^~~rPONgLsdM^T-#;~}+!%{h;@JyhHSW69{ zOf9MM-_jP9t3D?)@lz)zI#Tz(hLUaD7_kX{*=|B+E+iZ*dM8t+bnrq)1E>J!sKvg%uBD@@@?H%egS`lR|1; z+di#r__%7lU+wdGZ9UljK-NT8GG#AY{-DF=+V?ra+1ER#wcsk@f2yz3ht4!~zdT@# zhh1a5oL2nv`_oMR`WJip7wo&$e%;pg_Lt-P`O*4KBivyoFU(r8&s+1Dj=yVgUXss| z?2VI9#c)x9jTk6R(+d|dbd~gONDMa__fR%VUJ!m|A1g%r(^a(0Qu_5QSJ)f^xi<5f zniIdv<}R}b?2nJ$WC`!oCk2?gqWN|2$9|MnLb~iy3XNo_Bj@Mx!y~{Lhk5cWcyf1l zZMWnk&G+@rfwH)tzDiPj10yOY3z?Z#_I=yGsdG6a3wwg!?2g5P*W%4PKOyHS;ffE*aMyxFmQ8=jYvE z7R-o|Ba~{Z&V53;3mbVY{XI%KV-Kt1P({+lSXNLl1T!$_xNA`*}b)i~sBiQEd;%VqCK4o1kAEoXodH8GYXCBas;x3ieFd3VqLGiT`7!`Zr(wf20xrM*{o*OeX#(eLXm?FYP# zH41ySvcl5lP7l11Ubg9)=DKTI2mM;#1%q(9=9K@K2XUUiWfpLJed+`%a4gmu;;6E1x zq#zh#K~^jvqQd$tAn`%~%XxUT*rG{8<`B`Lz2*=IaDWH(j?cq0A6IT6uR0T{mOnq4 zj5IfD4(9}MhG><#R}BRQB*01BXmiR>@f5J#U4 zM$N>*>K*+r5c0dV^J#uG;#i}dnbVIx0OZ@^=WFx{JQBkA!gvbIqf>A}foVZlI~nPJ zznFhxy{A0Ihg;w8?gJVC0O4EWI|<&%z}iUI+`z*2@3N2c@`UYz0E+)_UZiXNsa&qA zA0hJDXW7ijj%9B8W}L#vV`-z!^6yXX3m{SV^q*cl=u<5>cNcZ#4gBT!%{p4a`$Zz7 zidrt`Ht+Jm8(H^ic0z2?CLMI=)iNUPlENHXUEXz-?8*#vME_@b3&(${cyE6|u6o_%&!`?@coaDpOUK(Ft{X;MSTU$O(Uy zRRw>v?OeP!PV#@Ez2QE@eSJJ-KSRIm^xWUK<$Zn(&FFo8 zOwH_kdY>Ls^W)RGyW+I!wd%HRdHIdu`+l7s>-nMuRL?#a*RKhZ_Pghk>PrEn`O^XF z0(FDqjXZbE8Udt%JgLgvV+<| z{TBh|8`IsX-mCz2U^}pFm`+Uh{~ACW*aERl6yNHtIIk^cK!0HveqB5fqXD zP~;PQ3;pkp=ox;i0Rfn>-!+D|Gh-I{w8O5U)sQId&ZuTNKJiJhNs7n&Ep(skt_yd3 z{EPT@uk)P>OC&t4U3NOcIL4AZ@K=@&&nK})dhLoO@=ZV1y-_N4FgFcVxd33HO_cML zeikB4fsYX|mI~OfG(X3W*$WB3sBEG;Ej$2Mou91)rE%G<;pxtlrOA1vsl#&4x~cU4 zSw8m_V&)794DsT5fF7ZO}Dt^>6<`=?rzQhh@n~lomnw z(@}nI@_1t5&OG_NJ^6KGTY3VUy<$%7npR8$MC~y9q+OSG%(q=Xnp7K)w(~wZ?;PLoWZL zNxO(v_KyRjCfCDHiUf0C7&?7mlI8Q43n3ISES~*8{+7S&(O9Y@;sSZD1hwraQ4^KR%ZiivYT#f~^4K9-Hm4~en|+n)lgLOUqwa=-y2Ef8rlXBb@I zm~PYN(ban#JZqY{e#C~k(x_fS`}L*Jn23f8ha$1K8Pne%Js!5xqFqQhQ!s=1qB+2* zoAv>3yd)?0)9>FZ6ieGs#qIl--{gE3toaNg{gkP&u zXc6CcW?wC(#k#&#oh31`qK=@FAf4{nDYzP_CnW_9G)xK`WHRHxnSPDY?csH=c9m>2jFD2&DP(LV1oCA_xNUl=!9 zMC~(We*&n{SaEYXi?Y=B~yfHw^e74cq*vaJD@6!k4jt*4U@qCUE0o@9@kYN;d~z<|Tvt>C_k z$i!iI_aZTj<;|RXkyNPMwp%B@&SuS7RxH(Ge$JW=Q!Cka%$+9#S^X-?!J4T%eeloV zIEV*xA08fFD0~v(K6gn+!tKmu*y@#H2=0&fraq+^>MP+m_aQg@jYP5Hl@a{cb83r#bbP>=I zEo|+-G2}YZqTzp_Ym(G;ozSYqpC!^}E3(*Ba&Fhqdj%N8>JRKnJ06VaYzS zr~0XU&r9riVHI?VK3NRs5m{xTUvC+X6OLF^cx6a`n5zWJ*F>mU*>G8u zihMZNDM?zSnTb!Jr_0GC!Xrz^@FXkq-1lI4N6qTPIEatKLb~@h4Qk*MiD@Y3h&Yvz z7rif%|2SO;G(T+7$eCw zIcn66HaBfWI+cNvDOHWvTmToHpJo$qm8ak3AJ<(oJMc3Gp{pPJ_k?6*D=09C20n+9 z#~Alu;9Nj2Byq~*VB&(IpE$co{l(C*{z3w_1!^vi*7qbg#mhSaD{6hP`O4hI{t72{ z8W6aMoS+h@8;_oPb<w|#{f(}6TWET0(ND4q1LOlS?Haej_CNB#$ z{)MFJvaqwK^d>cJflj`)4EElHi2B=5x@!<`Sa>K2O4`V%Kn{x$dXSd z+a{UaoP_NEYVXXWp>EqaPGu0vE^7-hwum9Sh{z;+VaQf?DeFAO^4NE28arhx60(eu zm`IjHStBthvWLOQR=vMG@B13_%=13y`SU#|j&sIwu8(_u^Zm_z-Pe8H-`RLt!yRf` zJ~}`6yxiGWzvJoU^{6r9k`t$-n9b6fA`a2wn34F;wr7fi$#Aly5qFBT_|4Q5w zI_e-5<1}-Z^^x9DX9m>_EWfRn?*1(gR7{xQSL2m)?$DYRSKVv`yya=~LE(rGpv&3E zZn6azSqIZ*-=YllV;}Qj@F-yiEpbuwMK*{J?He4M;nIZPKh6Do^K+^rp7oJm%(}2h z)|!&oLXiQuZXridb@876RySIP!R%@yCPlvGO9QWk#rt+ubB4)JnJgL2cdw%4l=I{V ztg8!??s@hn(Z4mvs@|D?n5n7yQh;mt^hRlauXsCyk!)mi9DhT*VXN`MT6f^Ra849f z&=+q1NWyKHBkSSi5aUVo(m~^ycaipH<4b{PX^Wz6c1T1)!iO$4k!S_x6vc+MiN*da zDe98*XJ7FT=qtWz9T{M#V?Ojo9@7X?4x~TA}kOtsNnsf(mwe}>2r&FaGkl>$6e~;g-Yi%QSI*+13kmGHN;ul(sWzn?4Fd? zZ;&k~^=U8=d>1~v36pDO4+KcwtwkcDnZ>u>ETqz*;cg!4W8k_!1iU7PV_ zKl56rF*P?k`uVw5!Gbd*LpK!tyb6>KMV|B5o>Ee$2*Fumf5>UASw$cWSh-n0pU_Ib zq1Hn$a_Kls+g!kKS%6J58Ow9!_zEV32kf3r>uM|?j^1Z}Ew`Zvm#ozJJuqvM5+&MI z_L7tHsLH3dupnkQD*jTnJw^TfW5R0is$-Jk;4P*6Qx0qC z{^JJo#pxd{`-|A$a1V?z2m6T`c2V47E`;zEe~b|cq>$;EzHgrQ+z<8*X!ZK@f_!?N zE$e5!LjrJAVgkHauDRRo6gnjA+&!-UA+`g*ELZ@6`Xg~S0F%A&Me2E8dYod#eAQfB z6BQZKF8rw;XrSIsJ68=D5K|v zKfw7k2i~}`ESju|ymVolx776Z9m_L|qvAOwFQ~Vf{9}V5En;^pigPT>wQ@0&N{#&H zoE(oDPvT!tiwcL^w8LoiJYws_VxTwf8wu+omL0Zdpr{l(16<_Co|A(9WbYkLuYSSAMUZYIHdyTM`U9J- z^@QM_Eb4?@m#6gr`wekKzWgXwJ7SXFHx^nUsMnGQIc}XVjxzh)_Hub>V=62ReDOG1 zCQ%4M(EQ_`T3 z{$PeJ-130Y*;@;K4f*FX`|wt3bfVmWA8*z8YNu9mPoa?~K8bE%ks~13`DJe*HE9J` zscY2~H3X4`-(F8kUZ)d=Tx3d3QKzW~1ma$dOhSSJj7oJpiWQ{&_9 zXRFm-I*ST*-0N)k7|V5-1%2j)N0m$w zj%t|I!=YPfqPDq8>J4LFWa#{*G+p>jsNzcp9t$t?Yq3>J`q_BD6atk`ENGca?RjfjRx)!9bgUs ztY(SkfSsLzU$@DBZW0O#^j*MRP?9xbQIw#n@!Z!gQ%(+?6CzM((u!u-^mekX)rHQ# zlBEQ^1v?$>qGEBbvk$xz_>}v%?#=rRl!jhCGUS*P`hsm=uzuBKykFakZyq3Fnqs?{ z%BssJs1Y_V2J$dz8@u|S+M`Z<>*qtG1$NokH5qPgX9BD zQIFBT?}wx|LUbY-4Z@aKIV!yjzfq|9nzkI%FD6)heGhAHUdTwiH(1;KY{=Z91$9&1 zuH$%;X=7?HO^>k-_zEp&MHCIs12g!C&c?jXpma^G%6CH{8}yyU_V{sUn1pWdB zRLln`cy~+dkA3<7ZfyN>mD|-aMOf#Hn0=?|L1VRJim2hNw{XTgGjKEZy6C54RbEok z%}z$?;LSC5JyEgw)3h7k^Kq(;#S5u}MvyLJ8E{;J-%+jos#A;a*jZ!av01{o!9r5= zwV!j9Ke|2;!%wh0&ef2WjLy&MpBJQ07S%b@el44)7$tRE!OXXcf7aX5-RryC*{?Q5 z=iHAS-30Y*r0FdGkYxzyIbY|-f7fqME%_BI$)XAQ4TFB+zM=0a!< z!jawBv`7{@@$zp}u(Rp%O9gvX;0$;Rg2oUzi`HINnQM|d(Y;X43EA^>ST3y#7wy_j zb(}^69~Q8K6m`@`C1fHIsqYx15iuZH6NZC%c_Xde4I8V3^8L-WcJ2-_j$^^}kv>@G zz?&GuWcYHliNOi(RE?giE%JW0tnADCI~nYhR({EVtPE^+WZ?Bv2JK)#2Gq4&TDLlE+f8Rp|B!+5PZ7g6w+N(qz=|1t6>Fzd5 z4lXVH(-n(Ry6dGC;(X0#8RG`}p+<{T%B!0T^&iO=*yx+MkbftGodUuy8T=0!piciE z%7Aj8IB=MR{3j_8J=dLwWIk6xAG#N?*QA{oFbQPLM3(KOfuvT|yP=%a|MemLk8w3= z@LreP-B6%;0yr=8*Pgkg98&w!U6Kw^5&A1LR8k7*dHr3=C~$%wppc&4Ck2omI@|@o zM0NnXM-WLvNzVxFhE4&^0KotDBt0=iN+9js+$G$RC%pz_J2^>#q^*#r?>0k` zQucQE5L5ckl2Ue(F;c+Z&KY7ry&5TCXBcEDd)q~bDU9bxDLcsk=`i;0&kzH~prnAE zVUVTlt>zO`)^$lKJChOVF!ol#i2;- zMB)D~sgXwSEiVv9yIdxU{^Pm)EH;n=_O9-T0pe!F*MQtwkCe4{jX=zjhLLCOt{6yJ tdlOA!mVzaD)^6HK%G#T)5VO>*$g_5H7kwxd=w}2M@Qnu^Y1{n#?O&96fF=L{ 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 From d52d45b941c5884eb0929a12f5fc537b96abd39d Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Fri, 2 Dec 2022 09:23:35 +0000 Subject: [PATCH 8/8] fix test (#1047) - we now do a lookup for the expected start year on current form --- spec/requests/bulk_upload_lettings_logs_controller_spec.rb | 4 +++- spec/requests/bulk_upload_sales_logs_controller_spec.rb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/requests/bulk_upload_lettings_logs_controller_spec.rb b/spec/requests/bulk_upload_lettings_logs_controller_spec.rb index 788965e83..3b59d449d 100644 --- a/spec/requests/bulk_upload_lettings_logs_controller_spec.rb +++ b/spec/requests/bulk_upload_lettings_logs_controller_spec.rb @@ -10,11 +10,13 @@ RSpec.describe BulkUploadLettingsLogsController, type: :request do describe "GET /lettings-logs/bulk-upload-logs/start" do context "when not in crossover period" do + let(:expected_year) { FormHandler.instance.forms["current_lettings"].start_date.year } + 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") + expect(response).to redirect_to("/lettings-logs/bulk-upload-logs/prepare-your-file?form%5Byear%5D=#{expected_year}") 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 index f668b0da1..348179dd2 100644 --- a/spec/requests/bulk_upload_sales_logs_controller_spec.rb +++ b/spec/requests/bulk_upload_sales_logs_controller_spec.rb @@ -10,11 +10,13 @@ RSpec.describe BulkUploadSalesLogsController, type: :request do describe "GET /sales-logs/bulk-upload-logs/start" do context "when not in crossover period" do + let(:expected_year) { FormHandler.instance.forms["current_sales"].start_date.year } + 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") + expect(response).to redirect_to("/sales-logs/bulk-upload-logs/prepare-your-file?form%5Byear%5D=#{expected_year}") end end end