From c5849048a978f981aaf279ac07c5433eb6b42852 Mon Sep 17 00:00:00 2001 From: natdeanlewissoftwire <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Mon, 6 Mar 2023 09:47:12 +0000 Subject: [PATCH 01/19] CLDC-1822 Add new reason for vacancy option (#1344) * feat: add new question option * test: add new test page --- app/models/form/lettings/questions/rsnvac.rb | 3 + .../form/lettings/questions/rsnvac_spec.rb | 79 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 spec/models/form/lettings/questions/rsnvac_spec.rb diff --git a/app/models/form/lettings/questions/rsnvac.rb b/app/models/form/lettings/questions/rsnvac.rb index 4968ebb2d..2899a9463 100644 --- a/app/models/form/lettings/questions/rsnvac.rb +++ b/app/models/form/lettings/questions/rsnvac.rb @@ -36,6 +36,9 @@ class Form::Lettings::Questions::Rsnvac < ::Form::Question "18" => { "value" => "Tenant moved to care home", }, + "20" => { + "value" => "Tenant moved to long-stay hospital or similar institution", + }, "6" => { "value" => "Tenant abandoned property", }, diff --git a/spec/models/form/lettings/questions/rsnvac_spec.rb b/spec/models/form/lettings/questions/rsnvac_spec.rb new file mode 100644 index 000000000..302f566fe --- /dev/null +++ b/spec/models/form/lettings/questions/rsnvac_spec.rb @@ -0,0 +1,79 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Questions::Rsnvac, 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("rsnvac") + end + + it "has the correct header" do + expect(question.header).to eq("What is the reason for the property being vacant?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Vacancy reason") + 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({ + "13" => { + "value" => "Internal transfer", + "hint" => "Excluding renewals of a fixed-term tenancy", + }, + "5" => { + "value" => "Previous tenant died with no succession", + }, + "9" => { + "value" => "Re-let to tenant who occupied same property as temporary accommodation", + }, + "14" => { + "value" => "Renewal of fixed-term tenancy", + }, + "19" => { + "value" => "Tenant involved in a succession downsize", + }, + "8" => { + "value" => "Tenant moved to private sector or other accommodation", + }, + "12" => { + "value" => "Tenant moved to other social housing provider", + }, + "18" => { + "value" => "Tenant moved to care home", + }, + "20" => { + "value" => "Tenant moved to long-stay hospital or similar institution", + }, + "6" => { + "value" => "Tenant abandoned property", + }, + "10" => { + "value" => "Tenant was evicted due to rent arrears", + }, + "11" => { + "value" => "Tenant was evicted due to anti-social behaviour", + }, + }) + end + + it "has the correct check_answers_card_number" do + expect(question.check_answers_card_number).to eq(0) + end +end From e0e5f9bbe02bff626507a50e1bc4ce81cfd4545c Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Mon, 6 Mar 2023 09:50:04 +0000 Subject: [PATCH 02/19] CLDC-1938 Import LA and mortgage used fixes (#1379) * import LA if it cannot be inferred * Assume mortgage is used if information about it is given * Only import 22/23 logs * remove comment * Include the first day of collection window --- .../imports/sales_logs_import_service.rb | 23 +++-- .../imports/sales_logs_import_service_spec.rb | 96 +++++++++++++++++-- 2 files changed, 105 insertions(+), 14 deletions(-) diff --git a/app/services/imports/sales_logs_import_service.rb b/app/services/imports/sales_logs_import_service.rb index 3593a1227..d9ac0b0d4 100644 --- a/app/services/imports/sales_logs_import_service.rb +++ b/app/services/imports/sales_logs_import_service.rb @@ -16,7 +16,9 @@ module Imports private def create_log(xml_doc) + # only import sales logs from 22/23 collection period onwards return unless meta_field_value(xml_doc, "form-name").include?("Sales") + return unless compose_date(xml_doc, "DAY", "MONTH", "YEAR") >= Time.zone.local(2022, 4, 1) attributes = {} @@ -76,7 +78,7 @@ module Imports attributes["inc2mort"] = unsafe_string_as_integer(xml_doc, "Q2Person2MortApplication") attributes["hb"] = unsafe_string_as_integer(xml_doc, "Q2a") attributes["frombeds"] = safe_string_as_integer(xml_doc, "Q20Bedrooms") - attributes["staircase"] = unsafe_string_as_integer(xml_doc, "Q17aStaircase") + attributes["staircase"] = unsafe_string_as_integer(xml_doc, "Q17aStaircase") if attributes["ownershipsch"] == 1 attributes["stairbought"] = safe_string_as_integer(xml_doc, "PercentBought") attributes["stairowned"] = safe_string_as_integer(xml_doc, "PercentOwns") if attributes["staircase"] == 1 attributes["mrent"] = safe_string_as_decimal(xml_doc, "Q28MonthlyRent") @@ -102,7 +104,6 @@ module Imports attributes["ppcodenk"] = previous_postcode_known(xml_doc, attributes["ppostcode_full"], attributes["prevloc"]) # Q7UNKNOWNPOSTCODE check mapping attributes["ppostc1"] = string_or_nil(xml_doc, "PPOSTC1") attributes["ppostc2"] = string_or_nil(xml_doc, "PPOSTC2") - attributes["previous_la_known"] = nil attributes["hhregres"] = unsafe_string_as_integer(xml_doc, "ArmedF") attributes["hhregresstill"] = still_serving(xml_doc) attributes["proplen"] = safe_string_as_integer(xml_doc, "Q16aProplen2") || safe_string_as_integer(xml_doc, "Q16aProplensec2") @@ -111,7 +112,7 @@ module Imports attributes["prevten"] = unsafe_string_as_integer(xml_doc, "Q6PrevTenure") attributes["mortlen"] = mortgage_length(xml_doc, attributes) attributes["extrabor"] = borrowing(xml_doc, attributes) - attributes["mortgageused"] = unsafe_string_as_integer(xml_doc, "MORTGAGEUSED") + attributes["mortgageused"] = mortgage_used(xml_doc, attributes) attributes["wchair"] = unsafe_string_as_integer(xml_doc, "Q15Wheelchair") attributes["armedforcesspouse"] = unsafe_string_as_integer(xml_doc, "ARMEDFORCESSPOUSE") attributes["hodate"] = compose_date(xml_doc, "HODAY", "HOMONTH", "HOYEAR") @@ -130,9 +131,8 @@ module Imports attributes["prevshared"] = nil # 23/24 variable attributes["staircasesale"] = nil # 23/24 variable - # Required for our form invalidated questions (not present in import) - attributes["previous_la_known"] = 1 if attributes["prevloc"].present? && attributes["ppostcode_full"].blank? - if attributes["la"].present? && attributes["postcode_full"].blank? + attributes["previous_la_known"] = 1 if attributes["prevloc"].present? + if attributes["la"].present? attributes["la_known"] = 1 attributes["is_la_inferred"] = false end @@ -446,6 +446,17 @@ module Imports UKPostcode.parse(postcode).to_s end + def mortgage_used(xml_doc, attributes) + mortgageused = unsafe_string_as_integer(xml_doc, "MORTGAGEUSED") + return mortgageused unless mortgageused == 3 + + if attributes["mortgage"].present? || attributes["mortlen"].present? || attributes["extrabor"].present? + 1 # yes + else + 3 # don't know + end + end + def set_default_values(attributes) attributes["armedforcesspouse"] ||= 7 attributes["hhregres"] ||= 8 diff --git a/spec/services/imports/sales_logs_import_service_spec.rb b/spec/services/imports/sales_logs_import_service_spec.rb index d2dc4ad94..056d492de 100644 --- a/spec/services/imports/sales_logs_import_service_spec.rb +++ b/spec/services/imports/sales_logs_import_service_spec.rb @@ -17,14 +17,6 @@ RSpec.describe Imports::SalesLogsImportService do end before do - { "GL519EX" => "E07000078", - "SW1A2AA" => "E09000033", - "SW1A1AA" => "E09000033", - "SW147QP" => "E09000027", - "B955HZ" => "E07000221" }.each do |postcode, district_code| - WebMock.stub_request(:get, /api.postcodes.io\/postcodes\/#{postcode}/).to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"#{district_code}\",\"codes\":{\"admin_district\":\"#{district_code}\"}}}", headers: {}) - end - allow(Organisation).to receive(:find_by).and_return(nil) allow(Organisation).to receive(:find_by).with(old_visible_id: organisation.old_visible_id).and_return(organisation) allow(Organisation).to receive(:find_by).with(old_visible_id: managing_organisation.old_visible_id).and_return(managing_organisation) @@ -119,6 +111,29 @@ RSpec.describe Imports::SalesLogsImportService do end end + context "and the log startdate is before 22/23 collection period" do + let(:sales_log_id) { "shared_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:DAY").content = 10 + sales_log_xml.at_xpath("//xmlns:MONTH").content = 10 + sales_log_xml.at_xpath("//xmlns:YEAR").content = 2021 + sales_log_xml.at_xpath("//xmlns:HODAY").content = 9 + sales_log_xml.at_xpath("//xmlns:HOMONTH").content = 10 + sales_log_xml.at_xpath("//xmlns:HOYEAR").content = 2021 + sales_log_xml.at_xpath("//xmlns:EXDAY").content = 9 + sales_log_xml.at_xpath("//xmlns:EXMONTH").content = 10 + sales_log_xml.at_xpath("//xmlns:EXYEAR").content = 2021 + end + + it "does not create the log" do + expect(logger).not_to receive(:error) + expect(logger).not_to receive(:warn) + expect { sales_log_service.send(:create_log, sales_log_xml) } + .to change(SalesLog, :count).by(0) + end + end + context "when the mortgage lender is set to an existing option" do let(:sales_log_id) { "discounted_ownership_sales_log" } @@ -799,6 +814,18 @@ RSpec.describe Imports::SalesLogsImportService do expect(sales_log&.ppostcode_full).to eq("GL51 9EX") expect(sales_log&.status).to eq("completed") end + + it "correctly sets location fields for when location cannot be inferred from postcode" do + sales_log_xml.at_xpath("//xmlns:Q14ONSLACode").content = "E07000142" + sales_log_xml.at_xpath("//xmlns:Q14Postcode").content = "A11AA" + sales_log_service.send(:create_log, sales_log_xml) + + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log&.pcodenk).to eq(0) # postcode known + expect(sales_log&.la_known).to eq(1) # la known + expect(sales_log&.la).to eq("E07000142") + expect(sales_log&.status).to eq("completed") + end end context "when setting default buyer 1 previous tenancy" do @@ -824,6 +851,59 @@ RSpec.describe Imports::SalesLogsImportService do expect(sales_log&.prevten).to eq(2) end end + + context "when mortgage used is don't know" do + let(:sales_log_id) { "discounted_ownership_sales_log" } + + before do + allow(logger).to receive(:warn).and_return(nil) + end + + it "sets mortgageused to don't know if mortlen, mortgage and extrabor are blank" do + sales_log_xml.at_xpath("//xmlns:MORTGAGEUSED").content = "3 Don't know" + sales_log_xml.at_xpath("//xmlns:Q35Borrowing").content = "" + sales_log_xml.at_xpath("//xmlns:Q34b").content = "" + sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "" + sales_log_xml.at_xpath("//xmlns:Q36CashDeposit").content = "134750" + sales_log_service.send(:create_log, sales_log_xml) + + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log&.mortgageused).to eq(3) + end + + it "sets mortgageused to yes if mortgage is given" do + sales_log_xml.at_xpath("//xmlns:MORTGAGEUSED").content = "3 Don't know" + sales_log_xml.at_xpath("//xmlns:Q35Borrowing").content = "" + sales_log_xml.at_xpath("//xmlns:Q34b").content = "" + sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "134750" + sales_log_service.send(:create_log, sales_log_xml) + + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log&.mortgageused).to eq(1) + end + + it "sets mortgageused to yes if mortlen is given" do + sales_log_xml.at_xpath("//xmlns:MORTGAGEUSED").content = "3 Don't know" + sales_log_xml.at_xpath("//xmlns:Q35Borrowing").content = "" + sales_log_xml.at_xpath("//xmlns:Q34b").content = "10" + sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "" + sales_log_service.send(:create_log, sales_log_xml) + + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log&.mortgageused).to eq(1) + end + + it "sets mortgageused to yes if extrabor is given" do + sales_log_xml.at_xpath("//xmlns:MORTGAGEUSED").content = "3 Don't know" + sales_log_xml.at_xpath("//xmlns:Q35Borrowing").content = "3000" + sales_log_xml.at_xpath("//xmlns:Q34b").content = "" + sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "" + sales_log_service.send(:create_log, sales_log_xml) + + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log&.mortgageused).to eq(1) + end + end end end end From 2aee5a80c5980758cc5d4c505f67b34f48cf6cf0 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Mon, 6 Mar 2023 10:00:44 +0000 Subject: [PATCH 03/19] CLDC-1768 Bulk upload file setup email (#1358) * add placeholder tests for bulk upload mailer * bulk upload fix setup errors email - this plumbs in the condition so if any setup sections are incomplete we send that partcular email and prevent the remaining flow * tag bulk upload setup errors for downsteam use * add category to bulk upload errors * persist bulk upload error category * populate bulk upload mailer with errors --- app/mailers/bulk_upload_mailer.rb | 39 +++++++++++++++---- .../bulk_upload/lettings/row_parser.rb | 24 +++++++++--- .../bulk_upload/lettings/validator.rb | 7 ++-- app/services/bulk_upload/processor.rb | 36 ++++++++++++----- ...0116_add_category_to_bulk_upload_errors.rb | 5 +++ db/schema.rb | 1 + spec/mailers/bulk_upload_mailer_spec.rb | 32 +++++++++++++++ .../bulk_upload/lettings/row_parser_spec.rb | 10 +++++ .../bulk_upload/lettings/validator_spec.rb | 5 +++ spec/services/bulk_upload/processor_spec.rb | 39 +++++++++++++++++++ 10 files changed, 173 insertions(+), 25 deletions(-) create mode 100644 db/migrate/20230301120116_add_category_to_bulk_upload_errors.rb diff --git a/app/mailers/bulk_upload_mailer.rb b/app/mailers/bulk_upload_mailer.rb index 94079828f..fe1e81517 100644 --- a/app/mailers/bulk_upload_mailer.rb +++ b/app/mailers/bulk_upload_mailer.rb @@ -66,17 +66,40 @@ class BulkUploadMailer < NotifyMailer ) end - def send_bulk_upload_failed_file_setup_error_mail(user, bulk_upload) + def send_bulk_upload_failed_file_setup_error_mail(bulk_upload:) + bulk_upload_link = if bulk_upload.lettings? + start_bulk_upload_lettings_logs_url + else + start_bulk_upload_sales_logs_url + end + + validator_class = if bulk_upload.lettings? + BulkUpload::Lettings::Validator + else + BulkUpload::Sales::Validator + end + + errors = bulk_upload + .bulk_upload_errors + .where(category: "setup") + .group(:col, :field) + .count + .keys + .sort_by { |_col, field| field } + .map do |col, field| + "- Column #{col} (#{validator_class.question_for_field(field.to_sym)})" + end + send_email( - user.email, + bulk_upload.user.email, BULK_UPLOAD_FAILED_FILE_SETUP_ERROR_TEMPLATE_ID, { - filename: "[#{bulk_upload} filename]", - upload_timestamp: "[#{bulk_upload} upload_timestamp]", - lettings_or_sales: "[#{bulk_upload} lettings_or_sales]", - year_combo: "[#{bulk_upload} year_combo]", - errors_list: "[#{bulk_upload} errors_list]", - bulk_upload_link: "[#{bulk_upload} bulk_upload_link]", + filename: bulk_upload.filename, + upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time), + lettings_or_sales: bulk_upload.log_type, + year_combo: bulk_upload.year_combo, + errors_list: errors.join("\n"), + bulk_upload_link:, }, ) end diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/row_parser.rb index 426f122a1..289566508 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/row_parser.rb @@ -203,6 +203,10 @@ class BulkUpload::Lettings::RowParser block_log_creation end + def setup_section_incomplete? + log.form.setup_sections[0].subsections[0].is_incomplete?(log) + end + private def validate_location_related @@ -222,7 +226,7 @@ private def validate_location_exists if scheme && field_5.present? && location.nil? - errors.add(:field_5, "Location could be found with provided scheme code") + errors.add(:field_5, "Location could be found with provided scheme code", category: :setup) end end @@ -240,7 +244,7 @@ private def validate_scheme_exists if field_4.present? && scheme.nil? - errors.add(:field_4, "The management group code is not correct") + errors.add(:field_4, "The management group code is not correct", category: :setup) end end @@ -254,7 +258,7 @@ private def validate_managing_org_exists if managing_organisation.nil? errors.delete(:field_113) - errors.add(:field_113, "The managing organisation code is incorrect") + errors.add(:field_113, "The managing organisation code is incorrect", category: :setup) end end @@ -269,7 +273,7 @@ private def validate_owning_org_exists if owning_organisation.nil? errors.delete(:field_111) - errors.add(:field_111, "The owning organisation code is incorrect") + errors.add(:field_111, "The owning organisation code is incorrect", category: :setup) end end @@ -387,10 +391,18 @@ private next if log.optional_fields.include?(question.id) next if question.completed?(log) - fields.each { |field| errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase)) } + if setup_question?(question) + fields.each { |field| errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase), category: :setup) } + else + fields.each { |field| errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase)) } + end end end + def setup_question?(question) + log.form.setup_sections[0].subsections[0].questions.include?(question) + end + def field_mapping_for_errors { lettype: [:field_1], @@ -398,6 +410,8 @@ private postcode_known: %i[field_107 field_108 field_109], postcode_full: %i[field_107 field_108 field_109], la: %i[field_107], + owning_organisation: [:field_111], + managing_organisation: [:field_113], owning_organisation_id: [:field_111], managing_organisation_id: [:field_113], renewal: [:field_134], diff --git a/app/services/bulk_upload/lettings/validator.rb b/app/services/bulk_upload/lettings/validator.rb index 992f06196..779f0315b 100644 --- a/app/services/bulk_upload/lettings/validator.rb +++ b/app/services/bulk_upload/lettings/validator.rb @@ -168,6 +168,7 @@ class BulkUpload::Lettings::Validator row:, cell: "#{cols[field_number_for_attribute(error.attribute) - col_offset + 1]}#{row}", col: cols[field_number_for_attribute(error.attribute) - col_offset + 1], + category: error.options[:category], ) end end @@ -185,12 +186,12 @@ class BulkUpload::Lettings::Validator QUESTIONS[field] end -private - def any_setup_sections_incomplete? - row_parsers.any? { |row_parser| row_parser.log.form.setup_sections[0].subsections[0].is_incomplete?(row_parser.log) } + row_parsers.any?(&:setup_section_incomplete?) end +private + def over_column_error_threshold? fields = ("field_1".."field_134").to_a percentage_threshold = (row_parsers.size * COLUMN_PERCENTAGE_ERROR_THRESHOLD).ceil diff --git a/app/services/bulk_upload/processor.rb b/app/services/bulk_upload/processor.rb index bbf7e6cd1..dcf68e594 100644 --- a/app/services/bulk_upload/processor.rb +++ b/app/services/bulk_upload/processor.rb @@ -12,11 +12,15 @@ 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? + if validator.any_setup_sections_incomplete? + send_setup_errors_mail + elsif validator.create_logs? + create_logs + send_fix_errors_mail if created_logs_but_incompleted? + send_success_mail if created_logs_and_all_completed? + else + send_correct_and_upload_again_mail + end rescue StandardError => e Sentry.capture_exception(e) send_failure_mail @@ -26,16 +30,28 @@ class BulkUpload::Processor private + def send_setup_errors_mail + BulkUploadMailer + .send_bulk_upload_failed_file_setup_error_mail(bulk_upload:) + .deliver_later + end + def send_correct_and_upload_again_mail - BulkUploadMailer.send_correct_and_upload_again_mail(bulk_upload:).deliver_later + 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 + BulkUploadMailer + .send_bulk_upload_with_errors_mail(bulk_upload:) + .deliver_later end def send_success_mail - BulkUploadMailer.send_bulk_upload_complete_mail(user:, bulk_upload:).deliver_later + BulkUploadMailer + .send_bulk_upload_complete_mail(user:, bulk_upload:) + .deliver_later end def created_logs_but_incompleted? @@ -47,7 +63,9 @@ private end def send_failure_mail - BulkUploadMailer.send_bulk_upload_failed_service_error_mail(bulk_upload:).deliver_later + BulkUploadMailer + .send_bulk_upload_failed_service_error_mail(bulk_upload:) + .deliver_later end def user diff --git a/db/migrate/20230301120116_add_category_to_bulk_upload_errors.rb b/db/migrate/20230301120116_add_category_to_bulk_upload_errors.rb new file mode 100644 index 000000000..000cbbd6d --- /dev/null +++ b/db/migrate/20230301120116_add_category_to_bulk_upload_errors.rb @@ -0,0 +1,5 @@ +class AddCategoryToBulkUploadErrors < ActiveRecord::Migration[7.0] + def change + add_column :bulk_upload_errors, :category, :text, null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index afa0b6b74..c2f8f8600 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -26,6 +26,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_01_144555) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "col" + t.text "category" t.index ["bulk_upload_id"], name: "index_bulk_upload_errors_on_bulk_upload_id" end diff --git a/spec/mailers/bulk_upload_mailer_spec.rb b/spec/mailers/bulk_upload_mailer_spec.rb index a0042cf69..0e38617a3 100644 --- a/spec/mailers/bulk_upload_mailer_spec.rb +++ b/spec/mailers/bulk_upload_mailer_spec.rb @@ -12,6 +12,38 @@ RSpec.describe BulkUploadMailer do allow(notify_client).to receive(:send_email).and_return(true) end + describe "#send_bulk_upload_failed_file_setup_error_mail" do + before do + create(:bulk_upload_error, bulk_upload:, col: "A", field: "field_1", category: "setup") + create(:bulk_upload_error, bulk_upload:, col: "E", field: "field_4", category: "setup") + create(:bulk_upload_error, bulk_upload:, col: "F", field: "field_5") + end + + let(:expected_errors) do + [ + "- Column A (What is the letting type?)", + "- Column E (Management group code)", + ] + end + + it "sends correctly formed email" do + expect(notify_client).to receive(:send_email).with( + email_address: bulk_upload.user.email, + template_id: described_class::BULK_UPLOAD_FAILED_FILE_SETUP_ERROR_TEMPLATE_ID, + personalisation: { + filename: bulk_upload.filename, + upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time), + lettings_or_sales: bulk_upload.log_type, + year_combo: bulk_upload.year_combo, + errors_list: expected_errors.join("\n"), + bulk_upload_link: start_bulk_upload_lettings_logs_url, + }, + ) + + mailer.send_bulk_upload_failed_file_setup_error_mail(bulk_upload:) + end + end + describe "#send_bulk_upload_complete_mail" do it "sends correctly formed email" do expect(notify_client).to receive(:send_email).with( diff --git a/spec/services/bulk_upload/lettings/row_parser_spec.rb b/spec/services/bulk_upload/lettings/row_parser_spec.rb index cd16a88a1..867e5028b 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/row_parser_spec.rb @@ -210,6 +210,16 @@ RSpec.describe BulkUpload::Lettings::RowParser do end end + context "when setup section not complete" do + let(:attributes) { { bulk_upload:, field_7: "123" } } + + it "has errors on setup fields" do + errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute) + + expect(errors).to eql(%i[field_1 field_129 field_130 field_98 field_97 field_96 field_111 field_113]) + end + end + describe "#field_1" do context "when null" do let(:attributes) { { bulk_upload:, field_1: nil, field_4: "1" } } diff --git a/spec/services/bulk_upload/lettings/validator_spec.rb b/spec/services/bulk_upload/lettings/validator_spec.rb index 0aaeaac78..ca52a19fe 100644 --- a/spec/services/bulk_upload/lettings/validator_spec.rb +++ b/spec/services/bulk_upload/lettings/validator_spec.rb @@ -51,6 +51,11 @@ RSpec.describe BulkUpload::Lettings::Validator do expect(error.row).to eql("7") expect(error.cell).to eql("L7") expect(error.col).to eql("L") + expect(error.category).to be_nil + + error = BulkUploadError.order(:row, :field).find_by(field: "field_111") + + expect(error.category).to eql("setup") end end diff --git a/spec/services/bulk_upload/processor_spec.rb b/spec/services/bulk_upload/processor_spec.rb index d56b2ff4e..48b0c0258 100644 --- a/spec/services/bulk_upload/processor_spec.rb +++ b/spec/services/bulk_upload/processor_spec.rb @@ -91,6 +91,42 @@ RSpec.describe BulkUpload::Processor do end end + context "when a log has an incomplete setup section" do + let(:mock_downloader) do + instance_double( + BulkUpload::Downloader, + call: nil, + path: file_fixture("2022_23_lettings_bulk_upload.csv"), + delete_local_file!: nil, + ) + end + + let(:mock_validator) do + instance_double( + BulkUpload::Lettings::Validator, + invalid?: false, + call: nil, + any_setup_sections_incomplete?: true, + ) + end + + before do + allow(BulkUpload::Downloader).to receive(:new).with(bulk_upload:).and_return(mock_downloader) + allow(BulkUpload::Lettings::Validator).to receive(:new).and_return(mock_validator) + end + + it "sends setup failure email" do + mail_double = instance_double("ActionMailer::MessageDelivery", deliver_later: nil) + + allow(BulkUploadMailer).to receive(:send_bulk_upload_failed_file_setup_error_mail).and_return(mail_double) + + processor.call + + expect(BulkUploadMailer).to have_received(:send_bulk_upload_failed_file_setup_error_mail) + expect(mail_double).to have_received(:deliver_later) + end + end + context "when processing a bulk upload with errors but below threshold (therefore creates logs)" do let(:mock_downloader) do instance_double( @@ -106,6 +142,7 @@ RSpec.describe BulkUpload::Processor do BulkUpload::Lettings::Validator, invalid?: false, call: nil, + any_setup_sections_incomplete?: false, create_logs?: true, ) end @@ -156,6 +193,7 @@ RSpec.describe BulkUpload::Processor do BulkUpload::Lettings::Validator, invalid?: false, call: nil, + any_setup_sections_incomplete?: false, create_logs?: false, ) end @@ -216,6 +254,7 @@ RSpec.describe BulkUpload::Processor do BulkUpload::Lettings::Validator, call: nil, create_logs?: true, + any_setup_sections_incomplete?: false, invalid?: false, ) end From 7853b75eaff1a8c534d54a2248cfbea06f5ced15 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Mon, 6 Mar 2023 10:06:13 +0000 Subject: [PATCH 04/19] CLDC-1904 Infer unitletas for renewal logs (#1303) * Infer unitletas for renewal logs * Update unitletas mapping for 23/24 forms --- .../lettings_log_variables.rb | 29 +++- spec/models/lettings_log_spec.rb | 159 +++++++++++++++++- 2 files changed, 186 insertions(+), 2 deletions(-) diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index 37e659652..d42fb3903 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -1,5 +1,31 @@ module DerivedVariables::LettingsLogVariables - RENT_TYPE_MAPPING = { 0 => 1, 1 => 2, 2 => 2, 3 => 3, 4 => 3, 5 => 3 }.freeze + # renttype and unitletas values are different for intermediate rent (3 for renttype and 4 for unitletas) + RENT_TYPE_MAPPING = { + 0 => 1, # "Social Rent" => "Social Rent" + 1 => 2, # "Affordable Rent" => "Affordable Rent" + 2 => 2, # "London Affordable Rent" => "Affordable Rent" + 3 => 3, # "Rent to Buy" => "Intermediate Rent" + 4 => 3, # "London Living Rent" => "Intermediate Rent" + 5 => 3, # "Other intermediate rent product" => "Intermediate Rent" + }.freeze + + UNITLETAS_MAPPING = { + 0 => 1, # "Social Rent" => "Social Rent basis" + 1 => 2, # "Affordable Rent" => "Affordable Rent basis" + 2 => 2, # "London Affordable Rent" => "Affordable Rent basis" + 3 => 4, # "Rent to Buy" => "Intermediate Rent basis" + 4 => 4, # "London Living Rent" => "Intermediate Rent basis" + 5 => 4, # "Other intermediate rent product" => "Intermediate Rent basis" + }.freeze + + UNITLETAS_MAPPING_23_24 = { + 0 => 1, # "Social Rent" => "Social Rent basis" + 1 => 2, # "Affordable Rent" => "Affordable Rent basis" + 2 => 5, # "London Affordable Rent" => "London Affordable Rent basis" + 3 => 6, # "Rent to Buy" => "Rent to Buy basis" + 4 => 7, # "London Living Rent" => "London Living Rent basis" + 5 => 8, # "Other intermediate rent product" => "Another Intermediate Rent basis" + }.freeze def scheme_has_multiple_locations? return false unless scheme @@ -49,6 +75,7 @@ module DerivedVariables::LettingsLogVariables self.voiddate = startdate self.first_time_property_let_as_social_housing = 0 self.rsnvac = 14 + self.unitletas = form.start_date.year >= 2023 ? UNITLETAS_MAPPING_23_24[rent_type] : UNITLETAS_MAPPING[rent_type] if is_general_needs? # fixed term self.prevten = 32 if managing_organisation&.provider_type == "PRP" diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index b28f8bfea..c1bfafb57 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -1474,6 +1474,163 @@ RSpec.describe LettingsLog do expect(record_from_db["rsnvac"]).to eq(14) expect(lettings_log["rsnvac"]).to eq(14) end + + context "when deriving renttype and unitletas" do + before do + allow(FeatureToggle).to receive(:startdate_two_week_validation_enabled?).and_return(false) + lettings_log.update!(rent_type:, irproduct_other: "other") + end + + context "when the rent_type is Social Rent (0)" do + let(:rent_type) { 0 } + + it "derives and saves renttype and unitletas as Social rent(1)" do + record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.renttype).to eq(1) + expect(record_from_db["renttype"]).to eq(1) + end + + it "derives and saves unitletas as Social rent(1)" do + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(1) + expect(record_from_db["unitletas"]).to eq(1) + end + + context "and it is a 23/24 form" do + it "derives and saves unitletas as Social rent(1)" do + lettings_log.update!(startdate: Time.zone.local(2023, 5, 1)) + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(1) + expect(record_from_db["unitletas"]).to eq(1) + end + end + end + + context "when the rent_type is Affordable Rent(1)" do + let(:rent_type) { 1 } + + it "derives and saves renttype as Affordable Rent(2)" do + record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.renttype).to eq(2) + expect(record_from_db["renttype"]).to eq(2) + end + + it "derives and saves unitletas as Affordable Rent(2)" do + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(2) + expect(record_from_db["unitletas"]).to eq(2) + end + + context "and it is a 23/24 form" do + it "derives and saves unitletas as Affordable Rent basis(2)" do + lettings_log.update!(startdate: Time.zone.local(2023, 5, 1)) + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(2) + expect(record_from_db["unitletas"]).to eq(2) + end + end + end + + context "when the rent_type is London Affordable Rent(2)" do + let(:rent_type) { 2 } + + it "derives and saves renttype as London Affordable Rent(2)" do + record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.renttype).to eq(2) + expect(record_from_db["renttype"]).to eq(2) + end + + it "derives and saves unitletas as London Affordable Rent(2)" do + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(2) + expect(record_from_db["unitletas"]).to eq(2) + end + + context "and it is a 23/24 form" do + it "derives and saves unitletas as London Affordable Rent basis(5)" do + lettings_log.update!(startdate: Time.zone.local(2023, 5, 1)) + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(5) + expect(record_from_db["unitletas"]).to eq(5) + end + end + end + + context "when the rent_type is Rent to Buy(3)" do + let(:rent_type) { 3 } + + it "derives and saves renttype as Intermediate Rent(3)" do + record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.renttype).to eq(3) + expect(record_from_db["renttype"]).to eq(3) + end + + it "derives and saves unitletas as Intermediate Rent(4)" do + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(4) + expect(record_from_db["unitletas"]).to eq(4) + end + + context "and it is a 23/24 form" do + it "derives and saves unitletas as Rent to Buy basis(6)" do + lettings_log.update!(startdate: Time.zone.local(2023, 5, 1)) + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(6) + expect(record_from_db["unitletas"]).to eq(6) + end + end + end + + context "when the rent_type is London Living Rent(4)" do + let(:rent_type) { 4 } + + it "derives and saves renttype as Intermediate Rent(3)" do + record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.renttype).to eq(3) + expect(record_from_db["renttype"]).to eq(3) + end + + it "derives and saves unitletas as Intermediate Rent(4)" do + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(4) + expect(record_from_db["unitletas"]).to eq(4) + end + + context "and it is a 23/24 form" do + it "derives and saves unitletas as London Living Rent basis(7)" do + lettings_log.update!(startdate: Time.zone.local(2023, 5, 1)) + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(7) + expect(record_from_db["unitletas"]).to eq(7) + end + end + end + + context "when the rent_type is Other intermediate rent product(5)" do + let(:rent_type) { 5 } + + it "derives and saves renttype as Intermediate Rent(3)" do + record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.renttype).to eq(3) + expect(record_from_db["renttype"]).to eq(3) + end + + it "derives and saves unitletas as Intermediate Rent(4)" do + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(4) + expect(record_from_db["unitletas"]).to eq(4) + end + + context "and it is a 23/24 form" do + it "derives and saves unitletas as Other intermediate rent basis(8)" do + lettings_log.update!(startdate: Time.zone.local(2023, 5, 1)) + record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0] + expect(lettings_log.unitletas).to eq(8) + expect(record_from_db["unitletas"]).to eq(8) + end + end + end + end end context "when answering the household characteristics questions" do @@ -1966,7 +2123,7 @@ 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, needstype: 1) } + let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, period: 2, needstype: 1, renewal: 0) } it "does not clear the answer value" do lettings_log.update!({ unitletas: 1 }) From dc3c118bbd6ca5e6ae8ad76a21308d2f32e6d154 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:41:51 +0000 Subject: [PATCH 05/19] Improve import error logging (#1382) --- app/services/imports/sales_logs_import_service.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/services/imports/sales_logs_import_service.rb b/app/services/imports/sales_logs_import_service.rb index d9ac0b0d4..4c49f0037 100644 --- a/app/services/imports/sales_logs_import_service.rb +++ b/app/services/imports/sales_logs_import_service.rb @@ -168,8 +168,16 @@ module Imports end end - def rescue_validation_or_raise(sales_log, _attributes, _previous_status, exception) + def rescue_validation_or_raise(sales_log, attributes, _previous_status, exception) @logger.error("Log #{sales_log.old_id}: Failed to import") + sales_log.errors.each do |error| + @logger.error("Validation error: Field #{error.attribute}:") + @logger.error("\tOwning Organisation: #{sales_log.owning_organisation&.name}") + @logger.error("\tOld CORE ID: #{sales_log.old_id}") + @logger.error("\tOld CORE: #{attributes[error.attribute.to_s]&.inspect}") + @logger.error("\tNew CORE: #{sales_log.read_attribute(error.attribute)&.inspect}") + @logger.error("\tError message: #{error.type}") + end raise exception end From 931922b01859899479650045c0d3a218c0685abf Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Tue, 7 Mar 2023 08:48:35 +0000 Subject: [PATCH 06/19] CLDC-1938 Set soft validations to confirmed (#1384) * Set soft validations to confirmed * Add remaining default soft validation values * Clears invalidated fields for in progress logs --- .../imports/sales_logs_import_service.rb | 48 ++++- .../shared_ownership_sales_log3.xml | 2 +- .../imports/sales_logs_import_service_spec.rb | 191 ++++++++++++++++++ 3 files changed, 229 insertions(+), 12 deletions(-) diff --git a/app/services/imports/sales_logs_import_service.rb b/app/services/imports/sales_logs_import_service.rb index 4c49f0037..6244e28e7 100644 --- a/app/services/imports/sales_logs_import_service.rb +++ b/app/services/imports/sales_logs_import_service.rb @@ -137,6 +137,23 @@ module Imports attributes["is_la_inferred"] = false end + # Soft validations can become required answers, set them to yes by default + attributes["mortgage_value_check"] = 0 + attributes["shared_ownership_deposit_value_check"] = 0 + attributes["value_value_check"] = 0 + attributes["savings_value_check"] = 0 + attributes["income1_value_check"] = 0 + attributes["deposit_value_check"] = 0 + attributes["wheel_value_check"] = 0 + attributes["retirement_value_check"] = 0 + attributes["extrabor_value_check"] = 0 + attributes["grant_value_check"] = 0 + attributes["staircase_bought_value_check"] = 0 + attributes["deposit_and_mortgage_value_check"] = 0 + attributes["old_persons_shared_ownership_value_check"] = 0 + attributes["income2_value_check"] = 0 + attributes["monthly_charges_value_check"] = 0 + # Sets the log creator owner_id = meta_field_value(xml_doc, "owner-user-id").strip if owner_id.present? @@ -168,17 +185,26 @@ module Imports end end - def rescue_validation_or_raise(sales_log, attributes, _previous_status, exception) - @logger.error("Log #{sales_log.old_id}: Failed to import") - sales_log.errors.each do |error| - @logger.error("Validation error: Field #{error.attribute}:") - @logger.error("\tOwning Organisation: #{sales_log.owning_organisation&.name}") - @logger.error("\tOld CORE ID: #{sales_log.old_id}") - @logger.error("\tOld CORE: #{attributes[error.attribute.to_s]&.inspect}") - @logger.error("\tNew CORE: #{sales_log.read_attribute(error.attribute)&.inspect}") - @logger.error("\tError message: #{error.type}") + def rescue_validation_or_raise(sales_log, attributes, previous_status, exception) + if %w[saved submitted-invalid].include?(previous_status) + sales_log.errors.each do |error| + @logger.warn("Log #{sales_log.old_id}: Removing field #{error.attribute} from log triggering validation: #{error.type}") + attributes.delete(error.attribute.to_s) + end + @logs_overridden << sales_log.old_id + save_sales_log(attributes, previous_status) + else + @logger.error("Log #{sales_log.old_id}: Failed to import") + sales_log.errors.each do |error| + @logger.error("Validation error: Field #{error.attribute}:") + @logger.error("\tOwning Organisation: #{sales_log.owning_organisation&.name}") + @logger.error("\tOld CORE ID: #{sales_log.old_id}") + @logger.error("\tOld CORE: #{attributes[error.attribute.to_s]&.inspect}") + @logger.error("\tNew CORE: #{sales_log.read_attribute(error.attribute)&.inspect}") + @logger.error("\tError message: #{error.type}") + end + raise exception end - raise exception end def compute_differences(sales_log, attributes) @@ -197,6 +223,7 @@ module Imports def fields_not_present_in_softwire_data %w[created_by income1_value_check + income2_value_check mortgage_value_check savings_value_check deposit_value_check @@ -396,7 +423,6 @@ module Imports end def monthly_charges(xml_doc, attributes) - safe_string_as_decimal(xml_doc, "Q29MonthlyCharges") case attributes["ownershipsch"] when 1 safe_string_as_decimal(xml_doc, "Q29MonthlyCharges") diff --git a/spec/fixtures/imports/sales_logs/shared_ownership_sales_log3.xml b/spec/fixtures/imports/sales_logs/shared_ownership_sales_log3.xml index b5dabde97..a2f124032 100644 --- a/spec/fixtures/imports/sales_logs/shared_ownership_sales_log3.xml +++ b/spec/fixtures/imports/sales_logs/shared_ownership_sales_log3.xml @@ -144,7 +144,7 @@ 0 0 0 - 76000 + 1 47000 0 diff --git a/spec/services/imports/sales_logs_import_service_spec.rb b/spec/services/imports/sales_logs_import_service_spec.rb index 056d492de..b8d1c592f 100644 --- a/spec/services/imports/sales_logs_import_service_spec.rb +++ b/spec/services/imports/sales_logs_import_service_spec.rb @@ -203,6 +203,197 @@ RSpec.describe Imports::SalesLogsImportService do end end + context "and the mortgage soft validation is triggered (mortgage_value_check)" do + let(:sales_log_id) { "discounted_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:Q2Person1Income").content = "10" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and the shared ownership deposit soft validation is triggered (shared_ownership_deposit_value_check)" do + let(:sales_log_id) { "shared_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:DerSaleType").content = "2" + sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "275000" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and the purchase price soft validation is triggered (value_value_check)" do + let(:sales_log_id) { "shared_ownership_sales_log" } + + before do + LaSaleRange.create!(la: "E09000033", bedrooms: 2, soft_min: 177_000, soft_max: 384_000, start_year: 2022) + sales_log_xml.at_xpath("//xmlns:Q22PurchasePrice").content = "2750" + sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "2750" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and the purchase price soft validation is triggered (income1_value_check, income2_value_check)" do + let(:sales_log_id) { "shared_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:Q2Person1Income").content = "20" + sales_log_xml.at_xpath("//xmlns:Q2Person2Income").content = "10" + sales_log_xml.at_xpath("//xmlns:P2Eco").content = "1" + sales_log_xml.at_xpath("//xmlns:joint").content = "1 Yes" + sales_log_xml.at_xpath("//xmlns:JointMore").content = "2 No" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and the savings soft validation is triggered (savings_value_check)" do + let(:sales_log_id) { "shared_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:Q3Savings").content = "200750" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and the deposit soft validation is triggered (deposit_value_check)" do + let(:sales_log_id) { "shared_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:Q3Savings").content = "10" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and the wheelchair soft validation is triggered (wheel_value_check)" do + let(:sales_log_id) { "shared_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:Q10Wheelchair").content = "1" + sales_log_xml.at_xpath("//xmlns:Disability").content = "2" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and the retirement soft validation is triggered (retirement_value_check)" do + let(:sales_log_id) { "shared_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:P1Eco").content = "5" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and the grant soft validation is triggered (grant_value_check)" do + let(:sales_log_id) { "discounted_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:Q32Reductions").content = "5000" + sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "270000" + sales_log_xml.at_xpath("//xmlns:Q33Discount").content = "" + sales_log_xml.at_xpath("//xmlns:DerSaleType").content = "22" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and the stairbought soft validation is triggered (staircase_bought_value_check)" do + let(:sales_log_id) { "shared_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//xmlns:PercentBought").content = "51" + sales_log_xml.at_xpath("//xmlns:PercentOwns").content = "81" + sales_log_xml.at_xpath("//xmlns:Q17aStaircase").content = "1" + sales_log_xml.at_xpath("//xmlns:Q17Resale").content = "" + sales_log_xml.at_xpath("//xmlns:EXDAY").content = "" + sales_log_xml.at_xpath("//xmlns:EXMONTH").content = "" + sales_log_xml.at_xpath("//xmlns:EXYEAR").content = "" + sales_log_xml.at_xpath("//xmlns:HODAY").content = "" + sales_log_xml.at_xpath("//xmlns:HOMONTH").content = "" + sales_log_xml.at_xpath("//xmlns:HOYEAR").content = "" + end + + it "completes the log" do + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + expect(sales_log.status).to eq("completed") + end + end + + context "and it has an invalid record with invalid child, student and 16-19 age combination" do + let(:sales_log_id) { "discounted_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//meta:status").content = "submitted-invalid" + sales_log_xml.at_xpath("//xmlns:P2Age").content = 16 + sales_log_xml.at_xpath("//xmlns:P2Eco").content = 7 + sales_log_xml.at_xpath("//xmlns:P2Rel").content = "X" + end + + it "intercepts the relevant validation error" do + expect(logger).to receive(:warn).with(/Removing field age2 from log triggering validation: Person cannot be aged 16-19 if they are a student but don't have relationship ‘child’/) + expect(logger).to receive(:warn).with(/Removing field relat2 from log triggering validation: Answer must be ‘child’ if the person is aged 16-19 and a student/) + expect(logger).to receive(:warn).with(/Removing field ecstat2 from log triggering validation: Person cannot be a student if they are aged 16-19 but don‘t have relationship ‘child’/) + expect { sales_log_service.send(:create_log, sales_log_xml) } + .not_to raise_error + end + + it "clears out the invalid answers" do + allow(logger).to receive(:warn) + + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + + expect(sales_log).not_to be_nil + expect(sales_log.age2).to be_nil + expect(sales_log.relat2).to be_nil + expect(sales_log.ecstat2).to be_nil + end + end + context "when inferring default answers for completed sales logs" do context "when the armedforcesspouse is not answered" do let(:sales_log_id) { "discounted_ownership_sales_log" } From ecab6ba2e194ae3d53c242fce9169a160b32e248 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Tue, 7 Mar 2023 12:00:31 +0000 Subject: [PATCH 07/19] CLDC-1938 Reset invalid postcodes (#1385) * Reset invalid postcodes * lint * Look at the correct previuos postcode field * Check the error before clearing postcode known --- .../sales/household_validations.rb | 4 +-- .../imports/sales_logs_import_service.rb | 5 +++ .../imports/sales_logs_import_service_spec.rb | 31 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/models/validations/sales/household_validations.rb b/app/models/validations/sales/household_validations.rb index f9318ab0f..ec73b4abb 100644 --- a/app/models/validations/sales/household_validations.rb +++ b/app/models/validations/sales/household_validations.rb @@ -14,8 +14,8 @@ module Validations::Sales::HouseholdValidations return unless record.postcode_full && record.ppostcode_full && record.discounted_ownership_sale? unless record.postcode_full == record.ppostcode_full - record.errors.add :postcode_full, I18n.t("validations.household.postcode.discounted_ownership") - record.errors.add :ppostcode_full, I18n.t("validations.household.postcode.discounted_ownership") + record.errors.add :postcode_full, :postcodes_not_matching, message: I18n.t("validations.household.postcode.discounted_ownership") + record.errors.add :ppostcode_full, :postcodes_not_matching, message: I18n.t("validations.household.postcode.discounted_ownership") end end diff --git a/app/services/imports/sales_logs_import_service.rb b/app/services/imports/sales_logs_import_service.rb index 6244e28e7..ed127bff2 100644 --- a/app/services/imports/sales_logs_import_service.rb +++ b/app/services/imports/sales_logs_import_service.rb @@ -192,6 +192,11 @@ module Imports attributes.delete(error.attribute.to_s) end @logs_overridden << sales_log.old_id + if sales_log.errors.of_kind?(:postcode_full, :postcodes_not_matching) + @logger.warn("Log #{sales_log.old_id}: Removing postcode known and previous postcode known as the postcodes are invalid") + attributes.delete("pcodenk") + attributes.delete("ppcodenk") + end save_sales_log(attributes, previous_status) else @logger.error("Log #{sales_log.old_id}: Failed to import") diff --git a/spec/services/imports/sales_logs_import_service_spec.rb b/spec/services/imports/sales_logs_import_service_spec.rb index b8d1c592f..2481cdce7 100644 --- a/spec/services/imports/sales_logs_import_service_spec.rb +++ b/spec/services/imports/sales_logs_import_service_spec.rb @@ -394,6 +394,37 @@ RSpec.describe Imports::SalesLogsImportService do end end + context "and it has an invalid record with invalid postcodes" do + let(:sales_log_id) { "discounted_ownership_sales_log" } + + before do + sales_log_xml.at_xpath("//meta:status").content = "submitted-invalid" + sales_log_xml.at_xpath("//xmlns:Q7Postcode").content = "A1 1AA" + sales_log_xml.at_xpath("//xmlns:Q14Postcode").content = "A1 2AA" + end + + it "intercepts the relevant validation error" do + expect(logger).to receive(:warn).with(/Removing field postcode_full from log triggering validation: Buyer's last accommodation and discounted ownership postcodes must match/) + expect(logger).to receive(:warn).with(/Removing field ppostcode_full from log triggering validation: Buyer's last accommodation and discounted ownership postcodes must match/) + expect(logger).to receive(:warn).with(/Removing field postcode_full from log triggering validation: postcodes_not_matching/) + expect(logger).to receive(:warn).with(/Removing field ppostcode_full from log triggering validation: postcodes_not_matching/) + expect(logger).to receive(:warn).with(/Removing postcode known and previous postcode known as the postcodes are invalid/) + expect { sales_log_service.send(:create_log, sales_log_xml) } + .not_to raise_error + end + + it "clears out the invalid answers" do + allow(logger).to receive(:warn) + + sales_log_service.send(:create_log, sales_log_xml) + sales_log = SalesLog.find_by(old_id: sales_log_id) + + expect(sales_log).not_to be_nil + expect(sales_log.postcode_full).to be_nil + expect(sales_log.ppostcode_full).to be_nil + end + end + context "when inferring default answers for completed sales logs" do context "when the armedforcesspouse is not answered" do let(:sales_log_id) { "discounted_ownership_sales_log" } From 03b21e4a2a734d67f33c62050a73ab31ca77b0ce Mon Sep 17 00:00:00 2001 From: natdeanlewissoftwire <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Tue, 7 Mar 2023 16:26:28 +0000 Subject: [PATCH 08/19] CLDC-2035 Fix search bug (#1327) * feat: fix search bug * feat: add correct sales log search filter behaviour * refactor: removal of logic from erbs * refactor: linting * test: add tests to check log filtering works --- app/components/search_component.rb | 2 + app/helpers/logs_helper.rb | 16 ++++++ app/models/lettings_log.rb | 1 - app/models/log.rb | 1 + app/models/sales_log.rb | 7 ++- app/views/logs/index.html.erb | 4 +- spec/services/filter_service_spec.rb | 80 +++++++++++++++++++++++----- 7 files changed, 94 insertions(+), 17 deletions(-) diff --git a/app/components/search_component.rb b/app/components/search_component.rb index d13882645..36d621240 100644 --- a/app/components/search_component.rb +++ b/app/components/search_component.rb @@ -23,6 +23,8 @@ class SearchComponent < ViewComponent::Base user_path(current_user) elsif request.path.include?("organisations") organisations_path + elsif request.path.include?("sales-logs") + sales_logs_path elsif request.path.include?("logs") lettings_logs_path end diff --git a/app/helpers/logs_helper.rb b/app/helpers/logs_helper.rb index 88ab2b314..7653b7e5c 100644 --- a/app/helpers/logs_helper.rb +++ b/app/helpers/logs_helper.rb @@ -23,4 +23,20 @@ module LogsHelper array = bulk_upload ? [bulk_upload.id] : [] array.index_with { |_bulk_upload_id| "With logs from bulk upload" } end + + def search_label_for_controller(controller) + case log_type_for_controller(controller) + when "lettings" + "Search by log ID, tenant code, property reference or postcode" + when "sales" + "Search by log ID, purchaser code or postcode" + end + end + + def csv_download_url_for_controller(controller) + case log_type_for_controller(controller) + when "lettings" + csv_download_lettings_logs_path(search: params["search"]) + end + end end diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 6864738cf..6491c100f 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -40,7 +40,6 @@ class LettingsLog < Log scope :filter_by_year, ->(year) { where(startdate: Time.zone.local(year.to_i, 4, 1)...Time.zone.local(year.to_i + 1, 4, 1)) } scope :filter_by_tenant_code, ->(tenant_code) { where("tenancycode ILIKE ?", "%#{tenant_code}%") } scope :filter_by_propcode, ->(propcode) { where("propcode ILIKE ?", "%#{propcode}%") } - scope :filter_by_postcode, ->(postcode_full) { where("REPLACE(postcode_full, ' ', '') ILIKE ?", "%#{postcode_full.delete(' ')}%") } scope :filter_by_location_postcode, ->(postcode_full) { left_joins(:location).where("REPLACE(locations.postcode, ' ', '') ILIKE ?", "%#{postcode_full.delete(' ')}%") } scope :search_by, lambda { |param| filter_by_location_postcode(param) diff --git a/app/models/log.rb b/app/models/log.rb index 505839c69..f6f5f4173 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -18,6 +18,7 @@ class Log < ApplicationRecord years.each { |year| query = query.or(filter_by_year(year)) } query.all } + scope :filter_by_postcode, ->(postcode_full) { where("REPLACE(postcode_full, ' ', '') ILIKE ?", "%#{postcode_full.delete(' ')}%") } scope :filter_by_id, ->(id) { where(id:) } scope :filter_by_user, lambda { |selected_user, user| if !selected_user.include?("all") && user.present? diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index b2c6b08da..629215386 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -33,7 +33,12 @@ class SalesLog < Log before_validation :set_derived_fields! scope :filter_by_year, ->(year) { where(saledate: Time.zone.local(year.to_i, 4, 1)...Time.zone.local(year.to_i + 1, 4, 1)) } - scope :search_by, ->(param) { filter_by_id(param) } + scope :filter_by_purchaser_code, ->(purchid) { where("purchid ILIKE ?", "%#{purchid}%") } + scope :search_by, lambda { |param| + filter_by_purchaser_code(param) + .or(filter_by_postcode(param)) + .or(filter_by_id(param)) + } scope :filter_by_organisation, ->(org, _user = nil) { where(owning_organisation: org) } OPTIONAL_FIELDS = %w[saledate_check purchid monthly_charges_value_check old_persons_shared_ownership_value_check].freeze diff --git a/app/views/logs/index.html.erb b/app/views/logs/index.html.erb index 31768c731..0e4cd76df 100644 --- a/app/views/logs/index.html.erb +++ b/app/views/logs/index.html.erb @@ -64,9 +64,9 @@ <%= render partial: "log_filters" %>
- <%= render SearchComponent.new(current_user:, search_label: "Search by log ID, tenant code, property reference or postcode", value: @searched) %> + <%= render SearchComponent.new(current_user:, search_label: search_label_for_controller(controller), value: @searched) %> <%= govuk_section_break(visible: true, size: "m") %> - <%= render partial: "log_list", locals: { logs: @logs, title: "Logs", pagy: @pagy, searched: @searched, item_label:, total_count: @total_count, csv_download_url: csv_download_lettings_logs_path(search: @search_term) } %> + <%= render partial: "log_list", locals: { logs: @logs, title: "Logs", pagy: @pagy, searched: @searched, item_label:, total_count: @total_count, csv_download_url: csv_download_url_for_controller(controller) } %> <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "logs" } %>
diff --git a/spec/services/filter_service_spec.rb b/spec/services/filter_service_spec.rb index 1e892fd56..2c89b1302 100644 --- a/spec/services/filter_service_spec.rb +++ b/spec/services/filter_service_spec.rb @@ -2,26 +2,80 @@ require "rails_helper" describe FilterService do describe "filter_by_search" do - before do - FactoryBot.create_list(:organisation, 5) - FactoryBot.create(:organisation, name: "Acme LTD") - end + context "when filtering organisations" do + before do + FactoryBot.create_list(:organisation, 5) + FactoryBot.create(:organisation, name: "Acme LTD") + end + + let(:organisation_list) { Organisation.all } + + context "when given a search term" do + let(:search_term) { "Acme" } - let(:organisation_list) { Organisation.all } + it "filters the collection on search term" do + expect(described_class.filter_by_search(organisation_list, search_term).count).to eq(1) + end + end - context "when given a search term" do - let(:search_term) { "Acme" } + context "when not given a search term" do + let(:search_term) { nil } - it "filters the collection on search term" do - expect(described_class.filter_by_search(organisation_list, search_term).count).to eq(1) + it "does not filter the given collection" do + expect(described_class.filter_by_search(organisation_list, search_term).count).to eq(6) + end end end - context "when not given a search term" do - let(:search_term) { nil } + context "when filtering logs" do + context "when filtering lettings logs" do + before do + FactoryBot.create_list(:lettings_log, 5) + FactoryBot.create(:lettings_log, postcode_full: "SW1 1AA") + end + + let(:lettings_log_list) { LettingsLog.all } + + context "when given a postcode" do + let(:search_term) { "SW1 1AA" } + + it "filters the collection on search term" do + expect(described_class.filter_by_search(lettings_log_list, search_term).count).to eq(1) + end + end + + context "when not given a search term" do + let(:search_term) { nil } + + it "does not filter the given collection" do + expect(described_class.filter_by_search(lettings_log_list, search_term).count).to eq(6) + end + end + end + + context "when filtering sales logs" do + before do + FactoryBot.create_list(:sales_log, 5) + FactoryBot.create(:sales_log, purchid: "2") + end + + let(:sales_log_list) { SalesLog.all } + + context "when given a purchid" do + let(:search_term) { "2" } + + it "filters the collection on search term" do + expect(described_class.filter_by_search(sales_log_list, search_term).count).to eq(1) + end + end + + context "when not given a search term" do + let(:search_term) { nil } - it "does not filter the given collection" do - expect(described_class.filter_by_search(organisation_list, search_term).count).to eq(6) + it "does not filter the given collection" do + expect(described_class.filter_by_search(sales_log_list, search_term).count).to eq(6) + end + end end end end From 1b34af2629a0fb6c6475693b0651ccf262f37f5d Mon Sep 17 00:00:00 2001 From: SamSeed-Softwire <63662292+SamSeed-Softwire@users.noreply.github.com> Date: Tue, 7 Mar 2023 16:27:00 +0000 Subject: [PATCH 09/19] feat: update header for housingneeds_other (#1389) --- app/models/form/lettings/questions/housingneeds_other.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/form/lettings/questions/housingneeds_other.rb b/app/models/form/lettings/questions/housingneeds_other.rb index 14d6c6d01..18d544ca9 100644 --- a/app/models/form/lettings/questions/housingneeds_other.rb +++ b/app/models/form/lettings/questions/housingneeds_other.rb @@ -3,7 +3,7 @@ class Form::Lettings::Questions::HousingneedsOther < ::Form::Question super @id = "housingneeds_other" @check_answer_label = "Other disabled access needs" - @header = "Do they have any other access needs?" + @header = "Do they have any other disabled access needs?" @type = "radio" @check_answers_card_number = 0 @hint_text = "" From 74f264f6f3b07d2d98231f3e682cd8b887cbcd8e Mon Sep 17 00:00:00 2001 From: Arthur Campbell <51094020+arfacamble@users.noreply.github.com> Date: Tue, 7 Mar 2023 16:29:08 +0000 Subject: [PATCH 10/19] change question and hint copy, create test file for question class (#1374) --- .../lettings/questions/offered_social_let.rb | 4 +- .../questions/offered_social_let_spec.rb | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 spec/models/form/lettings/questions/offered_social_let_spec.rb diff --git a/app/models/form/lettings/questions/offered_social_let.rb b/app/models/form/lettings/questions/offered_social_let.rb index d2658a8e6..c87ffb998 100644 --- a/app/models/form/lettings/questions/offered_social_let.rb +++ b/app/models/form/lettings/questions/offered_social_let.rb @@ -3,13 +3,13 @@ class Form::Lettings::Questions::OfferedSocialLet < ::Form::Question super @id = "offered" @check_answer_label = "Times previously offered since becoming available" - @header = "Since becoming available, how many times has the property been previously offered?" + @header = "How many times was the property offered between becoming vacant and this letting?" @type = "numeric" @width = 2 @check_answers_card_number = 0 @max = 150 @min = 0 - @hint_text = "If the property is being offered for let for the first time, enter 0." + @hint_text = "Do not include the offer that led to this letting.This is after the last tenancy ended. If the property is being offered for let for the first time, enter 0." @step = 1 end end diff --git a/spec/models/form/lettings/questions/offered_social_let_spec.rb b/spec/models/form/lettings/questions/offered_social_let_spec.rb new file mode 100644 index 000000000..8a3622e02 --- /dev/null +++ b/spec/models/form/lettings/questions/offered_social_let_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Questions::OfferedSocialLet, type: :model do + subject(:question) { described_class.new(nil, nil, page) } + + 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 "offered" + end + + it "has the correct header" do + expect(question.header).to eq "How many times was the property offered between becoming vacant and this letting?" + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq "Times previously offered since becoming available" + end + + it "has the correct type" do + expect(question.type).to eq "numeric" + end + + it "has the correct minimum and maximum values" do + expect(question.min).to eq 0 + expect(question.max).to eq 150 + end + + it "has the correct step" do + expect(question.step).to eq 1 + 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 eq "Do not include the offer that led to this letting.This is after the last tenancy ended. If the property is being offered for let for the first time, enter 0." + end +end From f361580d14421c2041385e5122be32860dd30e3c Mon Sep 17 00:00:00 2001 From: natdeanlewissoftwire <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Wed, 8 Mar 2023 10:59:26 +0000 Subject: [PATCH 11/19] feat: update rack version (#1394) --- Gemfile | 1 + Gemfile.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 358e71070..741f484bd 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,7 @@ gem "paper_trail" # Store active record objects in version whodunnits gem "paper_trail-globalid" # Request rate limiting +gem "rack", ">= 2.2.6.3" gem "rack-attack" gem "redis", "~> 4.8" # Receive exceptions and configure alerts diff --git a/Gemfile.lock b/Gemfile.lock index 7da2ef437..b9b16ab1e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -279,7 +279,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.6.2) - rack (2.2.6.2) + rack (2.2.6.3) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-mini-profiler (2.3.4) @@ -478,6 +478,7 @@ DEPENDENCIES propshaft pry-byebug puma (~> 5.0) + rack (>= 2.2.6.3) rack-attack rack-mini-profiler (~> 2.0) rails (~> 7.0.2) From 83981ea993013ec53b4937e308d86a30cbb4150b Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Wed, 8 Mar 2023 11:19:35 +0000 Subject: [PATCH 12/19] copy change for local authority known question (#1387) --- .../form/sales/questions/property_local_authority_known.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/form/sales/questions/property_local_authority_known.rb b/app/models/form/sales/questions/property_local_authority_known.rb index cbb534a06..912ddbd8e 100644 --- a/app/models/form/sales/questions/property_local_authority_known.rb +++ b/app/models/form/sales/questions/property_local_authority_known.rb @@ -3,7 +3,7 @@ class Form::Sales::Questions::PropertyLocalAuthorityKnown < ::Form::Question super @id = "la_known" @check_answer_label = "Local authority known" - @header = "Do you know the local authority of the property?" + @header = "Do you know the property’s local authority?" @type = "radio" @answer_options = ANSWER_OPTIONS @conditional_for = { "la" => [1] } From 13151fdaf7a87c038b33f84c6384f2203922e039 Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Wed, 8 Mar 2023 11:30:58 +0000 Subject: [PATCH 13/19] CLDC-1841 Copy change for net income known question (#1386) * copy change for net income known question * add minimal testing for question --- .../form/lettings/questions/net_income_known.rb | 2 +- .../lettings/questions/net_income_known_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 spec/models/form/lettings/questions/net_income_known_spec.rb diff --git a/app/models/form/lettings/questions/net_income_known.rb b/app/models/form/lettings/questions/net_income_known.rb index 5e9b1b4da..760c20b9f 100644 --- a/app/models/form/lettings/questions/net_income_known.rb +++ b/app/models/form/lettings/questions/net_income_known.rb @@ -2,7 +2,7 @@ class Form::Lettings::Questions::NetIncomeKnown < ::Form::Question def initialize(id, hsh, page) super @id = "net_income_known" - @check_answer_label = "Do you know the household’s combined income?" + @check_answer_label = "Do you know the household’s combined total income after tax?" @header = "Do you know the household’s combined income after tax?" @type = "radio" @check_answers_card_number = 0 diff --git a/spec/models/form/lettings/questions/net_income_known_spec.rb b/spec/models/form/lettings/questions/net_income_known_spec.rb new file mode 100644 index 000000000..4a0925fa4 --- /dev/null +++ b/spec/models/form/lettings/questions/net_income_known_spec.rb @@ -0,0 +1,17 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Questions::NetIncomeKnown do + subject(:question) { described_class.new(nil, {}, nil) } + + describe "#id" do + it "is net_income_known" do + expect(question.id).to eql("net_income_known") + end + end + + describe "#type" do + it "is radio" do + expect(question.type).to eql("radio") + end + end +end From e1b6e0f5a6fa7ee0594a824463925a5f4ee6675b Mon Sep 17 00:00:00 2001 From: Phil Lee Date: Wed, 8 Mar 2023 11:32:46 +0000 Subject: [PATCH 14/19] CLDC-2001 Bulk upload stacked errors (#1377) * limit setup errors if errors already present * bulk upload beter handles age validations - limit the number of errors on a field. if the field already has an existing error it does not add further errors to the field - shim in extra validation for ages which take precedent over existing log level validations which are lacking * setup errors always added - no longer observes if the grouping already has errors - as we have fine grain control in this class of how errors should be * fix missing pass through for renewal --- .../bulk_upload/lettings/row_parser.rb | 55 +++- .../bulk_upload/lettings/row_parser_spec.rb | 264 ++++++++++-------- .../bulk_upload/lettings/validator_spec.rb | 15 +- spec/support/bulk_upload/log_to_csv.rb | 16 +- 4 files changed, 210 insertions(+), 140 deletions(-) diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/row_parser.rb index 289566508..b772d0153 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/row_parser.rb @@ -143,6 +143,20 @@ 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_12, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 1 must be a number or the letter R" } + validates :field_13, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 2 must be a number or the letter R" } + validates :field_14, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 3 must be a number or the letter R" } + validates :field_15, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 4 must be a number or the letter R" } + validates :field_16, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 5 must be a number or the letter R" } + validates :field_17, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 6 must be a number or the letter R" } + validates :field_18, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 7 must be a number or the letter R" } + validates :field_19, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 8 must be a number or the letter R" } + + validates :field_96, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (day)") } + validates :field_97, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (month)") } + validates :field_98, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (year)") } + validates :field_98, format: { with: /\A\d{2}\z/, message: I18n.t("validations.setup.startdate.year_not_two_digits") } validate :validate_data_types @@ -181,7 +195,12 @@ class BulkUpload::Lettings::RowParser log.errors.each do |error| fields = field_mapping_for_errors[error.attribute] || [] - fields.each { |field| errors.add(field, error.type) } + + fields.each do |field| + unless errors.include?(field) + errors.add(field, error.type) + end + end end errors.blank? @@ -341,7 +360,7 @@ private end def validate_relevant_collection_window - return unless start_date && bulk_upload.form + return if start_date.blank? || bulk_upload.form.blank? unless bulk_upload.form.valid_start_date_for_form?(start_date) errors.add(:field_96, I18n.t("validations.date.outside_collection_window")) @@ -351,6 +370,8 @@ private end def start_date + return if field_98.blank? || field_97.blank? || field_96.blank? + Date.parse("20#{field_98.to_s.rjust(2, '0')}-#{field_97}-#{field_96}") rescue StandardError nil @@ -392,9 +413,17 @@ private next if question.completed?(log) if setup_question?(question) - fields.each { |field| errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase), category: :setup) } + fields.each do |field| + if errors[field].present? + errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase), category: :setup) + end + end else - fields.each { |field| errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase)) } + fields.each do |field| + unless errors.any? { |e| fields.include?(e.attribute) } + errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase)) + end + end end end end @@ -642,28 +671,28 @@ private attributes["declaration"] = field_132 attributes["age1_known"] = field_12 == "R" ? 1 : 0 - attributes["age1"] = field_12 if attributes["age1_known"].zero? + attributes["age1"] = field_12 if attributes["age1_known"].zero? && field_12&.match(/\A\d{1,3}\z|\AR\z/) attributes["age2_known"] = field_13 == "R" ? 1 : 0 - attributes["age2"] = field_13 if attributes["age2_known"].zero? + attributes["age2"] = field_13 if attributes["age2_known"].zero? && field_13&.match(/\A\d{1,3}\z|\AR\z/) attributes["age3_known"] = field_14 == "R" ? 1 : 0 - attributes["age3"] = field_14 if attributes["age3_known"].zero? + attributes["age3"] = field_14 if attributes["age3_known"].zero? && field_14&.match(/\A\d{1,3}\z|\AR\z/) attributes["age4_known"] = field_15 == "R" ? 1 : 0 - attributes["age4"] = field_15 if attributes["age4_known"].zero? + attributes["age4"] = field_15 if attributes["age4_known"].zero? && field_15&.match(/\A\d{1,3}\z|\AR\z/) attributes["age5_known"] = field_16 == "R" ? 1 : 0 - attributes["age5"] = field_16 if attributes["age5_known"].zero? + attributes["age5"] = field_16 if attributes["age5_known"].zero? && field_16&.match(/\A\d{1,3}\z|\AR\z/) attributes["age6_known"] = field_17 == "R" ? 1 : 0 - attributes["age6"] = field_17 if attributes["age6_known"].zero? + attributes["age6"] = field_17 if attributes["age6_known"].zero? && field_17&.match(/\A\d{1,3}\z|\AR\z/) attributes["age7_known"] = field_18 == "R" ? 1 : 0 - attributes["age7"] = field_18 if attributes["age7_known"].zero? + attributes["age7"] = field_18 if attributes["age7_known"].zero? && field_18&.match(/\A\d{1,3}\z|\AR\z/) attributes["age8_known"] = field_19 == "R" ? 1 : 0 - attributes["age8"] = field_19 if attributes["age8_known"].zero? + attributes["age8"] = field_19 if attributes["age8_known"].zero? && field_19&.match(/\A\d{1,3}\z|\AR\z/) attributes["sex1"] = field_20 attributes["sex2"] = field_21 @@ -882,6 +911,8 @@ private 0 when nil rsnvac == 14 ? 1 : 0 + else + field_134 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 867e5028b..73b35d70c 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/row_parser_spec.rb @@ -27,6 +27,118 @@ RSpec.describe BulkUpload::Lettings::RowParser do } end + let(:valid_attributes) do + { + bulk_upload:, + field_1: "1", + field_4: scheme.old_visible_id, + field_7: "123", + field_96: now.day.to_s, + field_97: now.month.to_s, + field_98: now.strftime("%g"), + field_108: "EC1N", + field_109: "2TD", + field_111: owning_org.old_visible_id, + field_113: managing_org.old_visible_id, + field_130: "1", + field_134: "2", + field_102: "2", + field_103: "1", + field_104: "1", + field_101: "1", + field_133: "2", + field_8: "1", + field_9: "2", + field_132: "1", + + field_12: "42", + field_13: "41", + field_14: "20", + field_15: "18", + field_16: "16", + field_17: "14", + field_18: "12", + field_19: "20", + + field_20: "F", + field_21: "M", + field_22: "F", + field_23: "M", + field_24: "F", + field_25: "M", + field_26: "F", + field_27: "M", + + field_43: "17", + field_44: "18", + + field_28: "P", + field_29: "C", + field_30: "X", + field_31: "R", + field_32: "C", + field_33: "C", + field_34: "X", + + field_35: "1", + field_36: "2", + field_37: "6", + field_38: "7", + field_39: "8", + field_40: "9", + field_41: "0", + field_42: "10", + + field_45: "1", + field_114: "4", + field_46: "1", + + field_47: "1", + + field_118: "2", + + field_66: "5", + field_67: "2", + field_52: "31", + field_61: "3", + field_68: "12", + + field_65: "1", + field_63: "EC1N", + field_64: "2TD", + + field_69: "1", + field_70: "1", + field_71: "", + field_72: "1", + field_73: "", + field_74: "", + + field_75: "1", + field_76: "2", + field_77: "2", + + field_78: "2", + + field_51: "1", + field_50: "2000", + field_116: "2", + field_48: "1", + field_49: "1", + + field_79: "4", + field_80: "1234.56", + field_87: "1", + field_88: "234.56", + + field_106: "15", + field_99: "0", + field_89: now.day.to_s, + field_90: now.month.to_s, + field_91: now.strftime("%g"), + } + end + before do create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org) end @@ -83,117 +195,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do end context "when valid row" do - let(:attributes) do - { - bulk_upload:, - field_1: "1", - field_4: scheme.old_visible_id, - field_7: "123", - field_96: now.day.to_s, - field_97: now.month.to_s, - field_98: now.strftime("%g"), - field_108: "EC1N", - field_109: "2TD", - field_111: owning_org.old_visible_id, - field_113: managing_org.old_visible_id, - field_130: "1", - field_134: "2", - field_102: "2", - field_103: "1", - field_104: "1", - field_101: "1", - field_133: "2", - field_8: "1", - field_9: "2", - field_132: "1", - - field_12: "42", - field_13: "41", - field_14: "20", - field_15: "18", - field_16: "16", - field_17: "14", - field_18: "12", - field_19: "20", - - field_20: "F", - field_21: "M", - field_22: "F", - field_23: "M", - field_24: "F", - field_25: "M", - field_26: "F", - field_27: "M", - - field_43: "17", - field_44: "18", - - field_28: "P", - field_29: "C", - field_30: "X", - field_31: "R", - field_32: "C", - field_33: "C", - field_34: "X", - - field_35: "1", - field_36: "2", - field_37: "6", - field_38: "7", - field_39: "8", - field_40: "9", - field_41: "0", - field_42: "10", - - field_45: "1", - field_114: "4", - field_46: "1", - - field_47: "1", - - field_118: "2", - - field_66: "5", - field_67: "2", - field_52: "31", - field_61: "3", - field_68: "12", - - field_65: "1", - field_63: "EC1N", - field_64: "2TD", - - field_69: "1", - field_70: "1", - field_71: "", - field_72: "1", - field_73: "", - field_74: "", - - field_75: "1", - field_76: "2", - field_77: "2", - - field_78: "2", - - field_51: "1", - field_50: "2000", - field_116: "2", - field_48: "1", - field_49: "1", - - field_79: "4", - field_80: "1234.56", - field_87: "1", - field_88: "234.56", - - field_106: "15", - field_99: "0", - field_89: now.day.to_s, - field_90: now.month.to_s, - field_91: now.strftime("%g"), - } - end + let(:attributes) { valid_attributes } it "returns true" do expect(parser).to be_valid @@ -216,7 +218,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do it "has errors on setup fields" do errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute) - expect(errors).to eql(%i[field_1 field_129 field_130 field_98 field_97 field_96 field_111 field_113]) + expect(errors).to eql(%i[field_1 field_98 field_97 field_96 field_111 field_113]) end end @@ -420,6 +422,16 @@ RSpec.describe BulkUpload::Lettings::RowParser do end end + describe "#field_12" do + context "when set to a non-sensical value" do + let(:attributes) { valid_attributes.merge(field_12: "A", field_35: "1") } + + it "returns only one error" do + expect(parser.errors[:field_12].size).to be(1) + end + end + end + describe "#field_52" do # leaving reason context "when field_134 is 1 meaning it is a renewal" do context "when field_52 is 40" do @@ -529,7 +541,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do end describe "fields 96, 97, 98 => startdate" do - context "when any one of these fields is blank" do + context "when all of these fields are blank" do let(:attributes) { { bulk_upload:, field_1: "1", field_96: nil, field_97: nil, field_98: nil } } it "returns an error" do @@ -541,6 +553,18 @@ RSpec.describe BulkUpload::Lettings::RowParser do end end + context "when one of these fields is blank" do + let(:attributes) { { bulk_upload:, field_1: "1", field_96: "1", field_97: "1", field_98: nil } } + + it "returns an error only on blank field" do + parser.valid? + + expect(parser.errors[:field_96]).to be_blank + expect(parser.errors[:field_97]).to be_blank + expect(parser.errors[:field_98]).to be_present + end + end + context "when field 98 is 4 digits instead of 2" do let(:attributes) { { bulk_upload:, field_98: "2022" } } @@ -658,7 +682,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do describe "#field_134" do context "when an unpermitted value" do - let(:attributes) { { bulk_upload:, field_134: 3 } } + let(:attributes) { { bulk_upload:, field_134: "3" } } it "has errors on the field" do expect(parser.errors[:field_134]).to be_present @@ -724,6 +748,18 @@ RSpec.describe BulkUpload::Lettings::RowParser do expect(parser.log.public_send(age)).to be(50) end end + + context "when #{field} is a non-sensical value" do + let(:attributes) { { bulk_upload:, field.to_s => "A" } } + + it "sets ##{known} to 0" do + expect(parser.log.public_send(known)).to be(0) + end + + it "sets ##{age} to nil" do + expect(parser.log.public_send(age)).to be_nil + end + end end end diff --git a/spec/services/bulk_upload/lettings/validator_spec.rb b/spec/services/bulk_upload/lettings/validator_spec.rb index ca52a19fe..b9d351281 100644 --- a/spec/services/bulk_upload/lettings/validator_spec.rb +++ b/spec/services/bulk_upload/lettings/validator_spec.rb @@ -42,7 +42,7 @@ RSpec.describe BulkUpload::Lettings::Validator do it "create validation error with correct values" do validator.call - error = BulkUploadError.order(:row, :field).first + error = BulkUploadError.find_by(field: "field_11") expect(error.field).to eql("field_11") expect(error.error).to eql("You must only answer the length of the tenancy if it's fixed-term") @@ -260,11 +260,14 @@ RSpec.describe BulkUpload::Lettings::Validator do let(:log_5) { build(:lettings_log, renttype: 2, created_by: user, builtype: nil, startdate: Time.zone.local(2022, 5, 1)) } before do - file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_csv_row) - file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0).to_csv_row) - file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0).to_csv_row) - file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0).to_csv_row) - file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0).to_csv_row) + overrides = { age1: 50, age2: "R", age3: "R", age4: "4", age5: "R", age6: "R", age7: "R", age8: "R" } + + file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row) + file.close end diff --git a/spec/support/bulk_upload/log_to_csv.rb b/spec/support/bulk_upload/log_to_csv.rb index 628ee14b8..e15c9173b 100644 --- a/spec/support/bulk_upload/log_to_csv.rb +++ b/spec/support/bulk_upload/log_to_csv.rb @@ -22,14 +22,14 @@ class BulkUpload::LogToCsv log.tenancy, log.tenancyother, # 10 log.tenancylength, - log.age1, - log.age2, - log.age3, - log.age4, - log.age5, - log.age6, - log.age7, - log.age8, + log.age1 || overrides[:age1], + log.age2 || overrides[:age2], + log.age3 || overrides[:age3], + log.age4 || overrides[:age4], + log.age5 || overrides[:age5], + log.age6 || overrides[:age6], + log.age7 || overrides[:age7], + log.age8 || overrides[:age8], log.sex1, # 20 log.sex2, From 9c59043c649ef5a8f39c163ab817f83a3ae76ee4 Mon Sep 17 00:00:00 2001 From: natdeanlewissoftwire <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Wed, 8 Mar 2023 11:50:37 +0000 Subject: [PATCH 15/19] CLDC-1788 Add bulk upload guidance page (#1335) * feat: add new guidance page with corresponding template links for sales and lettings log * feat: add specification files and links for sales/lettings * refactor: tidying * feat: change pseudo csv to xlsx to reflect reality * feat: add tests for guidance page routing and content * feat: add tests for lettings too --- .../bulk_upload_lettings_logs_controller.rb | 5 ++ .../bulk_upload_sales_logs_controller.rb | 5 ++ .../bulk_upload_lettings/prepare_your_file.rb | 4 ++ .../bulk_upload_sales/prepare_your_file.rb | 4 ++ .../forms/prepare_your_file.html.erb | 2 +- .../forms/prepare_your_file.html.erb | 2 +- .../bulk_upload_shared/guidance.html.erb | 52 ++++++++++++++++++ config/routes.rb | 2 + ...upload-lettings-specification-2022-23.xlsx | Bin 0 -> 52988 bytes ...lk-upload-sales-specification-2022-23.xlsx | Bin 0 -> 42425 bytes ...lk_upload_lettings_logs_controller_spec.rb | 24 ++++++++ .../bulk_upload_sales_logs_controller_spec.rb | 24 ++++++++ 12 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 app/views/bulk_upload_shared/guidance.html.erb create mode 100644 public/files/bulk-upload-lettings-specification-2022-23.xlsx create mode 100644 public/files/bulk-upload-sales-specification-2022-23.xlsx diff --git a/app/controllers/bulk_upload_lettings_logs_controller.rb b/app/controllers/bulk_upload_lettings_logs_controller.rb index 391651cde..9b37cd2b5 100644 --- a/app/controllers/bulk_upload_lettings_logs_controller.rb +++ b/app/controllers/bulk_upload_lettings_logs_controller.rb @@ -21,6 +21,11 @@ class BulkUploadLettingsLogsController < ApplicationController end end + def guidance + @form = Forms::BulkUploadLettings::PrepareYourFile.new + render "bulk_upload_shared/guidance" + end + private def current_year diff --git a/app/controllers/bulk_upload_sales_logs_controller.rb b/app/controllers/bulk_upload_sales_logs_controller.rb index 5f9b8d9a6..2b3432399 100644 --- a/app/controllers/bulk_upload_sales_logs_controller.rb +++ b/app/controllers/bulk_upload_sales_logs_controller.rb @@ -21,6 +21,11 @@ class BulkUploadSalesLogsController < ApplicationController end end + def guidance + @form = Forms::BulkUploadSales::PrepareYourFile.new + render "bulk_upload_shared/guidance" + end + private def current_year diff --git a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb index 5bfc0a2aa..bfaa622b2 100644 --- a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb +++ b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb @@ -29,6 +29,10 @@ module Forms "/files/bulk-upload-lettings-template-v1.xlsx" end + def specification_path + "/files/bulk-upload-lettings-specification-2022-23.xlsx" + end + def year_combo "#{year}/#{year + 1 - 2000}" end diff --git a/app/models/forms/bulk_upload_sales/prepare_your_file.rb b/app/models/forms/bulk_upload_sales/prepare_your_file.rb index 5c7e775ef..52d70a85c 100644 --- a/app/models/forms/bulk_upload_sales/prepare_your_file.rb +++ b/app/models/forms/bulk_upload_sales/prepare_your_file.rb @@ -27,6 +27,10 @@ module Forms "/files/bulk-upload-sales-template-v1.xlsx" end + def specification_path + "/files/bulk-upload-sales-specification-2022-23.xlsx" + end + def year_combo "#{year}/#{year + 1 - 2000}" end diff --git a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb index 199d87e5f..e48a190f9 100644 --- a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb +++ b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb @@ -13,7 +13,7 @@

Create your file

  • Download the <%= govuk_link_to "bulk lettings template", @form.template_path %>
  • -
  • Export the data from your housing management system, matching the template
  • +
  • Export the data from your housing management system, matching the template. <%= govuk_link_to "Find out more about exporting your data", guidance_bulk_upload_lettings_logs_path %>
  • If you cannot export it in this format, you may have to input it manually
  • You can not have a file with both general needs logs and supported housing logs. These must be in separate files
diff --git a/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb b/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb index 0157b66eb..d2a6bd70c 100644 --- a/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb +++ b/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb @@ -13,7 +13,7 @@

Create your file

  • Download the <%= govuk_link_to "bulk sales template", @form.template_path %>
  • -
  • Export the data from your housing management system, matching the template
  • +
  • Export the data from your housing management system, matching the template. <%= govuk_link_to "Find out more about exporting your data", guidance_bulk_upload_sales_logs_path %>
  • If you cannot export it in this format, you may have to input it manually
diff --git a/app/views/bulk_upload_shared/guidance.html.erb b/app/views/bulk_upload_shared/guidance.html.erb new file mode 100644 index 000000000..0c1f67bcc --- /dev/null +++ b/app/views/bulk_upload_shared/guidance.html.erb @@ -0,0 +1,52 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: :back %> +<% end %> + +
+
+ +

How to upload logs in bulk

+ +
+

Uploading sales and lettings logs

+

You can upload one sales or lettings log at a time, or many at once (known as ‘bulk upload’) with a comma-separated values (CSV) spreadsheet file.

+

Bulk upload may be easier if your organisation deals with many logs, or if you can export CSV data from your Housing Management System (HMS). If your organisation only deals with a small amount of logs, or you cannot export CSV data, it’s probably easier to enter logs individually.

+ <%= govuk_warning_text text: "You cannot upload lettings and sales logs with the same template - you must export each data type separately, then upload them" %> +
+ +
+

Creating your CSV files

+

To bulk upload successfully, all spreadsheets must be in the correct CSV format.

+

In most programs, you must resave files as CSV - it’s not usually the default setting. CSV files are also unformatted, so any formatting added before saving (for example colours) will automatically disappear.

+ <%= govuk_details(summary_text: "More about CSV") do %> +

A CSV file is a basic spreadsheet with data values in plain text, and columns separated by commas. Each data row is a new text line.

+

CSV data is easier to process than more common advanced spreadsheet formats, for example Excel. It means CSV is well suited to upload large, or multiple data sets.

+ <% end %> +
+ +
+

Exporting CSV data

+

Export CSV data directly from your current systems, or export then adjust it to CSV.

+

You can then upload it via a button at the top of the lettings and sales logs pages.

+ <%= govuk_details(summary_text: "My organisation has a CMS") do %> +

Some HMS providers sell an add-on "eCORE" module, which exports CSV data for you.

+

It can take HMS providers a while to update these per new collection year, so you may have to wait for updates to export, or adjust your data manually post-export.

+ <% end %> + <%= govuk_details(summary_text: "My organisation does not have a CMS") do %> +

Your organisation’s IT team may be able to export CSV data for you - <%= govuk_link_to "find out more about data specification", @form.specification_path %>. This document outlines:

+
    +
  • required fields
  • +
  • each field's valid response
  • +
  • if/when certain fields can be left blank
  • +
+

Fields can appear in any order, as long as you include the <%= govuk_link_to "template document", @form.template_path %> headers, to easily identify what each column represents. You can rearrange data columns to match your system exports, copy-pasting multiple columns at once. For data stored in multiple systems, you can copy-paste all columns for one system next to each other, repeating this for subsequent system exports.

+ <% end %> +
+ +
+

Getting help

+

There is no step-by-step bulk upload guide like there is with single log upload. However, you can download <%= govuk_link_to "our template", @form.template_path %>, which you can copy-paste data into from your systems column-by-column. You can also view a post-upload report showing any data errors, and our <%= govuk_link_to "data specification", @form.specification_path %> can help fix these.

+

If you still need support mapping data in the way we need, DLUHC’s helpdesk can help. If your data is across multiple systems, or is hard to export as a single file in the correct format, you could try different exports, or copy-pasting data by hand.

+
+
+
diff --git a/config/routes.rb b/config/routes.rb index 74b613989..02a9cb707 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -131,6 +131,7 @@ Rails.application.routes.draw do resources :bulk_upload_lettings_logs, path: "bulk-upload-logs", only: %i[show update] do collection do get :start + get "guidance", to: "bulk_upload_lettings_logs#guidance" end end @@ -165,6 +166,7 @@ Rails.application.routes.draw do resources :bulk_upload_sales_logs, path: "bulk-upload-logs" do collection do get :start + get "guidance", to: "bulk_upload_sales_logs#guidance" end end diff --git a/public/files/bulk-upload-lettings-specification-2022-23.xlsx b/public/files/bulk-upload-lettings-specification-2022-23.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..15cec38b607107a607e3318913f4a1982480a4e4 GIT binary patch literal 52988 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%0AQepiZ;$>Aax2@az zR96PsX{;XAW?);V%M-b1Lt#)jQ@!SKW9q*O@sgB0O0i(4K#n!z~%=H`pEz^fT+h5v>edtSJ+ed z2MsuX(BRT$5Guf4NZ>$#sXU;!KcYRC_=phxuDpARuo*ApokSe|+$dV4MGh_-i4) z1drK<@BjhP0~9alf8dvSF0Q7P{ z0e_C!=7FO zzKj+61=tt$6Yxc(&|f2mUV^?9|NR0LPygS5{*(f~1br!)`vq#6@e}k90o}{oe@f4O zBIWGZz z7UQodf9hL)M}f(EiSpA+d0C76eB0bG+~O>_WA_W&e-Szn8iH91;0Fw{F94 zxqpq1yaf2O+xa^HZ}aaq_**CRCDxyvm*24jTmO#rt1I&o>rW5(cPw#$NA#=4{VLZl ofA}TVpKjFeShAge$NJ?|eHH-&Xpn(`umOJofL}b7?jKkG2RO6a!2kdN literal 0 HcmV?d00001 diff --git a/public/files/bulk-upload-sales-specification-2022-23.xlsx b/public/files/bulk-upload-sales-specification-2022-23.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b1e825fbb9e7fc271b210f936c785df73bb475dc GIT binary patch literal 42425 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

iPCwPbfg^Oh8^cY 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 literal 0 HcmV?d00001 diff --git a/spec/requests/bulk_upload_lettings_logs_controller_spec.rb b/spec/requests/bulk_upload_lettings_logs_controller_spec.rb index fc27ed0d1..b915b571c 100644 --- a/spec/requests/bulk_upload_lettings_logs_controller_spec.rb +++ b/spec/requests/bulk_upload_lettings_logs_controller_spec.rb @@ -31,4 +31,28 @@ RSpec.describe BulkUploadLettingsLogsController, type: :request do end end end + + describe "GET /lettings-logs/bulk-upload-logs/guidance" do + context "when not in crossover period" do + let(:expected_year) { FormHandler.instance.forms["current_lettings"].start_date.year } + + it "shows guidance page with correct title" do + Timecop.freeze(2022, 1, 1) do + get "/lettings-logs/bulk-upload-logs/guidance", params: {} + + expect(response.body).to include("How to upload logs in bulk") + end + end + end + + context "when in crossover period" do + it "shows guidance page with correct title" do + Timecop.freeze(2023, 6, 1) do + get "/lettings-logs/bulk-upload-logs/guidance", params: {} + + expect(response.body).to include("How to upload logs in bulk") + end + end + end + end end diff --git a/spec/requests/bulk_upload_sales_logs_controller_spec.rb b/spec/requests/bulk_upload_sales_logs_controller_spec.rb index 348179dd2..3e2aa5910 100644 --- a/spec/requests/bulk_upload_sales_logs_controller_spec.rb +++ b/spec/requests/bulk_upload_sales_logs_controller_spec.rb @@ -31,4 +31,28 @@ RSpec.describe BulkUploadSalesLogsController, type: :request do end end end + + describe "GET /sales-logs/bulk-upload-logs/guidance" do + context "when not in crossover period" do + let(:expected_year) { FormHandler.instance.forms["current_sales"].start_date.year } + + it "shows guidance page with correct title" do + Timecop.freeze(2022, 1, 1) do + get "/sales-logs/bulk-upload-logs/guidance", params: {} + + expect(response.body).to include("How to upload logs in bulk") + end + end + end + + context "when in crossover period" do + it "shows guidance page with correct title" do + Timecop.freeze(2023, 6, 1) do + get "/sales-logs/bulk-upload-logs/guidance", params: {} + + expect(response.body).to include("How to upload logs in bulk") + end + end + end + end end From 7d4dcb905e9e1f43d9ce9e5fcc29893504c63c91 Mon Sep 17 00:00:00 2001 From: Arthur Campbell <51094020+arfacamble@users.noreply.github.com> Date: Wed, 8 Mar 2023 12:16:51 +0000 Subject: [PATCH 16/19] CLDC-1895 pluralise various copy when joint purchase (#1326) * update copy to handle pluralisation in the case of joint purchase for previous ownership, with associated testing changes * update copy to handle pluralisation in the case of joint purchase for staircasing, with associated testing changes * update copy to handle pluralisation in the case of joint purchase for buyer previous, with associated testing changes * make minor linting changes and update the evil test about number of pages * potential variation on how to achieve pluralisation * use built in rails methods of pluralising copy --- .../form/sales/pages/about_staircase.rb | 9 +++--- app/models/form/sales/pages/buyer_previous.rb | 9 +++--- .../form/sales/pages/previous_ownership.rb | 9 +++--- .../form/sales/questions/buyer_previous.rb | 8 ++--- app/models/form/sales/questions/prevown.rb | 8 ++--- .../form/sales/questions/staircase_owned.rb | 8 ++--- .../income_benefits_and_savings.rb | 3 +- .../subsections/shared_ownership_scheme.rb | 6 ++-- config/locales/en.yml | 20 +++++++++++++ .../form/sales/pages/about_staircase_spec.rb | 29 ++++++++++++++----- .../form/sales/pages/buyer_previous_spec.rb | 21 ++++++++++++-- .../sales/pages/previous_ownership_spec.rb | 19 ++++++++++-- .../sales/questions/buyer_previous_spec.rb | 25 ++++++++++++---- .../form/sales/questions/prevown_spec.rb | 25 ++++++++++++---- .../sales/questions/staircase_owned_spec.rb | 25 ++++++++++++---- .../income_benefits_and_savings_spec.rb | 6 ++-- .../shared_ownership_scheme_spec.rb | 6 ++-- 17 files changed, 177 insertions(+), 59 deletions(-) diff --git a/app/models/form/sales/pages/about_staircase.rb b/app/models/form/sales/pages/about_staircase.rb index 2d1a17db4..4d5882916 100644 --- a/app/models/form/sales/pages/about_staircase.rb +++ b/app/models/form/sales/pages/about_staircase.rb @@ -1,17 +1,18 @@ class Form::Sales::Pages::AboutStaircase < ::Form::Page - def initialize(id, hsh, subsection) - super - @id = "about_staircasing" + def initialize(id, hsh, subsection, joint_purchase:) + super(id, hsh, subsection) + @joint_purchase = joint_purchase @header = "About the staircasing transaction" @depends_on = [{ "staircase" => 1, + "joint_purchase?" => joint_purchase, }] end def questions @questions ||= [ Form::Sales::Questions::StaircaseBought.new(nil, nil, self), - Form::Sales::Questions::StaircaseOwned.new(nil, nil, self), + Form::Sales::Questions::StaircaseOwned.new(nil, nil, self, joint_purchase: @joint_purchase), staircase_sale_question, ].compact end diff --git a/app/models/form/sales/pages/buyer_previous.rb b/app/models/form/sales/pages/buyer_previous.rb index 361b3ba47..f37c6a847 100644 --- a/app/models/form/sales/pages/buyer_previous.rb +++ b/app/models/form/sales/pages/buyer_previous.rb @@ -1,12 +1,13 @@ class Form::Sales::Pages::BuyerPrevious < ::Form::Page - def initialize(id, hsh, subsection) - super - @id = "buyer_previous" + def initialize(id, hsh, subsection, joint_purchase:) + super(id, hsh, subsection) + @joint_purchase = joint_purchase + @depends_on = [{ "joint_purchase?" => joint_purchase }] end def questions @questions ||= [ - Form::Sales::Questions::BuyerPrevious.new(nil, nil, self), + Form::Sales::Questions::BuyerPrevious.new(nil, nil, self, joint_purchase: @joint_purchase), ] end end diff --git a/app/models/form/sales/pages/previous_ownership.rb b/app/models/form/sales/pages/previous_ownership.rb index 88b438e07..50711baf7 100644 --- a/app/models/form/sales/pages/previous_ownership.rb +++ b/app/models/form/sales/pages/previous_ownership.rb @@ -1,12 +1,13 @@ class Form::Sales::Pages::PreviousOwnership < ::Form::Page - def initialize(id, hsh, subsection) - super - @id = "previous_ownership" + def initialize(id, hsh, subsection, joint_purchase:) + super(id, hsh, subsection) + @joint_purchase = joint_purchase + @depends_on = [{ "joint_purchase?" => @joint_purchase }] end def questions @questions ||= [ - Form::Sales::Questions::Prevown.new(nil, nil, self), + Form::Sales::Questions::Prevown.new(nil, nil, self, joint_purchase: @joint_purchase), ] end end diff --git a/app/models/form/sales/questions/buyer_previous.rb b/app/models/form/sales/questions/buyer_previous.rb index 18ac019cb..15fe330f0 100644 --- a/app/models/form/sales/questions/buyer_previous.rb +++ b/app/models/form/sales/questions/buyer_previous.rb @@ -1,9 +1,9 @@ class Form::Sales::Questions::BuyerPrevious < ::Form::Question - def initialize(id, hsh, page) - super + def initialize(id, hsh, page, joint_purchase:) + super(id, hsh, page) @id = "soctenant" - @check_answer_label = "Buyer was a registered provider, housing association or local authority tenant immediately before this sale?" - @header = "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?" + @check_answer_label = I18n.t("check_answer_labels.soctenant", count: joint_purchase ? 2 : 1) + @header = I18n.t("questions.soctenant", count: joint_purchase ? 2 : 1) @type = "radio" @answer_options = ANSWER_OPTIONS end diff --git a/app/models/form/sales/questions/prevown.rb b/app/models/form/sales/questions/prevown.rb index a9a3b086b..571e1af8f 100644 --- a/app/models/form/sales/questions/prevown.rb +++ b/app/models/form/sales/questions/prevown.rb @@ -1,9 +1,9 @@ class Form::Sales::Questions::Prevown < ::Form::Question - def initialize(id, hsh, page) - super + def initialize(id, hsh, page, joint_purchase:) + super(id, hsh, page) @id = "prevown" - @check_answer_label = "Buyers previously owned a property" - @header = "Has the buyer previously owned a property?" + @check_answer_label = I18n.t("check_answer_labels.prevown", count: joint_purchase ? 2 : 1) + @header = I18n.t("questions.prevown", count: joint_purchase ? 2 : 1) @type = "radio" @answer_options = ANSWER_OPTIONS end diff --git a/app/models/form/sales/questions/staircase_owned.rb b/app/models/form/sales/questions/staircase_owned.rb index 7e6dab5d6..a3596554e 100644 --- a/app/models/form/sales/questions/staircase_owned.rb +++ b/app/models/form/sales/questions/staircase_owned.rb @@ -1,9 +1,9 @@ class Form::Sales::Questions::StaircaseOwned < ::Form::Question - def initialize(id, hsh, page) - super + def initialize(id, hsh, page, joint_purchase:) + super(id, hsh, page) @id = "stairowned" - @check_answer_label = "Percentage the buyer now owns in total" - @header = "What percentage of the property does the buyer now own in total?" + @check_answer_label = I18n.t("check_answer_labels.stairowned", count: joint_purchase ? 2 : 1) + @header = I18n.t("questions.stairowned", count: joint_purchase ? 2 : 1) @type = "numeric" @width = 5 @min = 0 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 9ff584d47..502b87fc9 100644 --- a/app/models/form/sales/subsections/income_benefits_and_savings.rb +++ b/app/models/form/sales/subsections/income_benefits_and_savings.rb @@ -23,7 +23,8 @@ class Form::Sales::Subsections::IncomeBenefitsAndSavings < ::Form::Subsection Form::Sales::Pages::Savings.new(nil, nil, self), Form::Sales::Pages::SavingsValueCheck.new("savings_value_check", nil, self), Form::Sales::Pages::DepositValueCheck.new("savings_deposit_value_check", nil, self), - Form::Sales::Pages::PreviousOwnership.new(nil, nil, self), + Form::Sales::Pages::PreviousOwnership.new("previous_ownership_joint_purchase", nil, self, joint_purchase: true), + Form::Sales::Pages::PreviousOwnership.new("previous_ownership_not_joint_purchase", nil, self, joint_purchase: false), previous_shared_page, ].compact end diff --git a/app/models/form/sales/subsections/shared_ownership_scheme.rb b/app/models/form/sales/subsections/shared_ownership_scheme.rb index 1f0cd6450..1c30f3b6f 100644 --- a/app/models/form/sales/subsections/shared_ownership_scheme.rb +++ b/app/models/form/sales/subsections/shared_ownership_scheme.rb @@ -10,14 +10,16 @@ class Form::Sales::Subsections::SharedOwnershipScheme < ::Form::Subsection @pages ||= [ Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership", nil, self), Form::Sales::Pages::Staircase.new(nil, nil, self), - Form::Sales::Pages::AboutStaircase.new(nil, nil, self), + Form::Sales::Pages::AboutStaircase.new("about_staircasing_joint_purchase", nil, self, joint_purchase: true), + Form::Sales::Pages::AboutStaircase.new("about_staircasing_not_joint_purchase", nil, self, joint_purchase: false), Form::Sales::Pages::StaircaseBoughtValueCheck.new(nil, nil, self), Form::Sales::Pages::Resale.new(nil, nil, self), Form::Sales::Pages::ExchangeDate.new(nil, nil, self), Form::Sales::Pages::HandoverDate.new(nil, nil, self), Form::Sales::Pages::HandoverDateCheck.new(nil, nil, self), Form::Sales::Pages::LaNominations.new(nil, nil, self), - Form::Sales::Pages::BuyerPrevious.new(nil, nil, self), + Form::Sales::Pages::BuyerPrevious.new("buyer_previous_joint_purchase", nil, self, joint_purchase: true), + Form::Sales::Pages::BuyerPrevious.new("buyer_previous_not_joint_purchase", nil, self, joint_purchase: false), Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self), Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self), Form::Sales::Pages::PreviousTenure.new(nil, nil, self), diff --git a/config/locales/en.yml b/config/locales/en.yml index 8665c022a..c3762e259 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -538,6 +538,15 @@ en: W: "Suitable for someone who uses a wheelchair and offers the full use of all rooms and facilities." A: "Fitted with stairlifts, ramps, level access showers or grab rails." N: "Not designed to wheelchair-user standards or fitted with any equipment or adaptations." + soctenant: + one: "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?" + other: "Were any of the buyers private registered providers, housing association or local authority tenants immediately before this sale?" + prevown: + one: "Has the buyer previously owned a property?" + other: "Have any of the buyers previously owned a property?" + stairowned: + one: "What percentage of the property does the buyer now own in total?" + other: "What percentage of the property do the buyers now own in total?" hints: location: @@ -551,6 +560,17 @@ en: bulk_upload: needstype: "General needs housing includes both self-contained and shared housing without support or specific adaptations. Supported housing can include direct access hostels, group homes, residential care and nursing homes." + check_answer_labels: + soctenant: + one: "Buyer was a registered provider, housing association or local authority tenant immediately before this sale?" + other: "Any buyers were registered providers, housing association or local authority tenants immediately before this sale?" + prevown: + one: "Buyer previously owned a property" + other: "Buyers previously owned a property" + stairowned: + one: "Percentage the buyer now owns in total" + other: "Percentage the buyers now own in total" + warnings: location: deactivate: diff --git a/spec/models/form/sales/pages/about_staircase_spec.rb b/spec/models/form/sales/pages/about_staircase_spec.rb index 3828f52e4..48aa82acf 100644 --- a/spec/models/form/sales/pages/about_staircase_spec.rb +++ b/spec/models/form/sales/pages/about_staircase_spec.rb @@ -1,11 +1,12 @@ require "rails_helper" RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do - subject(:page) { described_class.new(page_id, page_definition, subsection) } + subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase:) } - let(:page_id) { nil } + let(:page_id) { "an_id" } let(:page_definition) { nil } let(:subsection) { instance_double(Form::Subsection) } + let(:joint_purchase) { false } it "has correct subsection" do expect(page.subsection).to eq(subsection) @@ -32,7 +33,7 @@ RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do end it "has the correct id" do - expect(page.id).to eq("about_staircasing") + expect(page.id).to eq("an_id") end it "has the correct header" do @@ -43,9 +44,23 @@ RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do expect(page.description).to be_nil end - it "has correct depends_on" do - expect(page.depends_on).to eq([{ - "staircase" => 1, - }]) + context "when not a joint purchase" do + it "has correct depends_on" do + expect(page.depends_on).to eq([{ + "staircase" => 1, + "joint_purchase?" => false, + }]) + end + end + + context "when a joint purchase" do + let(:joint_purchase) { true } + + it "has correct depends_on" do + expect(page.depends_on).to eq([{ + "staircase" => 1, + "joint_purchase?" => true, + }]) + end end end diff --git a/spec/models/form/sales/pages/buyer_previous_spec.rb b/spec/models/form/sales/pages/buyer_previous_spec.rb index a3355c137..e6dda073e 100644 --- a/spec/models/form/sales/pages/buyer_previous_spec.rb +++ b/spec/models/form/sales/pages/buyer_previous_spec.rb @@ -1,11 +1,12 @@ require "rails_helper" RSpec.describe Form::Sales::Pages::BuyerPrevious, type: :model do - subject(:page) { described_class.new(page_id, page_definition, subsection) } + subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase:) } - let(:page_id) { nil } + let(:page_id) { "example" } let(:page_definition) { nil } let(:subsection) { instance_double(Form::Subsection) } + let(:joint_purchase) { false } it "has correct subsection" do expect(page.subsection).to eq(subsection) @@ -16,7 +17,7 @@ RSpec.describe Form::Sales::Pages::BuyerPrevious, type: :model do end it "has the correct id" do - expect(page.id).to eq("buyer_previous") + expect(page.id).to eq("example") end it "has the correct header" do @@ -26,4 +27,18 @@ RSpec.describe Form::Sales::Pages::BuyerPrevious, type: :model do it "has the correct description" do expect(page.description).to be_nil end + + context "when sales is a joint purchase" do + let(:joint_purchase) { true } + + it "has the correct depends on" do + expect(page.depends_on).to eq([{ "joint_purchase?" => true }]) + end + end + + context "when sales is not a joint purchase" do + it "has the correct depends on" do + expect(page.depends_on).to eq([{ "joint_purchase?" => false }]) + end + end end diff --git a/spec/models/form/sales/pages/previous_ownership_spec.rb b/spec/models/form/sales/pages/previous_ownership_spec.rb index 7fe0d2795..69c93fefa 100644 --- a/spec/models/form/sales/pages/previous_ownership_spec.rb +++ b/spec/models/form/sales/pages/previous_ownership_spec.rb @@ -1,11 +1,12 @@ require "rails_helper" RSpec.describe Form::Sales::Pages::PreviousOwnership, type: :model do - subject(:page) { described_class.new(page_id, page_definition, subsection) } + subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase:) } - let(:page_id) { nil } + let(:page_id) { "example_id" } let(:page_definition) { nil } let(:subsection) { instance_double(Form::Subsection) } + let(:joint_purchase) { true } it "has correct subsection" do expect(page.subsection).to eq(subsection) @@ -16,7 +17,7 @@ RSpec.describe Form::Sales::Pages::PreviousOwnership, type: :model do end it "has the correct id" do - expect(page.id).to eq("previous_ownership") + expect(page.id).to eq("example_id") end it "has the correct header" do @@ -26,4 +27,16 @@ RSpec.describe Form::Sales::Pages::PreviousOwnership, type: :model do it "has the correct description" do expect(page.description).to be_nil end + + it "when sale is a joint purchase has the correct depends on" do + expect(page.depends_on).to eq([{ "joint_purchase?" => true }]) + end + + context "when sale is not a joint purchase" do + let(:joint_purchase) { false } + + it "has the correct depends on" do + expect(page.depends_on).to eq([{ "joint_purchase?" => false }]) + end + end end diff --git a/spec/models/form/sales/questions/buyer_previous_spec.rb b/spec/models/form/sales/questions/buyer_previous_spec.rb index 07d3685f8..a1348f17b 100644 --- a/spec/models/form/sales/questions/buyer_previous_spec.rb +++ b/spec/models/form/sales/questions/buyer_previous_spec.rb @@ -1,11 +1,12 @@ require "rails_helper" RSpec.describe Form::Sales::Questions::BuyerPrevious, type: :model do - subject(:question) { described_class.new(question_id, question_definition, page) } + subject(:question) { described_class.new(question_id, question_definition, page, joint_purchase:) } let(:question_id) { nil } let(:question_definition) { nil } let(:page) { instance_double(Form::Page) } + let(:joint_purchase) { true } it "has correct page" do expect(question.page).to eq(page) @@ -15,12 +16,26 @@ RSpec.describe Form::Sales::Questions::BuyerPrevious, type: :model do expect(question.id).to eq("soctenant") end - it "has the correct header" do - expect(question.header).to eq("Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?") + context "when a joint purchase" do + it "has the correct header" do + expect(question.header).to eq("Were any of the buyers private registered providers, housing association or local authority tenants immediately before this sale?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Any buyers were registered providers, housing association or local authority tenants immediately before this sale?") + end end - it "has the correct check_answer_label" do - expect(question.check_answer_label).to eq("Buyer was a registered provider, housing association or local authority tenant immediately before this sale?") + context "when not a joint purchase" do + let(:joint_purchase) { false } + + it "has the correct header" do + expect(question.header).to eq("Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer was a registered provider, housing association or local authority tenant immediately before this sale?") + end end it "has the correct type" do diff --git a/spec/models/form/sales/questions/prevown_spec.rb b/spec/models/form/sales/questions/prevown_spec.rb index dda2b7c70..d7b101c99 100644 --- a/spec/models/form/sales/questions/prevown_spec.rb +++ b/spec/models/form/sales/questions/prevown_spec.rb @@ -1,11 +1,12 @@ require "rails_helper" RSpec.describe Form::Sales::Questions::Prevown, type: :model do - subject(:question) { described_class.new(question_id, question_definition, page) } + subject(:question) { described_class.new(question_id, question_definition, page, joint_purchase:) } let(:question_id) { nil } let(:question_definition) { nil } let(:page) { instance_double(Form::Page) } + let(:joint_purchase) { false } it "has correct page" do expect(question.page).to eq(page) @@ -15,12 +16,26 @@ RSpec.describe Form::Sales::Questions::Prevown, type: :model do expect(question.id).to eq("prevown") end - it "has the correct header" do - expect(question.header).to eq("Has the buyer previously owned a property?") + context "when sale is not a joint purchase" do + it "has the correct header" do + expect(question.header).to eq("Has the buyer previously owned a property?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer previously owned a property") + end end - it "has the correct check_answer_label" do - expect(question.check_answer_label).to eq("Buyers previously owned a property") + context "when sale is a joint purchase" do + let(:joint_purchase) { true } + + it "has the correct header" do + expect(question.header).to eq("Have any of the buyers previously owned a property?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyers previously owned a property") + end end it "has the correct type" do diff --git a/spec/models/form/sales/questions/staircase_owned_spec.rb b/spec/models/form/sales/questions/staircase_owned_spec.rb index cbd577784..8e78d540a 100644 --- a/spec/models/form/sales/questions/staircase_owned_spec.rb +++ b/spec/models/form/sales/questions/staircase_owned_spec.rb @@ -1,11 +1,12 @@ require "rails_helper" RSpec.describe Form::Sales::Questions::StaircaseOwned, type: :model do - subject(:question) { described_class.new(question_id, question_definition, page) } + subject(:question) { described_class.new(question_id, question_definition, page, joint_purchase:) } let(:question_id) { nil } let(:question_definition) { nil } let(:page) { instance_double(Form::Page) } + let(:joint_purchase) { false } it "has correct page" do expect(question.page).to eq(page) @@ -15,12 +16,26 @@ RSpec.describe Form::Sales::Questions::StaircaseOwned, type: :model do expect(question.id).to eq("stairowned") end - it "has the correct header" do - expect(question.header).to eq("What percentage of the property does the buyer now own in total?") + context "when a joint purchase" do + let(:joint_purchase) { true } + + it "has the correct header" do + expect(question.header).to eq("What percentage of the property do the buyers now own in total?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Percentage the buyers now own in total") + end end - it "has the correct check_answer_label" do - expect(question.check_answer_label).to eq("Percentage the buyer now owns in total") + context "when not a joint purchase" do + it "has the correct header" do + expect(question.header).to eq("What percentage of the property does the buyer now own in total?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Percentage the buyer now owns in total") + end end it "has the correct type" do 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 6c0997fd2..aef451878 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 @@ -35,7 +35,8 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model savings savings_value_check savings_deposit_value_check - previous_ownership + previous_ownership_joint_purchase + previous_ownership_not_joint_purchase ], ) end @@ -62,7 +63,8 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model savings savings_value_check savings_deposit_value_check - previous_ownership + previous_ownership_joint_purchase + previous_ownership_not_joint_purchase previous_shared ], ) diff --git a/spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb b/spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb index ee9f624aa..3c189f93f 100644 --- a/spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb +++ b/spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb @@ -16,14 +16,16 @@ RSpec.describe Form::Sales::Subsections::SharedOwnershipScheme, type: :model do %w[ living_before_purchase_shared_ownership staircasing - about_staircasing + about_staircasing_joint_purchase + about_staircasing_not_joint_purchase staircase_bought_value_check resale exchange_contracts handover_date handover_date_check la_nominations - buyer_previous + buyer_previous_joint_purchase + buyer_previous_not_joint_purchase previous_bedrooms previous_property_type shared_ownership_previous_tenure From 0216aae40f144081679b01bc9108597a7e6410fd Mon Sep 17 00:00:00 2001 From: Arthur Campbell <51094020+arfacamble@users.noreply.github.com> Date: Wed, 8 Mar 2023 12:17:51 +0000 Subject: [PATCH 17/19] when buyer is a company, do not route to household characteristics (#1336) --- .../subsections/household_characteristics.rb | 6 +++++- app/models/sales_log.rb | 4 ++++ spec/factories/sales_log.rb | 1 - .../household_characteristics_spec.rb | 18 +++++++++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/models/form/sales/subsections/household_characteristics.rb b/app/models/form/sales/subsections/household_characteristics.rb index 571968c8f..0277acffb 100644 --- a/app/models/form/sales/subsections/household_characteristics.rb +++ b/app/models/form/sales/subsections/household_characteristics.rb @@ -3,7 +3,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection super @id = "household_characteristics" @label = "Household characteristics" - @depends_on = [{ "setup_completed?" => true }] + @depends_on = [{ "setup_completed?" => true, "company_buyer?" => false }] end def pages @@ -92,4 +92,8 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Sales::Pages::Buyer2EthnicBackgroundWhite.new(nil, nil, self)] end end + + def displayed_in_tasklist?(log) + !log.company_buyer? + end end diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index 629215386..90f130c04 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -262,6 +262,10 @@ class SalesLog < Log ownershipsch == 1 end + def company_buyer? + companybuy == 1 + end + def buyers_age_for_old_persons_shared_ownership_invalid? return unless old_persons_shared_ownership? diff --git a/spec/factories/sales_log.rb b/spec/factories/sales_log.rb index f1da841a4..b2d6f37a5 100644 --- a/spec/factories/sales_log.rb +++ b/spec/factories/sales_log.rb @@ -14,7 +14,6 @@ FactoryBot.define do ownershipsch { 2 } type { 8 } saledate { Time.utc(2023, 2, 2, 10, 36, 49) } - companybuy { 1 } jointpur { 1 } beds { 2 } jointmore { 1 } diff --git a/spec/models/form/sales/subsections/household_characteristics_spec.rb b/spec/models/form/sales/subsections/household_characteristics_spec.rb index 4645b7231..5a025e8de 100644 --- a/spec/models/form/sales/subsections/household_characteristics_spec.rb +++ b/spec/models/form/sales/subsections/household_characteristics_spec.rb @@ -195,6 +195,22 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model end it "has correct depends on" do - expect(household_characteristics.depends_on).to eq([{ "setup_completed?" => true }]) + expect(household_characteristics.depends_on).to eq([{ "setup_completed?" => true, "company_buyer?" => false }]) + end + + context "when the sale is to a company buyer" do + let(:log) { FactoryBot.create(:sales_log, ownershipsch: 3, companybuy: 1) } + + it "is not displayed in tasklist" do + expect(household_characteristics.displayed_in_tasklist?(log)).to eq(false) + end + end + + context "when the sale is not to a company buyer" do + let(:log) { FactoryBot.create(:sales_log, ownershipsch: 3, companybuy: 2) } + + it "is displayed in tasklist" do + expect(household_characteristics.displayed_in_tasklist?(log)).to eq(true) + end end end From e0bde3a4b4af5c67e2596160694b8ecc65ecc1f5 Mon Sep 17 00:00:00 2001 From: Arthur Campbell <51094020+arfacamble@users.noreply.github.com> Date: Wed, 8 Mar 2023 13:10:12 +0000 Subject: [PATCH 18/19] CLDC-1817 age questions update for children (#1333) * amend copy of hint text * update test to reflect copy change * display hint text on age question if that person is a child * updates from code review * update tests to reflect changes in class interfaces for page and question --- app/models/form/lettings/pages/person_age.rb | 14 +++++--- app/models/form/lettings/questions/age.rb | 5 +-- .../lettings/questions/person_relationship.rb | 2 +- .../subsections/household_characteristics.rb | 7 ++++ app/models/log.rb | 6 ++++ .../form/lettings/pages/person_age_spec.rb | 35 ++++++++++++++----- .../form/lettings/questions/age_spec.rb | 19 +++++++--- .../questions/person_relationship_spec.rb | 2 +- .../household_characteristics_spec.rb | 21 +++++++---- 9 files changed, 83 insertions(+), 28 deletions(-) diff --git a/app/models/form/lettings/pages/person_age.rb b/app/models/form/lettings/pages/person_age.rb index 66f7d3d89..bbb5da01e 100644 --- a/app/models/form/lettings/pages/person_age.rb +++ b/app/models/form/lettings/pages/person_age.rb @@ -1,15 +1,21 @@ class Form::Lettings::Pages::PersonAge < ::Form::Page - def initialize(id, hsh, subsection, person_index:) + def initialize(id, hsh, subsection, person_index:, person_type: "non_child") super(id, hsh, subsection) - @id = "person_#{person_index}_age" - @depends_on = [{ "details_known_#{person_index}" => 0 }] + @id = "person_#{person_index}_age_#{person_type}" @person_index = person_index + @person_type = person_type + @depends_on = [ + { + "details_known_#{person_index}" => 0, + "person_#{person_index}_child_relation?" => (person_type == "child"), + }, + ] end def questions @questions ||= [ Form::Lettings::Questions::AgeKnown.new(nil, nil, self, person_index: @person_index), - Form::Lettings::Questions::Age.new(nil, nil, self, person_index: @person_index), + Form::Lettings::Questions::Age.new(nil, nil, self, person_index: @person_index, person_type: @person_type), ] end end diff --git a/app/models/form/lettings/questions/age.rb b/app/models/form/lettings/questions/age.rb index f160f1948..82975ebb7 100644 --- a/app/models/form/lettings/questions/age.rb +++ b/app/models/form/lettings/questions/age.rb @@ -1,5 +1,5 @@ class Form::Lettings::Questions::Age < ::Form::Question - def initialize(id, hsh, page, person_index:) + def initialize(id, hsh, page, person_index:, person_type:) super(id, hsh, page) @id = "age#{person_index}" @check_answer_label = "Person #{person_index}’s age" @@ -9,7 +9,8 @@ class Form::Lettings::Questions::Age < ::Form::Question @inferred_check_answers_value = [{ "condition" => { "age#{person_index}_known" => 1 }, "value" => "Not known" }] @check_answers_card_number = person_index @max = 120 - @min = 0 + @min = 1 @step = 1 + @hint_text = "For a child under 1, enter 1" if person_type == "child" end end diff --git a/app/models/form/lettings/questions/person_relationship.rb b/app/models/form/lettings/questions/person_relationship.rb index debb14a03..a346cf3cc 100644 --- a/app/models/form/lettings/questions/person_relationship.rb +++ b/app/models/form/lettings/questions/person_relationship.rb @@ -14,7 +14,7 @@ class Form::Lettings::Questions::PersonRelationship < ::Form::Question "P" => { "value" => "Partner" }, "C" => { "value" => "Child", - "hint" => "Must be eligible for child benefit, aged under 16 or under 20 if still in full-time education.", + "hint" => "Must be eligible for child benefit: under age 16 or under 20 if still in full-time education.", }, "X" => { "value" => "Other" }, "divider" => { "value" => true }, diff --git a/app/models/form/lettings/subsections/household_characteristics.rb b/app/models/form/lettings/subsections/household_characteristics.rb index 8a5bdf8c1..9fc6948bd 100644 --- a/app/models/form/lettings/subsections/household_characteristics.rb +++ b/app/models/form/lettings/subsections/household_characteristics.rb @@ -30,6 +30,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Lettings::Pages::LeadTenantOverRetirementValueCheck.new(nil, nil, self), Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 2), Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 2), + Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 2, person_type: "child"), Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 2), Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index: 2), @@ -44,6 +45,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 2), Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 3), Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 3), + Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 3, person_type: "child"), Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 3), Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index: 3), @@ -58,6 +60,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 3), Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 4), Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 4), + Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 4, person_type: "child"), Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 4), Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index: 4), @@ -72,6 +75,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 4), Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 5), Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 5), + Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 5, person_type: "child"), Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 5), Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index: 5), @@ -86,6 +90,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 5), Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 6), Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 6), + Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 6, person_type: "child"), Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 6), Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index: 6), @@ -100,6 +105,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 6), Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 7), Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 7), + Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 7, person_type: "child"), Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 7), Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index: 7), @@ -114,6 +120,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 7), Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 8), Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 8), + Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 8, person_type: "child"), Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 8), Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index: 8), diff --git a/app/models/log.rb b/app/models/log.rb index f6f5f4173..f5f5f4946 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -81,6 +81,12 @@ class Log < ApplicationRecord end end + (2..8).each do |person_num| + define_method("person_#{person_num}_child_relation?") do + send("relat#{person_num}") == "C" + end + end + private def plural_gender_for_person(person_num) diff --git a/spec/models/form/lettings/pages/person_age_spec.rb b/spec/models/form/lettings/pages/person_age_spec.rb index ca39ef2c9..3a5ee1655 100644 --- a/spec/models/form/lettings/pages/person_age_spec.rb +++ b/spec/models/form/lettings/pages/person_age_spec.rb @@ -1,11 +1,12 @@ require "rails_helper" RSpec.describe Form::Lettings::Pages::PersonAge, type: :model do - subject(:page) { described_class.new(nil, page_definition, subsection, person_index:) } + subject(:page) { described_class.new(nil, page_definition, subsection, person_index:, person_type:) } let(:page_definition) { nil } let(:subsection) { instance_double(Form::Subsection) } let(:person_index) { 2 } + let(:person_type) { "non_child" } it "has correct subsection" do expect(page.subsection).to eq(subsection) @@ -24,14 +25,30 @@ RSpec.describe Form::Lettings::Pages::PersonAge, type: :model do expect(page.questions.map(&:id)).to eq(%w[age2_known age2]) end - it "has the correct id" do - expect(page.id).to eq("person_2_age") + context "when child" do + let(:person_type) { "child" } + + it "has the correct id" do + expect(page.id).to eq("person_2_age_child") + end + + it "has correct depends_on" do + expect(page.depends_on).to eq( + [{ "details_known_2" => 0, "person_2_child_relation?" => true }], + ) + end end - it "has correct depends_on" do - expect(page.depends_on).to eq( - [{ "details_known_2" => 0 }], - ) + context "when not child" do + it "has the correct id" do + expect(page.id).to eq("person_2_age_non_child") + end + + it "has correct depends_on" do + expect(page.depends_on).to eq( + [{ "details_known_2" => 0, "person_2_child_relation?" => false }], + ) + end end end @@ -43,12 +60,12 @@ RSpec.describe Form::Lettings::Pages::PersonAge, type: :model do end it "has the correct id" do - expect(page.id).to eq("person_3_age") + expect(page.id).to eq("person_3_age_non_child") end it "has correct depends_on" do expect(page.depends_on).to eq( - [{ "details_known_3" => 0 }], + [{ "details_known_3" => 0, "person_3_child_relation?" => false }], ) end end diff --git a/spec/models/form/lettings/questions/age_spec.rb b/spec/models/form/lettings/questions/age_spec.rb index 87c3dce22..40a6356fd 100644 --- a/spec/models/form/lettings/questions/age_spec.rb +++ b/spec/models/form/lettings/questions/age_spec.rb @@ -1,11 +1,12 @@ require "rails_helper" RSpec.describe Form::Lettings::Questions::Age, type: :model do - subject(:question) { described_class.new(nil, question_definition, page, person_index:) } + subject(:question) { described_class.new(nil, question_definition, page, person_index:, person_type:) } let(:question_definition) { nil } let(:page) { instance_double(Form::Page) } let(:person_index) { 2 } + let(:person_type) { "non_child" } it "has correct page" do expect(question.page).to eq(page) @@ -23,12 +24,22 @@ RSpec.describe Form::Lettings::Questions::Age, type: :model do expect(question.derived?).to be false end - it "has the correct hint" do - expect(question.hint_text).to be_nil + context "when child" do + let(:person_type) { "child" } + + it "has the correct hint" do + expect(question.hint_text).to eq("For a child under 1, enter 1") + end + end + + context "when not child" do + it "has no hint" do + expect(question.hint_text).to be nil + end end it "has the correct min" do - expect(question.min).to eq(0) + expect(question.min).to eq(1) end it "has the correct max" do diff --git a/spec/models/form/lettings/questions/person_relationship_spec.rb b/spec/models/form/lettings/questions/person_relationship_spec.rb index 04f071b9b..dca3d82a3 100644 --- a/spec/models/form/lettings/questions/person_relationship_spec.rb +++ b/spec/models/form/lettings/questions/person_relationship_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Form::Lettings::Questions::PersonRelationship, type: :model do end it "has the correct answer_options" do - expect(question.answer_options).to eq("C" => { "hint" => "Must be eligible for child benefit, aged under 16 or under 20 if still in full-time education.", "value" => "Child" }, + expect(question.answer_options).to eq("C" => { "hint" => "Must be eligible for child benefit: under age 16 or under 20 if still in full-time education.", "value" => "Child" }, "P" => { "value" => "Partner" }, "R" => { "value" => "Person prefers not to say" }, "X" => { "value" => "Other" }, diff --git a/spec/models/form/lettings/subsections/household_characteristics_spec.rb b/spec/models/form/lettings/subsections/household_characteristics_spec.rb index 96d722a2b..d0d55b8bf 100644 --- a/spec/models/form/lettings/subsections/household_characteristics_spec.rb +++ b/spec/models/form/lettings/subsections/household_characteristics_spec.rb @@ -36,7 +36,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod lead_tenant_over_retirement_value_check person_2_known person_2_relationship_to_lead - person_2_age + person_2_age_child + person_2_age_non_child no_females_pregnant_household_person_2_age_value_check females_in_soft_age_range_in_pregnant_household_person_2_age_value_check person_2_gender_identity @@ -47,7 +48,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod person_2_over_retirement_value_check person_3_known person_3_relationship_to_lead - person_3_age + person_3_age_child + person_3_age_non_child no_females_pregnant_household_person_3_age_value_check females_in_soft_age_range_in_pregnant_household_person_3_age_value_check person_3_gender_identity @@ -58,7 +60,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod person_3_over_retirement_value_check person_4_known person_4_relationship_to_lead - person_4_age + person_4_age_child + person_4_age_non_child no_females_pregnant_household_person_4_age_value_check females_in_soft_age_range_in_pregnant_household_person_4_age_value_check person_4_gender_identity @@ -69,7 +72,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod person_4_over_retirement_value_check person_5_known person_5_relationship_to_lead - person_5_age + person_5_age_child + person_5_age_non_child no_females_pregnant_household_person_5_age_value_check females_in_soft_age_range_in_pregnant_household_person_5_age_value_check person_5_gender_identity @@ -80,7 +84,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod person_5_over_retirement_value_check person_6_known person_6_relationship_to_lead - person_6_age + person_6_age_child + person_6_age_non_child no_females_pregnant_household_person_6_age_value_check females_in_soft_age_range_in_pregnant_household_person_6_age_value_check person_6_gender_identity @@ -91,7 +96,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod person_6_over_retirement_value_check person_7_known person_7_relationship_to_lead - person_7_age + person_7_age_child + person_7_age_non_child no_females_pregnant_household_person_7_age_value_check females_in_soft_age_range_in_pregnant_household_person_7_age_value_check person_7_gender_identity @@ -102,7 +108,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod person_7_over_retirement_value_check person_8_known person_8_relationship_to_lead - person_8_age + person_8_age_child + person_8_age_non_child no_females_pregnant_household_person_8_age_value_check females_in_soft_age_range_in_pregnant_household_person_8_age_value_check person_8_gender_identity From 7716602544bf18d1f886ac78033d0f8c75bc72af Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Wed, 8 Mar 2023 13:37:52 +0000 Subject: [PATCH 19/19] CLDC-2006 Add and populate local authorities table (#1395) * Add local authorities table * Seed initial local authorities * set previous_location_only to false by default * Add unique index and rake task * Update field name and required fields * Remove previous_location_only field * tests --- app/models/local_authority.rb | 2 + .../imports/local_authorities_service.rb | 25 ++ .../initial_local_authorities.csv | 388 ++++++++++++++++++ ...20230308101826_create_local_authorities.rb | 13 + db/schema.rb | 12 +- db/seeds.rb | 6 + lib/tasks/local_authorities.rake | 13 + spec/fixtures/files/local_authorities.csv | 6 + .../tasks/local_authorities_import_spec.rb | 43 ++ 9 files changed, 507 insertions(+), 1 deletion(-) create mode 100644 app/models/local_authority.rb create mode 100644 app/services/imports/local_authorities_service.rb create mode 100644 config/local_authorities_data/initial_local_authorities.csv create mode 100644 db/migrate/20230308101826_create_local_authorities.rb create mode 100644 lib/tasks/local_authorities.rake create mode 100644 spec/fixtures/files/local_authorities.csv create mode 100644 spec/lib/tasks/local_authorities_import_spec.rb diff --git a/app/models/local_authority.rb b/app/models/local_authority.rb new file mode 100644 index 000000000..4017ada52 --- /dev/null +++ b/app/models/local_authority.rb @@ -0,0 +1,2 @@ +class LocalAuthority < ApplicationRecord +end diff --git a/app/services/imports/local_authorities_service.rb b/app/services/imports/local_authorities_service.rb new file mode 100644 index 000000000..23a4e2375 --- /dev/null +++ b/app/services/imports/local_authorities_service.rb @@ -0,0 +1,25 @@ +require "csv" + +module Imports + class LocalAuthoritiesService + attr_reader :path, :count + + def initialize(path:) + @path = path + @count = 0 + end + + def call + CSV.foreach(path, headers: true) do |row| + LocalAuthority.upsert( + { code: row["code"], + name: row["name"], + start_date: Time.zone.local(row["start_year"], 4, 1), + end_date: (Time.zone.local(row["end_year"], 3, 31) if row["end_year"]) }, + unique_by: %i[code], + ) + @count += 1 + end + end + end +end diff --git a/config/local_authorities_data/initial_local_authorities.csv b/config/local_authorities_data/initial_local_authorities.csv new file mode 100644 index 000000000..f80fbb6f5 --- /dev/null +++ b/config/local_authorities_data/initial_local_authorities.csv @@ -0,0 +1,388 @@ +code,name,start_year,end_year +S12000033,Aberdeen City,2021, +S12000034,Aberdeenshire,2021, +E07000223,Adur,2021, +E07000032,Amber Valley,2021, +S12000041,Angus,2021, +N09000001,Antrim and Newtownabbey,2021, +N09000011,Ards and North Down,2021, +S12000035,Argyll and Bute,2021, +N09000002,"Armagh City, Banbridge and Craigavon",2021, +E07000224,Arun,2021, +E07000170,Ashfield,2021, +E07000105,Ashford,2021, +E07000200,Babergh,2021, +E09000002,Barking and Dagenham,2021, +E09000003,Barnet,2021, +E08000016,Barnsley,2021, +E07000066,Basildon,2021, +E07000084,Basingstoke and Deane,2021, +E07000171,Bassetlaw,2021, +E06000022,Bath and North East Somerset,2021, +E06000055,Bedford,2021, +N09000003,Belfast,2021, +E09000004,Bexley,2021, +E08000025,Birmingham,2021, +E07000129,Blaby,2021, +E06000008,Blackburn with Darwen,2021, +E06000009,Blackpool,2021, +W06000019,Blaenau Gwent,2021, +E07000033,Bolsover,2021, +E08000001,Bolton,2021, +E07000136,Boston,2021, +E06000058,"Bournemouth, Christchurch and Poole",2021, +E06000036,Bracknell Forest,2021, +E08000032,Bradford,2021, +E07000067,Braintree,2021, +E07000143,Breckland,2021, +E09000005,Brent,2021, +E07000068,Brentwood,2021, +W06000013,Bridgend,2021, +E06000043,Brighton and Hove,2021, +E06000023,"Bristol, City of",2021, +E07000144,Broadland,2021, +E09000006,Bromley,2021, +E07000234,Bromsgrove,2021, +E07000095,Broxbourne,2021, +E07000172,Broxtowe,2021, +E06000060,Buckinghamshire,2021, +E07000117,Burnley,2021, +E08000002,Bury,2021, +W06000018,Caerphilly,2021, +E08000033,Calderdale,2021, +E07000008,Cambridge,2021, +E09000007,Camden,2021, +E07000192,Cannock Chase,2021, +E07000106,Canterbury,2021, +W06000015,Cardiff,2021, +W06000010,Carmarthenshire,2021, +E07000069,Castle Point,2021, +N09000004,Causeway Coast and Glens,2021, +E06000056,Central Bedfordshire,2021, +W06000008,Ceredigion,2021, +E07000130,Charnwood,2021, +E07000070,Chelmsford,2021, +E07000078,Cheltenham,2021, +E07000177,Cherwell,2021, +E06000049,Cheshire East,2021, +E06000050,Cheshire West and Chester,2021, +E07000034,Chesterfield,2021, +E07000225,Chichester,2021, +E07000118,Chorley,2021, +S12000036,City of Edinburgh,2021, +E09000001,City of London,2021, +S12000005,Clackmannanshire,2021, +E07000071,Colchester,2021, +W06000003,Conwy,2021, +E07000150,Corby,2021, +E06000052,Cornwall,2021, +E07000079,Cotswold,2021, +E06000047,County Durham,2021, +E08000026,Coventry,2021, +E07000226,Crawley,2021, +E09000008,Croydon,2021, +E06000063,Cumberland,2023, +E07000096,Dacorum,2021, +E06000005,Darlington,2021, +E07000107,Dartford,2021, +E07000151,Daventry,2021, +W06000004,Denbighshire,2021, +E06000015,Derby,2021, +E07000035,Derbyshire Dales,2021, +N09000005,Derry City and Strabane,2021, +E08000017,Doncaster,2021, +E06000059,Dorset,2021, +E07000108,Dover,2021, +E08000027,Dudley,2021, +S12000006,Dumfries and Galloway,2021, +S12000042,Dundee City,2021, +E09000009,Ealing,2021, +S12000008,East Ayrshire,2021, +E07000009,East Cambridgeshire,2021, +E07000040,East Devon,2021, +S12000045,East Dunbartonshire,2021, +E07000085,East Hampshire,2021, +E07000242,East Hertfordshire,2021, +E07000137,East Lindsey,2021, +S12000010,East Lothian,2021, +E07000152,East Northamptonshire,2021, +S12000011,East Renfrewshire,2021, +E06000011,East Riding of Yorkshire,2021, +E07000193,East Staffordshire,2021, +E07000244,East Suffolk,2021, +E07000061,Eastbourne,2021, +E07000086,Eastleigh,2021, +E07000207,Elmbridge,2021, +E09000010,Enfield,2021, +E07000072,Epping Forest,2021, +E07000208,Epsom and Ewell,2021, +E07000036,Erewash,2021, +E07000041,Exeter,2021, +S12000014,Falkirk,2021, +E07000087,Fareham,2021, +E07000010,Fenland,2021, +N09000006,Fermanagh and Omagh,2021, +S12000047,Fife,2021, +W06000005,Flintshire,2021, +E07000112,Folkestone and Hythe,2021, +E07000080,Forest of Dean,2021, +E07000119,Fylde,2021, +E08000037,Gateshead,2021, +E07000173,Gedling,2021, +S12000049,Glasgow City,2021, +E07000081,Gloucester,2021, +E07000088,Gosport,2021, +E07000109,Gravesham,2021, +E07000145,Great Yarmouth,2021, +E09000011,Greenwich,2021, +E07000209,Guildford,2021, +W06000002,Gwynedd,2021, +E09000012,Hackney,2021, +E06000006,Halton,2021, +E09000013,Hammersmith and Fulham,2021, +E07000131,Harborough,2021, +E09000014,Haringey,2021, +E07000073,Harlow,2021, +E09000015,Harrow,2021, +E07000089,Hart,2021, +E06000001,Hartlepool,2021, +E07000062,Hastings,2021, +E07000090,Havant,2021, +E09000016,Havering,2021, +E06000019,"Herefordshire, County of",2021, +E07000098,Hertsmere,2021, +E07000037,High Peak,2021, +S12000017,Highland,2021, +E09000017,Hillingdon,2021, +E07000132,Hinckley and Bosworth,2021, +E07000227,Horsham,2021, +E09000018,Hounslow,2021, +E07000011,Huntingdonshire,2021, +E07000120,Hyndburn,2021, +S12000018,Inverclyde,2021, +E07000202,Ipswich,2021, +W06000001,Isle of Anglesey,2021, +E06000046,Isle of Wight,2021, +E06000053,Isles of Scilly,2021, +E09000019,Islington,2021, +E09000020,Kensington and Chelsea,2021, +E07000153,Kettering,2021, +E07000146,King’s Lynn and West Norfolk,2021, +E06000010,"Kingston upon Hull, City of",2021, +E09000021,Kingston upon Thames,2021, +E08000034,Kirklees,2021, +E08000011,Knowsley,2021, +E09000022,Lambeth,2021, +E07000121,Lancaster,2021, +E08000035,Leeds,2021, +E06000016,Leicester,2021, +E07000063,Lewes,2021, +E09000023,Lewisham,2021, +E07000194,Lichfield,2021, +E07000138,Lincoln,2021, +N09000007,Lisburn and Castlereagh,2021, +E08000012,Liverpool,2021, +E06000032,Luton,2021, +E07000110,Maidstone,2021, +E07000074,Maldon,2021, +E07000235,Malvern Hills,2021, +E08000003,Manchester,2021, +E07000174,Mansfield,2021, +E06000035,Medway,2021, +E07000133,Melton,2021, +W06000024,Merthyr Tydfil,2021, +E09000024,Merton,2021, +E07000042,Mid Devon,2021, +E07000203,Mid Suffolk,2021, +E07000228,Mid Sussex,2021, +N09000009,Mid Ulster,2021, +N09000008,Mid and East Antrim,2021, +E06000002,Middlesbrough,2021, +S12000019,Midlothian,2021, +E06000042,Milton Keynes,2021, +E07000210,Mole Valley,2021, +W06000021,Monmouthshire,2021, +S12000020,Moray,2021, +S12000013,Na h-Eileanan Siar,2021, +W06000012,Neath Port Talbot,2021, +E07000091,New Forest,2021, +E07000175,Newark and Sherwood,2021, +E08000021,Newcastle upon Tyne,2021, +E07000195,Newcastle-under-Lyme,2021, +E09000025,Newham,2021, +W06000022,Newport,2021, +N09000010,"Newry, Mourne and Down",2021, +S12000021,North Ayrshire,2021, +E07000043,North Devon,2021, +E07000038,North East Derbyshire,2021, +E06000012,North East Lincolnshire,2021, +E07000099,North Hertfordshire,2021, +E07000139,North Kesteven,2021, +S12000050,North Lanarkshire,2021, +E06000013,North Lincolnshire,2021, +E07000147,North Norfolk,2021, +E06000024,North Somerset,2021, +E08000022,North Tyneside,2021, +E07000218,North Warwickshire,2021, +E07000134,North West Leicestershire,2021, +E07000154,Northampton,2021, +E06000057,Northumberland,2021, +E07000148,Norwich,2021, +E06000018,Nottingham,2021, +E07000219,Nuneaton and Bedworth,2021, +E07000135,Oadby and Wigston,2021, +E08000004,Oldham,2021, +S12000023,Orkney Islands,2021, +E07000178,Oxford,2021, +W06000009,Pembrokeshire,2021, +E07000122,Pendle,2021, +S12000048,Perth and Kinross,2021, +E06000031,Peterborough,2021, +E06000026,Plymouth,2021, +E06000044,Portsmouth,2021, +W06000023,Powys,2021, +E07000123,Preston,2021, +E06000038,Reading,2021, +E09000026,Redbridge,2021, +E06000003,Redcar and Cleveland,2021, +E07000236,Redditch,2021, +E07000211,Reigate and Banstead,2021, +S12000038,Renfrewshire,2021, +W06000016,Rhondda Cynon Taf,2021, +E07000124,Ribble Valley,2021, +E09000027,Richmond upon Thames,2021, +E08000005,Rochdale,2021, +E07000075,Rochford,2021, +E07000125,Rossendale,2021, +E07000064,Rother,2021, +E08000018,Rotherham,2021, +E07000220,Rugby,2021, +E07000212,Runnymede,2021, +E07000176,Rushcliffe,2021, +E07000092,Rushmoor,2021, +E06000017,Rutland,2021, +E08000006,Salford,2021, +E08000028,Sandwell,2021, +S12000026,Scottish Borders,2021, +E08000014,Sefton,2021, +E07000111,Sevenoaks,2021, +E08000019,Sheffield,2021, +S12000027,Shetland Islands,2021, +E06000051,Shropshire,2021, +E06000039,Slough,2021, +E08000029,Solihull,2021, +E06000066,Somerset,2023, +S12000028,South Ayrshire,2021, +E07000012,South Cambridgeshire,2021, +E07000039,South Derbyshire,2021, +E06000025,South Gloucestershire,2021, +E07000044,South Hams,2021, +E07000140,South Holland,2021, +E07000141,South Kesteven,2021, +S12000029,South Lanarkshire,2021, +E07000149,South Norfolk,2021, +E07000155,South Northamptonshire,2021, +E07000179,South Oxfordshire,2021, +E07000126,South Ribble,2021, +E07000196,South Staffordshire,2021, +E08000023,South Tyneside,2021, +E06000045,Southampton,2021, +E06000033,Southend-on-Sea,2021, +E09000028,Southwark,2021, +E07000213,Spelthorne,2021, +E07000240,St Albans,2021, +E08000013,St. Helens,2021, +E07000197,Stafford,2021, +E07000198,Staffordshire Moorlands,2021, +E07000243,Stevenage,2021, +S12000030,Stirling,2021, +E08000007,Stockport,2021, +E06000004,Stockton-on-Tees,2021, +E06000021,Stoke-on-Trent,2021, +E07000221,Stratford-on-Avon,2021, +E07000082,Stroud,2021, +E08000024,Sunderland,2021, +E07000214,Surrey Heath,2021, +E09000029,Sutton,2021, +E07000113,Swale,2021, +W06000011,Swansea,2021, +E06000030,Swindon,2021, +E08000008,Tameside,2021, +E07000199,Tamworth,2021, +E07000215,Tandridge,2021, +E07000045,Teignbridge,2021, +E06000020,Telford and Wrekin,2021, +E07000076,Tendring,2021, +E07000093,Test Valley,2021, +E07000083,Tewkesbury,2021, +E07000114,Thanet,2021, +E07000102,Three Rivers,2021, +E06000034,Thurrock,2021, +E07000115,Tonbridge and Malling,2021, +E06000027,Torbay,2021, +W06000020,Torfaen,2021, +E07000046,Torridge,2021, +E09000030,Tower Hamlets,2021, +E08000009,Trafford,2021, +E07000116,Tunbridge Wells,2021, +E07000077,Uttlesford,2021, +W06000014,Vale of Glamorgan,2021, +E07000180,Vale of White Horse,2021, +E08000036,Wakefield,2021, +E08000030,Walsall,2021, +E09000031,Waltham Forest,2021, +E09000032,Wandsworth,2021, +E06000007,Warrington,2021, +E07000222,Warwick,2021, +E07000103,Watford,2021, +E07000216,Waverley,2021, +E07000065,Wealden,2021, +E07000156,Wellingborough,2021, +E07000241,Welwyn Hatfield,2021, +E06000037,West Berkshire,2021, +E07000047,West Devon,2021, +S12000039,West Dunbartonshire,2021, +E07000127,West Lancashire,2021, +E07000142,West Lindsey,2021, +S12000040,West Lothian,2021, +E07000181,West Oxfordshire,2021, +E07000245,West Suffolk,2021, +E09000033,Westminster,2021, +E06000064,Westmorland and Furness,2023, +E08000010,Wigan,2021, +E06000054,Wiltshire,2021, +E07000094,Winchester,2021, +E06000040,Windsor and Maidenhead,2021, +E08000015,Wirral,2021, +E07000217,Woking,2021, +E06000041,Wokingham,2021, +E08000031,Wolverhampton,2021, +E07000237,Worcester,2021, +E07000229,Worthing,2021, +W06000006,Wrexham,2021, +E07000238,Wychavon,2021, +E07000128,Wyre,2021, +E07000239,Wyre Forest,2021, +E06000014,York,2021, +E06000065,North Yorkshire,2023, +N92000002,Northern Ireland,2021, +S92000003,Scotland,2021, +W92000004,Wales,2021, +9300000XX,Outside UK,2021, +E07000027,Barrow-in-Furness,2021,2023, +E07000030,Eden,2021,2023, +E07000031,South Lakeland,2021,2023, +E07000026,Allerdale,2021,2023, +E07000028,Carlisle,2021,2023, +E07000029,Copeland,2021,2023, +E07000163,Craven,2021,2023, +E07000164,Hambleton,2021,2023, +E07000165,Harrogate,2021,2023, +E07000166,Richmondshire,2021,2023, +E07000167,Ryedale,2021,2023, +E07000168,Scarborough,2021,2023, +E07000169,Selby,2021,2023, +E07000187,Mendip,2021,2023, +E07000188,Sedgemoor,2021,2023, +E07000246,Somerset West and Taunton,2021,2023, +E07000189,South Somerset,2021,2023, diff --git a/db/migrate/20230308101826_create_local_authorities.rb b/db/migrate/20230308101826_create_local_authorities.rb new file mode 100644 index 000000000..f64964455 --- /dev/null +++ b/db/migrate/20230308101826_create_local_authorities.rb @@ -0,0 +1,13 @@ +class CreateLocalAuthorities < ActiveRecord::Migration[7.0] + def change + create_table :local_authorities do |t| + t.string :code, null: false + t.string :name, null: false + t.datetime :start_date, null: false + t.datetime :end_date + t.index %w[code], name: "index_local_authority_code", unique: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c2f8f8600..eccfa98ca 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_03_01_144555) do +ActiveRecord::Schema[7.0].define(version: 2023_03_08_101826) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -289,6 +289,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_01_144555) do t.index ["updated_by_id"], name: "index_lettings_logs_on_updated_by_id" end + create_table "local_authorities", force: :cascade do |t| + t.string "code", null: false + t.string "name", null: false + t.datetime "start_date", null: false + t.datetime "end_date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["code"], name: "index_local_authority_code", unique: true + end + create_table "location_deactivation_periods", force: :cascade do |t| t.datetime "deactivation_date" t.datetime "reactivation_date" diff --git a/db/seeds.rb b/db/seeds.rb index 341677b27..e677ad50e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -325,4 +325,10 @@ unless Rails.env.test? end end end + +if LocalAuthority.count.zero? + path = "config/local_authorities_data/initial_local_authorities.csv" + service = Imports::LocalAuthoritiesService.new(path:) + service.call +end # rubocop:enable Rails/Output diff --git a/lib/tasks/local_authorities.rake b/lib/tasks/local_authorities.rake new file mode 100644 index 000000000..b812bbc94 --- /dev/null +++ b/lib/tasks/local_authorities.rake @@ -0,0 +1,13 @@ +namespace :data_import do + desc "Import local authorities data" + task :local_authorities, %i[path] => :environment do |_task, args| + path = args[:path] + + raise "Usage: rake data_import:local_authorities['path/to/csv_file']" if path.blank? + + service = Imports::LocalAuthoritiesService.new(path:) + service.call + + pp "Created/updated #{service.count} local authority records" unless Rails.env.test? + end +end diff --git a/spec/fixtures/files/local_authorities.csv b/spec/fixtures/files/local_authorities.csv new file mode 100644 index 000000000..d1cebba6b --- /dev/null +++ b/spec/fixtures/files/local_authorities.csv @@ -0,0 +1,6 @@ +code,name,start_year,end_year +S12000033,Aberdeen City,2021, +S12000034,Aberdeenshire,2021, +E07000223,Adur,2021, +E07000032,Amber Valley,2021, +S12000041,Angus,2021, \ No newline at end of file diff --git a/spec/lib/tasks/local_authorities_import_spec.rb b/spec/lib/tasks/local_authorities_import_spec.rb new file mode 100644 index 000000000..67d952767 --- /dev/null +++ b/spec/lib/tasks/local_authorities_import_spec.rb @@ -0,0 +1,43 @@ +require "rails_helper" +require "rake" + +RSpec.describe "data_import" do + describe ":local_authorities", type: :task do + subject(:task) { Rake::Task["data_import:local_authorities"] } + + before do + LocalAuthority.destroy_all + Rake.application.rake_require("tasks/local_authorities") + Rake::Task.define_task(:environment) + task.reenable + end + + context "when the rake task is run" do + let(:local_authorities_file_path) { "./spec/fixtures/files/local_authorities.csv" } + let(:wrong_file_path) { "/test/no_csv_here.csv" } + + it "creates new local authorities records" do + expect { task.invoke(local_authorities_file_path) }.to change(LocalAuthority, :count).by(5) + expect(LocalAuthority.where(code: "S12000041").exists?).to be true + end + + it "raises an error when no path is given" do + expect { task.invoke(nil) }.to raise_error(RuntimeError, "Usage: rake data_import:local_authorities['path/to/csv_file']") + end + + it "raises an error when no file exists at the given path" do + expect { task.invoke(wrong_file_path) }.to raise_error(Errno::ENOENT) + end + + context "when a record already exists with a matching code index" do + let!(:local_authority) { LocalAuthority.create(code: "S12000041", name: "Something else", start_date: Time.zone.local(2021, 4, 1)) } + + it "updates local authority if the record is matched on code" do + task.invoke(local_authorities_file_path) + local_authority.reload + expect(local_authority.name).to eq("Angus") + end + end + end + end +end