From c481055f4dc6fb6a913f31499cf38ea6c3c9a79f Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Fri, 10 Feb 2023 16:46:00 +0000 Subject: [PATCH 01/30] CLDC-1965 Bulk upload correct again email (#1274) * tests for bulk upload mailer * bulk upload send upload again email --- app/mailers/bulk_upload_mailer.rb | 28 ++++++++--- app/models/bulk_upload.rb | 8 +++ app/services/bulk_upload/processor.rb | 5 ++ spec/mailers/bulk_upload_mailer_spec.rb | 54 ++++++++++++++++++++- spec/services/bulk_upload/processor_spec.rb | 11 +++++ 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/app/mailers/bulk_upload_mailer.rb b/app/mailers/bulk_upload_mailer.rb index 59d03ce9b..be0a71e05 100644 --- a/app/mailers/bulk_upload_mailer.rb +++ b/app/mailers/bulk_upload_mailer.rb @@ -33,17 +33,29 @@ class BulkUploadMailer < NotifyMailer ) end - def send_bulk_upload_failed_csv_errors_mail(user, bulk_upload) + def columns_with_errors(bulk_upload:) + array = bulk_upload.columns_with_errors + + if array.size > 3 + "#{array.take(3).join(', ')} and more" + else + array.join(", ") + end + end + + def send_correct_and_upload_again_mail(bulk_upload:) + error_description = "We noticed that you have a lot of similar errors in column #{columns_with_errors(bulk_upload:)}. Please correct your data export and upload again." + send_email( - user.email, + bulk_upload.user.email, BULK_UPLOAD_FAILED_CSV_ERRORS_TEMPLATE_ID, { - filename: "[#{bulk_upload} filename]", - upload_timestamp: "[#{bulk_upload} upload_timestamp]", - year_combo: "[#{bulk_upload} year_combo]", - lettings_or_sales: "[#{bulk_upload} lettings_or_sales]", - error_description: "[#{bulk_upload} error_description]", - summary_report_link: "[#{bulk_upload} summary_report_link]", + filename: bulk_upload.filename, + upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time), + year_combo: bulk_upload.year_combo, + lettings_or_sales: bulk_upload.log_type, + error_description:, + summary_report_link: summary_bulk_upload_lettings_result_url(bulk_upload), }, ) end diff --git a/app/models/bulk_upload.rb b/app/models/bulk_upload.rb index cf2965446..8385b9ba4 100644 --- a/app/models/bulk_upload.rb +++ b/app/models/bulk_upload.rb @@ -30,6 +30,14 @@ class BulkUpload < ApplicationRecord end end + def columns_with_errors + bulk_upload_errors + .select(:col) + .distinct(:col) + .pluck(:col) + .sort_by { |col| col.rjust(2, "0") } + end + def general_needs? needstype == 1 end diff --git a/app/services/bulk_upload/processor.rb b/app/services/bulk_upload/processor.rb index d69f76243..bbf7e6cd1 100644 --- a/app/services/bulk_upload/processor.rb +++ b/app/services/bulk_upload/processor.rb @@ -13,6 +13,7 @@ class BulkUpload::Processor validator.call create_logs if validator.create_logs? + send_correct_and_upload_again_mail unless validator.create_logs? send_fix_errors_mail if created_logs_but_incompleted? send_success_mail if created_logs_and_all_completed? @@ -25,6 +26,10 @@ class BulkUpload::Processor private + def send_correct_and_upload_again_mail + BulkUploadMailer.send_correct_and_upload_again_mail(bulk_upload:).deliver_later + end + def send_fix_errors_mail BulkUploadMailer.send_bulk_upload_with_errors_mail(bulk_upload:).deliver_later end diff --git a/spec/mailers/bulk_upload_mailer_spec.rb b/spec/mailers/bulk_upload_mailer_spec.rb index a3706beb3..d0217bfaf 100644 --- a/spec/mailers/bulk_upload_mailer_spec.rb +++ b/spec/mailers/bulk_upload_mailer_spec.rb @@ -5,7 +5,7 @@ RSpec.describe BulkUploadMailer do let(:notify_client) { instance_double(Notifications::Client) } let(:user) { create(:user, email: "user@example.com") } - let(:bulk_upload) { build(:bulk_upload, :lettings, user:) } + let(:bulk_upload) { create(:bulk_upload, :lettings, user:) } before do allow(Notifications::Client).to receive(:new).and_return(notify_client) @@ -75,4 +75,56 @@ RSpec.describe BulkUploadMailer do end end end + + describe "#send_correct_and_upload_again_mail" do + context "when 2 columns with errors" do + before do + create(:bulk_upload_error, bulk_upload:, col: "A") + create(:bulk_upload_error, bulk_upload:, col: "B") + end + + it "sends correctly formed email with A, B" do + expect(notify_client).to receive(:send_email).with( + email_address: user.email, + template_id: described_class::BULK_UPLOAD_FAILED_CSV_ERRORS_TEMPLATE_ID, + personalisation: { + filename: bulk_upload.filename, + upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time), + year_combo: bulk_upload.year_combo, + lettings_or_sales: bulk_upload.log_type, + error_description: "We noticed that you have a lot of similar errors in column A, B. Please correct your data export and upload again.", + summary_report_link: "http://localhost:3000/lettings-logs/bulk-upload-results/#{bulk_upload.id}/summary", + }, + ) + + mailer.send_correct_and_upload_again_mail(bulk_upload:) + end + end + + context "when 4 columns with errors" do + before do + create(:bulk_upload_error, bulk_upload:, col: "A") + create(:bulk_upload_error, bulk_upload:, col: "B") + create(:bulk_upload_error, bulk_upload:, col: "C") + create(:bulk_upload_error, bulk_upload:, col: "D") + end + + it "sends correctly formed email with A, B, C and more" do + expect(notify_client).to receive(:send_email).with( + email_address: user.email, + template_id: described_class::BULK_UPLOAD_FAILED_CSV_ERRORS_TEMPLATE_ID, + personalisation: { + filename: bulk_upload.filename, + upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time), + year_combo: bulk_upload.year_combo, + lettings_or_sales: bulk_upload.log_type, + error_description: "We noticed that you have a lot of similar errors in column A, B, C and more. Please correct your data export and upload again.", + summary_report_link: "http://localhost:3000/lettings-logs/bulk-upload-results/#{bulk_upload.id}/summary", + }, + ) + + mailer.send_correct_and_upload_again_mail(bulk_upload:) + end + end + end end diff --git a/spec/services/bulk_upload/processor_spec.rb b/spec/services/bulk_upload/processor_spec.rb index b751e7dd7..d56b2ff4e 100644 --- a/spec/services/bulk_upload/processor_spec.rb +++ b/spec/services/bulk_upload/processor_spec.rb @@ -171,6 +171,17 @@ RSpec.describe BulkUpload::Processor do expect(mock_downloader).to have_received(:delete_local_file!) end + it "sends correct and upload again mail" do + mail_double = instance_double("ActionMailer::MessageDelivery", deliver_later: nil) + + allow(BulkUploadMailer).to receive(:send_correct_and_upload_again_mail).and_return(mail_double) + + processor.call + + expect(BulkUploadMailer).to have_received(:send_correct_and_upload_again_mail) + expect(mail_double).to have_received(:deliver_later) + end + it "does not send fix errors email" do allow(BulkUploadMailer).to receive(:send_bulk_upload_with_errors_mail).and_call_original From 0ff387ad84e83782ce9ab928ce95888f16b56cc1 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Mon, 13 Feb 2023 08:47:21 +0000 Subject: [PATCH 02/30] Remove nils before adding up location units (#1290) --- app/services/exports/lettings_log_export_service.rb | 2 +- spec/services/exports/lettings_log_export_service_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/services/exports/lettings_log_export_service.rb b/app/services/exports/lettings_log_export_service.rb index 6cc5c1f52..aed3ea98a 100644 --- a/app/services/exports/lettings_log_export_service.rb +++ b/app/services/exports/lettings_log_export_service.rb @@ -214,7 +214,7 @@ module Exports attribute_hash["reghome"] = scheme.registered_under_care_act_before_type_cast attribute_hash["schtype"] = scheme.scheme_type_before_type_cast attribute_hash["support"] = scheme.support_type_before_type_cast - attribute_hash["units_scheme"] = scheme.locations.map(&:units).sum + attribute_hash["units_scheme"] = scheme.locations.map(&:units).compact.sum end def add_location_fields!(location, attribute_hash) diff --git a/spec/services/exports/lettings_log_export_service_spec.rb b/spec/services/exports/lettings_log_export_service_spec.rb index a8d626b98..36adda071 100644 --- a/spec/services/exports/lettings_log_export_service_spec.rb +++ b/spec/services/exports/lettings_log_export_service_spec.rb @@ -258,6 +258,10 @@ RSpec.describe Exports::LettingsLogExportService do 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), underoccupation_benefitcap: 4, sheltered: 1) } + before do + FactoryBot.create(:location, scheme:, startdate: Time.zone.local(2021, 4, 1), units: nil) + end + it "generates an XML export file with the expected content" do expected_content = replace_entity_ids(lettings_log, export_file.read) expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| From f0a1e183252d2d78200942c611d43295da44992a Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Mon, 13 Feb 2023 15:40:20 +0000 Subject: [PATCH 03/30] CLDC-1885 Bulk upload type must match given needs type (#1288) * bulk upload type must match given needs type * refactor invert logic statement * tweak validation copy --- app/models/bulk_upload.rb | 4 ++ .../bulk_upload/lettings/row_parser.rb | 15 ++++++- config/locales/en.yml | 3 ++ .../bulk_upload/lettings/row_parser_spec.rb | 40 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/app/models/bulk_upload.rb b/app/models/bulk_upload.rb index 8385b9ba4..0adf0d84a 100644 --- a/app/models/bulk_upload.rb +++ b/app/models/bulk_upload.rb @@ -42,6 +42,10 @@ class BulkUpload < ApplicationRecord needstype == 1 end + def supported_housing? + needstype == 2 + end + private def generate_identifier diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/row_parser.rb index cee5fa319..42e762be1 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/row_parser.rb @@ -148,7 +148,8 @@ class BulkUpload::Lettings::RowParser validate :validate_relevant_collection_window validate :validate_la_with_local_housing_referral validate :validate_cannot_be_la_referral_if_general_needs - validate :leaving_reason_for_renewal + validate :validate_leaving_reason_for_renewal + validate :validate_lettings_type_matches_bulk_upload def valid? errors.clear @@ -171,6 +172,16 @@ class BulkUpload::Lettings::RowParser private + def validate_lettings_type_matches_bulk_upload + if [1, 3, 5, 7, 9, 11].include?(field_1) && !bulk_upload.general_needs? + errors.add(:field_1, I18n.t("validations.setup.lettype.supported_housing_mismatch")) + end + + if [2, 4, 6, 8, 10, 12].include?(field_1) && !bulk_upload.supported_housing? + errors.add(:field_1, I18n.t("validations.setup.lettype.general_needs_mismatch")) + end + end + def validate_cannot_be_la_referral_if_general_needs if field_78 == 4 && bulk_upload.general_needs? errors.add :field_78, I18n.t("validations.household.referral.la_general_needs.prp_referred_by_la") @@ -183,7 +194,7 @@ private end end - def leaving_reason_for_renewal + def validate_leaving_reason_for_renewal if field_134 == 1 && ![40, 42].include?(field_52) errors.add(:field_52, I18n.t("validations.household.reason.renewal_reason_needed")) end diff --git a/config/locales/en.yml b/config/locales/en.yml index a97863c30..0a4ae34de 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -170,6 +170,9 @@ en: invalid: "Please select owning organisation or managing organisation that you belong to" created_by: invalid: "Please select owning organisation or managing organisation that you belong to" + lettype: + general_needs_mismatch: Lettings type must be a general needs type because you selected general needs when uploading the file + supported_housing_mismatch: Lettings type must be a supported housing type because you selected supported housing when uploading the file property: mrcdate: diff --git a/spec/services/bulk_upload/lettings/row_parser_spec.rb b/spec/services/bulk_upload/lettings/row_parser_spec.rb index 297b9fd46..29ce6efbb 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/row_parser_spec.rb @@ -202,6 +202,46 @@ RSpec.describe BulkUpload::Lettings::RowParser do expect(parser.errors[:field_1]).to be_blank end end + + context "when bulk upload is for general needs" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: "1") } + + context "when general needs option selected" do + let(:attributes) { { bulk_upload:, field_1: "1" } } + + it "is permitted" do + expect(parser.errors[:field_1]).to be_blank + end + end + + context "when supported housing option selected" do + let(:attributes) { { bulk_upload:, field_1: "2" } } + + it "is not permitted" do + expect(parser.errors[:field_1]).to include("Lettings type must be a general needs type because you selected general needs when uploading the file") + end + end + end + + context "when bulk upload is for supported housing" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: "2") } + + context "when general needs option selected" do + let(:attributes) { { bulk_upload:, field_1: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_1]).to include("Lettings type must be a supported housing type because you selected supported housing when uploading the file") + end + end + + context "when supported housing option selected" do + let(:attributes) { { bulk_upload:, field_1: "2" } } + + it "is permitted" do + expect(parser.errors[:field_1]).to be_blank + end + end + end end describe "#field_4" do From ca8b7970127b8db56661d5515290298c200da349 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Mon, 13 Feb 2023 15:40:39 +0000 Subject: [PATCH 04/30] bulk upload ignores blank rows in csv (#1295) --- .../bulk_upload/lettings/log_creator.rb | 2 ++ .../bulk_upload/lettings/row_parser.rb | 6 ++++ .../bulk_upload/lettings/log_creator_spec.rb | 17 +++++++++++ .../bulk_upload/lettings/row_parser_spec.rb | 30 +++++++++++++++++-- 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/app/services/bulk_upload/lettings/log_creator.rb b/app/services/bulk_upload/lettings/log_creator.rb index 9dd4a2f66..625d53f43 100644 --- a/app/services/bulk_upload/lettings/log_creator.rb +++ b/app/services/bulk_upload/lettings/log_creator.rb @@ -10,6 +10,8 @@ class BulkUpload::Lettings::LogCreator row_parsers.each do |row_parser| row_parser.valid? + next if row_parser.blank_row? + row_parser.log.blank_invalid_non_setup_fields! row_parser.log.bulk_upload = bulk_upload diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/row_parser.rb index 42e762be1..9d5604587 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/row_parser.rb @@ -154,6 +154,8 @@ class BulkUpload::Lettings::RowParser def valid? errors.clear + return true if blank_row? + super log.valid? @@ -166,6 +168,10 @@ class BulkUpload::Lettings::RowParser errors.blank? end + def blank_row? + attribute_set.to_hash.reject { |k, _| %w[bulk_upload].include?(k) }.values.compact.empty? + end + def log @log ||= LettingsLog.new(attributes_for_log) end diff --git a/spec/services/bulk_upload/lettings/log_creator_spec.rb b/spec/services/bulk_upload/lettings/log_creator_spec.rb index fc7d1cdc0..39dfd00fe 100644 --- a/spec/services/bulk_upload/lettings/log_creator_spec.rb +++ b/spec/services/bulk_upload/lettings/log_creator_spec.rb @@ -24,6 +24,23 @@ RSpec.describe BulkUpload::Lettings::LogCreator do end end + context "when a valid csv with several blank rows" do + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:log) { LettingsLog.new } + + before do + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) + file.rewind + end + + it "ignores them and does not create the logs" do + expect { service.call }.not_to change(LettingsLog, :count) + end + end + context "when a valid csv with row with one invalid non setup field" do let(:file) { Tempfile.new } let(:path) { file.path } diff --git a/spec/services/bulk_upload/lettings/row_parser_spec.rb b/spec/services/bulk_upload/lettings/row_parser_spec.rb index 29ce6efbb..8f609838c 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/row_parser_spec.rb @@ -31,6 +31,24 @@ RSpec.describe BulkUpload::Lettings::RowParser do FormHandler.instance.use_fake_forms! end + describe "#blank_row?" do + context "when a new object" do + it "returns true" do + expect(parser).to be_blank_row + end + end + + context "when any field is populated" do + before do + parser.field_1 = "1" + end + + it "returns false" do + expect(parser).not_to be_blank_row + end + end + end + describe "validations" do before do stub_request(:get, /api.postcodes.io/) @@ -40,6 +58,14 @@ RSpec.describe BulkUpload::Lettings::RowParser do end describe "#valid?" do + context "when the row is blank" do + let(:attributes) { { bulk_upload: } } + + it "returns true" do + expect(parser).to be_valid + end + end + context "when calling the method multiple times" do let(:attributes) { { bulk_upload:, field_134: 2 } } @@ -172,7 +198,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do describe "#field_1" do context "when null" do - let(:attributes) { { bulk_upload:, field_1: nil } } + let(:attributes) { { bulk_upload:, field_1: nil, field_4: "1" } } it "returns an error" do expect(parser.errors[:field_1]).to be_present @@ -347,7 +373,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do describe "fields 96, 97, 98 => startdate" do context "when any one of these fields is blank" do - let(:attributes) { { bulk_upload:, field_96: nil, field_97: nil, field_98: nil } } + let(:attributes) { { bulk_upload:, field_1: "1", field_96: nil, field_97: nil, field_98: nil } } it "returns an error" do parser.valid? From 6c0780291b6ff7f6d253f5b0a24936f6ae167dd5 Mon Sep 17 00:00:00 2001 From: natdeanlewissoftwire <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Mon, 13 Feb 2023 17:01:54 +0000 Subject: [PATCH 05/30] CLDC-1810 new staircasing followup (#1286) * feat: behaviour * feat: conditional year behaviour and test * refactor: linting * db: update --- .../form/sales/pages/about_staircase.rb | 9 +++- .../form/sales/questions/staircase_sale.rb | 16 ++++++ ...0122037_add_staircasesale_to_sales_logs.rb | 5 ++ db/schema.rb | 3 +- .../form/sales/pages/about_staircase_spec.rb | 20 +++++++- .../sales/questions/staircase_sale_spec.rb | 49 +++++++++++++++++++ 6 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 app/models/form/sales/questions/staircase_sale.rb create mode 100644 db/migrate/20230210122037_add_staircasesale_to_sales_logs.rb create mode 100644 spec/models/form/sales/questions/staircase_sale_spec.rb diff --git a/app/models/form/sales/pages/about_staircase.rb b/app/models/form/sales/pages/about_staircase.rb index fd740c10e..2d1a17db4 100644 --- a/app/models/form/sales/pages/about_staircase.rb +++ b/app/models/form/sales/pages/about_staircase.rb @@ -12,6 +12,13 @@ class Form::Sales::Pages::AboutStaircase < ::Form::Page @questions ||= [ Form::Sales::Questions::StaircaseBought.new(nil, nil, self), Form::Sales::Questions::StaircaseOwned.new(nil, nil, self), - ] + staircase_sale_question, + ].compact + end + + def staircase_sale_question + if form.start_date.year >= 2023 + Form::Sales::Questions::StaircaseSale.new(nil, nil, self) + end end end diff --git a/app/models/form/sales/questions/staircase_sale.rb b/app/models/form/sales/questions/staircase_sale.rb new file mode 100644 index 000000000..fb56572cf --- /dev/null +++ b/app/models/form/sales/questions/staircase_sale.rb @@ -0,0 +1,16 @@ +class Form::Sales::Questions::StaircaseSale < ::Form::Question + def initialize(id, hsh, page) + super + @id = "staircasesale" + @check_answer_label = "Part of a back-to-back staircasing transaction" + @header = "Is this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?" + @type = "radio" + @answer_options = ANSWER_OPTIONS + end + + ANSWER_OPTIONS = { + "1" => { "value" => "Yes" }, + "2" => { "value" => "No" }, + "3" => { "value" => "Don't know" }, + }.freeze +end diff --git a/db/migrate/20230210122037_add_staircasesale_to_sales_logs.rb b/db/migrate/20230210122037_add_staircasesale_to_sales_logs.rb new file mode 100644 index 000000000..824549cff --- /dev/null +++ b/db/migrate/20230210122037_add_staircasesale_to_sales_logs.rb @@ -0,0 +1,5 @@ +class AddStaircasesaleToSalesLogs < ActiveRecord::Migration[7.0] + def change + add_column :sales_logs, :staircasesale, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 195315675..679b582c1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_02_03_174815) do +ActiveRecord::Schema[7.0].define(version: 2023_02_10_122037) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -524,6 +524,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_03_174815) do t.integer "details_known_5" t.integer "details_known_6" t.integer "saledate_check" + t.integer "staircasesale" t.integer "prevshared" t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id" t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" diff --git a/spec/models/form/sales/pages/about_staircase_spec.rb b/spec/models/form/sales/pages/about_staircase_spec.rb index a2fba103e..3828f52e4 100644 --- a/spec/models/form/sales/pages/about_staircase_spec.rb +++ b/spec/models/form/sales/pages/about_staircase_spec.rb @@ -11,8 +11,24 @@ RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do expect(page.subsection).to eq(subsection) end - it "has correct questions" do - expect(page.questions.map(&:id)).to eq(%w[stairbought stairowned]) + describe "questions" do + let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) } + + context "when 2022" do + let(:start_date) { Time.utc(2022, 2, 8) } + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[stairbought stairowned]) + end + end + + context "when 2023" do + let(:start_date) { Time.utc(2023, 2, 8) } + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[stairbought stairowned staircasesale]) + end + end end it "has the correct id" do diff --git a/spec/models/form/sales/questions/staircase_sale_spec.rb b/spec/models/form/sales/questions/staircase_sale_spec.rb new file mode 100644 index 000000000..39927c967 --- /dev/null +++ b/spec/models/form/sales/questions/staircase_sale_spec.rb @@ -0,0 +1,49 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::StaircaseSale, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("staircasesale") + end + + it "has the correct header" do + expect(question.header).to eq("Is this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Part of a back-to-back staircasing transaction") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "1" => { "value" => "Yes" }, + "2" => { "value" => "No" }, + "3" => { "value" => "Don't know" }, + }) + end + + it "has correct conditional for" do + expect(question.conditional_for).to eq(nil) + end + + it "has the correct hint" do + expect(question.hint_text).to be_nil + end +end From 92029ea2a09d135b84e843ac3ec2fe97c6ec3233 Mon Sep 17 00:00:00 2001 From: SamSeed-Softwire <63662292+SamSeed-Softwire@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:41:40 +0000 Subject: [PATCH 06/30] CLDC-859 Sales validation - bedsits cannot have more than 1 bed (#1188) * feat: add sales validation to check bedsits have <=1 beds * test: sales validation to check bedsits have <=1 beds * fix: typo in property validation method name * feat: add same bedsit validation to number of bedrooms question * test: fix typo propert -> property in property validations tests * test: add test for validate_property_number_of_bedrooms (sales) * feat: update wording for 'number of bedrooms' validation when bedsit * test: condense sales property validations tests * test: strengthen sales property validations error addition test * refactor: simplify sales property validations into one method * fix: update error message content to match Beth's choices * chore: lint * test: add requests test for invalid sales log params when posting * refactor: use if rather than unless... not in property validation * refactor: write method is_bedsit? on SalesLog * test: check bedsit error not added if proptype or beds is nil * lint: use update! not update * feat: update validation messages to improve readability * fix: provide valid date in request test for invalid proptype/beds * feat: make is_bedsit and validate_bedsit_number_of_beds more readable --- app/models/sales_log.rb | 4 +++ .../validations/sales/property_validations.rb | 9 +++++ config/locales/en.yml | 3 ++ .../sales/property_validations_spec.rb | 36 +++++++++++++++++++ spec/requests/sales_logs_controller_spec.rb | 25 +++++++++++++ 5 files changed, 77 insertions(+) diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index 463381559..add706f7c 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -223,6 +223,10 @@ class SalesLog < Log type == 24 end + def is_bedsit? + proptype == 2 + end + def shared_ownership_scheme? ownershipsch == 1 end diff --git a/app/models/validations/sales/property_validations.rb b/app/models/validations/sales/property_validations.rb index 879e37ff1..83f86ec8f 100644 --- a/app/models/validations/sales/property_validations.rb +++ b/app/models/validations/sales/property_validations.rb @@ -7,4 +7,13 @@ module Validations::Sales::PropertyValidations record.errors.add :ppostcode_full, I18n.t("validations.property.postcode.must_match_previous") end end + + def validate_bedsit_number_of_beds(record) + return unless record.proptype.present? && record.beds.present? + + if record.is_bedsit? && record.beds > 1 + record.errors.add :proptype, I18n.t("validations.property.proptype.bedsits_have_max_one_bedroom") + record.errors.add :beds, I18n.t("validations.property.beds.bedsits_have_max_one_bedroom") + end + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 0a4ae34de..98e40dfb3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -202,6 +202,9 @@ en: beds: non_positive: "Number of bedrooms has to be greater than 0" over_max: "Number of bedrooms cannot be more than 12" + bedsits_have_max_one_bedroom: "Number of bedrooms must be 1 if the property is a bedsit" + proptype: + bedsits_have_max_one_bedroom: "Answer cannot be 'Bedsit' if the property has 2 or more bedrooms" postcode: must_match_previous: "Buyer's last accommodation and discounted ownership postcodes must match" diff --git a/spec/models/validations/sales/property_validations_spec.rb b/spec/models/validations/sales/property_validations_spec.rb index fc3fdd1a8..180f16021 100644 --- a/spec/models/validations/sales/property_validations_spec.rb +++ b/spec/models/validations/sales/property_validations_spec.rb @@ -48,4 +48,40 @@ RSpec.describe Validations::Sales::PropertyValidations do end end end + + describe "#validate_property_unit_type" do + context "when number of bedrooms is 1" do + let(:record) { FactoryBot.build(:sales_log, beds: 1, proptype: 2) } + + it "does not add an error if it's a bedsit" do + property_validator.validate_bedsit_number_of_beds(record) + expect(record.errors).not_to be_present + end + end + + context "when number of bedrooms is > 1" do + let(:record) { FactoryBot.build(:sales_log, beds: 2, proptype: 2) } + + it "does add an error if it's a bedsit" do + property_validator.validate_bedsit_number_of_beds(record) + expect(record.errors.added?(:proptype, "Answer cannot be 'Bedsit' if the property has 2 or more bedrooms")).to be true + expect(record.errors.added?(:beds, "Number of bedrooms must be 1 if the property is a bedsit")).to be true + end + + it "does not add an error if proptype is undefined" do + record.update!(proptype: nil) + property_validator.validate_bedsit_number_of_beds(record) + expect(record.errors).not_to be_present + end + end + + context "when number of bedrooms is undefined" do + let(:record) { FactoryBot.build(:sales_log, beds: nil, proptype: 2) } + + it "does not add an error if it's a bedsit" do + property_validator.validate_bedsit_number_of_beds(record) + expect(record.errors).not_to be_present + end + end + end end diff --git a/spec/requests/sales_logs_controller_spec.rb b/spec/requests/sales_logs_controller_spec.rb index 925e3f1f3..e45fee440 100644 --- a/spec/requests/sales_logs_controller_spec.rb +++ b/spec/requests/sales_logs_controller_spec.rb @@ -61,6 +61,31 @@ RSpec.describe SalesLogsController, type: :request do expect(response).to have_http_status(:unauthorized) end end + + context "with a request containing invalid json parameters" do + let(:params) do + { + "saledate": Date.new(2022, 4, 1), + "purchid": "1", + "ownershipsch": 1, + "type": 2, + "jointpur": 1, + "jointmore": 1, + "beds": 2, + "proptype": 2, + } + end + + before do + post "/sales-logs", headers:, params: params.to_json + end + + it "validates sales log parameters" do + json_response = JSON.parse(response.body) + expect(response).to have_http_status(:unprocessable_entity) + expect(json_response["errors"]).to match_array([["beds", ["Number of bedrooms must be 1 if the property is a bedsit"]], ["proptype", ["Answer cannot be 'Bedsit' if the property has 2 or more bedrooms"]]]) + end + end end context "when UI" do From 8879748f823b0017d2646f27603dd03706954a60 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Tue, 14 Feb 2023 15:39:49 +0000 Subject: [PATCH 07/30] only load session fiters when getting or exporting the logs (#1301) --- app/controllers/lettings_logs_controller.rb | 4 ++-- app/controllers/organisations_controller.rb | 4 ++-- app/controllers/sales_logs_controller.rb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/lettings_logs_controller.rb b/app/controllers/lettings_logs_controller.rb index 5181d21ca..f86a55a27 100644 --- a/app/controllers/lettings_logs_controller.rb +++ b/app/controllers/lettings_logs_controller.rb @@ -1,7 +1,7 @@ class LettingsLogsController < LogsController before_action :find_resource, except: %i[create index edit] - before_action :session_filters, if: :current_user - before_action :set_session_filters, if: :current_user + before_action :session_filters, if: :current_user, only: %i[index email_csv download_csv] + before_action :set_session_filters, if: :current_user, only: %i[index email_csv download_csv] before_action :extract_bulk_upload_from_session_filters, only: [:index] before_action :redirect_if_bulk_upload_resolved, only: [:index] diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 0c8c4a172..211283ffe 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -6,8 +6,8 @@ class OrganisationsController < ApplicationController before_action :authenticate_user! before_action :find_resource, except: %i[index new create] before_action :authenticate_scope!, except: [:index] - before_action -> { session_filters(specific_org: true) }, if: -> { current_user.support? || current_user.organisation.has_managing_agents? } - before_action :set_session_filters, if: -> { current_user.support? || current_user.organisation.has_managing_agents? } + before_action -> { session_filters(specific_org: true) }, if: -> { current_user.support? || current_user.organisation.has_managing_agents? }, only: %i[lettings_logs sales_logs email_csv download_csv] + before_action :set_session_filters, if: -> { current_user.support? || current_user.organisation.has_managing_agents? }, only: %i[lettings_logs sales_logs email_csv download_csv] def index redirect_to organisation_path(current_user.organisation) unless current_user.support? diff --git a/app/controllers/sales_logs_controller.rb b/app/controllers/sales_logs_controller.rb index 8a6c9937f..ecfeabcad 100644 --- a/app/controllers/sales_logs_controller.rb +++ b/app/controllers/sales_logs_controller.rb @@ -1,6 +1,6 @@ class SalesLogsController < LogsController - before_action :session_filters, if: :current_user - before_action :set_session_filters, if: :current_user + before_action :session_filters, if: :current_user, only: %i[index email_csv download_csv] + before_action :set_session_filters, if: :current_user, only: %i[index email_csv download_csv] def create super { SalesLog.new(log_params) } From 00cb2347978cb55850e2b1ab01a96e11719187b4 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:01:15 +0000 Subject: [PATCH 08/30] CLDC-1811 Add buyer 2 ethnicity question for 23/24 form (#1287) * Add buyer 2 ethnic questions and pages * Add ethnic fields for buyer 2 * Add the new questions to the correct year --- .../pages/buyer2_ethnic_background_arab.rb | 15 ++ .../pages/buyer2_ethnic_background_asian.rb | 15 ++ .../pages/buyer2_ethnic_background_black.rb | 15 ++ .../pages/buyer2_ethnic_background_mixed.rb | 15 ++ .../pages/buyer2_ethnic_background_white.rb | 15 ++ .../form/sales/pages/buyer2_ethnic_group.rb | 22 ++ .../buyer2_ethnic_background_arab.rb | 16 ++ .../buyer2_ethnic_background_asian.rb | 19 ++ .../buyer2_ethnic_background_black.rb | 17 ++ .../buyer2_ethnic_background_mixed.rb | 18 ++ .../buyer2_ethnic_background_white.rb | 18 ++ .../sales/questions/buyer2_ethnic_group.rb | 27 +++ .../subsections/household_characteristics.rb | 14 +- ...0210143120_add_ethnic_fields_for_buyer2.rb | 8 + db/schema.rb | 4 +- .../buyer2_ethnic_background_arab_spec.rb | 33 +++ .../buyer2_ethnic_background_asian_spec.rb | 33 +++ .../buyer2_ethnic_background_black_spec.rb | 33 +++ .../buyer2_ethnic_background_mixed_spec.rb | 33 +++ .../buyer2_ethnic_background_white_spec.rb | 33 +++ .../sales/pages/buyer2_ethnic_group_spec.rb | 42 ++++ .../buyer2_ethnic_background_arab_spec.rb | 48 ++++ .../buyer2_ethnic_background_asian_spec.rb | 51 ++++ .../buyer2_ethnic_background_black_spec.rb | 49 ++++ .../buyer2_ethnic_background_mixed_spec.rb | 50 ++++ .../buyer2_ethnic_background_white_spec.rb | 50 ++++ .../questions/buyer2_ethnic_group_spec.rb | 62 +++++ .../household_characteristics_spec.rb | 218 ++++++++++++------ 28 files changed, 906 insertions(+), 67 deletions(-) create mode 100644 app/models/form/sales/pages/buyer2_ethnic_background_arab.rb create mode 100644 app/models/form/sales/pages/buyer2_ethnic_background_asian.rb create mode 100644 app/models/form/sales/pages/buyer2_ethnic_background_black.rb create mode 100644 app/models/form/sales/pages/buyer2_ethnic_background_mixed.rb create mode 100644 app/models/form/sales/pages/buyer2_ethnic_background_white.rb create mode 100644 app/models/form/sales/pages/buyer2_ethnic_group.rb create mode 100644 app/models/form/sales/questions/buyer2_ethnic_background_arab.rb create mode 100644 app/models/form/sales/questions/buyer2_ethnic_background_asian.rb create mode 100644 app/models/form/sales/questions/buyer2_ethnic_background_black.rb create mode 100644 app/models/form/sales/questions/buyer2_ethnic_background_mixed.rb create mode 100644 app/models/form/sales/questions/buyer2_ethnic_background_white.rb create mode 100644 app/models/form/sales/questions/buyer2_ethnic_group.rb create mode 100644 db/migrate/20230210143120_add_ethnic_fields_for_buyer2.rb create mode 100644 spec/models/form/sales/pages/buyer2_ethnic_background_arab_spec.rb create mode 100644 spec/models/form/sales/pages/buyer2_ethnic_background_asian_spec.rb create mode 100644 spec/models/form/sales/pages/buyer2_ethnic_background_black_spec.rb create mode 100644 spec/models/form/sales/pages/buyer2_ethnic_background_mixed_spec.rb create mode 100644 spec/models/form/sales/pages/buyer2_ethnic_background_white_spec.rb create mode 100644 spec/models/form/sales/pages/buyer2_ethnic_group_spec.rb create mode 100644 spec/models/form/sales/questions/buyer2_ethnic_background_arab_spec.rb create mode 100644 spec/models/form/sales/questions/buyer2_ethnic_background_asian_spec.rb create mode 100644 spec/models/form/sales/questions/buyer2_ethnic_background_black_spec.rb create mode 100644 spec/models/form/sales/questions/buyer2_ethnic_background_mixed_spec.rb create mode 100644 spec/models/form/sales/questions/buyer2_ethnic_background_white_spec.rb create mode 100644 spec/models/form/sales/questions/buyer2_ethnic_group_spec.rb diff --git a/app/models/form/sales/pages/buyer2_ethnic_background_arab.rb b/app/models/form/sales/pages/buyer2_ethnic_background_arab.rb new file mode 100644 index 000000000..593e43ebc --- /dev/null +++ b/app/models/form/sales/pages/buyer2_ethnic_background_arab.rb @@ -0,0 +1,15 @@ +class Form::Sales::Pages::Buyer2EthnicBackgroundArab < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_ethnic_background_arab" + @depends_on = [{ + "ethnic_group2" => 4, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::Buyer2EthnicBackgroundArab.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/buyer2_ethnic_background_asian.rb b/app/models/form/sales/pages/buyer2_ethnic_background_asian.rb new file mode 100644 index 000000000..98a36476e --- /dev/null +++ b/app/models/form/sales/pages/buyer2_ethnic_background_asian.rb @@ -0,0 +1,15 @@ +class Form::Sales::Pages::Buyer2EthnicBackgroundAsian < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_ethnic_background_asian" + @depends_on = [{ + "ethnic_group2" => 2, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::Buyer2EthnicBackgroundAsian.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/buyer2_ethnic_background_black.rb b/app/models/form/sales/pages/buyer2_ethnic_background_black.rb new file mode 100644 index 000000000..a772f46ac --- /dev/null +++ b/app/models/form/sales/pages/buyer2_ethnic_background_black.rb @@ -0,0 +1,15 @@ +class Form::Sales::Pages::Buyer2EthnicBackgroundBlack < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_ethnic_background_black" + @depends_on = [{ + "ethnic_group2" => 3, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::Buyer2EthnicBackgroundBlack.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/buyer2_ethnic_background_mixed.rb b/app/models/form/sales/pages/buyer2_ethnic_background_mixed.rb new file mode 100644 index 000000000..f02165dfc --- /dev/null +++ b/app/models/form/sales/pages/buyer2_ethnic_background_mixed.rb @@ -0,0 +1,15 @@ +class Form::Sales::Pages::Buyer2EthnicBackgroundMixed < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_ethnic_background_mixed" + @depends_on = [{ + "ethnic_group2" => 1, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::Buyer2EthnicBackgroundMixed.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/buyer2_ethnic_background_white.rb b/app/models/form/sales/pages/buyer2_ethnic_background_white.rb new file mode 100644 index 000000000..2013c8682 --- /dev/null +++ b/app/models/form/sales/pages/buyer2_ethnic_background_white.rb @@ -0,0 +1,15 @@ +class Form::Sales::Pages::Buyer2EthnicBackgroundWhite < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_ethnic_background_white" + @depends_on = [{ + "ethnic_group2" => 0, + }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::Buyer2EthnicBackgroundWhite.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/buyer2_ethnic_group.rb b/app/models/form/sales/pages/buyer2_ethnic_group.rb new file mode 100644 index 000000000..1b815996e --- /dev/null +++ b/app/models/form/sales/pages/buyer2_ethnic_group.rb @@ -0,0 +1,22 @@ +class Form::Sales::Pages::Buyer2EthnicGroup < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_ethnic_group" + @depends_on = [ + { + "jointpur" => 1, + "privacynotice" => 1, + }, + { + "jointpur" => 1, + "noint" => 1, + }, + ] + end + + def questions + @questions ||= [ + Form::Sales::Questions::Buyer2EthnicGroup.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/questions/buyer2_ethnic_background_arab.rb b/app/models/form/sales/questions/buyer2_ethnic_background_arab.rb new file mode 100644 index 000000000..1766780b7 --- /dev/null +++ b/app/models/form/sales/questions/buyer2_ethnic_background_arab.rb @@ -0,0 +1,16 @@ +class Form::Sales::Questions::Buyer2EthnicBackgroundArab < ::Form::Question + def initialize(id, hsh, page) + super + @id = "ethnicbuy2" + @check_answer_label = "Buyer 2’s ethnic background" + @header = "Which of the following best describes the buyer 2’s Arab background?" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @check_answers_card_number = 2 + end + + ANSWER_OPTIONS = { + "19" => { "value" => "Arab" }, + "16" => { "value" => "Other ethnic group" }, + }.freeze +end diff --git a/app/models/form/sales/questions/buyer2_ethnic_background_asian.rb b/app/models/form/sales/questions/buyer2_ethnic_background_asian.rb new file mode 100644 index 000000000..2578d220a --- /dev/null +++ b/app/models/form/sales/questions/buyer2_ethnic_background_asian.rb @@ -0,0 +1,19 @@ +class Form::Sales::Questions::Buyer2EthnicBackgroundAsian < ::Form::Question + def initialize(id, hsh, page) + super + @id = "ethnicbuy2" + @check_answer_label = "Buyer 2’s ethnic background" + @header = "Which of the following best describes the buyer 2’s Asian or Asian British background?" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @check_answers_card_number = 2 + end + + ANSWER_OPTIONS = { + "10" => { "value" => "Bangladeshi" }, + "15" => { "value" => "Chinese" }, + "8" => { "value" => "Indian" }, + "9" => { "value" => "Pakistani" }, + "11" => { "value" => "Any other Asian or Asian British background" }, + }.freeze +end diff --git a/app/models/form/sales/questions/buyer2_ethnic_background_black.rb b/app/models/form/sales/questions/buyer2_ethnic_background_black.rb new file mode 100644 index 000000000..11d47790c --- /dev/null +++ b/app/models/form/sales/questions/buyer2_ethnic_background_black.rb @@ -0,0 +1,17 @@ +class Form::Sales::Questions::Buyer2EthnicBackgroundBlack < ::Form::Question + def initialize(id, hsh, page) + super + @id = "ethnicbuy2" + @check_answer_label = "Buyer 2’s ethnic background" + @header = "Which of the following best describes the buyer 2’s Black, African, Caribbean or Black British background?" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @check_answers_card_number = 2 + end + + ANSWER_OPTIONS = { + "13" => { "value" => "African" }, + "12" => { "value" => "Caribbean" }, + "14" => { "value" => "Any other Black, African or Caribbean background" }, + }.freeze +end diff --git a/app/models/form/sales/questions/buyer2_ethnic_background_mixed.rb b/app/models/form/sales/questions/buyer2_ethnic_background_mixed.rb new file mode 100644 index 000000000..78600af34 --- /dev/null +++ b/app/models/form/sales/questions/buyer2_ethnic_background_mixed.rb @@ -0,0 +1,18 @@ +class Form::Sales::Questions::Buyer2EthnicBackgroundMixed < ::Form::Question + def initialize(id, hsh, page) + super + @id = "ethnicbuy2" + @check_answer_label = "Buyer 2’s ethnic background" + @header = "Which of the following best describes the buyer 2’s Mixed or Multiple ethnic groups background?" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @check_answers_card_number = 2 + end + + ANSWER_OPTIONS = { + "4" => { "value" => "White and Black Caribbean" }, + "5" => { "value" => "White and Black African" }, + "6" => { "value" => "White and Asian" }, + "7" => { "value" => "Any other Mixed or Multiple ethnic background" }, + }.freeze +end diff --git a/app/models/form/sales/questions/buyer2_ethnic_background_white.rb b/app/models/form/sales/questions/buyer2_ethnic_background_white.rb new file mode 100644 index 000000000..5b5fc8e2c --- /dev/null +++ b/app/models/form/sales/questions/buyer2_ethnic_background_white.rb @@ -0,0 +1,18 @@ +class Form::Sales::Questions::Buyer2EthnicBackgroundWhite < ::Form::Question + def initialize(id, hsh, page) + super + @id = "ethnicbuy2" + @check_answer_label = "Buyer 2’s ethnic background" + @header = "Which of the following best describes the buyer 2’s White background?" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @check_answers_card_number = 2 + end + + ANSWER_OPTIONS = { + "1" => { "value" => "English, Welsh, Northern Irish, Scottish or British" }, + "2" => { "value" => "Irish" }, + "18" => { "value" => "Gypsy or Irish Traveller" }, + "3" => { "value" => "Any other White background" }, + }.freeze +end diff --git a/app/models/form/sales/questions/buyer2_ethnic_group.rb b/app/models/form/sales/questions/buyer2_ethnic_group.rb new file mode 100644 index 000000000..5366910fe --- /dev/null +++ b/app/models/form/sales/questions/buyer2_ethnic_group.rb @@ -0,0 +1,27 @@ +class Form::Sales::Questions::Buyer2EthnicGroup < ::Form::Question + def initialize(id, hsh, page) + super + @id = "ethnic_group2" + @check_answer_label = "Buyer 2’s ethnic group" + @header = "What is buyer 2’s ethnic group?" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @inferred_check_answers_value = [{ + "condition" => { + "ethnic_group2" => 17, + }, + "value" => "Prefers not to say", + }] + @check_answers_card_number = 2 + end + + ANSWER_OPTIONS = { + "0" => { "value" => "White" }, + "1" => { "value" => "Mixed or Multiple ethnic groups" }, + "2" => { "value" => "Asian or Asian British" }, + "3" => { "value" => "Black, African, Caribbean or Black British" }, + "4" => { "value" => "Arab or other ethnic group" }, + "divider" => { "value" => true }, + "17" => { "value" => "Buyer 1 prefers not to say" }, + }.freeze +end diff --git a/app/models/form/sales/subsections/household_characteristics.rb b/app/models/form/sales/subsections/household_characteristics.rb index 7e342499e..55ec0b137 100644 --- a/app/models/form/sales/subsections/household_characteristics.rb +++ b/app/models/form/sales/subsections/household_characteristics.rb @@ -32,6 +32,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Sales::Pages::RetirementValueCheck.new("age_2_buyer_retirement_value_check", nil, self, person_index: 2), Form::Sales::Pages::GenderIdentity2.new(nil, nil, self), Form::Sales::Pages::RetirementValueCheck.new("gender_2_buyer_retirement_value_check", nil, self, person_index: 2), + ethnic_pages_for_buyer_2, Form::Sales::Pages::Buyer2WorkingSituation.new(nil, nil, self), Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_buyer_retirement_value_check", nil, self, person_index: 2), Form::Sales::Pages::Buyer2LiveInProperty.new(nil, nil, self), @@ -68,6 +69,17 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Sales::Pages::RetirementValueCheck.new("gender_5_retirement_value_check", nil, self, person_index: 5), Form::Sales::Pages::PersonWorkingSituation.new("person_5_working_situation", nil, self, person_index: 5), Form::Sales::Pages::RetirementValueCheck.new("working_situation_5_retirement_value_check", nil, self, person_index: 5), - ] + ].flatten.compact + end + + def ethnic_pages_for_buyer_2 + if form.start_date.year >= 2023 + [Form::Sales::Pages::Buyer2EthnicGroup.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundBlack.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundAsian.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundArab.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundMixed.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundWhite.new(nil, nil, self)] + end end end diff --git a/db/migrate/20230210143120_add_ethnic_fields_for_buyer2.rb b/db/migrate/20230210143120_add_ethnic_fields_for_buyer2.rb new file mode 100644 index 000000000..c545a0539 --- /dev/null +++ b/db/migrate/20230210143120_add_ethnic_fields_for_buyer2.rb @@ -0,0 +1,8 @@ +class AddEthnicFieldsForBuyer2 < ActiveRecord::Migration[7.0] + def change + change_table :sales_logs, bulk: true do |t| + t.column :ethnic_group2, :integer + t.column :ethnicbuy2, :integer + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 679b582c1..ffea32c01 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_02_10_122037) do +ActiveRecord::Schema[7.0].define(version: 2023_02_10_143120) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -526,6 +526,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_10_122037) do t.integer "saledate_check" t.integer "staircasesale" t.integer "prevshared" + t.integer "ethnic_group2" + t.integer "ethnicbuy2" t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id" t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" t.index ["owning_organisation_id"], name: "index_sales_logs_on_owning_organisation_id" diff --git a/spec/models/form/sales/pages/buyer2_ethnic_background_arab_spec.rb b/spec/models/form/sales/pages/buyer2_ethnic_background_arab_spec.rb new file mode 100644 index 000000000..1fbbe3bc8 --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_ethnic_background_arab_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2EthnicBackgroundArab, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[ethnicbuy2]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_2_ethnic_background_arab") + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([{ "ethnic_group2" => 4 }]) + end +end diff --git a/spec/models/form/sales/pages/buyer2_ethnic_background_asian_spec.rb b/spec/models/form/sales/pages/buyer2_ethnic_background_asian_spec.rb new file mode 100644 index 000000000..60878f99a --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_ethnic_background_asian_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2EthnicBackgroundAsian, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[ethnicbuy2]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_2_ethnic_background_asian") + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([{ "ethnic_group2" => 2 }]) + end +end diff --git a/spec/models/form/sales/pages/buyer2_ethnic_background_black_spec.rb b/spec/models/form/sales/pages/buyer2_ethnic_background_black_spec.rb new file mode 100644 index 000000000..2643f460b --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_ethnic_background_black_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2EthnicBackgroundBlack, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[ethnicbuy2]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_2_ethnic_background_black") + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([{ "ethnic_group2" => 3 }]) + end +end diff --git a/spec/models/form/sales/pages/buyer2_ethnic_background_mixed_spec.rb b/spec/models/form/sales/pages/buyer2_ethnic_background_mixed_spec.rb new file mode 100644 index 000000000..4505ce08b --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_ethnic_background_mixed_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2EthnicBackgroundMixed, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[ethnicbuy2]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_2_ethnic_background_mixed") + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([{ "ethnic_group2" => 1 }]) + end +end diff --git a/spec/models/form/sales/pages/buyer2_ethnic_background_white_spec.rb b/spec/models/form/sales/pages/buyer2_ethnic_background_white_spec.rb new file mode 100644 index 000000000..7647f35af --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_ethnic_background_white_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2EthnicBackgroundWhite, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[ethnicbuy2]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_2_ethnic_background_white") + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([{ "ethnic_group2" => 0 }]) + end +end diff --git a/spec/models/form/sales/pages/buyer2_ethnic_group_spec.rb b/spec/models/form/sales/pages/buyer2_ethnic_group_spec.rb new file mode 100644 index 000000000..2767a2168 --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_ethnic_group_spec.rb @@ -0,0 +1,42 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2EthnicGroup, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[ethnic_group2]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_2_ethnic_group") + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([ + { + "jointpur" => 1, + "privacynotice" => 1, + }, + { + "jointpur" => 1, + "noint" => 1, + }, + ]) + end +end diff --git a/spec/models/form/sales/questions/buyer2_ethnic_background_arab_spec.rb b/spec/models/form/sales/questions/buyer2_ethnic_background_arab_spec.rb new file mode 100644 index 000000000..9fd408db9 --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_ethnic_background_arab_spec.rb @@ -0,0 +1,48 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2EthnicBackgroundArab, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("ethnicbuy2") + end + + it "has the correct header" do + expect(question.header).to eq("Which of the following best describes the buyer 2’s Arab background?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 2’s ethnic background") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint_text" do + expect(question.hint_text).to be nil + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "16" => { "value" => "Other ethnic group" }, + "19" => { "value" => "Arab" }, + }) + end + + it "has the correct check_answers_card_number" do + expect(question.check_answers_card_number).to eq(2) + end +end diff --git a/spec/models/form/sales/questions/buyer2_ethnic_background_asian_spec.rb b/spec/models/form/sales/questions/buyer2_ethnic_background_asian_spec.rb new file mode 100644 index 000000000..67ce60e43 --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_ethnic_background_asian_spec.rb @@ -0,0 +1,51 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2EthnicBackgroundAsian, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("ethnicbuy2") + end + + it "has the correct header" do + expect(question.header).to eq("Which of the following best describes the buyer 2’s Asian or Asian British background?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 2’s ethnic background") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint_text" do + expect(question.hint_text).to be nil + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "10" => { "value" => "Bangladeshi" }, + "11" => { "value" => "Any other Asian or Asian British background" }, + "15" => { "value" => "Chinese" }, + "8" => { "value" => "Indian" }, + "9" => { "value" => "Pakistani" }, + }) + end + + it "has the correct check_answers_card_number" do + expect(question.check_answers_card_number).to eq(2) + end +end diff --git a/spec/models/form/sales/questions/buyer2_ethnic_background_black_spec.rb b/spec/models/form/sales/questions/buyer2_ethnic_background_black_spec.rb new file mode 100644 index 000000000..8e772f870 --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_ethnic_background_black_spec.rb @@ -0,0 +1,49 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2EthnicBackgroundBlack, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("ethnicbuy2") + end + + it "has the correct header" do + expect(question.header).to eq("Which of the following best describes the buyer 2’s Black, African, Caribbean or Black British background?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 2’s ethnic background") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint_text" do + expect(question.hint_text).to be nil + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "12" => { "value" => "Caribbean" }, + "13" => { "value" => "African" }, + "14" => { "value" => "Any other Black, African or Caribbean background" }, + }) + end + + it "has the correct check_answers_card_number" do + expect(question.check_answers_card_number).to eq(2) + end +end diff --git a/spec/models/form/sales/questions/buyer2_ethnic_background_mixed_spec.rb b/spec/models/form/sales/questions/buyer2_ethnic_background_mixed_spec.rb new file mode 100644 index 000000000..6c1b12865 --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_ethnic_background_mixed_spec.rb @@ -0,0 +1,50 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2EthnicBackgroundMixed, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("ethnicbuy2") + end + + it "has the correct header" do + expect(question.header).to eq("Which of the following best describes the buyer 2’s Mixed or Multiple ethnic groups background?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 2’s ethnic background") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint_text" do + expect(question.hint_text).to be nil + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "4" => { "value" => "White and Black Caribbean" }, + "5" => { "value" => "White and Black African" }, + "6" => { "value" => "White and Asian" }, + "7" => { "value" => "Any other Mixed or Multiple ethnic background" }, + }) + end + + it "has the correct check_answers_card_number" do + expect(question.check_answers_card_number).to eq(2) + end +end diff --git a/spec/models/form/sales/questions/buyer2_ethnic_background_white_spec.rb b/spec/models/form/sales/questions/buyer2_ethnic_background_white_spec.rb new file mode 100644 index 000000000..d3f2da508 --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_ethnic_background_white_spec.rb @@ -0,0 +1,50 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2EthnicBackgroundWhite, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("ethnicbuy2") + end + + it "has the correct header" do + expect(question.header).to eq("Which of the following best describes the buyer 2’s White background?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 2’s ethnic background") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint_text" do + expect(question.hint_text).to be nil + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "1" => { "value" => "English, Welsh, Northern Irish, Scottish or British" }, + "18" => { "value" => "Gypsy or Irish Traveller" }, + "2" => { "value" => "Irish" }, + "3" => { "value" => "Any other White background" }, + }) + end + + it "has the correct check_answers_card_number" do + expect(question.check_answers_card_number).to eq(2) + end +end diff --git a/spec/models/form/sales/questions/buyer2_ethnic_group_spec.rb b/spec/models/form/sales/questions/buyer2_ethnic_group_spec.rb new file mode 100644 index 000000000..2e9e0c28c --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_ethnic_group_spec.rb @@ -0,0 +1,62 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2EthnicGroup, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("ethnic_group2") + end + + it "has the correct header" do + expect(question.header).to eq("What is buyer 2’s ethnic group?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 2’s ethnic group") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint_text" do + expect(question.hint_text).to be nil + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "0" => { "value" => "White" }, + "1" => { "value" => "Mixed or Multiple ethnic groups" }, + "17" => { "value" => "Buyer 1 prefers not to say" }, + "2" => { "value" => "Asian or Asian British" }, + "3" => { "value" => "Black, African, Caribbean or Black British" }, + "4" => { "value" => "Arab or other ethnic group" }, + "divider" => { "value" => true }, + }) + end + + it "has the correct check_answers_card_number" do + expect(question.check_answers_card_number).to eq(2) + end + + it "has the correct inferred_check_answers_value" do + expect(question.inferred_check_answers_value).to eq([{ + "condition" => { + "ethnic_group2" => 17, + }, + "value" => "Prefers not to say", + }]) + end +end diff --git a/spec/models/form/sales/subsections/household_characteristics_spec.rb b/spec/models/form/sales/subsections/household_characteristics_spec.rb index 681b4b50b..76109b7f4 100644 --- a/spec/models/form/sales/subsections/household_characteristics_spec.rb +++ b/spec/models/form/sales/subsections/household_characteristics_spec.rb @@ -6,76 +6,164 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model let(:subsection_id) { nil } let(:subsection_definition) { nil } let(:section) { instance_double(Form::Sales::Sections::Household) } + let(:form) { instance_double(Form) } it "has correct section" do expect(household_characteristics.section).to eq(section) end - it "has correct pages" do - expect(household_characteristics.pages.map(&:id)).to eq( - %w[ - buyer_interview - privacy_notice - buyer_1_age - age_1_retirement_value_check - age_1_old_persons_shared_ownership_value_check - buyer_1_gender_identity - gender_1_retirement_value_check - buyer_1_ethnic_group - buyer_1_ethnic_background_black - buyer_1_ethnic_background_asian - buyer_1_ethnic_background_arab - buyer_1_ethnic_background_mixed - buyer_1_ethnic_background_white - buyer_1_nationality - buyer_1_working_situation - working_situation_1_retirement_value_check - working_situation_buyer_1_income_value_check - buyer_1_live_in_property - buyer_2_relationship_to_buyer_1 - buyer_2_age - age_2_old_persons_shared_ownership_value_check - age_2_buyer_retirement_value_check - buyer_2_gender_identity - gender_2_buyer_retirement_value_check - buyer_2_working_situation - working_situation_2_buyer_retirement_value_check - buyer_2_live_in_property - number_of_others_in_property - person_2_known - person_2_relationship_to_buyer_1 - person_2_age - age_2_retirement_value_check - person_2_gender_identity - gender_2_retirement_value_check - person_2_working_situation - working_situation_2_retirement_value_check - person_3_known - person_3_relationship_to_buyer_1 - person_3_age - age_3_retirement_value_check - person_3_gender_identity - gender_3_retirement_value_check - person_3_working_situation - working_situation_3_retirement_value_check - person_4_known - person_4_relationship_to_buyer_1 - person_4_age - age_4_retirement_value_check - person_4_gender_identity - gender_4_retirement_value_check - person_4_working_situation - working_situation_4_retirement_value_check - person_5_known - person_5_relationship_to_buyer_1 - person_5_age - age_5_retirement_value_check - person_5_gender_identity - gender_5_retirement_value_check - person_5_working_situation - working_situation_5_retirement_value_check - ], - ) + context "with 2022/23 form" do + before do + allow(form).to receive(:start_date).and_return(Time.zone.local(2022, 4, 1)) + allow(section).to receive(:form).and_return(form) + end + + it "has correct pages" do + expect(household_characteristics.pages.map(&:id)).to eq( + %w[ + buyer_interview + privacy_notice + buyer_1_age + age_1_retirement_value_check + age_1_old_persons_shared_ownership_value_check + buyer_1_gender_identity + gender_1_retirement_value_check + buyer_1_ethnic_group + buyer_1_ethnic_background_black + buyer_1_ethnic_background_asian + buyer_1_ethnic_background_arab + buyer_1_ethnic_background_mixed + buyer_1_ethnic_background_white + buyer_1_nationality + buyer_1_working_situation + working_situation_1_retirement_value_check + working_situation_buyer_1_income_value_check + buyer_1_live_in_property + buyer_2_relationship_to_buyer_1 + buyer_2_age + age_2_old_persons_shared_ownership_value_check + age_2_buyer_retirement_value_check + buyer_2_gender_identity + gender_2_buyer_retirement_value_check + buyer_2_working_situation + working_situation_2_buyer_retirement_value_check + buyer_2_live_in_property + number_of_others_in_property + person_2_known + person_2_relationship_to_buyer_1 + person_2_age + age_2_retirement_value_check + person_2_gender_identity + gender_2_retirement_value_check + person_2_working_situation + working_situation_2_retirement_value_check + person_3_known + person_3_relationship_to_buyer_1 + person_3_age + age_3_retirement_value_check + person_3_gender_identity + gender_3_retirement_value_check + person_3_working_situation + working_situation_3_retirement_value_check + person_4_known + person_4_relationship_to_buyer_1 + person_4_age + age_4_retirement_value_check + person_4_gender_identity + gender_4_retirement_value_check + person_4_working_situation + working_situation_4_retirement_value_check + person_5_known + person_5_relationship_to_buyer_1 + person_5_age + age_5_retirement_value_check + person_5_gender_identity + gender_5_retirement_value_check + person_5_working_situation + working_situation_5_retirement_value_check + ], + ) + end + end + + context "with 2023/24 form" do + before do + allow(form).to receive(:start_date).and_return(Time.zone.local(2023, 4, 1)) + allow(section).to receive(:form).and_return(form) + end + + it "has correct pages" do + expect(household_characteristics.pages.map(&:id)).to eq( + %w[ + buyer_interview + privacy_notice + buyer_1_age + age_1_retirement_value_check + age_1_old_persons_shared_ownership_value_check + buyer_1_gender_identity + gender_1_retirement_value_check + buyer_1_ethnic_group + buyer_1_ethnic_background_black + buyer_1_ethnic_background_asian + buyer_1_ethnic_background_arab + buyer_1_ethnic_background_mixed + buyer_1_ethnic_background_white + buyer_1_nationality + buyer_1_working_situation + working_situation_1_retirement_value_check + working_situation_buyer_1_income_value_check + buyer_1_live_in_property + buyer_2_relationship_to_buyer_1 + buyer_2_age + age_2_old_persons_shared_ownership_value_check + age_2_buyer_retirement_value_check + buyer_2_gender_identity + gender_2_buyer_retirement_value_check + buyer_2_ethnic_group + buyer_2_ethnic_background_black + buyer_2_ethnic_background_asian + buyer_2_ethnic_background_arab + buyer_2_ethnic_background_mixed + buyer_2_ethnic_background_white + buyer_2_working_situation + working_situation_2_buyer_retirement_value_check + buyer_2_live_in_property + number_of_others_in_property + person_2_known + person_2_relationship_to_buyer_1 + person_2_age + age_2_retirement_value_check + person_2_gender_identity + gender_2_retirement_value_check + person_2_working_situation + working_situation_2_retirement_value_check + person_3_known + person_3_relationship_to_buyer_1 + person_3_age + age_3_retirement_value_check + person_3_gender_identity + gender_3_retirement_value_check + person_3_working_situation + working_situation_3_retirement_value_check + person_4_known + person_4_relationship_to_buyer_1 + person_4_age + age_4_retirement_value_check + person_4_gender_identity + gender_4_retirement_value_check + person_4_working_situation + working_situation_4_retirement_value_check + person_5_known + person_5_relationship_to_buyer_1 + person_5_age + age_5_retirement_value_check + person_5_gender_identity + gender_5_retirement_value_check + person_5_working_situation + working_situation_5_retirement_value_check + ], + ) + end end it "has the correct id" do From 02f04fde60948154f812919e51bce8bef0d3728a Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Wed, 15 Feb 2023 13:45:36 +0000 Subject: [PATCH 09/30] CLDC-1884 Bulk upload disability access fields and validations (#1299) * bulk upload considers housing needs fields * bulk upload only permits one housing needs type * add bulk upload validation - no disabled needs cannot be selected in conjunction with a disabled need * add bulk upload validation - dont know disabled needs cannot be selected in conjunction with a disabled need * add bulk upload validation - no and don't know disabled access needs cannot be selected together --- .../bulk_upload/lettings/row_parser.rb | 49 +++++++++- config/locales/en.yml | 6 ++ .../bulk_upload/lettings/row_parser_spec.rb | 97 +++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/row_parser.rb index 9d5604587..bf1f62eba 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/row_parser.rb @@ -150,6 +150,10 @@ class BulkUpload::Lettings::RowParser validate :validate_cannot_be_la_referral_if_general_needs validate :validate_leaving_reason_for_renewal validate :validate_lettings_type_matches_bulk_upload + validate :validate_only_one_housing_needs_type + validate :validate_no_disabled_needs_conjunction + validate :validate_dont_know_disabled_needs_conjunction + validate :validate_no_and_dont_know_disabled_needs_conjunction def valid? errors.clear @@ -178,6 +182,33 @@ class BulkUpload::Lettings::RowParser private + def validate_no_and_dont_know_disabled_needs_conjunction + if field_59 == 1 && field_60 == 1 + errors.add(:field_59, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction")) + errors.add(:field_60, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction")) + end + end + + def validate_dont_know_disabled_needs_conjunction + if field_60 == 1 && [field_55, field_56, field_57, field_58].compact.count.positive? + errors.add(:field_60, I18n.t("validations.household.housingneeds.dont_know_disabled_needs_conjunction")) + end + end + + def validate_no_disabled_needs_conjunction + if field_59 == 1 && [field_55, field_56, field_57, field_58].compact.count.positive? + errors.add(:field_59, I18n.t("validations.household.housingneeds.no_disabled_needs_conjunction")) + end + end + + def validate_only_one_housing_needs_type + if [field_55, field_56, field_57].compact.count.positive? + errors.add(:field_55, I18n.t("validations.household.housingneeds_type.only_one_option_permitted")) + errors.add(:field_56, I18n.t("validations.household.housingneeds_type.only_one_option_permitted")) + errors.add(:field_57, I18n.t("validations.household.housingneeds_type.only_one_option_permitted")) + end + end + def validate_lettings_type_matches_bulk_upload if [1, 3, 5, 7, 9, 11].include?(field_1) && !bulk_upload.general_needs? errors.add(:field_1, I18n.t("validations.setup.lettype.supported_housing_mismatch")) @@ -552,6 +583,8 @@ private attributes["preg_occ"] = field_47 attributes["housingneeds"] = housingneeds + attributes["housingneeds_type"] = housingneeds_type + attributes["housingneeds_other"] = housingneeds_other attributes["illness"] = field_118 @@ -808,7 +841,7 @@ private def housingneeds if field_59 == 1 - 1 + 2 elsif field_60 == 1 3 else @@ -816,6 +849,20 @@ private end end + def housingneeds_type + if field_55 == 1 + 0 + elsif field_56 == 1 + 1 + elsif field_57 == 1 + 2 + end + end + + def housingneeds_other + return 1 if field_58 == 1 + end + def ethnic_group_from_ethnic return nil if field_43.blank? diff --git a/config/locales/en.yml b/config/locales/en.yml index 98e40dfb3..23d39bbe8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -347,6 +347,12 @@ en: must_be_child: "Answer must be ‘child’ if the person is aged 16-19 and a student" housingneeds_a: one_or_two_choices: "You can only select one option or ‘other disabled access needs’ plus ‘wheelchair-accessible housing’, ‘wheelchair access to essential rooms’ or ‘level access housing’" + housingneeds_type: + only_one_option_permitted: "Only one disabled access need: fully wheelchair-accessible housing, wheelchair access to essential rooms or level access housing, can be selected" + housingneeds: + no_disabled_needs_conjunction: "No disabled access needs can’t be selected if you have selected fully wheelchair-accessible housing, wheelchair access to essential rooms, level access housing or other disabled access needs" + dont_know_disabled_needs_conjunction: "Don’t know disabled access needs can’t be selected if you have selected fully wheelchair-accessible housing, wheelchair access to essential rooms, level access housing or other disabled access needs" + no_and_dont_know_disabled_needs_conjunction: "No disabled access needs and don’t know disabled access needs cannot be selected together" prevten: non_temp_accommodation: "Answer cannot be non-temporary accommodation as this is a re-let to a tenant who occupied the same property as temporary accommodation" over_20_foster_care: "Answer cannot be a children’s home or foster care as the lead tenant is 20 or older" diff --git a/spec/services/bulk_upload/lettings/row_parser_spec.rb b/spec/services/bulk_upload/lettings/row_parser_spec.rb index 8f609838c..1bebd6444 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/row_parser_spec.rb @@ -344,6 +344,49 @@ RSpec.describe BulkUpload::Lettings::RowParser do end end + describe "#field_55, #field_56, #field_57" do + context "when more than one item selected" do + let(:attributes) { { bulk_upload:, field_55: "1", field_56: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_55]).to be_present + expect(parser.errors[:field_56]).to be_present + expect(parser.errors[:field_57]).to be_present + end + end + end + + describe "#field_59" do + context "when 1 and another disability field selected" do + let(:attributes) { { bulk_upload:, field_59: "1", field_58: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_59]).to be_present + end + end + end + + describe "#field_60" do + context "when 1 and another disability field selected" do + let(:attributes) { { bulk_upload:, field_60: "1", field_58: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_60]).to be_present + end + end + end + + describe "#field_59, #field_60" do + context "when both 1" do + let(:attributes) { { bulk_upload:, field_59: "1", field_60: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_59]).to be_present + expect(parser.errors[:field_60]).to be_present + end + end + end + describe "#field_78" do # referral context "when 3 ie PRP nominated by LA and owning org is LA" do let(:attributes) { { bulk_upload:, field_78: "3", field_111: owning_org.old_visible_id } } @@ -894,6 +937,60 @@ RSpec.describe BulkUpload::Lettings::RowParser do end end end + + describe "#housingneeds" do + context "when no disabled needs" do + let(:attributes) { { bulk_upload:, field_59: "1" } } + + it "sets to 2" do + expect(parser.log.housingneeds).to eq(2) + end + end + + context "when dont know about disabled needs" do + let(:attributes) { { bulk_upload:, field_60: "1" } } + + it "sets to 3" do + expect(parser.log.housingneeds).to eq(3) + end + end + end + + describe "#housingneeds_type" do + context "when field_55 is 1" do + let(:attributes) { { bulk_upload:, field_55: "1" } } + + it "set to 0" do + expect(parser.log.housingneeds_type).to eq(0) + end + end + + context "when field_56 is 1" do + let(:attributes) { { bulk_upload:, field_56: "1" } } + + it "set to 1" do + expect(parser.log.housingneeds_type).to eq(1) + end + end + + context "when field_57 is 1" do + let(:attributes) { { bulk_upload:, field_57: "1" } } + + it "set to 2" do + expect(parser.log.housingneeds_type).to eq(2) + end + end + end + + describe "#housingneeds_other" do + context "when field_58 is 1" do + let(:attributes) { { bulk_upload:, field_58: "1" } } + + it "sets to 1" do + expect(parser.log.housingneeds_other).to eq(1) + end + end + end end describe "#start_date" do From 2b918a09d7a2baee05f6caaef05810546086c5cd Mon Sep 17 00:00:00 2001 From: James Rose Date: Thu, 16 Feb 2023 08:55:01 +0000 Subject: [PATCH 10/30] Rename lettings log export field `form` to `formid` (#1306) --- app/services/exports/lettings_log_export_constants.rb | 2 +- app/services/exports/lettings_log_export_service.rb | 2 +- spec/fixtures/exports/general_needs_log.csv | 2 +- spec/fixtures/exports/general_needs_log.xml | 2 +- spec/fixtures/exports/supported_housing_logs.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/services/exports/lettings_log_export_constants.rb b/app/services/exports/lettings_log_export_constants.rb index 9184efb25..332f22550 100644 --- a/app/services/exports/lettings_log_export_constants.rb +++ b/app/services/exports/lettings_log_export_constants.rb @@ -30,7 +30,7 @@ module Exports::LettingsLogExportConstants "confidential", "earnings", "ethnic", - "form", + "formid", "has_benefits", "hb", "hbrentshortfall", diff --git a/app/services/exports/lettings_log_export_service.rb b/app/services/exports/lettings_log_export_service.rb index aed3ea98a..fb985f931 100644 --- a/app/services/exports/lettings_log_export_service.rb +++ b/app/services/exports/lettings_log_export_service.rb @@ -152,7 +152,7 @@ module Exports def apply_cds_transformation(lettings_log, export_mode) attribute_hash = lettings_log.attributes_before_type_cast - attribute_hash["form"] = attribute_hash["old_form_id"] || (attribute_hash["id"] + LOG_ID_OFFSET) + attribute_hash["formid"] = attribute_hash["old_form_id"] || (attribute_hash["id"] + LOG_ID_OFFSET) # We can't have a variable number of columns in CSV unless export_mode == EXPORT_MODE[:csv] diff --git a/spec/fixtures/exports/general_needs_log.csv b/spec/fixtures/exports/general_needs_log.csv index afaa9e745..8cc656f19 100644 --- a/spec/fixtures/exports/general_needs_log.csv +++ b/spec/fixtures/exports/general_needs_log.csv @@ -1,2 +1,2 @@ -status,tenancycode,age1,sex1,ethnic,national,prevten,ecstat1,hhmemb,age2,sex2,ecstat2,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,leftreg,reservist,illness,preg_occ,startertenancy,tenancylength,tenancy,ppostcode_full,rsnvac,unittype_gn,beds,offered,wchair,earnings,incfreq,benefits,period,layear,waityear,postcode_full,reasonpref,cbl,chr,cap,reasonother,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_f,housingneeds_g,housingneeds_h,illness_type_1,illness_type_2,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,irproduct_other,reason,propcode,la,prevloc,hb,hbrentshortfall,mrcdate,incref,startdate,armedforces,unitletas,builtype,voiddate,renttype,needstype,lettype,totchild,totelder,totadult,nocharge,referral,brent,scharge,pscharge,supcharg,tcharge,tshortfall,chcharge,ppcodenk,has_benefits,renewal,wrent,wscharge,wpschrge,wsupchrg,wtcharge,wtshortfall,refused,housingneeds,wchchrg,newprop,relat2,relat3,relat4,relat5,relat6,relat7,relat8,lar,irproduct,joint,sheltered,hhtype,new_old,vacdays,form,owningorgid,owningorgname,hcnum,maningorgid,maningorgname,manhcnum,createddate,uploaddate +status,tenancycode,age1,sex1,ethnic,national,prevten,ecstat1,hhmemb,age2,sex2,ecstat2,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,leftreg,reservist,illness,preg_occ,startertenancy,tenancylength,tenancy,ppostcode_full,rsnvac,unittype_gn,beds,offered,wchair,earnings,incfreq,benefits,period,layear,waityear,postcode_full,reasonpref,cbl,chr,cap,reasonother,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_f,housingneeds_g,housingneeds_h,illness_type_1,illness_type_2,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,irproduct_other,reason,propcode,la,prevloc,hb,hbrentshortfall,mrcdate,incref,startdate,armedforces,unitletas,builtype,voiddate,renttype,needstype,lettype,totchild,totelder,totadult,nocharge,referral,brent,scharge,pscharge,supcharg,tcharge,tshortfall,chcharge,ppcodenk,has_benefits,renewal,wrent,wscharge,wpschrge,wsupchrg,wtcharge,wtshortfall,refused,housingneeds,wchchrg,newprop,relat2,relat3,relat4,relat5,relat6,relat7,relat8,lar,irproduct,joint,sheltered,hhtype,new_old,vacdays,formid,owningorgid,owningorgname,hcnum,maningorgid,maningorgname,manhcnum,createddate,uploaddate 2,BZ737,35,F,2,4,6,0,2,32,M,6,,,,,,,,,,,,,,,,,,,1,4,1,1,1,2,1,5,1,SE2 6RT,6,7,3,2,1,68,1,1,2,2,1,NW1 5TY,1,2,1,2,,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,,,4,123,E09000003,E07000105,6,1,2020-05-05T10:36:49+01:00,0,2022-02-02T10:36:49+00:00,1,2,1,2019-11-03T00:00:00+00:00,2,1,7,0,0,2,0,2,200.0,50.0,40.0,35.0,325.0,12.0,,1,1,0,100.0,25.0,20.0,17.5,162.5,6.0,0,1,,2,P,,,,,,,,,,,4,2,638,{id},{owning_org_id},DLUHC,1234,{managing_org_id},DLUHC,1234,2022-02-08T16:52:15+00:00,2022-02-08T16:52:15+00:00 diff --git a/spec/fixtures/exports/general_needs_log.xml b/spec/fixtures/exports/general_needs_log.xml index 1e60333dc..62542af54 100644 --- a/spec/fixtures/exports/general_needs_log.xml +++ b/spec/fixtures/exports/general_needs_log.xml @@ -135,7 +135,7 @@ 4 2 638 -
{id}
+ {id} {owning_org_id} DLUHC 1234 diff --git a/spec/fixtures/exports/supported_housing_logs.xml b/spec/fixtures/exports/supported_housing_logs.xml index 7674984b4..69f52161c 100644 --- a/spec/fixtures/exports/supported_housing_logs.xml +++ b/spec/fixtures/exports/supported_housing_logs.xml @@ -134,7 +134,7 @@ 4 2 638 -
{id}
+ {id} {owning_org_id} DLUHC 1234 From be3e782a0eb77854de6ee639651d674ba346c32c Mon Sep 17 00:00:00 2001 From: Jack <113976590+bibblobcode@users.noreply.github.com> Date: Thu, 16 Feb 2023 09:41:56 +0000 Subject: [PATCH 11/30] Add 2023 sales validations (#1305) --- config/sale_range_data/2023.csv | 1185 +++++++++++++++++++++++++++++++ 1 file changed, 1185 insertions(+) create mode 100644 config/sale_range_data/2023.csv diff --git a/config/sale_range_data/2023.csv b/config/sale_range_data/2023.csv new file mode 100644 index 000000000..ebe873c07 --- /dev/null +++ b/config/sale_range_data/2023.csv @@ -0,0 +1,1185 @@ +la_name,la,bedrooms,soft_min,soft_max +Adur,E07000223,1,126000,447000 +Adur,E07000223,2,220000,480000 +Adur,E07000223,3,312000,658000 +Adur,E07000223,4,383000,938000 +Amber Valley,E07000032,1,99000,306000 +Amber Valley,E07000032,2,99000,306000 +Amber Valley,E07000032,3,123000,407000 +Amber Valley,E07000032,4,236000,842000 +Arun,E07000224,1,102000,486000 +Arun,E07000224,2,182000,486000 +Arun,E07000224,3,248000,569000 +Arun,E07000224,4,351000,966000 +Ashfield,E07000170,1,77000,250000 +Ashfield,E07000170,2,87000,250000 +Ashfield,E07000170,3,103000,298000 +Ashfield,E07000170,4,198000,547000 +Ashford,E07000105,1,102000,435000 +Ashford,E07000105,2,105000,435000 +Ashford,E07000105,3,239000,543000 +Ashford,E07000105,4,340000,1120000 +Babergh,E07000200,1,113000,428000 +Babergh,E07000200,2,149000,428000 +Babergh,E07000200,3,215000,557000 +Babergh,E07000200,4,341000,1230000 +Barking and Dagenham,E09000002,1,88000,383000 +Barking and Dagenham,E09000002,2,103000,401000 +Barking and Dagenham,E09000002,3,220000,506000 +Barking and Dagenham,E09000002,4,308000,661000 +Barnet,E09000003,1,142000,538000 +Barnet,E09000003,2,205000,696000 +Barnet,E09000003,3,416000,1071000 +Barnet,E09000003,4,654000,2254000 +Barnsley,E08000016,1,70000,225000 +Barnsley,E08000016,2,70000,225000 +Barnsley,E08000016,3,93000,289000 +Barnsley,E08000016,4,175000,574000 +Basildon,E07000066,1,96000,457000 +Basildon,E07000066,2,145000,457000 +Basildon,E07000066,3,273000,567000 +Basildon,E07000066,4,376000,1118000 +Basingstoke and Deane,E07000084,1,113000,448000 +Basingstoke and Deane,E07000084,2,139000,448000 +Basingstoke and Deane,E07000084,3,275000,578000 +Basingstoke and Deane,E07000084,4,394000,1115000 +Bassetlaw,E07000171,1,92000,313000 +Bassetlaw,E07000171,2,92000,313000 +Bassetlaw,E07000171,3,105000,356000 +Bassetlaw,E07000171,4,186000,627000 +Bath and North East Somerset,E06000022,1,119000,609000 +Bath and North East Somerset,E06000022,2,154000,609000 +Bath and North East Somerset,E06000022,3,249000,762000 +Bath and North East Somerset,E06000022,4,380000,1604000 +Bedford,E06000055,1,96000,349000 +Bedford,E06000055,2,120000,349000 +Bedford,E06000055,3,210000,467000 +Bedford,E06000055,4,371000,946000 +Bexley,E09000004,1,109000,449000 +Bexley,E09000004,2,142000,449000 +Bexley,E09000004,3,312000,581000 +Bexley,E09000004,4,412000,972000 +Birmingham,E08000025,1,79000,373000 +Birmingham,E08000025,2,107000,373000 +Birmingham,E08000025,3,137000,452000 +Birmingham,E08000025,4,198000,1006000 +Blaby,E07000129,1,111000,357000 +Blaby,E07000129,2,111000,357000 +Blaby,E07000129,3,194000,389000 +Blaby,E07000129,4,285000,723000 +Blackburn with Darwen,E06000008,1,61000,234000 +Blackburn with Darwen,E06000008,2,74000,234000 +Blackburn with Darwen,E06000008,3,92000,310000 +Blackburn with Darwen,E06000008,4,148000,608000 +Blackpool,E06000009,1,59000,228000 +Blackpool,E06000009,2,83000,228000 +Blackpool,E06000009,3,100000,265000 +Blackpool,E06000009,4,114000,450000 +Bolsover,E07000033,1,81000,222000 +Bolsover,E07000033,2,81000,222000 +Bolsover,E07000033,3,87000,279000 +Bolsover,E07000033,4,169000,543000 +Bolton,E08000001,1,77000,251000 +Bolton,E08000001,2,84000,251000 +Bolton,E08000001,3,104000,340000 +Bolton,E08000001,4,175000,762000 +Boston,E07000136,1,58000,258000 +Boston,E07000136,2,58000,258000 +Boston,E07000136,3,111000,308000 +Boston,E07000136,4,212000,510000 +Bournemouth, Christchurch and Poole,E06000058,1,140000,500000 +Bournemouth, Christchurch and Poole,E06000058,2,197000,500000 +Bournemouth, Christchurch and Poole,E06000058,3,285000,657000 +Bournemouth, Christchurch and Poole,E06000058,4,370000,1321000 +Bracknell Forest,E06000036,1,95000,488000 +Bracknell Forest,E06000036,2,157000,488000 +Bracknell Forest,E06000036,3,341000,654000 +Bracknell Forest,E06000036,4,417000,1185000 +Bradford,E08000032,1,65000,291000 +Bradford,E08000032,2,77000,291000 +Bradford,E08000032,3,96000,365000 +Bradford,E08000032,4,124000,799000 +Braintree,E07000067,1,126000,425000 +Braintree,E07000067,2,166000,425000 +Braintree,E07000067,3,273000,534000 +Braintree,E07000067,4,370000,1177000 +Breckland,E07000143,1,105000,315000 +Breckland,E07000143,2,130000,315000 +Breckland,E07000143,3,173000,415000 +Breckland,E07000143,4,262000,795000 +Brent,E09000005,1,109000,625000 +Brent,E09000005,2,160000,817000 +Brent,E09000005,3,387000,1319000 +Brent,E09000005,4,555000,2717000 +Brentwood,E07000068,1,146000,530000 +Brentwood,E07000068,2,244000,703000 +Brentwood,E07000068,3,402000,897000 +Brentwood,E07000068,4,508000,1851000 +Brighton and Hove,E06000043,1,185000,526000 +Brighton and Hove,E06000043,2,270000,610000 +Brighton and Hove,E06000043,3,350000,846000 +Brighton and Hove,E06000043,4,458000,1487000 +Bristol, City of,E06000023,1,145000,417000 +Bristol, City of,E06000023,2,184000,562000 +Bristol, City of,E06000023,3,242000,685000 +Bristol, City of,E06000023,4,331000,1394000 +Broadland,E07000144,1,126000,334000 +Broadland,E07000144,2,140000,334000 +Broadland,E07000144,3,225000,433000 +Broadland,E07000144,4,302000,749000 +Bromley,E09000006,1,198000,559000 +Bromley,E09000006,2,276000,629000 +Bromley,E09000006,3,352000,824000 +Bromley,E09000006,4,499000,1553000 +Bromsgrove,E07000234,1,119000,485000 +Bromsgrove,E07000234,2,121000,485000 +Bromsgrove,E07000234,3,195000,556000 +Bromsgrove,E07000234,4,347000,1090000 +Broxbourne,E07000095,1,170000,480000 +Broxbourne,E07000095,2,252000,480000 +Broxbourne,E07000095,3,371000,658000 +Broxbourne,E07000095,4,485000,1428000 +Broxtowe,E07000172,1,99000,297000 +Broxtowe,E07000172,2,109000,297000 +Broxtowe,E07000172,3,151000,381000 +Broxtowe,E07000172,4,235000,789000 +Buckinghamshire,E06000060,1,157000,650000 +Buckinghamshire,E06000060,2,171000,650000 +Buckinghamshire,E06000060,3,314000,867000 +Buckinghamshire,E06000060,4,459000,1926000 +Burnley,E07000117,1,52000,225000 +Burnley,E07000117,2,62000,225000 +Burnley,E07000117,3,83000,283000 +Burnley,E07000117,4,147000,475000 +Bury,E08000002,1,89000,319000 +Bury,E08000002,2,99000,319000 +Bury,E08000002,3,136000,390000 +Bury,E08000002,4,224000,792000 +Calderdale,E08000033,1,74000,283000 +Calderdale,E08000033,2,79000,283000 +Calderdale,E08000033,3,103000,393000 +Calderdale,E08000033,4,163000,749000 +Cambridge,E07000008,1,139000,603000 +Cambridge,E07000008,2,174000,663000 +Cambridge,E07000008,3,339000,870000 +Cambridge,E07000008,4,530000,1952000 +Camden,E09000007,1,256000,806000 +Camden,E09000007,2,449000,1468000 +Camden,E09000007,3,559000,2535000 +Camden,E09000007,4,1009000,6026000 +Cannock Chase,E07000192,1,75000,248000 +Cannock Chase,E07000192,2,111000,248000 +Cannock Chase,E07000192,3,147000,326000 +Cannock Chase,E07000192,4,228000,511000 +Canterbury,E07000106,1,96000,465000 +Canterbury,E07000106,2,167000,465000 +Canterbury,E07000106,3,239000,591000 +Canterbury,E07000106,4,356000,1076000 +Castle Point,E07000069,1,155000,465000 +Castle Point,E07000069,2,219000,465000 +Castle Point,E07000069,3,283000,543000 +Castle Point,E07000069,4,378000,1017000 +Central Bedfordshire,E06000056,1,106000,418000 +Central Bedfordshire,E06000056,2,134000,418000 +Central Bedfordshire,E06000056,3,255000,537000 +Central Bedfordshire,E06000056,4,395000,942000 +Charnwood,E07000130,1,95000,338000 +Charnwood,E07000130,2,110000,338000 +Charnwood,E07000130,3,168000,397000 +Charnwood,E07000130,4,286000,803000 +Chelmsford,E07000070,1,89000,498000 +Chelmsford,E07000070,2,125000,498000 +Chelmsford,E07000070,3,330000,675000 +Chelmsford,E07000070,4,449000,1189000 +Cheltenham,E07000078,1,118000,487000 +Cheltenham,E07000078,2,175000,511000 +Cheltenham,E07000078,3,226000,638000 +Cheltenham,E07000078,4,328000,1280000 +Cherwell,E07000177,1,114000,432000 +Cherwell,E07000177,2,114000,432000 +Cherwell,E07000177,3,235000,543000 +Cherwell,E07000177,4,359000,999000 +Cheshire East,E06000049,1,98000,454000 +Cheshire East,E06000049,2,98000,454000 +Cheshire East,E06000049,3,132000,566000 +Cheshire East,E06000049,4,288000,1199000 +Cheshire West and Chester,E06000050,1,84000,375000 +Cheshire West and Chester,E06000050,2,94000,375000 +Cheshire West and Chester,E06000050,3,122000,450000 +Cheshire West and Chester,E06000050,4,253000,975000 +Chesterfield,E07000034,1,73000,240000 +Chesterfield,E07000034,2,95000,240000 +Chesterfield,E07000034,3,117000,353000 +Chesterfield,E07000034,4,194000,629000 +Chichester,E07000225,1,101000,610000 +Chichester,E07000225,2,115000,610000 +Chichester,E07000225,3,282000,787000 +Chichester,E07000225,4,410000,2095000 +Chorley,E07000118,1,73000,333000 +Chorley,E07000118,2,100000,333000 +Chorley,E07000118,3,123000,393000 +Chorley,E07000118,4,234000,716000 +City of London,E09000001,1,467000,1026000 +City of London,E09000001,2,681000,2732000 +City of London,E09000001,3,1741000,5775000 +City of London,E09000001,4,1932000,2288000 +Colchester,E07000071,1,114000,383000 +Colchester,E07000071,2,164000,383000 +Colchester,E07000071,3,256000,490000 +Colchester,E07000071,4,364000,928000 +Cornwall,E06000052,1,98000,406000 +Cornwall,E06000052,2,102000,406000 +Cornwall,E06000052,3,161000,524000 +Cornwall,E06000052,4,250000,864000 +Cotswold,E07000079,1,74000,659000 +Cotswold,E07000079,2,121000,659000 +Cotswold,E07000079,3,181000,812000 +Cotswold,E07000079,4,408000,1603000 +County Durham,E06000047,1,55000,232000 +County Durham,E06000047,2,55000,232000 +County Durham,E06000047,3,72000,299000 +County Durham,E06000047,4,148000,593000 +Coventry,E08000026,1,88000,277000 +Coventry,E08000026,2,118000,277000 +Coventry,E08000026,3,165000,396000 +Coventry,E08000026,4,237000,783000 +Crawley,E07000226,1,159000,387000 +Crawley,E07000226,2,230000,416000 +Crawley,E07000226,3,315000,528000 +Crawley,E07000226,4,378000,794000 +Croydon,E09000008,1,115000,444000 +Croydon,E09000008,2,183000,553000 +Croydon,E09000008,3,341000,692000 +Croydon,E09000008,4,464000,1215000 +Cumberland,E06000063,1,67000,271000 +Cumberland,E06000063,2,81000,271000 +Cumberland,E06000063,3,96000,349000 +Cumberland,E06000063,4,153000,577000 +Dacorum,E07000096,1,174000,618000 +Dacorum,E07000096,2,234000,621000 +Dacorum,E07000096,3,354000,839000 +Dacorum,E07000096,4,478000,1765000 +Darlington,E06000005,1,56000,240000 +Darlington,E06000005,2,69000,240000 +Darlington,E06000005,3,91000,310000 +Darlington,E06000005,4,185000,614000 +Dartford,E07000107,1,117000,431000 +Dartford,E07000107,2,117000,431000 +Dartford,E07000107,3,307000,559000 +Dartford,E07000107,4,426000,963000 +Derby,E06000015,1,67000,258000 +Derby,E06000015,2,94000,258000 +Derby,E06000015,3,117000,352000 +Derby,E06000015,4,237000,646000 +Derbyshire Dales,E07000035,1,73000,474000 +Derbyshire Dales,E07000035,2,88000,474000 +Derbyshire Dales,E07000035,3,181000,601000 +Derbyshire Dales,E07000035,4,300000,973000 +Doncaster,E08000017,1,77000,233000 +Doncaster,E08000017,2,78000,233000 +Doncaster,E08000017,3,93000,286000 +Doncaster,E08000017,4,173000,582000 +Dorset,E06000059,1,112000,471000 +Dorset,E06000059,2,159000,471000 +Dorset,E06000059,3,231000,615000 +Dorset,E06000059,4,328000,1150000 +Dover,E07000108,1,109000,384000 +Dover,E07000108,2,120000,384000 +Dover,E07000108,3,190000,501000 +Dover,E07000108,4,265000,956000 +Dudley,E08000027,1,76000,281000 +Dudley,E08000027,2,102000,281000 +Dudley,E08000027,3,137000,371000 +Dudley,E08000027,4,209000,650000 +Ealing,E09000009,1,129000,574000 +Ealing,E09000009,2,195000,760000 +Ealing,E09000009,3,338000,1095000 +Ealing,E09000009,4,523000,2198000 +East Cambridgeshire,E07000009,1,120000,385000 +East Cambridgeshire,E07000009,2,128000,385000 +East Cambridgeshire,E07000009,3,227000,499000 +East Cambridgeshire,E07000009,4,331000,916000 +East Devon,E07000040,1,91000,459000 +East Devon,E07000040,2,114000,459000 +East Devon,E07000040,3,181000,593000 +East Devon,E07000040,4,324000,1123000 +East Hampshire,E07000085,1,122000,577000 +East Hampshire,E07000085,2,152000,577000 +East Hampshire,E07000085,3,288000,727000 +East Hampshire,E07000085,4,438000,1371000 +East Hertfordshire,E07000242,1,143000,578000 +East Hertfordshire,E07000242,2,170000,578000 +East Hertfordshire,E07000242,3,349000,816000 +East Hertfordshire,E07000242,4,512000,1582000 +East Lindsey,E07000137,1,68000,320000 +East Lindsey,E07000137,2,85000,320000 +East Lindsey,E07000137,3,112000,359000 +East Lindsey,E07000137,4,199000,699000 +East Riding of Yorkshire,E06000011,1,79000,322000 +East Riding of Yorkshire,E06000011,2,101000,322000 +East Riding of Yorkshire,E06000011,3,133000,376000 +East Riding of Yorkshire,E06000011,4,205000,717000 +East Staffordshire,E07000193,1,80000,330000 +East Staffordshire,E07000193,2,100000,330000 +East Staffordshire,E07000193,3,128000,379000 +East Staffordshire,E07000193,4,236000,753000 +East Suffolk,E07000244,1,94000,414000 +East Suffolk,E07000244,2,125000,414000 +East Suffolk,E07000244,3,147000,481000 +East Suffolk,E07000244,4,247000,913000 +Eastbourne,E07000061,1,121000,371000 +Eastbourne,E07000061,2,169000,371000 +Eastbourne,E07000061,3,238000,496000 +Eastbourne,E07000061,4,307000,870000 +Eastleigh,E07000086,1,113000,389000 +Eastleigh,E07000086,2,117000,389000 +Eastleigh,E07000086,3,202000,514000 +Eastleigh,E07000086,4,369000,936000 +Elmbridge,E07000207,1,145000,857000 +Elmbridge,E07000207,2,265000,857000 +Elmbridge,E07000207,3,462000,1175000 +Elmbridge,E07000207,4,668000,2805000 +Enfield,E09000010,1,126000,507000 +Enfield,E09000010,2,175000,600000 +Enfield,E09000010,3,336000,858000 +Enfield,E09000010,4,498000,1612000 +Epping Forest,E07000072,1,131000,697000 +Epping Forest,E07000072,2,287000,697000 +Epping Forest,E07000072,3,391000,923000 +Epping Forest,E07000072,4,545000,2007000 +Epsom and Ewell,E07000208,1,183000,693000 +Epsom and Ewell,E07000208,2,270000,693000 +Epsom and Ewell,E07000208,3,430000,929000 +Epsom and Ewell,E07000208,4,627000,1574000 +Erewash,E07000036,1,90000,270000 +Erewash,E07000036,2,99000,270000 +Erewash,E07000036,3,133000,362000 +Erewash,E07000036,4,211000,701000 +Exeter,E07000041,1,112000,402000 +Exeter,E07000041,2,130000,402000 +Exeter,E07000041,3,226000,500000 +Exeter,E07000041,4,329000,1005000 +Fareham,E07000087,1,127000,418000 +Fareham,E07000087,2,190000,418000 +Fareham,E07000087,3,265000,520000 +Fareham,E07000087,4,356000,943000 +Fenland,E07000010,1,85000,268000 +Fenland,E07000010,2,120000,268000 +Fenland,E07000010,3,154000,348000 +Fenland,E07000010,4,230000,594000 +Folkestone and Hythe,E07000112,1,106000,406000 +Folkestone and Hythe,E07000112,2,115000,406000 +Folkestone and Hythe,E07000112,3,208000,549000 +Folkestone and Hythe,E07000112,4,287000,883000 +Forest of Dean,E07000080,1,101000,439000 +Forest of Dean,E07000080,2,119000,439000 +Forest of Dean,E07000080,3,176000,522000 +Forest of Dean,E07000080,4,262000,872000 +Fylde,E07000119,1,87000,394000 +Fylde,E07000119,2,109000,407000 +Fylde,E07000119,3,127000,407000 +Fylde,E07000119,4,240000,763000 +Gateshead,E08000037,1,59000,229000 +Gateshead,E08000037,2,71000,229000 +Gateshead,E08000037,3,94000,337000 +Gateshead,E08000037,4,151000,583000 +Gedling,E07000173,1,77000,288000 +Gedling,E07000173,2,102000,288000 +Gedling,E07000173,3,147000,376000 +Gedling,E07000173,4,232000,747000 +Gloucester,E07000081,1,80000,302000 +Gloucester,E07000081,2,126000,302000 +Gloucester,E07000081,3,181000,397000 +Gloucester,E07000081,4,286000,619000 +Gosport,E07000088,1,107000,369000 +Gosport,E07000088,2,139000,370000 +Gosport,E07000088,3,201000,425000 +Gosport,E07000088,4,270000,791000 +Gravesham,E07000109,1,85000,432000 +Gravesham,E07000109,2,124000,432000 +Gravesham,E07000109,3,272000,549000 +Gravesham,E07000109,4,365000,1107000 +Great Yarmouth,E07000145,1,89000,287000 +Great Yarmouth,E07000145,2,93000,287000 +Great Yarmouth,E07000145,3,116000,352000 +Great Yarmouth,E07000145,4,178000,586000 +Greenwich,E09000011,1,115000,592000 +Greenwich,E09000011,2,158000,705000 +Greenwich,E09000011,3,283000,888000 +Greenwich,E09000011,4,410000,1778000 +Guildford,E07000209,1,124000,629000 +Guildford,E07000209,2,240000,738000 +Guildford,E07000209,3,387000,946000 +Guildford,E07000209,4,525000,2156000 +Hackney,E09000012,1,181000,698000 +Hackney,E09000012,2,236000,935000 +Hackney,E09000012,3,423000,1443000 +Hackney,E09000012,4,732000,2120000 +Halton,E06000006,1,76000,255000 +Halton,E06000006,2,87000,255000 +Halton,E06000006,3,102000,327000 +Halton,E06000006,4,174000,581000 +Hammersmith and Fulham,E09000013,1,206000,790000 +Hammersmith and Fulham,E09000013,2,395000,1248000 +Hammersmith and Fulham,E09000013,3,484000,1767000 +Hammersmith and Fulham,E09000013,4,1058000,3462000 +Harborough,E07000131,1,65000,458000 +Harborough,E07000131,2,65000,458000 +Harborough,E07000131,3,154000,535000 +Harborough,E07000131,4,352000,1070000 +Haringey,E09000014,1,155000,589000 +Haringey,E09000014,2,251000,862000 +Haringey,E09000014,3,406000,1279000 +Haringey,E09000014,4,644000,2638000 +Harlow,E07000073,1,128000,369000 +Harlow,E07000073,2,166000,422000 +Harlow,E07000073,3,283000,536000 +Harlow,E07000073,4,384000,920000 +Harrow,E09000015,1,129000,536000 +Harrow,E09000015,2,282000,623000 +Harrow,E09000015,3,418000,836000 +Harrow,E09000015,4,570000,1607000 +Hart,E07000089,1,108000,576000 +Hart,E07000089,2,150000,576000 +Hart,E07000089,3,351000,705000 +Hart,E07000089,4,508000,1431000 +Hartlepool,E06000001,1,56000,242000 +Hartlepool,E06000001,2,56000,242000 +Hartlepool,E06000001,3,77000,261000 +Hartlepool,E06000001,4,133000,559000 +Hastings,E07000062,1,106000,340000 +Hastings,E07000062,2,155000,381000 +Hastings,E07000062,3,214000,437000 +Hastings,E07000062,4,282000,727000 +Havant,E07000090,1,98000,414000 +Havant,E07000090,2,144000,414000 +Havant,E07000090,3,229000,501000 +Havant,E07000090,4,309000,855000 +Havering,E09000016,1,137000,472000 +Havering,E09000016,2,204000,481000 +Havering,E09000016,3,336000,657000 +Havering,E09000016,4,412000,1232000 +Herefordshire, County of,E06000019,1,98000,419000 +Herefordshire, County of,E06000019,2,105000,419000 +Herefordshire, County of,E06000019,3,162000,499000 +Herefordshire, County of,E06000019,4,283000,885000 +Hertsmere,E07000098,1,178000,666000 +Hertsmere,E07000098,2,316000,666000 +Hertsmere,E07000098,3,440000,918000 +Hertsmere,E07000098,4,591000,2232000 +High Peak,E07000037,1,110000,322000 +High Peak,E07000037,2,126000,322000 +High Peak,E07000037,3,161000,433000 +High Peak,E07000037,4,247000,804000 +Hillingdon,E09000017,1,132000,502000 +Hillingdon,E09000017,2,196000,577000 +Hillingdon,E09000017,3,401000,788000 +Hillingdon,E09000017,4,507000,1414000 +Hinckley and Bosworth,E07000132,1,99000,328000 +Hinckley and Bosworth,E07000132,2,110000,328000 +Hinckley and Bosworth,E07000132,3,164000,418000 +Hinckley and Bosworth,E07000132,4,270000,840000 +Horsham,E07000227,1,140000,529000 +Horsham,E07000227,2,140000,529000 +Horsham,E07000227,3,297000,745000 +Horsham,E07000227,4,496000,1513000 +Hounslow,E09000018,1,119000,563000 +Hounslow,E09000018,2,154000,733000 +Hounslow,E09000018,3,330000,1036000 +Hounslow,E09000018,4,475000,2743000 +Huntingdonshire,E07000011,1,91000,389000 +Huntingdonshire,E07000011,2,107000,389000 +Huntingdonshire,E07000011,3,208000,478000 +Huntingdonshire,E07000011,4,318000,814000 +Hyndburn,E07000120,1,66000,214000 +Hyndburn,E07000120,2,66000,214000 +Hyndburn,E07000120,3,87000,307000 +Hyndburn,E07000120,4,168000,587000 +Ipswich,E07000202,1,97000,309000 +Ipswich,E07000202,2,125000,309000 +Ipswich,E07000202,3,182000,399000 +Ipswich,E07000202,4,253000,851000 +Isle of Wight,E06000046,1,96000,352000 +Isle of Wight,E06000046,2,131000,352000 +Isle of Wight,E06000046,3,185000,452000 +Isle of Wight,E06000046,4,239000,827000 +Islington,E09000019,1,203000,741000 +Islington,E09000019,2,398000,1223000 +Islington,E09000019,3,490000,1891000 +Islington,E09000019,4,762000,2824000 +Kensington and Chelsea,E09000020,1,234000,1261000 +Kensington and Chelsea,E09000020,2,599000,2368000 +Kensington and Chelsea,E09000020,3,706000,4202000 +Kensington and Chelsea,E09000020,4,1405000,13278000 +King's Lynn and West Norfolk,E07000146,1,77000,346000 +King's Lynn and West Norfolk,E07000146,2,123000,346000 +King's Lynn and West Norfolk,E07000146,3,161000,408000 +King's Lynn and West Norfolk,E07000146,4,243000,778000 +Kingston upon Hull, City of,E06000010,1,63000,189000 +Kingston upon Hull, City of,E06000010,2,67000,189000 +Kingston upon Hull, City of,E06000010,3,84000,259000 +Kingston upon Hull, City of,E06000010,4,110000,415000 +Kingston upon Thames,E09000021,1,156000,649000 +Kingston upon Thames,E09000021,2,325000,708000 +Kingston upon Thames,E09000021,3,398000,935000 +Kingston upon Thames,E09000021,4,549000,1957000 +Kirklees,E08000034,1,70000,261000 +Kirklees,E08000034,2,85000,261000 +Kirklees,E08000034,3,115000,356000 +Kirklees,E08000034,4,198000,752000 +Knowsley,E08000011,1,74000,223000 +Knowsley,E08000011,2,74000,223000 +Knowsley,E08000011,3,94000,302000 +Knowsley,E08000011,4,134000,481000 +Lambeth,E09000022,1,166000,639000 +Lambeth,E09000022,2,242000,842000 +Lambeth,E09000022,3,392000,1259000 +Lambeth,E09000022,4,573000,2054000 +Lancaster,E07000121,1,79000,312000 +Lancaster,E07000121,2,93000,312000 +Lancaster,E07000121,3,126000,378000 +Lancaster,E07000121,4,147000,699000 +Leeds,E08000035,1,87000,323000 +Leeds,E08000035,2,101000,323000 +Leeds,E08000035,3,126000,435000 +Leeds,E08000035,4,181000,878000 +Leicester,E06000016,1,47000,282000 +Leicester,E06000016,2,82000,282000 +Leicester,E06000016,3,102000,364000 +Leicester,E06000016,4,235000,728000 +Lewes,E07000063,1,140000,528000 +Lewes,E07000063,2,204000,528000 +Lewes,E07000063,3,251000,725000 +Lewes,E07000063,4,363000,1374000 +Lewisham,E09000023,1,169000,506000 +Lewisham,E09000023,2,238000,675000 +Lewisham,E09000023,3,357000,881000 +Lewisham,E09000023,4,486000,1540000 +Lichfield,E07000194,1,100000,433000 +Lichfield,E07000194,2,100000,433000 +Lichfield,E07000194,3,176000,528000 +Lichfield,E07000194,4,296000,990000 +Lincoln,E07000138,1,63000,258000 +Lincoln,E07000138,2,85000,258000 +Lincoln,E07000138,3,132000,335000 +Lincoln,E07000138,4,195000,563000 +Liverpool,E08000012,1,76000,323000 +Liverpool,E08000012,2,76000,323000 +Liverpool,E08000012,3,95000,381000 +Liverpool,E08000012,4,138000,790000 +Luton,E06000032,1,123000,349000 +Luton,E06000032,2,154000,349000 +Luton,E06000032,3,253000,462000 +Luton,E06000032,4,310000,755000 +Maidstone,E07000110,1,75000,455000 +Maidstone,E07000110,2,121000,455000 +Maidstone,E07000110,3,217000,566000 +Maidstone,E07000110,4,402000,1099000 +Maldon,E07000074,1,127000,516000 +Maldon,E07000074,2,127000,516000 +Maldon,E07000074,3,293000,612000 +Maldon,E07000074,4,400000,1389000 +Malvern Hills,E07000235,1,103000,448000 +Malvern Hills,E07000235,2,103000,448000 +Malvern Hills,E07000235,3,201000,588000 +Malvern Hills,E07000235,4,330000,942000 +Manchester,E08000003,1,106000,348000 +Manchester,E08000003,2,106000,438000 +Manchester,E08000003,3,109000,517000 +Manchester,E08000003,4,165000,1108000 +Mansfield,E07000174,1,77000,270000 +Mansfield,E07000174,2,80000,270000 +Mansfield,E07000174,3,103000,305000 +Mansfield,E07000174,4,184000,544000 +Medway,E06000035,1,72000,375000 +Medway,E06000035,2,148000,375000 +Medway,E06000035,3,226000,484000 +Medway,E06000035,4,309000,758000 +Melton,E07000133,1,62000,393000 +Melton,E07000133,2,62000,393000 +Melton,E07000133,3,173000,464000 +Melton,E07000133,4,264000,1049000 +Mendip,E07000187,1,110000,417000 +Mendip,E07000187,2,121000,417000 +Mendip,E07000187,3,222000,525000 +Mendip,E07000187,4,320000,1134000 +Merton,E09000024,1,205000,696000 +Merton,E09000024,2,263000,792000 +Merton,E09000024,3,370000,1016000 +Merton,E09000024,4,508000,2679000 +Mid Devon,E07000042,1,100000,412000 +Mid Devon,E07000042,2,115000,412000 +Mid Devon,E07000042,3,189000,526000 +Mid Devon,E07000042,4,292000,931000 +Mid Suffolk,E07000203,1,111000,389000 +Mid Suffolk,E07000203,2,124000,389000 +Mid Suffolk,E07000203,3,218000,502000 +Mid Suffolk,E07000203,4,315000,962000 +Mid Sussex,E07000228,1,187000,548000 +Mid Sussex,E07000228,2,245000,548000 +Mid Sussex,E07000228,3,352000,713000 +Mid Sussex,E07000228,4,471000,1406000 +Middlesbrough,E06000002,1,54000,228000 +Middlesbrough,E06000002,2,63000,228000 +Middlesbrough,E06000002,3,80000,272000 +Middlesbrough,E06000002,4,183000,533000 +Milton Keynes,E06000042,1,67000,377000 +Milton Keynes,E06000042,2,93000,377000 +Milton Keynes,E06000042,3,207000,495000 +Milton Keynes,E06000042,4,354000,921000 +Mole Valley,E07000210,1,173000,743000 +Mole Valley,E07000210,2,261000,753000 +Mole Valley,E07000210,3,429000,981000 +Mole Valley,E07000210,4,617000,1913000 +New Forest,E07000091,1,127000,505000 +New Forest,E07000091,2,199000,556000 +New Forest,E07000091,3,272000,756000 +New Forest,E07000091,4,378000,1262000 +Newark and Sherwood,E07000175,1,92000,353000 +Newark and Sherwood,E07000175,2,92000,353000 +Newark and Sherwood,E07000175,3,122000,402000 +Newark and Sherwood,E07000175,4,233000,829000 +Newcastle upon Tyne,E08000021,1,58000,293000 +Newcastle upon Tyne,E08000021,2,69000,293000 +Newcastle upon Tyne,E08000021,3,88000,381000 +Newcastle upon Tyne,E08000021,4,170000,828000 +Newcastle-under-Lyme,E07000195,1,92000,245000 +Newcastle-under-Lyme,E07000195,2,92000,245000 +Newcastle-under-Lyme,E07000195,3,121000,348000 +Newcastle-under-Lyme,E07000195,4,205000,684000 +Newham,E09000025,1,111000,515000 +Newham,E09000025,2,149000,665000 +Newham,E09000025,3,246000,794000 +Newham,E09000025,4,425000,1108000 +North Devon,E07000043,1,92000,415000 +North Devon,E07000043,2,110000,415000 +North Devon,E07000043,3,170000,468000 +North Devon,E07000043,4,255000,831000 +North East Derbyshire,E07000038,1,79000,322000 +North East Derbyshire,E07000038,2,79000,322000 +North East Derbyshire,E07000038,3,117000,424000 +North East Derbyshire,E07000038,4,230000,882000 +North East Lincolnshire,E06000012,1,53000,255000 +North East Lincolnshire,E06000012,2,69000,255000 +North East Lincolnshire,E06000012,3,87000,283000 +North East Lincolnshire,E06000012,4,161000,562000 +North Hertfordshire,E07000099,1,108000,528000 +North Hertfordshire,E07000099,2,157000,528000 +North Hertfordshire,E07000099,3,318000,752000 +North Hertfordshire,E07000099,4,453000,1558000 +North Kesteven,E07000139,1,85000,339000 +North Kesteven,E07000139,2,85000,339000 +North Kesteven,E07000139,3,149000,367000 +North Kesteven,E07000139,4,250000,634000 +North Lincolnshire,E06000013,1,71000,291000 +North Lincolnshire,E06000013,2,84000,291000 +North Lincolnshire,E06000013,3,100000,305000 +North Lincolnshire,E06000013,4,186000,560000 +North Norfolk,E07000147,1,106000,403000 +North Norfolk,E07000147,2,136000,403000 +North Norfolk,E07000147,3,176000,507000 +North Norfolk,E07000147,4,251000,1072000 +North Northamptonshire,E06000061,1,79000,330000 +North Northamptonshire,E06000061,2,95000,330000 +North Northamptonshire,E06000061,3,170000,407000 +North Northamptonshire,E06000061,4,270000,782000 +North Somerset,E06000024,1,108000,430000 +North Somerset,E06000024,2,145000,430000 +North Somerset,E06000024,3,222000,588000 +North Somerset,E06000024,4,306000,1105000 +North Tyneside,E08000022,1,69000,263000 +North Tyneside,E08000022,2,69000,263000 +North Tyneside,E08000022,3,91000,373000 +North Tyneside,E08000022,4,205000,619000 +North Warwickshire,E07000218,1,112000,369000 +North Warwickshire,E07000218,2,118000,369000 +North Warwickshire,E07000218,3,153000,434000 +North Warwickshire,E07000218,4,260000,968000 +North West Leicestershire,E07000134,1,103000,315000 +North West Leicestershire,E07000134,2,103000,315000 +North West Leicestershire,E07000134,3,130000,398000 +North West Leicestershire,E07000134,4,271000,733000 +North Yorkshire,E06000065,1,81000,384000 +North Yorkshire,E06000065,2,108000,384000 +North Yorkshire,E06000065,3,143000,496000 +North Yorkshire,E06000065,4,234000,1005000 +Northumberland,E06000057,1,62000,333000 +Northumberland,E06000057,2,64000,333000 +Northumberland,E06000057,3,95000,376000 +Northumberland,E06000057,4,188000,790000 +Norwich,E07000148,1,83000,263000 +Norwich,E07000148,2,83000,346000 +Norwich,E07000148,3,149000,466000 +Norwich,E07000148,4,230000,960000 +Nottingham,E06000018,1,72000,304000 +Nottingham,E06000018,2,79000,304000 +Nottingham,E06000018,3,89000,350000 +Nottingham,E06000018,4,165000,761000 +Nuneaton and Bedworth,E07000219,1,95000,266000 +Nuneaton and Bedworth,E07000219,2,98000,266000 +Nuneaton and Bedworth,E07000219,3,142000,356000 +Nuneaton and Bedworth,E07000219,4,225000,637000 +Oadby and Wigston,E07000135,1,90000,303000 +Oadby and Wigston,E07000135,2,125000,303000 +Oadby and Wigston,E07000135,3,188000,401000 +Oadby and Wigston,E07000135,4,295000,911000 +Oldham,E08000004,1,85000,262000 +Oldham,E08000004,2,85000,262000 +Oldham,E08000004,3,107000,357000 +Oldham,E08000004,4,160000,789000 +Oxford,E07000178,1,126000,656000 +Oxford,E07000178,2,198000,707000 +Oxford,E07000178,3,328000,794000 +Oxford,E07000178,4,440000,2267000 +Pendle,E07000122,1,59000,251000 +Pendle,E07000122,2,60000,251000 +Pendle,E07000122,3,81000,366000 +Pendle,E07000122,4,155000,700000 +Peterborough,E06000031,1,75000,282000 +Peterborough,E06000031,2,89000,282000 +Peterborough,E06000031,3,152000,337000 +Peterborough,E06000031,4,223000,697000 +Plymouth,E06000026,1,91000,320000 +Plymouth,E06000026,2,95000,320000 +Plymouth,E06000026,3,148000,370000 +Plymouth,E06000026,4,218000,635000 +Portsmouth,E06000044,1,107000,332000 +Portsmouth,E06000044,2,145000,341000 +Portsmouth,E06000044,3,202000,451000 +Portsmouth,E06000044,4,276000,875000 +Preston,E07000123,1,47000,253000 +Preston,E07000123,2,82000,253000 +Preston,E07000123,3,106000,329000 +Preston,E07000123,4,197000,649000 +Reading,E06000038,1,141000,434000 +Reading,E06000038,2,228000,471000 +Reading,E06000038,3,319000,662000 +Reading,E06000038,4,428000,1149000 +Redbridge,E09000026,1,102000,497000 +Redbridge,E09000026,2,162000,639000 +Redbridge,E09000026,3,355000,826000 +Redbridge,E09000026,4,488000,1329000 +Redcar and Cleveland,E06000003,1,62000,244000 +Redcar and Cleveland,E06000003,2,62000,244000 +Redcar and Cleveland,E06000003,3,82000,273000 +Redcar and Cleveland,E06000003,4,158000,482000 +Redditch,E07000236,1,57000,296000 +Redditch,E07000236,2,102000,296000 +Redditch,E07000236,3,157000,389000 +Redditch,E07000236,4,258000,627000 +Reigate and Banstead,E07000211,1,154000,574000 +Reigate and Banstead,E07000211,2,154000,574000 +Reigate and Banstead,E07000211,3,383000,864000 +Reigate and Banstead,E07000211,4,522000,1872000 +Ribble Valley,E07000124,1,116000,350000 +Ribble Valley,E07000124,2,116000,350000 +Ribble Valley,E07000124,3,131000,483000 +Ribble Valley,E07000124,4,282000,935000 +Richmond upon Thames,E09000027,1,218000,798000 +Richmond upon Thames,E09000027,2,346000,978000 +Richmond upon Thames,E09000027,3,481000,1355000 +Richmond upon Thames,E09000027,4,686000,2642000 +Rochdale,E08000005,1,73000,224000 +Rochdale,E08000005,2,75000,224000 +Rochdale,E08000005,3,102000,316000 +Rochdale,E08000005,4,174000,594000 +Rochford,E07000075,1,147000,494000 +Rochford,E07000075,2,237000,494000 +Rochford,E07000075,3,320000,585000 +Rochford,E07000075,4,406000,1011000 +Rossendale,E07000125,1,64000,283000 +Rossendale,E07000125,2,77000,283000 +Rossendale,E07000125,3,108000,383000 +Rossendale,E07000125,4,188000,736000 +Rother,E07000064,1,104000,406000 +Rother,E07000064,2,151000,456000 +Rother,E07000064,3,233000,628000 +Rother,E07000064,4,350000,1165000 +Rotherham,E08000018,1,69000,246000 +Rotherham,E08000018,2,73000,246000 +Rotherham,E08000018,3,90000,305000 +Rotherham,E08000018,4,174000,574000 +Rugby,E07000220,1,109000,343000 +Rugby,E07000220,2,109000,343000 +Rugby,E07000220,3,184000,441000 +Rugby,E07000220,4,314000,835000 +Runnymede,E07000212,1,143000,543000 +Runnymede,E07000212,2,177000,582000 +Runnymede,E07000212,3,398000,819000 +Runnymede,E07000212,4,537000,1644000 +Rushcliffe,E07000176,1,99000,512000 +Rushcliffe,E07000176,2,99000,512000 +Rushcliffe,E07000176,3,178000,528000 +Rushcliffe,E07000176,4,329000,950000 +Rushmoor,E07000092,1,107000,413000 +Rushmoor,E07000092,2,183000,413000 +Rushmoor,E07000092,3,317000,555000 +Rushmoor,E07000092,4,433000,831000 +Rutland,E06000017,1,89000,390000 +Rutland,E06000017,2,89000,452000 +Rutland,E06000017,3,191000,615000 +Rutland,E06000017,4,333000,1262000 +Salford,E08000006,1,94000,355000 +Salford,E08000006,2,105000,355000 +Salford,E08000006,3,131000,396000 +Salford,E08000006,4,210000,784000 +Sandwell,E08000028,1,72000,241000 +Sandwell,E08000028,2,96000,241000 +Sandwell,E08000028,3,125000,323000 +Sandwell,E08000028,4,185000,465000 +Sefton,E08000014,1,78000,361000 +Sefton,E08000014,2,86000,361000 +Sefton,E08000014,3,105000,376000 +Sefton,E08000014,4,182000,841000 +Sevenoaks,E07000111,1,186000,677000 +Sevenoaks,E07000111,2,186000,677000 +Sevenoaks,E07000111,3,327000,940000 +Sevenoaks,E07000111,4,470000,2260000 +Sheffield,E08000019,1,76000,318000 +Sheffield,E08000019,2,79000,318000 +Sheffield,E08000019,3,108000,423000 +Sheffield,E08000019,4,211000,921000 +Shropshire,E06000051,1,85000,407000 +Shropshire,E06000051,2,108000,407000 +Shropshire,E06000051,3,164000,465000 +Shropshire,E06000051,4,252000,849000 +Slough,E06000039,1,163000,446000 +Slough,E06000039,2,224000,462000 +Slough,E06000039,3,352000,685000 +Slough,E06000039,4,432000,1004000 +Solihull,E08000029,1,93000,516000 +Solihull,E08000029,2,124000,516000 +Solihull,E08000029,3,162000,571000 +Solihull,E08000029,4,343000,1276000 +Somerset,E06000066,1,97000,376000 +Somerset,E06000066,2,120000,376000 +Somerset,E06000066,3,181000,469000 +Somerset,E06000066,4,278000,917000 +South Cambridgeshire,E07000012,1,137000,510000 +South Cambridgeshire,E07000012,2,164000,510000 +South Cambridgeshire,E07000012,3,271000,685000 +South Cambridgeshire,E07000012,4,403000,1219000 +South Derbyshire,E07000039,1,93000,330000 +South Derbyshire,E07000039,2,93000,330000 +South Derbyshire,E07000039,3,150000,380000 +South Derbyshire,E07000039,4,257000,747000 +South Gloucestershire,E06000025,1,95000,395000 +South Gloucestershire,E06000025,2,143000,395000 +South Gloucestershire,E06000025,3,263000,510000 +South Gloucestershire,E06000025,4,387000,890000 +South Hams,E07000044,1,106000,503000 +South Hams,E07000044,2,106000,503000 +South Hams,E07000044,3,207000,696000 +South Hams,E07000044,4,335000,1182000 +South Holland,E07000140,1,69000,278000 +South Holland,E07000140,2,69000,278000 +South Holland,E07000140,3,138000,367000 +South Holland,E07000140,4,238000,622000 +South Kesteven,E07000141,1,85000,358000 +South Kesteven,E07000141,2,92000,358000 +South Kesteven,E07000141,3,143000,448000 +South Kesteven,E07000141,4,250000,903000 +South Norfolk,E07000149,1,107000,364000 +South Norfolk,E07000149,2,107000,364000 +South Norfolk,E07000149,3,214000,482000 +South Norfolk,E07000149,4,295000,810000 +South Oxfordshire,E07000179,1,152000,615000 +South Oxfordshire,E07000179,2,152000,615000 +South Oxfordshire,E07000179,3,290000,784000 +South Oxfordshire,E07000179,4,440000,1898000 +South Ribble,E07000126,1,96000,282000 +South Ribble,E07000126,2,96000,282000 +South Ribble,E07000126,3,136000,337000 +South Ribble,E07000126,4,226000,640000 +South Staffordshire,E07000196,1,90000,369000 +South Staffordshire,E07000196,2,98000,369000 +South Staffordshire,E07000196,3,169000,475000 +South Staffordshire,E07000196,4,258000,850000 +South Tyneside,E08000023,1,58000,248000 +South Tyneside,E08000023,2,67000,248000 +South Tyneside,E08000023,3,80000,324000 +South Tyneside,E08000023,4,162000,624000 +Southampton,E06000045,1,80000,254000 +Southampton,E06000045,2,149000,348000 +Southampton,E06000045,3,216000,449000 +Southampton,E06000045,4,265000,794000 +Southend-on-Sea,E06000033,1,128000,451000 +Southend-on-Sea,E06000033,2,154000,503000 +Southend-on-Sea,E06000033,3,277000,658000 +Southend-on-Sea,E06000033,4,376000,1133000 +Southwark,E09000028,1,144000,676000 +Southwark,E09000028,2,185000,928000 +Southwark,E09000028,3,342000,1257000 +Southwark,E09000028,4,574000,2501000 +Spelthorne,E07000213,1,227000,560000 +Spelthorne,E07000213,2,293000,580000 +Spelthorne,E07000213,3,392000,768000 +Spelthorne,E07000213,4,524000,1262000 +St Albans,E07000240,1,194000,818000 +St Albans,E07000240,2,303000,818000 +St Albans,E07000240,3,443000,1077000 +St Albans,E07000240,4,679000,2067000 +St. Helens,E08000013,1,76000,226000 +St. Helens,E08000013,2,78000,226000 +St. Helens,E08000013,3,97000,315000 +St. Helens,E08000013,4,174000,628000 +Stafford,E07000197,1,76000,360000 +Stafford,E07000197,2,87000,360000 +Stafford,E07000197,3,150000,432000 +Stafford,E07000197,4,257000,736000 +Staffordshire Moorlands,E07000198,1,96000,347000 +Staffordshire Moorlands,E07000198,2,109000,347000 +Staffordshire Moorlands,E07000198,3,141000,442000 +Staffordshire Moorlands,E07000198,4,216000,826000 +Stevenage,E07000243,1,88000,392000 +Stevenage,E07000243,2,109000,392000 +Stevenage,E07000243,3,297000,504000 +Stevenage,E07000243,4,328000,848000 +Stockport,E08000007,1,95000,396000 +Stockport,E08000007,2,100000,396000 +Stockport,E08000007,3,174000,527000 +Stockport,E08000007,4,302000,1084000 +Stockton-on-Tees,E06000004,1,59000,245000 +Stockton-on-Tees,E06000004,2,59000,245000 +Stockton-on-Tees,E06000004,3,86000,302000 +Stockton-on-Tees,E06000004,4,178000,643000 +Stoke-on-Trent,E06000021,1,71000,197000 +Stoke-on-Trent,E06000021,2,72000,197000 +Stoke-on-Trent,E06000021,3,85000,287000 +Stoke-on-Trent,E06000021,4,178000,540000 +Stratford-on-Avon,E07000221,1,112000,487000 +Stratford-on-Avon,E07000221,2,112000,487000 +Stratford-on-Avon,E07000221,3,198000,649000 +Stratford-on-Avon,E07000221,4,384000,1338000 +Stroud,E07000082,1,99000,436000 +Stroud,E07000082,2,121000,436000 +Stroud,E07000082,3,220000,589000 +Stroud,E07000082,4,330000,1123000 +Sunderland,E08000024,1,60000,213000 +Sunderland,E08000024,2,66000,213000 +Sunderland,E08000024,3,81000,282000 +Sunderland,E08000024,4,139000,514000 +Surrey Heath,E07000214,1,160000,574000 +Surrey Heath,E07000214,2,160000,574000 +Surrey Heath,E07000214,3,366000,731000 +Surrey Heath,E07000214,4,528000,1387000 +Sutton,E09000029,1,128000,443000 +Sutton,E09000029,2,236000,495000 +Sutton,E09000029,3,362000,692000 +Sutton,E09000029,4,506000,1252000 +Swale,E07000113,1,116000,371000 +Swale,E07000113,2,121000,371000 +Swale,E07000113,3,207000,487000 +Swale,E07000113,4,318000,943000 +Swindon,E06000030,1,114000,317000 +Swindon,E06000030,2,118000,317000 +Swindon,E06000030,3,208000,433000 +Swindon,E06000030,4,322000,768000 +Tameside,E08000008,1,98000,237000 +Tameside,E08000008,2,110000,237000 +Tameside,E08000008,3,139000,336000 +Tameside,E08000008,4,209000,679000 +Tamworth,E07000199,1,102000,269000 +Tamworth,E07000199,2,116000,269000 +Tamworth,E07000199,3,159000,381000 +Tamworth,E07000199,4,243000,609000 +Tandridge,E07000215,1,189000,601000 +Tandridge,E07000215,2,225000,601000 +Tandridge,E07000215,3,392000,876000 +Tandridge,E07000215,4,532000,1958000 +Teignbridge,E07000045,1,98000,419000 +Teignbridge,E07000045,2,112000,419000 +Teignbridge,E07000045,3,208000,511000 +Teignbridge,E07000045,4,302000,952000 +Telford and Wrekin,E06000020,1,86000,260000 +Telford and Wrekin,E06000020,2,96000,260000 +Telford and Wrekin,E06000020,3,118000,332000 +Telford and Wrekin,E06000020,4,210000,618000 +Tendring,E07000076,1,82000,360000 +Tendring,E07000076,2,137000,360000 +Tendring,E07000076,3,184000,471000 +Tendring,E07000076,4,281000,915000 +Test Valley,E07000093,1,112000,503000 +Test Valley,E07000093,2,114000,511000 +Test Valley,E07000093,3,216000,577000 +Test Valley,E07000093,4,357000,1156000 +Tewkesbury,E07000083,1,72000,365000 +Tewkesbury,E07000083,2,102000,365000 +Tewkesbury,E07000083,3,155000,510000 +Tewkesbury,E07000083,4,352000,928000 +Thanet,E07000114,1,101000,377000 +Thanet,E07000114,2,153000,377000 +Thanet,E07000114,3,217000,484000 +Thanet,E07000114,4,292000,814000 +Three Rivers,E07000102,1,215000,574000 +Three Rivers,E07000102,2,287000,772000 +Three Rivers,E07000102,3,423000,1000000 +Three Rivers,E07000102,4,612000,2138000 +Thurrock,E06000034,1,134000,400000 +Thurrock,E06000034,2,176000,400000 +Thurrock,E06000034,3,277000,513000 +Thurrock,E06000034,4,377000,804000 +Tonbridge and Malling,E07000115,1,123000,472000 +Tonbridge and Malling,E07000115,2,133000,472000 +Tonbridge and Malling,E07000115,3,293000,691000 +Tonbridge and Malling,E07000115,4,439000,1315000 +Torbay,E06000027,1,92000,383000 +Torbay,E06000027,2,120000,383000 +Torbay,E06000027,3,174000,443000 +Torbay,E06000027,4,235000,779000 +Torridge,E07000046,1,110000,372000 +Torridge,E07000046,2,136000,372000 +Torridge,E07000046,3,181000,513000 +Torridge,E07000046,4,249000,807000 +Tower Hamlets,E09000030,1,153000,616000 +Tower Hamlets,E09000030,2,204000,872000 +Tower Hamlets,E09000030,3,261000,1335000 +Tower Hamlets,E09000030,4,448000,1896000 +Trafford,E08000009,1,102000,518000 +Trafford,E08000009,2,151000,518000 +Trafford,E08000009,3,212000,619000 +Trafford,E08000009,4,346000,1630000 +Tunbridge Wells,E07000116,1,102000,578000 +Tunbridge Wells,E07000116,2,143000,578000 +Tunbridge Wells,E07000116,3,326000,843000 +Tunbridge Wells,E07000116,4,490000,1729000 +Uttlesford,E07000077,1,83000,543000 +Uttlesford,E07000077,2,141000,543000 +Uttlesford,E07000077,3,266000,687000 +Uttlesford,E07000077,4,476000,1497000 +Vale of White Horse,E07000180,1,98000,473000 +Vale of White Horse,E07000180,2,132000,473000 +Vale of White Horse,E07000180,3,274000,585000 +Vale of White Horse,E07000180,4,409000,1157000 +Wakefield,E08000036,1,74000,233000 +Wakefield,E08000036,2,88000,233000 +Wakefield,E08000036,3,107000,322000 +Wakefield,E08000036,4,192000,605000 +Walsall,E08000030,1,79000,275000 +Walsall,E08000030,2,99000,275000 +Walsall,E08000030,3,121000,383000 +Walsall,E08000030,4,204000,853000 +Waltham Forest,E09000031,1,146000,538000 +Waltham Forest,E09000031,2,256000,643000 +Waltham Forest,E09000031,3,401000,821000 +Waltham Forest,E09000031,4,510000,1068000 +Wandsworth,E09000032,1,172000,715000 +Wandsworth,E09000032,2,284000,973000 +Wandsworth,E09000032,3,459000,1399000 +Wandsworth,E09000032,4,802000,2725000 +Warrington,E06000007,1,92000,361000 +Warrington,E06000007,2,108000,361000 +Warrington,E06000007,3,134000,449000 +Warrington,E06000007,4,251000,946000 +Warwick,E07000222,1,104000,473000 +Warwick,E07000222,2,139000,473000 +Warwick,E07000222,3,265000,647000 +Warwick,E07000222,4,421000,1253000 +Watford,E07000103,1,198000,516000 +Watford,E07000103,2,281000,547000 +Watford,E07000103,3,402000,849000 +Watford,E07000103,4,520000,1495000 +Waverley,E07000216,1,162000,723000 +Waverley,E07000216,2,206000,723000 +Waverley,E07000216,3,382000,929000 +Waverley,E07000216,4,565000,2204000 +Wealden,E07000065,1,129000,521000 +Wealden,E07000065,2,174000,521000 +Wealden,E07000065,3,264000,691000 +Wealden,E07000065,4,390000,1515000 +Welwyn Hatfield,E07000241,1,104000,482000 +Welwyn Hatfield,E07000241,2,149000,590000 +Welwyn Hatfield,E07000241,3,331000,824000 +Welwyn Hatfield,E07000241,4,493000,1714000 +West Berkshire,E06000037,1,97000,515000 +West Berkshire,E06000037,2,136000,515000 +West Berkshire,E06000037,3,312000,696000 +West Berkshire,E06000037,4,452000,1531000 +West Devon,E07000047,1,95000,421000 +West Devon,E07000047,2,145000,421000 +West Devon,E07000047,3,205000,527000 +West Devon,E07000047,4,259000,1018000 +West Lancashire,E07000127,1,57000,411000 +West Lancashire,E07000127,2,81000,411000 +West Lancashire,E07000127,3,81000,432000 +West Lancashire,E07000127,4,133000,880000 +West Lindsey,E07000142,1,78000,319000 +West Lindsey,E07000142,2,78000,319000 +West Lindsey,E07000142,3,110000,372000 +West Lindsey,E07000142,4,195000,691000 +West Northamptonshire,E06000062,1,59000,371000 +West Northamptonshire,E06000062,2,99000,371000 +West Northamptonshire,E06000062,3,185000,477000 +West Northamptonshire,E06000062,4,320000,947000 +West Oxfordshire,E07000181,1,119000,483000 +West Oxfordshire,E07000181,2,122000,483000 +West Oxfordshire,E07000181,3,279000,636000 +West Oxfordshire,E07000181,4,377000,1202000 +West Suffolk,E07000245,1,104000,367000 +West Suffolk,E07000245,2,126000,367000 +West Suffolk,E07000245,3,192000,475000 +West Suffolk,E07000245,4,278000,893000 +Westminster,E09000033,1,281000,1026000 +Westminster,E09000033,2,478000,2045000 +Westminster,E09000033,3,560000,3377000 +Westminster,E09000033,4,869000,11513000 +Westmorland,E06000064,1,81000,346000 +Westmorland,E06000064,2,81000,346000 +Westmorland,E06000064,3,114000,458000 +Westmorland,E06000064,4,195000,770000 +Wigan,E08000010,1,81000,243000 +Wigan,E08000010,2,81000,243000 +Wigan,E08000010,3,99000,318000 +Wigan,E08000010,4,197000,530000 +Wiltshire,E06000054,1,105000,438000 +Wiltshire,E06000054,2,120000,438000 +Wiltshire,E06000054,3,225000,565000 +Wiltshire,E06000054,4,341000,1139000 +Winchester,E07000094,1,124000,624000 +Winchester,E07000094,2,138000,660000 +Winchester,E07000094,3,298000,893000 +Winchester,E07000094,4,458000,1615000 +Windsor and Maidenhead,E06000040,1,228000,695000 +Windsor and Maidenhead,E06000040,2,307000,695000 +Windsor and Maidenhead,E06000040,3,422000,974000 +Windsor and Maidenhead,E06000040,4,597000,2059000 +Wirral,E08000015,1,77000,350000 +Wirral,E08000015,2,89000,350000 +Wirral,E08000015,3,112000,375000 +Wirral,E08000015,4,161000,818000 +Woking,E07000217,1,191000,570000 +Woking,E07000217,2,279000,570000 +Woking,E07000217,3,393000,875000 +Woking,E07000217,4,538000,1750000 +Wokingham,E06000041,1,140000,546000 +Wokingham,E06000041,2,151000,546000 +Wokingham,E06000041,3,290000,686000 +Wokingham,E06000041,4,518000,1299000 +Wolverhampton,E08000031,1,43000,249000 +Wolverhampton,E08000031,2,69000,249000 +Wolverhampton,E08000031,3,101000,327000 +Wolverhampton,E08000031,4,170000,703000 +Worcester,E07000237,1,90000,309000 +Worcester,E07000237,2,138000,309000 +Worcester,E07000237,3,190000,423000 +Worcester,E07000237,4,290000,793000 +Worthing,E07000229,1,148000,460000 +Worthing,E07000229,2,188000,465000 +Worthing,E07000229,3,297000,586000 +Worthing,E07000229,4,393000,864000 +Wychavon,E07000238,1,100000,432000 +Wychavon,E07000238,2,100000,452000 +Wychavon,E07000238,3,167000,546000 +Wychavon,E07000238,4,320000,969000 +Wyre,E07000128,1,72000,328000 +Wyre,E07000128,2,87000,328000 +Wyre,E07000128,3,114000,381000 +Wyre,E07000128,4,199000,648000 +Wyre Forest,E07000239,1,93000,358000 +Wyre Forest,E07000239,2,100000,358000 +Wyre Forest,E07000239,3,146000,398000 +Wyre Forest,E07000239,4,231000,825000 +York,E06000014,1,105000,414000 +York,E06000014,2,173000,414000 +York,E06000014,3,220000,529000 +York,E06000014,4,322000,983000 From ef0fc992e79d5712ea8ebc13a2e159747e9497c6 Mon Sep 17 00:00:00 2001 From: natdeanlewissoftwire <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Thu, 16 Feb 2023 10:18:37 +0000 Subject: [PATCH 12/30] CLDC-1906 infer offered (#1298) * feat: infer offered if renewal * feat: update test * db:update --- app/models/derived_variables/lettings_log_variables.rb | 1 + db/schema.rb | 2 +- spec/models/lettings_log_spec.rb | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index 96b937ab4..526963d9c 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -45,6 +45,7 @@ module DerivedVariables::LettingsLogVariables self.underoccupation_benefitcap = 2 if collection_start_year == 2021 self.referral = 1 self.waityear = 2 + self.offered = 0 if is_general_needs? # fixed term self.prevten = 32 if managing_organisation&.provider_type == "PRP" diff --git a/db/schema.rb b/db/schema.rb index ffea32c01..2c1ff7692 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -524,8 +524,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_10_143120) do t.integer "details_known_5" t.integer "details_known_6" t.integer "saledate_check" - t.integer "staircasesale" t.integer "prevshared" + t.integer "staircasesale" t.integer "ethnic_group2" t.integer "ethnicbuy2" t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id" diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index c8cdc466e..abe3057a6 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -1946,12 +1946,12 @@ RSpec.describe LettingsLog do end context "when a non select question associated with several pages is routed to" do - let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, period: 2) } + let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, period: 2, needstype: 1) } it "does not clear the answer value" do - lettings_log.update!({ offered: 4 }) + lettings_log.update!({ unitletas: 1 }) lettings_log.reload - expect(lettings_log.offered).to eq(4) + expect(lettings_log.unitletas).to eq(1) end end From b973e8348e4e013350186264eb2778a919cbdf5d Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Thu, 16 Feb 2023 12:16:39 +0000 Subject: [PATCH 13/30] CLDC-1861 Update options order for lead tenant's working situation (#1289) * Switch the first two options so Full time comes before Part time * Enable dates validations on staging --- app/models/form/lettings/questions/working_situation1.rb | 2 +- config/initializers/feature_toggle.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/form/lettings/questions/working_situation1.rb b/app/models/form/lettings/questions/working_situation1.rb index 866271f4d..20f5dc984 100644 --- a/app/models/form/lettings/questions/working_situation1.rb +++ b/app/models/form/lettings/questions/working_situation1.rb @@ -11,8 +11,8 @@ class Form::Lettings::Questions::WorkingSituation1 < ::Form::Question end ANSWER_OPTIONS = { - "2" => { "value" => "Part-time – Less than 30 hours" }, "1" => { "value" => "Full-time – 30 hours or more" }, + "2" => { "value" => "Part-time – Less than 30 hours" }, "7" => { "value" => "Full-time student" }, "3" => { "value" => "In government training into work, such as New Deal" }, "4" => { "value" => "Jobseeker" }, diff --git a/config/initializers/feature_toggle.rb b/config/initializers/feature_toggle.rb index 37f6aa653..d31ee184b 100644 --- a/config/initializers/feature_toggle.rb +++ b/config/initializers/feature_toggle.rb @@ -1,14 +1,14 @@ class FeatureToggle def self.startdate_two_week_validation_enabled? - Rails.env.production? || Rails.env.test? + Rails.env.production? || Rails.env.test? || Rails.env.staging? end def self.startdate_collection_window_validation_enabled? - Rails.env.production? || Rails.env.test? + Rails.env.production? || Rails.env.test? || Rails.env.staging? end def self.saledate_collection_window_validation_enabled? - Rails.env.production? || Rails.env.test? + Rails.env.production? || Rails.env.test? || Rails.env.staging? end def self.sales_log_enabled? From 293077695ca5e10077915681664c6f61a2e5ebd8 Mon Sep 17 00:00:00 2001 From: James Rose Date: Thu, 16 Feb 2023 12:32:19 +0000 Subject: [PATCH 14/30] Allow issues/pull-request write permissions for GitHub token (#1310) --- .github/workflows/review_pipeline.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/review_pipeline.yml b/.github/workflows/review_pipeline.yml index 1e7ae0efb..5656898bc 100644 --- a/.github/workflows/review_pipeline.yml +++ b/.github/workflows/review_pipeline.yml @@ -72,6 +72,9 @@ jobs: runs-on: ubuntu-latest environment: staging needs: [postgres, redis] + permissions: + issues: write + pull-requests: write steps: - name: Checkout code From d8689c30a2bc493167602ce5c5cbf755b336d758 Mon Sep 17 00:00:00 2001 From: Jack <113976590+bibblobcode@users.noreply.github.com> Date: Thu, 16 Feb 2023 12:33:58 +0000 Subject: [PATCH 15/30] CLDC-1968 Add review page for Sales logs (#1292) * CLDC-1968 Add review page for Sales logs * use log#sales? * Do not use type checking --- Gemfile | 2 +- app/helpers/tasklist_helper.rb | 4 ++- app/models/lettings_log.rb | 12 +++++--- app/models/log.rb | 4 +++ app/models/sales_log.rb | 8 ++++++ app/views/form/review.html.erb | 40 ++++++++++++++++----------- config/routes.rb | 4 +++ spec/helpers/tasklist_helper_spec.rb | 2 +- spec/models/lettings_log_spec.rb | 5 ++++ spec/models/sales_log_spec.rb | 7 ++++- spec/requests/form_controller_spec.rb | 6 ++++ 11 files changed, 70 insertions(+), 24 deletions(-) diff --git a/Gemfile b/Gemfile index b20b7fe97..f77a70899 100644 --- a/Gemfile +++ b/Gemfile @@ -68,6 +68,7 @@ group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem "byebug", platforms: %i[mri mingw x64_mingw] gem "dotenv-rails" + gem "factory_bot_rails" gem "pry-byebug" gem "parallel_tests" @@ -90,7 +91,6 @@ end group :test do gem "capybara", require: false gem "capybara-lockstep" - gem "factory_bot_rails" gem "faker" gem "rspec-rails", require: false gem "selenium-webdriver", require: false diff --git a/app/helpers/tasklist_helper.rb b/app/helpers/tasklist_helper.rb index f297de2ff..f4f1d51dd 100644 --- a/app/helpers/tasklist_helper.rb +++ b/app/helpers/tasklist_helper.rb @@ -39,7 +39,9 @@ module TasklistHelper def review_log_text(log) if log.collection_period_open? - "You can #{govuk_link_to 'review and make changes to this log', review_lettings_log_path(log)} until #{log.form.end_date.to_formatted_s(:govuk_date)}.".html_safe + link = log.sales? ? review_sales_log_path(id: log, sales_log: true) : review_lettings_log_path(log) + + "You can #{govuk_link_to 'review and make changes to this log', link} until #{log.form.end_date.to_formatted_s(:govuk_date)}.".html_safe else "This log is from the #{log.form.start_date.year}/#{log.form.start_date.year + 1} collection window, which is now closed." end diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 66f622270..02236f70b 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -70,6 +70,14 @@ class LettingsLog < Log collection_start_year end + def lettings? + true + end + + def sales? + false + end + def form_name return unless startdate @@ -481,10 +489,6 @@ class LettingsLog < Log location.type_of_unit_before_type_cast if location end - def lettings? - true - end - def rent_type_detail form.get_question("rent_type", self)&.label_from_value(rent_type) end diff --git a/app/models/log.rb b/app/models/log.rb index 1b43fd1cb..0cd3add92 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -43,6 +43,10 @@ class Log < ApplicationRecord false end + def sales? + false + end + def ethnic_refused? ethnic_group == 17 end diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index add706f7c..6ed653955 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -38,6 +38,14 @@ class SalesLog < Log OPTIONAL_FIELDS = %w[saledate_check purchid monthly_charges_value_check old_persons_shared_ownership_value_check].freeze RETIREMENT_AGES = { "M" => 65, "F" => 60, "X" => 65 }.freeze + def lettings? + false + end + + def sales? + true + end + def startdate saledate end diff --git a/app/views/form/review.html.erb b/app/views/form/review.html.erb index a95b577bd..bc46c89d8 100644 --- a/app/views/form/review.html.erb +++ b/app/views/form/review.html.erb @@ -1,9 +1,18 @@ -<% content_for :title, "Review lettings log" %> -<% content_for :breadcrumbs, govuk_breadcrumbs(breadcrumbs: { - "Logs" => "/logs", - "Log #{@log.id}" => "/lettings-logs/#{@log.id}", - "Review lettings log" => "", -}) %> +<% if @log.sales? %> + <% content_for :title, "Review sales log" %> + <% content_for :breadcrumbs, govuk_breadcrumbs(breadcrumbs: { + "Logs" => "/logs", + "Log #{@log.id}" => "/sales-logs/#{@log.id}", + "Review sales log" => "", + }) %> +<% else %> + <% content_for :title, "Review lettings log" %> + <% content_for :breadcrumbs, govuk_breadcrumbs(breadcrumbs: { + "Logs" => "/logs", + "Log #{@log.id}" => "/lettings-logs/#{@log.id}", + "Review lettings log" => "", + }) %> +<% end %>
@@ -16,17 +25,16 @@ <% @log.form.sections.map do |section| %>

<%= section.label %>

<% section.subsections.map do |subsection| %> -
-
-

<%= subsection.label %>

+ <% if total_applicable_questions(subsection, @log, current_user).any? %> +
+
+

<%= subsection.label %>

+
+
+ <%= render partial: "form/check_answers_summary_list", locals: { subsection: } %> +
-
- <%= render partial: "form/check_answers_summary_list", locals: { - subsection:, - lettings_log: @log, - } %> -
-
+ <% end %> <% end %> <% end %>
diff --git a/config/routes.rb b/config/routes.rb index be765c8c0..74b613989 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -171,6 +171,10 @@ Rails.application.routes.draw do resources :bulk_upload_sales_results, path: "bulk-upload-results", only: [:show] end + member do + get "review", to: "form#review" + 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/spec/helpers/tasklist_helper_spec.rb b/spec/helpers/tasklist_helper_spec.rb index f7726745d..e14e06ea4 100644 --- a/spec/helpers/tasklist_helper_spec.rb +++ b/spec/helpers/tasklist_helper_spec.rb @@ -136,7 +136,7 @@ RSpec.describe TasklistHelper do it "returns relevant text" do expect(review_log_text(sales_log)).to eq( - "You can #{govuk_link_to 'review and make changes to this log', review_lettings_log_path(sales_log)} until 1 July 2023.".html_safe, + "You can #{govuk_link_to 'review and make changes to this log', review_sales_log_path(id: sales_log, sales_log: true)} until 1 July 2023.".html_safe, ) end end diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index abe3057a6..2aee5ddcf 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -18,6 +18,11 @@ RSpec.describe LettingsLog do expect(described_class).to be < ApplicationRecord end + it "is a not a sales log" do + lettings_log = FactoryBot.build(:lettings_log, created_by: created_by_user) + expect(lettings_log.sales?).to be false + end + it "is a lettings log" do lettings_log = FactoryBot.build(:lettings_log, created_by: created_by_user) expect(lettings_log).to be_lettings diff --git a/spec/models/sales_log_spec.rb b/spec/models/sales_log_spec.rb index 42bf9d4cd..523c2924b 100644 --- a/spec/models/sales_log_spec.rb +++ b/spec/models/sales_log_spec.rb @@ -12,11 +12,16 @@ RSpec.describe SalesLog, type: :model do expect(described_class).to be < ApplicationRecord end - it "is a sales log" do + it "is a not a lettings log" do sales_log = build(:sales_log, created_by: created_by_user) expect(sales_log.lettings?).to be false end + it "is a sales log" do + sales_log = build(:sales_log, created_by: created_by_user) + expect(sales_log.sales?).to be true + end + describe "#new" do context "when creating a record" do let(:sales_log) do diff --git a/spec/requests/form_controller_spec.rb b/spec/requests/form_controller_spec.rb index dcf67cb71..d5a8dbcf8 100644 --- a/spec/requests/form_controller_spec.rb +++ b/spec/requests/form_controller_spec.rb @@ -314,6 +314,12 @@ RSpec.describe FormController, type: :request do get "/lettings-logs/#{setup_complete_lettings_log.id}/review", headers: headers, params: {} expect(response.body).to match("Review lettings log") end + + it "renders the review page for the sales log" do + log = create(:sales_log, :completed, created_by: user) + get "/sales-logs/#{log.id}/review", headers: headers, params: { sales_log: true } + expect(response.body).to match("Review sales log") + end end context "when viewing a user dependent page" do From 65bde2b7f55a2f56f7442642f68f033a261e000f Mon Sep 17 00:00:00 2001 From: Jack <113976590+bibblobcode@users.noreply.github.com> Date: Thu, 16 Feb 2023 12:34:22 +0000 Subject: [PATCH 16/30] CLDC-1903 Infer first time property let question (#1300) --- .../lettings_log_variables.rb | 3 ++- lib/tasks/data_export.rake | 18 ------------------ spec/models/lettings_log_spec.rb | 9 +++++++++ 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index 526963d9c..7d664e48f 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -46,6 +46,8 @@ module DerivedVariables::LettingsLogVariables self.referral = 1 self.waityear = 2 self.offered = 0 + self.voiddate = startdate + self.first_time_property_let_as_social_housing = 0 if is_general_needs? # fixed term self.prevten = 32 if managing_organisation&.provider_type == "PRP" @@ -61,7 +63,6 @@ module DerivedVariables::LettingsLogVariables if is_supported_housing? && location self.wchair = location.mobility_type_before_type_cast == "W" ? 1 : 2 end - self.voiddate = startdate if is_renewal? self.vacdays = property_vacant_days set_housingneeds_fields if housingneeds? diff --git a/lib/tasks/data_export.rake b/lib/tasks/data_export.rake index 17f258293..a86712d83 100644 --- a/lib/tasks/data_export.rake +++ b/lib/tasks/data_export.rake @@ -11,21 +11,3 @@ namespace :core do DataExportXmlJob.perform_later(full_update:) end end - -namespace :illness_type_0 do - desc "Export log data where illness_type_0 == 1" - task export: :environment do |_task| - logs = LettingsLog.where(illness_type_0: 1, status: "completed").includes(created_by: :organisation) - puts "log_id,created_by_id,organisation_id,organisation_name,startdate" - - logs.each do |log| - puts [ - log.id, - log.created_by_id, - log.created_by.organisation.id, - log.created_by.organisation.name, - log.startdate&.strftime("%d/%m/%Y"), - ].join(",") - end - end -end diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index 2aee5ddcf..d4bbd8378 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -1459,6 +1459,15 @@ RSpec.describe LettingsLog do expect(record_from_db["vacdays"]).to eq(0) expect(lettings_log["vacdays"]).to eq(0) end + + it "correctly derives and saves first_time_property_let_as_social_housing" do + record_from_db = ActiveRecord::Base.connection.execute( + "select first_time_property_let_as_social_housing" \ + " from lettings_logs where id=#{lettings_log.id}", + ).to_a[0] + expect(record_from_db["first_time_property_let_as_social_housing"]).to eq(0) + expect(lettings_log["first_time_property_let_as_social_housing"]).to eq(0) + end end context "when answering the household characteristics questions" do From b6ff8bf896ed93df4769651e01c721b8e93f45b2 Mon Sep 17 00:00:00 2001 From: Jack <113976590+bibblobcode@users.noreply.github.com> Date: Fri, 17 Feb 2023 09:38:25 +0000 Subject: [PATCH 17/30] CLDC-1909 Add 2023-2024 collection year filter (#1314) --- app/helpers/filters_helper.rb | 8 ++++++++ app/views/logs/_log_filters.erb | 3 +-- config/initializers/feature_toggle.rb | 4 ++++ spec/helpers/filters_helper_spec.rb | 26 ++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index a1875081c..7f906b4ea 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -27,4 +27,12 @@ module FiltersHelper organisation_options = user.support? ? Organisation.all : [user.organisation] + user.organisation.managing_agents [OpenStruct.new(id: "", name: "Select an option")] + organisation_options.map { |org| OpenStruct.new(id: org.id, name: org.name) } end + + def collection_year_options + if FeatureToggle.collection_2023_2024_year_enabled? + { "2023": "2023/24", "2022": "2022/23", "2021": "2021/22" } + else + { "2022": "2022/23", "2021": "2021/22" } + end + end end diff --git a/app/views/logs/_log_filters.erb b/app/views/logs/_log_filters.erb index 8fb4f2ba4..a1aa752c3 100644 --- a/app/views/logs/_log_filters.erb +++ b/app/views/logs/_log_filters.erb @@ -6,7 +6,6 @@
<%= form_with html: { method: :get } do |f| %> - <% years = { "2021": "2021/22", "2022": "2022/23" } %> <% all_or_yours = { "all": { label: "All" }, "yours": { label: "Yours" } } %> <% if bulk_upload_options(@bulk_upload).present? %> @@ -23,7 +22,7 @@ <%= render partial: "filters/checkbox_filter", locals: { f: f, - options: years, + options: collection_year_options, label: "Collection year", category: "years", } %> diff --git a/config/initializers/feature_toggle.rb b/config/initializers/feature_toggle.rb index d31ee184b..60a97c95c 100644 --- a/config/initializers/feature_toggle.rb +++ b/config/initializers/feature_toggle.rb @@ -42,4 +42,8 @@ class FeatureToggle def self.validate_valid_radio_options? !(Rails.env.production? || Rails.env.staging?) end + + def self.collection_2023_2024_year_enabled? + !Rails.env.production? + end end diff --git a/spec/helpers/filters_helper_spec.rb b/spec/helpers/filters_helper_spec.rb index 656578326..3eccac743 100644 --- a/spec/helpers/filters_helper_spec.rb +++ b/spec/helpers/filters_helper_spec.rb @@ -116,4 +116,30 @@ RSpec.describe FiltersHelper do end end end + + describe "#collection_year_options" do + context "when not production" do + it "includes 2023/2024 option" do + expect(collection_year_options).to eq( + { + "2021": "2021/22", "2022": "2022/23", "2023": "2023/24" + }, + ) + end + end + + context "when production" do + before do + allow(Rails.env).to receive(:production?).and_return(true) + end + + it "includes 2023/2024 option" do + expect(collection_year_options).to eq( + { + "2021": "2021/22", "2022": "2022/23" + }, + ) + end + end + end end From 26aa5244bb003c829d281457f45775724abbe03f Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Fri, 17 Feb 2023 09:42:58 +0000 Subject: [PATCH 18/30] mass update gems used by docs (#1313) --- docs/Gemfile.lock | 63 ++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 8d38e2e0a..c8a433b8f 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,12 +1,11 @@ GEM remote: https://rubygems.org/ specs: - activesupport (6.0.6) + activesupport (7.0.4.2) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) coffee-script (2.4.1) @@ -14,30 +13,30 @@ GEM execjs coffee-script-source (1.11.1) colorator (1.1.0) - commonmarker (0.23.6) - concurrent-ruby (1.1.10) + commonmarker (0.23.8) + concurrent-ruby (1.2.0) dnsruby (1.61.9) simpleidn (~> 0.1) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) - ethon (0.15.0) + ethon (0.16.0) ffi (>= 1.15.0) eventmachine (1.2.7) execjs (2.8.1) - faraday (2.5.2) + faraday (2.7.4) faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.0) + faraday-net_http (3.0.2) ffi (1.15.5) forwardable-extended (2.6.0) gemoji (3.0.1) - github-pages (227) + github-pages (228) github-pages-health-check (= 1.17.9) - jekyll (= 3.9.2) + jekyll (= 3.9.3) jekyll-avatar (= 0.7.0) jekyll-coffeescript (= 1.1.1) - jekyll-commonmark-ghpages (= 0.2.0) + jekyll-commonmark-ghpages (= 0.4.0) jekyll-default-layout (= 0.1.4) jekyll-feed (= 0.15.1) jekyll-gist (= 1.5.0) @@ -71,7 +70,7 @@ GEM jemoji (= 0.12.0) kramdown (= 2.3.2) kramdown-parser-gfm (= 1.1.0) - liquid (= 4.0.3) + liquid (= 4.0.4) mercenary (~> 0.3) minima (= 2.5.1) nokogiri (>= 1.13.6, < 2.0) @@ -83,17 +82,17 @@ GEM octokit (~> 4.0) public_suffix (>= 3.0, < 5.0) typhoeus (~> 1.3) - html-pipeline (2.14.2) + html-pipeline (2.14.3) activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.8.0) - i18n (0.9.5) + i18n (1.12.0) concurrent-ruby (~> 1.0) - jekyll (3.9.2) + jekyll (3.9.3) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) - i18n (~> 0.7) + i18n (>= 0.7, < 2) jekyll-sass-converter (~> 1.0) jekyll-watch (~> 2.0) kramdown (>= 1.17, < 3) @@ -109,11 +108,11 @@ GEM coffee-script-source (~> 1.11.1) jekyll-commonmark (1.4.0) commonmarker (~> 0.22) - jekyll-commonmark-ghpages (0.2.0) - commonmarker (~> 0.23.4) + jekyll-commonmark-ghpages (0.4.0) + commonmarker (~> 0.23.7) jekyll (~> 3.9.0) jekyll-commonmark (~> 1.4.0) - rouge (>= 2.0, < 4.0) + rouge (>= 2.0, < 5.0) jekyll-default-layout (0.1.4) jekyll (~> 3.0) jekyll-feed (0.15.1) @@ -201,8 +200,8 @@ GEM rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - liquid (4.0.3) - listen (3.7.1) + liquid (4.0.4) + listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) @@ -210,12 +209,12 @@ GEM jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.16.3) - nokogiri (1.13.10-arm64-darwin) + minitest (5.17.0) + nokogiri (1.14.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.10-x86_64-darwin) + nokogiri (1.14.2-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.10-x86_64-linux) + nokogiri (1.14.2-x86_64-linux) racc (~> 1.4) octokit (4.25.1) faraday (>= 1, < 3) @@ -223,7 +222,7 @@ GEM pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (4.0.7) - racc (1.6.1) + racc (1.6.2) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) @@ -244,17 +243,15 @@ GEM unf (~> 0.1.4) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - thread_safe (0.3.6) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (1.2.10) - thread_safe (~> 0.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) unicode-display_width (1.8.0) - webrick (1.7.0) - zeitwerk (2.6.0) + webrick (1.8.1) PLATFORMS arm64-darwin-21 From c19b200e84bfeb4989b9d9a1081c91277c170c87 Mon Sep 17 00:00:00 2001 From: James Rose Date: Fri, 17 Feb 2023 09:46:47 +0000 Subject: [PATCH 19/30] Add arcturus.net to whitelisted delivery domains (#1316) - We whitelist domains that we will deliver to in non-production environments - This adds arcturus.net so that they can conduct their pentest --- config/credentials.yml.enc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 69dad9f7a..c9d564782 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -PWC9A1AmalyFv4A63MG9jCm2YtwE1Eyu1BJKodtbmq2nW7FD9svZZ3V4yC0JO9J8sLfCS6UrxQC4TOKKYWP5iYpOr5TKPi9MqNCVueLsoErN+nMAPhCJVpl8+eJ4BOvNYGlmle4bcwbAvHLq6IoANTBjktFh5/tgJS+IHBWK1FwPzK1eAhgQ1fE1jluWslee4iYesmh+ufHIMZAkoWuGJVky4i4uN5nKnwQfaPN5MUxBlvGiDH+s+yex4pcIaJ6hxYWAQRZXJRQVpZcf/agwU0Tk/S/fuDMm8zVeHCQkkmsCYzH5czB8b8IWYwyslqFBZNCix7YnbgwgYk8MUh7wPBuF8CFoPKVyteqic9HgUp9KY8kkt/RcWJ4zpv+Vwz3cre+iZ3S1bxFcSxXqO0MGRug2H9iwhAnBQDLl3vXLNNRYEL5LhNv0Z9Cy7at1fnYe1FcvF+3DR9kG/RAYzR8S2eEDdzBl797+DG81yhfkjP3/gfWxD+J+Mx0F4SDEOaGK5c/MqNTdiRRbnhzuRaQFMg6itoJbZybe+EOQScNft1QLqC4QwPd4Qevhj/A=--QffpsNB1u+1Lk/tD--spkCqBIGHl8g6HGwd89yCg== \ No newline at end of file +EZNV2LiNWzf52erbQ41Dz3Bh+2f3Uih8liEyhXp5XzHCLzAbmN6/IJqr7b9cTZiCiroFo4n/dFoG3yYrospp3frKsDXxF1K2/MTCJWjpgnn7wc+HiPQWG0W3HRtQCNkyyrHes0YKcYyDWIP6kztYv1I/Me3p0pGEx6t3CpSTg1v46eRnOlDWiUz3rVxPauwq9IYZ75gmnThqvg/Z8wcYsWLx0arago0SXtRPASCNj4uO/lbqTcAfyIXOTSiOlcAIjoPFRSQY7UqY0o2p8jRR/1L16SmGDsk8ijm+UygNmMexa3Khy5WcKctpQICakHs4NRjHNflqgXpXKL9dVBmNc9d7h+gbhbGJQ53Y0d+a35UbhPRMiv4SRH98FwB+WEsLCDdGSHvdmM6ArfOLljTrqrsmSRf0JfUrvzyVYmMCxjv4xgJwUS/TD5lQD1yPwkp2ss00kQJqzNmB7qwFhA8a3e2iNzV8qtAV/Nj+tMlr99Hb7vZZs98/38G2p5RAsE/5Xl9taKhc/ACnVc/bwJND4JWaBB7duCa08xVB8nkjlt5cCwMurzAcy1ZT+e8JepR+g6s8fpScMEWVJXE0hd8=--rZ41rY9TMXmiBUJw--QiLRVNVXZzTW446s7cec1g== \ No newline at end of file From 02d8247f59cf29a57627a69b79a5721b7ff329c9 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Fri, 17 Feb 2023 09:53:28 +0000 Subject: [PATCH 20/30] tweak bulk upload validation (#1302) - LA referral not permitted if general needs and now also if owning org is an LA --- app/services/bulk_upload/lettings/row_parser.rb | 6 +++--- spec/factories/organisation.rb | 4 ++++ .../bulk_upload/lettings/row_parser_spec.rb | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/row_parser.rb index bf1f62eba..005d5caf9 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/row_parser.rb @@ -147,7 +147,7 @@ class BulkUpload::Lettings::RowParser validate :validate_nulls validate :validate_relevant_collection_window validate :validate_la_with_local_housing_referral - validate :validate_cannot_be_la_referral_if_general_needs + validate :validate_cannot_be_la_referral_if_general_needs_and_la validate :validate_leaving_reason_for_renewal validate :validate_lettings_type_matches_bulk_upload validate :validate_only_one_housing_needs_type @@ -219,8 +219,8 @@ private end end - def validate_cannot_be_la_referral_if_general_needs - if field_78 == 4 && bulk_upload.general_needs? + def validate_cannot_be_la_referral_if_general_needs_and_la + if field_78 == 4 && bulk_upload.general_needs? && owning_organisation && owning_organisation.la? errors.add :field_78, I18n.t("validations.household.referral.la_general_needs.prp_referred_by_la") end end diff --git a/spec/factories/organisation.rb b/spec/factories/organisation.rb index 147f847d6..7aee3ad66 100644 --- a/spec/factories/organisation.rb +++ b/spec/factories/organisation.rb @@ -13,6 +13,10 @@ FactoryBot.define do trait :with_old_visible_id do old_visible_id { rand(9_999_999).to_s } end + + trait :prp do + provider_type { "PRP" } + end end factory :organisation_rent_period do diff --git a/spec/services/bulk_upload/lettings/row_parser_spec.rb b/spec/services/bulk_upload/lettings/row_parser_spec.rb index 1bebd6444..4c0788033 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/row_parser_spec.rb @@ -396,14 +396,24 @@ RSpec.describe BulkUpload::Lettings::RowParser do end end - context "when 4 ie referred by LA and is general needs" do - let(:attributes) { { bulk_upload:, field_78: "4" } } + context "when 4 ie referred by LA and is general needs and owning org is LA" do + let(:attributes) { { bulk_upload:, field_78: "4", field_111: owning_org.old_visible_id.to_s } } it "is not permitted" do expect(parser.errors[:field_78]).to be_present end end + context "when 4 ie referred by LA and is general needs and owning org is PRP" do + let(:owning_org) { create(:organisation, :prp, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_78: "4", field_111: owning_org.old_visible_id.to_s } } + + it "is permitted" do + expect(parser.errors[:field_78]).to be_blank + end + end + context "when 4 ie referred by LA and is not general needs" do let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: 2) } let(:attributes) { { bulk_upload:, field_78: "4" } } From 7c62d8b4a6445812d8d5f407195272053659010a Mon Sep 17 00:00:00 2001 From: James Rose Date: Fri, 17 Feb 2023 09:54:44 +0000 Subject: [PATCH 21/30] Change CDS export archive names from quarter to financial year (#1309) - We previously pushed logs into archives categorised by the quarter that they were created for. - CDS requested that instead we push everything into a larger bucket seperated by FY. --- app/helpers/collection_time_helper.rb | 20 ++++++-- .../exports/lettings_log_export_constants.rb | 7 --- .../exports/lettings_log_export_service.rb | 7 +-- spec/helpers/collection_time_helper_spec.rb | 46 +++++++++++++++++++ .../lettings_log_export_service_spec.rb | 8 ++-- 5 files changed, 69 insertions(+), 19 deletions(-) diff --git a/app/helpers/collection_time_helper.rb b/app/helpers/collection_time_helper.rb index 8478d06e7..be0abae20 100644 --- a/app/helpers/collection_time_helper.rb +++ b/app/helpers/collection_time_helper.rb @@ -1,16 +1,26 @@ module CollectionTimeHelper + def collection_start_year(date) + window_end_date = Time.zone.local(date.year, 4, 1) + date < window_end_date ? date.year - 1 : date.year + end + def current_collection_start_year - today = Time.zone.now - window_end_date = Time.zone.local(today.year, 4, 1) - today < window_end_date ? today.year - 1 : today.year + collection_start_year(Time.zone.now) 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) + Time.zone.local(collection_start_year(date), 4, 1) end def current_collection_start_date Time.zone.local(current_collection_start_year, 4, 1) end + + def collection_end_date(date) + Time.zone.local(collection_start_year(date) + 1, 3, 31) + end + + def current_collection_end_date + Time.zone.local(current_collection_start_year + 1, 3, 31) + end end diff --git a/app/services/exports/lettings_log_export_constants.rb b/app/services/exports/lettings_log_export_constants.rb index 332f22550..3ae7d71fa 100644 --- a/app/services/exports/lettings_log_export_constants.rb +++ b/app/services/exports/lettings_log_export_constants.rb @@ -7,13 +7,6 @@ module Exports::LettingsLogExportConstants csv: 2, }.freeze - QUARTERS = { - 0 => "jan_mar", - 1 => "apr_jun", - 2 => "jul_sep", - 3 => "oct_dec", - }.freeze - EXPORT_FIELDS = Set[ "armedforces", "beds", diff --git a/app/services/exports/lettings_log_export_service.rb b/app/services/exports/lettings_log_export_service.rb index fb985f931..197e0ed4b 100644 --- a/app/services/exports/lettings_log_export_service.rb +++ b/app/services/exports/lettings_log_export_service.rb @@ -1,6 +1,7 @@ module Exports class LettingsLogExportService include Exports::LettingsLogExportConstants + include CollectionTimeHelper def initialize(storage_service, logger = Rails.logger) @storage_service = storage_service @@ -66,11 +67,11 @@ module Exports return unless lettings_log.startdate collection_start = lettings_log.collection_start_year - month = lettings_log.startdate.month - quarter = QUARTERS[(month - 1) / 3] + start_month = collection_start_date(lettings_log.startdate).strftime("%b") + end_month = collection_end_date(lettings_log.startdate).strftime("%b") base_number_str = "f#{base_number.to_s.rjust(4, '0')}" increment_str = "inc#{increment.to_s.rjust(4, '0')}" - "core_#{collection_start}_#{collection_start + 1}_#{quarter}_#{base_number_str}_#{increment_str}" + "core_#{collection_start}_#{collection_start + 1}_#{start_month}_#{end_month}_#{base_number_str}_#{increment_str}".downcase end def write_export_archive(export, lettings_logs) diff --git a/spec/helpers/collection_time_helper_spec.rb b/spec/helpers/collection_time_helper_spec.rb index 3b02802f2..c2eb2fedb 100644 --- a/spec/helpers/collection_time_helper_spec.rb +++ b/spec/helpers/collection_time_helper_spec.rb @@ -21,6 +21,10 @@ RSpec.describe CollectionTimeHelper do it "returns the correct current start date" do expect(current_collection_start_date).to eq(Time.zone.local(2022, 4, 1)) end + + it "returns the correct current end date" do + expect(current_collection_end_date).to eq(Time.zone.local(2023, 3, 31)) + end end context "with the date before 1st of April" do @@ -29,6 +33,48 @@ RSpec.describe CollectionTimeHelper do it "returns the previous year as the current start year" do expect(current_collection_start_year).to eq(2021) end + + it "returns the correct current start date" do + expect(current_collection_start_date).to eq(Time.zone.local(2021, 4, 1)) + end + + it "returns the correct current end date" do + expect(current_collection_end_date).to eq(Time.zone.local(2022, 3, 31)) + end + end + end + + describe "Any collection year" do + context "when the date is after 1st of April" do + let(:now) { Time.utc(2022, 8, 3) } + + it "returns the same year as the current start year" do + expect(collection_start_year(now)).to eq(2022) + end + + it "returns the correct current start date" do + expect(collection_start_date(now)).to eq(Time.zone.local(2022, 4, 1)) + end + + it "returns the correct current end date" do + expect(collection_end_date(now)).to eq(Time.zone.local(2023, 3, 31)) + end + end + + context "with the date before 1st of April" do + let(:now) { Time.utc(2022, 2, 3) } + + it "returns the previous year as the current start year" do + expect(collection_start_year(now)).to eq(2021) + end + + it "returns the correct current start date" do + expect(collection_start_date(now)).to eq(Time.zone.local(2021, 4, 1)) + end + + it "returns the correct current end date" do + expect(collection_end_date(now)).to eq(Time.zone.local(2022, 3, 31)) + end end end end diff --git a/spec/services/exports/lettings_log_export_service_spec.rb b/spec/services/exports/lettings_log_export_service_spec.rb index 36adda071..55897c354 100644 --- a/spec/services/exports/lettings_log_export_service_spec.rb +++ b/spec/services/exports/lettings_log_export_service_spec.rb @@ -13,8 +13,8 @@ RSpec.describe Exports::LettingsLogExportService do let(:expected_master_manifest_filename) { "Manifest_2022_05_01_0001.csv" } let(:expected_master_manifest_rerun) { "Manifest_2022_05_01_0002.csv" } - let(:expected_zip_filename) { "core_2021_2022_jan_mar_f0001_inc0001.zip" } - let(:expected_data_filename) { "core_2021_2022_jan_mar_f0001_inc0001_pt001.xml" } + let(:expected_zip_filename) { "core_2021_2022_apr_mar_f0001_inc0001.zip" } + let(:expected_data_filename) { "core_2021_2022_apr_mar_f0001_inc0001_pt001.xml" } let(:expected_manifest_filename) { "manifest.xml" } let(:start_time) { Time.zone.local(2022, 5, 1) } @@ -108,7 +108,7 @@ RSpec.describe Exports::LettingsLogExportService do end context "and multiple lettings logs are available for export on different periods" do - let(:expected_zip_filename2) { "core_2022_2023_apr_jun_f0001_inc0001.zip" } + let(:expected_zip_filename2) { "core_2022_2023_apr_mar_f0001_inc0001.zip" } before do FactoryBot.create(:lettings_log, startdate: Time.zone.local(2022, 2, 1)) @@ -206,7 +206,7 @@ RSpec.describe Exports::LettingsLogExportService do end it "generates a ZIP export file with the expected filename" do - expect(storage_service).to receive(:write_file).with("core_2021_2022_jan_mar_f0002_inc0001.zip", any_args) + expect(storage_service).to receive(:write_file).with("core_2021_2022_apr_mar_f0002_inc0001.zip", any_args) export_service.export_xml_lettings_logs(full_update: true) end end From 5490e2ecb037cb75ec5eac323631c6b2d1eeef27 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Fri, 17 Feb 2023 10:20:39 +0000 Subject: [PATCH 22/30] CLDC-1892 Bulk upload validation for managing organisation (#1315) * bulk upload validation for owning org * block log creation at row parser level * bubble up block_log_creation so it blocks logs * owning org id looks up serveral fields * validate bulk upload managing org --- app/models/organisation.rb | 27 ++++++ .../bulk_upload/lettings/row_parser.rb | 67 ++++++++++++-- .../bulk_upload/lettings/validator.rb | 1 + spec/factories/organisation.rb | 4 + .../bulk_upload/lettings/row_parser_spec.rb | 88 +++++++++++++++++++ .../bulk_upload/lettings/validator_spec.rb | 20 ++++- 6 files changed, 199 insertions(+), 8 deletions(-) diff --git a/app/models/organisation.rb b/app/models/organisation.rb index 89d7d1f34..32d3540d1 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -17,8 +17,21 @@ class Organisation < ApplicationRecord has_many :managing_agent_relationships, foreign_key: :parent_organisation_id, class_name: "OrganisationRelationship" has_many :managing_agents, through: :managing_agent_relationships, source: :child_organisation + def affiliated_stock_owners + ids = [] + + if holds_own_stock? && persisted? + ids << id + end + + ids.concat(stock_owners.pluck(:id)) + + Organisation.where(id: ids) + end + scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") } scope :search_by, ->(param) { search_by_name(param) } + has_paper_trail auto_strip_attributes :name @@ -35,6 +48,20 @@ class Organisation < ApplicationRecord validates :name, presence: { message: I18n.t("validations.organisation.name_missing") } validates :provider_type, presence: { message: I18n.t("validations.organisation.provider_type_missing") } + def self.find_by_id_on_mulitple_fields(id) + return if id.nil? + + if id.start_with?("ORG") + where(id: id[3..]).first + else + where(old_visible_id: id).first + end + end + + def can_be_managed_by?(organisation:) + organisation == self || managing_agents.include?(organisation) + end + def lettings_logs LettingsLog.filter_by_organisation(self) end diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/row_parser.rb index 005d5caf9..bdb35dbc6 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/row_parser.rb @@ -3,6 +3,7 @@ class BulkUpload::Lettings::RowParser include ActiveModel::Attributes attribute :bulk_upload + attribute :block_log_creation, :boolean, default: -> { false } attribute :field_1, :integer attribute :field_2 @@ -114,9 +115,9 @@ class BulkUpload::Lettings::RowParser attribute :field_108, :string attribute :field_109, :string attribute :field_110 - attribute :field_111, :integer + attribute :field_111, :string attribute :field_112, :string - attribute :field_113, :integer + attribute :field_113, :string attribute :field_114, :integer attribute :field_115 attribute :field_116, :integer @@ -155,6 +156,13 @@ class BulkUpload::Lettings::RowParser validate :validate_dont_know_disabled_needs_conjunction validate :validate_no_and_dont_know_disabled_needs_conjunction + validate :validate_owning_org_permitted + validate :validate_owning_org_owns_stock + validate :validate_owning_org_exists + + validate :validate_managing_org_related + validate :validate_managing_org_exists + def valid? errors.clear @@ -173,15 +181,60 @@ class BulkUpload::Lettings::RowParser end def blank_row? - attribute_set.to_hash.reject { |k, _| %w[bulk_upload].include?(k) }.values.compact.empty? + attribute_set.to_hash.reject { |k, _| %w[bulk_upload block_log_creation].include?(k) }.values.compact.empty? end def log @log ||= LettingsLog.new(attributes_for_log) end + def block_log_creation! + self.block_log_creation = true + end + + def block_log_creation? + block_log_creation + end + private + def validate_managing_org_related + if owning_organisation && managing_organisation && !owning_organisation.can_be_managed_by?(organisation: managing_organisation) + block_log_creation! + errors.add(:field_113, "This managing organisation does not have a relationship with the owning organisation") + end + end + + def validate_managing_org_exists + if managing_organisation.nil? + errors.delete(:field_113) + errors.add(:field_113, "The managing organisation code is incorrect") + end + end + + def validate_owning_org_owns_stock + if owning_organisation && !owning_organisation.holds_own_stock? + block_log_creation! + errors.delete(:field_111) + errors.add(:field_111, "The owning organisation code provided is for an organisation that does not own stock") + end + end + + def validate_owning_org_exists + if owning_organisation.nil? + errors.delete(:field_111) + errors.add(:field_111, "The owning organisation code is incorrect") + end + end + + def validate_owning_org_permitted + if owning_organisation && !bulk_upload.user.organisation.affiliated_stock_owners.include?(owning_organisation) + block_log_creation! + errors.delete(:field_111) + errors.add(:field_111, "You do not have permission to add logs for this owning organisation") + end + end + def validate_no_and_dont_know_disabled_needs_conjunction if field_59 == 1 && field_60 == 1 errors.add(:field_59, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction")) @@ -483,15 +536,19 @@ private end def owning_organisation - Organisation.find_by(old_visible_id: field_111) + Organisation.find_by_id_on_mulitple_fields(field_111) end def owning_organisation_id owning_organisation&.id end + def managing_organisation + Organisation.find_by_id_on_mulitple_fields(field_113) + end + def managing_organisation_id - Organisation.find_by(old_visible_id: field_113)&.id + managing_organisation&.id end def attributes_for_log diff --git a/app/services/bulk_upload/lettings/validator.rb b/app/services/bulk_upload/lettings/validator.rb index 6f37c0f3a..992f06196 100644 --- a/app/services/bulk_upload/lettings/validator.rb +++ b/app/services/bulk_upload/lettings/validator.rb @@ -176,6 +176,7 @@ class BulkUpload::Lettings::Validator def create_logs? return false if any_setup_sections_incomplete? return false if over_column_error_threshold? + return false if row_parsers.any?(&:block_log_creation?) row_parsers.all? { |row_parser| row_parser.log.valid? } end diff --git a/spec/factories/organisation.rb b/spec/factories/organisation.rb index 7aee3ad66..fa2650eb5 100644 --- a/spec/factories/organisation.rb +++ b/spec/factories/organisation.rb @@ -17,6 +17,10 @@ FactoryBot.define do trait :prp do provider_type { "PRP" } end + + trait :does_not_own_stock do + holds_own_stock { false } + end end factory :organisation_rent_period do diff --git a/spec/services/bulk_upload/lettings/row_parser_spec.rb b/spec/services/bulk_upload/lettings/row_parser_spec.rb index 4c0788033..36f145a5e 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/row_parser_spec.rb @@ -8,8 +8,10 @@ RSpec.describe BulkUpload::Lettings::RowParser do let(:attributes) { { bulk_upload: } } let(:bulk_upload) { create(:bulk_upload, :lettings, user:) } let(:user) { create(:user, organisation: owning_org) } + let(:owning_org) { create(:organisation, :with_old_visible_id) } let(:managing_org) { create(:organisation, :with_old_visible_id) } + let(:setup_section_params) do { bulk_upload:, @@ -23,6 +25,10 @@ RSpec.describe BulkUpload::Lettings::RowParser do } end + before do + create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org) + end + around do |example| FormHandler.instance.use_real_forms! @@ -472,6 +478,68 @@ RSpec.describe BulkUpload::Lettings::RowParser do end end + describe "#field_111" do # owning org + context "when cannot find owning org" do + let(:attributes) { { bulk_upload:, field_111: "donotexist" } } + + it "is not permitted" do + expect(parser.errors[:field_111]).to eql(["The owning organisation code is incorrect"]) + end + end + + context "when org is not stock owning" do + let(:owning_org) { create(:organisation, :with_old_visible_id, :does_not_own_stock) } + + let(:attributes) { { bulk_upload:, field_111: owning_org.old_visible_id } } + + it "is not permitted" do + expect(parser.errors[:field_111]).to eql(["The owning organisation code provided is for an organisation that does not own stock"]) + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + + context "when not affiliated with owning org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_111: unaffiliated_org.old_visible_id } } + + it "is not permitted" do + expect(parser.errors[:field_111]).to eql(["You do not have permission to add logs for this owning organisation"]) + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + end + + describe "#field_113" do # managing org + context "when cannot find managing org" do + let(:attributes) { { bulk_upload:, field_113: "donotexist" } } + + it "is not permitted" do + expect(parser.errors[:field_113]).to eql(["The managing organisation code is incorrect"]) + end + end + + context "when not affiliated with managing org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_111: owning_org.old_visible_id, field_113: unaffiliated_org.old_visible_id } } + + it "is not permitted" do + expect(parser.errors[:field_113]).to eql(["This managing organisation does not have a relationship with the owning organisation"]) + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + end + describe "#field_134" do context "when an unpermitted value" do let(:attributes) { { bulk_upload:, field_134: 3 } } @@ -506,6 +574,26 @@ RSpec.describe BulkUpload::Lettings::RowParser do end describe "#log" do + describe "#owning_organisation" do + context "when lookup is via id prefixed with ORG" do + let(:attributes) { { bulk_upload:, field_111: "ORG#{owning_org.id}" } } + + it "assigns the correct org" do + expect(parser.log.owning_organisation).to eql(owning_org) + end + end + end + + describe "#managing_organisation" do + context "when lookup is via id prefixed with ORG" do + let(:attributes) { { bulk_upload:, field_113: "ORG#{managing_org.id}" } } + + it "assigns the correct org" do + expect(parser.log.managing_organisation).to eql(managing_org) + end + end + end + describe "#cbl" do context "when field_75 is yes ie 1" do let(:attributes) { { bulk_upload:, field_75: 1 } } diff --git a/spec/services/bulk_upload/lettings/validator_spec.rb b/spec/services/bulk_upload/lettings/validator_spec.rb index 263c83163..0aaeaac78 100644 --- a/spec/services/bulk_upload/lettings/validator_spec.rb +++ b/spec/services/bulk_upload/lettings/validator_spec.rb @@ -87,7 +87,7 @@ RSpec.describe BulkUpload::Lettings::Validator do end end - describe "#should_create_logs?" do + describe "#create_logs?" do context "when all logs are valid" do let(:target_path) { file_fixture("2022_23_lettings_bulk_upload.csv") } @@ -111,9 +111,7 @@ RSpec.describe BulkUpload::Lettings::Validator do expect(validator).not_to be_create_logs end end - end - describe "#create_logs?" do context "when a log is not valid?" do let(:log_1) { build(:lettings_log, :completed, created_by: user) } let(:log_2) { build(:lettings_log, :completed, created_by: user) } @@ -146,6 +144,22 @@ RSpec.describe BulkUpload::Lettings::Validator do end end + context "when a single log wants to block log creation" do + let(:unaffiliated_org) { create(:organisation) } + + let(:log_1) { build(:lettings_log, :completed, renttype: 1, created_by: user, owning_organisation: unaffiliated_org) } + + before do + file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_csv_row) + file.close + end + + it "will not create logs" do + validator.call + expect(validator).not_to be_create_logs + end + end + context "when a log has incomplete setup secion" do let(:log) { build(:lettings_log, :in_progress, created_by: user, startdate: Time.zone.local(2022, 5, 1)) } From b24ab538ce4ec8888fd7376e0ff52d6600b171d2 Mon Sep 17 00:00:00 2001 From: James Rose Date: Fri, 17 Feb 2023 12:07:14 +0000 Subject: [PATCH 23/30] Update collection year end dates for 2022 onwards (#1312) The source of truth for this update is: https://digital.dclg.gov.uk/confluence/pages/viewpage.action?spaceKey=MC&title=CORE+And+Collection+Years --- app/models/form.rb | 6 +++++- config/forms/2022_2023.json | 2 +- spec/helpers/tasklist_helper_spec.rb | 4 ++-- spec/models/form_spec.rb | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/models/form.rb b/app/models/form.rb index 3da85d52e..f9959959e 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -6,7 +6,11 @@ class Form def initialize(form_path, start_year = "", sections_in_form = [], type = "lettings") if sales_or_start_year_after_2022?(type, start_year) @start_date = Time.zone.local(start_year, 4, 1) - @end_date = Time.zone.local(start_year + 1, 7, 1) + @end_date = if start_year && start_year.to_i > 2022 + Time.zone.local(start_year + 1, 7, 9) + else + Time.zone.local(start_year + 1, 7, 7) + end @setup_sections = type == "sales" ? [Form::Sales::Sections::Setup.new(nil, nil, self)] : [Form::Lettings::Sections::Setup.new(nil, nil, self)] @form_sections = sections_in_form.map { |sec| sec.new(nil, nil, self) } @type = type diff --git a/config/forms/2022_2023.json b/config/forms/2022_2023.json index ebd1bd594..e0ef19198 100644 --- a/config/forms/2022_2023.json +++ b/config/forms/2022_2023.json @@ -1,7 +1,7 @@ { "form_type": "lettings", "start_date": "2022-04-01T00:00:00.000+01:00", - "end_date": "2023-07-01T00:00:00.000+01:00", + "end_date": "2023-07-09T00:00:00.000+01:00", "unresolved_log_redirect_page_id": "tenancy_start_date", "sections": { "tenancy_and_property": { diff --git a/spec/helpers/tasklist_helper_spec.rb b/spec/helpers/tasklist_helper_spec.rb index e14e06ea4..1ac6cf738 100644 --- a/spec/helpers/tasklist_helper_spec.rb +++ b/spec/helpers/tasklist_helper_spec.rb @@ -113,7 +113,7 @@ RSpec.describe TasklistHelper do it "returns relevant text" do expect(review_log_text(lettings_log)).to eq( - "You can #{govuk_link_to 'review and make changes to this log', review_lettings_log_path(lettings_log)} until 1 July 2024.".html_safe, + "You can #{govuk_link_to 'review and make changes to this log', review_lettings_log_path(lettings_log)} until 9 July 2024.".html_safe, ) end end @@ -136,7 +136,7 @@ RSpec.describe TasklistHelper do it "returns relevant text" do expect(review_log_text(sales_log)).to eq( - "You can #{govuk_link_to 'review and make changes to this log', review_sales_log_path(id: sales_log, sales_log: true)} until 1 July 2023.".html_safe, + "You can #{govuk_link_to 'review and make changes to this log', review_sales_log_path(id: sales_log, sales_log: true)} until 7 July 2023.".html_safe, ) end end diff --git a/spec/models/form_spec.rb b/spec/models/form_spec.rb index 72223c1b5..cd58587dc 100644 --- a/spec/models/form_spec.rb +++ b/spec/models/form_spec.rb @@ -223,7 +223,7 @@ RSpec.describe Form, type: :model do expect(form.questions.count).to eq(16) expect(form.questions.first.id).to eq("owning_organisation_id") expect(form.start_date).to eq(Time.zone.parse("2022-04-01")) - expect(form.end_date).to eq(Time.zone.parse("2023-07-01")) + expect(form.end_date).to eq(Time.zone.parse("2023-07-07")) expect(form.unresolved_log_redirect_page_id).to eq(nil) end From 01b957cc1093c6e9578062dc4fe57f1c165d4e65 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Fri, 17 Feb 2023 12:17:33 +0000 Subject: [PATCH 24/30] better validation on bulk upload start year (#1319) --- .../bulk_upload/lettings/row_parser.rb | 3 +++ config/locales/en.yml | 1 + .../bulk_upload/lettings/row_parser_spec.rb | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/row_parser.rb index bdb35dbc6..9cb3edb78 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/row_parser.rb @@ -143,6 +143,7 @@ class BulkUpload::Lettings::RowParser validates :field_1, presence: { message: I18n.t("validations.not_answered", question: "letting type") }, inclusion: { in: (1..12).to_a, message: I18n.t("validations.invalid_option", question: "letting type") } validates :field_4, presence: { if: proc { [2, 4, 6, 8, 10, 12].include?(field_1) } } + validates :field_98, format: { with: /\A\d{2}\z/, message: I18n.t("validations.setup.startdate.year_not_two_digits") } validate :validate_data_types validate :validate_nulls @@ -500,6 +501,8 @@ private def startdate Date.new(field_98 + 2000, field_97, field_96) if field_98.present? && field_97.present? && field_96.present? + rescue Date::Error + Date.new end def renttype diff --git a/config/locales/en.yml b/config/locales/en.yml index 23d39bbe8..747ed4414 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -156,6 +156,7 @@ 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" + year_not_two_digits: Tenancy start year must be 2 digits location: deactivated: "The location %{postcode} was deactivated on %{date} and was not available on the day you entered." reactivating_soon: "The location %{postcode} is not available until %{date}. Select another location or edit the tenancy start date" diff --git a/spec/services/bulk_upload/lettings/row_parser_spec.rb b/spec/services/bulk_upload/lettings/row_parser_spec.rb index 36f145a5e..d1792e9df 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/row_parser_spec.rb @@ -443,6 +443,24 @@ RSpec.describe BulkUpload::Lettings::RowParser do end end + context "when field 98 is 4 digits instead of 2" do + let(:attributes) { { bulk_upload:, field_98: "2022" } } + + it "returns an error" do + parser.valid? + + expect(parser.errors[:field_98]).to include("Tenancy start year must be 2 digits") + end + end + + context "when invalid date given" do + let(:attributes) { { bulk_upload:, field_1: "1", field_96: "a", field_97: "12", field_98: "2022" } } + + it "does not raise an error" do + expect { parser.valid? }.not_to raise_error + end + end + context "when inside of collection year" do let(:attributes) { { bulk_upload:, field_96: "1", field_97: "10", field_98: "22" } } From e7e67c8b58c4014f17235cf2dbb1b48a1e98a744 Mon Sep 17 00:00:00 2001 From: Jack <113976590+bibblobcode@users.noreply.github.com> Date: Tue, 21 Feb 2023 10:36:31 +0000 Subject: [PATCH 25/30] Bump depts to fix security issues (#1328) * Bump depts * Fix deprecation --- Gemfile | 2 +- Gemfile.lock | 270 ++++++++++++++++++++++--------------------- spec/rails_helper.rb | 2 +- 3 files changed, 138 insertions(+), 136 deletions(-) diff --git a/Gemfile b/Gemfile index f77a70899..17e10865c 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem "bootsnap", ">= 1.4.4", require: false # GOV UK frontend components gem "govuk-components" # GOV UK component form builder DSL -gem "govuk_design_system_formbuilder" +gem "govuk_design_system_formbuilder", "3.1.2" # Convert Markdown into GOV.UK frontend-styled HTML gem "govuk_markdown" # GOV UK Notify diff --git a/Gemfile.lock b/Gemfile.lock index 76ab1021c..fa61ec504 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,67 +13,67 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.4.1) - actionpack (= 7.0.4.1) - activesupport (= 7.0.4.1) + actioncable (7.0.4.2) + actionpack (= 7.0.4.2) + activesupport (= 7.0.4.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.4.1) - actionpack (= 7.0.4.1) - activejob (= 7.0.4.1) - activerecord (= 7.0.4.1) - activestorage (= 7.0.4.1) - activesupport (= 7.0.4.1) + actionmailbox (7.0.4.2) + actionpack (= 7.0.4.2) + activejob (= 7.0.4.2) + activerecord (= 7.0.4.2) + activestorage (= 7.0.4.2) + activesupport (= 7.0.4.2) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.4.1) - actionpack (= 7.0.4.1) - actionview (= 7.0.4.1) - activejob (= 7.0.4.1) - activesupport (= 7.0.4.1) + actionmailer (7.0.4.2) + actionpack (= 7.0.4.2) + actionview (= 7.0.4.2) + activejob (= 7.0.4.2) + activesupport (= 7.0.4.2) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.4.1) - actionview (= 7.0.4.1) - activesupport (= 7.0.4.1) + actionpack (7.0.4.2) + actionview (= 7.0.4.2) + activesupport (= 7.0.4.2) rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.4.1) - actionpack (= 7.0.4.1) - activerecord (= 7.0.4.1) - activestorage (= 7.0.4.1) - activesupport (= 7.0.4.1) + actiontext (7.0.4.2) + actionpack (= 7.0.4.2) + activerecord (= 7.0.4.2) + activestorage (= 7.0.4.2) + activesupport (= 7.0.4.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.4.1) - activesupport (= 7.0.4.1) + actionview (7.0.4.2) + activesupport (= 7.0.4.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.4.1) - activesupport (= 7.0.4.1) + activejob (7.0.4.2) + activesupport (= 7.0.4.2) globalid (>= 0.3.6) - activemodel (7.0.4.1) - activesupport (= 7.0.4.1) - activerecord (7.0.4.1) - activemodel (= 7.0.4.1) - activesupport (= 7.0.4.1) - activestorage (7.0.4.1) - actionpack (= 7.0.4.1) - activejob (= 7.0.4.1) - activerecord (= 7.0.4.1) - activesupport (= 7.0.4.1) + activemodel (7.0.4.2) + activesupport (= 7.0.4.2) + activerecord (7.0.4.2) + activemodel (= 7.0.4.2) + activesupport (= 7.0.4.2) + activestorage (7.0.4.2) + actionpack (= 7.0.4.2) + activejob (= 7.0.4.2) + activerecord (= 7.0.4.2) + activesupport (= 7.0.4.2) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.4.1) + activesupport (7.0.4.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -84,20 +84,20 @@ GEM auto_strip_attributes (2.6.0) activerecord (>= 4.0) aws-eventstream (1.2.0) - aws-partitions (1.635.0) - aws-sdk-core (3.153.0) + aws-partitions (1.714.0) + aws-sdk-core (3.170.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.525.0) - aws-sigv4 (~> 1.1) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.58.0) - aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (1.62.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.114.0) - aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-s3 (1.119.1) + aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.1) + aws-sigv4 (1.5.2) aws-eventstream (~> 1, >= 1.0.2) bcrypt (3.1.18) better_html (2.0.1) @@ -108,14 +108,14 @@ GEM parser (>= 2.4) smart_properties bindex (0.8.1) - bootsnap (1.13.0) + bootsnap (1.16.0) msgpack (~> 1.2) builder (3.2.4) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) byebug (11.1.3) - capybara (3.37.1) + capybara (3.38.0) addressable matrix mini_mime (>= 0.1.3) @@ -124,14 +124,14 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - capybara-lockstep (1.2.1) + capybara-lockstep (1.3.0) activesupport (>= 3.2) capybara (>= 2.0) ruby2_keywords selenium-webdriver (>= 3) childprocess (4.1.0) coderay (1.1.3) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.0) connection_pool (2.3.0) crack (0.4.5) rexml @@ -150,7 +150,7 @@ GEM dotenv (= 2.8.1) railties (>= 3.2) encryptor (3.0.0) - erb_lint (0.2.0) + erb_lint (0.3.1) activesupport better_html (>= 2.0.1) parser (>= 2.7.1.4) @@ -160,19 +160,19 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - excon (0.92.5) + excon (0.99.0) factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - faker (2.23.0) + faker (3.1.1) i18n (>= 1.8.11, < 2) ffi (1.15.5) fugit (1.8.1) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) - globalid (1.0.1) + globalid (1.1.0) activesupport (>= 5.0) govuk-components (3.2.1) actionpack (>= 6.1) @@ -186,7 +186,7 @@ GEM activemodel (>= 6.1) activesupport (>= 6.1) html-attributes-utils (~> 0.9, >= 0.9.2) - govuk_markdown (1.0.0) + govuk_markdown (2.0.0) activesupport redcarpet hashdiff (1.0.1) @@ -195,19 +195,19 @@ GEM i18n (1.12.0) concurrent-ruby (~> 1.0) iniparse (1.5.0) - jmespath (1.6.1) - jsbundling-rails (1.0.3) + jmespath (1.6.2) + jsbundling-rails (1.1.1) railties (>= 6.0.0) json-schema (3.0.0) addressable (>= 2.8) - jwt (2.5.0) - listen (3.7.1) + jwt (2.7.0) + listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.8.0.1) + mail (2.8.1) mini_mime (>= 0.1.1) net-imap net-pop @@ -217,7 +217,7 @@ GEM method_source (1.0.0) mini_mime (1.1.2) minitest (5.17.0) - msgpack (1.5.6) + msgpack (1.6.0) net-imap (0.3.4) date net-protocol @@ -228,33 +228,33 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) - nokogiri (1.14.0-arm64-darwin) + nokogiri (1.14.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.14.0-x86_64-darwin) + nokogiri (1.14.2-x86_64-darwin) racc (~> 1.4) - nokogiri (1.14.0-x86_64-linux) + nokogiri (1.14.2-x86_64-linux) racc (~> 1.4) - notifications-ruby-client (5.3.0) + notifications-ruby-client (5.4.0) jwt (>= 1.5, < 3) orm_adapter (0.5.0) - overcommit (0.59.1) + overcommit (0.60.0) childprocess (>= 0.6.3, < 5) iniparse (~> 1.4) rexml (~> 3.2) pagy (5.10.1) activesupport - paper_trail (13.0.0) - activerecord (>= 5.2) - request_store (~> 1.1) + paper_trail (14.0.0) + activerecord (>= 6.0) + request_store (~> 1.4) paper_trail-globalid (0.2.0) globalid paper_trail (>= 3.0.0) parallel (1.22.1) - parallel_tests (4.0.0) + parallel_tests (4.2.0) parallel - parser (3.1.2.1) + parser (3.2.1.0) ast (~> 2.4.1) - pg (1.4.3) + pg (1.4.5) possessive (1.0.1) postcodes_io (0.4.0) excon (~> 0.39) @@ -263,13 +263,13 @@ GEM activesupport (>= 7.0.0) rack railties (>= 7.0.0) - pry (0.14.1) + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) pry-byebug (3.10.1) byebug (~> 11.0) pry (>= 0.13, < 0.15) - public_suffix (5.0.0) + public_suffix (5.0.1) puma (5.6.5) nio4r (~> 2.0) raabro (1.4.0) @@ -281,28 +281,28 @@ GEM rack (>= 1.2.0) rack-test (2.0.2) rack (>= 1.3) - rails (7.0.4.1) - actioncable (= 7.0.4.1) - actionmailbox (= 7.0.4.1) - actionmailer (= 7.0.4.1) - actionpack (= 7.0.4.1) - actiontext (= 7.0.4.1) - actionview (= 7.0.4.1) - activejob (= 7.0.4.1) - activemodel (= 7.0.4.1) - activerecord (= 7.0.4.1) - activestorage (= 7.0.4.1) - activesupport (= 7.0.4.1) + rails (7.0.4.2) + actioncable (= 7.0.4.2) + actionmailbox (= 7.0.4.2) + actionmailer (= 7.0.4.2) + actionpack (= 7.0.4.2) + actiontext (= 7.0.4.2) + actionview (= 7.0.4.2) + activejob (= 7.0.4.2) + activemodel (= 7.0.4.2) + activerecord (= 7.0.4.2) + activestorage (= 7.0.4.2) + activesupport (= 7.0.4.2) bundler (>= 1.15.0) - railties (= 7.0.4.1) + railties (= 7.0.4.2) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.4) + rails-html-sanitizer (1.5.0) loofah (~> 2.19, >= 2.19.1) - railties (7.0.4.1) - actionpack (= 7.0.4.1) - activesupport (= 7.0.4.1) + railties (7.0.4.2) + actionpack (= 7.0.4.2) + activesupport (= 7.0.4.2) method_source rake (>= 12.2) thor (~> 1.0) @@ -313,36 +313,38 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - redcarpet (3.5.1) - redis (4.8.0) - regexp_parser (2.5.0) + redcarpet (3.6.0) + redis (4.8.1) + redis-client (0.12.2) + connection_pool + regexp_parser (2.7.0) request_store (1.5.1) rack (>= 1.4) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) rexml (3.2.5) - roo (2.9.0) + roo (2.10.0) nokogiri (~> 1) rubyzip (>= 1.3.0, < 3.0.0) - rotp (6.2.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.1) + rotp (6.2.2) + rspec-core (3.12.1) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.11.1) + rspec-support (~> 3.12.0) + rspec-rails (6.0.1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) + rspec-support (3.12.0) rubocop (1.25.0) parallel (~> 1.10) parser (>= 3.1.0.0) @@ -360,7 +362,7 @@ GEM rubocop-rails (= 2.13.2) rubocop-rake (= 0.6.0) rubocop-rspec (= 2.7.0) - rubocop-performance (1.15.0) + rubocop-performance (1.16.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.13.2) @@ -374,39 +376,39 @@ GEM ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) - selenium-webdriver (4.4.0) - childprocess (>= 0.5, < 5.0) + selenium-webdriver (4.8.1) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sentry-rails (5.4.2) + sentry-rails (5.8.0) railties (>= 5.0) - sentry-ruby (~> 5.4.2) - sentry-ruby (5.4.2) + sentry-ruby (~> 5.8.0) + sentry-ruby (5.8.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (6.5.5) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.5.0) - sidekiq-cron (1.8.0) - fugit (~> 1) + sidekiq (7.0.5) + concurrent-ruby (< 2) + connection_pool (>= 2.3.0) + rack (>= 2.2.4) + redis-client (>= 0.11.0) + sidekiq-cron (1.9.1) + fugit (~> 1.8) sidekiq (>= 4.2.1) - simplecov (0.21.2) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) - stimulus-rails (1.1.0) + stimulus-rails (1.2.1) railties (>= 6.0.0) thor (1.2.1) - timecop (0.9.5) - timeout (0.3.1) - tzinfo (2.0.5) + timecop (0.9.6) + timeout (0.3.2) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) uk_postcode (2.1.8) - unicode-display_width (2.3.0) + unicode-display_width (2.4.2) view_component (2.69.0) activesupport (>= 5.0.0, < 8.0) concurrent-ruby (~> 1.0) @@ -428,7 +430,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.6) + zeitwerk (2.6.7) PLATFORMS arm64-darwin-21 @@ -454,7 +456,7 @@ DEPENDENCIES factory_bot_rails faker govuk-components - govuk_design_system_formbuilder + govuk_design_system_formbuilder (= 3.1.2) govuk_markdown jsbundling-rails json-schema diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 7ae06f7e4..81d9246ec 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -13,7 +13,7 @@ Capybara.register_driver :headless do |app| options = Selenium::WebDriver::Firefox::Options.new options.add_argument("--headless") - Capybara::Selenium::Driver.new(app, browser: :firefox, capabilities: options) + Capybara::Selenium::Driver.new(app, browser: :firefox, options:) end Capybara.javascript_driver = :headless From 4a31906d45dac7875b8113a924dd4897d37f004b Mon Sep 17 00:00:00 2001 From: David May-Miller Date: Tue, 21 Feb 2023 16:53:30 +0000 Subject: [PATCH 26/30] CLDC-853 Added validations for sales income2 (#1101) * CLDC-853 Added hard validations for sales income2 * CLDC-853 Added soft validation for sales income2 * CLDC-853 Fix tests broken by new code * CLDC-853 Add new tests for new page and refactor slightly * CLDC-853 Fix linting errors * CLDC-853 Rename migration and update schema version * CLDC-853 Fix broken sales income2 test * CLDC-853 Rename migration * CLDC-853 Move income 2 to cya card 2 and commonise combined income validation * CLDC-853 Actually use the validate_combined_income method * combine duplicate methods after rebase, ensure hard validations are triggered on all relevant fields * move validation on child income to financial validations to stop it being triggered on lettings logs, minor amendments to tests broken by changes * revamp financial validations tests against income to reflect updates * amend child income validation to reflect specifications and write tests to cover this validation * correct linting errors and play a little code golf * change copy for some validations, add sales log method and amend interruption screen helper to support this * extract duplicate code to private method * update buyer 1 and 2 income value check to be consistent * remove ecstat from income checks, the only ecstat we care about is child which is dealt with elsewhere * rename constant struct with same anme as existing variable * amend tests to reflect the chagnes in validations and copy * enable currency formatting of numbers for inserting into informative_text or title_text * update evil test in form handler spec * rebase and fix conflicts and tests * change a variable name and correct minor rebase errors * update interruption screen helper tests * correct linting errors, minor test failure and typo * add tests for new sales log method for formatting currency * fix merge conflicts --------- Co-authored-by: Arthur Campbell --- app/helpers/interruption_screen_helper.rb | 33 ++- ...bout_price_shared_ownership_value_check.rb | 6 +- .../sales/pages/buyer1_income_value_check.rb | 15 ++ .../sales/pages/buyer2_income_value_check.rb | 35 +++ .../questions/buyer1_income_value_check.rb | 2 +- .../form/sales/questions/buyer2_income.rb | 1 + .../questions/buyer2_income_value_check.rb | 25 ++ .../subsections/household_characteristics.rb | 3 +- .../income_benefits_and_savings.rb | 1 + app/models/log.rb | 4 + app/models/sales_log.rb | 18 ++ .../sales/financial_validations.rb | 63 +++-- .../validations/sales/soft_validations.rb | 12 +- config/locales/en.yml | 12 +- .../20230105103134_add_income2_value_check.rb | 5 + db/schema.rb | 1 + .../interruption_screen_helper_spec.rb | 53 ++++- .../buyer1_income_value_check_spec.rb | 2 +- .../household_characteristics_spec.rb | 6 +- .../income_benefits_and_savings_spec.rb | 2 + spec/models/form_handler_spec.rb | 4 +- spec/models/sales_log_spec.rb | 23 ++ .../sales/financial_validations_spec.rb | 222 ++++++++++++------ 23 files changed, 420 insertions(+), 128 deletions(-) create mode 100644 app/models/form/sales/pages/buyer2_income_value_check.rb create mode 100644 app/models/form/sales/questions/buyer2_income_value_check.rb create mode 100644 db/migrate/20230105103134_add_income2_value_check.rb diff --git a/app/helpers/interruption_screen_helper.rb b/app/helpers/interruption_screen_helper.rb index c30ba7bfa..939f70bb2 100644 --- a/app/helpers/interruption_screen_helper.rb +++ b/app/helpers/interruption_screen_helper.rb @@ -1,17 +1,10 @@ module InterruptionScreenHelper - def display_informative_text(informative_text, lettings_log) + def display_informative_text(informative_text, log) return "" unless informative_text["arguments"] translation_params = {} informative_text["arguments"].each do |argument| - value = if argument["label"] - pre_casing_value = lettings_log.form.get_question(argument["key"], lettings_log).answer_label(lettings_log) - pre_casing_value.downcase - elsif argument["currency"] - number_to_currency(lettings_log.public_send(argument["key"]), delimiter: ",", format: "%n", unit: "£") - else - lettings_log.public_send(argument["key"]) - end + value = get_value_from_argument(log, argument) translation_params[argument["i18n_template"].to_sym] = value end @@ -24,21 +17,27 @@ module InterruptionScreenHelper end end - def display_title_text(title_text, lettings_log) + def display_title_text(title_text, log) return "" if title_text.nil? translation_params = {} arguments = title_text["arguments"] || {} arguments.each do |argument| - value = if argument["label"] - lettings_log.form.get_question(argument["key"], lettings_log).answer_label(lettings_log).downcase - elsif argument["currency"] - number_to_currency(lettings_log.public_send(argument["key"]), delimiter: ",", format: "%n", unit: "£") - else - lettings_log.public_send(argument["key"]) - end + value = get_value_from_argument(log, argument) translation_params[argument["i18n_template"].to_sym] = value end I18n.t(title_text["translation"], **translation_params).to_s end + +private + + def get_value_from_argument(log, argument) + if argument["label"] + log.form.get_question(argument["key"], log).answer_label(log).downcase + elsif argument["arguments_for_key"] + log.public_send(argument["key"], argument["arguments_for_key"]) + else + log.public_send(argument["key"]) + end + end end diff --git a/app/models/form/sales/pages/about_price_shared_ownership_value_check.rb b/app/models/form/sales/pages/about_price_shared_ownership_value_check.rb index f4f0955cd..5b668006a 100644 --- a/app/models/form/sales/pages/about_price_shared_ownership_value_check.rb +++ b/app/models/form/sales/pages/about_price_shared_ownership_value_check.rb @@ -20,14 +20,12 @@ class Form::Sales::Pages::AboutPriceSharedOwnershipValueCheck < ::Form::Page "translation" => "soft_validations.purchase_price.hint_text", "arguments" => [ { - "key" => "purchase_price_soft_min_or_soft_max", - "label" => false, + "key" => "field_formatted_as_currency", + "arguments_for_key" => "purchase_price_soft_min_or_soft_max", "i18n_template" => "soft_min_or_soft_max", - "currency" => true, }, { "key" => "purchase_price_min_or_max_text", - "label" => false, "i18n_template" => "min_or_max", }, ], diff --git a/app/models/form/sales/pages/buyer1_income_value_check.rb b/app/models/form/sales/pages/buyer1_income_value_check.rb index 04540c47f..48d8f5fff 100644 --- a/app/models/form/sales/pages/buyer1_income_value_check.rb +++ b/app/models/form/sales/pages/buyer1_income_value_check.rb @@ -6,6 +6,21 @@ class Form::Sales::Pages::Buyer1IncomeValueCheck < ::Form::Page "income1_under_soft_min?" => true, }, ] + @title_text = { + "translation" => "soft_validations.income.under_soft_min_for_economic_status", + "arguments" => [ + { + "key" => "field_formatted_as_currency", + "arguments_for_key" => "income1", + "i18n_template" => "income", + }, + { + "key" => "income_soft_min_for_ecstat", + "arguments_for_key" => "ecstat1", + "i18n_template" => "minimum", + }, + ], + } @informative_text = {} end diff --git a/app/models/form/sales/pages/buyer2_income_value_check.rb b/app/models/form/sales/pages/buyer2_income_value_check.rb new file mode 100644 index 000000000..598c4c7a6 --- /dev/null +++ b/app/models/form/sales/pages/buyer2_income_value_check.rb @@ -0,0 +1,35 @@ +class Form::Sales::Pages::Buyer2IncomeValueCheck < ::Form::Page + def initialize(id, hsh, subsection) + super + @header = "" + @description = "" + @subsection = subsection + @depends_on = [ + { + "income2_under_soft_min?" => true, + }, + ] + @title_text = { + "translation" => "soft_validations.income.under_soft_min_for_economic_status", + "arguments" => [ + { + "key" => "field_formatted_as_currency", + "arguments_for_key" => "income2", + "i18n_template" => "income", + }, + { + "key" => "income_soft_min_for_ecstat", + "arguments_for_key" => "ecstat2", + "i18n_template" => "minimum", + }, + ], + } + @informative_text = {} + end + + def questions + @questions ||= [ + Form::Sales::Questions::Buyer2IncomeValueCheck.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/questions/buyer1_income_value_check.rb b/app/models/form/sales/questions/buyer1_income_value_check.rb index 0913dd788..8843d6736 100644 --- a/app/models/form/sales/questions/buyer1_income_value_check.rb +++ b/app/models/form/sales/questions/buyer1_income_value_check.rb @@ -3,7 +3,7 @@ class Form::Sales::Questions::Buyer1IncomeValueCheck < ::Form::Question super @id = "income1_value_check" @check_answer_label = "Income confirmation" - @header = "Are you sure this income is correct?" + @header = "Are you sure this is correct?" @type = "interruption_screen" @answer_options = { "0" => { "value" => "Yes" }, diff --git a/app/models/form/sales/questions/buyer2_income.rb b/app/models/form/sales/questions/buyer2_income.rb index 680bf8ae7..f7d96720b 100644 --- a/app/models/form/sales/questions/buyer2_income.rb +++ b/app/models/form/sales/questions/buyer2_income.rb @@ -7,6 +7,7 @@ class Form::Sales::Questions::Buyer2Income < ::Form::Question @type = "numeric" @hint_text = "Provide the gross annual income (i.e. salary before tax) plus the annual amount of benefits, Universal Credit or pensions, and income from investments." @min = 0 + @max = 999_999 @step = 1 @width = 5 @prefix = "£" diff --git a/app/models/form/sales/questions/buyer2_income_value_check.rb b/app/models/form/sales/questions/buyer2_income_value_check.rb new file mode 100644 index 000000000..9508cc59a --- /dev/null +++ b/app/models/form/sales/questions/buyer2_income_value_check.rb @@ -0,0 +1,25 @@ +class Form::Sales::Questions::Buyer2IncomeValueCheck < ::Form::Question + def initialize(id, hsh, page) + super + @id = "income2_value_check" + @check_answer_label = "Income confirmation" + @header = "Are you sure this is correct?" + @type = "interruption_screen" + @answer_options = { + "0" => { "value" => "Yes" }, + "1" => { "value" => "No" }, + } + @hidden_in_check_answers = { + "depends_on" => [ + { + "income2_value_check" => 0, + }, + { + "income2_value_check" => 1, + }, + ], + } + @check_answers_card_number = 2 + @page = page + end +end diff --git a/app/models/form/sales/subsections/household_characteristics.rb b/app/models/form/sales/subsections/household_characteristics.rb index 55ec0b137..181460ae1 100644 --- a/app/models/form/sales/subsections/household_characteristics.rb +++ b/app/models/form/sales/subsections/household_characteristics.rb @@ -34,7 +34,8 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Sales::Pages::RetirementValueCheck.new("gender_2_buyer_retirement_value_check", nil, self, person_index: 2), ethnic_pages_for_buyer_2, Form::Sales::Pages::Buyer2WorkingSituation.new(nil, nil, self), - Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_buyer_retirement_value_check", nil, self, person_index: 2), + Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_retirement_value_check_joint_purchase", nil, self, person_index: 2), + Form::Sales::Pages::Buyer2IncomeValueCheck.new("working_situation_buyer_2_income_value_check", nil, self), Form::Sales::Pages::Buyer2LiveInProperty.new(nil, nil, self), Form::Sales::Pages::NumberOfOthersInProperty.new(nil, nil, self), Form::Sales::Pages::PersonKnown.new("person_2_known", nil, self, person_index: 2), diff --git a/app/models/form/sales/subsections/income_benefits_and_savings.rb b/app/models/form/sales/subsections/income_benefits_and_savings.rb index fe95bfd8c..7642e84bd 100644 --- a/app/models/form/sales/subsections/income_benefits_and_savings.rb +++ b/app/models/form/sales/subsections/income_benefits_and_savings.rb @@ -15,6 +15,7 @@ class Form::Sales::Subsections::IncomeBenefitsAndSavings < ::Form::Subsection Form::Sales::Pages::MortgageValueCheck.new("buyer_1_mortgage_value_check", nil, self, 1), Form::Sales::Pages::Buyer2Income.new(nil, nil, self), Form::Sales::Pages::MortgageValueCheck.new("buyer_2_income_mortgage_value_check", nil, self, 2), + Form::Sales::Pages::Buyer2IncomeValueCheck.new("buyer_2_income_value_check", nil, self), Form::Sales::Pages::Buyer2Mortgage.new(nil, nil, self), Form::Sales::Pages::MortgageValueCheck.new("buyer_2_mortgage_value_check", nil, self, 2), Form::Sales::Pages::HousingBenefits.new(nil, nil, self), diff --git a/app/models/log.rb b/app/models/log.rb index 0cd3add92..d8aa9e236 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -147,4 +147,8 @@ private self[is_inferred_key] = false self[postcode_key] = nil end + + def format_as_currency(num_string) + ActionController::Base.helpers.number_to_currency(num_string, unit: "£") + end end diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index 6ed653955..447f37597 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -124,6 +124,10 @@ class SalesLog < Log la && LONDON_BOROUGHS.include?(la) end + def property_not_in_london? + !london_property? + end + def income1_used_for_mortgage? inc1mort == 1 end @@ -256,4 +260,18 @@ class SalesLog < Log def purchase_price_soft_max LaSaleRange.find_by(start_year: collection_start_year, la:, bedrooms: beds).soft_max end + + def income_soft_min_for_ecstat(ecstat_field) + economic_status_code = public_send(ecstat_field) + + return unless ALLOWED_INCOME_RANGES_SALES + + soft_min = ALLOWED_INCOME_RANGES_SALES[economic_status_code]&.soft_min + format_as_currency(soft_min) + end + + def field_formatted_as_currency(field_name) + field_value = public_send(field_name) + format_as_currency(field_value) + end end diff --git a/app/models/validations/sales/financial_validations.rb b/app/models/validations/sales/financial_validations.rb index df7dc8df5..21f3743ca 100644 --- a/app/models/validations/sales/financial_validations.rb +++ b/app/models/validations/sales/financial_validations.rb @@ -3,20 +3,36 @@ module Validations::Sales::FinancialValidations # or 'validate_' to run on submit as well def validate_income1(record) - if record.ecstat1 && record.income1 && record.la && record.ownershipsch == 1 - if record.london_property? - record.errors.add :income1, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000 - record.errors.add :ecstat1, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000 - record.errors.add :ownershipsch, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000 - record.errors.add :la, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000 - record.errors.add :postcode_full, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000 - elsif record.income1 > 80_000 - record.errors.add :income1, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000) - record.errors.add :ecstat1, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000) - record.errors.add :ownershipsch, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000) - record.errors.add :la, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000) if record.income1 > 80_000 - record.errors.add :postcode_full, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000) if record.income1 > 80_000 - end + return unless record.income1 && record.la && record.shared_ownership_scheme? + + relevant_fields = %i[income1 ownershipsch la postcode_full] + if record.london_property? && record.income1 > 90_000 + relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_london") } + elsif record.property_not_in_london? && record.income1 > 80_000 + relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_outside_london") } + end + end + + def validate_income2(record) + return unless record.income2 && record.la && record.shared_ownership_scheme? + + relevant_fields = %i[income2 ownershipsch la postcode_full] + if record.london_property? && record.income2 > 90_000 + relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_london") } + elsif record.property_not_in_london? && record.income2 > 80_000 + relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_outside_london") } + end + end + + def validate_combined_income(record) + return unless record.income1 && record.income2 && record.la && record.shared_ownership_scheme? + + combined_income = record.income1 + record.income2 + relevant_fields = %i[income1 income2 ownershipsch la postcode_full] + if record.london_property? && combined_income > 90_000 + relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.combined_over_hard_max_for_london") } + elsif record.property_not_in_london? && combined_income > 80_000 + relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.combined_over_hard_max_for_outside_london") } end end @@ -36,6 +52,15 @@ module Validations::Sales::FinancialValidations end end + def validate_child_income(record) + return unless record.income2 && record.ecstat2 + + if record.income2.positive? && is_economic_status_child?(record.ecstat2) + record.errors.add :ecstat2, I18n.t("validations.financial.income.child_has_income") + record.errors.add :income2, I18n.t("validations.financial.income.child_has_income") + end + end + def validate_percentage_owned_not_too_much_if_older_person(record) return unless record.old_persons_shared_ownership? && record.stairowned @@ -44,4 +69,14 @@ module Validations::Sales::FinancialValidations record.errors.add :type, I18n.t("validations.financial.staircasing.older_person_percentage_owned_maximum_75") end end + +private + + def is_relationship_child?(relationship) + relationship == "C" + end + + def is_economic_status_child?(economic_status) + economic_status == 9 + end end diff --git a/app/models/validations/sales/soft_validations.rb b/app/models/validations/sales/soft_validations.rb index a7b9fd4c0..c1704d948 100644 --- a/app/models/validations/sales/soft_validations.rb +++ b/app/models/validations/sales/soft_validations.rb @@ -1,5 +1,5 @@ module Validations::Sales::SoftValidations - ALLOWED_INCOME_RANGES = { + ALLOWED_INCOME_RANGES_SALES = { 1 => OpenStruct.new(soft_min: 5000), 2 => OpenStruct.new(soft_min: 1500), 3 => OpenStruct.new(soft_min: 1000), @@ -8,9 +8,15 @@ module Validations::Sales::SoftValidations }.freeze def income1_under_soft_min? - return false unless ecstat1 && income1 && ALLOWED_INCOME_RANGES[ecstat1] + return false unless ecstat1 && income1 && ALLOWED_INCOME_RANGES_SALES[ecstat1] - income1 < ALLOWED_INCOME_RANGES[ecstat1][:soft_min] + income1 < ALLOWED_INCOME_RANGES_SALES[ecstat1][:soft_min] + end + + def income2_under_soft_min? + return false unless ecstat2 && income2 && ALLOWED_INCOME_RANGES_SALES[ecstat2] + + income2 < ALLOWED_INCOME_RANGES_SALES[ecstat2][:soft_min] end def staircase_bought_above_fifty? diff --git a/config/locales/en.yml b/config/locales/en.yml index 747ed4414..bc54b74ce 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -223,8 +223,12 @@ en: under_hard_min: "Net income cannot be less than £%{hard_min} per week given the tenant’s working situation" freq_missing: "Select how often the household receives income" earnings_missing: "Enter how much income the household has in total" - income1: - over_hard_max: "Income must be lower than £%{hard_max}" + income: + over_hard_max_for_london: "Income must not exceed £90,000 for properties within London local authorities" + over_hard_max_for_outside_london: "Income must not exceed £80,000 for properties outside London local authorities" + combined_over_hard_max_for_london: "Combined income must not exceed £90,000 for properties within London local authorities" + combined_over_hard_max_for_outside_london: "Combined income must not exceed £80,000 for properties outside London local authorities" + child_has_income: "Child's income must be £0" negative_currency: "Enter an amount above 0" rent: less_than_shortfall: "Enter an amount that is more than the shortfall in basic rent" @@ -467,6 +471,8 @@ en: message: "Net income is lower than expected based on the lead tenant’s working situation. Are you sure this is correct?" in_soft_max_range: message: "Net income is higher than expected based on the lead tenant’s working situation. Are you sure this is correct?" + income: + under_soft_min_for_economic_status: "You said income was %{income}, which is below this working situation's minimum (%{minimum})" rent: outside_range_title: "You told us the rent is %{brent}" min_hint_text: "The minimum rent expected for this type of property in this local authority is £%{soft_min_for_period}." @@ -559,4 +565,4 @@ en: one_argument: "This is based on the tenant’s work situation: %{ecstat1}" title_text: no_argument: "Some test text" - one_argument: "You said this: %{ecstat1}" + one_argument: "You said this: %{argument}" diff --git a/db/migrate/20230105103134_add_income2_value_check.rb b/db/migrate/20230105103134_add_income2_value_check.rb new file mode 100644 index 000000000..38a3ebc65 --- /dev/null +++ b/db/migrate/20230105103134_add_income2_value_check.rb @@ -0,0 +1,5 @@ +class AddIncome2ValueCheck < ActiveRecord::Migration[7.0] + def change + add_column :sales_logs, :income2_value_check, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 2c1ff7692..6cb5420d5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -520,6 +520,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_10_143120) do t.integer "value_value_check" t.integer "old_persons_shared_ownership_value_check" t.integer "staircase_bought_value_check" + t.integer "income2_value_check" t.integer "monthly_charges_value_check" t.integer "details_known_5" t.integer "details_known_6" diff --git a/spec/helpers/interruption_screen_helper_spec.rb b/spec/helpers/interruption_screen_helper_spec.rb index 8153b9d7b..f46aca9ae 100644 --- a/spec/helpers/interruption_screen_helper_spec.rb +++ b/spec/helpers/interruption_screen_helper_spec.rb @@ -13,6 +13,7 @@ RSpec.describe InterruptionScreenHelper do earnings: 750, incfreq: 1, created_by: user, + sex1: "F", ) end @@ -94,6 +95,54 @@ RSpec.describe InterruptionScreenHelper do .to eq("") end end + + context "when an argument is given not for a label" do + translation = "test.title_text.one_argument" + it "returns the correct text" do + informative_text_hash = { + "translation" => translation, + "arguments" => [ + { + "key" => "earnings", + "i18n_template" => "argument", + }, + ], + } + expect(display_informative_text(informative_text_hash, lettings_log)).to eq(I18n.t(translation, argument: lettings_log.earnings)) + end + end + + context "when and argument is given with a key and arguments for the key" do + it "makes the correct method call" do + informative_text_hash = { + "arguments" => [ + { + "key" => "retirement_age_for_person", + "arguments_for_key" => 1, + "i18n_template" => "argument", + }, + ], + } + allow(lettings_log).to receive(:retirement_age_for_person) + display_informative_text(informative_text_hash, lettings_log) + expect(lettings_log).to have_received(:retirement_age_for_person).with(1) + end + + it "returns the correct text" do + translation = "test.title_text.one_argument" + informative_text_hash = { + "translation" => translation, + "arguments" => [ + { + "key" => "retirement_age_for_person", + "arguments_for_key" => 1, + "i18n_template" => "argument", + }, + ], + } + expect(display_informative_text(informative_text_hash, lettings_log)).to eq(I18n.t(translation, argument: lettings_log.retirement_age_for_person(1))) + end + end end describe "display_title_text" do @@ -113,12 +162,12 @@ RSpec.describe InterruptionScreenHelper do { "key" => "ecstat1", "label" => true, - "i18n_template" => "ecstat1", + "i18n_template" => "argument", }, ], } expect(display_title_text(title_text, lettings_log)) - .to eq(I18n.t("test.title_text.one_argument", ecstat1: lettings_log.form.get_question("ecstat1", lettings_log).answer_label(lettings_log).downcase)) + .to eq(I18n.t("test.title_text.one_argument", argument: lettings_log.form.get_question("ecstat1", lettings_log).answer_label(lettings_log).downcase)) end end diff --git a/spec/models/form/sales/questions/buyer1_income_value_check_spec.rb b/spec/models/form/sales/questions/buyer1_income_value_check_spec.rb index 552f4f56f..89801a398 100644 --- a/spec/models/form/sales/questions/buyer1_income_value_check_spec.rb +++ b/spec/models/form/sales/questions/buyer1_income_value_check_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Form::Sales::Questions::Buyer1IncomeValueCheck, type: :model do end it "has the correct header" do - expect(question.header).to eq("Are you sure this income is correct?") + expect(question.header).to eq("Are you sure this is correct?") end it "has the correct check_answer_label" do diff --git a/spec/models/form/sales/subsections/household_characteristics_spec.rb b/spec/models/form/sales/subsections/household_characteristics_spec.rb index 76109b7f4..33e9a1409 100644 --- a/spec/models/form/sales/subsections/household_characteristics_spec.rb +++ b/spec/models/form/sales/subsections/household_characteristics_spec.rb @@ -46,7 +46,8 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model buyer_2_gender_identity gender_2_buyer_retirement_value_check buyer_2_working_situation - working_situation_2_buyer_retirement_value_check + working_situation_2_retirement_value_check_joint_purchase + working_situation_buyer_2_income_value_check buyer_2_live_in_property number_of_others_in_property person_2_known @@ -126,7 +127,8 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model buyer_2_ethnic_background_mixed buyer_2_ethnic_background_white buyer_2_working_situation - working_situation_2_buyer_retirement_value_check + working_situation_2_retirement_value_check_joint_purchase + working_situation_buyer_2_income_value_check buyer_2_live_in_property number_of_others_in_property person_2_known diff --git a/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb b/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb index 74002d5e0..cfc677233 100644 --- a/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb +++ b/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb @@ -27,6 +27,7 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model buyer_1_mortgage_value_check buyer_2_income buyer_2_income_mortgage_value_check + buyer_2_income_value_check buyer_2_mortgage buyer_2_mortgage_value_check housing_benefits @@ -52,6 +53,7 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model buyer_1_mortgage_value_check buyer_2_income buyer_2_income_mortgage_value_check + buyer_2_income_value_check buyer_2_mortgage buyer_2_mortgage_value_check housing_benefits diff --git a/spec/models/form_handler_spec.rb b/spec/models/form_handler_spec.rb index 9b4920198..b04980ce4 100644 --- a/spec/models/form_handler_spec.rb +++ b/spec/models/form_handler_spec.rb @@ -54,14 +54,14 @@ RSpec.describe FormHandler do it "is able to load a current sales form" do form = form_handler.get_form("current_sales") expect(form).to be_a(Form) - expect(form.pages.count).to eq(179) + expect(form.pages.count).to eq(181) expect(form.name).to eq("2022_2023_sales") end it "is able to load a previous sales form" do form = form_handler.get_form("previous_sales") expect(form).to be_a(Form) - expect(form.pages.count).to eq(179) + expect(form.pages.count).to eq(181) expect(form.name).to eq("2021_2022_sales") end end diff --git a/spec/models/sales_log_spec.rb b/spec/models/sales_log_spec.rb index 523c2924b..7b9f7c7ef 100644 --- a/spec/models/sales_log_spec.rb +++ b/spec/models/sales_log_spec.rb @@ -266,6 +266,7 @@ RSpec.describe SalesLog, type: :model do relat4: "X", relat5: "X", relat6: "P", + income2: 0, ecstat2: 9, ecstat3: 7, age1: 47, @@ -370,4 +371,26 @@ RSpec.describe SalesLog, type: :model do expect(completed_sales_log.expected_shared_ownership_deposit_value).to eq(500) end end + + describe "#field_formatted_as_currency" do + let(:completed_sales_log) { FactoryBot.create(:sales_log, :completed) } + + it "returns small numbers correctly formatted as currency" do + completed_sales_log.update!(savings: 4) + + expect(completed_sales_log.field_formatted_as_currency("savings")).to eq("£4.00") + end + + it "returns quite large numbers correctly formatted as currency" do + completed_sales_log.update!(savings: 40_000) + + expect(completed_sales_log.field_formatted_as_currency("savings")).to eq("£40,000.00") + end + + it "returns very large numbers correctly formatted as currency" do + completed_sales_log.update!(savings: 400_000_000) + + expect(completed_sales_log.field_formatted_as_currency("savings")).to eq("£400,000,000.00") + end + end end diff --git a/spec/models/validations/sales/financial_validations_spec.rb b/spec/models/validations/sales/financial_validations_spec.rb index 88f36943f..fae0e4536 100644 --- a/spec/models/validations/sales/financial_validations_spec.rb +++ b/spec/models/validations/sales/financial_validations_spec.rb @@ -5,75 +5,110 @@ RSpec.describe Validations::Sales::FinancialValidations do let(:validator_class) { Class.new { include Validations::Sales::FinancialValidations } } - describe "income validations" do - let(:record) { FactoryBot.create(:sales_log, ownershipsch: 1, la: "E08000035") } - - context "with shared ownership" do - context "and non london borough" do - (0..8).each do |ecstat| - it "adds an error when buyer 1 income is over hard max for ecstat #{ecstat}" do - record.income1 = 85_000 - record.ecstat1 = ecstat - financial_validator.validate_income1(record) - expect(record.errors["income1"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000)) - expect(record.errors["ecstat1"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000)) - expect(record.errors["ownershipsch"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000)) - expect(record.errors["la"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000)) - expect(record.errors["postcode_full"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000)) - end - end - - it "validates that the income is within the expected range for the tenant’s employment status" do - record.income1 = 75_000 - record.ecstat1 = 1 - financial_validator.validate_income1(record) - expect(record.errors["income1"]).to be_empty - expect(record.errors["ecstat1"]).to be_empty - expect(record.errors["ownershipsch"]).to be_empty - expect(record.errors["la"]).to be_empty - expect(record.errors["postcode_full"]).to be_empty - end - end - - context "and a london borough" do - before do - record.update!(la: "E09000030") - record.reload - end - - (0..8).each do |ecstat| - it "adds an error when buyer 1 income is over hard max for ecstat #{ecstat}" do - record.income1 = 95_000 - record.ecstat1 = ecstat - financial_validator.validate_income1(record) - expect(record.errors["income1"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000)) - expect(record.errors["ecstat1"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000)) - expect(record.errors["ownershipsch"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000)) - expect(record.errors["la"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000)) - expect(record.errors["postcode_full"]) - .to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000)) - end - end - - it "validates that the income is within the expected range for the tenant’s employment status" do - record.income1 = 85_000 - record.ecstat1 = 1 - financial_validator.validate_income1(record) - expect(record.errors["income1"]).to be_empty - expect(record.errors["ecstat1"]).to be_empty - expect(record.errors["ownershipsch"]).to be_empty - expect(record.errors["la"]).to be_empty - expect(record.errors["postcode_full"]).to be_empty - end + describe "income validations for shared ownership" do + let(:record) { FactoryBot.create(:sales_log, ownershipsch: 1) } + + context "when buying in a non london borough" do + before do + record.update!(la: "E08000035") + record.reload + end + + it "adds errors if buyer 1 has income over 80,000" do + record.income1 = 85_000 + financial_validator.validate_income1(record) + expect(record.errors["income1"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london")) + expect(record.errors["ownershipsch"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london")) + expect(record.errors["la"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london")) + expect(record.errors["postcode_full"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london")) + end + + it "adds errors if buyer 2 has income over 80,000" do + record.income2 = 85_000 + financial_validator.validate_income2(record) + expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london")) + expect(record.errors["ownershipsch"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london")) + expect(record.errors["la"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london")) + expect(record.errors["postcode_full"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london")) + end + + it "does not add errors if buyer 1 has income below 80_000" do + record.income1 = 75_000 + financial_validator.validate_income1(record) + expect(record.errors).to be_empty + end + + it "does not add errors if buyer 2 has income below 80_000" do + record.income2 = 75_000 + financial_validator.validate_income2(record) + expect(record.errors).to be_empty + end + + it "adds errors when combined income is over 80_000" do + record.income1 = 45_000 + record.income2 = 40_000 + financial_validator.validate_combined_income(record) + expect(record.errors["income1"]).to include(match I18n.t("validations.financial.income.combined_over_hard_max_for_outside_london")) + expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.combined_over_hard_max_for_outside_london")) + end + + it "does not add errors when combined income is under 80_000" do + record.income1 = 35_000 + record.income2 = 40_000 + financial_validator.validate_combined_income(record) + expect(record.errors).to be_empty + end + end + + context "when buying in a london borough" do + before do + record.update!(la: "E09000030") + record.reload + end + + it "adds errors if buyer 1 has income over 90,000" do + record.income1 = 95_000 + financial_validator.validate_income1(record) + expect(record.errors["income1"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london")) + expect(record.errors["ownershipsch"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london")) + expect(record.errors["la"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london")) + expect(record.errors["postcode_full"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london")) + end + + it "adds errors if buyer 2 has income over 90,000" do + record.income2 = 95_000 + financial_validator.validate_income2(record) + expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london")) + expect(record.errors["ownershipsch"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london")) + expect(record.errors["la"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london")) + expect(record.errors["postcode_full"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london")) + end + + it "does not add errors if buyer 1 has income below 90_000" do + record.income1 = 75_000 + financial_validator.validate_income1(record) + expect(record.errors).to be_empty + end + + it "does not add errors if buyer 2 has income below 90_000" do + record.income2 = 75_000 + financial_validator.validate_income2(record) + expect(record.errors).to be_empty + end + + it "adds errors when combined income is over 90_000" do + record.income1 = 55_000 + record.income2 = 40_000 + financial_validator.validate_combined_income(record) + expect(record.errors["income1"]).to include(match I18n.t("validations.financial.income.combined_over_hard_max_for_london")) + expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.combined_over_hard_max_for_london")) + end + + it "does not add errors when combined income is under 90_000" do + record.income1 = 35_000 + record.income2 = 40_000 + financial_validator.validate_combined_income(record) + expect(record.errors).to be_empty end end end @@ -96,7 +131,7 @@ RSpec.describe Validations::Sales::FinancialValidations do it "does not add an error if the cash discount is in the expected range" do record.cashdis = 10_000 financial_validator.validate_cash_discount(record) - expect(record.errors["cashdis"]).to be_empty + expect(record.errors).to be_empty end end @@ -107,16 +142,14 @@ RSpec.describe Validations::Sales::FinancialValidations do record.stairbought = 20 record.stairowned = 40 financial_validator.validate_percentage_bought_not_greater_than_percentage_owned(record) - expect(record.errors["stairbought"]).to be_empty - expect(record.errors["stairowned"]).to be_empty + expect(record.errors).to be_empty end it "does not add an error if the percentage bought is equal to the percentage owned" do record.stairbought = 30 record.stairowned = 30 financial_validator.validate_percentage_bought_not_greater_than_percentage_owned(record) - expect(record.errors["stairbought"]).to be_empty - expect(record.errors["stairowned"]).to be_empty + expect(record.errors).to be_empty end it "adds an error to stairowned and not stairbought if the percentage bought is more than the percentage owned" do @@ -135,8 +168,7 @@ RSpec.describe Validations::Sales::FinancialValidations do record.type = 2 record.stairowned = 80 financial_validator.validate_percentage_owned_not_too_much_if_older_person(record) - expect(record.errors["stairowned"]).to be_empty - expect(record.errors["type"]).to be_empty + expect(record.errors).to be_empty end end @@ -145,8 +177,7 @@ RSpec.describe Validations::Sales::FinancialValidations do record.type = 24 record.stairowned = 50 financial_validator.validate_percentage_owned_not_too_much_if_older_person(record) - expect(record.errors["stairowned"]).to be_empty - expect(record.errors["type"]).to be_empty + expect(record.errors).to be_empty end it "adds an error when percentage owned after staircasing transaction exceeds 75%" do @@ -158,4 +189,39 @@ RSpec.describe Validations::Sales::FinancialValidations do end end end + + describe "#validate_child_income" do + let(:record) { FactoryBot.create(:sales_log) } + + context "when buyer 2 is not a child" do + before do + record.update!(ecstat2: rand(0..8)) + record.reload + end + + it "does not add an error if buyer 2 has an income" do + record.ecstat2 = rand(0..8) + record.income2 = 40_000 + financial_validator.validate_child_income(record) + expect(record.errors).to be_empty + end + end + + context "when buyer 2 is a child" do + it "does not add an error if buyer 2 has no income" do + record.ecstat2 = 9 + record.income2 = 0 + financial_validator.validate_child_income(record) + expect(record.errors).to be_empty + end + + it "adds errors if buyer 2 has an income" do + record.ecstat2 = 9 + record.income2 = 40_000 + financial_validator.validate_child_income(record) + expect(record.errors["ecstat2"]).to include(match I18n.t("validations.financial.income.child_has_income")) + expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.child_has_income")) + end + end + end end From 5f9151158adde342d1b74873a2940eb120c1a3ec Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Wed, 22 Feb 2023 09:53:57 +0000 Subject: [PATCH 27/30] CLDC-1889 Bulk upload schemes + locations (#1329) * bulk upload validates scheme data * bulk upload schemes can use new core IDs * bulk upload handles locations --- app/models/location.rb | 6 + app/models/scheme.rb | 10 ++ .../bulk_upload/lettings/row_parser.rb | 50 ++++++++- spec/factories/location.rb | 5 + spec/factories/scheme.rb | 4 + .../bulk_upload/lettings/row_parser_spec.rb | 106 +++++++++++++++++- 6 files changed, 177 insertions(+), 4 deletions(-) diff --git a/app/models/location.rb b/app/models/location.rb index 36a134f81..3798c57e8 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -366,6 +366,12 @@ class Location < ApplicationRecord enum type_of_unit: TYPE_OF_UNIT + def self.find_by_id_on_mulitple_fields(id) + return if id.nil? + + where(id:).or(where(old_visible_id: id)).first + end + def postcode=(postcode) if postcode super UKPostcode.parse(postcode).to_s diff --git a/app/models/scheme.rb b/app/models/scheme.rb index f8cf6bf63..ca3624e3a 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -110,6 +110,16 @@ class Scheme < ApplicationRecord enum arrangement_type: ARRANGEMENT_TYPE, _suffix: true + def self.find_by_id_on_mulitple_fields(id) + return if id.nil? + + if id.start_with?("S") + where(id: id[1..]).first + else + where(old_visible_id: id).first + end + end + def id_to_display "S#{id}" end diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/row_parser.rb index 9cb3edb78..dcba8be28 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/row_parser.rb @@ -8,7 +8,7 @@ class BulkUpload::Lettings::RowParser attribute :field_1, :integer attribute :field_2 attribute :field_3 - attribute :field_4, :integer + attribute :field_4, :string attribute :field_5, :integer attribute :field_6 attribute :field_7, :string @@ -164,6 +164,12 @@ class BulkUpload::Lettings::RowParser validate :validate_managing_org_related validate :validate_managing_org_exists + validate :validate_scheme_related + validate :validate_scheme_exists + + validate :validate_location_related + validate :validate_location_exists + def valid? errors.clear @@ -199,6 +205,45 @@ class BulkUpload::Lettings::RowParser private + def validate_location_related + return if scheme.blank? || location.blank? + + unless location.scheme == scheme + block_log_creation! + errors.add(:field_5, "Scheme code must relate to a location that is owned by owning organisation or managing organisation") + end + end + + def location + return if scheme.nil? + + @location ||= scheme.locations.find_by_id_on_mulitple_fields(field_5) + end + + def validate_location_exists + if scheme && field_5.present? && location.nil? + errors.add(:field_5, "Location could be found with provided scheme code") + end + end + + def validate_scheme_related + return unless field_4.present? && scheme.present? + + owned_by_owning_org = owning_organisation && scheme.owning_organisation == owning_organisation + owned_by_managing_org = managing_organisation && scheme.owning_organisation == managing_organisation + + unless owned_by_owning_org || owned_by_managing_org + block_log_creation! + errors.add(:field_4, "This management group code does not belong to your organisation, or any of your stock owners / managing agents") + end + end + + def validate_scheme_exists + if field_4.present? && scheme.nil? + errors.add(:field_4, "The management group code is not correct") + end + end + def validate_managing_org_related if owning_organisation && managing_organisation && !owning_organisation.can_be_managed_by?(organisation: managing_organisation) block_log_creation! @@ -566,6 +611,7 @@ private attributes["managing_organisation_id"] = managing_organisation_id attributes["renewal"] = renewal attributes["scheme"] = scheme + attributes["location"] = location attributes["created_by"] = bulk_upload.user attributes["needstype"] = bulk_upload.needstype attributes["rent_type"] = rent_type @@ -943,6 +989,6 @@ private end def scheme - @scheme ||= Scheme.find_by(old_visible_id: field_4) + @scheme ||= Scheme.find_by_id_on_mulitple_fields(field_4) end end diff --git a/spec/factories/location.rb b/spec/factories/location.rb index 75b4380f5..f43da0ac8 100644 --- a/spec/factories/location.rb +++ b/spec/factories/location.rb @@ -10,6 +10,7 @@ FactoryBot.define do startdate { Time.zone.local(2022, 4, 1) } confirmed { true } scheme + trait :export do postcode { "SW1A 2AA" } name { "Downing Street" } @@ -19,5 +20,9 @@ FactoryBot.define do scheme { FactoryBot.create(:scheme, :export) } old_visible_id { "111" } end + + trait :with_old_visible_id do + old_visible_id { rand(9_999_999).to_s } + end end end diff --git a/spec/factories/scheme.rb b/spec/factories/scheme.rb index 6c08e269d..155dea11a 100644 --- a/spec/factories/scheme.rb +++ b/spec/factories/scheme.rb @@ -21,5 +21,9 @@ FactoryBot.define do primary_client_group { "G" } secondary_client_group { "M" } end + + trait :with_old_visible_id do + old_visible_id { rand(9_999_999) } + end end end diff --git a/spec/services/bulk_upload/lettings/row_parser_spec.rb b/spec/services/bulk_upload/lettings/row_parser_spec.rb index d1792e9df..7a09ba4b8 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/row_parser_spec.rb @@ -11,6 +11,8 @@ RSpec.describe BulkUpload::Lettings::RowParser do let(:owning_org) { create(:organisation, :with_old_visible_id) } let(:managing_org) { create(:organisation, :with_old_visible_id) } + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:location) { create(:location, :with_old_visible_id, scheme:) } let(:setup_section_params) do { @@ -85,7 +87,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do { bulk_upload:, field_1: "1", - field_4: "1", + field_4: scheme.old_visible_id, field_7: "123", field_96: now.day.to_s, field_97: now.month.to_s, @@ -296,10 +298,90 @@ RSpec.describe BulkUpload::Lettings::RowParser do context "when matching scheme cannot be found" do let(:attributes) { { bulk_upload:, field_1: "1", field_4: "123" } } - xit "returns an error" do + it "returns an error" do expect(parser.errors[:field_4]).to be_present end end + + context "when scheme belongs to someone else" do + let(:other_scheme) { create(:scheme, :with_old_visible_id) } + let(:attributes) { { bulk_upload:, field_1: "1", field_4: other_scheme.old_visible_id, field_111: owning_org.old_visible_id } } + + it "returns an error" do + expect(parser.errors[:field_4]).to include("This management group code does not belong to your organisation, or any of your stock owners / managing agents") + end + end + + context "when scheme belongs to owning org" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:attributes) { { bulk_upload:, field_1: "1", field_4: scheme.old_visible_id, field_111: owning_org.old_visible_id } } + + it "does not return an error" do + expect(parser.errors[:field_4]).to be_blank + end + end + + context "when scheme belongs to managing org" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: managing_org) } + let(:attributes) { { bulk_upload:, field_1: "1", field_4: scheme.old_visible_id, field_113: managing_org.old_visible_id } } + + it "does not return an error" do + expect(parser.errors[:field_4]).to be_blank + end + end + end + + describe "#field_5" do + context "when location does not exist" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:attributes) do + { + bulk_upload:, + field_1: "1", + field_4: scheme.old_visible_id, + field_5: "dontexist", + field_111: owning_org.old_visible_id, + } + end + + it "returns an error" do + expect(parser.errors[:field_5]).to be_present + end + end + + context "when location exists" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:attributes) do + { + bulk_upload:, + field_1: "1", + field_4: scheme.old_visible_id, + field_5: location.old_visible_id, + field_111: owning_org.old_visible_id, + } + end + + it "does not return an error" do + expect(parser.errors[:field_5]).to be_blank + end + end + + context "when location exists but not related" do + let(:location) { create(:scheme, :with_old_visible_id) } + let(:attributes) do + { + bulk_upload:, + field_1: "1", + field_4: scheme.old_visible_id, + field_5: location.old_visible_id, + field_111: owning_org.old_visible_id, + } + end + + it "returns an error" do + expect(parser.errors[:field_5]).to be_present + end + end end describe "#field_7" do @@ -592,6 +674,26 @@ RSpec.describe BulkUpload::Lettings::RowParser do end describe "#log" do + describe "#location" do + context "when lookup is via new core id" do + let(:attributes) { { bulk_upload:, field_4: scheme.old_visible_id, field_5: location.id, field_111: owning_org } } + + it "assigns the correct location" do + expect(parser.log.location).to eql(location) + end + end + end + + describe "#scheme" do + context "when lookup is via id prefixed with S" do + let(:attributes) { { bulk_upload:, field_4: "S#{scheme.id}", field_111: owning_org } } + + it "assigns the correct scheme" do + expect(parser.log.scheme).to eql(scheme) + end + end + end + describe "#owning_organisation" do context "when lookup is via id prefixed with ORG" do let(:attributes) { { bulk_upload:, field_111: "ORG#{owning_org.id}" } } From 68a1c17ae5d7d457bd8ceeb8a09eaaaca6ae72b7 Mon Sep 17 00:00:00 2001 From: Jack <113976590+bibblobcode@users.noreply.github.com> Date: Thu, 23 Feb 2023 09:08:28 +0000 Subject: [PATCH 28/30] CLDC-1916 Handle sales forms and validations during comparison period (#1317) * Add more time helpers * CLDC-1916 Validate this and next year start date in saleslogs * Add capybara-screenshot * Use sales_in_crossover_period? --- Gemfile | 1 + Gemfile.lock | 6 + app/helpers/collection_time_helper.rb | 16 ++ app/helpers/tasklist_helper.rb | 21 +-- app/models/form_handler.rb | 8 +- .../validations/sales/setup_validations.rb | 38 ++++- config/initializers/feature_toggle.rb | 7 +- config/locales/en.yml | 6 +- spec/helpers/tasklist_helper_spec.rb | 143 ++++++++---------- .../sales/setup_validations_spec.rb | 97 +++++++++--- spec/rails_helper.rb | 1 + 11 files changed, 222 insertions(+), 122 deletions(-) diff --git a/Gemfile b/Gemfile index 17e10865c..358e71070 100644 --- a/Gemfile +++ b/Gemfile @@ -91,6 +91,7 @@ end group :test do gem "capybara", require: false gem "capybara-lockstep" + gem "capybara-screenshot" gem "faker" gem "rspec-rails", require: false gem "selenium-webdriver", require: false diff --git a/Gemfile.lock b/Gemfile.lock index fa61ec504..7da2ef437 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -129,6 +129,9 @@ GEM capybara (>= 2.0) ruby2_keywords selenium-webdriver (>= 3) + capybara-screenshot (1.0.26) + capybara (>= 1.0, < 4) + launchy childprocess (4.1.0) coderay (1.1.3) concurrent-ruby (1.2.0) @@ -201,6 +204,8 @@ GEM json-schema (3.0.0) addressable (>= 2.8) jwt (2.7.0) + launchy (2.5.2) + addressable (~> 2.8) listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -449,6 +454,7 @@ DEPENDENCIES byebug capybara capybara-lockstep + capybara-screenshot devise! devise_two_factor_authentication dotenv-rails diff --git a/app/helpers/collection_time_helper.rb b/app/helpers/collection_time_helper.rb index be0abae20..23014b8d1 100644 --- a/app/helpers/collection_time_helper.rb +++ b/app/helpers/collection_time_helper.rb @@ -23,4 +23,20 @@ module CollectionTimeHelper def current_collection_end_date Time.zone.local(current_collection_start_year + 1, 3, 31) end + + def previous_collection_end_date + current_collection_end_date - 1.year + end + + def next_collection_start_year + current_collection_start_year + 1 + end + + def previous_collection_start_year + current_collection_start_year - 1 + end + + def previous_collection_start_date + current_collection_start_date - 1.year + end end diff --git a/app/helpers/tasklist_helper.rb b/app/helpers/tasklist_helper.rb index f4f1d51dd..30112894f 100644 --- a/app/helpers/tasklist_helper.rb +++ b/app/helpers/tasklist_helper.rb @@ -11,15 +11,6 @@ module TasklistHelper log.form.subsections.count { |subsection| subsection.status(log) == status && subsection.applicable_questions(log).count.positive? } end - def next_page_or_check_answers(subsection, log, current_user) - path = if subsection.is_started?(log) - "#{log.class.name.underscore}_#{subsection.id}_check_answers_path" - else - "#{log.class.name.underscore}_#{next_question_page(subsection, log, current_user)}_path" - end - send(path, log) - end - def next_question_page(subsection, log, current_user) if subsection.pages.first.routed_to?(log, current_user) subsection.pages.first.id @@ -46,4 +37,16 @@ module TasklistHelper "This log is from the #{log.form.start_date.year}/#{log.form.start_date.year + 1} collection window, which is now closed." end end + +private + + def next_page_or_check_answers(subsection, log, current_user) + path = if subsection.is_started?(log) + "#{log.class.name.underscore}_#{subsection.id}_check_answers_path" + else + "#{log.class.name.underscore}_#{next_question_page(subsection, log, current_user)}_path" + end + + send(path, log) + end end diff --git a/app/models/form_handler.rb b/app/models/form_handler.rb index 8f34288b3..4ba300552 100644 --- a/app/models/form_handler.rb +++ b/app/models/form_handler.rb @@ -35,8 +35,8 @@ class FormHandler def sales_forms { "current_sales" => Form.new(nil, current_collection_start_year, SALES_SECTIONS, "sales"), - "previous_sales" => Form.new(nil, current_collection_start_year - 1, SALES_SECTIONS, "sales"), - "next_sales" => Form.new(nil, current_collection_start_year + 1, SALES_SECTIONS, "sales"), + "previous_sales" => Form.new(nil, previous_collection_start_year, SALES_SECTIONS, "sales"), + "next_sales" => Form.new(nil, next_collection_start_year, SALES_SECTIONS, "sales"), } end @@ -52,10 +52,10 @@ class FormHandler end if forms["previous_lettings"].blank? && current_collection_start_year >= 2022 - forms["previous_lettings"] = Form.new(nil, current_collection_start_year - 1, LETTINGS_SECTIONS, "lettings") + forms["previous_lettings"] = Form.new(nil, previous_collection_start_year, LETTINGS_SECTIONS, "lettings") end forms["current_lettings"] = Form.new(nil, current_collection_start_year, LETTINGS_SECTIONS, "lettings") if forms["current_lettings"].blank? - forms["next_lettings"] = Form.new(nil, current_collection_start_year + 1, LETTINGS_SECTIONS, "lettings") if forms["next_lettings"].blank? + forms["next_lettings"] = Form.new(nil, next_collection_start_year, LETTINGS_SECTIONS, "lettings") if forms["next_lettings"].blank? forms end diff --git a/app/models/validations/sales/setup_validations.rb b/app/models/validations/sales/setup_validations.rb index dadf85650..e7090aabf 100644 --- a/app/models/validations/sales/setup_validations.rb +++ b/app/models/validations/sales/setup_validations.rb @@ -1,11 +1,45 @@ module Validations::Sales::SetupValidations include Validations::SharedValidations + include CollectionTimeHelper def validate_saledate(record) return unless record.saledate && date_valid?("saledate", record) - unless record.saledate.between?(Time.zone.local(2022, 4, 1), Time.zone.local(2023, 3, 31)) || !FeatureToggle.saledate_collection_window_validation_enabled? - record.errors.add :saledate, I18n.t("validations.setup.saledate.financial_year") + unless record.saledate.between?(active_collection_start_date, current_collection_end_date) || !FeatureToggle.saledate_collection_window_validation_enabled? + record.errors.add :saledate, validation_error_message + end + end + +private + + def active_collection_start_date + if FormHandler.instance.sales_in_crossover_period? + previous_collection_start_date + else + current_collection_start_date + end + end + + def validation_error_message + current_end_year_long = current_collection_end_date.strftime("#{current_collection_end_date.day.ordinalize} %B %Y") + + if FormHandler.instance.sales_in_crossover_period? + I18n.t( + "validations.setup.saledate.previous_and_current_financial_year", + previous_start_year_short: previous_collection_start_date.strftime("%y"), + previous_end_year_short: previous_collection_end_date.strftime("%y"), + previous_start_year_long: previous_collection_start_date.strftime("#{previous_collection_start_date.day.ordinalize} %B %Y"), + current_end_year_short: current_collection_end_date.strftime("%y"), + current_end_year_long:, + ) + else + I18n.t( + "validations.setup.saledate.current_financial_year", + current_start_year_short: current_collection_start_date.strftime("%y"), + current_end_year_short: current_collection_end_date.strftime("%y"), + current_start_year_long: current_collection_start_date.strftime("#{current_collection_start_date.day.ordinalize} %B %Y"), + current_end_year_long:, + ) end end end diff --git a/config/initializers/feature_toggle.rb b/config/initializers/feature_toggle.rb index 60a97c95c..dda281e10 100644 --- a/config/initializers/feature_toggle.rb +++ b/config/initializers/feature_toggle.rb @@ -1,13 +1,14 @@ class FeatureToggle - def self.startdate_two_week_validation_enabled? + # Disable check on preview apps to allow for testing of future forms + def self.saledate_collection_window_validation_enabled? Rails.env.production? || Rails.env.test? || Rails.env.staging? end - def self.startdate_collection_window_validation_enabled? + def self.startdate_two_week_validation_enabled? Rails.env.production? || Rails.env.test? || Rails.env.staging? end - def self.saledate_collection_window_validation_enabled? + def self.startdate_collection_window_validation_enabled? Rails.env.production? || Rails.env.test? || Rails.env.staging? end diff --git a/config/locales/en.yml b/config/locales/en.yml index bc54b74ce..8665c022a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -150,7 +150,11 @@ en: intermediate_rent_product_name: blank: "Enter name of other intermediate rent product" saledate: - financial_year: "Date must be from 22/23 financial year, which is between 1st April 2022 and 31st March 2023" + current_financial_year: + Enter a date within the %{current_start_year_short}/%{current_end_year_short} financial year, which is between %{current_start_year_long} and %{current_end_year_long} + previous_and_current_financial_year: + "Enter a date within the %{previous_start_year_short}/%{previous_end_year_short} or %{previous_end_year_short}/%{current_end_year_short} financial years, which is between %{previous_start_year_long} and %{current_end_year_long}" + startdate: later_than_14_days_after: "The tenancy start date must not be later than 14 days from today’s date" before_scheme_end_date: "The tenancy start date must be before the end date for this supported housing scheme" diff --git a/spec/helpers/tasklist_helper_spec.rb b/spec/helpers/tasklist_helper_spec.rb index 1ac6cf738..b5bfe89f5 100644 --- a/spec/helpers/tasklist_helper_spec.rb +++ b/spec/helpers/tasklist_helper_spec.rb @@ -1,100 +1,55 @@ require "rails_helper" RSpec.describe TasklistHelper do - describe "with lettings" do - let(:empty_lettings_log) { FactoryBot.create(:lettings_log) } - let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, needstype: 1) } - let(:fake_2021_2022_form) { Form.new("spec/fixtures/forms/2021_2022.json") } + let(:now) { Time.utc(2022, 6, 1) } - context "with 2021 2022 form" do - before do - allow(FormHandler.instance).to receive(:current_lettings_form).and_return(fake_2021_2022_form) - end + around do |example| + Timecop.freeze(now) do + Singleton.__init__(FormHandler) + example.run + end + Timecop.return + Singleton.__init__(FormHandler) + end - describe "get next incomplete section" do - it "returns the first subsection name if it is not completed" do - expect(get_next_incomplete_section(lettings_log).id).to eq("household_characteristics") - end + describe "with lettings" do + let(:empty_lettings_log) { create(:lettings_log) } + let(:lettings_log) { build(:lettings_log, :in_progress, needstype: 1) } - it "returns the first subsection name if it is partially completed" do - lettings_log["tenancycode"] = 123 - expect(get_next_incomplete_section(lettings_log).id).to eq("household_characteristics") - end + describe "get next incomplete section" do + it "returns the first subsection name if it is not completed" do + expect(get_next_incomplete_section(lettings_log).id).to eq("household_characteristics") end - describe "get sections count" do - it "returns the total of sections if no status is given" do - expect(get_subsections_count(empty_lettings_log)).to eq(8) - end - - it "returns 0 sections for completed sections if no sections are completed" do - expect(get_subsections_count(empty_lettings_log, :completed)).to eq(0) - end - - it "returns the number of not started sections" do - expect(get_subsections_count(empty_lettings_log, :not_started)).to eq(8) - end - - it "returns the number of sections in progress" do - expect(get_subsections_count(lettings_log, :in_progress)).to eq(3) - end - - it "returns 0 for invalid state" do - expect(get_subsections_count(lettings_log, :fake)).to eq(0) - end + it "returns the first subsection name if it is partially completed" do + lettings_log["tenancycode"] = 123 + expect(get_next_incomplete_section(lettings_log).id).to eq("household_characteristics") end + end - describe "get_next_page_or_check_answers" do - let(:subsection) { lettings_log.form.get_subsection("household_characteristics") } - let(:user) { FactoryBot.build(:user) } - - it "returns the check answers page path if the section has been started already" do - expect(next_page_or_check_answers(subsection, lettings_log, user)).to match(/check-answers/) - end - - it "returns the first question page path for the section if it has not been started yet" do - expect(next_page_or_check_answers(subsection, empty_lettings_log, user)).to match(/tenant-code-test/) - end - - it "when first question being not routed to returns the next routed question link" do - empty_lettings_log.housingneeds_a = "No" - expect(next_page_or_check_answers(subsection, empty_lettings_log, user)).to match(/person-1-gender/) - end + describe "get sections count" do + it "returns the total of sections if no status is given" do + expect(get_subsections_count(empty_lettings_log)).to eq(1) end - describe "subsection link" do - let(:subsection) { lettings_log.form.get_subsection("household_characteristics") } - let(:user) { FactoryBot.build(:user) } - - context "with a subsection that's enabled" do - it "returns the subsection link url" do - expect(subsection_link(subsection, lettings_log, user)).to match(/household-characteristics/) - end - end + it "returns 0 sections for completed sections if no sections are completed" do + expect(get_subsections_count(empty_lettings_log, :completed)).to eq(0) + end - context "with a subsection that cannot be started yet" do - before do - allow(subsection).to receive(:status).with(lettings_log).and_return(:cannot_start_yet) - end + it "returns the number of not started sections" do + expect(get_subsections_count(empty_lettings_log, :not_started)).to eq(1) + end - it "returns the label instead of a link" do - expect(subsection_link(subsection, lettings_log, user)).to match(subsection.label) - end - end + it "returns the number of sections in progress" do + expect(get_subsections_count(lettings_log, :in_progress)).to eq(2) end - end - end - describe "#review_log_text" do - around do |example| - Timecop.freeze(now) do - Singleton.__init__(FormHandler) - example.run + it "returns 0 for invalid state" do + expect(get_subsections_count(lettings_log, :fake)).to eq(0) end - Singleton.__init__(FormHandler) end - context "with lettings log" do + describe "review_log_text" do context "when collection_period_open? == true" do context "with 2023 deadline" do let(:now) { Time.utc(2022, 6, 1) } @@ -129,6 +84,30 @@ RSpec.describe TasklistHelper do end end + describe "subsection link" do + let(:lettings_log) { create(:lettings_log, :completed) } + let(:subsection) { lettings_log.form.get_subsection("household_characteristics") } + let(:user) { build(:user) } + + context "with a subsection that's enabled" do + it "returns the subsection link url" do + expect(subsection_link(subsection, lettings_log, user)).to match(/household-characteristics/) + end + end + + context "with a subsection that cannot be started yet" do + before do + allow(subsection).to receive(:status).with(lettings_log).and_return(:cannot_start_yet) + end + + it "returns the label instead of a link" do + expect(subsection_link(subsection, lettings_log, user)).to match(subsection.label) + end + end + end + end + + describe "#review_log_text" do context "with sales log" do context "when collection_period_open? == true" do let(:now) { Time.utc(2022, 6, 1) } @@ -142,11 +121,13 @@ RSpec.describe TasklistHelper do end context "when collection_period_open? == false" do - let(:now) { Time.utc(2023, 7, 8) } - let(:sales_log) { create(:sales_log, :completed, saledate: Time.utc(2023, 2, 8)) } + let(:now) { Time.utc(2022, 6, 1) } + let!(:sales_log) { create(:sales_log, :completed) } it "returns relevant text" do - expect(review_log_text(sales_log)).to eq("This log is from the 2022/2023 collection window, which is now closed.") + Timecop.freeze(now + 1.year) do + expect(review_log_text(sales_log)).to eq("This log is from the 2021/2022 collection window, which is now closed.") + end end end end diff --git a/spec/models/validations/sales/setup_validations_spec.rb b/spec/models/validations/sales/setup_validations_spec.rb index 74b7834ab..4571ae5cd 100644 --- a/spec/models/validations/sales/setup_validations_spec.rb +++ b/spec/models/validations/sales/setup_validations_spec.rb @@ -6,43 +6,96 @@ RSpec.describe Validations::Sales::SetupValidations do let(:validator_class) { Class.new { include Validations::Sales::SetupValidations } } describe "#validate_saledate" do - context "when saledate is blank" do - let(:record) { FactoryBot.build(:sales_log, saledate: nil) } + context "with sales_in_crossover_period == false" do + context "when saledate is blank" do + let(:record) { build(:sales_log, saledate: nil) } - it "does not add an error" do - setup_validator.validate_saledate(record) + it "does not add an error" do + setup_validator.validate_saledate(record) - expect(record.errors).to be_empty + expect(record.errors).to be_empty + end end - end - context "when saledate is in the 22/23 financial year" do - let(:record) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2023, 1, 1)) } + context "when saledate is in the 22/23 financial year" do + let(:record) { build(:sales_log, saledate: Time.zone.local(2023, 1, 1)) } - it "does not add an error" do - setup_validator.validate_saledate(record) + it "does not add an error" do + setup_validator.validate_saledate(record) - expect(record.errors).to be_empty + expect(record.errors).to be_empty + end + end + + context "when saledate is before the 22/23 financial year" do + let(:record) { build(:sales_log, saledate: Time.zone.local(2020, 1, 1)) } + + it "adds error" do + setup_validator.validate_saledate(record) + + expect(record.errors[:saledate]).to include("Enter a date within the 22/23 financial year, which is between 1st April 2022 and 31st March 2023") + end end - end - context "when saledate is before the 22/23 financial year" do - let(:record) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2022, 1, 1)) } + context "when saledate is after the 22/23 financial year" do + let(:record) { build(:sales_log, saledate: Time.zone.local(2025, 4, 1)) } - it "adds error" do - setup_validator.validate_saledate(record) + it "adds error" do + setup_validator.validate_saledate(record) - expect(record.errors[:saledate]).to include(I18n.t("validations.setup.saledate.financial_year")) + expect(record.errors[:saledate]).to include("Enter a date within the 22/23 financial year, which is between 1st April 2022 and 31st March 2023") + end end end - context "when saledate is after the 22/23 financial year" do - let(:record) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2023, 4, 1)) } + context "with sales_in_crossover_period == true" do + around do |example| + Timecop.freeze(Time.zone.local(2024, 5, 1)) do + Singleton.__init__(FormHandler) + example.run + end + Timecop.return + Singleton.__init__(FormHandler) + end + + context "when saledate is blank" do + let(:record) { build(:sales_log, saledate: nil) } + + it "does not add an error" do + setup_validator.validate_saledate(record) + + expect(record.errors).to be_empty + end + end + + context "when saledate is in the 22/23 financial year" do + let(:record) { build(:sales_log, saledate: Time.zone.local(2024, 1, 1)) } + + it "does not add an error" do + setup_validator.validate_saledate(record) + + expect(record.errors).to be_empty + end + end + + context "when saledate is before the 22/23 financial year" do + let(:record) { build(:sales_log, saledate: Time.zone.local(2020, 5, 1)) } + + it "adds error" do + setup_validator.validate_saledate(record) + + expect(record.errors[:saledate]).to include("Enter a date within the 23/24 or 24/25 financial years, which is between 1st April 2023 and 31st March 2025") + end + end + + context "when saledate is after the 22/23 financial year" do + let(:record) { build(:sales_log, saledate: Time.zone.local(2025, 4, 1)) } - it "adds error" do - setup_validator.validate_saledate(record) + it "adds error" do + setup_validator.validate_saledate(record) - expect(record.errors[:saledate]).to include(I18n.t("validations.setup.saledate.financial_year")) + expect(record.errors[:saledate]).to include("Enter a date within the 23/24 or 24/25 financial years, which is between 1st April 2023 and 31st March 2025") + end end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 81d9246ec..8e175ccf6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -6,6 +6,7 @@ require File.expand_path("../config/environment", __dir__) abort("The Rails environment is running in production mode!") if Rails.env.production? require "rspec/rails" require "capybara/rspec" +require "capybara-screenshot/rspec" require "selenium-webdriver" require "view_component/test_helpers" From 66a528bd48412e7da8441985c8e5fbe506c62e49 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Thu, 23 Feb 2023 09:56:54 +0000 Subject: [PATCH 29/30] CLDC-1860 Change location question header for 23/24 (#1291) * Change location question header for 23/24 * Stup form in page tests --- .../form/lettings/questions/location_id.rb | 10 +++++++++- .../form/lettings/pages/location_spec.rb | 6 ++++++ .../lettings/questions/location_id_spec.rb | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/models/form/lettings/questions/location_id.rb b/app/models/form/lettings/questions/location_id.rb index 5a99e7734..fc197cf82 100644 --- a/app/models/form/lettings/questions/location_id.rb +++ b/app/models/form/lettings/questions/location_id.rb @@ -2,7 +2,7 @@ class Form::Lettings::Questions::LocationId < ::Form::Question def initialize(_id, hsh, page) super("location_id", hsh, page) @check_answer_label = "Location" - @header = "Which location is this log for?" + @header = header_text @type = "radio" @answer_options = answer_options @inferred_answers = { @@ -47,4 +47,12 @@ private def selected_answer_option_is_derived?(_lettings_log) false end + + def header_text + if form.start_date && form.start_date.year >= 2023 + "Which location is this letting for?" + else + "Which location is this log for?" + end + end end diff --git a/spec/models/form/lettings/pages/location_spec.rb b/spec/models/form/lettings/pages/location_spec.rb index aefcd59a9..659a4dd26 100644 --- a/spec/models/form/lettings/pages/location_spec.rb +++ b/spec/models/form/lettings/pages/location_spec.rb @@ -6,6 +6,12 @@ RSpec.describe Form::Lettings::Pages::Location, type: :model do let(:page_id) { nil } let(:page_definition) { nil } let(:subsection) { instance_double(Form::Subsection) } + let(:form) { instance_double(Form) } + + before do + allow(form).to receive(:start_date).and_return(Time.zone.local(2022, 4, 1)) + allow(subsection).to receive(:form).and_return(form) + end it "has correct subsection" do expect(page.subsection).to eq(subsection) diff --git a/spec/models/form/lettings/questions/location_id_spec.rb b/spec/models/form/lettings/questions/location_id_spec.rb index ca3d402a9..afb39ce11 100644 --- a/spec/models/form/lettings/questions/location_id_spec.rb +++ b/spec/models/form/lettings/questions/location_id_spec.rb @@ -6,6 +6,14 @@ RSpec.describe Form::Lettings::Questions::LocationId, type: :model do let(:question_id) { nil } let(:question_definition) { nil } let(:page) { instance_double(Form::Page) } + let(:subsection) { instance_double(Form::Subsection) } + let(:form) { instance_double(Form) } + + before do + allow(form).to receive(:start_date).and_return(Time.zone.local(2022, 4, 1)) + allow(page).to receive(:subsection).and_return(subsection) + allow(subsection).to receive(:form).and_return(form) + end it "has correct page" do expect(question.page).to eq(page) @@ -103,4 +111,14 @@ RSpec.describe Form::Lettings::Questions::LocationId, type: :model do end end end + + context "with collection year on or after 2023" do + before do + allow(form).to receive(:start_date).and_return(Time.zone.local(2023, 4, 1)) + end + + it "has the correct header" do + expect(question.header).to eq("Which location is this letting for?") + end + end end From f1c7b8188f02d6e0ef71c776234fbb3968b06eec Mon Sep 17 00:00:00 2001 From: Jack <113976590+bibblobcode@users.noreply.github.com> Date: Thu, 23 Feb 2023 14:24:04 +0000 Subject: [PATCH 30/30] [CLDC-1915] Update privacy notice (#1332) * [CLDC-1915] Update privacy notice * Use generic notice --- app/views/content/privacy_notice.md | 45 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/app/views/content/privacy_notice.md b/app/views/content/privacy_notice.md index f80a5e4ca..b1188eb5a 100644 --- a/app/views/content/privacy_notice.md +++ b/app/views/content/privacy_notice.md @@ -1,46 +1,47 @@ -## How are we using your information? +## How do we use your information? -If your household has entered a new social housing tenancy, social housing providers will share your personal information with the Department for Levelling Up, Housing & Communities (DLUHC) for research and statistical purposes. +If your household enters a new social housing tenancy or purchases a social housing property, social housing providers will share your personal information with the Department for Levelling Up, Housing & Communities (DLUHC) for research and statistical purposes only. -## How is this information provided? +## How do we get this information? -The information is provided via ‘<%= t('service_name') %>’, a service funded and managed by DLUHC. It collects information on the tenants or buyers, tenancy or sale, and the dwelling itself. Some of this information is personal and sensitive, so DLUHC is responsible for ensuring that all data is processed in line with data protection legislation. +The information is provided via ‘<%= t('service_name') %>’, a service funded and managed by DLUHC. It collects information on the tenants or residents, tenancy or sale, and the dwelling itself. Some of this data is personal and sensitive, so DLUHC is responsible for ensuring it’s processed in line with data protection legislation. -## Why are we sharing this information? -Information collected using this service is shared with other government departments and agencies. Data is shared with the Greater London Authority and the Regulator of Social Housing. Data providers can also access data for their organisations via the online service. Data is only shared for research and statistical purposes. +## Why do we share this information? + +Information collected via CORE is shared with other government departments and agencies. It’s shared with the Greater London Authority and the Regulator of Social Housing. Data providers can also access data for their organisations via CORE. Data is only shared for research and statistical purposes. ## How does this affect you? -It will not affect your benefits, services or any treatments you receive. The information shared is anonymous and handled in accordance with the law. We are collecting and sharing your information to help us better understand the social housing market and inform social housing policy. +Information sharing will not affect your benefits, services or any treatments you receive. It’s anonymous and handled in accordance with the law. We collect and share your information to help us better understand the social housing market and inform social housing policy. -## If you want to know more +## To find out more… -Social housing lettings and sales data is collected on behalf of DLUHC for research and statistical purposes only. Data providers do not require the consent of tenants to provide the information, but tenants have the right to know how and for what purpose data is being collected, held and used. +Social housing lettings and sales data is collected on DLUHC’s behalf. Data providers do not require the tenant or buyer’s consent to provide this information, but tenants and buyers have the right to know how and for what purpose data is being collected, held and used. -The processing must have a lawful basis. In this case the processing is necessary for the performance of a task carried out in the public interest to meet a function of the Crown, a Minister of the Crown, or a government department. +Data processing must have a lawful basis. In this case it’s necessary for a task carried out in the public interest meeting a function of the Crown, a Minister of the Crown, or government department. -You have the right to object and you have the right to obtain confirmation that your data is being processed, and to access your personal data. You also have the right to have any incorrect personal data corrected. +You have the right to object, and obtain confirmation that your data is being processed, as well as access your personal data, and have any incorrect personal data corrected. -The information collected via this service relates to your tenancy, the dwelling you are living in or buying, and your household. Some of the information may have been provided by you as a tenant when signing the new tenancy or buying your property. Other information has been gathered from the housing management systems of social housing providers. +Information collected via CORE relates to your tenancy, the dwelling you are living in or buying, and your household. Some information may have been provided by you (as a tenant or buyer) when signing the new tenancy or buying your property. Other information has been gathered from the housing management systems of social housing providers. -Data collected will be held for as long as necessary for research and statistical purposes. When no longer needed, data will be deleted in a safe manner. We are aware that some of the data collected is particularly sensitive. For example: +Collected data will be held for as long as necessary for research and statistical purposes. When no longer needed, data will be deleted in a safe manner. We’re aware some collected data is particularly sensitive. For example: * ethnic group -* if previous tenure is a hospital or prison or approved probation hostel support +* if previous tenure is a hospital, prison or approved probation hostel support * if household left last settled home because discharged from prison, a long stay hospital or other institution -* if source of referral is probation or prison, youth offending team, community mental health team or health service +* if referral source is probation or prison, youth offending or community mental health team, or health service -All the information collected via this service is treated in accordance with data protection requirements and guidelines. +DLUHC publishes data annually, in aggregate form, as part of a report and complementary tables. -Data is published by DLUHC in aggregate form on an annual basis as part of a report and complementary tables. +* For annual lettings data, visit: [www.gov.uk/government/collections/rents-lettings-and-tenancies](www.gov.uk/government/collections/rents-lettings-and-tenancies) -You can visit to access the annual publications on lettings. Or visit to view the publications on sales. +* For annual sales data, visit: [www.gov.uk/government/collections/social-housing-sales-including-right-to-buy-and-transfers](www.gov.uk/government/collections/social-housing-sales-including-right-to-buy-and-transfers) -The detail level data is anonymised and protected to minimise the risk of identification and held with the UK Data Archive for research purposes. +Detail-level data is anonymised and protected, minimising identification risk. It's held with the UK Data Archive. -## Making a complaint +## Complaints -If you are unhappy with any aspect of this privacy notice, or how your personal information is being processed, contact the Department Data Protection Officer at: +If you’re unhappy with any privacy notice aspect, or how we process your information, contact the Department Data Protection Officer: -If you are still not happy, you have the right to lodge a complaint with the Information Commissioner’s Office (ICO) at [ico.org.uk/concern](https://ico.org.uk/concern). +You also have the right to complain to the Information Commissioner’s Office (ICO): [www.ico.org.uk/concern](www.ico.org.uk/concern)